diff --git a/NEWS.md b/NEWS.md index 73470dcc1..983e35791 100644 --- a/NEWS.md +++ b/NEWS.md @@ -16,6 +16,7 @@ * Prepare and populate index for classification browse ([MSEARCH-668](https://issues.folio.org/browse/MSEARCH-668)) * Instance search: Add query search option that search instances by normalized LCCN ([MSEARCH-661](https://issues.folio.org/browse/MSEARCH-661)) * Implement browse config management endpoints ([MSEARCH-674](https://issues.folio.org/browse/MSEARCH-674)) +* Implement endpoint to browse by classifications ([MSEARCH-665](https://issues.folio.org/browse/MSEARCH-665)) * Authority search: Modify query search option to search authorities by normalized LCCN ([MSEARCH-663](https://issues.folio.org/browse/MSEARCH-663)) ### Bug fixes diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 42bc1876b..64978bb0f 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -183,7 +183,7 @@ }, { "id": "browse", - "version": "1.2", + "version": "1.3", "handlers": [ { "methods": [ @@ -197,6 +197,18 @@ "user-tenants.collection.get" ] }, + { + "methods": [ + "GET" + ], + "pathPattern": "/browse/classification-numbers/{browseOptionId}/instances", + "permissionsRequired": [ + "browse.classification-numbers.instances.collection.get" + ], + "modulePermissions": [ + "user-tenants.collection.get" + ] + }, { "methods": [ "GET" @@ -469,6 +481,11 @@ "displayName": "Browse - provides collections of browse items for instance by call number", "description": "Browse instances by given query" }, + { + "permissionName": "browse.classification-numbers.instances.collection.get", + "displayName": "Browse - provides collections of browse by classification number", + "description": "Browse by classification number" + }, { "permissionName": "browse.subjects.instances.collection.get", "displayName": "Browse - provides collections of browse items for instance by subjects", diff --git a/docker/.env b/docker/.env index d8111b7bb..82039bd40 100644 --- a/docker/.env +++ b/docker/.env @@ -2,3 +2,6 @@ COMPOSE_PROJECT_NAME=folio-mod-search DEBUG_PORT=5010 OKAPI_URL=http://api-mock:8080 APP_PORT=8081 +PGADMIN_DEFAULT_EMAIL=user@domain.com +PGADMIN_DEFAULT_PASSWORD=admin +PGADMIN_PORT=5050 \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 08a6429a4..d18d4aec1 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -23,10 +23,14 @@ services: JAVA_OPTIONS: -Xmx120m -Xms120m JAVA_TOOL_OPTIONS: -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:${DEBUG_PORT} okapi.url: ${OKAPI_URL} + KAFKA_CONTRIBUTORS_TOPIC_PARTITIONS: 1 + KAFKA_SUBJECTS_TOPIC_PARTITIONS: 1 + KAFKA_CONSORTIUM_INSTANCE_TOPIC_PARTITIONS: 1 + BROWSE_CLASSIFICATIONS_ENABLED: true api-mock: + container_name: api-mock-mod-search image: wiremock/wiremock:2.32.0 - container_name: api-mock networks: - mod-search-local command: @@ -37,7 +41,7 @@ services: - ../src/test/resources/mappings:/home/wiremock/mappings opensearch: - container_name: opensearch + container_name: opensearch-mod-search image: dev.folio/opensearch:1.3.2 build: context: opensearch @@ -56,7 +60,7 @@ services: - "ES_JAVA_OPTS=-Xms512m -Xmx512m" opensearch-dashboards: - container_name: opensearch-dashboards + container_name: opensearch-dashboards-mod-search image: dev.folio/opensearch-dashboards:1.3.2 build: context: dashboards @@ -71,7 +75,7 @@ services: - opensearch zookeeper: - container_name: zookeeper + container_name: zookeeper-mod-search image: wurstmeister/zookeeper:3.4.6 networks: - mod-search-local @@ -79,7 +83,7 @@ services: - "2181:2181" kafka: - container_name: kafka + container_name: kafka-mod-search image: wurstmeister/kafka:2.13-2.8.1 networks: - mod-search-local @@ -99,7 +103,7 @@ services: KAFKA_AUTO_CREATE_TOPICS_ENABLE: "false" kafka-ui: - container_name: kafka-ui-quick-marc + container_name: kafka-ui-mod-search image: provectuslabs/kafka-ui:latest ports: - "8080:8080" @@ -115,7 +119,7 @@ services: - mod-search-local postgres: - container_name: postgres + container_name: postgres-mod-search image: postgres:12-alpine networks: - mod-search-local @@ -126,6 +130,20 @@ services: POSTGRES_PASSWORD: postgres POSTGRES_DB: okapi_modules + pgadmin: + container_name: pgadmin-mod-search + image: dpage/pgadmin4:6.7 + networks: + - mod-search-local + ports: + - ${PGADMIN_PORT}:80 + volumes: + - "pgadmin-data:/var/lib/pgadmin" + environment: + PGADMIN_CONFIG_SERVER_MODE: "False" + PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL} + PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD} + networks: mod-search-local: driver: bridge @@ -133,3 +151,4 @@ networks: volumes: es-data: { } db-data: { } + pgadmin-data: { } diff --git a/pom.xml b/pom.xml index 0ac8fcad4..02e386c60 100644 --- a/pom.xml +++ b/pom.xml @@ -37,8 +37,10 @@ 8.0.0 3.1.0 1.6.0 - 35.1.2 + + 35.2.0 2.12.0 + 1.5.5.Final diff --git a/src/main/java/org/folio/search/configuration/properties/SearchConfigurationProperties.java b/src/main/java/org/folio/search/configuration/properties/SearchConfigurationProperties.java index 6b06996c3..046127711 100644 --- a/src/main/java/org/folio/search/configuration/properties/SearchConfigurationProperties.java +++ b/src/main/java/org/folio/search/configuration/properties/SearchConfigurationProperties.java @@ -11,7 +11,6 @@ import java.util.Set; import lombok.Data; import org.folio.search.domain.dto.TenantConfiguredFeature; -import org.folio.search.model.types.ClassificationType; import org.folio.search.model.types.IndexingDataFormat; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; @@ -47,8 +46,6 @@ public class SearchConfigurationProperties { */ private Map searchFeatures = emptyMap(); - private Map browseClassificationTypes = emptyMap(); - /** * Indexing settings for different resources. */ diff --git a/src/main/java/org/folio/search/controller/BrowseController.java b/src/main/java/org/folio/search/controller/BrowseController.java index 5e0ad0661..44796ccd5 100644 --- a/src/main/java/org/folio/search/controller/BrowseController.java +++ b/src/main/java/org/folio/search/controller/BrowseController.java @@ -4,8 +4,10 @@ import static org.folio.search.utils.SearchUtils.AUTHORITY_BROWSING_FIELD; import static org.folio.search.utils.SearchUtils.AUTHORITY_RESOURCE; import static org.folio.search.utils.SearchUtils.CALL_NUMBER_BROWSING_FIELD; +import static org.folio.search.utils.SearchUtils.CLASSIFICATION_NUMBER_BROWSING_FIELD; import static org.folio.search.utils.SearchUtils.CONTRIBUTOR_BROWSING_FIELD; import static org.folio.search.utils.SearchUtils.CONTRIBUTOR_RESOURCE; +import static org.folio.search.utils.SearchUtils.INSTANCE_CLASSIFICATION_RESOURCE; import static org.folio.search.utils.SearchUtils.INSTANCE_RESOURCE; import static org.folio.search.utils.SearchUtils.INSTANCE_SUBJECT_RESOURCE; import static org.folio.search.utils.SearchUtils.SHELVING_ORDER_BROWSING_FIELD; @@ -14,16 +16,21 @@ import lombok.RequiredArgsConstructor; import org.folio.search.domain.dto.AuthorityBrowseResult; +import org.folio.search.domain.dto.BrowseOptionType; import org.folio.search.domain.dto.CallNumberBrowseResult; import org.folio.search.domain.dto.CallNumberType; +import org.folio.search.domain.dto.ClassificationNumberBrowseItem; +import org.folio.search.domain.dto.ClassificationNumberBrowseResult; import org.folio.search.domain.dto.ContributorBrowseResult; import org.folio.search.domain.dto.SubjectBrowseResult; import org.folio.search.exception.RequestValidationException; +import org.folio.search.model.BrowseResult; import org.folio.search.model.service.BrowseRequest; import org.folio.search.model.service.BrowseRequest.BrowseRequestBuilder; import org.folio.search.rest.resource.BrowseApi; import org.folio.search.service.browse.AuthorityBrowseService; import org.folio.search.service.browse.CallNumberBrowseService; +import org.folio.search.service.browse.ClassificationBrowseService; import org.folio.search.service.browse.ContributorBrowseService; import org.folio.search.service.browse.SubjectBrowseService; import org.folio.search.service.consortium.TenantProvider; @@ -42,6 +49,7 @@ public class BrowseController implements BrowseApi { private final AuthorityBrowseService authorityBrowseService; private final CallNumberBrowseService callNumberBrowseService; private final ContributorBrowseService contributorBrowseService; + private final ClassificationBrowseService classificationBrowseService; private final TenantProvider tenantProvider; @Override @@ -80,6 +88,21 @@ public ResponseEntity browseInstancesByCallNumber(String .next(instanceByCallNumber.getNext())); } + @Override + public ResponseEntity browseInstancesByClassificationNumber( + BrowseOptionType browseOptionId, String query, String tenant, Integer limit, Boolean expandAll, + Boolean highlightMatch, Integer precedingRecordsCount) { + + var browseRequest = getBrowseRequestBuilder(query, tenant, limit, expandAll, highlightMatch, precedingRecordsCount) + .resource(INSTANCE_CLASSIFICATION_RESOURCE) + .browseOptionType(browseOptionId) + .targetField(CLASSIFICATION_NUMBER_BROWSING_FIELD) + .build(); + + var browseResult = classificationBrowseService.browse(browseRequest); + return ResponseEntity.ok(toBrowseResultDto(browseResult)); + } + @Override public ResponseEntity browseInstancesByContributor(String query, String tenant, Integer limit, Boolean highlightMatch, @@ -110,9 +133,17 @@ public ResponseEntity browseInstancesBySubject(String query .next(browseResult.getNext())); } + private ClassificationNumberBrowseResult toBrowseResultDto(BrowseResult result) { + return new ClassificationNumberBrowseResult() + .totalRecords(result.getTotalRecords()) + .items(result.getRecords()) + .prev(result.getPrev()) + .next(result.getNext()); + } + private BrowseRequestBuilder getBrowseRequestBuilder(String query, String tenant, Integer limit, - Boolean expandAll, Boolean highlightMatch, - Integer precedingRecordsCount) { + Boolean expandAll, Boolean highlightMatch, + Integer precedingRecordsCount) { if (precedingRecordsCount != null && precedingRecordsCount >= limit) { throw new RequestValidationException("Preceding records count must be less than request limit", "precedingRecordsCount", String.valueOf(precedingRecordsCount)); diff --git a/src/main/java/org/folio/search/integration/ClassificationTypeHelper.java b/src/main/java/org/folio/search/integration/ClassificationTypeHelper.java deleted file mode 100644 index 6966626dd..000000000 --- a/src/main/java/org/folio/search/integration/ClassificationTypeHelper.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.folio.search.integration; - -import static java.util.Arrays.asList; -import static org.folio.search.client.InventoryReferenceDataClient.ReferenceDataType.CLASSIFICATION_TYPES; -import static org.folio.search.configuration.SearchCacheNames.REFERENCE_DATA_CACHE; -import static org.folio.search.model.client.CqlQueryParam.NAME; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import lombok.RequiredArgsConstructor; -import org.folio.search.configuration.properties.SearchConfigurationProperties; -import org.folio.search.model.types.ClassificationType; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class ClassificationTypeHelper { - - private final ReferenceDataService referenceDataService; - private final SearchConfigurationProperties configurationProperties; - - @Cacheable(cacheNames = REFERENCE_DATA_CACHE, - unless = "#result.isEmpty()", - key = "@folioExecutionContext.tenantId + ':classification-types'") - public Map getClassificationTypeMap() { - var browseClassificationTypes = configurationProperties.getBrowseClassificationTypes(); - if (browseClassificationTypes.isEmpty()) { - return Collections.emptyMap(); - } - Map result = new HashMap<>(); - - for (var typeEntry : browseClassificationTypes.entrySet()) { - var ids = referenceDataService.fetchReferenceData(CLASSIFICATION_TYPES, NAME, asList(typeEntry.getValue())); - ids.forEach(id -> result.put(id, typeEntry.getKey())); - } - - return result; - } - -} diff --git a/src/main/java/org/folio/search/model/config/BrowseConfigEntity.java b/src/main/java/org/folio/search/model/config/BrowseConfigEntity.java index f22ecafe7..7585bc74c 100644 --- a/src/main/java/org/folio/search/model/config/BrowseConfigEntity.java +++ b/src/main/java/org/folio/search/model/config/BrowseConfigEntity.java @@ -13,7 +13,7 @@ import lombok.Setter; import lombok.ToString; import org.folio.search.configuration.jpa.StringListConverter; -import org.hibernate.proxy.HibernateProxy; +import org.hibernate.Hibernate; @Getter @Setter @@ -46,12 +46,8 @@ public final boolean equals(Object o) { if (o == null) { return false; } - Class effectiveClass = o instanceof HibernateProxy - ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() - : o.getClass(); - Class thisEffectiveClass = this instanceof HibernateProxy - ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() - : this.getClass(); + Class effectiveClass = Hibernate.getClass(o); + Class thisEffectiveClass = Hibernate.getClass(this); if (thisEffectiveClass != effectiveClass) { return false; } diff --git a/src/main/java/org/folio/search/model/config/BrowseConfigId.java b/src/main/java/org/folio/search/model/config/BrowseConfigId.java index 8336c8042..32b3d8f10 100644 --- a/src/main/java/org/folio/search/model/config/BrowseConfigId.java +++ b/src/main/java/org/folio/search/model/config/BrowseConfigId.java @@ -6,7 +6,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; -import org.hibernate.proxy.HibernateProxy; +import org.hibernate.Hibernate; @Getter @Embeddable @@ -32,12 +32,8 @@ public final boolean equals(Object o) { if (o == null) { return false; } - Class effectiveClass = o instanceof HibernateProxy - ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() - : o.getClass(); - Class thisEffectiveClass = this instanceof HibernateProxy - ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() - : this.getClass(); + Class effectiveClass = Hibernate.getClass(o); + Class thisEffectiveClass = Hibernate.getClass(this); if (thisEffectiveClass != effectiveClass) { return false; } diff --git a/src/main/java/org/folio/search/model/service/BrowseRequest.java b/src/main/java/org/folio/search/model/service/BrowseRequest.java index 655c4a591..a5e9503b3 100644 --- a/src/main/java/org/folio/search/model/service/BrowseRequest.java +++ b/src/main/java/org/folio/search/model/service/BrowseRequest.java @@ -3,10 +3,11 @@ import lombok.Builder; import lombok.Data; import lombok.RequiredArgsConstructor; +import org.folio.search.domain.dto.BrowseOptionType; import org.folio.search.model.ResourceRequest; @Data -@Builder() +@Builder @RequiredArgsConstructor(staticName = "of") public class BrowseRequest implements ResourceRequest { @@ -20,6 +21,11 @@ public class BrowseRequest implements ResourceRequest { */ private final String tenantId; + /** + * Browse option type. + */ + private final BrowseOptionType browseOptionType; + /** * A CQL query string with search conditions. */ @@ -63,7 +69,14 @@ public class BrowseRequest implements ResourceRequest { public static BrowseRequest of(String resource, String tenantId, String query, Integer limit, String targetField, String subField, Boolean expandAll, Boolean highlightMatch, Integer precedingRecordsCount) { - return new BrowseRequest(resource, tenantId, query, limit, targetField, subField, null, expandAll, + return new BrowseRequest(resource, tenantId, null, query, limit, targetField, subField, null, expandAll, + highlightMatch, precedingRecordsCount); + } + + public static BrowseRequest of(String resource, String tenantId, BrowseOptionType optionType, String query, + Integer limit, String targetField, String subField, Boolean expandAll, + Boolean highlightMatch, Integer precedingRecordsCount) { + return new BrowseRequest(resource, tenantId, optionType, query, limit, targetField, subField, null, expandAll, highlightMatch, precedingRecordsCount); } } diff --git a/src/main/java/org/folio/search/service/ResourceService.java b/src/main/java/org/folio/search/service/ResourceService.java index 4c2456aa7..e2df9aee6 100644 --- a/src/main/java/org/folio/search/service/ResourceService.java +++ b/src/main/java/org/folio/search/service/ResourceService.java @@ -21,6 +21,7 @@ import java.util.Map; import java.util.Objects; import java.util.function.Function; +import java.util.function.UnaryOperator; import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; @@ -44,6 +45,7 @@ import org.folio.search.service.consortium.ConsortiumTenantExecutor; import org.folio.search.service.consortium.ConsortiumTenantService; import org.folio.search.service.converter.MultiTenantSearchDocumentConverter; +import org.folio.search.service.converter.preprocessor.InstanceEventPreProcessor; import org.folio.search.service.metadata.ResourceDescriptionService; import org.springframework.stereotype.Service; @@ -66,6 +68,7 @@ public class ResourceService { private final ConsortiumTenantExecutor consortiumTenantExecutor; private final ConsortiumInstanceService consortiumInstanceService; private final IndexNameProvider indexNameProvider; + private final InstanceEventPreProcessor instanceEventPreProcessor; /** * Saves list of resourceEvents to elasticsearch. @@ -154,13 +157,34 @@ private Map> processIndexInstanceEvents(List preProcessEvents(List instanceEvents, + UnaryOperator> consortiumFunc) { + if (instanceEvents == null) { + instanceEvents = Collections.emptyList(); + } + var list = instanceEvents.stream() + .map(event -> consortiumTenantExecutor.execute(() -> instanceEventPreProcessor.preProcess(event))) + .filter(Objects::nonNull) + .flatMap(List::stream) + .collect(toList()); + + var eventsToIndex = consortiumFunc.apply(instanceEvents); + if (eventsToIndex != null) { + list.addAll(eventsToIndex); + } + return list; } private Map> processDeleteInstanceEvents(List deleteEvents) { messageProducer.prepareAndSendContributorEvents(deleteEvents); messageProducer.prepareAndSendSubjectEvents(deleteEvents); - return multiTenantSearchDocumentConverter.convert(consortiumInstanceService.deleteInstances(deleteEvents)); + var list = preProcessEvents(deleteEvents, consortiumInstanceService::deleteInstances); + return multiTenantSearchDocumentConverter.convert(list); } private FolioIndexOperationResponse indexSearchDocuments(Map> eventsByResource) { diff --git a/src/main/java/org/folio/search/service/browse/ClassificationBrowseService.java b/src/main/java/org/folio/search/service/browse/ClassificationBrowseService.java new file mode 100644 index 000000000..f997964ab --- /dev/null +++ b/src/main/java/org/folio/search/service/browse/ClassificationBrowseService.java @@ -0,0 +1,136 @@ +package org.folio.search.service.browse; + +import static java.util.Locale.ROOT; +import static org.folio.search.utils.SearchUtils.BROWSE_FIELDS_MAP; +import static org.folio.search.utils.SearchUtils.CLASSIFICATION_TYPE_ID_FIELD; +import static org.folio.search.utils.SearchUtils.DEFAULT_SHELVING_ORDER_BROWSING_FIELD; +import static org.opensearch.index.query.QueryBuilders.boolQuery; +import static org.opensearch.index.query.QueryBuilders.matchAllQuery; +import static org.opensearch.index.query.QueryBuilders.termQuery; +import static org.opensearch.search.builder.SearchSourceBuilder.searchSource; +import static org.opensearch.search.sort.SortBuilders.fieldSort; +import static org.opensearch.search.sort.SortOrder.ASC; +import static org.opensearch.search.sort.SortOrder.DESC; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.folio.search.domain.dto.BrowseConfig; +import org.folio.search.domain.dto.BrowseType; +import org.folio.search.domain.dto.ClassificationNumberBrowseItem; +import org.folio.search.model.BrowseResult; +import org.folio.search.model.SearchResult; +import org.folio.search.model.index.ClassificationResource; +import org.folio.search.model.index.InstanceSubResource; +import org.folio.search.model.service.BrowseContext; +import org.folio.search.model.service.BrowseRequest; +import org.folio.search.service.consortium.BrowseConfigServiceDecorator; +import org.folio.search.service.consortium.ConsortiumSearchHelper; +import org.folio.search.utils.ShelvingOrderCalculationHelper; +import org.opensearch.index.query.QueryBuilder; +import org.opensearch.index.query.TermQueryBuilder; +import org.opensearch.search.builder.SearchSourceBuilder; +import org.springframework.stereotype.Service; + +@Log4j2 +@Service +@RequiredArgsConstructor +public class ClassificationBrowseService + extends AbstractBrowseServiceBySearchAfter { + + private final ConsortiumSearchHelper consortiumSearchHelper; + private final BrowseConfigServiceDecorator configService; + + @Override + protected String getValueForBrowsing(ClassificationNumberBrowseItem browseItem) { + return browseItem.getClassificationNumber(); + } + + @Override + protected SearchSourceBuilder getAnchorSearchQuery(BrowseRequest req, BrowseContext ctx) { + log.debug("getAnchorSearchQuery:: by [request: {}]", req); + var config = configService.getConfig(BrowseType.INSTANCE_CLASSIFICATION, req.getBrowseOptionType()); + var termQueryBuilder = getQuery(ctx, config, termQuery(req.getTargetField(), ctx.getAnchor())); + var query = consortiumSearchHelper.filterBrowseQueryForActiveAffiliation(ctx, + termQueryBuilder, req.getResource()); + return searchSource().query(query) + .size(ctx.getLimit(ctx.isBrowsingForward())) + .from(0); + } + + @Override + protected SearchSourceBuilder getSearchQuery(BrowseRequest req, BrowseContext ctx, boolean isBrowsingForward) { + log.debug("getSearchQuery:: by [request: {}, isBrowsingForward: {}]", req, isBrowsingForward); + var config = configService.getConfig(BrowseType.INSTANCE_CLASSIFICATION, req.getBrowseOptionType()); + + var browseField = getBrowseField(config); + var normalizedAnchor = ShelvingOrderCalculationHelper.calculate(ctx.getAnchor(), config.getShelvingAlgorithm()); + var query = consortiumSearchHelper.filterBrowseQueryForActiveAffiliation(ctx, getQuery(ctx, config, null), + req.getResource()); + + return searchSource().query(query) + .searchAfter(new Object[] {normalizedAnchor.toLowerCase(ROOT)}) + .sort(fieldSort(browseField).order(isBrowsingForward ? ASC : DESC)) + .size(ctx.getLimit(isBrowsingForward) + 1) + .from(0); + } + + @Override + protected ClassificationNumberBrowseItem getEmptyBrowseItem(BrowseContext context) { + return new ClassificationNumberBrowseItem() + .classificationNumber(context.getAnchor()) + .totalRecords(0) + .isAnchor(true); + } + + @Override + protected BrowseResult mapToBrowseResult(BrowseContext ctx, + SearchResult res, + boolean isAnchor) { + return BrowseResult.of(res) + .map(resource -> new ClassificationNumberBrowseItem() + .classificationNumber(resource.number()) + .classificationTypeId(resource.typeId()) + .isAnchor(isAnchor ? true : null) + .totalRecords(getTotalRecords(ctx, resource))); + } + + private static QueryBuilder getQuery(BrowseContext ctx, BrowseConfig config, TermQueryBuilder anchorQuery) { + var typeIds = config.getTypeIds(); + if (config.getTypeIds().isEmpty() && ctx.getFilters().isEmpty()) { + if (anchorQuery != null) { + return anchorQuery; + } + return matchAllQuery(); + } else { + var boolQueryMain = boolQuery(); + if (!config.getTypeIds().isEmpty()) { + var boolQuery = boolQuery(); + for (var typeId : typeIds) { + boolQuery.should(termQuery(CLASSIFICATION_TYPE_ID_FIELD, typeId.toString())); + } + boolQueryMain.must(boolQuery); + } + if (!ctx.getFilters().isEmpty()) { + ctx.getFilters().forEach(boolQueryMain::filter); + } + if (anchorQuery != null) { + boolQueryMain.must(anchorQuery); + } + return boolQueryMain; + } + } + + private static String getBrowseField(BrowseConfig config) { + return BROWSE_FIELDS_MAP.getOrDefault(config.getShelvingAlgorithm(), DEFAULT_SHELVING_ORDER_BROWSING_FIELD); + } + + private Integer getTotalRecords(BrowseContext ctx, ClassificationResource classificationResource) { + return consortiumSearchHelper.filterSubResourcesForConsortium(ctx, classificationResource, + ClassificationResource::instances).stream() + .map(InstanceSubResource::getInstanceId) + .distinct() + .map(e -> 1) + .reduce(0, Integer::sum); + } + +} diff --git a/src/main/java/org/folio/search/service/config/BrowseConfigService.java b/src/main/java/org/folio/search/service/config/BrowseConfigService.java index 6ca08db2f..75ba0fcc0 100644 --- a/src/main/java/org/folio/search/service/config/BrowseConfigService.java +++ b/src/main/java/org/folio/search/service/config/BrowseConfigService.java @@ -11,6 +11,7 @@ import org.folio.search.domain.dto.BrowseOptionType; import org.folio.search.domain.dto.BrowseType; import org.folio.search.exception.RequestValidationException; +import org.folio.search.model.config.BrowseConfigId; import org.folio.search.repository.BrowseConfigEntityRepository; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; @@ -26,13 +27,26 @@ public class BrowseConfigService { private final BrowseConfigEntityRepository repository; private final BrowseConfigMapper mapper; - @Cacheable(cacheNames = BROWSE_CONFIG_CACHE, key = "@folioExecutionContext.tenantId + ':' + #type.value") public BrowseConfigCollection getConfigs(@NonNull BrowseType type) { log.debug("Fetch browse configuration [browseType: {}]", type.getValue()); return mapper.convert(repository.findByConfigId_BrowseType(type.getValue())); } - @CacheEvict(cacheNames = BROWSE_CONFIG_CACHE, key = "@folioExecutionContext.tenantId + ':' + #type.value") + @Cacheable(cacheNames = BROWSE_CONFIG_CACHE, + key = "@folioExecutionContext.tenantId + ':' + #type.value + ':' + #optionType.value") + public BrowseConfig getConfig(@NonNull BrowseType type, @NonNull BrowseOptionType optionType) { + var typeValue = type.getValue(); + var optionTypeValue = optionType.getValue(); + log.debug("Fetch browse configuration [browseType: {}, browseOptionType: {}]", typeValue, optionTypeValue); + + return repository.findById(new BrowseConfigId(typeValue, optionTypeValue)) + .map(mapper::convert) + .orElseThrow(() -> new IllegalStateException( + "Config for %s type %s must be present in database".formatted(typeValue, optionTypeValue))); + } + + @CacheEvict(cacheNames = BROWSE_CONFIG_CACHE, + key = "@folioExecutionContext.tenantId + ':' + #type.value + ':' + #optionType.value") public void upsertConfig(@NonNull BrowseType type, @NonNull BrowseOptionType optionType, @NonNull BrowseConfig config) { diff --git a/src/main/java/org/folio/search/service/consortium/BrowseConfigServiceDecorator.java b/src/main/java/org/folio/search/service/consortium/BrowseConfigServiceDecorator.java index 3583f6806..4cc41e8ec 100644 --- a/src/main/java/org/folio/search/service/consortium/BrowseConfigServiceDecorator.java +++ b/src/main/java/org/folio/search/service/consortium/BrowseConfigServiceDecorator.java @@ -19,6 +19,10 @@ public BrowseConfigCollection getConfigs(BrowseType type) { return consortiumTenantExecutor.execute(() -> browseConfigService.getConfigs(type)); } + public BrowseConfig getConfig(BrowseType type, BrowseOptionType optionType) { + return consortiumTenantExecutor.execute(() -> browseConfigService.getConfig(type, optionType)); + } + public void upsertConfig(BrowseType type, BrowseOptionType configId, BrowseConfig config) { consortiumTenantExecutor.run(() -> browseConfigService.upsertConfig(type, configId, config)); } 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 fb3f93cee..a4feee7ce 100644 --- a/src/main/java/org/folio/search/service/consortium/ConsortiumSearchHelper.java +++ b/src/main/java/org/folio/search/service/consortium/ConsortiumSearchHelper.java @@ -1,6 +1,7 @@ package org.folio.search.service.consortium; import static org.folio.search.utils.SearchUtils.CONTRIBUTOR_RESOURCE; +import static org.folio.search.utils.SearchUtils.INSTANCE_CLASSIFICATION_RESOURCE; import static org.folio.search.utils.SearchUtils.INSTANCE_SUBJECT_RESOURCE; import static org.folio.search.utils.SearchUtils.SHARED_FIELD_NAME; import static org.folio.search.utils.SearchUtils.TENANT_ID_FIELD_NAME; @@ -226,7 +227,9 @@ private boolean sharedFilterValue(TermQueryBuilder sharedQuery) { } private String getFieldForResource(String fieldName, String resourceName) { - if (resourceName.equals(CONTRIBUTOR_RESOURCE) || resourceName.equals(INSTANCE_SUBJECT_RESOURCE)) { + if (resourceName.equals(CONTRIBUTOR_RESOURCE) + || resourceName.equals(INSTANCE_SUBJECT_RESOURCE) + || resourceName.equals(INSTANCE_CLASSIFICATION_RESOURCE)) { return "instances." + fieldName; } return fieldName; diff --git a/src/main/java/org/folio/search/service/converter/preprocessor/InstanceEventPreProcessor.java b/src/main/java/org/folio/search/service/converter/preprocessor/InstanceEventPreProcessor.java index 4076098cd..8103a0067 100644 --- a/src/main/java/org/folio/search/service/converter/preprocessor/InstanceEventPreProcessor.java +++ b/src/main/java/org/folio/search/service/converter/preprocessor/InstanceEventPreProcessor.java @@ -61,12 +61,7 @@ public List preProcess(ResourceEvent event) { return List.of(event); } - List events = new ArrayList<>(); - events.add(event); - - var classificationEvents = prepareClassificationEvents(event); - - events.addAll(classificationEvents); + var events = prepareClassificationEvents(event); log.info("preProcess::Finished instance event pre-processing"); if (log.isDebugEnabled()) { diff --git a/src/main/java/org/folio/search/service/setter/AbstractIdentifierProcessor.java b/src/main/java/org/folio/search/service/setter/AbstractIdentifierProcessor.java index 283bc7e8a..f644a3a07 100644 --- a/src/main/java/org/folio/search/service/setter/AbstractIdentifierProcessor.java +++ b/src/main/java/org/folio/search/service/setter/AbstractIdentifierProcessor.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Objects; import java.util.Set; +import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.folio.search.domain.dto.Identifiers; @@ -19,12 +20,9 @@ public abstract class AbstractIdentifierProcessor implements FieldProcessor> { private final ReferenceDataService referenceDataService; + @Getter private final List identifierNames; - public List getIdentifierNames() { - return identifierNames; - } - /** * Returns set of filtered identifiers value from event body by specified set of types. * @@ -51,8 +49,7 @@ private Set fetchIdentifierIdsFromCache() { var identifierTypeIds = referenceDataService.fetchReferenceData(IDENTIFIER_TYPES, CqlQueryParam.NAME, getIdentifierNames()); if (identifierTypeIds.isEmpty()) { - log.warn("Failed to provide identifiers for processor: {}]", - this.getClass().getSimpleName()); + log.warn("Failed to provide identifiers for [processor: {}]", this.getClass().getSimpleName()); } return identifierTypeIds; } diff --git a/src/main/java/org/folio/search/service/setter/ClassificationShelvingOrderProcessor.java b/src/main/java/org/folio/search/service/setter/ClassificationShelvingOrderProcessor.java deleted file mode 100644 index c88e02d59..000000000 --- a/src/main/java/org/folio/search/service/setter/ClassificationShelvingOrderProcessor.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.folio.search.service.setter; - -import java.util.Map; -import java.util.function.Function; -import lombok.RequiredArgsConstructor; -import org.folio.search.cql.SuDocCallNumber; -import org.folio.search.integration.ClassificationTypeHelper; -import org.folio.search.model.index.ClassificationResource; -import org.folio.search.model.types.ClassificationType; -import org.folio.search.utils.CallNumberUtils; -import org.marc4j.callnum.CallNumber; -import org.marc4j.callnum.DeweyCallNumber; -import org.marc4j.callnum.LCCallNumber; -import org.marc4j.callnum.NlmCallNumber; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class ClassificationShelvingOrderProcessor implements FieldProcessor { - - private static final Map> CN_TYPE_TO_SHELF_KEY_GENERATOR = Map.of( - ClassificationType.NLM, cn -> getShelfKey(new NlmCallNumber(cn)), - ClassificationType.LC, cn -> getShelfKey(new LCCallNumber(cn)), - ClassificationType.DEWEY, cn -> getShelfKey(new DeweyCallNumber(cn)), - ClassificationType.SUDOC, cn -> getShelfKey(new SuDocCallNumber(cn)), - ClassificationType.DEFAULT, CallNumberUtils::normalizeEffectiveShelvingOrder - ); - - private final ClassificationTypeHelper classificationTypeHelper; - - @Override - public String getFieldValue(ClassificationResource eventBody) { - var number = eventBody.number(); - var typeId = eventBody.typeId(); - var classificationByIdMap = classificationTypeHelper.getClassificationTypeMap(); - var classificationType = classificationByIdMap.getOrDefault(typeId, ClassificationType.DEFAULT); - return CN_TYPE_TO_SHELF_KEY_GENERATOR.get(classificationType).apply(number); - } - - private static String getShelfKey(CallNumber value) { - return value.getShelfKey(); - } - -} diff --git a/src/main/java/org/folio/search/service/setter/classification/ClassificationShelvingOrderFieldProcessor.java b/src/main/java/org/folio/search/service/setter/classification/ClassificationShelvingOrderFieldProcessor.java new file mode 100644 index 000000000..0245b5b8f --- /dev/null +++ b/src/main/java/org/folio/search/service/setter/classification/ClassificationShelvingOrderFieldProcessor.java @@ -0,0 +1,23 @@ +package org.folio.search.service.setter.classification; + +import java.util.function.UnaryOperator; +import lombok.NonNull; +import org.folio.search.model.index.ClassificationResource; +import org.folio.search.service.setter.FieldProcessor; + +public abstract class ClassificationShelvingOrderFieldProcessor + implements FieldProcessor { + + private final UnaryOperator numberFunction; + + protected ClassificationShelvingOrderFieldProcessor(@NonNull UnaryOperator numberFunction) { + this.numberFunction = numberFunction; + } + + @Override + public String getFieldValue(ClassificationResource eventBody) { + var number = eventBody.number(); + return numberFunction.apply(number); + } +} + diff --git a/src/main/java/org/folio/search/service/setter/classification/DefaultClassificationShelvingOrderFieldProcessor.java b/src/main/java/org/folio/search/service/setter/classification/DefaultClassificationShelvingOrderFieldProcessor.java new file mode 100644 index 000000000..2f962b308 --- /dev/null +++ b/src/main/java/org/folio/search/service/setter/classification/DefaultClassificationShelvingOrderFieldProcessor.java @@ -0,0 +1,13 @@ +package org.folio.search.service.setter.classification; + +import org.folio.search.domain.dto.ShelvingOrderAlgorithmType; +import org.folio.search.utils.ShelvingOrderCalculationHelper; +import org.springframework.stereotype.Component; + +@Component +public class DefaultClassificationShelvingOrderFieldProcessor extends ClassificationShelvingOrderFieldProcessor { + + protected DefaultClassificationShelvingOrderFieldProcessor() { + super(number -> ShelvingOrderCalculationHelper.calculate(number, ShelvingOrderAlgorithmType.DEFAULT)); + } +} diff --git a/src/main/java/org/folio/search/service/setter/classification/DeweyClassificationShelvingOrderFieldProcessor.java b/src/main/java/org/folio/search/service/setter/classification/DeweyClassificationShelvingOrderFieldProcessor.java new file mode 100644 index 000000000..134568a48 --- /dev/null +++ b/src/main/java/org/folio/search/service/setter/classification/DeweyClassificationShelvingOrderFieldProcessor.java @@ -0,0 +1,12 @@ +package org.folio.search.service.setter.classification; + +import org.folio.search.domain.dto.ShelvingOrderAlgorithmType; +import org.folio.search.utils.ShelvingOrderCalculationHelper; +import org.springframework.stereotype.Component; + +@Component +public class DeweyClassificationShelvingOrderFieldProcessor extends ClassificationShelvingOrderFieldProcessor { + protected DeweyClassificationShelvingOrderFieldProcessor() { + super(number -> ShelvingOrderCalculationHelper.calculate(number, ShelvingOrderAlgorithmType.DEWEY)); + } +} diff --git a/src/main/java/org/folio/search/service/setter/classification/LcClassificationShelvingOrderFieldProcessor.java b/src/main/java/org/folio/search/service/setter/classification/LcClassificationShelvingOrderFieldProcessor.java new file mode 100644 index 000000000..e245e841a --- /dev/null +++ b/src/main/java/org/folio/search/service/setter/classification/LcClassificationShelvingOrderFieldProcessor.java @@ -0,0 +1,13 @@ +package org.folio.search.service.setter.classification; + +import org.folio.search.domain.dto.ShelvingOrderAlgorithmType; +import org.folio.search.utils.ShelvingOrderCalculationHelper; +import org.springframework.stereotype.Component; + +@Component +public class LcClassificationShelvingOrderFieldProcessor extends ClassificationShelvingOrderFieldProcessor { + + protected LcClassificationShelvingOrderFieldProcessor() { + super(number -> ShelvingOrderCalculationHelper.calculate(number, ShelvingOrderAlgorithmType.LC)); + } +} diff --git a/src/main/java/org/folio/search/utils/SearchUtils.java b/src/main/java/org/folio/search/utils/SearchUtils.java index bd12b5042..49ade1819 100644 --- a/src/main/java/org/folio/search/utils/SearchUtils.java +++ b/src/main/java/org/folio/search/utils/SearchUtils.java @@ -20,6 +20,7 @@ import org.folio.search.domain.dto.Contributor; import org.folio.search.domain.dto.Instance; import org.folio.search.domain.dto.ResourceEvent; +import org.folio.search.domain.dto.ShelvingOrderAlgorithmType; import org.folio.search.exception.SearchOperationException; import org.folio.search.model.ResourceRequest; import org.folio.search.model.index.SearchDocumentBody; @@ -45,8 +46,13 @@ public class SearchUtils { public static final String TENANT_ID_FIELD_NAME = "tenantId"; public static final String IS_BOUND_WITH_FIELD_NAME = "isBoundWith"; public static final String CALL_NUMBER_BROWSING_FIELD = "callNumber"; + public static final String CLASSIFICATION_NUMBER_BROWSING_FIELD = "number"; + public static final String CLASSIFICATION_TYPE_ID_FIELD = "typeId"; public static final String TYPED_CALL_NUMBER_BROWSING_FIELD = "typedCallNumber"; public static final String SHELVING_ORDER_BROWSING_FIELD = "itemEffectiveShelvingOrder"; + public static final String DEFAULT_SHELVING_ORDER_BROWSING_FIELD = "defaultShelvingOrder"; + public static final String LC_SHELVING_ORDER_BROWSING_FIELD = "lcShelvingOrder"; + public static final String DEWEY_SHELVING_ORDER_BROWSING_FIELD = "deweyShelvingOrder"; public static final String SUBJECT_BROWSING_FIELD = "value"; public static final String CONTRIBUTOR_BROWSING_FIELD = "name"; public static final String AUTHORITY_BROWSING_FIELD = "headingRef"; @@ -67,6 +73,12 @@ public class SearchUtils { public static final String KEYWORD_FIELD_INDEX = "keyword"; public static final float CONST_SIZE_LOAD_FACTOR = 1.0f; + public static final Map BROWSE_FIELDS_MAP = Map.of( + ShelvingOrderAlgorithmType.DEFAULT, DEFAULT_SHELVING_ORDER_BROWSING_FIELD, + ShelvingOrderAlgorithmType.LC, LC_SHELVING_ORDER_BROWSING_FIELD, + ShelvingOrderAlgorithmType.DEWEY, DEWEY_SHELVING_ORDER_BROWSING_FIELD + ); + //CHECKSTYLE.ON: LineLength public static final String INSTANCE_SUBJECT_UPSERT_SCRIPT_ID = "instance_subject_upsert_script"; public static final String INSTANCE_SUBJECT_UPSERT_SCRIPT = """ diff --git a/src/main/java/org/folio/search/utils/ShelvingOrderCalculationHelper.java b/src/main/java/org/folio/search/utils/ShelvingOrderCalculationHelper.java new file mode 100644 index 000000000..db4d26137 --- /dev/null +++ b/src/main/java/org/folio/search/utils/ShelvingOrderCalculationHelper.java @@ -0,0 +1,24 @@ +package org.folio.search.utils; + +import java.util.Locale; +import lombok.NonNull; +import lombok.experimental.UtilityClass; +import org.folio.search.domain.dto.ShelvingOrderAlgorithmType; +import org.marc4j.callnum.DeweyCallNumber; +import org.marc4j.callnum.LCCallNumber; + +@UtilityClass +public class ShelvingOrderCalculationHelper { + + public static String calculate(@NonNull String input, @NonNull ShelvingOrderAlgorithmType algorithmType) { + return switch (algorithmType) { + case LC -> new LCCallNumber(input).getShelfKey().trim(); + case DEWEY -> new DeweyCallNumber(input).getShelfKey().trim(); + case DEFAULT -> normalize(input); + }; + } + + private static String normalize(String input) { + return input.toUpperCase(Locale.ROOT).trim(); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f57bdbe52..95042960f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -66,11 +66,6 @@ folio: browse-cn-intermediate-values: ${BROWSE_CN_INTERMEDIATE_VALUES_ENABLED:true} browse-cn-intermediate-remove-duplicates: ${BROWSE_CN_INTERMEDIATE_REMOVE_DUPLICATES:true} browse-classifications: ${BROWSE_CLASSIFICATIONS_ENABLED:false} - browse-classification-types: - lc: LC,LC (local) - nlm: NLM - dewey: Dewey,Additional Dewey - sudoc: SUDOC indexing: instance-subjects: retry-attempts: ${INSTANCE_SUBJECTS_INDEXING_RETRY_ATTEMPTS:3} diff --git a/src/main/resources/model/instance.json b/src/main/resources/model/instance.json index fbd0f16ab..af9c334d7 100644 --- a/src/main/resources/model/instance.json +++ b/src/main/resources/model/instance.json @@ -3,9 +3,6 @@ "eventBodyJavaClass": "org.folio.search.domain.dto.Instance", "reindexSupported": true, "languageSourcePaths": [ "$.languages" ], - "indexingConfiguration": { - "eventPreProcessor": "instanceEventPreProcessor" - }, "searchFieldModifiers": [ "itemSearchFieldModifier" ], diff --git a/src/main/resources/model/instance_classification.json b/src/main/resources/model/instance_classification.json index 6ed3aba42..f312e2b53 100644 --- a/src/main/resources/model/instance_classification.json +++ b/src/main/resources/model/instance_classification.json @@ -30,10 +30,20 @@ } }, "searchFields": { - "shelvingOrder": { + "defaultShelvingOrder": { "type": "search", "index": "keyword_lowercase", - "processor": "classificationShelvingOrderProcessor" + "processor": "defaultClassificationShelvingOrderFieldProcessor" + }, + "lcShelvingOrder": { + "type": "search", + "index": "keyword_lowercase", + "processor": "lcClassificationShelvingOrderFieldProcessor" + }, + "deweyShelvingOrder": { + "type": "search", + "index": "keyword_lowercase", + "processor": "deweyClassificationShelvingOrderFieldProcessor" } } } diff --git a/src/main/resources/swagger.api/mod-search.yaml b/src/main/resources/swagger.api/mod-search.yaml index 2974ee977..f0460663a 100644 --- a/src/main/resources/swagger.api/mod-search.yaml +++ b/src/main/resources/swagger.api/mod-search.yaml @@ -234,6 +234,33 @@ paths: '500': $ref: '#/components/responses/internalServerErrorResponse' + /browse/classification-numbers/{browseOptionId}/instances: + get: + operationId: browseInstancesByClassificationNumber + description: Provides list of instances for browsing by classification number + tags: + - browse + parameters: + - $ref: '#/components/parameters/browse-option-id' + - $ref: '#/components/parameters/cql-query' + - $ref: '#/components/parameters/browse-limit-param' + - $ref: '#/components/parameters/expand-all-param' + - $ref: '#/components/parameters/highlight-match' + - $ref: '#/components/parameters/preceding-records-count' + - $ref: '#/components/parameters/x-okapi-tenant-header' + responses: + '200': + description: 'Search result for browsing by call number' + content: + application/json: + example: examples/searchResult.sample + schema: + $ref: '#/components/schemas/classificationNumberBrowseResult' + '400': + $ref: '#/components/responses/badRequestResponse' + '500': + $ref: '#/components/responses/internalServerErrorResponse' + /browse/subjects/instances: get: operationId: browseInstancesBySubject @@ -585,7 +612,7 @@ paths: schema: $ref: '#/components/schemas/browseConfigCollection' - /browse/config/{browseType}/{browseConfigId}: + /browse/config/{browseType}/{browseOptionId}: put: operationId: putBrowseConfig description: Update configuration for browse type @@ -593,7 +620,7 @@ paths: - config parameters: - $ref: '#/components/parameters/browse-type' - - $ref: '#/components/parameters/browse-config-id' + - $ref: '#/components/parameters/browse-option-id' requestBody: required: true content: @@ -630,6 +657,8 @@ components: $ref: schemas/response/authoritySearchResult.json callNumberBrowseResult: $ref: schemas/response/callNumberBrowseResult.json + classificationNumberBrowseResult: + $ref: schemas/response/classificationNumberBrowseResult.json subjectBrowseResult: $ref: schemas/response/subjectBrowseResult.json contributorBrowseResult: @@ -833,11 +862,11 @@ components: schema: type: boolean default: true - browse-config-id: - name: browseConfigId + browse-option-id: + name: browseOptionId in: path required: true - description: 'ID of browse config' + description: 'Browse option type' schema: $ref: '#/components/schemas/browseOptionType' browse-type: diff --git a/src/main/resources/swagger.api/schemas/response/classificationNumberBrowseItem.json b/src/main/resources/swagger.api/schemas/response/classificationNumberBrowseItem.json new file mode 100644 index 000000000..071958581 --- /dev/null +++ b/src/main/resources/swagger.api/schemas/response/classificationNumberBrowseItem.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Classification number browse search result item", + "type": "object", + "properties": { + "classificationNumber": { + "type": "string", + "description": "Classification number value to display" + }, + "classificationTypeId": { + "type": "string", + "description": "Classification type ID value" + }, + "isAnchor": { + "type": "boolean", + "description": "Marks if current value is anchor or not" + }, + "totalRecords": { + "type": "integer", + "description": "Amount of records for the classification number value" + } + } +} diff --git a/src/main/resources/swagger.api/schemas/response/classificationNumberBrowseResult.json b/src/main/resources/swagger.api/schemas/response/classificationNumberBrowseResult.json new file mode 100644 index 000000000..8eb4c1774 --- /dev/null +++ b/src/main/resources/swagger.api/schemas/response/classificationNumberBrowseResult.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Classification number browse search response", + "type": "object", + "properties": { + "totalRecords": { + "type": "integer", + "description": "Amount of items to display" + }, + "prev": { + "type": "string", + "description": "Previous value for browsing backward" + }, + "next": { + "type": "string", + "description": "Next value for browsing forward" + }, + "items": { + "type": "array", + "description": "List of classification number browse items", + "items": { + "$ref": "classificationNumberBrowseItem.json" + } + } + } +} diff --git a/src/test/java/org/folio/search/controller/BrowseClassificationConsortiumIT.java b/src/test/java/org/folio/search/controller/BrowseClassificationConsortiumIT.java new file mode 100644 index 000000000..a054c8910 --- /dev/null +++ b/src/test/java/org/folio/search/controller/BrowseClassificationConsortiumIT.java @@ -0,0 +1,150 @@ +package org.folio.search.controller; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.awaitility.Durations.ONE_MINUTE; +import static org.awaitility.Durations.ONE_SECOND; +import static org.folio.search.model.Pair.pair; +import static org.folio.search.support.base.ApiEndpoints.instanceClassificationBrowsePath; +import static org.folio.search.support.base.ApiEndpoints.instanceSearchPath; +import static org.folio.search.utils.SearchUtils.getIndexName; +import static org.folio.search.utils.TestConstants.CONSORTIUM_TENANT_ID; +import static org.folio.search.utils.TestConstants.MEMBER_TENANT_ID; +import static org.folio.search.utils.TestUtils.classificationBrowseItem; +import static org.folio.search.utils.TestUtils.classificationBrowseResult; +import static org.folio.search.utils.TestUtils.parseResponse; +import static org.folio.search.utils.TestUtils.randomId; +import static org.opensearch.index.query.QueryBuilders.matchAllQuery; +import static org.opensearch.search.builder.SearchSourceBuilder.searchSource; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; + +import java.util.List; +import java.util.stream.Collectors; +import org.folio.search.domain.dto.BrowseOptionType; +import org.folio.search.domain.dto.ClassificationNumberBrowseResult; +import org.folio.search.domain.dto.Instance; +import org.folio.search.domain.dto.InstanceClassificationsInner; +import org.folio.search.model.Pair; +import org.folio.search.support.base.BaseConsortiumIntegrationTest; +import org.folio.search.utils.SearchUtils; +import org.folio.spring.testing.type.IntegrationTest; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.client.RequestOptions; + +@IntegrationTest +class BrowseClassificationConsortiumIT extends BaseConsortiumIntegrationTest { + + private static final String LC_TYPE_ID = "e62bbefe-adf5-4b1e-b3e7-43d877b0c91a"; + private static final String LC2_TYPE_ID = "308c950f-8209-4f2e-9702-0c004a9f21bc"; + private static final String DEWEY_TYPE_ID = "50524585-046b-49a1-8ca7-8d46f2a8dc19"; + private static final Instance[] INSTANCES_MEMBER = instancesMember(); + private static final Instance[] INSTANCES_CENTRAL = instancesCentral(); + + @BeforeAll + static void prepare() { + setUpTenant(CONSORTIUM_TENANT_ID); + setUpTenant(MEMBER_TENANT_ID); + saveRecords(CONSORTIUM_TENANT_ID, instanceSearchPath(), asList(INSTANCES_CENTRAL), + INSTANCES_CENTRAL.length, + instance -> inventoryApi.createInstance(CONSORTIUM_TENANT_ID, instance)); + saveRecords(MEMBER_TENANT_ID, instanceSearchPath(), asList(INSTANCES_MEMBER), + INSTANCES_CENTRAL.length + INSTANCES_MEMBER.length, + instance -> inventoryApi.createInstance(MEMBER_TENANT_ID, instance)); + + await().atMost(ONE_MINUTE).pollInterval(ONE_SECOND).untilAsserted(() -> { + var searchRequest = new SearchRequest() + .source(searchSource().query(matchAllQuery()).trackTotalHits(true).from(0).size(100)) + .indices(getIndexName(SearchUtils.INSTANCE_CLASSIFICATION_RESOURCE, CONSORTIUM_TENANT_ID)); + var searchResponse = elasticClient.search(searchRequest, RequestOptions.DEFAULT); + assertThat(searchResponse.getHits().getTotalHits().value).isEqualTo(17); + }); + } + + @AfterAll + static void cleanUp() { + removeTenant(); + } + + @Test + void browseByClassification_shared() { + var request = get(instanceClassificationBrowsePath(BrowseOptionType.ALL)) + .param("query", prepareQuery("number < {value} or number >= {value} and instances.shared==true", + "\"QD33 .O87\"")) + .param("limit", "4") + .param("precedingRecordsCount", "2"); + var actual = parseResponse(doGet(request), ClassificationNumberBrowseResult.class); + assertThat(actual).isEqualTo(classificationBrowseResult("HQ536 .A565 2018", null, 8, List.of( + classificationBrowseItem("HQ536 .A565 2018", LC2_TYPE_ID, 1), + classificationBrowseItem("N6679.R64 G88 2010", LC_TYPE_ID, 1), + classificationBrowseItem("QD33 .O87", LC_TYPE_ID, 1, true), + classificationBrowseItem("QD453 .M8 1961", LC_TYPE_ID, 1) + + ))); + } + + @Test + void browseByClassification_local() { + var request = get(instanceClassificationBrowsePath(BrowseOptionType.ALL)) + .param("query", prepareQuery("number < {value} or number >= {value} and instances.shared==false", + "\"QD33 .O87\"")) + .param("limit", "4") + .param("precedingRecordsCount", "2"); + var actual = parseResponse(doGet(request), ClassificationNumberBrowseResult.class); + assertThat(actual).isEqualTo(classificationBrowseResult("333.91", "SF433 .D47 2004", 11, List.of( + classificationBrowseItem("333.91", DEWEY_TYPE_ID, 1), + classificationBrowseItem("372.4", DEWEY_TYPE_ID, 1), + classificationBrowseItem("QD33 .O87", LC_TYPE_ID, 1, true), + classificationBrowseItem("SF433 .D47 2004", LC_TYPE_ID, 1) + ))); + } + + private static Instance[] instancesCentral() { + return classificationBrowseInstanceData().subList(0, 5).stream() + .map(BrowseClassificationConsortiumIT::instance) + .toArray(Instance[]::new); + } + + private static Instance[] instancesMember() { + return classificationBrowseInstanceData().subList(5, 10).stream() + .map(BrowseClassificationConsortiumIT::instance) + .toArray(Instance[]::new); + } + + @SuppressWarnings("unchecked") + private static Instance instance(List data) { + return new Instance() + .id(randomId()) + .title((String) data.get(0)) + .classifications(((List>) data.get(1)).stream() + .map(pair -> new InstanceClassificationsInner() + .classificationNumber(String.valueOf(pair.getFirst())) + .classificationTypeId(String.valueOf(pair.getSecond()))) + .collect(Collectors.toList())) + .staffSuppress(false) + .discoverySuppress(false) + .holdings(emptyList()); + } + + private static List> classificationBrowseInstanceData() { + return List.of( + List.of("instance #01", List.of(pair("BJ1453 .I49 1983", LC_TYPE_ID), pair("HD1691 .I5 1967", LC_TYPE_ID))), + List.of("instance #02", List.of(pair("BJ1453 .I49 1983", LC2_TYPE_ID))), + List.of("instance #03", List.of(pair("HQ536 .A565 2018", LC2_TYPE_ID), pair("N6679.R64 G88 2010", LC_TYPE_ID))), + List.of("instance #04", List.of(pair("QD33 .O87", LC_TYPE_ID))), + List.of("instance #05", List.of(pair("QD453 .M8 1961", LC_TYPE_ID), pair("146.4", DEWEY_TYPE_ID))), + List.of("instance #06", List.of(pair("SF433 .D47 2004", LC_TYPE_ID), pair("TX545 .M45", LC_TYPE_ID))), + List.of("instance #07", List.of(pair("221.609", DEWEY_TYPE_ID), pair("SF991 .M94", LC2_TYPE_ID))), + List.of("instance #08", List.of(pair("TN800 .F4613", LC_TYPE_ID))), + List.of("instance #09", List.of(pair("292.07", DEWEY_TYPE_ID), pair("333.91", DEWEY_TYPE_ID), + pair("372.4", DEWEY_TYPE_ID))), + List.of("instance #10", List.of(pair("146.4", DEWEY_TYPE_ID), pair("QD33 .O87", LC_TYPE_ID), + pair("SF991 .M94", null))) + ); + } + +} diff --git a/src/test/java/org/folio/search/controller/BrowseClassificationIT.java b/src/test/java/org/folio/search/controller/BrowseClassificationIT.java new file mode 100644 index 000000000..3fbd8a380 --- /dev/null +++ b/src/test/java/org/folio/search/controller/BrowseClassificationIT.java @@ -0,0 +1,247 @@ +package org.folio.search.controller; + +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.awaitility.Durations.ONE_MINUTE; +import static org.awaitility.Durations.ONE_SECOND; +import static org.folio.search.model.Pair.pair; +import static org.folio.search.support.base.ApiEndpoints.browseConfigPath; +import static org.folio.search.support.base.ApiEndpoints.instanceClassificationBrowsePath; +import static org.folio.search.utils.SearchUtils.getIndexName; +import static org.folio.search.utils.TestConstants.TENANT_ID; +import static org.folio.search.utils.TestUtils.classificationBrowseItem; +import static org.folio.search.utils.TestUtils.classificationBrowseResult; +import static org.folio.search.utils.TestUtils.parseResponse; +import static org.folio.search.utils.TestUtils.randomId; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.opensearch.index.query.QueryBuilders.matchAllQuery; +import static org.opensearch.search.builder.SearchSourceBuilder.searchSource; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.folio.search.domain.dto.BrowseConfig; +import org.folio.search.domain.dto.BrowseOptionType; +import org.folio.search.domain.dto.BrowseType; +import org.folio.search.domain.dto.ClassificationNumberBrowseResult; +import org.folio.search.domain.dto.Instance; +import org.folio.search.domain.dto.InstanceClassificationsInner; +import org.folio.search.domain.dto.ShelvingOrderAlgorithmType; +import org.folio.search.model.Pair; +import org.folio.search.support.base.BaseIntegrationTest; +import org.folio.search.utils.SearchUtils; +import org.folio.spring.testing.type.IntegrationTest; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.client.RequestOptions; +import org.opensearch.client.RestHighLevelClient; +import org.springframework.beans.factory.annotation.Autowired; + +@IntegrationTest +class BrowseClassificationIT extends BaseIntegrationTest { + + private static final String LC_TYPE_ID = "e62bbefe-adf5-4b1e-b3e7-43d877b0c91a"; + private static final String LC2_TYPE_ID = "308c950f-8209-4f2e-9702-0c004a9f21bc"; + private static final String DEWEY_TYPE_ID = "50524585-046b-49a1-8ca7-8d46f2a8dc19"; + + @BeforeAll + static void prepare(@Autowired RestHighLevelClient restHighLevelClient) { + setUpTenant(instances()); + await().atMost(ONE_MINUTE).pollInterval(ONE_SECOND).untilAsserted(() -> { + var searchRequest = new SearchRequest() + .source(searchSource().query(matchAllQuery()).trackTotalHits(true).from(0).size(0)) + .indices(getIndexName(SearchUtils.INSTANCE_CLASSIFICATION_RESOURCE, TENANT_ID)); + var searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); + assertThat(searchResponse.getHits().getTotalHits().value).isEqualTo(17); + }); + } + + @AfterAll + static void cleanUp() { + removeTenant(); + } + + @BeforeEach + void setUp() { + updateLcConfig(List.of(UUID.fromString(LC_TYPE_ID))); + } + + @MethodSource("classificationBrowsingDataProvider") + @DisplayName("browseByClassification_parameterized") + @ParameterizedTest(name = "[{index}] query={0}, value=''{1}'', limit={2}") + void browseByClassification_parameterized(String query, String anchor, Integer limit, + ClassificationNumberBrowseResult expected) { + var request = get(instanceClassificationBrowsePath(BrowseOptionType.LC)) + .param("query", prepareQuery(query, '"' + anchor + '"')) + .param("limit", String.valueOf(limit)); + var actual = parseResponse(doGet(request), ClassificationNumberBrowseResult.class); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void browseByClassification_allOption_browsingAroundWithPrecedingRecordsCount() { + var request = get(instanceClassificationBrowsePath(BrowseOptionType.ALL)) + .param("query", prepareQuery("number < {value} or number >= {value}", "\"292.07\"")) + .param("limit", "10") + .param("precedingRecordsCount", "2"); + var actual = parseResponse(doGet(request), ClassificationNumberBrowseResult.class); + assertThat(actual).isEqualTo(classificationBrowseResult(null, "N6679.R64 G88 2010", 17, List.of( + classificationBrowseItem("146.4", DEWEY_TYPE_ID, 2), + classificationBrowseItem("221.609", DEWEY_TYPE_ID, 1), + classificationBrowseItem("292.07", DEWEY_TYPE_ID, 1, true), + classificationBrowseItem("333.91", DEWEY_TYPE_ID, 1), + classificationBrowseItem("372.4", DEWEY_TYPE_ID, 1), + classificationBrowseItem("BJ1453 .I49 1983", LC_TYPE_ID, 1), + classificationBrowseItem("BJ1453 .I49 1983", LC2_TYPE_ID, 1), + classificationBrowseItem("HD1691 .I5 1967", LC_TYPE_ID, 1), + classificationBrowseItem("HQ536 .A565 2018", LC2_TYPE_ID, 1), + classificationBrowseItem("N6679.R64 G88 2010", LC_TYPE_ID, 1) + ))); + } + + @Test + void browseByClassification_noExactMatch() { + var request = get(instanceClassificationBrowsePath(BrowseOptionType.ALL)) + .param("query", prepareQuery("number < {value} or number >= {value}", "\"292.08\"")) + .param("limit", "3") + .param("precedingRecordsCount", "1"); + var actual = parseResponse(doGet(request), ClassificationNumberBrowseResult.class); + assertThat(actual).isEqualTo(classificationBrowseResult("292.07", "333.91", 17, List.of( + classificationBrowseItem("292.07", DEWEY_TYPE_ID, 1), + classificationBrowseItem("292.08", null, 0, true), + classificationBrowseItem("333.91", DEWEY_TYPE_ID, 1) + ))); + } + + @Test + void browseByClassification_lcOptionConfiguredWithTwoIds() { + updateLcConfig(List.of(UUID.fromString(LC_TYPE_ID), UUID.fromString(DEWEY_TYPE_ID))); + + var request = get(instanceClassificationBrowsePath(BrowseOptionType.LC)) + .param("query", prepareQuery("number < {value} or number >= {value}", "\"292.07\"")) + .param("limit", "10") + .param("precedingRecordsCount", "2"); + var actual = parseResponse(doGet(request), ClassificationNumberBrowseResult.class); + assertThat(actual).isEqualTo(classificationBrowseResult(null, "QD453 .M8 1961", 13, List.of( + classificationBrowseItem("146.4", DEWEY_TYPE_ID, 2), + classificationBrowseItem("221.609", DEWEY_TYPE_ID, 1), + classificationBrowseItem("292.07", DEWEY_TYPE_ID, 1, true), + classificationBrowseItem("333.91", DEWEY_TYPE_ID, 1), + classificationBrowseItem("372.4", DEWEY_TYPE_ID, 1), + classificationBrowseItem("BJ1453 .I49 1983", LC_TYPE_ID, 1), + classificationBrowseItem("HD1691 .I5 1967", LC_TYPE_ID, 1), + classificationBrowseItem("N6679.R64 G88 2010", LC_TYPE_ID, 1), + classificationBrowseItem("QD33 .O87", LC_TYPE_ID, 2), + classificationBrowseItem("QD453 .M8 1961", LC_TYPE_ID, 1) + ))); + } + + private static void updateLcConfig(List typeIds) { + var config = new BrowseConfig() + .id(BrowseOptionType.LC) + .shelvingAlgorithm(ShelvingOrderAlgorithmType.LC) + .typeIds(typeIds); + + doPut(browseConfigPath(BrowseType.INSTANCE_CLASSIFICATION, BrowseOptionType.LC), config); + } + + private static Stream classificationBrowsingDataProvider() { + var aroundIncludingQuery = "number < {value} or number >= {value}"; + var forwardQuery = "number > {value}"; + var forwardIncludingQuery = "number >= {value}"; + var backwardQuery = "number < {value}"; + var backwardIncludingQuery = "number <= {value}"; + + return Stream.of( + arguments(aroundIncludingQuery, "QD33 .O87", 5, classificationBrowseResult("HD1691 .I5 1967", + "SF433 .D47 2004", 8, List.of( + classificationBrowseItem("HD1691 .I5 1967", LC_TYPE_ID, 1), + classificationBrowseItem("N6679.R64 G88 2010", LC_TYPE_ID, 1), + classificationBrowseItem("QD33 .O87", LC_TYPE_ID, 2, true), + classificationBrowseItem("QD453 .M8 1961", LC_TYPE_ID, 1), + classificationBrowseItem("SF433 .D47 2004", LC_TYPE_ID, 1) + ))), + + arguments(forwardQuery, "QD33 .O87", 5, classificationBrowseResult("QD453 .M8 1961", null, 8, List.of( + classificationBrowseItem("QD453 .M8 1961", LC_TYPE_ID, 1), + classificationBrowseItem("SF433 .D47 2004", LC_TYPE_ID, 1), + classificationBrowseItem("TN800 .F4613", LC_TYPE_ID, 1), + classificationBrowseItem("TX545 .M45", LC_TYPE_ID, 1) + ))), + + arguments(forwardQuery, "Z", 10, classificationBrowseResult(null, null, 8, emptyList())), + + arguments(forwardIncludingQuery, "QD33 .O87", 5, classificationBrowseResult("QD33 .O87", null, 8, List.of( + classificationBrowseItem("QD33 .O87", LC_TYPE_ID, 2), + classificationBrowseItem("QD453 .M8 1961", LC_TYPE_ID, 1), + classificationBrowseItem("SF433 .D47 2004", LC_TYPE_ID, 1), + classificationBrowseItem("TN800 .F4613", LC_TYPE_ID, 1), + classificationBrowseItem("TX545 .M45", LC_TYPE_ID, 1) + ))), + + arguments(backwardQuery, "QD33 .O87", 5, classificationBrowseResult(null, "N6679.R64 G88 2010", 8, List.of( + classificationBrowseItem("BJ1453 .I49 1983", LC_TYPE_ID, 1), + classificationBrowseItem("HD1691 .I5 1967", LC_TYPE_ID, 1), + classificationBrowseItem("N6679.R64 G88 2010", LC_TYPE_ID, 1) + ))), + + arguments(backwardQuery, "A", 10, classificationBrowseResult(null, null, 8, emptyList())), + + arguments(backwardIncludingQuery, "QD33 .O87", 5, classificationBrowseResult(null, "QD33 .O87", 8, List.of( + classificationBrowseItem("BJ1453 .I49 1983", LC_TYPE_ID, 1), + classificationBrowseItem("HD1691 .I5 1967", LC_TYPE_ID, 1), + classificationBrowseItem("N6679.R64 G88 2010", LC_TYPE_ID, 1), + classificationBrowseItem("QD33 .O87", LC_TYPE_ID, 2) + ))) + ); + } + + private static Instance[] instances() { + return classificationBrowseInstanceData().stream() + .map(BrowseClassificationIT::instance) + .toArray(Instance[]::new); + } + + @SuppressWarnings("unchecked") + private static Instance instance(List data) { + return new Instance() + .id(randomId()) + .title((String) data.get(0)) + .classifications(((List>) data.get(1)).stream() + .map(pair -> new InstanceClassificationsInner() + .classificationNumber(String.valueOf(pair.getFirst())) + .classificationTypeId(String.valueOf(pair.getSecond()))) + .collect(Collectors.toList())) + .staffSuppress(false) + .discoverySuppress(false) + .holdings(emptyList()); + } + + private static List> classificationBrowseInstanceData() { + return List.of( + List.of("instance #01", List.of(pair("BJ1453 .I49 1983", LC_TYPE_ID), pair("HD1691 .I5 1967", LC_TYPE_ID))), + List.of("instance #02", List.of(pair("BJ1453 .I49 1983", LC2_TYPE_ID))), + List.of("instance #03", List.of(pair("HQ536 .A565 2018", LC2_TYPE_ID), pair("N6679.R64 G88 2010", LC_TYPE_ID))), + List.of("instance #04", List.of(pair("QD33 .O87", LC_TYPE_ID))), + List.of("instance #05", List.of(pair("QD453 .M8 1961", LC_TYPE_ID), pair("146.4", DEWEY_TYPE_ID))), + List.of("instance #06", List.of(pair("SF433 .D47 2004", LC_TYPE_ID), pair("TX545 .M45", LC_TYPE_ID))), + List.of("instance #07", List.of(pair("221.609", DEWEY_TYPE_ID), pair("SF991 .M94", LC2_TYPE_ID))), + List.of("instance #08", List.of(pair("TN800 .F4613", LC_TYPE_ID))), + List.of("instance #09", List.of(pair("292.07", DEWEY_TYPE_ID), pair("333.91", DEWEY_TYPE_ID), + pair("372.4", DEWEY_TYPE_ID))), + List.of("instance #10", List.of(pair("146.4", DEWEY_TYPE_ID), pair("QD33 .O87", LC_TYPE_ID), + pair("SF991 .M94", null))) + ); + } +} diff --git a/src/test/java/org/folio/search/controller/BrowseControllerTest.java b/src/test/java/org/folio/search/controller/BrowseControllerTest.java index 25811d4df..d4cc8844c 100644 --- a/src/test/java/org/folio/search/controller/BrowseControllerTest.java +++ b/src/test/java/org/folio/search/controller/BrowseControllerTest.java @@ -29,6 +29,7 @@ import org.folio.search.model.service.BrowseRequest; import org.folio.search.service.browse.AuthorityBrowseService; import org.folio.search.service.browse.CallNumberBrowseService; +import org.folio.search.service.browse.ClassificationBrowseService; import org.folio.search.service.browse.ContributorBrowseService; import org.folio.search.service.browse.SubjectBrowseService; import org.folio.search.service.consortium.TenantProvider; @@ -60,6 +61,8 @@ class BrowseControllerTest { @MockBean private ContributorBrowseService contributorBrowseService; @MockBean + private ClassificationBrowseService classificationBrowseService; + @MockBean private TenantProvider tenantProvider; @Mock private Map, SearchResponsePostProcessor> searchResponsePostProcessors = Collections.emptyMap(); diff --git a/src/test/java/org/folio/search/controller/IndexingInstanceClassificationIT.java b/src/test/java/org/folio/search/controller/IndexingInstanceClassificationIT.java index 4d9f46197..03a64ebbe 100644 --- a/src/test/java/org/folio/search/controller/IndexingInstanceClassificationIT.java +++ b/src/test/java/org/folio/search/controller/IndexingInstanceClassificationIT.java @@ -5,7 +5,6 @@ import static org.awaitility.Durations.ONE_HUNDRED_MILLISECONDS; import static org.awaitility.Durations.ONE_MINUTE; import static org.folio.search.model.client.CqlQuery.exactMatchAny; -import static org.folio.search.model.client.CqlQueryParam.ID; import static org.folio.search.support.base.ApiEndpoints.instanceSearchPath; import static org.folio.search.utils.SearchUtils.getIndexName; import static org.folio.search.utils.TestConstants.TENANT_ID; @@ -63,13 +62,19 @@ void shouldIndexInstanceClassification_createNewDocument() { var instance2 = new Instance().id(instanceId2).addClassificationsItem(classification); inventoryApi.createInstance(TENANT_ID, instance1); inventoryApi.createInstance(TENANT_ID, instance2); - assertCountByQuery(instanceSearchPath(), ID, List.of(instanceId1, instanceId2), 2); + assertCountByIds(instanceSearchPath(), List.of(instanceId1, instanceId2), 2); await(() -> assertThat(fetchAllInstanceClassifications(TENANT_ID).getHits().getHits()).hasSize(1)); var hits = fetchAllInstanceClassifications(TENANT_ID).getHits().getHits(); var sourceAsMap = hits[0].getSourceAsMap(); assertThat(sourceAsMap) - .contains(entry("number", number), entry("typeId", lcTypeId), entry("shelvingOrder", "N 3123")); + .contains( + entry("number", number), + entry("typeId", lcTypeId), + entry("defaultShelvingOrder", "N123"), + entry("deweyShelvingOrder", "N 3123"), + entry("lcShelvingOrder", "N 3123") + ); @SuppressWarnings("unchecked") var instances = (List>) sourceAsMap.get("instances"); @@ -86,14 +91,14 @@ void shouldIndexInstanceClassification_deleteDocument() { var classification = new InstanceClassificationsInner().classificationNumber("N123").classificationTypeId("type1"); var instance = new Instance().id(instanceId).addClassificationsItem(classification); inventoryApi.createInstance(TENANT_ID, instance); - assertCountByQuery(instanceSearchPath(), ID, List.of(instanceId), 1); + assertCountByIds(instanceSearchPath(), List.of(instanceId), 1); await(() -> assertThat(fetchAllInstanceClassifications(TENANT_ID).getHits().getHits()).hasSize(1)); inventoryApi.updateInstance(TENANT_ID, instance.classifications(null)); await(() -> assertThat(fetchAllInstanceClassifications(TENANT_ID).getHits().getHits()).isEmpty()); } - private static void assertCountByQuery(String path, CqlQueryParam param, List ids, int expected) { - var query = exactMatchAny(param, ids).toString(); + private static void assertCountByIds(String path, List ids, int expected) { + var query = exactMatchAny(CqlQueryParam.ID, ids).toString(); await(() -> doSearch(path, query).andExpect(jsonPath("$.totalRecords", is(expected)))); } diff --git a/src/test/java/org/folio/search/integration/ClassificationTypeHelperTest.java b/src/test/java/org/folio/search/integration/ClassificationTypeHelperTest.java deleted file mode 100644 index c9a919110..000000000 --- a/src/test/java/org/folio/search/integration/ClassificationTypeHelperTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.folio.search.integration; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.when; - -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import org.folio.search.configuration.properties.SearchConfigurationProperties; -import org.folio.search.model.types.ClassificationType; -import org.folio.spring.testing.type.UnitTest; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -@UnitTest -@ExtendWith(MockitoExtension.class) -class ClassificationTypeHelperTest { - - private @Mock ReferenceDataService referenceDataService; - private @Mock SearchConfigurationProperties configurationProperties; - private @InjectMocks ClassificationTypeHelper typeHelper; - - @Test - void getClassificationTypeMap_ReturnsEmptyMap_WhenBrowseClassificationTypesIsEmpty() { - // Arrange - when(configurationProperties.getBrowseClassificationTypes()).thenReturn(Collections.emptyMap()); - - // Act - Map result = typeHelper.getClassificationTypeMap(); - - // Assert - assertEquals(Collections.emptyMap(), result); - } - - @Test - void getClassificationTypeMap_ReturnsCorrectMap_WhenBrowseClassificationTypesIsNotEmpty() { - // Arrange - Map browseClassificationTypes = new HashMap<>(); - browseClassificationTypes.put(ClassificationType.NLM, new String[] {"nlm_type"}); - browseClassificationTypes.put(ClassificationType.LC, new String[] {"lc_type", "lc_additional"}); - - when(configurationProperties.getBrowseClassificationTypes()).thenReturn(browseClassificationTypes); - when(referenceDataService.fetchReferenceData(any(), any(), eq(List.of("nlm_type")))) - .thenReturn(Collections.singleton("nlmId")); - when(referenceDataService.fetchReferenceData(any(), any(), eq(List.of("lc_type", "lc_additional")))) - .thenReturn(Set.of("lcId1", "lcId2")); - - // Act - Map result = typeHelper.getClassificationTypeMap(); - - // Assert - assertEquals(3, result.size()); - assertEquals(ClassificationType.NLM, result.get("nlmId")); - assertEquals(ClassificationType.LC, result.get("lcId1")); - assertEquals(ClassificationType.LC, result.get("lcId2")); - } - -} diff --git a/src/test/java/org/folio/search/service/ResourceServiceTest.java b/src/test/java/org/folio/search/service/ResourceServiceTest.java index 6f1c5d783..efec6930e 100644 --- a/src/test/java/org/folio/search/service/ResourceServiceTest.java +++ b/src/test/java/org/folio/search/service/ResourceServiceTest.java @@ -49,6 +49,7 @@ import org.folio.search.service.consortium.ConsortiumTenantExecutor; import org.folio.search.service.consortium.ConsortiumTenantService; import org.folio.search.service.converter.MultiTenantSearchDocumentConverter; +import org.folio.search.service.converter.preprocessor.InstanceEventPreProcessor; import org.folio.search.service.metadata.ResourceDescriptionService; import org.folio.search.utils.SearchUtils; import org.folio.spring.testing.type.UnitTest; @@ -89,6 +90,8 @@ class ResourceServiceTest { private IndexNameProvider indexNameProvider; @Mock private Map resourceRepositoryBeans; + @Mock + private InstanceEventPreProcessor instanceEventPreProcessor; @InjectMocks private ResourceService indexService; @@ -103,6 +106,7 @@ public void setUp() { .thenAnswer(invocation -> ((Callable) invocation.getArgument(1)).call()); lenient().when(indexNameProvider.getIndexName(any(ResourceEvent.class))) .thenAnswer(invocation -> SearchUtils.getIndexName((ResourceEvent) invocation.getArgument(0))); + lenient().when(instanceEventPreProcessor.preProcess(any())).thenReturn(emptyList()); } @Test diff --git a/src/test/java/org/folio/search/service/config/BrowseConfigServiceTest.java b/src/test/java/org/folio/search/service/config/BrowseConfigServiceTest.java index 17a3b220d..fe774d930 100644 --- a/src/test/java/org/folio/search/service/config/BrowseConfigServiceTest.java +++ b/src/test/java/org/folio/search/service/config/BrowseConfigServiceTest.java @@ -7,10 +7,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; import java.util.List; +import java.util.Optional; import org.folio.search.converter.BrowseConfigMapper; import org.folio.search.domain.dto.BrowseConfig; import org.folio.search.domain.dto.BrowseConfigCollection; @@ -21,6 +23,7 @@ import org.folio.search.model.config.BrowseConfigEntity; import org.folio.search.model.config.BrowseConfigId; import org.folio.search.repository.BrowseConfigEntityRepository; +import org.folio.spring.testing.type.UnitTest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -28,15 +31,14 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +@UnitTest @ExtendWith(MockitoExtension.class) class BrowseConfigServiceTest { @Mock private BrowseConfigEntityRepository repository; - @Mock private BrowseConfigMapper mapper; - @InjectMocks private BrowseConfigService service; @@ -64,6 +66,30 @@ void shouldGetConfigs() { verify(repository).findByConfigId_BrowseType(type.getValue()); } + @Test + void shouldGetConfig() { + var browseConfigId = new BrowseConfigId("instance-classification", "lc"); + var configEntity = getEntity(); + given(repository.findById(browseConfigId)).willReturn(Optional.of(configEntity)); + given(mapper.convert(configEntity)).willReturn(config); + + var result = service.getConfig(type, configId); + + assertEquals(config, result); + verify(repository).findById(browseConfigId); + } + + @Test + void shouldThrowExceptionIfConfigNotExists() { + given(repository.findById(any())).willReturn(Optional.empty()); + + var exception = assertThrows(IllegalStateException.class, () -> service.getConfig(type, configId)); + + String expectedMessage = String.format("Config for %s type %s must be present in database", type.getValue(), + configId.getValue()); + assertTrue(exception.getMessage().contains(expectedMessage)); + } + @Test void shouldUpsertConfigWhenConfigIdMatches() { var entity = getEntity(); diff --git a/src/test/java/org/folio/search/service/consortium/BrowseConfigServiceDecoratorTest.java b/src/test/java/org/folio/search/service/consortium/BrowseConfigServiceDecoratorTest.java index 339bce9a4..840431e9f 100644 --- a/src/test/java/org/folio/search/service/consortium/BrowseConfigServiceDecoratorTest.java +++ b/src/test/java/org/folio/search/service/consortium/BrowseConfigServiceDecoratorTest.java @@ -39,6 +39,19 @@ void getConfigs() { verify(consortiumTenantExecutor).execute(any()); } + @Test + void getConfig() { + var expected = new BrowseConfig(); + when(service.getConfig(INSTANCE_CLASSIFICATION, BrowseOptionType.LC)).thenReturn(expected); + mockExecutor(consortiumTenantExecutor); + + var actual = decorator.getConfig(INSTANCE_CLASSIFICATION, BrowseOptionType.LC); + + assertThat(actual).isEqualTo(expected); + verify(service).getConfig(INSTANCE_CLASSIFICATION, BrowseOptionType.LC); + verify(consortiumTenantExecutor).execute(any()); + } + @Test void upsertConfig() { var config = new BrowseConfig(); diff --git a/src/test/java/org/folio/search/service/converter/preprocessor/InstanceEventPreProcessorTest.java b/src/test/java/org/folio/search/service/converter/preprocessor/InstanceEventPreProcessorTest.java index 0e1176867..0889d37cf 100644 --- a/src/test/java/org/folio/search/service/converter/preprocessor/InstanceEventPreProcessorTest.java +++ b/src/test/java/org/folio/search/service/converter/preprocessor/InstanceEventPreProcessorTest.java @@ -105,8 +105,7 @@ void preProcess_FeatureIsDisabled_ShouldNotProcessClassifications() { // Assert assertThat(resourceEvents) - .hasSize(1) - .containsExactly(resourceEvent); + .isEmpty(); verifyNoInteractions(instanceClassificationRepository); } @@ -124,8 +123,7 @@ void preProcess_NoChangeToClassifications_ShouldNotProcessClassifications() { // Assert assertThat(resourceEvents) - .hasSize(1) - .containsExactly(resourceEvent); + .isEmpty(); verifyNoInteractions(instanceClassificationRepository); } @@ -148,9 +146,7 @@ void preProcess_CreateEvent_ShouldProcessClassifications() { // Assert assertThat(resourceEvents) - .hasSize(3) - .startsWith(resourceEvent) - .elements(1, 2) + .hasSize(2) .allSatisfy(event -> assertThat(event) .extracting(ResourceEvent::getResourceName, ResourceEvent::getTenant, ResourceEvent::getType) .containsExactly(INSTANCE_CLASSIFICATION_RESOURCE, TENANT_ID, CREATE)) @@ -183,9 +179,7 @@ void preProcess_DeleteEvent_ShouldProcessClassifications() { // Assert assertThat(resourceEvents) - .hasSize(3) - .startsWith(resourceEvent) - .elements(1, 2) + .hasSize(2) .allSatisfy(event -> assertThat(event) .extracting(ResourceEvent::getResourceName, ResourceEvent::getTenant) .containsExactly(INSTANCE_CLASSIFICATION_RESOURCE, TENANT_ID)) @@ -221,9 +215,7 @@ void preProcess_UpdateEvent_ShouldProcessClassifications() { // Assert assertThat(resourceEvents) - .hasSize(4) - .startsWith(resourceEvent) - .elements(1, 2, 3) + .hasSize(3) .allSatisfy(event -> assertThat(event) .extracting(ResourceEvent::getResourceName, ResourceEvent::getTenant) .containsExactly(INSTANCE_CLASSIFICATION_RESOURCE, TENANT_ID)) @@ -260,8 +252,7 @@ void preProcess_AnyEventInConsortium_ShouldProcessClassificationsAndSetShared(Re // Assert assertThat(resourceEvents) - .hasSize(2) - .startsWith(resourceEvent); + .hasSize(1); verify(instanceClassificationRepository).saveAll(createCaptor.capture()); diff --git a/src/test/java/org/folio/search/service/setter/ClassificationShelvingOrderProcessorTest.java b/src/test/java/org/folio/search/service/setter/ClassificationShelvingOrderProcessorTest.java deleted file mode 100644 index 23ec708d4..000000000 --- a/src/test/java/org/folio/search/service/setter/ClassificationShelvingOrderProcessorTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.folio.search.service.setter; - -import static java.util.Collections.emptySet; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.when; - -import java.util.HashMap; -import java.util.Map; -import org.folio.search.integration.ClassificationTypeHelper; -import org.folio.search.model.index.ClassificationResource; -import org.folio.search.model.types.ClassificationType; -import org.folio.spring.testing.type.UnitTest; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -@UnitTest -@ExtendWith(MockitoExtension.class) -class ClassificationShelvingOrderProcessorTest { - - private @Mock ClassificationTypeHelper classificationTypeHelper; - - private @InjectMocks ClassificationShelvingOrderProcessor processor; - - @Test - void getFieldValue_ReturnsCalculatedShelfKey_ForLc() { - // Arrange - var resource = new ClassificationResource("LC", "12345", "NL 12.4N", emptySet()); - - when(classificationTypeHelper.getClassificationTypeMap()).thenReturn(Map.of("12345", ClassificationType.LC)); - - // Act - String result = processor.getFieldValue(resource); - - // Assert - assertEquals("NL 212.4 _N", result); - } - - @Test - void getFieldValue_ReturnsNormalizedShelfKey_ForDefault() { - // Arrange - var resource = new ClassificationResource("Default", "1", "S df123.dd", emptySet()); - - when(classificationTypeHelper.getClassificationTypeMap()).thenReturn(new HashMap<>()); - - // Act - String result = processor.getFieldValue(resource); - - // Assert - assertEquals("S DF123.DD", result); - } -} diff --git a/src/test/java/org/folio/search/service/setter/classification/ClassificationShelvingOrderFieldProcessorTest.java b/src/test/java/org/folio/search/service/setter/classification/ClassificationShelvingOrderFieldProcessorTest.java new file mode 100644 index 000000000..b9b460957 --- /dev/null +++ b/src/test/java/org/folio/search/service/setter/classification/ClassificationShelvingOrderFieldProcessorTest.java @@ -0,0 +1,54 @@ +package org.folio.search.service.setter.classification; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.folio.search.domain.dto.ShelvingOrderAlgorithmType.DEFAULT; +import static org.folio.search.domain.dto.ShelvingOrderAlgorithmType.DEWEY; +import static org.folio.search.domain.dto.ShelvingOrderAlgorithmType.LC; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.stream.Stream; +import org.folio.search.domain.dto.ShelvingOrderAlgorithmType; +import org.folio.search.model.index.ClassificationResource; +import org.folio.search.utils.ShelvingOrderCalculationHelper; +import org.folio.spring.testing.type.UnitTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; + +@UnitTest +class ClassificationShelvingOrderFieldProcessorTest { + + private static final String INPUT = "TestNum"; + private static final String OUTPUT = "ResultNum"; + private ClassificationResource eventBody; + + @BeforeEach + void setUp() { + eventBody = mock(ClassificationResource.class); + when(eventBody.number()).thenReturn(INPUT); + } + + @MethodSource("testData") + @ParameterizedTest + void testDefaultClassificationShelvingOrderFieldProcessor(ClassificationShelvingOrderFieldProcessor processor, + ShelvingOrderAlgorithmType algorithmType) { + try (var helper = Mockito.mockStatic(ShelvingOrderCalculationHelper.class)) { + helper.when(() -> ShelvingOrderCalculationHelper.calculate(INPUT, algorithmType)).thenReturn(OUTPUT); + + var fieldValue = processor.getFieldValue(eventBody); + assertThat(fieldValue).isEqualTo(OUTPUT); + } + } + + public static Stream testData() { + return Stream.of( + Arguments.arguments(new DefaultClassificationShelvingOrderFieldProcessor(), DEFAULT), + Arguments.arguments(new DeweyClassificationShelvingOrderFieldProcessor(), DEWEY), + Arguments.arguments(new LcClassificationShelvingOrderFieldProcessor(), LC) + ); + } + +} diff --git a/src/test/java/org/folio/search/support/base/ApiEndpoints.java b/src/test/java/org/folio/search/support/base/ApiEndpoints.java index fbf6b50d7..cf55339b4 100644 --- a/src/test/java/org/folio/search/support/base/ApiEndpoints.java +++ b/src/test/java/org/folio/search/support/base/ApiEndpoints.java @@ -30,6 +30,10 @@ public static String instanceContributorBrowsePath() { return "/browse/contributors/instances"; } + public static String instanceClassificationBrowsePath(BrowseOptionType optionType) { + return "/browse/classification-numbers/" + optionType.getValue() + "/instances"; + } + public static String authorityBrowsePath() { return "/browse/authorities"; } diff --git a/src/test/java/org/folio/search/support/base/BaseIntegrationTest.java b/src/test/java/org/folio/search/support/base/BaseIntegrationTest.java index 53fe27d5e..1054833e9 100644 --- a/src/test/java/org/folio/search/support/base/BaseIntegrationTest.java +++ b/src/test/java/org/folio/search/support/base/BaseIntegrationTest.java @@ -50,6 +50,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.extension.RegisterExtension; +import org.opensearch.client.RestHighLevelClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; @@ -74,6 +75,7 @@ public abstract class BaseIntegrationTest { protected static InventoryApi inventoryApi; protected static KafkaTemplate kafkaTemplate; protected static OkapiConfiguration okapi; + protected static RestHighLevelClient elasticClient; @RegisterExtension static OkapiExtension okapiExtension = @@ -82,11 +84,13 @@ public abstract class BaseIntegrationTest { @BeforeAll static void setUpDefaultTenant( @Autowired MockMvc mockMvc, - @Autowired KafkaTemplate kafkaTemplate) { + @Autowired KafkaTemplate kafkaTemplate, + @Autowired RestHighLevelClient restHighLevelClient) { setEnvProperty("folio-test"); BaseIntegrationTest.mockMvc = mockMvc; BaseIntegrationTest.kafkaTemplate = kafkaTemplate; BaseIntegrationTest.inventoryApi = new InventoryApi(kafkaTemplate); + BaseIntegrationTest.elasticClient = restHighLevelClient; } @BeforeAll diff --git a/src/test/java/org/folio/search/utils/ShelvingOrderCalculationHelperTest.java b/src/test/java/org/folio/search/utils/ShelvingOrderCalculationHelperTest.java new file mode 100644 index 000000000..4031482c6 --- /dev/null +++ b/src/test/java/org/folio/search/utils/ShelvingOrderCalculationHelperTest.java @@ -0,0 +1,59 @@ +package org.folio.search.utils; + +import static org.folio.search.utils.ShelvingOrderCalculationHelper.calculate; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.folio.search.domain.dto.ShelvingOrderAlgorithmType; +import org.folio.spring.testing.type.UnitTest; +import org.junit.jupiter.api.Test; + +@UnitTest +class ShelvingOrderCalculationHelperTest { + + @Test + void shouldCalculateLcNumber() { + String input = "HD1691 .I5 1967"; + String expectedShelfKey = "HD 41691 I5 41967"; + + String result = calculate(input, ShelvingOrderAlgorithmType.LC); + + assertEquals(expectedShelfKey, result); + } + + @Test + void shouldCalculateDeweyNumber() { + String input = "302.55"; + String expectedShelfKey = "3302.55"; + + String result = calculate(input, ShelvingOrderAlgorithmType.DEWEY); + + assertEquals(expectedShelfKey, result); + } + + @Test + void shouldCalculateDefaultNumber() { + String input = "hd1691 ^I5 1967"; + String expectedShelfKey = "HD1691 ^I5 1967"; + + String result = calculate(input, ShelvingOrderAlgorithmType.DEFAULT); + + assertEquals(expectedShelfKey, result); + } + + @Test + void shouldThrowExceptionOnNullInput() { + Exception exception = assertThrows(NullPointerException.class, + () -> calculate(null, ShelvingOrderAlgorithmType.DEFAULT)); + + assertEquals("input is marked non-null but is null", exception.getMessage()); + } + + @Test + void shouldThrowExceptionOnNullAlgorithmType() { + Exception exception = assertThrows(NullPointerException.class, + () -> calculate("Valid Input", null)); + + assertEquals("algorithmType is marked non-null but is null", exception.getMessage()); + } +} diff --git a/src/test/java/org/folio/search/utils/TestUtils.java b/src/test/java/org/folio/search/utils/TestUtils.java index b9f36da0a..eaf1e8ff1 100644 --- a/src/test/java/org/folio/search/utils/TestUtils.java +++ b/src/test/java/org/folio/search/utils/TestUtils.java @@ -55,12 +55,13 @@ import lombok.Data; import lombok.NoArgsConstructor; import lombok.SneakyThrows; -import org.folio.search.cql.EffectiveShelvingOrderTermProcessor; import org.folio.search.cql.SuDocCallNumber; import org.folio.search.domain.dto.Authority; import org.folio.search.domain.dto.AuthorityBrowseItem; import org.folio.search.domain.dto.CallNumberBrowseItem; import org.folio.search.domain.dto.CallNumberBrowseResult; +import org.folio.search.domain.dto.ClassificationNumberBrowseItem; +import org.folio.search.domain.dto.ClassificationNumberBrowseResult; import org.folio.search.domain.dto.Facet; import org.folio.search.domain.dto.FacetItem; import org.folio.search.domain.dto.FacetResult; @@ -116,14 +117,12 @@ public class TestUtils { public static final NamedXContentRegistry NAMED_XCONTENT_REGISTRY = new NamedXContentRegistry(TestUtils.elasticsearchClientNamedContentRegistryEntries()); - private static final EffectiveShelvingOrderTermProcessor SHELVING_ORDER_TERM_PROCESSOR = - new EffectiveShelvingOrderTermProcessor(); - - private static final Map> SHERVING_ORDER_GENERATORS = Map.of( - NLM, callNumber -> new NlmCallNumber(callNumber), - LC, callNumber -> new LCCallNumber(callNumber), - DEWEY, callNumber -> new DeweyCallNumber(callNumber), - SUDOC, callNumber -> new SuDocCallNumber(callNumber) + + private static final Map> SHELVING_ORDER_GENERATORS = Map.of( + NLM, NlmCallNumber::new, + LC, LCCallNumber::new, + DEWEY, DeweyCallNumber::new, + SUDOC, SuDocCallNumber::new ); @SneakyThrows @@ -211,9 +210,8 @@ public static CallNumberBrowseItem cnBrowseItem(Instance instance, String callNu var callNumberType = Optional.ofNullable(instance.getItems().get(itemNumberForCallNumberType)) .flatMap(item -> Optional.ofNullable(item.getEffectiveCallNumberComponents()) .map(ItemEffectiveCallNumberComponents::getTypeId)); - var shelfKey = callNumberType.isEmpty() - ? getShelfKeyFromCallNumber(callNumber) - : getShelfKeyFromCallNumber(callNumber, callNumberType.get()); + var shelfKey = callNumberType.map(s -> getShelfKeyFromCallNumber(callNumber, s)) + .orElseGet(() -> getShelfKeyFromCallNumber(callNumber)); return new CallNumberBrowseItem().fullCallNumber(callNumber).shelfKey(shelfKey) .instance(instance).totalRecords(1).isAnchor(isAnchor); } @@ -252,7 +250,7 @@ public static String getShelfKeyFromCallNumber(String callNumber) { public static String getShelfKeyFromCallNumber(String callNumber, String typeId) { var callNumberType = CallNumberType.fromId(typeId); - return callNumberType.flatMap(numberType -> Optional.ofNullable(SHERVING_ORDER_GENERATORS.get(numberType)) + return callNumberType.flatMap(numberType -> Optional.ofNullable(SHELVING_ORDER_GENERATORS.get(numberType)) .map(generator -> generator.apply(callNumber)) .filter(AbstractCallNumber::isValid) .map(AbstractCallNumber::getShelfKey)) @@ -288,6 +286,29 @@ public static SubjectBrowseItem subjectBrowseItem(String subject) { return new SubjectBrowseItem().value(subject); } + public static ClassificationNumberBrowseResult classificationBrowseResult( + String prev, String next, int totalRecords, List items) { + return new ClassificationNumberBrowseResult() + .prev(prev) + .next(next) + .items(items) + .totalRecords(totalRecords); + } + + public static ClassificationNumberBrowseItem classificationBrowseItem(String number, String typeId, + Integer totalRecords) { + return classificationBrowseItem(number, typeId, totalRecords, null); + } + + public static ClassificationNumberBrowseItem classificationBrowseItem(String number, String typeId, + Integer totalRecords, Boolean isAnchor) { + return new ClassificationNumberBrowseItem() + .classificationNumber(number) + .classificationTypeId(typeId) + .totalRecords(totalRecords) + .isAnchor(isAnchor); + } + public static InstanceContributorBrowseItem contributorBrowseItem(Integer totalRecords, String name, String nameTypeId, String authorityId, String... typeIds) { @@ -306,7 +327,7 @@ public static InstanceContributorBrowseItem contributorBrowseItem(Integer totalR .name(name) .contributorNameTypeId(nameTypeId) .authorityId(authorityId) - .contributorTypeId(Arrays.asList(typeIds)) + .contributorTypeId(asList(typeIds)) .totalRecords(totalRecords) .isAnchor(isAnchor); } diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 618ac042b..26561fc7d 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -51,11 +51,6 @@ folio: browse-cn-intermediate-values: true browse-cn-intermediate-remove-duplicates: true browse-classifications: true - browse-classification-types: - lc: LC,LC (local) - nlm: NLM - dewey: Dewey,Additional Dewey - sudoc: SUDOC indexing: instance-subjects: retry-attempts: 3