From c68142b3e013e7619c98941d3de4a3cf746a314a Mon Sep 17 00:00:00 2001 From: psmagin Date: Mon, 27 Jan 2025 12:52:26 +0200 Subject: [PATCH] feat(cn-browse): support showing instance title in browse results list Closes: MSEARCH-948 --- NEWS.md | 1 + .../model/index/InstanceSubResource.java | 4 +- ...elvingOrderBrowseServiceBySearchAfter.java | 2 +- .../browse/CallNumberBrowseService.java | 23 ++++ .../consortium/ConsortiumSearchHelper.java | 35 +++--- .../reindex/jdbc/CallNumberRepository.java | 23 ++-- ...CallNumberSearchResponsePostProcessor.java | 84 +++++++++++++ .../folio/search/utils/CallNumberUtils.java | 118 ------------------ .../resources/model/instance_call_number.json | 6 +- .../response/callNumberBrowseItem.yaml | 3 + .../BrowseCallNumberConsortiumIT.java | 41 +++--- .../search/controller/BrowseCallNumberIT.java | 97 ++++++++------ .../IndexingInstanceCallNumberIT.java | 2 +- .../ConsortiumSearchHelperTest.java | 2 +- .../reindex/jdbc/CallNumberRepositoryIT.java | 6 +- 15 files changed, 233 insertions(+), 214 deletions(-) create mode 100644 src/main/java/org/folio/search/service/setter/callnumber/CallNumberSearchResponsePostProcessor.java diff --git a/NEWS.md b/NEWS.md index d5c2668cc..ae70a5d1a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -16,6 +16,7 @@ * Call Numbers Browse: Implement Indexing and Re-indexing Mechanisms for Call-Numbers ([MSEARCH-864](https://folio-org.atlassian.net/browse/MSEARCH-864)) * Call Numbers Browse: Implement Browsing Endpoint for Call-Numbers ([MSEARCH-865](https://folio-org.atlassian.net/browse/MSEARCH-865)) * Call Numbers Browse: Support aliases for callNumberTypeId filters ([MSEARCH-942](https://folio-org.atlassian.net/browse/MSEARCH-942)) +* Call Numbers Browse: Support showing instance title in browse results list ([MSEARCH-948](https://folio-org.atlassian.net/browse/MSEARCH-948)) ### Bug fixes * Remove shelving order calculation for local call-number types diff --git a/src/main/java/org/folio/search/model/index/InstanceSubResource.java b/src/main/java/org/folio/search/model/index/InstanceSubResource.java index 44e99fe59..5cdab81e7 100644 --- a/src/main/java/org/folio/search/model/index/InstanceSubResource.java +++ b/src/main/java/org/folio/search/model/index/InstanceSubResource.java @@ -11,5 +11,7 @@ public class InstanceSubResource { private Boolean shared; private int count; private List typeId; - private List locationId; + private String locationId; + private List instanceId; + private String instanceTitle; } diff --git a/src/main/java/org/folio/search/service/browse/AbstractShelvingOrderBrowseServiceBySearchAfter.java b/src/main/java/org/folio/search/service/browse/AbstractShelvingOrderBrowseServiceBySearchAfter.java index 71f7cf7f6..57518cc5a 100644 --- a/src/main/java/org/folio/search/service/browse/AbstractShelvingOrderBrowseServiceBySearchAfter.java +++ b/src/main/java/org/folio/search/service/browse/AbstractShelvingOrderBrowseServiceBySearchAfter.java @@ -31,7 +31,7 @@ public abstract class AbstractShelvingOrderBrowseServiceBySearchAfter extends AbstractBrowseServiceBySearchAfter { - private final ConsortiumSearchHelper consortiumSearchHelper; + protected final ConsortiumSearchHelper consortiumSearchHelper; private final BrowseConfigServiceDecorator configService; protected AbstractShelvingOrderBrowseServiceBySearchAfter(ConsortiumSearchHelper consortiumSearchHelper, diff --git a/src/main/java/org/folio/search/service/browse/CallNumberBrowseService.java b/src/main/java/org/folio/search/service/browse/CallNumberBrowseService.java index 9645e37cc..e199082e3 100644 --- a/src/main/java/org/folio/search/service/browse/CallNumberBrowseService.java +++ b/src/main/java/org/folio/search/service/browse/CallNumberBrowseService.java @@ -2,12 +2,16 @@ import static org.folio.search.utils.SearchUtils.CALL_NUMBER_TYPE_ID_FIELD; +import java.util.List; +import java.util.Set; +import java.util.function.Function; import lombok.extern.log4j.Log4j2; import org.folio.search.domain.dto.BrowseType; import org.folio.search.domain.dto.CallNumberBrowseItem; import org.folio.search.model.BrowseResult; import org.folio.search.model.SearchResult; import org.folio.search.model.index.CallNumberResource; +import org.folio.search.model.index.InstanceSubResource; import org.folio.search.model.service.BrowseContext; import org.folio.search.service.consortium.BrowseConfigServiceDecorator; import org.folio.search.service.consortium.ConsortiumSearchHelper; @@ -61,8 +65,27 @@ protected BrowseResult mapToBrowseResult(BrowseContext ctx .chronology(resource.chronology()) .enumeration(resource.enumeration()) .copyNumber(resource.copyNumber()) + .instanceTitle(getInstanceTitle(ctx, resource, CallNumberResource::instances)) .isAnchor(isAnchor ? true : null) .totalRecords(getTotalRecords(ctx, resource, CallNumberResource::instances))); } + private String getInstanceTitle(BrowseContext ctx, CallNumberResource resource, + Function> func) { + var instanceSubResources = consortiumSearchHelper.filterSubResourcesForConsortium(ctx, resource, func); + if (instanceSubResources.size() == 1) { + return instanceSubResources.iterator().next().getInstanceTitle(); + } + return null; + } + + @Override + protected Integer getTotalRecords(BrowseContext ctx, CallNumberResource resource, + Function> func) { + return consortiumSearchHelper.filterSubResourcesForConsortium(ctx, resource, func) + .stream() + .map(InstanceSubResource::getInstanceId) + .map(List::size) + .reduce(0, Integer::sum); + } } diff --git a/src/main/java/org/folio/search/service/consortium/ConsortiumSearchHelper.java b/src/main/java/org/folio/search/service/consortium/ConsortiumSearchHelper.java index 54ca6b0af..0e91bd253 100644 --- a/src/main/java/org/folio/search/service/consortium/ConsortiumSearchHelper.java +++ b/src/main/java/org/folio/search/service/consortium/ConsortiumSearchHelper.java @@ -6,15 +6,15 @@ import static org.opensearch.index.query.QueryBuilders.termQuery; import java.util.LinkedList; -import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; -import java.util.stream.Stream; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.folio.search.model.index.CallNumberResource; import org.folio.search.model.index.InstanceSubResource; import org.folio.search.model.service.BrowseContext; import org.folio.search.model.types.ResourceType; @@ -34,6 +34,7 @@ public class ConsortiumSearchHelper { private static final Logger logger = LoggerFactory.getLogger(ConsortiumSearchHelper.class); private static final String BROWSE_SHARED_FILTER_KEY = "instances.shared"; private static final String BROWSE_TENANT_FILTER_KEY = "instances.tenantId"; + private static final String BROWSE_LOCATION_FILTER_KEY = "instances.locationId"; private final FolioExecutionContext folioExecutionContext; private final ConsortiumTenantService consortiumTenantService; @@ -138,22 +139,29 @@ public Set filterSubResourcesForConsortium( } return subResources.stream() .filter(subResourcesFilter) + .filter(instanceSubResource -> filterForCallNumbers(context, resource, instanceSubResource)) .collect(Collectors.toSet()); } - public static Optional getBrowseFilter(BrowseContext context, String filterKey) { + protected Optional getBrowseFilter(BrowseContext context, String filterKey) { return context.getFilters().stream() .map(filter -> getTermFilterForKey(filter, filterKey)) .filter(Objects::nonNull) .findFirst(); } - public static List getBrowseFilterValues(BrowseContext context, String filterKey) { - return context.getFilters().stream() - .flatMap(filter -> getTermFiltersForKey(filter, filterKey)) - .filter(Objects::nonNull) - .map(TermQueryBuilder::value) - .toList(); + private boolean filterForCallNumbers(BrowseContext context, T resource, InstanceSubResource instanceSubResource) { + if (resource instanceof CallNumberResource) { + var locationIds = context.getFilters().stream() + .map(filter -> getTermFilterForKey(filter, BROWSE_LOCATION_FILTER_KEY)) + .filter(Objects::nonNull) + .map(TermQueryBuilder::value) + .map(String::valueOf) + .filter(StringUtils::isNotBlank) + .toList(); + return locationIds.isEmpty() || locationIds.contains(instanceSubResource.getLocationId()); + } + return true; } private BoolQueryBuilder prepareBoolQueryForActiveAffiliation(QueryBuilder query) { @@ -224,15 +232,6 @@ private static TermQueryBuilder getTermFilterForKey(QueryBuilder filter, String : null; } - private static Stream getTermFiltersForKey(QueryBuilder filter, String filterKey) { - if (filter instanceof TermQueryBuilder termFilter && termFilter.fieldName().equals(filterKey)) { - return Stream.of(termFilter); - } else if (filter instanceof BoolQueryBuilder boolFilter) { - return boolFilter.should().stream().map(shouldFilter -> getTermFilterForKey(shouldFilter, filterKey)); - } - return null; - } - private boolean sharedFilterValue(TermQueryBuilder sharedQuery) { return sharedQuery.value() instanceof Boolean boolValue && boolValue || sharedQuery.value() instanceof String stringValue && Boolean.parseBoolean(stringValue); diff --git a/src/main/java/org/folio/search/service/reindex/jdbc/CallNumberRepository.java b/src/main/java/org/folio/search/service/reindex/jdbc/CallNumberRepository.java index c25f367e7..0141881d6 100644 --- a/src/main/java/org/folio/search/service/reindex/jdbc/CallNumberRepository.java +++ b/src/main/java/org/folio/search/service/reindex/jdbc/CallNumberRepository.java @@ -46,21 +46,21 @@ public class CallNumberRepository extends UploadRangeRepository implements Insta SELECT c.*, json_agg( json_build_object( - 'count', sub.instance_count, + 'instanceId', sub.instance_ids, 'tenantId', sub.tenant_id, 'shared', sub.shared, - 'locationId', sub.location_ids + 'locationId', sub.location_id ) ) AS instances FROM (SELECT ins.call_number_id, ins.tenant_id, i.shared, - array_agg(DISTINCT ins.location_id) FILTER (WHERE ins.location_id IS NOT NULL) AS location_ids, - count(DISTINCT ins.instance_id) AS instance_count + ins.location_id, + array_agg(DISTINCT i.id) AS instance_ids FROM %1$s.instance_call_number ins INNER JOIN %1$s.instance i ON i.id = ins.instance_id WHERE %2$s - GROUP BY ins.call_number_id, ins.tenant_id, i.shared) sub + GROUP BY ins.call_number_id, ins.tenant_id, i.shared, ins.location_id) sub JOIN %1$s.call_number c ON c.id = sub.call_number_id WHERE %3$s GROUP BY c.id; @@ -108,12 +108,12 @@ WITH cte AS ( c.last_updated_date, json_agg( CASE - WHEN sub.instance_count IS NULL THEN NULL + WHEN sub.instance_ids IS NULL THEN NULL ELSE json_build_object( - 'count', sub.instance_count, + 'instanceId', sub.instance_ids, 'tenantId', sub.tenant_id, 'shared', sub.shared, - 'locationId', sub.location_ids + 'locationId', sub.location_id ) END ) AS instances @@ -123,15 +123,16 @@ LEFT JOIN ( cte.id, ins.tenant_id, i.shared, - array_agg(DISTINCT ins.location_id) FILTER (WHERE ins.location_id IS NOT NULL) AS location_ids, - count(DISTINCT ins.instance_id) AS instance_count + ins.location_id, + array_agg(DISTINCT i.id) AS instance_ids FROM %1$s.instance_call_number ins INNER JOIN cte ON ins.call_number_id = cte.id INNER JOIN %1$s.instance i ON i.id = ins.instance_id GROUP BY cte.id, ins.tenant_id, - i.shared + i.shared, + ins.location_id ) sub ON c.id = sub.id GROUP BY c.id, diff --git a/src/main/java/org/folio/search/service/setter/callnumber/CallNumberSearchResponsePostProcessor.java b/src/main/java/org/folio/search/service/setter/callnumber/CallNumberSearchResponsePostProcessor.java new file mode 100644 index 000000000..9bf720b48 --- /dev/null +++ b/src/main/java/org/folio/search/service/setter/callnumber/CallNumberSearchResponsePostProcessor.java @@ -0,0 +1,84 @@ +package org.folio.search.service.setter.callnumber; + +import static org.folio.search.utils.LogUtils.collectionToLogMsg; +import static org.opensearch.index.query.QueryBuilders.idsQuery; + +import java.util.Collection; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.collections4.MapUtils; +import org.folio.search.model.SimpleResourceRequest; +import org.folio.search.model.index.CallNumberResource; +import org.folio.search.model.index.InstanceSubResource; +import org.folio.search.model.types.ResourceType; +import org.folio.search.repository.SearchRepository; +import org.folio.search.service.consortium.TenantProvider; +import org.folio.search.service.setter.SearchResponsePostProcessor; +import org.folio.spring.FolioExecutionContext; +import org.opensearch.search.builder.SearchSourceBuilder; +import org.springframework.stereotype.Component; + +@Log4j2 +@Component +@RequiredArgsConstructor +public final class CallNumberSearchResponsePostProcessor implements SearchResponsePostProcessor { + + private static final String INSTANCE_TITLE_FIELD = "plain_title"; + + private final SearchRepository searchRepository; + private final FolioExecutionContext context; + private final TenantProvider tenantProvider; + + @Override + public Class getGeneric() { + return CallNumberResource.class; + } + + @Override + public void process(List res) { + log.debug("process:: by [res: {}]", collectionToLogMsg(res, true)); + + if (res == null || res.isEmpty()) { + return; + } + var subResources = res.stream() + .flatMap(resource -> resource.instances().stream()) + .toList(); + + countAndSetNumberOfLinkedInstances(subResources); + } + + private void countAndSetNumberOfLinkedInstances(List authorities) { + var ids = authorities.stream() + .map(InstanceSubResource::getInstanceId) + .filter(instanceIds -> instanceIds.size() == 1) + .flatMap(Collection::stream) + .distinct() + .toList(); + var queries = buildQuery(ids); + + var resourceRequest = SimpleResourceRequest.of(ResourceType.INSTANCE, + tenantProvider.getTenant(context.getTenantId())); + var searchHits = searchRepository.search(resourceRequest, queries).getHits().getHits(); + + for (var searchHit : searchHits) { + var instanceId = searchHit.getId(); + var instanceTitle = MapUtils.getString(searchHit.getSourceAsMap(), INSTANCE_TITLE_FIELD); + for (var authority : authorities) { + if (authority.getInstanceId().size() == 1 && authority.getInstanceId().get(0).equals(instanceId)) { + authority.setInstanceTitle(instanceTitle); + } + } + } + } + + private SearchSourceBuilder buildQuery(List instanceIds) { + var boolQueryBuilder = idsQuery().addIds(instanceIds.toArray(String[]::new)); + + return new SearchSourceBuilder() + .query(boolQueryBuilder) + .fetchSource(INSTANCE_TITLE_FIELD, null) + .trackTotalHits(true); + } +} diff --git a/src/main/java/org/folio/search/utils/CallNumberUtils.java b/src/main/java/org/folio/search/utils/CallNumberUtils.java index 2e46f1ca2..8e4d779b4 100644 --- a/src/main/java/org/folio/search/utils/CallNumberUtils.java +++ b/src/main/java/org/folio/search/utils/CallNumberUtils.java @@ -5,30 +5,21 @@ import static java.util.stream.Collectors.joining; import static org.apache.commons.lang3.StringUtils.isNotBlank; -import jakarta.validation.Valid; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; import lombok.experimental.UtilityClass; import org.apache.commons.lang3.RegExUtils; import org.apache.commons.lang3.StringUtils; -import org.folio.search.domain.dto.Instance; import org.folio.search.domain.dto.Item; -import org.folio.search.domain.dto.LegacyCallNumberBrowseItem; -import org.folio.search.model.service.BrowseContext; -import org.folio.search.model.types.CallNumberType; -import org.folio.search.service.consortium.ConsortiumSearchHelper; -import org.jetbrains.annotations.NotNull; import org.marc4j.callnum.CallNumber; import org.marc4j.callnum.DeweyCallNumber; import org.marc4j.callnum.LCCallNumber; import org.marc4j.callnum.NlmCallNumber; -import org.opensearch.index.query.TermQueryBuilder; import org.springframework.util.Assert; @UtilityClass @@ -38,8 +29,6 @@ public class CallNumberUtils { private static final char ASCII_SPACE = ' '; private static final int MAX_SUPPORTED_CHARACTERS = 52; private static final String SUPPORTED_CHARACTERS_STRING = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ .,:;=-+~_/\\#@?!"; - private static final String BROWSE_TENANT_FILTER_KEY = "holdings.tenantId"; - private static final String BROWSE_LOCATION_FILTER_KEY = "items.effectiveLocationId"; private static final Map VALID_CHARACTERS_MAP = getValidCharactersMap(); private static final Pattern NORMALIZE_REGEX = Pattern.compile("[^a-z0-9]"); @@ -196,119 +185,12 @@ public static Long getCallNumberAsLong(String callNumber, int firstPosition) { return callNumberToLong(callNumber, startVal, CN_MAX_CHARS - 1); } - /** - * Excludes irrelevant items from result. - * - *

- * This algorithm takes call number browse result and call number type and removes irrelevant items from result - * approach: - *

    - *
  • Each call number browse item has its own fullCallNumber field, which may vary from its instance's item or - * holding call number. Matching ones filtered
  • - *
  • CallNumberBrowseItem's instance may have items or holdings - * which may have call numbers with different type. They will also be filtered by callNumberType
  • - *
  • all filtered records are added together and returned
  • - *
- *

- * - * @param context - call number browse context - * @param callNumberTypeValue - call number type to check/compare result items' types - * @param browseItems - list of CallNumberBrowseItem objects - * @return filtered records - */ - public static List excludeIrrelevantResultItems(BrowseContext context, - String callNumberTypeValue, - Set folioCallNumberTypes, - List browseItems) { - var callNumberType = Optional.ofNullable(StringUtils.trimToNull(callNumberTypeValue)); - var tenantFilter = ConsortiumSearchHelper.getBrowseFilter(context, BROWSE_TENANT_FILTER_KEY); - var locationFilter = ConsortiumSearchHelper.getBrowseFilterValues(context, BROWSE_LOCATION_FILTER_KEY); - if (browseItems == null || browseItems.isEmpty() - || callNumberType.isEmpty() && tenantFilter.isEmpty() && locationFilter.isEmpty()) { - return browseItems; - } - - browseItems.forEach(r -> { - if (r.getInstance() != null) { - r.getInstance().setItems( - getItemsFiltered(tenantFilter, locationFilter, callNumberType, folioCallNumberTypes, r)); - } - }); - return browseItems.stream() - .filter(CallNumberUtils::isItemRelevant) - .toList(); - } - private static Optional getValidShelfKey(CallNumber value) { return Optional.of(value) .filter(CallNumber::isValid) .map(CallNumber::getShelfKey); } - @NotNull - private static List<@Valid Item> getItemsFiltered(Optional tenantFilter, - List locationFilter, - Optional callNumberType, - Set folioCallNumberTypes, - LegacyCallNumberBrowseItem item) { - return item.getInstance().getItems() - .stream() - .filter(i -> tenantIdMatch(tenantFilter, i) && callNumberTypeMatch(callNumberType, folioCallNumberTypes, i) - && locationMatch(locationFilter, i)) - .toList(); - } - - private static boolean isItemRelevant(LegacyCallNumberBrowseItem r) { - Instance instance = r.getInstance(); - if (instance != null) { - return instance.getItems() - .stream() - .anyMatch(i -> { - String fullCallNumber = getFullCallNumber(i); - return r.getFullCallNumber() == null - || fullCallNumber != null && fullCallNumber.equals(r.getFullCallNumber()); - }); - } - return true; - } - - private static boolean callNumberTypeMatch(Optional callNumberType, Set folioCallNumberTypes, - Item item) { - if (callNumberType.isEmpty()) { - return true; - } - - if (item.getEffectiveCallNumberComponents() == null) { - return false; - } - - var itemCallNumberTypeId = item.getEffectiveCallNumberComponents().getTypeId(); - var itemCallNumberType = CallNumberType.fromId(itemCallNumberTypeId); - var requestCallNumberType = CallNumberType.fromName(callNumberType.get()); - return itemCallNumberType.equals(requestCallNumberType) - || itemCallNumberTypeId != null - && itemCallNumberType.isEmpty() - && requestCallNumberType.map(cnt -> cnt.equals(CallNumberType.LOCAL)).orElse(false) - && !folioCallNumberTypes.contains(itemCallNumberTypeId); - } - - private static boolean tenantIdMatch(Optional tenantFilter, Item item) { - return tenantFilter.isEmpty() || tenantFilter.get().value().equals(item.getTenantId()); - } - - private static boolean locationMatch(List locationFilter, Item item) { - return locationFilter.isEmpty() || locationFilter.stream() - .anyMatch(location -> location.equals(item.getEffectiveLocationId())); - } - - private static String getFullCallNumber(Item item) { - return Optional.ofNullable(item.getEffectiveCallNumberComponents()) - .map(iecnc -> Stream.of(iecnc.getPrefix(), iecnc.getCallNumber(), iecnc.getSuffix()) - .filter(StringUtils::isNotBlank) - .collect(Collectors.joining(StringUtils.SPACE))) - .orElse(null); - } - private static long callNumberToLong(String callNumber, long startVal, int maxChars) { var cleanCallNumber = cleanCallNumber(callNumber, maxChars); if (StringUtils.isBlank(cleanCallNumber)) { diff --git a/src/main/resources/model/instance_call_number.json b/src/main/resources/model/instance_call_number.json index 4539e86fd..8ea738c5c 100644 --- a/src/main/resources/model/instance_call_number.json +++ b/src/main/resources/model/instance_call_number.json @@ -53,12 +53,12 @@ "searchTypes": [ "facet", "filter" ], "default": false }, + "instanceId": { + "index": "source" + }, "locationId": { "index": "keyword", "searchTypes": [ "facet", "filter" ] - }, - "count": { - "index": "source" } } } diff --git a/src/main/resources/swagger.api/schemas/response/callNumberBrowseItem.yaml b/src/main/resources/swagger.api/schemas/response/callNumberBrowseItem.yaml index 295ee0fc0..0556b0f20 100644 --- a/src/main/resources/swagger.api/schemas/response/callNumberBrowseItem.yaml +++ b/src/main/resources/swagger.api/schemas/response/callNumberBrowseItem.yaml @@ -28,6 +28,9 @@ properties: copyNumber: type: string description: Copy number + instanceTitle: + type: string + description: Instance title isAnchor: type: boolean description: Marks if current value is anchor or not diff --git a/src/test/java/org/folio/search/controller/BrowseCallNumberConsortiumIT.java b/src/test/java/org/folio/search/controller/BrowseCallNumberConsortiumIT.java index 37ecf58dc..76a93eab4 100644 --- a/src/test/java/org/folio/search/controller/BrowseCallNumberConsortiumIT.java +++ b/src/test/java/org/folio/search/controller/BrowseCallNumberConsortiumIT.java @@ -36,6 +36,7 @@ import org.folio.search.domain.dto.CallNumberBrowseResult; import org.folio.search.domain.dto.Facet; import org.folio.search.domain.dto.FacetResult; +import org.folio.search.domain.dto.Instance; import org.folio.search.domain.dto.ShelvingOrderAlgorithmType; import org.folio.search.model.index.CallNumberResource; import org.folio.search.model.types.ResourceType; @@ -53,19 +54,19 @@ @IntegrationTest class BrowseCallNumberConsortiumIT extends BaseConsortiumIntegrationTest { - public static final String LOCATION_FACET = "instances.locationId"; - public static final String TENANT_FACET = "instances.tenantId"; + private static final String LOCATION_FACET = "instances.locationId"; + private static final String TENANT_FACET = "instances.tenantId"; + private static final List INSTANCES = CallNumberTestData.instances(); @BeforeAll static void prepare() { - var allInstances = CallNumberTestData.instances(); setUpTenant(CENTRAL_TENANT_ID); setUpTenant(MEMBER_TENANT_ID); - var centralInstances = allInstances.subList(0, 30); + var centralInstances = INSTANCES.subList(0, 30); saveRecords(CENTRAL_TENANT_ID, instanceSearchPath(), centralInstances, centralInstances.size(), instance -> inventoryApi.createInstance(CENTRAL_TENANT_ID, instance)); - var memberInstances = allInstances.subList(30, allInstances.size()); - saveRecords(MEMBER_TENANT_ID, instanceSearchPath(), memberInstances, allInstances.size(), + var memberInstances = INSTANCES.subList(30, INSTANCES.size()); + saveRecords(MEMBER_TENANT_ID, instanceSearchPath(), memberInstances, INSTANCES.size(), instance -> inventoryApi.createInstance(MEMBER_TENANT_ID, instance)); await().atMost(ONE_MINUTE).pollInterval(ONE_SECOND).untilAsserted(() -> { @@ -122,20 +123,20 @@ private static Stream callNumberBrowsingDataProvider() { return Stream.of( arguments(aroundQuery, CENTRAL_TENANT_ID, BrowseOptionType.ALL, callNumbers.get(1).fullCallNumber(), 5, cnBrowseResult(callNumbers.get(35).fullCallNumber(), callNumbers.get(43).fullCallNumber(), 60, List.of( - cnBrowseItem(callNumbers.get(35), 1), - cnBrowseItem(callNumbers.get(25), 1), - cnBrowseItem(callNumbers.get(1), 3, true), - cnBrowseItem(callNumbers.get(33), 1), - cnBrowseItem(callNumbers.get(43), 1) + cnBrowseItem(callNumbers.get(35), 18, 1), + cnBrowseItem(callNumbers.get(25), 15, 1), + cnBrowseItem(callNumbers.get(1), 0, 3, true), + cnBrowseItem(callNumbers.get(33), 17, 1), + cnBrowseItem(callNumbers.get(43), 23, 1) ))), arguments(aroundQuery, MEMBER_TENANT_ID, BrowseOptionType.ALL, callNumbers.get(1).fullCallNumber(), 5, cnBrowseResult(callNumbers.get(91).fullCallNumber(), callNumbers.get(68).fullCallNumber(), 100, List.of( - cnBrowseItem(callNumbers.get(91), 1), - cnBrowseItem(callNumbers.get(25), 1), - cnBrowseItem(callNumbers.get(1), 3, true), - cnBrowseItem(callNumbers.get(70), 1), - cnBrowseItem(callNumbers.get(68), 1) + cnBrowseItem(callNumbers.get(91), 45, 1), + cnBrowseItem(callNumbers.get(25), 15, 1), + cnBrowseItem(callNumbers.get(1), 0, 3, true), + cnBrowseItem(callNumbers.get(70), 34, 1), + cnBrowseItem(callNumbers.get(68), 34, 1) ))) ); } @@ -149,11 +150,12 @@ private static CallNumberBrowseItem cnEmptyBrowseItem(String callNumber) { return new CallNumberBrowseItem().fullCallNumber(callNumber).isAnchor(true).totalRecords(0); } - private static CallNumberBrowseItem cnBrowseItem(CallNumberResource resource, int count) { - return cnBrowseItem(resource, count, null); + private static CallNumberBrowseItem cnBrowseItem(CallNumberResource resource, int instanceIndex, int count) { + return cnBrowseItem(resource, instanceIndex, count, null); } - private static CallNumberBrowseItem cnBrowseItem(CallNumberResource resource, int count, Boolean isAnchor) { + private static CallNumberBrowseItem cnBrowseItem(CallNumberResource resource, int instanceIndex, int count, + Boolean isAnchor) { return new CallNumberBrowseItem() .fullCallNumber(resource.fullCallNumber()) .callNumber(resource.callNumber()) @@ -164,6 +166,7 @@ private static CallNumberBrowseItem cnBrowseItem(CallNumberResource resource, in .chronology(resource.chronology()) .enumeration(resource.enumeration()) .copyNumber(resource.copyNumber()) + .instanceTitle(count == 1 ? INSTANCES.get(instanceIndex).getTitle() : null) .totalRecords(count) .isAnchor(isAnchor); } diff --git a/src/test/java/org/folio/search/controller/BrowseCallNumberIT.java b/src/test/java/org/folio/search/controller/BrowseCallNumberIT.java index b8af1ed78..9cc28a612 100644 --- a/src/test/java/org/folio/search/controller/BrowseCallNumberIT.java +++ b/src/test/java/org/folio/search/controller/BrowseCallNumberIT.java @@ -1,5 +1,6 @@ package org.folio.search.controller; +import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.function.Function.identity; import static org.assertj.core.api.Assertions.assertThat; @@ -8,6 +9,7 @@ import static org.awaitility.Durations.TWO_SECONDS; import static org.folio.search.support.base.ApiEndpoints.browseConfigPath; import static org.folio.search.support.base.ApiEndpoints.instanceCallNumberBrowsePath; +import static org.folio.search.support.base.ApiEndpoints.instanceSearchPath; import static org.folio.search.support.base.ApiEndpoints.recordFacetsPath; import static org.folio.search.utils.CallNumberTestData.CallNumberTypeId.LC; import static org.folio.search.utils.CallNumberTestData.callNumbers; @@ -38,7 +40,9 @@ import org.folio.search.domain.dto.RecordType; import org.folio.search.domain.dto.ShelvingOrderAlgorithmType; import org.folio.search.model.index.CallNumberResource; +import org.folio.search.model.types.ReindexEntityType; import org.folio.search.model.types.ResourceType; +import org.folio.search.service.reindex.jdbc.SubResourcesLockRepository; import org.folio.search.support.base.BaseIntegrationTest; import org.folio.search.utils.CallNumberTestData; import org.folio.spring.testing.type.IntegrationTest; @@ -49,13 +53,26 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; @IntegrationTest class BrowseCallNumberIT extends BaseIntegrationTest { + private static final List INSTANCES = CallNumberTestData.instances(); + @BeforeAll - static void prepare() { - setUpTenant(CallNumberTestData.instances().toArray(Instance[]::new)); + static void prepare(@Autowired SubResourcesLockRepository subResourcesLockRepository) { + setUpTenant(TENANT_ID); + + var timestamp = subResourcesLockRepository.lockSubResource(ReindexEntityType.CALL_NUMBER, TENANT_ID); + if (timestamp.isEmpty()) { + throw new IllegalStateException("Unexpected state of database: unable to lock call-number resource"); + } + var instances = BrowseCallNumberIT.INSTANCES.toArray(Instance[]::new); + saveRecords(TENANT_ID, instanceSearchPath(), asList(instances), instances.length, emptyList(), + instance -> inventoryApi.createInstance(TENANT_ID, instance)); + subResourcesLockRepository.unlockSubResource(ReindexEntityType.CALL_NUMBER, timestamp.get(), TENANT_ID); + await().atMost(TWO_MINUTES).pollInterval(TWO_SECONDS).untilAsserted(() -> { var counted = countIndexDocument(ResourceType.INSTANCE_CALL_NUMBER, TENANT_ID); assertThat(counted).isEqualTo(100); @@ -127,73 +144,73 @@ private static Stream callNumberBrowsingDataProvider() { // anchor call number appears in the middle of the result set arguments(1, aroundQuery, BrowseOptionType.ALL, callNumbers.get(1).fullCallNumber(), 5, cnBrowseResult(callNumbers.get(91).fullCallNumber(), callNumbers.get(68).fullCallNumber(), 100, List.of( - cnBrowseItem(callNumbers.get(91), 1), - cnBrowseItem(callNumbers.get(25), 1), - cnBrowseItem(callNumbers.get(1), 3, true), - cnBrowseItem(callNumbers.get(70), 1), - cnBrowseItem(callNumbers.get(68), 1) + cnBrowseItem(callNumbers.get(91), 1, INSTANCES.get(45).getTitle()), + cnBrowseItem(callNumbers.get(25), 1, INSTANCES.get(15).getTitle()), + cnBrowseItem(callNumbers.get(1), 3, null, true), + cnBrowseItem(callNumbers.get(70), 1, INSTANCES.get(34).getTitle()), + cnBrowseItem(callNumbers.get(68), 1, INSTANCES.get(34).getTitle()) ))), // not existed anchor call number appears in the middle of the result set arguments(2, aroundQuery, BrowseOptionType.ALL, "TA357 .A78 2011", 5, cnBrowseResult(callNumbers.get(25).fullCallNumber(), callNumbers.get(68).fullCallNumber(), 100, List.of( - cnBrowseItem(callNumbers.get(25), 1), - cnBrowseItem(callNumbers.get(1), 3), + cnBrowseItem(callNumbers.get(25), 1, INSTANCES.get(15).getTitle()), + cnBrowseItem(callNumbers.get(1), 3, null), cnEmptyBrowseItem("TA357 .A78 2011"), - cnBrowseItem(callNumbers.get(70), 1), - cnBrowseItem(callNumbers.get(68), 1) + cnBrowseItem(callNumbers.get(70), 1, INSTANCES.get(34).getTitle()), + cnBrowseItem(callNumbers.get(68), 1, INSTANCES.get(34).getTitle()) ))), // anchor call number appears first in the result set arguments(3, aroundQuery, BrowseOptionType.ALL, callNumbers.get(50).fullCallNumber(), 5, cnBrowseResult(null, callNumbers.get(95).fullCallNumber(), 100, List.of( - cnBrowseItem(callNumbers.get(50), 1, true), - cnBrowseItem(callNumbers.get(97), 1), - cnBrowseItem(callNumbers.get(95), 1) + cnBrowseItem(callNumbers.get(50), 1, INSTANCES.get(26).getTitle(), true), + cnBrowseItem(callNumbers.get(97), 1, INSTANCES.get(48).getTitle()), + cnBrowseItem(callNumbers.get(95), 1, INSTANCES.get(47).getTitle()) ))), // not existed anchor call number appears first in the result set arguments(4, aroundQuery, BrowseOptionType.ALL, "0.0", 5, cnBrowseResult(null, callNumbers.get(97).fullCallNumber(), 100, List.of( cnEmptyBrowseItem("0.0"), - cnBrowseItem(callNumbers.get(50), 1), - cnBrowseItem(callNumbers.get(97), 1) + cnBrowseItem(callNumbers.get(50), 1, INSTANCES.get(26).getTitle()), + cnBrowseItem(callNumbers.get(97), 1, INSTANCES.get(48).getTitle()) ))), // anchor call number appears last in the result set arguments(5, aroundQuery, BrowseOptionType.ALL, callNumbers.get(11).fullCallNumber(), 5, cnBrowseResult(callNumbers.get(49).fullCallNumber(), null, 100, List.of( - cnBrowseItem(callNumbers.get(49), 1), - cnBrowseItem(callNumbers.get(44), 1), - cnBrowseItem(callNumbers.get(11), 1, true) + cnBrowseItem(callNumbers.get(49), 1, INSTANCES.get(26).getTitle()), + cnBrowseItem(callNumbers.get(44), 1, INSTANCES.get(24).getTitle()), + cnBrowseItem(callNumbers.get(11), 1, INSTANCES.get(5).getTitle(), true) ))), // not existed anchor call number appears last in the result set arguments(6, aroundQuery, BrowseOptionType.ALL, "ZZ", 5, cnBrowseResult(callNumbers.get(44).fullCallNumber(), null, 100, List.of( - cnBrowseItem(callNumbers.get(44), 1), - cnBrowseItem(callNumbers.get(11), 1), + cnBrowseItem(callNumbers.get(44), 1, INSTANCES.get(24).getTitle()), + cnBrowseItem(callNumbers.get(11), 1, INSTANCES.get(5).getTitle()), cnEmptyBrowseItem("ZZ") ))), // anchor call number appears in the middle of the result set when filtering by type arguments(7, aroundQuery, BrowseOptionType.LC, callNumbers.get(46).fullCallNumber(), 5, cnBrowseResult(callNumbers.get(66).fullCallNumber(), callNumbers.get(21).fullCallNumber(), 20, List.of( - cnBrowseItem(callNumbers.get(66), 1), - cnBrowseItem(callNumbers.get(96), 1), - cnBrowseItem(callNumbers.get(46), 1, true), - cnBrowseItem(callNumbers.get(86), 1), - cnBrowseItem(callNumbers.get(21), 2) + cnBrowseItem(callNumbers.get(66), 1, INSTANCES.get(32).getTitle()), + cnBrowseItem(callNumbers.get(96), 1, INSTANCES.get(47).getTitle()), + cnBrowseItem(callNumbers.get(46), 1, INSTANCES.get(25).getTitle(), true), + cnBrowseItem(callNumbers.get(86), 1, INSTANCES.get(42).getTitle()), + cnBrowseItem(callNumbers.get(21), 2, null) ))), // forward browsing from the middle of the result set arguments(8, forwardQuery, BrowseOptionType.ALL, callNumbers.get(22).fullCallNumber(), 5, cnBrowseResult(callNumbers.get(47).fullCallNumber(), callNumbers.get(32).fullCallNumber(), 100, List.of( - cnBrowseItem(callNumbers.get(47), 1), - cnBrowseItem(callNumbers.get(62), 1), - cnBrowseItem(callNumbers.get(67), 1), - cnBrowseItem(callNumbers.get(55), 1), - cnBrowseItem(callNumbers.get(32), 1) + cnBrowseItem(callNumbers.get(47), 1, INSTANCES.get(26).getTitle()), + cnBrowseItem(callNumbers.get(62), 1, INSTANCES.get(30).getTitle()), + cnBrowseItem(callNumbers.get(67), 1, INSTANCES.get(33).getTitle()), + cnBrowseItem(callNumbers.get(55), 1, INSTANCES.get(28).getTitle()), + cnBrowseItem(callNumbers.get(32), 1, INSTANCES.get(16).getTitle()) ))), // forward browsing from the end of the result set @@ -203,11 +220,11 @@ private static Stream callNumberBrowsingDataProvider() { // backward browsing from the middle of the result set arguments(10, backwardQuery, BrowseOptionType.ALL, callNumbers.get(22).fullCallNumber(), 5, cnBrowseResult(callNumbers.get(92).fullCallNumber(), callNumbers.get(90).fullCallNumber(), 100, List.of( - cnBrowseItem(callNumbers.get(92), 1), - cnBrowseItem(callNumbers.get(17), 1), - cnBrowseItem(callNumbers.get(27), 1), - cnBrowseItem(callNumbers.get(42), 1), - cnBrowseItem(callNumbers.get(90), 1) + cnBrowseItem(callNumbers.get(92), 1, INSTANCES.get(45).getTitle()), + cnBrowseItem(callNumbers.get(17), 1, INSTANCES.get(10).getTitle()), + cnBrowseItem(callNumbers.get(27), 1, INSTANCES.get(15).getTitle()), + cnBrowseItem(callNumbers.get(42), 1, INSTANCES.get(22).getTitle()), + cnBrowseItem(callNumbers.get(90), 1, INSTANCES.get(44).getTitle()) ))), // backward browsing from the end of the result set @@ -236,11 +253,12 @@ private static CallNumberBrowseItem cnEmptyBrowseItem(String callNumber) { return new CallNumberBrowseItem().fullCallNumber(callNumber).isAnchor(true).totalRecords(0); } - private static CallNumberBrowseItem cnBrowseItem(CallNumberResource resource, int count) { - return cnBrowseItem(resource, count, null); + private static CallNumberBrowseItem cnBrowseItem(CallNumberResource resource, int count, String instanceTitle) { + return cnBrowseItem(resource, count, instanceTitle, null); } - private static CallNumberBrowseItem cnBrowseItem(CallNumberResource resource, int count, Boolean isAnchor) { + private static CallNumberBrowseItem cnBrowseItem(CallNumberResource resource, int count, + String instanceTitle, Boolean isAnchor) { return new CallNumberBrowseItem() .fullCallNumber(resource.fullCallNumber()) .callNumber(resource.callNumber()) @@ -251,6 +269,7 @@ private static CallNumberBrowseItem cnBrowseItem(CallNumberResource resource, in .chronology(resource.chronology()) .enumeration(resource.enumeration()) .copyNumber(resource.copyNumber()) + .instanceTitle(instanceTitle) .totalRecords(count) .isAnchor(isAnchor); } diff --git a/src/test/java/org/folio/search/controller/IndexingInstanceCallNumberIT.java b/src/test/java/org/folio/search/controller/IndexingInstanceCallNumberIT.java index 198e64cb3..fb86dc067 100644 --- a/src/test/java/org/folio/search/controller/IndexingInstanceCallNumberIT.java +++ b/src/test/java/org/folio/search/controller/IndexingInstanceCallNumberIT.java @@ -82,7 +82,7 @@ void shouldIndexInstanceCallNumber_createNewDocument_onItemCreate() { .hasSize(1) .allSatisfy(map -> assertThat(map).containsEntry("shared", false)) .allSatisfy(map -> assertThat(map).containsEntry("tenantId", TENANT_ID)) - .allSatisfy(map -> assertThat(map).containsEntry("count", 2)); + .allSatisfy(map -> assertThat(map).containsEntry("instanceId", List.of(INSTANCE_ID_1, INSTANCE_ID_2))); } @Test diff --git a/src/test/java/org/folio/search/service/consortium/ConsortiumSearchHelperTest.java b/src/test/java/org/folio/search/service/consortium/ConsortiumSearchHelperTest.java index 0fb0f2609..d7fede1e1 100644 --- a/src/test/java/org/folio/search/service/consortium/ConsortiumSearchHelperTest.java +++ b/src/test/java/org/folio/search/service/consortium/ConsortiumSearchHelperTest.java @@ -369,7 +369,7 @@ void getBrowseFilter_positive() { var browseContext = browseContext(null, null); browseContext.getFilters().add(expected); - var actual = ConsortiumSearchHelper.getBrowseFilter(browseContext, filterKey); + var actual = consortiumSearchHelper.getBrowseFilter(browseContext, filterKey); assertThat(actual).contains(expected); } diff --git a/src/test/java/org/folio/search/service/reindex/jdbc/CallNumberRepositoryIT.java b/src/test/java/org/folio/search/service/reindex/jdbc/CallNumberRepositoryIT.java index f677ca33e..ccd20c00d 100644 --- a/src/test/java/org/folio/search/service/reindex/jdbc/CallNumberRepositoryIT.java +++ b/src/test/java/org/folio/search/service/reindex/jdbc/CallNumberRepositoryIT.java @@ -103,9 +103,11 @@ void saveAll() { .extracting("callNumber", "instances") .contains( tuple("number1", - List.of(mapOf("count", 1, "locationId", null, "shared", false, "tenantId", TENANT_ID, "typeId", null))), + List.of(mapOf("count", 0, "instanceId", List.of("9f8febd1-e96c-46c4-a5f4-84a45cc499a2"), + "instanceTitle", null, "locationId", null, "shared", false, "tenantId", TENANT_ID, "typeId", null))), tuple("number2", - List.of(mapOf("count", 1, "locationId", null, "shared", false, "tenantId", TENANT_ID, "typeId", null)))); + List.of(mapOf("count", 0, "instanceId", List.of("9f8febd1-e96c-46c4-a5f4-84a45cc499a2"), + "instanceTitle", null, "locationId", null, "shared", false, "tenantId", TENANT_ID, "typeId", null)))); } private static List getList(int size) {