Skip to content

Commit

Permalink
Merge branch 'master' into MODINVSTOR-1205
Browse files Browse the repository at this point in the history
  • Loading branch information
dmytrokrutii authored Aug 29, 2024
2 parents 92a62a7 + e450f3f commit b28b3b0
Show file tree
Hide file tree
Showing 12 changed files with 354 additions and 1 deletion.
14 changes: 14 additions & 0 deletions descriptors/ModuleDescriptor-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,14 @@
"inventory-storage.instances.item.get"
]
},
{
"methods": ["POST"],
"pathPattern": "/inventory/tenant-items",
"permissionsRequired": ["inventory.tenant-items.collection.get"],
"modulePermissions": [
"inventory-storage.items.collection.get"
]
},
{
"methods": ["GET"],
"pathPattern": "/inventory/instances/{id}",
Expand Down Expand Up @@ -681,6 +689,11 @@
"displayName": "Inventory - get item collection",
"description": "Get item collection"
},
{
"permissionName": "inventory.tenant-items.collection.get",
"displayName": "Inventory - get item collection from multiple tenants",
"description": "Get item collection from multiple tenants"
},
{
"permissionName": "inventory.items.collection.delete",
"displayName": "Inventory - delete entire item collection",
Expand Down Expand Up @@ -828,6 +841,7 @@
"description": "Entire set of permissions needed to use the inventory",
"subPermissions": [
"inventory.items.collection.get",
"inventory.tenant-items.collection.get",
"inventory.items.item.get",
"inventory.items.item.post",
"inventory.items.item.put",
Expand Down
3 changes: 3 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,9 @@
<path>${basedir}/ramls/items_update_ownership.json</path>
<path>${basedir}/ramls/update_ownership_response.json</path>
<path>${basedir}/ramls/instance-ingress-event.json</path>
<path>${basedir}/ramls/tenantItemPair.json</path>
<path>${basedir}/ramls/tenantItemPairCollection.json</path>
<path>${basedir}/ramls/tenantItemResponse.json</path>
</sourcePaths>
<targetPackage>org.folio</targetPackage>
<generateBuilders>true</generateBuilders>
Expand Down
13 changes: 13 additions & 0 deletions ramls/inventory.raml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ types:
holdings: !include holdings-record.json
instance: !include instance.json
instances: !include instances.json
tenantItemPairCollection: !include tenantItemPairCollection.json

traits:
language: !include raml-util/traits/language.raml
Expand Down Expand Up @@ -294,6 +295,18 @@ resourceTypes:
Possible values of the 'relations' parameter are: 'onlyBoundWiths', 'onlyBoundWithsSkipDirectlyLinkedItem'",
example: "holdingsRecordId==\"[UUID]\""}
]
/tenant-items:
displayName: Fetch items based on tenant IDs
post:
body:
application/json:
type: tenantItemPairCollection
responses:
200:
description: "Fetched items based on tenant IDs"
body:
application/json:
type: items
/holdings/{holdingsId}:
put:
description: Update Holdings by holdingsId
Expand Down
18 changes: 18 additions & 0 deletions ramls/tenantItemPair.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Pair of item and tenant IDs",
"type": "object",
"properties": {
"tenantId": {
"type": "string",
"description": "Unique ID of the tenant where the item is located"
},
"itemId": {
"type": "string",
"description": "Unique ID (UUID) of the item",
"$ref": "uuid.json"
}
},
"additionalProperties": false,
"required": ["itemId", "tenantId"]
}
17 changes: 17 additions & 0 deletions ramls/tenantItemPairCollection.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Collection of pairs of item and tenant IDs",
"type": "object",
"properties": {
"tenantItemPairs": {
"type": "array",
"description": "Pairs of tenantId and itemId",
"items": {
"type": "object",
"$ref": "tenantItemPair.json"
}
}
},
"additionalProperties": false,
"required": ["tenantItemPairs"]
}
20 changes: 20 additions & 0 deletions ramls/tenantItemResponse.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Collection of pairs of item and tenant IDs",
"type": "object",
"properties": {
"tenantItems": {
"type": "array",
"description": "Items with corresponding tenantIds",
"items": {
"type": "object",
"additionalProperties": true
}
},
"totalRecords": {
"type": "integer"
}
},
"additionalProperties": false,
"required": ["item", "totalRecords"]
}
2 changes: 2 additions & 0 deletions src/main/java/org/folio/inventory/InventoryVerticle.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.folio.inventory.resources.ItemsByHoldingsRecordId;
import org.folio.inventory.resources.MoveApi;
import org.folio.inventory.resources.TenantApi;
import org.folio.inventory.resources.TenantItems;
import org.folio.inventory.resources.UpdateOwnershipApi;
import org.folio.inventory.storage.Storage;

Expand Down Expand Up @@ -71,6 +72,7 @@ public void start(Promise<Void> started) {
new InventoryConfigApi().register(router);
new TenantApi().register(router);
new UpdateOwnershipApi(storage, client, consortiumService).register(router);
new TenantItems(client).register(router);

Handler<AsyncResult<HttpServer>> onHttpServerStart = result -> {
if (result.succeeded()) {
Expand Down
140 changes: 140 additions & 0 deletions src/main/java/org/folio/inventory/resources/TenantItems.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package org.folio.inventory.resources;

import static java.lang.String.format;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;
import static org.folio.inventory.support.CqlHelper.multipleRecordsCqlQuery;

import java.lang.invoke.MethodHandles;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;

import org.apache.http.HttpStatus;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.folio.TenantItem;
import org.folio.TenantItemPair;
import org.folio.TenantItemPairCollection;
import org.folio.TenantItemResponse;
import org.folio.inventory.common.WebContext;
import org.folio.inventory.storage.external.CollectionResourceClient;
import org.folio.inventory.support.JsonArrayHelper;
import org.folio.inventory.support.http.client.OkapiHttpClient;
import org.folio.inventory.support.http.client.Response;
import org.folio.inventory.support.http.server.JsonResponse;
import org.folio.inventory.support.http.server.ServerErrorResponse;

import io.vertx.core.http.HttpClient;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.handler.BodyHandler;

/**
* Resource that allows to get Inventory items from multiple tenants at once.
* User should have an affiliation in order to be able to retrieve items from the corresponding tenant.
*/
public class TenantItems {

private static final Logger LOG = LogManager.getLogger(MethodHandles.lookup().lookupClass());

private static final String TENANT_ITEMS_PATH = "/inventory/tenant-items";
public static final String ITEMS_FIELD = "items";
public static final String ITEM_FIELD = "item";
public static final String TOTAL_RECORDS_FIELD = "totalRecords";
public static final String TENANT_ID_FIELD = "tenantId";

private final HttpClient client;

public TenantItems(HttpClient client) {
this.client = client;
}

public void register(Router router) {
router.post(TENANT_ITEMS_PATH + "*").handler(BodyHandler.create());
router.post(TENANT_ITEMS_PATH).handler(this::getItemsFromTenants);
}

/**
* This API is meant to be used by UI to fetch different items from several
* tenants together within one call
*
*/
private void getItemsFromTenants(RoutingContext routingContext) {
var getItemsFutures = routingContext.body().asPojo(TenantItemPairCollection.class)
.getTenantItemPairs().stream()
.collect(groupingBy(TenantItemPair::getTenantId, mapping(TenantItemPair::getItemId, toList())))
.entrySet().stream()
.map(tenantToItems -> getItemsWithTenantId(tenantToItems.getKey(), tenantToItems.getValue(), routingContext))
.toList();

CompletableFuture.allOf(getItemsFutures.toArray(new CompletableFuture[0]))
.thenApply(v -> getItemsFutures.stream()
.map(CompletableFuture::join)
.flatMap(List::stream)
.toList())
.thenApply(items -> new TenantItemResponse().withTenantItems(items).withTotalRecords(items.size()))
.thenAccept(response -> JsonResponse.success(routingContext.response(), JsonObject.mapFrom(response)));
}

private CompletableFuture<List<TenantItem>> getItemsWithTenantId(String tenantId, List<String> itemIds, RoutingContext routingContext) {
LOG.info("getItemsWithTenantId:: Fetching items - {} from tenant - {}", itemIds, tenantId);
var context = new WebContext(routingContext);
CollectionResourceClient itemsStorageClient;
try {
OkapiHttpClient okapiClient = createHttpClient(tenantId, context, routingContext);
itemsStorageClient = createItemsStorageClient(okapiClient, context);
}
catch (MalformedURLException e) {
invalidOkapiUrlResponse(routingContext, context);
return CompletableFuture.completedFuture(List.of());
}

var getByIdsQuery = multipleRecordsCqlQuery(itemIds);
var itemsFetched = new CompletableFuture<Response>();
itemsStorageClient.getAll(getByIdsQuery, itemsFetched::complete);

return itemsFetched
.thenApply(this::getItems)
.thenApply(items -> items.stream()
.map(item -> new TenantItem()
.withAdditionalProperty(ITEM_FIELD, item)
.withAdditionalProperty(TENANT_ID_FIELD, tenantId))
.toList());
}

private List<JsonObject> getItems(Response response) {
if (response.getStatusCode() != HttpStatus.SC_OK || !response.hasBody()) {
return List.of();
}
return JsonArrayHelper.toList(response.getJson(), ITEMS_FIELD);
}

private CollectionResourceClient createItemsStorageClient(OkapiHttpClient client, WebContext context) throws MalformedURLException {
return new CollectionResourceClient(client, new URL(context.getOkapiLocation() + "/item-storage/items"));
}

private OkapiHttpClient createHttpClient(String tenantId, WebContext context,
RoutingContext routingContext) throws MalformedURLException {
return new OkapiHttpClient(WebClient.wrap(client),
URI.create(context.getOkapiLocation()).toURL(),
Optional.ofNullable(tenantId).orElse(context.getTenantId()),
context.getToken(),
context.getUserId(),
context.getRequestId(),
exception -> ServerErrorResponse.internalError(routingContext.response(),
format("Failed to contact storage module: %s", exception.toString())));
}

private void invalidOkapiUrlResponse(RoutingContext routingContext, WebContext context) {
ServerErrorResponse.internalError(routingContext.response(),
String.format("Invalid Okapi URL: %s", context.getOkapiLocation()));
}

}
4 changes: 3 additions & 1 deletion src/test/java/api/ApiTestSuite.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import api.items.MarkItemUnavailableApiTests;
import api.items.MarkItemUnknownApiTests;
import api.items.MarkItemWithdrawnApiTests;
import api.items.TenantItemApiTests;
import api.support.ControlledVocabularyPreparation;
import api.support.http.ResourceClient;
import io.vertx.core.json.JsonArray;
Expand Down Expand Up @@ -72,7 +73,8 @@
AdminApiTest.class,
InventoryConfigApiTest.class,
HoldingsUpdateOwnershipApiTest.class,
ItemUpdateOwnershipApiTest.class
ItemUpdateOwnershipApiTest.class,
TenantItemApiTests.class
})
public class ApiTestSuite {
public static final int INVENTORY_VERTICLE_TEST_PORT = 9603;
Expand Down
Loading

0 comments on commit b28b3b0

Please sign in to comment.