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/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/browse/ClassificationBrowseService.java b/src/main/java/org/folio/search/service/browse/ClassificationBrowseService.java new file mode 100644 index 000000000..034ff6370 --- /dev/null +++ b/src/main/java/org/folio/search/service/browse/ClassificationBrowseService.java @@ -0,0 +1,127 @@ +package org.folio.search.service.browse; + +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.config.BrowseConfigService; +import org.folio.search.service.consortium.ConsortiumSearchHelper; +import org.folio.search.utils.ShelvingOrderCalculationHelper; +import org.opensearch.index.query.QueryBuilder; +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 BrowseConfigService 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 termQueryBuilder = 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), + req.getResource()); + + return searchSource().query(query) + .searchAfter(new Object[] {normalizedAnchor}) + .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) { + var typeIds = config.getTypeIds(); + if (config.getTypeIds().isEmpty() && ctx.getFilters().isEmpty()) { + 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)); + } + boolQueryMain.must(boolQuery); + } + if (!ctx.getFilters().isEmpty()) { + ctx.getFilters().forEach(boolQueryMain::filter); + } + 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/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/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..97b87cf6d --- /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.Function; +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 Function numberFunction; + + protected ClassificationShelvingOrderFieldProcessor(@NonNull Function 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..058b0b6d8 --- /dev/null +++ b/src/main/java/org/folio/search/utils/ShelvingOrderCalculationHelper.java @@ -0,0 +1,19 @@ +package org.folio.search.utils; + +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(); + case DEWEY -> new DeweyCallNumber(input).getShelfKey(); + case DEFAULT -> CallNumberUtils.normalizeEffectiveShelvingOrder(input); + }; + } +} 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_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/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/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/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