Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions descriptors/ModuleDescriptor-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,17 @@
"users.item.get"
]
},
{
"methods": ["PATCH"],
"pathPattern": "/inventory/items/{id}",
"permissionsRequired": ["inventory.items.item.patch.execute"],
"modulePermissions": [
"inventory-storage.items.item.get",
"inventory-storage.items.collection.get",
"inventory-storage.items.item.patch.execute",
"users.item.get"
]
},
{
"methods": ["DELETE"],
"pathPattern": "/inventory/items/{id}",
Expand Down Expand Up @@ -473,9 +484,9 @@
{
"methods": ["PATCH"],
"pathPattern": "/inventory/instances/{id}",
"permissionsRequired": ["inventory.instances.item.patch"],
"permissionsRequired": ["inventory.instances.item.patch.execute"],
"modulePermissions": [
"inventory-storage.instances.item.patch",
"inventory-storage.instances.item.patch.execute",
"inventory-storage.instances.item.get",
"inventory-storage.instances.item.post",
"inventory-storage.instances.item.delete",
Expand Down Expand Up @@ -679,7 +690,7 @@
},
{
"id": "item-storage",
"version": "11.0"
"version": "11.2"
},
{
"id": "instance-storage",
Expand Down Expand Up @@ -877,6 +888,11 @@
"displayName": "Inventory - modify item",
"description": "Modify item"
},
{
"permissionName": "inventory.items.item.patch.execute",
"displayName": "Inventory - modify item partially",
"description": "Modify item partially"
},
{
"permissionName": "inventory.items.item.delete",
"displayName": "Inventory - delete individual item",
Expand Down Expand Up @@ -923,7 +939,7 @@
"description": "Modify instance"
},
{
"permissionName": "inventory.instances.item.patch",
"permissionName": "inventory.instances.item.patch.execute",
"displayName": "Inventory - partially modify instance",
"description": "Partially modify instance"
},
Expand Down Expand Up @@ -953,6 +969,7 @@
"inventory.items.item.get",
"inventory.items.item.post",
"inventory.items.item.put",
"inventory.items.item.patch.execute",
"inventory.items.item.delete",
"inventory.items.collection.delete",
"inventory.instances.collection.get",
Expand All @@ -961,7 +978,7 @@
"inventory.instances.item.post",
"inventory.instances.batch.post",
"inventory.instances.item.put",
"inventory.instances.item.patch",
"inventory.instances.item.patch.execute",
"inventory.instances.item.delete",
"inventory.instances.collection.delete",
"inventory.config.instances.blocked-fields.get",
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,7 @@
<path>${basedir}/ramls/tenantItemPair.json</path>
<path>${basedir}/ramls/tenantItemPairCollection.json</path>
<path>${basedir}/ramls/tenantItemResponse.json</path>
<path>${basedir}/ramls/instance_patch.json</path>
<path>${basedir}/ramls/patch_request.json</path>
</sourcePaths>
<targetPackage>org.folio</targetPackage>
<generateBuilders>true</generateBuilders>
Expand Down
File renamed without changes.
35 changes: 32 additions & 3 deletions ramls/inventory.raml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ types:
instance: !include instance.json
instances: !include instances.json
tenantItemPairCollection: !include tenantItemPairCollection.json
instancePatchRequest: !include instance_patch.json
patchRequest: !include patch_request.json

traits:
language: !include raml-util/traits/language.raml
Expand Down Expand Up @@ -76,6 +76,35 @@ resourceTypes:
exampleItem: !include examples/item_get.json
schema: item
get:
patch:
description: Partial update of item with given ID
body:
application/json:
type: patchRequest
example: !include examples/patch-request.json
responses:
204:
description: "Item successfully updated"
404:
description: "Item with a given ID not found"
body:
text/plain:
example: Not found
400:
description: "Bad request, e.g. malformed request body or query parameter."
body:
text/plain:
example: "unable to update instance - malformed JSON"
409:
description: "Optimistic locking version conflict"
body:
text/plain:
example: "version conflict"
500:
description: "Internal server error, e.g. due to misconfiguration"
body:
text/plain:
example: "internal server error, contact administrator"
/mark-withdrawn:
post:
responses:
Expand Down Expand Up @@ -398,8 +427,8 @@ resourceTypes:
description: Partial update of instance with given ID
body:
application/json:
type: instancePatchRequest
example: !include examples/instance-patch.json
type: patchRequest
example: !include examples/patch-request.json
responses:
204:
description: "Instance successfully updated"
Expand Down
2 changes: 1 addition & 1 deletion ramls/instance_patch.json → ramls/patch_request.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "An instance record patch request",
"javaType": "org.folio.rest.jaxrs.model.InstancePatchRequest",
"javaType": "org.folio.rest.jaxrs.model.PatchRequest",
"type": "object",
"properties": {
"id": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@

package org.folio.inventory.domain.items;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.vertx.core.json.JsonObject;
import org.joda.time.DateTime;
import org.joda.time.format.ISODateTimeFormat;

@JsonIgnoreProperties(ignoreUnknown = true)
public class LastCheckIn {

private final DateTime dateTime;
Expand Down
109 changes: 102 additions & 7 deletions src/main/java/org/folio/inventory/resources/Items.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package org.folio.inventory.resources;

import static java.util.concurrent.CompletableFuture.completedFuture;
import static org.folio.HttpStatus.HTTP_OK;
import static org.folio.inventory.common.FutureAssistance.allOf;
import static org.folio.inventory.support.CompletableFutures.failedFuture;
import static org.folio.inventory.support.CqlHelper.multipleRecordsCqlQuery;
import static org.folio.inventory.support.EndpointFailureHandler.doExceptionally;
import static org.folio.inventory.support.EndpointFailureHandler.handleFailure;
import static org.folio.inventory.support.ItemUtil.HOLDINGS_RECORD_ID;
import static org.folio.inventory.support.http.server.JsonResponse.unprocessableEntity;
import static org.folio.inventory.validation.ItemStatusValidator.checkStatusIfPresent;
import static org.folio.inventory.validation.ItemStatusValidator.itemHasCorrectStatus;
import static org.folio.inventory.validation.ItemsValidator.barcodeChanged;
import static org.folio.inventory.validation.ItemsValidator.claimedReturnedMarkedAsMissing;
import static org.folio.inventory.validation.ItemsValidator.hridChanged;

Expand Down Expand Up @@ -99,6 +103,7 @@ public Items(final Storage storage, final HttpClient client) {
public void register(Router router) {
router.post(RELATIVE_ITEMS_PATH + "*").handler(BodyHandler.create());
router.put(RELATIVE_ITEMS_PATH + "*").handler(BodyHandler.create());
router.patch(RELATIVE_ITEMS_PATH + "*").handler(BodyHandler.create());

router.get(RELATIVE_ITEMS_PATH).handler(this::getAll);
router.post(RELATIVE_ITEMS_PATH + "/retrieve").handler(this::retrieveAllByCQLBody);
Expand All @@ -107,6 +112,7 @@ public void register(Router router) {

router.get(RELATIVE_ITEMS_PATH_ID).handler(this::getById);
router.put(RELATIVE_ITEMS_PATH_ID).handler(this::update);
router.patch(RELATIVE_ITEMS_PATH_ID).handler(this::patch);
router.delete(RELATIVE_ITEMS_PATH_ID).handler(this::deleteById);

Arrays.stream(ItemStatusName.values())
Expand Down Expand Up @@ -293,6 +299,53 @@ private void update(RoutingContext routingContext) {
}).exceptionally(doExceptionally(routingContext));
}

private void patch(RoutingContext routingContext) {
WebContext context = new WebContext(routingContext);

var patchRequest = routingContext.body().asJsonObject();

Optional<ValidationError> validationError = checkStatusIfPresent(patchRequest);
if (validationError.isPresent()) {
unprocessableEntity(routingContext.response(), validationError.get());
return;
}

ItemCollection itemCollection = storage.getItemCollection(context);
UserCollection userCollection = storage.getUserCollection(context);

final String itemId = routingContext.request().getParam("id");
final CompletableFuture<Success<Item>> getItemFuture = new CompletableFuture<>();

itemCollection.findById(itemId, getItemFuture::complete,
FailureResponseConsumer.serverError(routingContext.response()));

getItemFuture
.thenApply(Success::getResult)
.thenCompose(ItemsValidator::refuseWhenItemNotFound)
.thenCompose(oldItem ->
applyPatch(oldItem, patchRequest)
.thenCompose(patchedItem -> hridChanged(oldItem, patchedItem).thenApply(x -> patchedItem))
.thenCompose(patchedItem -> barcodeChanged(oldItem, patchedItem).thenApply(x -> patchedItem))
.thenCompose(patchedItem -> claimedReturnedMarkedAsMissing(oldItem, patchedItem))
.thenCompose(patchedItem -> {
findUserAndPatchItem(routingContext, patchRequest, oldItem, userCollection, itemCollection);
return completedFuture(null);
}
)
)
.exceptionally(doExceptionally(routingContext));
}

private CompletableFuture<Item> applyPatch(Item existingItem, JsonObject patchJson) {
try {
JsonObject mergedJson = JsonObject.mapFrom(existingItem);
mergedJson.mergeIn(patchJson, false);
return completedFuture(ItemUtil.jsonToItem(mergedJson));
} catch (Exception e) {
return failedFuture(e);
}
}

private void deleteById(RoutingContext routingContext) {
WebContext context = new WebContext(routingContext);
CollectionResourceClient itemsStorageClient;
Expand Down Expand Up @@ -842,25 +895,67 @@ private void findUserAndUpdateItem(
failure -> updateItem(routingContext, newItem, oldItem, null, itemCollection));
}

private void findUserAndPatchItem(
RoutingContext routingContext,
JsonObject patchJson,
Item oldItem,
UserCollection userCollection,
ItemCollection itemCollection) {

String userId = routingContext.request().getHeader("X-Okapi-User-Id");
userCollection.findById(userId,
success -> patchItem(routingContext, patchJson, oldItem, success.getResult(), itemCollection),
failure -> patchItem(routingContext, patchJson, oldItem, null, itemCollection));
}

private void updateItem(
RoutingContext routingContext,
Item newItem,
Item oldItem,
User user,
ItemCollection itemCollection) {

Map<String, CirculationNote> oldNotes = oldItem.getCirculationNotes()
List<CirculationNote> updatedNotes = updateCirculationNotes(oldItem.getCirculationNotes(),
newItem.getCirculationNotes(), user);

itemCollection.update(newItem.withCirculationNotes(updatedNotes),
v -> SuccessResponse.noContent(routingContext.response()),
failure -> ForwardResponse.forward(routingContext.response(), failure));
}

private void patchItem(
RoutingContext routingContext,
JsonObject patchJson,
Item oldItem,
User user,
ItemCollection itemCollection) {

if (patchJson.containsKey("circulationNotes")) {
var newItem = ItemUtil.jsonToItem(patchJson.mergeIn(JsonObject.mapFrom(oldItem), false));
List<CirculationNote> updatedNotes = updateCirculationNotes(oldItem.getCirculationNotes(),
newItem.getCirculationNotes(), user);
var notesJson = new JsonArray(
updatedNotes.stream()
.map(JsonObject::mapFrom)
.toList());
patchJson.put("circulationNotes", notesJson);
}

itemCollection.patch(patchJson.getString("id"), patchJson,
v -> SuccessResponse.noContent(routingContext.response()),
failure -> ForwardResponse.forward(routingContext.response(), failure));
}

List<CirculationNote> updateCirculationNotes(List<CirculationNote> oldNotes,
List<CirculationNote> newNotes, User user) {
Map<String, CirculationNote> oldNoteList = oldNotes
.stream()
.collect(Collectors.toMap(CirculationNote::getId, Function.identity()));

List<CirculationNote> updatedNotes = newItem.getCirculationNotes()
return newNotes
.stream()
.map(note -> updateCirculationNoteIfChanged(note, user, oldNotes))
.map(note -> updateCirculationNoteIfChanged(note, user, oldNoteList))
.collect(Collectors.toList());

itemCollection.update(newItem.withCirculationNotes(updatedNotes),
v -> SuccessResponse.noContent(routingContext.response()),
failure -> ForwardResponse.forward(routingContext.response(), failure));
}

private void checkForNonUniqueBarcode(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,10 @@ public static Optional<ValidationError> itemHasCorrectStatus(JsonObject itemRequ

return Optional.empty();
}

public static Optional<ValidationError> checkStatusIfPresent(JsonObject patchRequest) {
return patchRequest.containsKey("status")
? itemHasCorrectStatus(patchRequest)
: Optional.empty();
}
}
11 changes: 11 additions & 0 deletions src/main/java/org/folio/inventory/validation/ItemsValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ public static CompletableFuture<Item> hridChanged(Item oldItem, Item newItem) {
return completedFuture(oldItem);
}

public static CompletableFuture<Item> barcodeChanged(Item oldItem, Item newItem) {
if (!Objects.equals(newItem.getBarcode(), oldItem.getBarcode())) {
final ValidationError validationError = new ValidationError(
"Barcode can not be patched", "barcode", newItem.getBarcode());

return failedFuture(new UnprocessableEntityException(validationError));
}

return completedFuture(oldItem);
}

private static boolean isClaimedReturnedItemMarkedMissing(Item oldItem, Item newItem) {
return oldItem.getStatus().getName() == ItemStatusName.CLAIMED_RETURNED
&& newItem.getStatus().getName() == ItemStatusName.MISSING;
Expand Down
Loading