Skip to content

Commit

Permalink
CIRC-2101: Fetch item details across tenants (#1474)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
OleksandrVidinieiev authored Jun 6, 2024
1 parent ba47983 commit 3f6bd4f
Show file tree
Hide file tree
Showing 9 changed files with 121 additions and 23 deletions.
8 changes: 7 additions & 1 deletion descriptors/ModuleDescriptor-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
]
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<Result<SearchInstance>> getInstanceWithItems(List<String> queryParams) {
Expand All @@ -59,11 +66,27 @@ private CompletableFuture<Result<SearchInstance>> updateItemDetails(SearchInstan
return CompletableFuture.completedFuture(failed(new BadRequestFailure(
"Search result is empty")));
}
return AsyncCoordinationUtil.allOf(searchInstance.getItems(), this::fetchItemDetails)

Map<String, List<Item>> 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<Result<Item>> fetchItemDetails(Item searchItem) {
private CompletableFuture<Result<List<Item>>> fetchItemDetails(String tenantId, List<Item> items) {
ItemRepository itemRepository = new ItemRepository(Clients.create(webContext, httpClient, tenantId));

return AsyncCoordinationUtil.allOf(items, item -> fetchItemDetails(item, itemRepository));
}

private CompletableFuture<Result<Item>> fetchItemDetails(Item searchItem,
ItemRepository itemRepository) {

return itemRepository.fetchById(searchItem.getItemId())
.thenApply(r -> r.map(item -> item.changeTenantId(searchItem.getTenantId())));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/org/folio/circulation/support/Clients.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -184,6 +186,7 @@ public CompletableFuture<Result<Response>> delete(String url,
}

private HttpRequest<Buffer> withStandardHeaders(HttpRequest<Buffer> request) {
log.debug("withStandardHeaders:: url={}, tenantId={}", request.uri(), tenantId);
return request
.putHeader(ACCEPT, "application/json, text/plain")
.putHeader(OKAPI_URL, okapiUrl.toString())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -86,7 +90,7 @@ public OkapiHttpClient createHttpClient(HttpClient httpClient) {
}

return VertxWebClientOkapiHttpClient.createClientUsing(httpClient,
okapiUrl, getTenantId(), getOkapiToken(), getUserId(),
okapiUrl, tenantId, getOkapiToken(), getUserId(),
getRequestId());
}

Expand Down
72 changes: 61 additions & 11 deletions src/test/java/api/ItemsByInstanceResourceTest.java
Original file line number Diff line number Diff line change
@@ -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<JsonObject> 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) {
Expand Down
12 changes: 11 additions & 1 deletion src/test/java/api/support/APITestContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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";
Expand All @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion src/test/java/api/support/RestAssuredConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public static RequestSpecification standardHeaders(OkapiHeaders okapiHeaders) {
final HashMap<String, String> 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());
Expand Down

0 comments on commit 3f6bd4f

Please sign in to comment.