From 26d5c800b2acce27d277763ae9fd40bee53026ad Mon Sep 17 00:00:00 2001 From: pkjacob <121239864+pkjacob@users.noreply.github.com> Date: Wed, 5 Jun 2024 11:24:06 -0400 Subject: [PATCH] feat(bibframe-search): Implement bibframe search functionality Implement bibframe search functionality - Create new open-search index for bibframe - Consume events from kafka topic "search.bibframe" - Expose new API - "GET /search/bibframe" Closes: MSEARCH-781 --- NEWS.md | 1 + README.md | 2 + descriptors/ModuleDescriptor-template.json | 14 + .../search/controller/SearchController.java | 22 ++ .../search/cql/LccnSearchTermProcessor.java | 7 +- .../integration/KafkaMessageListener.java | 17 ++ .../service/lccn/DefaultLccnNormalizer.java | 21 ++ .../search/service/lccn/LccnNormalizer.java | 7 + .../lccn/LccnNormalizerStructureB.java | 67 ++++ .../service/setter/AbstractLccnProcessor.java | 10 +- .../authority/LccnAuthorityProcessor.java | 6 +- .../BibframeContributorProcessor.java | 34 +++ .../bibframe/BibframeIsbnProcessor.java | 38 +++ .../bibframe/BibframeLccnProcessor.java | 42 +++ .../bibframe/BibframeSortTitleProcessor.java | 31 ++ .../bibframe/BibframeTitleProcessor.java | 34 +++ .../instance/LccnInstanceProcessor.java | 6 +- .../folio/search/utils/KafkaConstants.java | 1 + .../org/folio/search/utils/SearchUtils.java | 15 +- src/main/resources/application.yml | 4 + .../elasticsearch/index/bibframe.json | 68 +++++ src/main/resources/model/bibframe.json | 156 ++++++++++ .../examples/result/bibframeSearchResult.yaml | 38 +++ .../resources/swagger.api/mod-search.yaml | 3 + .../parameters/bibframe-limit-param.yaml | 8 + .../search-bibframe/search-bibframe.yaml | 25 ++ .../schemas/dto/bibframe/bibframe.yaml | 152 ++++++++++ .../response/bibframeSearchResult.yaml | 20 ++ .../search/controller/SearchBibframeIT.java | 286 ++++++++++++++++++ .../cql/LccnSearchTermProcessorTest.java | 19 +- .../folio/search/sample/SampleBibframe.java | 26 ++ .../lccn/DefaultLccnNormalizerTest.java | 21 ++ .../lccn/LccnNormalizerStructurebTest.java | 65 ++++ .../authority/LccnAuthorityProcessorTest.java | 10 +- .../instance/LccnInstanceProcessorTest.java | 10 +- .../search/support/base/ApiEndpoints.java | 4 + .../support/base/BaseIntegrationTest.java | 15 +- .../folio/search/utils/SearchUtilsTest.java | 9 - .../org/folio/search/utils/TestConstants.java | 5 + src/test/resources/application.yml | 7 + .../resources/samples/bibframe/bibframe.json | 131 ++++++++ .../resources/samples/bibframe/bibframe2.json | 40 +++ 42 files changed, 1457 insertions(+), 40 deletions(-) create mode 100644 src/main/java/org/folio/search/service/lccn/DefaultLccnNormalizer.java create mode 100644 src/main/java/org/folio/search/service/lccn/LccnNormalizer.java create mode 100644 src/main/java/org/folio/search/service/lccn/LccnNormalizerStructureB.java create mode 100644 src/main/java/org/folio/search/service/setter/bibframe/BibframeContributorProcessor.java create mode 100644 src/main/java/org/folio/search/service/setter/bibframe/BibframeIsbnProcessor.java create mode 100644 src/main/java/org/folio/search/service/setter/bibframe/BibframeLccnProcessor.java create mode 100644 src/main/java/org/folio/search/service/setter/bibframe/BibframeSortTitleProcessor.java create mode 100644 src/main/java/org/folio/search/service/setter/bibframe/BibframeTitleProcessor.java create mode 100644 src/main/resources/elasticsearch/index/bibframe.json create mode 100644 src/main/resources/model/bibframe.json create mode 100644 src/main/resources/swagger.api/examples/result/bibframeSearchResult.yaml create mode 100644 src/main/resources/swagger.api/parameters/bibframe-limit-param.yaml create mode 100644 src/main/resources/swagger.api/paths/search-bibframe/search-bibframe.yaml create mode 100644 src/main/resources/swagger.api/schemas/dto/bibframe/bibframe.yaml create mode 100644 src/main/resources/swagger.api/schemas/response/bibframeSearchResult.yaml create mode 100644 src/test/java/org/folio/search/controller/SearchBibframeIT.java create mode 100644 src/test/java/org/folio/search/sample/SampleBibframe.java create mode 100644 src/test/java/org/folio/search/service/lccn/DefaultLccnNormalizerTest.java create mode 100644 src/test/java/org/folio/search/service/lccn/LccnNormalizerStructurebTest.java create mode 100644 src/test/resources/samples/bibframe/bibframe.json create mode 100644 src/test/resources/samples/bibframe/bibframe2.json diff --git a/NEWS.md b/NEWS.md index c6de0537b..f5b2883f1 100644 --- a/NEWS.md +++ b/NEWS.md @@ -18,6 +18,7 @@ * Return Unified List of Inventory Locations in a Consortium ([MSEARCH-681](https://folio-org.atlassian.net/browse/MSEARCH-681)) * Remove ability to match on LCCN searches without a prefix ([MSEARCH-752](https://folio-org.atlassian.net/browse/MSEARCH-752)) * Search consolidated items/holdings data in consortium ([MSEARCH-759](https://folio-org.atlassian.net/browse/MSEARCH-759)) +* Create bibframe index and process bibframe events ([MSEARCH-781](https://folio-org.atlassian.net/browse/MSEARCH-781)) ### Bug fixes * Do not delete kafka topics if collection topic is enabled ([MSEARCH-725](https://folio-org.atlassian.net/browse/MSEARCH-725)) diff --git a/README.md b/README.md index 3ec557d78..f6db6b5b9 100644 --- a/README.md +++ b/README.md @@ -244,6 +244,7 @@ and [Cross-cluster replication](https://docs.aws.amazon.com/opensearch-service/l | KAFKA_CONTRIBUTORS_TOPIC_REPLICATION_FACTOR | - | Replication factor for `search.instance-contributor` topic. | | KAFKA_CONSORTIUM_INSTANCE_CONCURRENCY | 2 | Custom number of kafka concurrent threads for consortium.instance message consuming. | | KAFKA_LOCATION_CONCURRENCY | 1 | Custom number of kafka concurrent threads for inventory.location message consuming. | +| KAFKA_BIBFRAME_CONCURRENCY | 1 | Custom number of kafka concurrent threads for bibframe message consuming. | | KAFKA_CONSORTIUM_INSTANCE_TOPIC_PARTITIONS | 50 | Amount of partitions for `search.consortium.instance` topic. | | KAFKA_CONSORTIUM_INSTANCE_TOPIC_REPLICATION_FACTOR | - | Replication factor for `search.consortium.instance` topic. | | KAFKA_SUBJECTS_CONCURRENCY | 2 | Custom number of kafka concurrent threads for subject message consuming. | @@ -417,6 +418,7 @@ Consortium feature on module enable is defined by 'centralTenantId' tenant param |:-------|:------------------------------|:-------------------------------------------------------------------------------------| | GET | `/search/instances` | Search by instances and to this instance items and holding-records | | GET | `/search/authorities` | Search by authority records | +| GET | `/search/bibframe` | Search linked data graph resource descriptions | | GET | `/search/{recordType}/facets` | Get facets where recordType could be: instances, authorities, contributors, subjects | | GET | ~~`/search/instances/ids`~~ | (DEPRECATED) Stream instance ids as JSON or plain text | | GET | ~~`/search/holdings/ids`~~ | (DEPRECATED) Stream holding record ids as JSON or plain text | diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index b2b5e29c8..e741c054a 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -102,6 +102,15 @@ "user-tenants.collection.get" ] }, + { + "methods": [ + "GET" + ], + "pathPattern": "/search/bibframe", + "permissionsRequired": [ + "search.bibframe.collection.get" + ] + }, { "methods": [ "GET" @@ -573,6 +582,11 @@ "displayName": "Search - searches authorities by given query", "description": "Searches authorities by given query" }, + { + "permissionName": "search.bibframe.collection.get", + "displayName": "Search - searches bibframe by given query", + "description": "Searches bibframe by given query" + }, { "permissionName": "browse.call-numbers.instances.collection.get", "displayName": "Browse - provides collections of browse items for instance by call number", diff --git a/src/main/java/org/folio/search/controller/SearchController.java b/src/main/java/org/folio/search/controller/SearchController.java index 0d5d3ec3e..7813c6165 100644 --- a/src/main/java/org/folio/search/controller/SearchController.java +++ b/src/main/java/org/folio/search/controller/SearchController.java @@ -3,6 +3,8 @@ import lombok.RequiredArgsConstructor; import org.folio.search.domain.dto.Authority; import org.folio.search.domain.dto.AuthoritySearchResult; +import org.folio.search.domain.dto.Bibframe; +import org.folio.search.domain.dto.BibframeSearchResult; import org.folio.search.domain.dto.Instance; import org.folio.search.domain.dto.InstanceSearchResult; import org.folio.search.model.service.CqlSearchRequest; @@ -47,4 +49,24 @@ public ResponseEntity searchInstances(String tenantId, Str .instances(result.getRecords()) .totalRecords(result.getTotalRecords())); } + + @Override + public ResponseEntity searchBibframe(String tenant, String query, Integer limit, + Integer offset) { + var searchRequest = CqlSearchRequest.of( + Bibframe.class, tenant, query, limit, offset, true); + var result = searchService.search(searchRequest); + return ResponseEntity.ok(new BibframeSearchResult() + .searchQuery(query) + .content(result.getRecords()) + .pageNumber(divPlusOneIfRemainder(offset, limit)) + .totalPages(divPlusOneIfRemainder(result.getTotalRecords(), limit)) + .totalRecords(result.getTotalRecords()) + ); + } + + private int divPlusOneIfRemainder(int one, int two) { + var modulo = one % two; + return one / two + (modulo > 0 ? 1 : 0); + } } diff --git a/src/main/java/org/folio/search/cql/LccnSearchTermProcessor.java b/src/main/java/org/folio/search/cql/LccnSearchTermProcessor.java index bc8c92a84..decf0a617 100644 --- a/src/main/java/org/folio/search/cql/LccnSearchTermProcessor.java +++ b/src/main/java/org/folio/search/cql/LccnSearchTermProcessor.java @@ -1,15 +1,18 @@ package org.folio.search.cql; import lombok.RequiredArgsConstructor; -import org.folio.search.utils.SearchUtils; +import org.folio.search.service.lccn.LccnNormalizer; import org.springframework.stereotype.Component; @Component @RequiredArgsConstructor public class LccnSearchTermProcessor implements SearchTermProcessor { + private final LccnNormalizer lccnNormalizer; + @Override public String getSearchTerm(String inputTerm) { - return SearchUtils.normalizeLccn(inputTerm); + return lccnNormalizer.apply(inputTerm) + .orElse(null); } } diff --git a/src/main/java/org/folio/search/integration/KafkaMessageListener.java b/src/main/java/org/folio/search/integration/KafkaMessageListener.java index 65b3eb50a..f580bdd84 100644 --- a/src/main/java/org/folio/search/integration/KafkaMessageListener.java +++ b/src/main/java/org/folio/search/integration/KafkaMessageListener.java @@ -10,6 +10,7 @@ import static org.folio.search.utils.SearchConverterUtils.getEventPayload; import static org.folio.search.utils.SearchConverterUtils.getResourceEventId; import static org.folio.search.utils.SearchConverterUtils.getResourceSource; +import static org.folio.search.utils.SearchUtils.BIBFRAME_RESOURCE; import static org.folio.search.utils.SearchUtils.ID_FIELD; import static org.folio.search.utils.SearchUtils.INSTANCE_ID_FIELD; import static org.folio.search.utils.SearchUtils.INSTANCE_RESOURCE; @@ -195,6 +196,22 @@ public void handleLocationEvents(List> con indexResources(batch, resourceService::indexResources); } + @KafkaListener( + id = KafkaConstants.BIBFRAME_LISTENER_ID, + containerFactory = "standardListenerContainerFactory", + groupId = "#{folioKafkaProperties.listener['bibframe'].groupId}", + concurrency = "#{folioKafkaProperties.listener['bibframe'].concurrency}", + topicPattern = "#{folioKafkaProperties.listener['bibframe'].topicPattern}") + public void handleBibframeEvents(List> consumerRecords) { + log.info("Processing bibframe events from Kafka [number of events: {}]", consumerRecords.size()); + var batch = consumerRecords.stream() + .map(ConsumerRecord::value) + .map(bibframe -> bibframe.resourceName(BIBFRAME_RESOURCE).id(getResourceEventId(bibframe))) + .toList(); + + indexResources(batch, resourceService::indexResources); + } + private void indexResources(List batch, Consumer> indexConsumer) { var batchByTenant = batch.stream().collect(Collectors.groupingBy(ResourceEvent::getTenant)); diff --git a/src/main/java/org/folio/search/service/lccn/DefaultLccnNormalizer.java b/src/main/java/org/folio/search/service/lccn/DefaultLccnNormalizer.java new file mode 100644 index 000000000..7da59a92a --- /dev/null +++ b/src/main/java/org/folio/search/service/lccn/DefaultLccnNormalizer.java @@ -0,0 +1,21 @@ +package org.folio.search.service.lccn; + +import java.util.Optional; +import org.apache.commons.lang3.StringUtils; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; + +@Service +@Primary +public class DefaultLccnNormalizer implements LccnNormalizer { + + @Override + public Optional apply(String lccn) { + if (StringUtils.isBlank(lccn)) { + return Optional.empty(); + } + + return Optional.of(StringUtils.deleteWhitespace(lccn)) + .map(String::toLowerCase); + } +} diff --git a/src/main/java/org/folio/search/service/lccn/LccnNormalizer.java b/src/main/java/org/folio/search/service/lccn/LccnNormalizer.java new file mode 100644 index 000000000..cfbae4b20 --- /dev/null +++ b/src/main/java/org/folio/search/service/lccn/LccnNormalizer.java @@ -0,0 +1,7 @@ +package org.folio.search.service.lccn; + +import java.util.Optional; +import java.util.function.Function; + +public interface LccnNormalizer extends Function> { +} diff --git a/src/main/java/org/folio/search/service/lccn/LccnNormalizerStructureB.java b/src/main/java/org/folio/search/service/lccn/LccnNormalizerStructureB.java new file mode 100644 index 000000000..07e4bbdc6 --- /dev/null +++ b/src/main/java/org/folio/search/service/lccn/LccnNormalizerStructureB.java @@ -0,0 +1,67 @@ +package org.folio.search.service.lccn; + +import jakarta.validation.constraints.NotNull; +import java.util.Optional; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +/** + * Class responsible for normalizing Structure B LCCN values. + */ +@Log4j2 +@Service +public class LccnNormalizerStructureB implements LccnNormalizer { + private static final String NORMALIZED_LCCN_REGEX = "\\d{10}"; + private static final char HYPHEN = '-'; + + /** + * Normalizes the given LCCN value and returns the normalized LCCN. + * If the given LCCN is invalid, an empty Optional is returned. + * + * @param lccn LCCN to be normalized + * @return Returns the normalized LCCN. If the given LCCN is invalid, returns an empty Optional + */ + public Optional apply(@NotNull final String lccn) { + var normalizedLccn = lccn; + + // Remove white spaces + normalizedLccn = normalizedLccn.replaceAll("\\s", StringUtils.EMPTY); + + // If lccn contains "/", remove it & all characters to the right of "/" + normalizedLccn = normalizedLccn.replaceAll("/.*", StringUtils.EMPTY); + + // Process the serial number component of LCCN + normalizedLccn = processSerialNumber(normalizedLccn); + + if (normalizedLccn.matches(NORMALIZED_LCCN_REGEX)) { + return Optional.of(normalizedLccn); + } + + log.warn("LCCN is not in expected format: [{}]", lccn); + return Optional.empty(); + } + + /** + * Serial number is demarcated by a hyphen (fifth character in the value). Further, the serial number must be six + * digits in length. If fewer than six digits, remove the hyphen and left fill with zeroes so that there are six + * digits in the serial number. + */ + private String processSerialNumber(String lccn) { + if (lccn.length() >= 5 && lccn.charAt(4) == HYPHEN) { + var lccnParts = lccn.split(String.valueOf(HYPHEN)); + if (lccnParts.length == 2) { + String prefix = lccnParts[0]; + StringBuilder serialNumber = new StringBuilder(lccnParts[1]); + + // Left fill the serial number with zeroes to make it six digits + while (serialNumber.length() < 6) { + serialNumber.insert(0, "0"); + } + + return serialNumber.insert(0, prefix).toString(); + } + } + return lccn; + } +} diff --git a/src/main/java/org/folio/search/service/setter/AbstractLccnProcessor.java b/src/main/java/org/folio/search/service/setter/AbstractLccnProcessor.java index d7eda0d3f..27baa9ecd 100644 --- a/src/main/java/org/folio/search/service/setter/AbstractLccnProcessor.java +++ b/src/main/java/org/folio/search/service/setter/AbstractLccnProcessor.java @@ -3,24 +3,28 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import org.folio.search.domain.dto.Identifier; import org.folio.search.integration.ReferenceDataService; -import org.folio.search.utils.SearchUtils; +import org.folio.search.service.lccn.LccnNormalizer; public abstract class AbstractLccnProcessor extends AbstractIdentifierProcessor { private static final List LCCN_IDENTIFIER_NAME = List.of("LCCN", "Canceled LCCN"); + private final LccnNormalizer lccnNormalizer; - protected AbstractLccnProcessor(ReferenceDataService referenceDataService) { + protected AbstractLccnProcessor(ReferenceDataService referenceDataService, LccnNormalizer lccnNormalizer) { super(referenceDataService, LCCN_IDENTIFIER_NAME); + this.lccnNormalizer = lccnNormalizer; } @Override public Set getFieldValue(T entity) { return filterIdentifiersValue(getIdentifiers(entity)).stream() - .map(SearchUtils::normalizeLccn) + .map(lccnNormalizer) + .flatMap(Optional::stream) .filter(Objects::nonNull) .collect(Collectors.toCollection(LinkedHashSet::new)); } diff --git a/src/main/java/org/folio/search/service/setter/authority/LccnAuthorityProcessor.java b/src/main/java/org/folio/search/service/setter/authority/LccnAuthorityProcessor.java index 8f2e56cf4..527baf320 100644 --- a/src/main/java/org/folio/search/service/setter/authority/LccnAuthorityProcessor.java +++ b/src/main/java/org/folio/search/service/setter/authority/LccnAuthorityProcessor.java @@ -6,6 +6,7 @@ import org.folio.search.domain.dto.Authority; import org.folio.search.domain.dto.Identifier; import org.folio.search.integration.ReferenceDataService; +import org.folio.search.service.lccn.LccnNormalizer; import org.folio.search.service.setter.AbstractLccnProcessor; import org.springframework.stereotype.Component; @@ -16,9 +17,10 @@ public class LccnAuthorityProcessor extends AbstractLccnProcessor { * Used by dependency injection. * * @param referenceDataService {@link ReferenceDataService} bean + * @param lccnNormalizer {@link LccnNormalizer} bean */ - public LccnAuthorityProcessor(ReferenceDataService referenceDataService) { - super(referenceDataService); + public LccnAuthorityProcessor(ReferenceDataService referenceDataService, LccnNormalizer lccnNormalizer) { + super(referenceDataService, lccnNormalizer); } @Override diff --git a/src/main/java/org/folio/search/service/setter/bibframe/BibframeContributorProcessor.java b/src/main/java/org/folio/search/service/setter/bibframe/BibframeContributorProcessor.java new file mode 100644 index 000000000..a985f87cc --- /dev/null +++ b/src/main/java/org/folio/search/service/setter/bibframe/BibframeContributorProcessor.java @@ -0,0 +1,34 @@ +package org.folio.search.service.setter.bibframe; + +import static java.util.Optional.ofNullable; +import static java.util.stream.Collectors.toCollection; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; +import org.apache.commons.lang3.StringUtils; +import org.folio.search.domain.dto.Bibframe; +import org.folio.search.domain.dto.BibframeContributorsInner; +import org.folio.search.domain.dto.BibframeInstancesInner; +import org.folio.search.service.setter.FieldProcessor; +import org.springframework.stereotype.Component; + +@Component +public class BibframeContributorProcessor implements FieldProcessor> { + + @Override + public Set getFieldValue(Bibframe bibframe) { + var workContributors = ofNullable(bibframe.getContributors()).stream().flatMap(Collection::stream); + var instanceContributors = ofNullable(bibframe.getInstances()).stream().flatMap(Collection::stream) + .map(BibframeInstancesInner::getContributors).filter(Objects::nonNull).flatMap(Collection::stream); + return Stream.concat(workContributors, instanceContributors) + .filter(Objects::nonNull) + .map(BibframeContributorsInner::getName) + .filter(StringUtils::isNotBlank) + .map(String::trim) + .collect(toCollection(LinkedHashSet::new)); + } + +} diff --git a/src/main/java/org/folio/search/service/setter/bibframe/BibframeIsbnProcessor.java b/src/main/java/org/folio/search/service/setter/bibframe/BibframeIsbnProcessor.java new file mode 100644 index 000000000..493240fcf --- /dev/null +++ b/src/main/java/org/folio/search/service/setter/bibframe/BibframeIsbnProcessor.java @@ -0,0 +1,38 @@ +package org.folio.search.service.setter.bibframe; + +import static java.util.Objects.nonNull; +import static java.util.Optional.ofNullable; +import static java.util.stream.Collectors.toCollection; +import static org.folio.search.domain.dto.BibframeInstancesInnerIdentifiersInner.TypeEnum.ISBN; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.folio.search.domain.dto.Bibframe; +import org.folio.search.domain.dto.BibframeInstancesInnerIdentifiersInner; +import org.folio.search.service.setter.FieldProcessor; +import org.folio.search.service.setter.instance.IsbnProcessor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class BibframeIsbnProcessor implements FieldProcessor> { + + private final IsbnProcessor isbnProcessor; + + @Override + public Set getFieldValue(Bibframe bibframe) { + return ofNullable(bibframe.getInstances()).stream() + .flatMap(Collection::stream) + .filter(i -> nonNull(i.getIdentifiers())) + .flatMap(i -> i.getIdentifiers().stream()) + .filter(i -> ISBN.equals(i.getType())) + .map(BibframeInstancesInnerIdentifiersInner::getValue) + .filter(Objects::nonNull) + .map(isbnProcessor::normalizeIsbn) + .flatMap(Collection::stream) + .collect(toCollection(LinkedHashSet::new)); + } +} diff --git a/src/main/java/org/folio/search/service/setter/bibframe/BibframeLccnProcessor.java b/src/main/java/org/folio/search/service/setter/bibframe/BibframeLccnProcessor.java new file mode 100644 index 000000000..814229c13 --- /dev/null +++ b/src/main/java/org/folio/search/service/setter/bibframe/BibframeLccnProcessor.java @@ -0,0 +1,42 @@ +package org.folio.search.service.setter.bibframe; + +import static java.util.Objects.nonNull; +import static java.util.Optional.ofNullable; +import static java.util.stream.Collectors.toCollection; +import static org.folio.search.domain.dto.BibframeInstancesInnerIdentifiersInner.TypeEnum.LCCN; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.folio.search.domain.dto.Bibframe; +import org.folio.search.domain.dto.BibframeInstancesInnerIdentifiersInner; +import org.folio.search.service.lccn.LccnNormalizer; +import org.folio.search.service.setter.FieldProcessor; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class BibframeLccnProcessor implements FieldProcessor> { + + @Qualifier("lccnNormalizerStructureB") + private final LccnNormalizer lccnNormalizer; + + @Override + public Set getFieldValue(Bibframe bibframe) { + return ofNullable(bibframe.getInstances()).stream() + .flatMap(Collection::stream) + .filter(i -> nonNull(i.getIdentifiers())) + .flatMap(i -> i.getIdentifiers().stream()) + .filter(i -> LCCN.equals(i.getType())) + .map(BibframeInstancesInnerIdentifiersInner::getValue) + .filter(Objects::nonNull) + .map(lccnNormalizer) + .flatMap(Optional::stream) + .collect(toCollection(LinkedHashSet::new)); + } + +} diff --git a/src/main/java/org/folio/search/service/setter/bibframe/BibframeSortTitleProcessor.java b/src/main/java/org/folio/search/service/setter/bibframe/BibframeSortTitleProcessor.java new file mode 100644 index 000000000..0a3930f42 --- /dev/null +++ b/src/main/java/org/folio/search/service/setter/bibframe/BibframeSortTitleProcessor.java @@ -0,0 +1,31 @@ +package org.folio.search.service.setter.bibframe; + +import static java.util.Optional.ofNullable; + +import java.util.Collection; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.commons.lang3.StringUtils; +import org.folio.search.domain.dto.Bibframe; +import org.folio.search.domain.dto.BibframeInstancesInner; +import org.folio.search.domain.dto.BibframeTitlesInner; +import org.folio.search.service.setter.FieldProcessor; +import org.springframework.stereotype.Component; + +@Component +public class BibframeSortTitleProcessor implements FieldProcessor { + + @Override + public String getFieldValue(Bibframe bibframe) { + var workTitles = ofNullable(bibframe.getTitles()).stream().flatMap(Collection::stream); + var instanceTitles = ofNullable(bibframe.getInstances()).stream().flatMap(Collection::stream) + .map(BibframeInstancesInner::getTitles).filter(Objects::nonNull).flatMap(Collection::stream); + return Stream.concat(workTitles, instanceTitles) + .filter(Objects::nonNull) + .map(BibframeTitlesInner::getValue) + .filter(StringUtils::isNotBlank) + .collect(Collectors.joining()); + } + +} diff --git a/src/main/java/org/folio/search/service/setter/bibframe/BibframeTitleProcessor.java b/src/main/java/org/folio/search/service/setter/bibframe/BibframeTitleProcessor.java new file mode 100644 index 000000000..488f6340f --- /dev/null +++ b/src/main/java/org/folio/search/service/setter/bibframe/BibframeTitleProcessor.java @@ -0,0 +1,34 @@ +package org.folio.search.service.setter.bibframe; + +import static java.util.Optional.ofNullable; +import static java.util.stream.Collectors.toCollection; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; +import org.apache.commons.lang3.StringUtils; +import org.folio.search.domain.dto.Bibframe; +import org.folio.search.domain.dto.BibframeInstancesInner; +import org.folio.search.domain.dto.BibframeTitlesInner; +import org.folio.search.service.setter.FieldProcessor; +import org.springframework.stereotype.Component; + +@Component +public class BibframeTitleProcessor implements FieldProcessor> { + + @Override + public Set getFieldValue(Bibframe bibframe) { + var workTitles = ofNullable(bibframe.getTitles()).stream().flatMap(Collection::stream); + var instTitles = ofNullable(bibframe.getInstances()).stream().flatMap(Collection::stream).filter(Objects::nonNull) + .map(BibframeInstancesInner::getTitles).filter(Objects::nonNull).flatMap(Collection::stream); + return Stream.concat(workTitles, instTitles) + .filter(Objects::nonNull) + .map(BibframeTitlesInner::getValue) + .filter(StringUtils::isNotBlank) + .map(String::trim) + .collect(toCollection(LinkedHashSet::new)); + } + +} diff --git a/src/main/java/org/folio/search/service/setter/instance/LccnInstanceProcessor.java b/src/main/java/org/folio/search/service/setter/instance/LccnInstanceProcessor.java index 6c24483df..fd2d1a991 100644 --- a/src/main/java/org/folio/search/service/setter/instance/LccnInstanceProcessor.java +++ b/src/main/java/org/folio/search/service/setter/instance/LccnInstanceProcessor.java @@ -6,6 +6,7 @@ import org.folio.search.domain.dto.Identifier; import org.folio.search.domain.dto.Instance; import org.folio.search.integration.ReferenceDataService; +import org.folio.search.service.lccn.LccnNormalizer; import org.folio.search.service.setter.AbstractLccnProcessor; import org.springframework.stereotype.Component; @@ -19,9 +20,10 @@ public class LccnInstanceProcessor extends AbstractLccnProcessor { * Used by dependency injection. * * @param referenceDataService {@link ReferenceDataService} bean + * @param lccnNormalizer {@link LccnNormalizer} bean */ - public LccnInstanceProcessor(ReferenceDataService referenceDataService) { - super(referenceDataService); + public LccnInstanceProcessor(ReferenceDataService referenceDataService, LccnNormalizer lccnNormalizer) { + super(referenceDataService, lccnNormalizer); } @Override diff --git a/src/main/java/org/folio/search/utils/KafkaConstants.java b/src/main/java/org/folio/search/utils/KafkaConstants.java index d93743139..d192c4245 100644 --- a/src/main/java/org/folio/search/utils/KafkaConstants.java +++ b/src/main/java/org/folio/search/utils/KafkaConstants.java @@ -8,6 +8,7 @@ public final class KafkaConstants { public static final String CONSORTIUM_INSTANCE_LISTENER_ID = "mod-search-consortium-instance-listener"; public static final String CLASSIFICATION_TYPE_LISTENER_ID = "mod-search-classification-type-listener"; public static final String LOCATION_LISTENER_ID = "mod-search-location-listener"; + public static final String BIBFRAME_LISTENER_ID = "mod-search-bibframe-listener"; private KafkaConstants() {} } diff --git a/src/main/java/org/folio/search/utils/SearchUtils.java b/src/main/java/org/folio/search/utils/SearchUtils.java index 64cb1948d..5e4439f36 100644 --- a/src/main/java/org/folio/search/utils/SearchUtils.java +++ b/src/main/java/org/folio/search/utils/SearchUtils.java @@ -38,6 +38,7 @@ public class SearchUtils { public static final String CONTRIBUTOR_RESOURCE = getResourceName(Contributor.class); public static final String LOCATION_RESOURCE = "location"; public static final String CLASSIFICATION_TYPE_RESOURCE = "classification-type"; + public static final String BIBFRAME_RESOURCE = "bibframe"; public static final String ID_FIELD = "id"; public static final String SOURCE_FIELD = "source"; @@ -328,20 +329,6 @@ public static int getNumberOfRequests(Map> requ .sum(); } - /** - * Normalizes LCCN value. - * - * @param value LCCN value - * @return normalized LCCN value - */ - public static String normalizeLccn(String value) { - if (StringUtils.isBlank(value)) { - return null; - } - - return StringUtils.deleteWhitespace(value).toLowerCase(); - } - /** * This method normalize the given string to an alphanumeric string. * If the input string is null or blank, this method returns null. diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 528ad8355..969013fc7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -135,6 +135,10 @@ folio: concurrency: ${KAFKA_LOCATION_CONCURRENCY:1} topic-pattern: (${folio.environment}\.)(.*\.)inventory\.location group-id: ${folio.environment}-mod-search-location-type-group + bibframe: + concurrency: ${KAFKA_BIBFRAME_CONCURRENCY:1} + topic-pattern: (${folio.environment}\.)(.*\.)search\.bibframe + group-id: ${folio.environment}-mod-search-bibframe-group okapiUrl: ${okapi.url} logging: request: diff --git a/src/main/resources/elasticsearch/index/bibframe.json b/src/main/resources/elasticsearch/index/bibframe.json new file mode 100644 index 000000000..137a49b25 --- /dev/null +++ b/src/main/resources/elasticsearch/index/bibframe.json @@ -0,0 +1,68 @@ +{ + "index": { + "number_of_shards": 4, + "number_of_replicas": 2, + "refresh_interval": "1s", + "codec": "best_compression", + "mapping.total_fields.limit": 1000 + }, + "analysis": { + "filter": { + "folio_word_delimiter_graph": { + "type": "word_delimiter_graph", + "catenate_words": true + } + }, + "normalizer": { + "keyword_lowercase": { + "filter": [ + "lowercase", + "trim" + ], + "type": "custom" + }, + "keyword_uppercase": { + "filter": [ + "uppercase", + "trim" + ], + "type": "custom" + }, + "keyword_trimmed": { + "filter": [ + "trim" + ], + "type": "custom" + } + }, + "analyzer": { + "source_analyzer": { + "tokenizer": "icu_tokenizer", + "filter": [ + "folio_word_delimiter_graph", + "icu_folding" + ], + "char_filter": [ + "and_char_replacement" + ], + "type": "custom" + }, + "whitespace_lowercase_analyzer": { + "tokenizer": "whitespace", + "filter": [ + "lowercase", + "icu_folding" + ], + "type": "custom" + } + }, + "tokenizers": { }, + "char_filter": { + "and_char_replacement": { + "type": "pattern_replace", + "pattern": " & ", + "replacement": " and " + } + } + } +} diff --git a/src/main/resources/model/bibframe.json b/src/main/resources/model/bibframe.json new file mode 100644 index 000000000..a58546f9c --- /dev/null +++ b/src/main/resources/model/bibframe.json @@ -0,0 +1,156 @@ +{ + "name": "bibframe", + "eventBodyJavaClass": "org.folio.search.domain.dto.Bibframe", + "languageSourcePaths": [ "$.languages" ], + "fields": { + "id": { + "index": "whitespace" + }, + "titles": { + "type": "object", + "properties": { + "value": { + "index": "whitespace" + }, + "type": { + "index": "whitespace" + } + } + }, + "contributors": { + "type": "object", + "properties": { + "name": { + "index": "whitespace" + }, + "type": { + "index": "whitespace" + }, + "isCreator": { + "index": "whitespace" + } + } + }, + "languages": { + "type": "object", + "properties": { + "value": { + "index": "whitespace" + } + } + }, + "classifications": { + "type": "object", + "properties": { + "number": { + "index": "whitespace" + }, + "source": { + "index": "whitespace" + } + } + }, + "subjects": { + "type": "object", + "properties": { + "value": { + "index": "whitespace" + } + } + }, + "instances": { + "type": "object", + "properties": { + "id": { + "index": "keyword" + }, + "titles": { + "type": "object", + "properties": { + "value": { + "index": "whitespace" + }, + "type": { + "index": "whitespace" + } + } + }, + "identifiers": { + "type": "object", + "properties": { + "value": { + "index": "whitespace" + }, + "type": { + "index": "whitespace" + } + } + }, + "contributors": { + "type": "object", + "properties": { + "name": { + "index": "whitespace" + }, + "type": { + "index": "whitespace" + }, + "isCreator": { + "index": "whitespace" + } + } + }, + "publications": { + "type": "object", + "properties": { + "name": { + "index": "whitespace" + }, + "date": { + "index": "whitespace" + } + } + }, + "editionStatements": { + "type": "object", + "properties": { + "value": { + "index": "whitespace" + } + } + } + } + } + }, + "searchFields": { + "isbn": { + "type": "search", + "processor": "bibframeIsbnProcessor", + "searchTermProcessor": "isbnSearchTermProcessor", + "index": "standard", + "indexPlainValue": false + }, + "lccn": { + "type": "search", + "index": "standard", + "processor": "bibframeLccnProcessor" + }, + "title": { + "type": "search", + "index": "multilang", + "processor": "bibframeTitleProcessor" + }, + "sort_title": { + "searchTypes": "sort", + "type": "search", + "index": "keyword_lowercase", + "processor": "bibframeSortTitleProcessor" + }, + "contributor": { + "type": "search", + "index": "multilang", + "processor": "bibframeContributorProcessor" + } + }, + "indexMappings": { } +} diff --git a/src/main/resources/swagger.api/examples/result/bibframeSearchResult.yaml b/src/main/resources/swagger.api/examples/result/bibframeSearchResult.yaml new file mode 100644 index 000000000..de9cbbda1 --- /dev/null +++ b/src/main/resources/swagger.api/examples/result/bibframeSearchResult.yaml @@ -0,0 +1,38 @@ +value: + searchQuery: "query string" + content: + - id: "1" + titles: + - value: "Title Value" + type: "Main" + contributors: + - name: "John Doe" + type: "Person" + isCreator: true + languages: + - value: "eng" + classifications: + - number: "1234" + source: "ddc" + subjects: + - value: "Subject" + instances: + - id: "2" + titles: + - value: "Title Value" + type: "Main" + identifiers: + - value: "1234567890" + type: "ISBN" + contributors: + - name: "John Doe" + type: "Person" + isCreator: true + publications: + - name: "Publisher Name" + date: "2022" + editionStatements: + - value: "Edition 1" + pageNumber: 0 + totalPages: 10 + totalRecords: 100 diff --git a/src/main/resources/swagger.api/mod-search.yaml b/src/main/resources/swagger.api/mod-search.yaml index 5a158fef0..71390f7e3 100644 --- a/src/main/resources/swagger.api/mod-search.yaml +++ b/src/main/resources/swagger.api/mod-search.yaml @@ -115,6 +115,9 @@ paths: /search/config/features/{featureId}: $ref: 'paths/search-config/search-config-features-feature-id.yaml' + /search/bibframe: + $ref: 'paths/search-bibframe/search-bibframe.yaml' + /browse/call-numbers/instances: $ref: 'paths/browse-call-numbers/browse-call-numbers-instances.yaml' diff --git a/src/main/resources/swagger.api/parameters/bibframe-limit-param.yaml b/src/main/resources/swagger.api/parameters/bibframe-limit-param.yaml new file mode 100644 index 000000000..ba4039bbc --- /dev/null +++ b/src/main/resources/swagger.api/parameters/bibframe-limit-param.yaml @@ -0,0 +1,8 @@ +in: query +name: limit +description: Limit the number of elements returned in the response. +schema: + type: integer + minimum: 0 + maximum: 100 + default: 10 diff --git a/src/main/resources/swagger.api/paths/search-bibframe/search-bibframe.yaml b/src/main/resources/swagger.api/paths/search-bibframe/search-bibframe.yaml new file mode 100644 index 000000000..91191d6dd --- /dev/null +++ b/src/main/resources/swagger.api/paths/search-bibframe/search-bibframe.yaml @@ -0,0 +1,25 @@ +get: + operationId: searchBibframe + summary: Search Bibframe + description: Get a list of bibframe records for CQL query + tags: + - search + parameters: + - $ref: '../../parameters/x-okapi-tenant-header.yaml' + - $ref: '../../parameters/cql-query.yaml' + - $ref: '../../parameters/bibframe-limit-param.yaml' + - $ref: '../../parameters/offset-param.yaml' + responses: + '200': + description: 'Bibframe search result' + content: + application/json: + schema: + $ref: '../../schemas/response/bibframeSearchResult.yaml' + examples: + searchResult: + $ref: '../../examples/result/bibframeSearchResult.yaml' + '400': + $ref: '../../responses/badRequestResponse.yaml' + '500': + $ref: '../../responses/internalServerErrorResponse.yaml' diff --git a/src/main/resources/swagger.api/schemas/dto/bibframe/bibframe.yaml b/src/main/resources/swagger.api/schemas/dto/bibframe/bibframe.yaml new file mode 100644 index 000000000..df2dc34b5 --- /dev/null +++ b/src/main/resources/swagger.api/schemas/dto/bibframe/bibframe.yaml @@ -0,0 +1,152 @@ +description: "Bibframe search dto, contains Work and linked Instances" +type: "object" +properties: + id: + description: "The Linked Data ID of a Work" + type: "string" + titles: + type: "array" + description: "Title array" + items: + properties: + value: + type: "string" + description: "Value of Title" + type: + type: "string" + enum: + - "Main" + - "Sub" + - "Main Parallel" + - "Sub Parallel" + - "Main Variant" + - "Sub Variant" + contributors: + type: "array" + description: "Contributor array" + items: + properties: + name: + type: "string" + description: "Name of contributor" + type: + type: "string" + enum: + - "Family" + - "Jurisdiction" + - "Meeting" + - "Organization" + - "Person" + isCreator: + type: "boolean" + description: "A flag indicating whether the contributor is the creator" + languages: + type: "array" + description: "Language array" + items: + properties: + value: + type: "string" + description: "Language value" + classifications: + type: "array" + description: "List of classification items" + items: + type: "object" + properties: + number: + type: "string" + description: "Classification number" + source: + type: "string" + description: "The source of the classification" + subjects: + type: "array" + description: "Subject array" + items: + properties: + value: + type: "string" + description: "Subject label" + instances: + type: "array" + description: "Instance array" + items: + properties: + id: + description: "The Linked Data ID of an Instance" + type: "string" + titles: + type: "array" + description: "Title array" + items: + properties: + value: + type: "string" + description: "Value of Title" + type: + type: "string" + enum: + - "Main" + - "Sub" + - "Main Parallel" + - "Sub Parallel" + - "Main Variant" + - "Sub Variant" + identifiers: + type: "array" + description: "Resource identifier array" + items: + properties: + value: + type: "string" + description: "Value of Identifier" + type: + type: "string" + enum: + - "Ean" + - "ISBN" + - "LCCN" + - "LocalId" + - "UNKNOWN" + contributors: + type: "array" + description: "Contributor array" + items: + properties: + name: + type: "string" + description: "Name of contributor" + type: + type: "string" + enum: + - "Family" + - "Jurisdiction" + - "Meeting" + - "Organization" + - "Person" + isCreator: + type: "boolean" + description: "A flag indicating whether the contributor is the creator" + publications: + type: "array" + description: "List of publication items" + items: + type: "object" + properties: + name: + type: "string" + description: "Name of publisher, distributor, etc." + date: + type: "string" + description: "Date (year YYYY) of publication, distribution, etc." + editionStatements: + type: "array" + description: "Edition statement array" + items: + properties: + value: + type: "string" + description: "Edition statement value" +required: + - "id" diff --git a/src/main/resources/swagger.api/schemas/response/bibframeSearchResult.yaml b/src/main/resources/swagger.api/schemas/response/bibframeSearchResult.yaml new file mode 100644 index 000000000..dcc4203fa --- /dev/null +++ b/src/main/resources/swagger.api/schemas/response/bibframeSearchResult.yaml @@ -0,0 +1,20 @@ +description: "Bibframe search result response" +type: "object" +properties: + searchQuery: + type: "string" + description: "Initial search query" + content: + type: "array" + description: "List of bibframe records found" + items: + $ref: "../../schemas/dto/bibframe/bibframe.yaml" + pageNumber: + type: "integer" + description: "Current results page number, 0 by default" + totalPages: + type: "integer" + description: "Total pages count" + totalRecords: + type: "integer" + description: "Total results count" diff --git a/src/test/java/org/folio/search/controller/SearchBibframeIT.java b/src/test/java/org/folio/search/controller/SearchBibframeIT.java new file mode 100644 index 000000000..48fa0a74c --- /dev/null +++ b/src/test/java/org/folio/search/controller/SearchBibframeIT.java @@ -0,0 +1,286 @@ +package org.folio.search.controller; + +import static java.lang.String.format; +import static java.lang.String.join; +import static org.folio.search.sample.SampleBibframe.getBibframe2SampleAsMap; +import static org.folio.search.sample.SampleBibframe.getBibframeSampleAsMap; +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; + +import org.folio.search.domain.dto.Bibframe; +import org.folio.search.support.base.BaseIntegrationTest; +import org.folio.spring.testing.type.IntegrationTest; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +@IntegrationTest +class SearchBibframeIT extends BaseIntegrationTest { + + @BeforeAll + static void prepare() { + setUpTenant(Bibframe.class, 2, getBibframeSampleAsMap(), getBibframe2SampleAsMap()); + } + + @AfterAll + static void cleanUp() { + removeTenant(); + } + + @DisplayName("search by bibframe (all 2 bibframe are found)") + @ParameterizedTest(name = "[{0}] {1}") + @CsvSource({ + "1, cql.allRecords = 1", + "2, title all \"titleAbc\"", + "3, title any \"titleAbc\"", + "4, title any \"titleAbc def\"", + "5, title any \"titleAbc XXX\"", + "6, title = \"titleAbc\"", + "7, title <> \"titleXXX\"", + "8, title = \"title*\"", + "9, title = \"*\"", + "10, isbn <> \"1234\"", + "11, lccn <> \"2023\"", + "12, contributor all \"common\"", + "13, contributor any \"common\"", + "14, contributor = \"common\"", + "15, contributor <> \"commonXXX\"", + "16, contributor = \"com*\"", + "17, contributor = \"*\"", + "18, (title all \"titleAbc\") sortBy title", + "19, title all \"titleAbc\" sortBy title", + "20, title all \"titleAbc\" sortBy title/sort.ascending", + "21, title all \"titleAbc\" sortBy title/sort.descending", + }) + void searchByBibframe_parameterized_allResults(int index, String query) throws Throwable { + var asc = query.contains("titleAbc def") || query.contains("sortBy") && !query.contains("descending"); + doSearchByBibframe(query) + .andExpect(jsonPath("$.totalRecords", is(2))) + .andExpect(jsonPath("$.content[0].titles[0].value", is(asc ? "titleAbc def" : "titleAbc xyz"))) + .andExpect(jsonPath("$.content[1].titles[0].value", is(asc ? "titleAbc xyz" : "titleAbc def"))); + } + + @DisplayName("search by bibframe (single bibframe is found)") + @ParameterizedTest(name = "[{0}] {1}") + @CsvSource({ + "1, title any \"def\"", + "2, title = \"titleAbc def\"", + "3, title == \"titleAbc def\"", + "4, title ==/string \"titleAbc def\"", + "5, isbn = \"*\"", + "6, isbn = \"1234567890123\"", + "7, isbn = \"1234*\"", + "8, isbn == \"1234567890123\"", + "9, isbn ==/string \"1234567890123\"", + "10, isbn any \"1234567890123\"", + "11, isbn any \"1234567890123 XXX\"", + "12, isbn all \"1234567890123\"", + "13, lccn = \"*\"", + "14, lccn = \"2023202345\"", + "15, lccn = \"2023*\"", + "16, lccn == \"2023202345\"", + "17, lccn ==/string \"2023202345\"", + "18, lccn any \"2023202345\"", + "19, lccn any \"2023202345 XXX\"", + "20, lccn all \"2023202345\"", + "21, contributor = Family", + "22, contributor == Meeting", + "23, contributor ==/string Organization", + "24, contributor any Person", + "25, contributor all Family" + }) + void searchByBibframe_parameterized_singleResult(int index, String query) throws Throwable { + doSearchByBibframe(query) + .andExpect(jsonPath("$.totalRecords", is(1))) + .andExpect(jsonPath(toId(toWork()), is("123456123456"))) + .andExpect(jsonPath(toTitleValue(toWork(), 0), is("titleAbc def"))) + .andExpect(jsonPath(toTitleType(toWork(), 0), is("Main"))) + .andExpect(jsonPath(toTitleValue(toWork(), 1), is("sub"))) + .andExpect(jsonPath(toTitleType(toWork(), 1), is("Sub"))) + .andExpect(jsonPath(toContributorName(toWork(), 0), is("Family"))) + .andExpect(jsonPath(toContributorType(toWork(), 0), is("Family"))) + .andExpect(jsonPath(toContributorIsCreator(toWork(), 0), is(true))) + .andExpect(jsonPath(toContributorName(toWork(), 1), is("Meeting"))) + .andExpect(jsonPath(toContributorType(toWork(), 1), is("Meeting"))) + .andExpect(jsonPath(toContributorIsCreator(toWork(), 1), is(false))) + .andExpect(jsonPath(toContributorName(toWork(), 2), is("Organization"))) + .andExpect(jsonPath(toContributorType(toWork(), 2), is("Organization"))) + .andExpect(jsonPath(toContributorIsCreator(toWork(), 2), is(true))) + .andExpect(jsonPath(toContributorName(toWork(), 3), is("Person"))) + .andExpect(jsonPath(toContributorType(toWork(), 3), is("Person"))) + .andExpect(jsonPath(toContributorIsCreator(toWork(), 3), is(false))) + .andExpect(jsonPath(toContributorName(toWork(), 4), is("common"))) + .andExpect(jsonPath(toContributorType(toWork(), 4), is("Family"))) + .andExpect(jsonPath(toContributorIsCreator(toWork(), 4), is(true))) + .andExpect(jsonPath(toLanguage(0), is("eng"))) + .andExpect(jsonPath(toLanguage(1), is("rus"))) + .andExpect(jsonPath(toClassificationNumber(0), is("1234"))) + .andExpect(jsonPath(toClassificationSource(0), is("ddc"))) + .andExpect(jsonPath(toClassificationNumber(1), is("5678"))) + .andExpect(jsonPath(toClassificationSource(1), is("other"))) + .andExpect(jsonPath(toSubject(0), is("Subject 1"))) + .andExpect(jsonPath(toSubject(1), is("Subject 2"))) + .andExpect(jsonPath(toId(toInstance()), is("instance1"))) + .andExpect(jsonPath(toTitleValue(toInstance(), 0), is("Instance1_Title"))) + .andExpect(jsonPath(toTitleType(toInstance(), 0), is("Main"))) + .andExpect(jsonPath(toTitleValue(toInstance(), 1), is("Instance1_Subtitle"))) + .andExpect(jsonPath(toTitleType(toInstance(), 1), is("Sub"))) + .andExpect(jsonPath(toIdValue(0), is("1234567890123"))) + .andExpect(jsonPath(toIdType(0), is("ISBN"))) + .andExpect(jsonPath(toIdValue(1), is(" 2023-202345/AC/r932"))) + .andExpect(jsonPath(toIdType(1), is("LCCN"))) + .andExpect(jsonPath(toContributorName(toInstance(), 0), is("Instance1_Family"))) + .andExpect(jsonPath(toContributorType(toInstance(), 0), is("Family"))) + .andExpect(jsonPath(toContributorIsCreator(toInstance(), 0), is(true))) + .andExpect(jsonPath(toContributorName(toInstance(), 1), is("Instance1_Meeting"))) + .andExpect(jsonPath(toContributorType(toInstance(), 1), is("Meeting"))) + .andExpect(jsonPath(toContributorIsCreator(toInstance(), 1), is(false))) + .andExpect(jsonPath(toContributorName(toInstance(), 2), is("Instance1_Organization"))) + .andExpect(jsonPath(toContributorType(toInstance(), 2), is("Organization"))) + .andExpect(jsonPath(toContributorIsCreator(toInstance(), 2), is(true))) + .andExpect(jsonPath(toContributorName(toInstance(), 3), is("Instance1_Person"))) + .andExpect(jsonPath(toContributorType(toInstance(), 3), is("Person"))) + .andExpect(jsonPath(toContributorIsCreator(toInstance(), 3), is(false))) + .andExpect(jsonPath(toPublicationName(0), is("publisher"))) + .andExpect(jsonPath(toPublicationDate(0), is("2023"))) + .andExpect(jsonPath(toPublicationName(1), is("publisher2"))) + .andExpect(jsonPath(toPublicationDate(1), is("2024"))) + .andExpect(jsonPath(toEditionStatement(0), is("1st edition"))) + .andExpect(jsonPath(toEditionStatement(1), is("2nd edition"))) + ; + } + + @DisplayName("search by bibframe (nothing is found)") + @ParameterizedTest(name = "[{0}] {1}") + @CsvSource({ + "1, title ==/string \"titleAbc\"", + "2, title ==/string \"def\"", + "3, title ==/string \"xyz\"", + "4, title == \"def titleAbc\"", + "5, title == \"titleAbcdef def\"", + "6, title all \"titleAbcdef\"", + "7, title any \"titleAbcdef\"", + "8, title = \"titleAbcdef\"", + "9, title <> \"titleAbc\"", + "10, isbn ==/string \"1234\"", + "11, isbn == \"1234\"", + "12, isbn any \"1234\"", + "13, isbn any \"12345678901231\"", + "14, isbn all \"1234\"", + "15, isbn = \"1234\"", + "16, lccn ==/string \"2023\"", + "17, lccn == \"2023\"", + "18, lccn any \"2023\"", + "19, lccn any \"202320231\"", + "20, lccn all \"2023\"", + "21, lccn = \"2023\"", + "22, contributor ==/string \"Famil\"", + "23, contributor ==/string \"Meeting1\"", + "24, contributor ==/string \"rganizatio\"", + "25, contributor == \"Person common\"", + "26, contributor == \"common Person\"", + "27, contributor all \"comm\"", + "28, contributor any \"comm\"", + "29, contributor = \"comm\"", + "30, contributor <> \"common\"", + }) + void searchByBibframe_parameterized_zeroResults(int index, String query) throws Throwable { + doSearchByBibframe(query) + .andExpect(jsonPath("$.totalRecords", is(0))); + } + + private String path(String path) { + return format("['%s']", path); + } + + private String arrayPath(String path) { + return arrayPath(path, 0); + } + + private String arrayPath(String path, int number) { + return format("['%s'][%s]", path, number); + } + + private String toWork() { + return join(".", "$", arrayPath("content")); + } + + private String toId(String base) { + return join(".", base, path("id")); + } + + private String toTitle(String base, int number) { + return join(".", base, arrayPath("titles", number)); + } + + private String toTitleValue(String base, int number) { + return join(".", toTitle(base, number), path("value")); + } + + private String toTitleType(String base, int number) { + return join(".", toTitle(base, number), path("type")); + } + + private String toContributor(String base, int number) { + return join(".", base, arrayPath("contributors", number)); + } + + private String toContributorName(String base, int number) { + return join(".", toContributor(base, number), path("name")); + } + + private String toContributorType(String base, int number) { + return join(".", toContributor(base, number), path("type")); + } + + private String toContributorIsCreator(String base, int number) { + return join(".", toContributor(base, number), path("isCreator")); + } + + private String toLanguage(int number) { + return join(".", toWork(), arrayPath("languages", number), path("value")); + } + + private String toClassification(int number) { + return join(".", toWork(), arrayPath("classifications", number)); + } + + private String toClassificationNumber(int number) { + return join(".", toClassification(number), path("number")); + } + + private String toClassificationSource(int number) { + return join(".", toClassification(number), path("source")); + } + + private String toSubject(int number) { + return join(".", toWork(), arrayPath("subjects", number), path("value")); + } + + private String toInstance() { + return join(".", toWork(), arrayPath("instances")); + } + + private String toIdValue(int number) { + return join(".", toInstance(), arrayPath("identifiers", number), path("value")); + } + + private String toIdType(int number) { + return join(".", toInstance(), arrayPath("identifiers", number), path("type")); + } + + private String toPublicationName(int number) { + return join(".", toInstance(), arrayPath("publications", number), path("name")); + } + + private String toPublicationDate(int number) { + return join(".", toInstance(), arrayPath("publications", number), path("date")); + } + + private String toEditionStatement(int number) { + return join(".", toInstance(), arrayPath("editionStatements", number), path("value")); + } + +} diff --git a/src/test/java/org/folio/search/cql/LccnSearchTermProcessorTest.java b/src/test/java/org/folio/search/cql/LccnSearchTermProcessorTest.java index f04dc6810..e0eac345b 100644 --- a/src/test/java/org/folio/search/cql/LccnSearchTermProcessorTest.java +++ b/src/test/java/org/folio/search/cql/LccnSearchTermProcessorTest.java @@ -1,23 +1,36 @@ package org.folio.search.cql; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; +import java.util.Optional; +import org.folio.search.service.lccn.LccnNormalizer; 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 LccnSearchTermProcessorTest { + @Mock + private LccnNormalizer normalizer; + @InjectMocks + private LccnSearchTermProcessor lccnSearchTermProcessor; @Test void getSearchTerm_positive() { + // given var searchTerm = " N 123456 "; - var lccnSearchTermProcessor = new LccnSearchTermProcessor(); + var normalizedTerm = "n123456"; + when(normalizer.apply(searchTerm)).thenReturn(Optional.of(normalizedTerm)); + // when var actual = lccnSearchTermProcessor.getSearchTerm(searchTerm); - assertThat(actual).isEqualTo("n123456"); + + // then + assertThat(actual).isEqualTo(normalizedTerm); } } diff --git a/src/test/java/org/folio/search/sample/SampleBibframe.java b/src/test/java/org/folio/search/sample/SampleBibframe.java new file mode 100644 index 000000000..56962f4d0 --- /dev/null +++ b/src/test/java/org/folio/search/sample/SampleBibframe.java @@ -0,0 +1,26 @@ +package org.folio.search.sample; + +import static org.folio.search.utils.JsonConverter.MAP_TYPE_REFERENCE; +import static org.folio.search.utils.TestUtils.readJsonFromFile; + +import java.util.Map; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class SampleBibframe { + + private static final Map BIBFRAME_AS_MAP = + readJsonFromFile("/samples/bibframe/bibframe.json", MAP_TYPE_REFERENCE); + + private static final Map BIBFRAME_2_AS_MAP = + readJsonFromFile("/samples/bibframe/bibframe2.json", MAP_TYPE_REFERENCE); + + public static Map getBibframeSampleAsMap() { + return BIBFRAME_AS_MAP; + } + + public static Map getBibframe2SampleAsMap() { + return BIBFRAME_2_AS_MAP; + } +} diff --git a/src/test/java/org/folio/search/service/lccn/DefaultLccnNormalizerTest.java b/src/test/java/org/folio/search/service/lccn/DefaultLccnNormalizerTest.java new file mode 100644 index 000000000..52cd187ce --- /dev/null +++ b/src/test/java/org/folio/search/service/lccn/DefaultLccnNormalizerTest.java @@ -0,0 +1,21 @@ +package org.folio.search.service.lccn; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.folio.spring.testing.type.UnitTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +@UnitTest +class DefaultLccnNormalizerTest { + private final DefaultLccnNormalizer lccnNormalizer = new DefaultLccnNormalizer(); + + @DisplayName("LCCN value normalization") + @CsvSource({"n 1234,n1234", " N 1234 ,n1234", "*1234,*1234", "1234*,1234*"}) + @ParameterizedTest(name = "[{index}] value={0}, expected={1}") + void getLccnNormalized_parameterized(String value, String expected) { + var normalized = lccnNormalizer.apply(value); + assertThat(normalized).contains(expected); + } +} diff --git a/src/test/java/org/folio/search/service/lccn/LccnNormalizerStructurebTest.java b/src/test/java/org/folio/search/service/lccn/LccnNormalizerStructurebTest.java new file mode 100644 index 000000000..528a7afab --- /dev/null +++ b/src/test/java/org/folio/search/service/lccn/LccnNormalizerStructurebTest.java @@ -0,0 +1,65 @@ +package org.folio.search.service.lccn; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Optional; +import org.folio.spring.testing.type.UnitTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +@UnitTest +class LccnNormalizerStructurebTest { + private final LccnNormalizerStructureB lccnNormalizer = new LccnNormalizerStructureB(); + + @Test + void shouldRemoveSpaces() { + assertThat(lccnNormalizer.apply(" 2017000002")).isEqualTo(Optional.of("2017000002")); + assertThat(lccnNormalizer.apply("2017000002 ")).isEqualTo(Optional.of("2017000002")); + assertThat(lccnNormalizer.apply("2017 000002")).isEqualTo(Optional.of("2017000002")); + assertThat(lccnNormalizer.apply(" 20 17000 002 ")).isEqualTo(Optional.of("2017000002")); + } + + @ParameterizedTest + @CsvSource({ + "2012425165//r75, 2012425165", + "2022139101/AC/r932, 2022139101", + }) + void shouldRemoveForwardSlash(String input, String expected) { + assertThat(lccnNormalizer.apply(input)).isEqualTo(Optional.of(expected)); + } + + @Test + void shouldRemoveHyphen() { + assertThat(lccnNormalizer.apply("2022-890351")).isEqualTo(Optional.of("2022890351")); + } + + @ParameterizedTest + @CsvSource({ + "2011-89035, 2011089035", + "2020-2, 2020000002", + }) + void shouldNormalizeSerialNumber(String input, String expected) { + assertThat(lccnNormalizer.apply(input)).isEqualTo(Optional.of(expected)); + } + + @Test + void shouldNormalizeSpacesAndHyphenAndForwardSlash() { + assertThat(lccnNormalizer.apply(" 20 20-2 //r23/AC")).isEqualTo(Optional.of("2020000002")); + } + + @ParameterizedTest + @ValueSource(strings = { + "01234567891", // more than 10 digits + "2020-2-34", // more than one hyphen + "A017000002", // non-digit character + "/2017000002", // slash in the beginning + "", // empty string + "202-0234334", // "-" is in third index (instead of forth) + "2020-", // "-" is the last character + }) + void shouldReturnEmptyOptionalWhenLccnIsNotValid(String toNormalize) { + assertThat(lccnNormalizer.apply(toNormalize)).isEmpty(); + } +} diff --git a/src/test/java/org/folio/search/service/setter/authority/LccnAuthorityProcessorTest.java b/src/test/java/org/folio/search/service/setter/authority/LccnAuthorityProcessorTest.java index 6f36b0d0f..84e1dd899 100644 --- a/src/test/java/org/folio/search/service/setter/authority/LccnAuthorityProcessorTest.java +++ b/src/test/java/org/folio/search/service/setter/authority/LccnAuthorityProcessorTest.java @@ -16,14 +16,15 @@ import org.folio.search.domain.dto.Identifier; import org.folio.search.integration.ReferenceDataService; import org.folio.search.model.client.CqlQueryParam; +import org.folio.search.service.lccn.DefaultLccnNormalizer; import org.folio.spring.testing.type.UnitTest; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -31,11 +32,16 @@ @ExtendWith(MockitoExtension.class) class LccnAuthorityProcessorTest { - @InjectMocks private LccnAuthorityProcessor lccnProcessor; @Mock private ReferenceDataService referenceDataService; + @BeforeEach + void setup() { + var lccnNormalizer = new DefaultLccnNormalizer(); + lccnProcessor = new LccnAuthorityProcessor(referenceDataService, lccnNormalizer); + } + @MethodSource("lccnDataProvider") @DisplayName("getFieldValue_parameterized") @ParameterizedTest(name = "[{index}] authority with {0}, expected={2}") diff --git a/src/test/java/org/folio/search/service/setter/instance/LccnInstanceProcessorTest.java b/src/test/java/org/folio/search/service/setter/instance/LccnInstanceProcessorTest.java index e2a39ad94..8536d156d 100644 --- a/src/test/java/org/folio/search/service/setter/instance/LccnInstanceProcessorTest.java +++ b/src/test/java/org/folio/search/service/setter/instance/LccnInstanceProcessorTest.java @@ -16,14 +16,15 @@ import org.folio.search.domain.dto.Instance; import org.folio.search.integration.ReferenceDataService; import org.folio.search.model.client.CqlQueryParam; +import org.folio.search.service.lccn.DefaultLccnNormalizer; import org.folio.spring.testing.type.UnitTest; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -35,9 +36,14 @@ class LccnInstanceProcessorTest { @Mock private ReferenceDataService referenceDataService; - @InjectMocks private LccnInstanceProcessor lccnProcessor; + @BeforeEach + void setup() { + var lccnNormalizer = new DefaultLccnNormalizer(); + lccnProcessor = new LccnInstanceProcessor(referenceDataService, lccnNormalizer); + } + @MethodSource("lccnDataProvider") @DisplayName("getFieldValue_parameterized") @ParameterizedTest(name = "[{index}] instance with {0}, expected={2}") 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 dfc6d31c4..5f8c14f3d 100644 --- a/src/test/java/org/folio/search/support/base/ApiEndpoints.java +++ b/src/test/java/org/folio/search/support/base/ApiEndpoints.java @@ -77,6 +77,10 @@ public static String instanceClassificationBrowsePath(BrowseOptionType optionTyp return "/browse/classification-numbers/" + optionType.getValue() + "/instances"; } + public static String bibframeSearchPath() { + return "/search/bibframe"; + } + 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 386ce8eb9..96c644e06 100644 --- a/src/test/java/org/folio/search/support/base/BaseIntegrationTest.java +++ b/src/test/java/org/folio/search/support/base/BaseIntegrationTest.java @@ -5,9 +5,11 @@ import static org.awaitility.Durations.TWO_HUNDRED_MILLISECONDS; import static org.awaitility.Durations.TWO_MINUTES; import static org.folio.search.support.base.ApiEndpoints.authoritySearchPath; +import static org.folio.search.support.base.ApiEndpoints.bibframeSearchPath; 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; +import static org.folio.search.utils.TestConstants.bibframeTopic; import static org.folio.search.utils.TestConstants.inventoryAuthorityTopic; import static org.folio.search.utils.TestUtils.asJsonString; import static org.folio.search.utils.TestUtils.doIfNotNull; @@ -37,6 +39,7 @@ import lombok.SneakyThrows; import lombok.extern.log4j.Log4j2; import org.folio.search.domain.dto.Authority; +import org.folio.search.domain.dto.Bibframe; import org.folio.search.domain.dto.FeatureConfig; import org.folio.search.domain.dto.Instance; import org.folio.search.domain.dto.ResourceEvent; @@ -156,6 +159,11 @@ protected static ResultActions doSearchByAuthorities(String query) { return doSearch(authoritySearchPath(), TENANT_ID, query, null, null, null); } + @SneakyThrows + protected static ResultActions doSearchByBibframe(String query) { + return doSearch(bibframeSearchPath(), TENANT_ID, query, null, null, null); + } + @SneakyThrows protected static ResultActions attemptSearchByAuthorities(String query) { return attemptSearch(authoritySearchPath(), TENANT_ID, query, null, null, null); @@ -275,7 +283,12 @@ protected static void setUpTenant(Class type, String tenant, Runnable postIni if (type.equals(Authority.class)) { setUpTenant(tenant, authoritySearchPath(), postInitAction, asList(records), expectedCount, - record -> kafkaTemplate.send(inventoryAuthorityTopic(tenant), resourceEvent(null, null, record))); + authority -> kafkaTemplate.send(inventoryAuthorityTopic(tenant), resourceEvent(null, null, authority))); + } + + if (type.equals(Bibframe.class)) { + setUpTenant(tenant, bibframeSearchPath(), postInitAction, asList(records), expectedCount, + bibframe -> kafkaTemplate.send(bibframeTopic(tenant), resourceEvent(null, null, bibframe))); } } diff --git a/src/test/java/org/folio/search/utils/SearchUtilsTest.java b/src/test/java/org/folio/search/utils/SearchUtilsTest.java index bc16a1e7a..00e23e532 100644 --- a/src/test/java/org/folio/search/utils/SearchUtilsTest.java +++ b/src/test/java/org/folio/search/utils/SearchUtilsTest.java @@ -11,7 +11,6 @@ import static org.folio.search.utils.SearchUtils.getIndexName; import static org.folio.search.utils.SearchUtils.getResourceName; import static org.folio.search.utils.SearchUtils.getTotalPages; -import static org.folio.search.utils.SearchUtils.normalizeLccn; import static org.folio.search.utils.SearchUtils.performExceptionalOperation; import static org.folio.search.utils.SearchUtils.updateMultilangPlainFieldKey; import static org.folio.search.utils.TestConstants.INDEX_NAME; @@ -91,14 +90,6 @@ void getTotalPages_parameterized(long total, long expected) { assertThat(totalPages).isEqualTo(expected); } - @DisplayName("LCCN value normalization") - @CsvSource({"n 1234,n1234", " N 1234 ,n1234", "*1234,*1234", "1234*,1234*"}) - @ParameterizedTest(name = "[{index}] value={0}, expected={1}") - void getLccnNormalized_parameterized(String value, String expected) { - var normalized = normalizeLccn(value); - assertThat(normalized).isEqualTo(expected); - } - @CsvSource({ "path,path", "object.value,object.value", diff --git a/src/test/java/org/folio/search/utils/TestConstants.java b/src/test/java/org/folio/search/utils/TestConstants.java index cc3aec769..d9805e3d4 100644 --- a/src/test/java/org/folio/search/utils/TestConstants.java +++ b/src/test/java/org/folio/search/utils/TestConstants.java @@ -37,6 +37,7 @@ public class TestConstants { public static final String INVENTORY_BOUND_WITH_TOPIC = "inventory.bound-with"; public static final String INVENTORY_CLASSIFICATION_TYPE_TOPIC = "inventory.classification-type"; public static final String CONSORTIUM_INSTANCE_TOPIC = "search.consortium.instance"; + public static final String BIBFRAME_TOPIC = "search.bibframe"; public static final String LOCAL_CN_TYPE = "6fd29f52-5c9c-44d0-b529-e9c5eb3a0aba"; public static final String FOLIO_CN_TYPE = "6e4d7565-b277-4dfa-8b7d-fbf306d9d0cd"; @@ -119,6 +120,10 @@ public static String inventoryBoundWithTopic(String tenantId) { return getTopicName(tenantId, INVENTORY_BOUND_WITH_TOPIC); } + public static String bibframeTopic(String tenantId) { + return getTopicName(tenantId, BIBFRAME_TOPIC); + } + public static String indexName(String tenantId) { return String.join("_", ENV, INSTANCE_RESOURCE, tenantId); } diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 0b4f4b263..939849430 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -110,6 +110,9 @@ folio: - name: inventory.location numPartitions: 1 replicationFactor: 1 + - name: search.bibframe + numPartitions: 1 + replicationFactor: 1 listener: events: concurrency: 2 @@ -139,6 +142,10 @@ folio: concurrency: 1 topic-pattern: (${folio.environment}\.)(.*\.)inventory\.location group-id: ${folio.environment}-mod-search-location-type-group + bibframe: + concurrency: ${KAFKA_BIBFRAME_CONCURRENCY:1} + topic-pattern: ${KAFKA_BIBFRAME_CONSUMER_PATTERN:(${folio.environment}\.)(.*\.)search\.bibframe} + group-id: ${folio.environment}-mod-search-bibframe-group okapiUrl: ${okapi.url} logging: request: diff --git a/src/test/resources/samples/bibframe/bibframe.json b/src/test/resources/samples/bibframe/bibframe.json new file mode 100644 index 000000000..96a1faa67 --- /dev/null +++ b/src/test/resources/samples/bibframe/bibframe.json @@ -0,0 +1,131 @@ +{ + "id": "123456123456", + "titles": [ + { + "value": "titleAbc def", + "type": "Main" + }, + { + "value": "sub", + "type": "Sub" + } + ], + "contributors": [ + { + "name": "Family", + "type": "Family", + "isCreator": true + }, + { + "name": "Meeting", + "type": "Meeting", + "isCreator": false + }, + { + "name": "Organization", + "type": "Organization", + "isCreator": true + }, + { + "name": "Person", + "type": "Person", + "isCreator": false + }, + { + "name": "common", + "type": "Family", + "isCreator": true + } + ], + "languages": [ + { + "value": "eng" + }, + { + "value": "rus" + } + ], + "classifications": [ + { + "number": "1234", + "source": "ddc" + }, + { + "number": "5678", + "source": "other" + } + ], + "subjects": [ + { + "value": "Subject 1" + }, + { + "value": "Subject 2" + } + ], + "instances": [ + { + "id": "instance1", + "titles": [ + { + "value": "Instance1_Title", + "type": "Main" + }, + { + "value": "Instance1_Subtitle", + "type": "Sub" + } + ], + "identifiers": [ + { + "value": "1234567890123", + "type": "ISBN" + }, + { + "value": " 2023-202345/AC/r932", + "type": "LCCN" + } + ], + "contributors": [ + { + "name": "Instance1_Family", + "type": "Family", + "isCreator": true + }, + { + "name": "Instance1_Meeting", + "type": "Meeting", + "isCreator": false + }, + { + "name": "Instance1_Organization", + "type": "Organization", + "isCreator": true + }, + { + "name": "Instance1_Person", + "type": "Person", + "isCreator": false + } + ], + "publications": [ + { + "name": "publisher", + "date": 2023 + }, + { + "name": "publisher2", + "date": 2024 + } + ], + "editionStatements": [ + { + "value": "1st edition" + }, + { + "value": "2nd edition" + } + ] + } + ] +} diff --git a/src/test/resources/samples/bibframe/bibframe2.json b/src/test/resources/samples/bibframe/bibframe2.json new file mode 100644 index 000000000..f1f7a802f --- /dev/null +++ b/src/test/resources/samples/bibframe/bibframe2.json @@ -0,0 +1,40 @@ +{ + "id": "999999999999", + "titles": [ + { + "value": "titleAbc xyz", + "type": "Main" + }, + { + "value": "sub", + "type": "Sub" + } + ], + "contributors": [ + { + "name": "name one", + "type": "Family", + "isCreator": true + }, + { + "name": "name two", + "type": "Meeting", + "isCreator": false + }, + { + "name": "name three", + "type": "Organization", + "isCreator": true + }, + { + "name": "name four", + "type": "Person", + "isCreator": false + }, + { + "name": "common", + "type": "Family", + "isCreator": true + } + ] +}