From 3f6bd4f9dace149f962a02e412b38c5f905d5cc8 Mon Sep 17 00:00:00 2001 From: OleksandrVidinieiev <56632770+OleksandrVidinieiev@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:20:04 +0300 Subject: [PATCH] CIRC-2101: Fetch item details across tenants (#1474) * CIRC-2101 Fetch items across tenants * CIRC-2101 Fetch items across tenants * CIRC-2101 Remove @Setter from static field * CIRC-2101 Fix formatting * CIRC-2101 Fetch items across tenants * CIRC-2101 Remove @Setter from static field * CIRC-2101 Fix formatting * CIRC-2101 Add missing permission set * CIRC-2101 Reuse item repository for items in same tenant --- descriptors/ModuleDescriptor-template.json | 8 ++- .../storage/SearchRepository.java | 33 +++++++-- .../resources/ItemsByInstanceResource.java | 4 +- .../folio/circulation/support/Clients.java | 4 ++ .../client/VertxWebClientOkapiHttpClient.java | 3 + .../support/http/server/WebContext.java | 6 +- .../java/api/ItemsByInstanceResourceTest.java | 72 ++++++++++++++++--- src/test/java/api/support/APITestContext.java | 12 +++- .../api/support/RestAssuredConfiguration.java | 2 +- 9 files changed, 121 insertions(+), 23 deletions(-) diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 7a749525a6..dee5864e0e 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -1536,6 +1536,11 @@ "displayName": "circulation settings - Read configuration", "description": "To read the configuration from mod settings." }, + { + "permissionName": "circulation.items-by-instance.get", + "displayName": "circulation - get items by instance", + "description": "get items by instance" + }, { "permissionName": "circulation.all", "displayName": "circulation - all permissions", @@ -1577,7 +1582,8 @@ "circulation.requests.allowed-service-points.get", "circulation.inventory.items-in-transit-report.get", "circulation.pick-slips.get", - "circulation.search-slips.get" + "circulation.search-slips.get", + "circulation.items-by-instance.get" ] }, { diff --git a/src/main/java/org/folio/circulation/infrastructure/storage/SearchRepository.java b/src/main/java/org/folio/circulation/infrastructure/storage/SearchRepository.java index bb8b3635a9..a999c2158d 100644 --- a/src/main/java/org/folio/circulation/infrastructure/storage/SearchRepository.java +++ b/src/main/java/org/folio/circulation/infrastructure/storage/SearchRepository.java @@ -6,9 +6,11 @@ import static org.folio.circulation.support.results.ResultBinding.mapResult; import java.lang.invoke.MethodHandles; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -21,19 +23,24 @@ import org.folio.circulation.support.Clients; import org.folio.circulation.support.CollectionResourceClient; import org.folio.circulation.support.http.client.Response; +import org.folio.circulation.support.http.server.WebContext; import org.folio.circulation.support.results.Result; +import io.vertx.core.http.HttpClient; import lombok.AllArgsConstructor; @AllArgsConstructor public class SearchRepository { private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); - private final ItemRepository itemRepository; + private final WebContext webContext; + private final HttpClient httpClient; private final CollectionResourceClient searchClient; - public SearchRepository(Clients clients) { - this(new ItemRepository(clients), clients.searchClient()); + public SearchRepository(WebContext webContext, HttpClient httpClient) { + this.webContext = webContext; + this.httpClient = httpClient; + this.searchClient = Clients.create(webContext, httpClient).searchClient(); } public CompletableFuture> getInstanceWithItems(List queryParams) { @@ -59,11 +66,27 @@ private CompletableFuture> updateItemDetails(SearchInstan return CompletableFuture.completedFuture(failed(new BadRequestFailure( "Search result is empty"))); } - return AsyncCoordinationUtil.allOf(searchInstance.getItems(), this::fetchItemDetails) + + Map> itemsByTenant = searchInstance.getItems() + .stream() + .collect(Collectors.groupingBy(Item::getTenantId)); + + log.info("updateItemDetails:: fetching item details from tenants: {}", itemsByTenant::keySet); + + return AsyncCoordinationUtil.allOf(itemsByTenant, this::fetchItemDetails) + .thenApply(r -> r.map(lists -> lists.stream().flatMap(Collection::stream).toList())) .thenApply(r -> r.map(searchInstance::changeItems)); } - private CompletableFuture> fetchItemDetails(Item searchItem) { + private CompletableFuture>> fetchItemDetails(String tenantId, List items) { + ItemRepository itemRepository = new ItemRepository(Clients.create(webContext, httpClient, tenantId)); + + return AsyncCoordinationUtil.allOf(items, item -> fetchItemDetails(item, itemRepository)); + } + + private CompletableFuture> fetchItemDetails(Item searchItem, + ItemRepository itemRepository) { + return itemRepository.fetchById(searchItem.getItemId()) .thenApply(r -> r.map(item -> item.changeTenantId(searchItem.getTenantId()))); } diff --git a/src/main/java/org/folio/circulation/resources/ItemsByInstanceResource.java b/src/main/java/org/folio/circulation/resources/ItemsByInstanceResource.java index 882ebaf29c..381808e951 100644 --- a/src/main/java/org/folio/circulation/resources/ItemsByInstanceResource.java +++ b/src/main/java/org/folio/circulation/resources/ItemsByInstanceResource.java @@ -6,7 +6,6 @@ import org.apache.logging.log4j.Logger; import org.folio.circulation.domain.SearchInstance; import org.folio.circulation.infrastructure.storage.SearchRepository; -import org.folio.circulation.support.Clients; import org.folio.circulation.support.http.server.JsonHttpResponse; import org.folio.circulation.support.http.server.WebContext; @@ -31,8 +30,7 @@ public void register(Router router) { private void getInstanceItems(RoutingContext routingContext) { final WebContext context = new WebContext(routingContext); - final Clients clients = Clients.create(context, client); - new SearchRepository(clients).getInstanceWithItems(routingContext.queryParam("query")) + new SearchRepository(context, client).getInstanceWithItems(routingContext.queryParam("query")) .thenApply(r -> r.map(this::toJson)) .thenApply(r -> r.map(JsonHttpResponse::ok)) .thenAccept(context::writeResultToHttpResponse); diff --git a/src/main/java/org/folio/circulation/support/Clients.java b/src/main/java/org/folio/circulation/support/Clients.java index bf7dc48056..a035bd904e 100644 --- a/src/main/java/org/folio/circulation/support/Clients.java +++ b/src/main/java/org/folio/circulation/support/Clients.java @@ -75,6 +75,10 @@ public static Clients create(WebContext context, HttpClient httpClient) { return new Clients(context.createHttpClient(httpClient), context); } + public static Clients create(WebContext context, HttpClient httpClient, String tenantId) { + return new Clients(context.createHttpClient(httpClient, tenantId), context); + } + private Clients(OkapiHttpClient client, WebContext context) { try { requestsStorageClient = createRequestsStorageClient(client, context); diff --git a/src/main/java/org/folio/circulation/support/http/client/VertxWebClientOkapiHttpClient.java b/src/main/java/org/folio/circulation/support/http/client/VertxWebClientOkapiHttpClient.java index e4815a30bf..a203f4386f 100644 --- a/src/main/java/org/folio/circulation/support/http/client/VertxWebClientOkapiHttpClient.java +++ b/src/main/java/org/folio/circulation/support/http/client/VertxWebClientOkapiHttpClient.java @@ -27,7 +27,9 @@ import io.vertx.ext.web.client.HttpResponse; import io.vertx.ext.web.client.WebClient; import io.vertx.core.http.HttpMethod; +import lombok.extern.log4j.Log4j2; +@Log4j2 public class VertxWebClientOkapiHttpClient implements OkapiHttpClient { private static final Duration DEFAULT_TIMEOUT = Duration.of(20, SECONDS); private static final String ACCEPT = HttpHeaderNames.ACCEPT.toString(); @@ -184,6 +186,7 @@ public CompletableFuture> delete(String url, } private HttpRequest withStandardHeaders(HttpRequest request) { + log.debug("withStandardHeaders:: url={}, tenantId={}", request.uri(), tenantId); return request .putHeader(ACCEPT, "application/json, text/plain") .putHeader(OKAPI_URL, okapiUrl.toString()) diff --git a/src/main/java/org/folio/circulation/support/http/server/WebContext.java b/src/main/java/org/folio/circulation/support/http/server/WebContext.java index a38173b438..df6ec49778 100644 --- a/src/main/java/org/folio/circulation/support/http/server/WebContext.java +++ b/src/main/java/org/folio/circulation/support/http/server/WebContext.java @@ -76,6 +76,10 @@ public URL getOkapiBasedUrl(String path) throws MalformedURLException { } public OkapiHttpClient createHttpClient(HttpClient httpClient) { + return createHttpClient(httpClient, getTenantId()); + } + + public OkapiHttpClient createHttpClient(HttpClient httpClient, String tenantId) { URL okapiUrl; try { @@ -86,7 +90,7 @@ public OkapiHttpClient createHttpClient(HttpClient httpClient) { } return VertxWebClientOkapiHttpClient.createClientUsing(httpClient, - okapiUrl, getTenantId(), getOkapiToken(), getUserId(), + okapiUrl, tenantId, getOkapiToken(), getUserId(), getRequestId()); } diff --git a/src/test/java/api/ItemsByInstanceResourceTest.java b/src/test/java/api/ItemsByInstanceResourceTest.java index b644e8fb95..71d3ce18b6 100644 --- a/src/test/java/api/ItemsByInstanceResourceTest.java +++ b/src/test/java/api/ItemsByInstanceResourceTest.java @@ -1,30 +1,80 @@ package api; +import static api.support.APITestContext.clearTempTenantId; +import static api.support.APITestContext.setTempTenantId; import static api.support.http.InterfaceUrls.itemsByInstanceUrl; +import static api.support.matchers.JsonObjectMatcher.hasJsonPath; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.iterableWithSize; import static org.hamcrest.core.Is.is; +import java.util.List; import java.util.UUID; import org.folio.circulation.support.http.client.Response; import org.junit.jupiter.api.Test; import api.support.APITests; -import api.support.http.ItemResource; +import api.support.builders.SearchInstanceBuilder; +import api.support.http.IndividualResource; +import api.support.http.ResourceClient; +import api.support.matchers.UUIDMatcher; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; class ItemsByInstanceResourceTest extends APITests { + + private static final String TENANT_ID_COLLEGE = "college"; + private static final String TENANT_ID_UNIVERSITY = "university"; + @Test void canGetInstanceById() { - UUID instanceId = UUID.randomUUID(); - ItemResource itemResource = itemsFixture.basedUponDunkirk(); - searchFixture.basedUponDunkirk(instanceId, itemResource); - Response response = - get(String.format("query=(id==%s)", instanceId), 200); - assertThat(response.getStatusCode(), is(200)); - assertThat(response.getJson().getString("id"), is(instanceId.toString())); - assertThat(response.getJson().getJsonArray("items").size(), is(1)); - assertThat(response.getJson().getJsonArray("items").getJsonObject(0).getString("id"), - is(itemResource.getId().toString())); + IndividualResource instance = instancesFixture.basedUponDunkirk(); + UUID instanceId = instance.getId(); + + // create item in tenant "college" + setTempTenantId(TENANT_ID_COLLEGE); + IndividualResource collegeLocation = locationsFixture.mainFloor(); + IndividualResource collegeHoldings = holdingsFixture.defaultWithHoldings(instanceId); + IndividualResource collegeItem = itemsFixture.createItemWithHoldingsAndLocation( + collegeHoldings.getId(), collegeLocation.getId()); + clearTempTenantId(); + + // create item in tenant "university" + setTempTenantId(TENANT_ID_UNIVERSITY); + IndividualResource universityLocation = locationsFixture.thirdFloor(); + IndividualResource universityHoldings = holdingsFixture.defaultWithHoldings(instanceId); + IndividualResource universityItem = itemsFixture.createItemWithHoldingsAndLocation( + universityHoldings.getId(), universityLocation.getId()); + clearTempTenantId(); + + // make sure neither item exists in current tenant + assertThat(itemsFixture.getById(collegeItem.getId()).getResponse().getStatusCode(), is(404)); + assertThat(itemsFixture.getById(universityItem.getId()).getResponse().getStatusCode(), is(404)); + + List searchItems = List.of( + collegeItem.getJson().put("tenantId", TENANT_ID_COLLEGE), + universityItem.getJson().put("tenantId", TENANT_ID_UNIVERSITY)); + + JsonObject searchInstance = new SearchInstanceBuilder(instance.getJson()) + .withItems(searchItems) + .create(); + + ResourceClient.forSearchClient().create(searchInstance); + Response response = get(String.format("query=(id==%s)", instanceId), 200); + JsonObject responseJson = response.getJson(); + JsonArray items = responseJson.getJsonArray("items"); + + assertThat(responseJson.getString("id"), UUIDMatcher.is(instanceId)); + assertThat(items, iterableWithSize(2)); + assertThat(items, hasItem(allOf( + hasJsonPath("id", UUIDMatcher.is(collegeItem.getId())), + hasJsonPath("tenantId", is(TENANT_ID_COLLEGE))))); + assertThat(items, hasItem(allOf( + hasJsonPath("id", UUIDMatcher.is(universityItem.getId())), + hasJsonPath("tenantId", is(TENANT_ID_UNIVERSITY))))); } private Response get(String query, int expectedStatusCode) { diff --git a/src/test/java/api/support/APITestContext.java b/src/test/java/api/support/APITestContext.java index 167f11e91e..0f7890eb34 100644 --- a/src/test/java/api/support/APITestContext.java +++ b/src/test/java/api/support/APITestContext.java @@ -9,6 +9,7 @@ import java.net.URL; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; +import java.util.Optional; import java.util.Properties; import java.util.Random; import java.util.concurrent.CompletableFuture; @@ -40,6 +41,7 @@ public class APITestContext { private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); public static final String TENANT_ID = "test_tenant"; + public static String tempTenantId; private static String USER_ID = "79ff2a8b-d9c3-5b39-ad4a-0a84025ab085"; private static final String TOKEN = "eyJhbGciOiJIUzUxMiJ9eyJzdWIiOiJhZG1pbiIsInVzZXJfaWQiOiI3OWZmMmE4Yi1kOWMzLTViMzktYWQ0YS0wYTg0MDI1YWIwODUiLCJ0ZW5hbnQiOiJ0ZXN0X3RlbmFudCJ9BShwfHcNClt5ZXJ8ImQTMQtAM1sQEnhsfWNmXGsYVDpuaDN3RVQ9"; @@ -66,7 +68,15 @@ static String getToken() { } public static String getTenantId() { - return TENANT_ID; + return Optional.ofNullable(tempTenantId).orElse(TENANT_ID); + } + + public static void setTempTenantId(String tenantId) { + tempTenantId = tenantId; + } + + public static void clearTempTenantId() { + setTempTenantId(null); } public static String getUserId() { diff --git a/src/test/java/api/support/RestAssuredConfiguration.java b/src/test/java/api/support/RestAssuredConfiguration.java index bae905f073..e14bf8db7f 100644 --- a/src/test/java/api/support/RestAssuredConfiguration.java +++ b/src/test/java/api/support/RestAssuredConfiguration.java @@ -27,7 +27,7 @@ public static RequestSpecification standardHeaders(OkapiHeaders okapiHeaders) { final HashMap headers = new HashMap<>(); headers.put(OKAPI_URL, okapiHeaders.getUrl().toString()); - headers.put(TENANT, okapiHeaders.getTenantId()); + headers.put(TENANT, APITestContext.getTenantId()); headers.put(TOKEN, okapiHeaders.getToken()); headers.put(REQUEST_ID, okapiHeaders.getRequestId()); headers.put(OKAPI_PERMISSIONS, okapiHeaders.getOkapiPermissions());