diff --git a/NEWS.md b/NEWS.md index 430c5098a..64c27f94b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -21,6 +21,7 @@ * Add tenantId/shared fields to contributors/subjects ([MSEARCH-551](https://issues.folio.org/browse/MSEARCH-551)) * Implement Active Affiliation Context for stream IDs in Consortia Mode ([MSEARCH-576](https://issues.folio.org/browse/MSEARCH-576)) * Restrict central tenant queries to only shared records ([MSEARCH-588](https://issues.folio.org/browse/MSEARCH-588)) +* Implement Active Affiliation Context for browsing ([MSEARCH-580](https://issues.folio.org/browse/MSEARCH-580)) ### Bug fixes * Fix bug when number of titles response is greater than real ([MSEARCH-526](https://issues.folio.org/browse/MSEARCH-526)) diff --git a/src/main/java/org/folio/search/cql/CqlSearchQueryConverter.java b/src/main/java/org/folio/search/cql/CqlSearchQueryConverter.java index 4bab8a6f2..7c4b1b5be 100644 --- a/src/main/java/org/folio/search/cql/CqlSearchQueryConverter.java +++ b/src/main/java/org/folio/search/cql/CqlSearchQueryConverter.java @@ -3,23 +3,17 @@ import static org.folio.search.utils.SearchQueryUtils.isBoolQuery; import static org.folio.search.utils.SearchQueryUtils.isDisjunctionFilterQuery; import static org.folio.search.utils.SearchQueryUtils.isFilterQuery; -import static org.folio.search.utils.SearchUtils.SHARED_FIELD_NAME; -import static org.folio.search.utils.SearchUtils.TENANT_ID_FIELD_NAME; import static org.opensearch.index.query.QueryBuilders.boolQuery; -import static org.opensearch.index.query.QueryBuilders.termQuery; import java.util.ArrayList; -import java.util.LinkedList; import java.util.List; import java.util.function.Function; import java.util.function.Predicate; import lombok.RequiredArgsConstructor; import org.folio.search.model.types.SearchType; -import org.folio.search.service.consortium.ConsortiumTenantService; +import org.folio.search.service.consortium.ConsortiumSearchHelper; import org.folio.search.service.metadata.SearchFieldProvider; -import org.folio.spring.FolioExecutionContext; import org.opensearch.index.query.BoolQueryBuilder; -import org.opensearch.index.query.MatchAllQueryBuilder; import org.opensearch.index.query.QueryBuilder; import org.opensearch.search.builder.SearchSourceBuilder; import org.springframework.stereotype.Component; @@ -43,8 +37,7 @@ public class CqlSearchQueryConverter { private final CqlSortProvider cqlSortProvider; private final SearchFieldProvider searchFieldProvider; private final CqlTermQueryConverter cqlTermQueryConverter; - private final FolioExecutionContext folioExecutionContext; - private final ConsortiumTenantService consortiumTenantService; + private final ConsortiumSearchHelper consortiumSearchHelper; /** * Converts given CQL search query value to the elasticsearch {@link SearchSourceBuilder} object. @@ -66,9 +59,6 @@ public SearchSourceBuilder convert(String query, String resource) { return queryBuilder.query(enhancedQuery); } - //todo(MSEARCH-576): may be reworked after implemented for browse/streamIds. - // Implemented separately because it crashes 'browse/streamIds' functionality. - /** * Converts given CQL search query value to the elasticsearch {@link SearchSourceBuilder} object. * Wraps base 'convert' and adds tenantId+shared filter in case of consortia mode @@ -79,7 +69,7 @@ public SearchSourceBuilder convert(String query, String resource) { */ public SearchSourceBuilder convertForConsortia(String query, String resource) { var sourceBuilder = convert(query, resource); - var queryBuilder = filterForActiveAffiliation(sourceBuilder.query()); + var queryBuilder = consortiumSearchHelper.filterQueryForActiveAffiliation(sourceBuilder.query()); return sourceBuilder.query(queryBuilder); } @@ -156,61 +146,6 @@ private BoolQueryBuilder flattenBoolQuery(CQLBooleanNode node, String resource, return boolQuery; } - private QueryBuilder filterForActiveAffiliation(QueryBuilder query) { - var contextTenantId = folioExecutionContext.getTenantId(); - var centralTenantId = consortiumTenantService.getCentralTenant(contextTenantId); - if (centralTenantId.isEmpty()) { - return query; - } - - var boolQuery = prepareBoolQueryForActiveAffiliation(query); - addActiveAffiliationClauses(boolQuery, contextTenantId, centralTenantId.get()); - - return boolQuery; - } - - private BoolQueryBuilder prepareBoolQueryForActiveAffiliation(QueryBuilder query) { - BoolQueryBuilder boolQuery; - if (query instanceof MatchAllQueryBuilder) { - boolQuery = boolQuery(); - } else if (query instanceof BoolQueryBuilder bq) { - boolQuery = bq; - } else { - boolQuery = boolQuery().must(query); - } - boolQuery.minimumShouldMatch(1); - return boolQuery; - } - - private void addActiveAffiliationClauses(BoolQueryBuilder boolQuery, String contextTenantId, String centralTenantId) { - var affiliationShouldClauses = getAffiliationShouldClauses(contextTenantId, centralTenantId); - if (boolQuery.should().isEmpty()) { - affiliationShouldClauses.forEach(boolQuery::should); - } else { - var innerBoolQuery = boolQuery(); - affiliationShouldClauses.forEach(innerBoolQuery::should); - boolQuery.must(innerBoolQuery); - } - } - - private LinkedList getAffiliationShouldClauses(String contextTenantId, String centralTenantId) { - var affiliationShouldClauses = new LinkedList(); - addTenantIdAffiliationShouldClause(contextTenantId, centralTenantId, affiliationShouldClauses); - addSharedAffiliationShouldClause(affiliationShouldClauses); - return affiliationShouldClauses; - } - - private void addTenantIdAffiliationShouldClause(String contextTenantId, String centralTenantId, - LinkedList affiliationShouldClauses) { - if (!contextTenantId.equals(centralTenantId)) { - affiliationShouldClauses.add(termQuery(TENANT_ID_FIELD_NAME, contextTenantId)); - } - } - - private void addSharedAffiliationShouldClause(LinkedList affiliationShouldClauses) { - affiliationShouldClauses.add(termQuery(SHARED_FIELD_NAME, true)); - } - private QueryBuilder enhanceQuery(QueryBuilder query, String resource) { Predicate filterFieldCheck = field -> isFilterField(field, resource); if (isDisjunctionFilterQuery(query, filterFieldCheck)) { diff --git a/src/main/java/org/folio/search/model/index/SubjectResource.java b/src/main/java/org/folio/search/model/index/SubjectResource.java index 85ad3c7bd..ce74bd330 100644 --- a/src/main/java/org/folio/search/model/index/SubjectResource.java +++ b/src/main/java/org/folio/search/model/index/SubjectResource.java @@ -1,9 +1,15 @@ package org.folio.search.model.index; import java.util.Set; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@Builder +@NoArgsConstructor +@AllArgsConstructor public class SubjectResource { private String id; diff --git a/src/main/java/org/folio/search/service/browse/AbstractBrowseServiceBySearchAfter.java b/src/main/java/org/folio/search/service/browse/AbstractBrowseServiceBySearchAfter.java index 39dfaac2b..d5901d7b4 100644 --- a/src/main/java/org/folio/search/service/browse/AbstractBrowseServiceBySearchAfter.java +++ b/src/main/java/org/folio/search/service/browse/AbstractBrowseServiceBySearchAfter.java @@ -8,21 +8,14 @@ import static org.springframework.core.GenericTypeResolver.resolveTypeArguments; import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; import lombok.extern.log4j.Log4j2; import org.folio.search.model.BrowseResult; import org.folio.search.model.SearchResult; -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.repository.SearchRepository; import org.folio.search.service.converter.ElasticsearchDocumentConverter; import org.opensearch.action.search.MultiSearchResponse.Item; -import org.opensearch.index.query.TermQueryBuilder; import org.opensearch.search.builder.SearchSourceBuilder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.Assert; @@ -30,8 +23,6 @@ @Log4j2 public abstract class AbstractBrowseServiceBySearchAfter extends AbstractBrowseService { - private static final String SHARED_FILTER_KEY = "instances.shared"; - protected SearchRepository searchRepository; protected ElasticsearchDocumentConverter documentConverter; protected Class browseResponseClass; @@ -130,28 +121,6 @@ protected abstract SearchSourceBuilder getSearchQuery( protected abstract BrowseResult mapToBrowseResult(BrowseContext context, SearchResult searchResult, boolean isAnchor); - protected Set filterSubResourcesForConsortium( - BrowseContext context, R resource, - Function> subResourceExtractor) { - - var subResources = subResourceExtractor.apply(resource); - var sharedFilter = getBrowseFilter(context, SHARED_FILTER_KEY); - - return sharedFilter.map(shared -> subResources.stream() - .filter(subResource -> subResource.getShared().equals(Boolean.valueOf(shared))) - .collect(Collectors.toSet())) - .orElse(subResources); - } - - private Optional getBrowseFilter(BrowseContext context, String filterKey) { - return context.getFilters().stream() - .map(filter -> filter instanceof TermQueryBuilder termFilter && termFilter.fieldName().equals(filterKey) - ? String.valueOf(termFilter.value()) - : null) - .filter(Objects::nonNull) - .findFirst(); - } - private BrowseResult createBrowseResult(Item[] responses, BrowseRequest request, BrowseContext context) { var precedingResult = documentConverter.convertToSearchResult(responses[0].getResponse(), browseResponseClass); var succeedingResult = documentConverter.convertToSearchResult(responses[1].getResponse(), browseResponseClass); diff --git a/src/main/java/org/folio/search/service/browse/AuthorityBrowseService.java b/src/main/java/org/folio/search/service/browse/AuthorityBrowseService.java index 78c9a2e2d..b78b5899e 100644 --- a/src/main/java/org/folio/search/service/browse/AuthorityBrowseService.java +++ b/src/main/java/org/folio/search/service/browse/AuthorityBrowseService.java @@ -23,6 +23,7 @@ import org.folio.search.model.SearchResult; import org.folio.search.model.service.BrowseContext; import org.folio.search.model.service.BrowseRequest; +import org.folio.search.service.consortium.ConsortiumSearchHelper; import org.folio.search.service.metadata.SearchFieldProvider; import org.opensearch.index.query.TermsQueryBuilder; import org.opensearch.search.builder.SearchSourceBuilder; @@ -37,6 +38,7 @@ public class AuthorityBrowseService extends AbstractBrowseServiceBySearchAfter mapToBrowseResult(BrowseContext context, SearchResult result, @@ -62,7 +64,8 @@ protected SearchSourceBuilder getSearchQuery(BrowseRequest request, BrowseContex var boolQuery = boolQuery().filter(FILTER_QUERY); ctx.getFilters().forEach(boolQuery::filter); - return searchSource().query(boolQuery) + var query = consortiumSearchHelper.filterQueryForActiveAffiliation(boolQuery); + return searchSource().query(query) .searchAfter(new Object[] {ctx.getAnchor().toLowerCase(ROOT)}) .sort(fieldSort(request.getTargetField()).order(isBrowsingForward ? ASC : DESC)) .size(ctx.getLimit(isBrowsingForward) + 1) @@ -77,7 +80,8 @@ protected SearchSourceBuilder getAnchorSearchQuery(BrowseRequest request, Browse var boolQuery = boolQuery().filter(FILTER_QUERY).must(termQuery(request.getTargetField(), context.getAnchor())); context.getFilters().forEach(boolQuery::filter); - return searchSource().query(boolQuery).from(0).size(1).fetchSource(getIncludedSourceFields(request), null); + var query = consortiumSearchHelper.filterQueryForActiveAffiliation(boolQuery); + return searchSource().query(query).from(0).size(1).fetchSource(getIncludedSourceFields(request), null); } @Override diff --git a/src/main/java/org/folio/search/service/browse/BrowseContextProvider.java b/src/main/java/org/folio/search/service/browse/BrowseContextProvider.java index 77afc3fa7..ee36434b2 100644 --- a/src/main/java/org/folio/search/service/browse/BrowseContextProvider.java +++ b/src/main/java/org/folio/search/service/browse/BrowseContextProvider.java @@ -38,7 +38,6 @@ public class BrowseContextProvider { public BrowseContext get(BrowseRequest request) { log.debug("get:: by [query: {}, resource: {}]", request.getQuery(), request.getResource()); - // todo(MSEARCH-580): use 'convertForConsortia' or/and check todo item for 'convertForConsortia' var searchSource = cqlSearchQueryConverter.convert(request.getQuery(), request.getResource()); var cqlQuery = request.getQuery(); if (isNotEmpty(searchSource.sorts())) { diff --git a/src/main/java/org/folio/search/service/browse/CallNumberBrowseQueryProvider.java b/src/main/java/org/folio/search/service/browse/CallNumberBrowseQueryProvider.java index 921d19aee..bd55a86c7 100644 --- a/src/main/java/org/folio/search/service/browse/CallNumberBrowseQueryProvider.java +++ b/src/main/java/org/folio/search/service/browse/CallNumberBrowseQueryProvider.java @@ -20,6 +20,7 @@ import org.folio.search.model.service.BrowseContext; import org.folio.search.model.service.BrowseRequest; import org.folio.search.model.types.CallNumberType; +import org.folio.search.service.consortium.ConsortiumSearchHelper; import org.folio.search.service.metadata.SearchFieldProvider; import org.opensearch.index.query.QueryBuilder; import org.opensearch.script.Script; @@ -38,6 +39,7 @@ public class CallNumberBrowseQueryProvider { private final SearchFieldProvider searchFieldProvider; private final SearchQueryConfigurationProperties queryConfiguration; private final CallNumberBrowseRangeService callNumberBrowseRangeService; + private final ConsortiumSearchHelper consortiumSearchHelper; /** * Creates query as {@link SearchSourceBuilder} object for call number browsing. @@ -56,8 +58,10 @@ public SearchSourceBuilder get(BrowseRequest request, BrowseContext ctx, boolean var multiplier = queryConfiguration.getRangeQueryLimitMultiplier(); var pageSize = (int) Math.max(MIN_QUERY_SIZE, Math.ceil(ctx.getLimit(isBrowsingForward) * multiplier)); + var initialQuery = getQuery(ctx, request, pageSize, isBrowsingForward); + var query = consortiumSearchHelper.filterQueryForActiveAffiliation(initialQuery); var searchSource = searchSource().from(0).size(pageSize) - .query(getQuery(ctx, request, pageSize, isBrowsingForward)) + .query(query) .sort(scriptSort(script, STRING).order(isBrowsingForward ? ASC : DESC)); if (isFalse(request.getExpandAll())) { diff --git a/src/main/java/org/folio/search/service/browse/ContributorBrowseService.java b/src/main/java/org/folio/search/service/browse/ContributorBrowseService.java index 47f965e16..ea9bb1bdd 100644 --- a/src/main/java/org/folio/search/service/browse/ContributorBrowseService.java +++ b/src/main/java/org/folio/search/service/browse/ContributorBrowseService.java @@ -11,6 +11,7 @@ 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 one.util.streamex.StreamEx; import org.folio.search.domain.dto.InstanceContributorBrowseItem; @@ -20,6 +21,7 @@ import org.folio.search.model.index.InstanceSubResource; import org.folio.search.model.service.BrowseContext; import org.folio.search.model.service.BrowseRequest; +import org.folio.search.service.consortium.ConsortiumSearchHelper; import org.opensearch.index.query.QueryBuilder; import org.opensearch.search.builder.SearchSourceBuilder; import org.opensearch.search.sort.SortMode; @@ -27,6 +29,7 @@ @Log4j2 @Service +@RequiredArgsConstructor public class ContributorBrowseService extends AbstractBrowseServiceBySearchAfter { @@ -34,12 +37,15 @@ public class ContributorBrowseService extends private static final String CONTRIBUTOR_NAME_TYPE_ID_FIELD = "contributorNameTypeId"; private static final String CONTRIBUTOR_TYPE_ID_FIELD = "instances.contributorTypeId"; + private final ConsortiumSearchHelper consortiumSearchHelper; + @Override protected SearchSourceBuilder getAnchorSearchQuery(BrowseRequest request, BrowseContext context) { log.debug("getAnchorSearchQuery:: by [request: {}]", request); var boolQuery = boolQuery().must(termQuery(request.getTargetField(), context.getAnchor())); context.getFilters().forEach(boolQuery::filter); - return searchSource().query(boolQuery) + var query = consortiumSearchHelper.filterBrowseQueryForActiveAffiliation(context, boolQuery); + return searchSource().query(query) .size(context.getLimit(context.isBrowsingForward())) .from(0); } @@ -56,6 +62,7 @@ protected SearchSourceBuilder getSearchQuery(BrowseRequest req, BrowseContext ct ctx.getFilters().forEach(boolQuery::filter); query = boolQuery; } + query = consortiumSearchHelper.filterBrowseQueryForActiveAffiliation(ctx, query); return searchSource().query(query) .searchAfter(new Object[] {ctx.getAnchor().toLowerCase(ROOT), null, null, null}) .sort(fieldSort(req.getTargetField()).order(isBrowsingForward ? ASC : DESC)) @@ -77,7 +84,7 @@ protected BrowseResult mapToBrowseResult(BrowseCo boolean isAnchor) { return BrowseResult.of(res) .map(item -> { - var filteredInstanceResources = filterSubResourcesForConsortium(context, item, + var filteredInstanceResources = consortiumSearchHelper.filterSubResourcesForConsortium(context, item, ContributorResource::getInstances); var typeIds = filteredInstanceResources.stream() .map(InstanceSubResource::getTypeId) diff --git a/src/main/java/org/folio/search/service/browse/SubjectBrowseService.java b/src/main/java/org/folio/search/service/browse/SubjectBrowseService.java index 86b02ae43..bd5b45898 100644 --- a/src/main/java/org/folio/search/service/browse/SubjectBrowseService.java +++ b/src/main/java/org/folio/search/service/browse/SubjectBrowseService.java @@ -17,8 +17,10 @@ import org.folio.search.model.index.SubjectResource; import org.folio.search.model.service.BrowseContext; import org.folio.search.model.service.BrowseRequest; +import org.folio.search.service.consortium.ConsortiumSearchHelper; import org.opensearch.index.query.QueryBuilder; import org.opensearch.search.builder.SearchSourceBuilder; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Log4j2 @@ -26,10 +28,19 @@ @RequiredArgsConstructor public class SubjectBrowseService extends AbstractBrowseServiceBySearchAfter { + private ConsortiumSearchHelper consortiumSearchHelper; + + @Autowired + public void setConsortiumSearchHelper(ConsortiumSearchHelper consortiumSearchHelper) { + this.consortiumSearchHelper = consortiumSearchHelper; + } + @Override protected SearchSourceBuilder getAnchorSearchQuery(BrowseRequest request, BrowseContext context) { log.debug("getAnchorSearchQuery:: by [request: {}]", request); - return searchSource().query(termQuery(request.getTargetField(), context.getAnchor())) + var query = consortiumSearchHelper.filterBrowseQueryForActiveAffiliation(context, + termQuery(request.getTargetField(), context.getAnchor())); + return searchSource().query(query) .size(context.getLimit(context.isBrowsingForward())) .from(0); } @@ -45,6 +56,7 @@ protected SearchSourceBuilder getSearchQuery(BrowseRequest req, BrowseContext ct ctx.getFilters().forEach(boolQuery::filter); query = boolQuery; } + query = consortiumSearchHelper.filterBrowseQueryForActiveAffiliation(ctx, query); return searchSource().query(query) .searchAfter(new Object[] {ctx.getAnchor().toLowerCase(ROOT)}) .sort(fieldSort(req.getTargetField()).order(isBrowsingForward ? ASC : DESC)) @@ -65,7 +77,8 @@ protected BrowseResult mapToBrowseResult(BrowseContext contex .value(subjectResource.getValue()) .authorityId(subjectResource.getAuthorityId()) .isAnchor(isAnchor ? true : null) - .totalRecords(filterSubResourcesForConsortium(context, subjectResource, SubjectResource::getInstances).size())); + .totalRecords(consortiumSearchHelper.filterSubResourcesForConsortium( + context, subjectResource, SubjectResource::getInstances).size())); } @Override diff --git a/src/main/java/org/folio/search/service/consortium/ConsortiumSearchHelper.java b/src/main/java/org/folio/search/service/consortium/ConsortiumSearchHelper.java new file mode 100644 index 000000000..1e7d16a4d --- /dev/null +++ b/src/main/java/org/folio/search/service/consortium/ConsortiumSearchHelper.java @@ -0,0 +1,173 @@ +package org.folio.search.service.consortium; + +import static org.apache.commons.lang3.StringUtils.EMPTY; +import static org.folio.search.utils.SearchUtils.SHARED_FIELD_NAME; +import static org.folio.search.utils.SearchUtils.TENANT_ID_FIELD_NAME; +import static org.opensearch.index.query.QueryBuilders.boolQuery; +import static org.opensearch.index.query.QueryBuilders.termQuery; + +import java.util.LinkedList; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.folio.search.model.index.InstanceSubResource; +import org.folio.search.model.service.BrowseContext; +import org.folio.spring.FolioExecutionContext; +import org.opensearch.index.query.BoolQueryBuilder; +import org.opensearch.index.query.MatchAllQueryBuilder; +import org.opensearch.index.query.QueryBuilder; +import org.opensearch.index.query.TermQueryBuilder; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ConsortiumSearchHelper { + + private static final String BROWSE_SHARED_FILTER_KEY = "instances.shared"; + + private final FolioExecutionContext folioExecutionContext; + private final ConsortiumTenantService consortiumTenantService; + + public QueryBuilder filterQueryForActiveAffiliation(QueryBuilder query) { + var contextTenantId = folioExecutionContext.getTenantId(); + var centralTenantId = consortiumTenantService.getCentralTenant(contextTenantId); + if (centralTenantId.isEmpty()) { + return query; + } + + return filterQueryForActiveAffiliation(query, contextTenantId, centralTenantId.get()); + } + + public QueryBuilder filterQueryForActiveAffiliation(QueryBuilder query, String tenantId, String centralTenantId) { + return filterQueryForActiveAffiliation(EMPTY, query, tenantId, centralTenantId); + } + + public QueryBuilder filterQueryForActiveAffiliation(String fieldPrefix, QueryBuilder query, String tenantId, + String centralTenantId) { + var boolQuery = prepareBoolQueryForActiveAffiliation(query); + addActiveAffiliationClauses(fieldPrefix, boolQuery, tenantId, centralTenantId); + + return boolQuery; + } + + private BoolQueryBuilder prepareBoolQueryForActiveAffiliation(QueryBuilder query) { + BoolQueryBuilder boolQuery; + if (query instanceof MatchAllQueryBuilder) { + boolQuery = boolQuery(); + } else if (query instanceof BoolQueryBuilder bq) { + boolQuery = bq; + } else { + boolQuery = boolQuery().must(query); + } + boolQuery.minimumShouldMatch(1); + return boolQuery; + } + + private void addActiveAffiliationClauses(String fieldPrefix, BoolQueryBuilder boolQuery, String contextTenantId, + String centralTenantId) { + var affiliationShouldClauses = getAffiliationShouldClauses(fieldPrefix, contextTenantId, centralTenantId); + if (boolQuery.should().isEmpty()) { + affiliationShouldClauses.forEach(boolQuery::should); + } else { + var innerBoolQuery = boolQuery(); + affiliationShouldClauses.forEach(innerBoolQuery::should); + boolQuery.must(innerBoolQuery); + } + } + + private LinkedList getAffiliationShouldClauses(String fieldPrefix, + String contextTenantId, String centralTenantId) { + var affiliationShouldClauses = new LinkedList(); + addTenantIdAffiliationShouldClause(fieldPrefix, contextTenantId, centralTenantId, affiliationShouldClauses); + addSharedAffiliationShouldClause(fieldPrefix, affiliationShouldClauses); + return affiliationShouldClauses; + } + + private void addTenantIdAffiliationShouldClause(String fieldPrefix, String contextTenantId, String centralTenantId, + LinkedList affiliationShouldClauses) { + if (!contextTenantId.equals(centralTenantId)) { + affiliationShouldClauses.add(termQuery(fieldPrefix + TENANT_ID_FIELD_NAME, contextTenantId)); + } + } + + private void addSharedAffiliationShouldClause(String fieldPrefix, LinkedList affiliationShouldClauses) { + affiliationShouldClauses.add(termQuery(fieldPrefix + SHARED_FIELD_NAME, true)); + } + + /** + * Modifies query to support both 'instances.shared' filter and Active Affiliation. + * 'instances.shared' filter have precedence over Active Affiliation so in case of 'false' value - only local records + * will be returned (original query have only 'tenantId' additional filter). + * */ + public QueryBuilder filterBrowseQueryForActiveAffiliation(BrowseContext browseContext, QueryBuilder query) { + var contextTenantId = folioExecutionContext.getTenantId(); + var centralTenantId = consortiumTenantService.getCentralTenant(contextTenantId); + var sharedFilter = getBrowseSharedFilter(browseContext); + if (centralTenantId.isEmpty()) { + sharedFilter.ifPresent(filter -> browseContext.getFilters().remove(filter)); + return query; + } + + removeOriginalSharedFilterFromQuery(query); + + var shared = sharedFilter.map(this::sharedFilterValue).orElse(true); + if (Boolean.TRUE.equals(shared)) { + return filterQueryForActiveAffiliation("instances.", query, contextTenantId, centralTenantId.get()); + } + + var boolQuery = prepareBoolQueryForActiveAffiliation(query); + if (boolQuery.should().isEmpty()) { + boolQuery.minimumShouldMatch(null); + } + boolQuery.must(termQuery("instances.tenantId", contextTenantId)); + + return boolQuery; + } + + private void removeOriginalSharedFilterFromQuery(QueryBuilder queryBuilder) { + if (queryBuilder instanceof BoolQueryBuilder bqb) { + bqb.filter().removeIf(filter -> filter instanceof TermQueryBuilder tqb + && tqb.fieldName().equals(BROWSE_SHARED_FILTER_KEY)); + } + } + + public Set filterSubResourcesForConsortium( + BrowseContext context, T resource, + Function> subResourceExtractor) { + + var subResources = subResourceExtractor.apply(resource); + var contextTenantId = folioExecutionContext.getTenantId(); + var centralTenantId = consortiumTenantService.getCentralTenant(contextTenantId); + if (centralTenantId.isEmpty() || contextTenantId.equals(centralTenantId.get())) { + return subResources; + } + + var sharedFilter = getBrowseSharedFilter(context); + Predicate subResourcesFilter = + sharedFilter.isPresent() && !sharedFilterValue(sharedFilter.get()) + ? subResource -> subResource.getTenantId().equals(contextTenantId) + : subResource -> subResource.getTenantId().equals(contextTenantId) || subResource.getShared(); + return subResources.stream() + .filter(subResourcesFilter) + .collect(Collectors.toSet()); + } + + private Optional getBrowseSharedFilter(BrowseContext context) { + return context.getFilters().stream() + .map(filter -> + filter instanceof TermQueryBuilder termFilter && termFilter.fieldName().equals(BROWSE_SHARED_FILTER_KEY) + ? termFilter + : null) + .filter(Objects::nonNull) + .findFirst(); + } + + private boolean sharedFilterValue(TermQueryBuilder sharedQuery) { + return sharedQuery.value() instanceof Boolean boolValue && boolValue + || sharedQuery.value() instanceof String stringValue && Boolean.parseBoolean(stringValue); + } +} diff --git a/src/test/java/org/folio/search/controller/BrowseContributorConsortiumIT.java b/src/test/java/org/folio/search/controller/BrowseContributorConsortiumIT.java index e90deb68c..588bf58b5 100644 --- a/src/test/java/org/folio/search/controller/BrowseContributorConsortiumIT.java +++ b/src/test/java/org/folio/search/controller/BrowseContributorConsortiumIT.java @@ -146,19 +146,37 @@ private static Contributor contributor(String name, String nameTypeId, String au } @Test - void browseByContributor_withNameAndConsortiumInstanceFilter() { + void browseByContributor_shared() { var request = get(instanceContributorBrowsePath()).param("query", "(" + prepareQuery("name >= {value} or name < {value}", '"' + "Bon Jovi" + '"') + ") " + "and instances.shared==true").param("limit", "5"); var actual = parseResponse(doGet(request), InstanceContributorBrowseResult.class); - var expected = new InstanceContributorBrowseResult().totalRecords(5).prev(null).next(null).items( + var expected = new InstanceContributorBrowseResult().totalRecords(12).prev(null).next("George Harrison").items( List.of( contributorBrowseItem(1, "Anthony Kiedis", NAME_TYPE_IDS[0], AUTHORITY_IDS[1], TYPE_IDS[0]), contributorBrowseItem(1, "Anthony Kiedis", NAME_TYPE_IDS[1], AUTHORITY_IDS[1], TYPE_IDS[2]), + contributorBrowseItem(2, true, "Bon Jovi", NAME_TYPE_IDS[0], AUTHORITY_IDS[0], + TYPE_IDS[0], TYPE_IDS[1], TYPE_IDS[2]), contributorBrowseItem(1, true, "Bon Jovi", NAME_TYPE_IDS[1], AUTHORITY_IDS[1], TYPE_IDS[0]), - contributorBrowseItem(1, true, "Bon Jovi", NAME_TYPE_IDS[0], AUTHORITY_IDS[0], TYPE_IDS[0]), - contributorBrowseItem(2, "Klaus Meine", NAME_TYPE_IDS[0], AUTHORITY_IDS[1], TYPE_IDS[0], TYPE_IDS[1]))); + contributorBrowseItem(2, "George Harrison", NAME_TYPE_IDS[1], AUTHORITY_IDS[0], TYPE_IDS[2]))); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void browseByContributor_local() { + var request = get(instanceContributorBrowsePath()).param("query", + "(" + prepareQuery("name >= {value} or name < {value}", '"' + "Bon Jovi" + '"') + ") " + + "and instances.shared==false").param("limit", "5"); + + var actual = parseResponse(doGet(request), InstanceContributorBrowseResult.class); + var expected = new InstanceContributorBrowseResult().totalRecords(8).prev(null).next("John Lennon").items( + List.of( + contributorBrowseItem(1, true, "Bon Jovi", NAME_TYPE_IDS[0], AUTHORITY_IDS[0], + TYPE_IDS[1], TYPE_IDS[2]), + contributorBrowseItem(2, "George Harrison", NAME_TYPE_IDS[1], AUTHORITY_IDS[0], TYPE_IDS[2]), + contributorBrowseItem(2, "John Lennon", NAME_TYPE_IDS[2], AUTHORITY_IDS[1], TYPE_IDS[0]))); assertThat(actual).isEqualTo(expected); } diff --git a/src/test/java/org/folio/search/controller/BrowseSubjectConsortiumIT.java b/src/test/java/org/folio/search/controller/BrowseSubjectConsortiumIT.java index 3f527614f..86586383a 100644 --- a/src/test/java/org/folio/search/controller/BrowseSubjectConsortiumIT.java +++ b/src/test/java/org/folio/search/controller/BrowseSubjectConsortiumIT.java @@ -117,7 +117,7 @@ private static List> subjectBrowseInstanceData() { } @Test - void browseBySubject_browsingAroundWithConsortiumInstanceFilter() { + void browseBySubject_browsingAround_shared() { var request = get(instanceSubjectBrowsePath()) .param("query", "(" + prepareQuery("value < {value} or value >= {value}", "\"Rules\"") + ") " @@ -126,13 +126,32 @@ void browseBySubject_browsingAroundWithConsortiumInstanceFilter() { .param("precedingRecordsCount", "2"); var actual = parseResponse(doGet(request), SubjectBrowseResult.class); assertThat(actual).isEqualTo(new SubjectBrowseResult() - .totalRecords(10).prev("Music") + .totalRecords(23).prev("Philosophy").next("Science--Methodology") .items(List.of( - subjectBrowseItem(1, "Music", MUSIC_AUTHORITY_ID_2), - subjectBrowseItem(2, "Music", MUSIC_AUTHORITY_ID_1), + subjectBrowseItem(1, "Philosophy"), + subjectBrowseItem(1, "Religion"), subjectBrowseItem(1, "Rules", true), - subjectBrowseItem(1, "Text"), - subjectBrowseItem(1, "United States")))); + subjectBrowseItem(1, "Science"), + subjectBrowseItem(1, "Science--Methodology")))); + } + + @Test + void browseBySubject_browsingAround_local() { + var request = get(instanceSubjectBrowsePath()) + .param("query", "(" + + prepareQuery("value < {value} or value >= {value}", "\"Science\"") + ") " + + "and instances.shared==false") + .param("limit", "5") + .param("precedingRecordsCount", "2"); + var actual = parseResponse(doGet(request), SubjectBrowseResult.class); + assertThat(actual).isEqualTo(new SubjectBrowseResult() + .totalRecords(15).prev("Philosophy").next("Science--Philosophy") + .items(List.of( + subjectBrowseItem(1, "Philosophy"), + subjectBrowseItem(1, "Religion"), + subjectBrowseItem(1, "Science", true), + subjectBrowseItem(1, "Science--Methodology"), + subjectBrowseItem(1, "Science--Philosophy")))); } //todo: move 4 methods below to consortium integration test base in a scope of MSEARCH-562 diff --git a/src/test/java/org/folio/search/cql/CqlSearchQueryConverterTest.java b/src/test/java/org/folio/search/cql/CqlSearchQueryConverterTest.java index c700667de..fc27b670b 100644 --- a/src/test/java/org/folio/search/cql/CqlSearchQueryConverterTest.java +++ b/src/test/java/org/folio/search/cql/CqlSearchQueryConverterTest.java @@ -4,9 +4,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.folio.search.utils.SearchUtils.INSTANCE_RESOURCE; -import static org.folio.search.utils.TestConstants.CONSORTIUM_TENANT_ID; import static org.folio.search.utils.TestConstants.RESOURCE_NAME; -import static org.folio.search.utils.TestConstants.TENANT_ID; import static org.folio.search.utils.TestUtils.filterField; import static org.folio.search.utils.TestUtils.keywordField; import static org.folio.search.utils.TestUtils.multilangField; @@ -14,6 +12,7 @@ import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.when; import static org.opensearch.index.query.MultiMatchQueryBuilder.Type.CROSS_FIELDS; @@ -21,6 +20,7 @@ import static org.opensearch.index.query.Operator.AND; import static org.opensearch.index.query.Operator.OR; import static org.opensearch.index.query.QueryBuilders.boolQuery; +import static org.opensearch.index.query.QueryBuilders.disMaxQuery; import static org.opensearch.index.query.QueryBuilders.existsQuery; import static org.opensearch.index.query.QueryBuilders.matchAllQuery; import static org.opensearch.index.query.QueryBuilders.matchQuery; @@ -37,6 +37,7 @@ import org.folio.search.exception.RequestValidationException; import org.folio.search.exception.SearchServiceException; import org.folio.search.model.metadata.PlainFieldDescription; +import org.folio.search.service.consortium.ConsortiumSearchHelper; import org.folio.search.service.consortium.ConsortiumTenantService; import org.folio.search.service.metadata.LocalSearchFieldProvider; import org.folio.spring.FolioExecutionContext; @@ -65,7 +66,6 @@ class CqlSearchQueryConverterTest { private static final String[] TITLE_FIELDS = new String[] {"title.*", "source.*", "source"}; private static final String TITLE_SEARCH_TYPE = "title"; private static final String FIELD = "field"; - private static final String SHARED_FIELD = "shared"; @Autowired private CqlSearchQueryConverter cqlSearchQueryConverter; @@ -77,10 +77,14 @@ class CqlSearchQueryConverterTest { private FolioExecutionContext folioExecutionContext; @MockBean private ConsortiumTenantService consortiumTenantService; + @MockBean + private ConsortiumSearchHelper consortiumSearchHelper; @BeforeEach void setUp() { when(searchFieldProvider.getModifiedField(any(), any())).thenAnswer(f -> f.getArguments()[0]); + doAnswer(invocation -> invocation.getArgument(0)) + .when(consortiumSearchHelper).filterQueryForActiveAffiliation(any()); } @MethodSource("convertCqlQueryDataProvider") @@ -357,60 +361,12 @@ void convertForConsortia_positive_whenConsortiaDisabled() { @Test void convertForConsortia_positive() { - when(folioExecutionContext.getTenantId()).thenReturn(TENANT_ID); - when(consortiumTenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CONSORTIUM_TENANT_ID)); + var consortiumQueryMock = disMaxQuery(); + when(consortiumSearchHelper.filterQueryForActiveAffiliation(any())).thenReturn(consortiumQueryMock); doReturn(Optional.of(filterField())).when(searchFieldProvider).getPlainFieldByPath(RESOURCE_NAME, "f1"); var cqlQuery = "f1==value"; var actual = cqlSearchQueryConverter.convertForConsortia(cqlQuery, RESOURCE_NAME); - assertThat(actual).isEqualTo(searchSource().query(boolQuery() - .filter(termQuery("f1", "value")) - .should(termQuery("tenantId", TENANT_ID)) - .should(termQuery(SHARED_FIELD, true)) - .minimumShouldMatch(1))); - } - - @Test - void convertForConsortia_positive_whenCentralTenant() { - when(folioExecutionContext.getTenantId()).thenReturn(CONSORTIUM_TENANT_ID); - when(consortiumTenantService.getCentralTenant(CONSORTIUM_TENANT_ID)).thenReturn(Optional.of(CONSORTIUM_TENANT_ID)); - doReturn(Optional.of(filterField())).when(searchFieldProvider).getPlainFieldByPath(RESOURCE_NAME, "f1"); - var cqlQuery = "f1==value"; - var actual = cqlSearchQueryConverter.convertForConsortia(cqlQuery, RESOURCE_NAME); - assertThat(actual).isEqualTo(searchSource().query( - boolQuery() - .filter(termQuery("f1", "value")) - .should(termQuery(SHARED_FIELD, true)) - .minimumShouldMatch(1))); - } - - @Test - void convertForConsortia_positive_whenOriginalQueryMatchAll() { - when(folioExecutionContext.getTenantId()).thenReturn(TENANT_ID); - when(consortiumTenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CONSORTIUM_TENANT_ID)); - when(searchFieldProvider.getPlainFieldByPath(eq(RESOURCE_NAME), any())).thenReturn(Optional.of(keywordField())); - var actual = cqlSearchQueryConverter.convertForConsortia("cql.allRecords = 1", RESOURCE_NAME); - assertThat(actual).isEqualTo(searchSource().query( - boolQuery() - .should(termQuery("tenantId", TENANT_ID)) - .should(termQuery(SHARED_FIELD, true)) - .minimumShouldMatch(1))); - } - - @Test - void convertForConsortia_positive_whenOriginalBooleanWithShould() { - when(folioExecutionContext.getTenantId()).thenReturn(TENANT_ID); - when(consortiumTenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CONSORTIUM_TENANT_ID)); - when(searchFieldProvider.getPlainFieldByPath(eq(RESOURCE_NAME), any())).thenReturn(Optional.of(keywordField())); - var actual = cqlSearchQueryConverter.convertForConsortia("f1==v1 or f2==v2", RESOURCE_NAME); - assertThat(actual).isEqualTo(searchSource().query( - boolQuery() - .should(termQuery("f1", "v1")) - .should(termQuery("f2", "v2")) - .must(boolQuery() - .should(termQuery("tenantId", TENANT_ID)) - .should(termQuery(SHARED_FIELD, true))) - .minimumShouldMatch(1) - )); + assertThat(actual).isEqualTo(searchSource().query(consortiumQueryMock)); } private static Stream convertCqlQueryDataProvider() { diff --git a/src/test/java/org/folio/search/service/browse/AuthorityBrowseServiceTest.java b/src/test/java/org/folio/search/service/browse/AuthorityBrowseServiceTest.java index 4aa453aad..f9c1b3241 100644 --- a/src/test/java/org/folio/search/service/browse/AuthorityBrowseServiceTest.java +++ b/src/test/java/org/folio/search/service/browse/AuthorityBrowseServiceTest.java @@ -6,9 +6,12 @@ import static org.folio.search.utils.TestConstants.TENANT_ID; import static org.folio.search.utils.TestUtils.authorityBrowseItem; import static org.folio.search.utils.TestUtils.searchResult; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.opensearch.index.query.QueryBuilders.boolQuery; +import static org.opensearch.index.query.QueryBuilders.disMaxQuery; import static org.opensearch.index.query.QueryBuilders.rangeQuery; import static org.opensearch.index.query.QueryBuilders.termQuery; import static org.opensearch.index.query.QueryBuilders.termsQuery; @@ -28,6 +31,7 @@ import org.folio.search.model.service.BrowseContext; import org.folio.search.model.service.BrowseRequest; import org.folio.search.repository.SearchRepository; +import org.folio.search.service.consortium.ConsortiumSearchHelper; import org.folio.search.service.converter.ElasticsearchDocumentConverter; import org.folio.search.service.metadata.SearchFieldProvider; import org.folio.search.service.setter.SearchResponsePostProcessor; @@ -63,12 +67,16 @@ class AuthorityBrowseServiceTest { @Mock private SearchFieldProvider searchFieldProvider; @Mock + private ConsortiumSearchHelper consortiumSearchHelper; + @Mock private SearchResponse searchResponse; @Mock private Map, SearchResponsePostProcessor> searchResponsePostProcessors = Collections.emptyMap(); @BeforeEach void setUp() { + doAnswer(invocation -> invocation.getArgument(0)) + .when(consortiumSearchHelper).filterQueryForActiveAffiliation(any()); authorityBrowseService.setDocumentConverter(documentConverter); authorityBrowseService.setSearchRepository(searchRepository); authorityBrowseService.setBrowseContextProvider(browseContextProvider); @@ -313,6 +321,28 @@ void browse_positive_aroundIncludingMissingAnchorWithoutHighlighting() { browseItem("r1"), browseItem("r2"), browseItem("s1"), browseItem("s2"), browseItem("s3")))); } + @Test + void getSearchQuery_positive_consortium() { + var query = disMaxQuery(); + when(consortiumSearchHelper.filterQueryForActiveAffiliation(any())).thenReturn(query); + + var actual = authorityBrowseService.getSearchQuery( + BrowseRequest.builder().targetField("test").build(), + BrowseContext.builder().anchor("test").succeedingLimit(1).build(), true); + assertThat(actual.query()).isEqualTo(query); + } + + @Test + void getAnchorSearchQuery_positive_consortium() { + var query = disMaxQuery(); + when(consortiumSearchHelper.filterQueryForActiveAffiliation(any())).thenReturn(query); + + var actual = authorityBrowseService.getSearchQuery( + BrowseRequest.builder().targetField("test").build(), + BrowseContext.builder().anchor("test").succeedingLimit(1).build(), true); + assertThat(actual.query()).isEqualTo(query); + } + private static SearchSourceBuilder searchSource(String heading, int size, SortOrder sortOrder) { return SearchSourceBuilder.searchSource() .query(boolQuery().filter(termsQuery("authRefType", "Authorized", "Reference"))) diff --git a/src/test/java/org/folio/search/service/browse/CallNumberBrowseQueryProviderTest.java b/src/test/java/org/folio/search/service/browse/CallNumberBrowseQueryProviderTest.java index f71db5899..5000947d3 100644 --- a/src/test/java/org/folio/search/service/browse/CallNumberBrowseQueryProviderTest.java +++ b/src/test/java/org/folio/search/service/browse/CallNumberBrowseQueryProviderTest.java @@ -5,10 +5,13 @@ import static org.folio.search.model.types.ResponseGroupType.CN_BROWSE; import static org.folio.search.utils.TestConstants.RESOURCE_NAME; import static org.folio.search.utils.TestConstants.TENANT_ID; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.opensearch.index.query.QueryBuilders.boolQuery; +import static org.opensearch.index.query.QueryBuilders.disMaxQuery; import static org.opensearch.index.query.QueryBuilders.rangeQuery; import static org.opensearch.index.query.QueryBuilders.termQuery; import static org.opensearch.script.Script.DEFAULT_SCRIPT_LANG; @@ -25,9 +28,11 @@ import org.folio.search.configuration.properties.SearchQueryConfigurationProperties; import org.folio.search.model.service.BrowseContext; import org.folio.search.model.service.BrowseRequest; +import org.folio.search.service.consortium.ConsortiumSearchHelper; import org.folio.search.service.metadata.SearchFieldProvider; import org.folio.search.utils.CallNumberUtils; import org.folio.spring.test.type.UnitTest; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -53,9 +58,17 @@ class CallNumberBrowseQueryProviderTest { private SearchFieldProvider searchFieldProvider; @Mock private CallNumberBrowseRangeService browseRangeService; + @Mock + private ConsortiumSearchHelper consortiumSearchHelper; @InjectMocks private CallNumberBrowseQueryProvider queryProvider; + @BeforeEach + public void setUpMocks() { + lenient().doAnswer(invocation -> invocation.getArgument(0)) + .when(consortiumSearchHelper).filterQueryForActiveAffiliation(any()); + } + @Test void get_positive_forward() { when(searchFieldProvider.getSourceFields(RESOURCE_NAME, CN_BROWSE)).thenReturn(new String[] {"id", "title"}); @@ -106,6 +119,19 @@ void get_positive_forwardWithEnabledOptimization() { verify(queryConfiguration).getRangeQueryLimitMultiplier(); } + @Test + void get_positive_forwardConsortium() { + var query = disMaxQuery(); + when(consortiumSearchHelper.filterQueryForActiveAffiliation(any())).thenReturn(query); + when(searchFieldProvider.getSourceFields(RESOURCE_NAME, CN_BROWSE)).thenReturn(new String[] {"id", "title"}); + var context = BrowseContext.builder().anchor(ANCHOR).succeedingLimit(5).build(); + + var actual = mockCallNumberConversion(() -> queryProvider.get(request(false), context, true)); + assertThat(actual) + .isEqualTo(expectedSucceedingQuery(25).query(query).fetchSource(new String[] {"id", "title"}, null)); + verify(queryConfiguration).getRangeQueryLimitMultiplier(); + } + @Test void get_positive_backward() { when(searchFieldProvider.getSourceFields(RESOURCE_NAME, CN_BROWSE)).thenReturn(new String[] {"id", "title"}); diff --git a/src/test/java/org/folio/search/service/browse/ContributorBrowseServiceTest.java b/src/test/java/org/folio/search/service/browse/ContributorBrowseServiceTest.java index e2843493e..4bbbfb911 100644 --- a/src/test/java/org/folio/search/service/browse/ContributorBrowseServiceTest.java +++ b/src/test/java/org/folio/search/service/browse/ContributorBrowseServiceTest.java @@ -2,6 +2,11 @@ import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.opensearch.index.query.QueryBuilders.disMaxQuery; +import static org.opensearch.index.query.QueryBuilders.rangeQuery; import static org.opensearch.index.query.QueryBuilders.termQuery; import java.util.List; @@ -11,19 +16,29 @@ import org.folio.search.model.index.ContributorResource; import org.folio.search.model.index.InstanceSubResource; import org.folio.search.model.service.BrowseContext; +import org.folio.search.model.service.BrowseRequest; +import org.folio.search.service.consortium.ConsortiumSearchHelper; import org.folio.spring.test.type.UnitTest; 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.ValueSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @UnitTest @ExtendWith(MockitoExtension.class) class ContributorBrowseServiceTest { - private final ContributorBrowseService service = new ContributorBrowseService(); + @Mock + private ConsortiumSearchHelper consortiumSearchHelper; + + @InjectMocks + private ContributorBrowseService service; @Test - void mapToBrowseResult_positive_withConsortiumFilter() { + void mapToBrowseResult_positive() { var contributors = contributors(); var searchResult = new SearchResult(); searchResult.setTotalRecords(1); @@ -31,6 +46,14 @@ void mapToBrowseResult_positive_withConsortiumFilter() { var browseContext = BrowseContext.builder() .filters(singletonList(termQuery("instances.shared", false))) .build(); + var instanceId1 = "ins1"; + var instanceId2 = "ins2"; + var contributorsSubResourcesMock = Set.of( + contributorSubResource(instanceId1, "type1"), + contributorSubResource(instanceId1, "type2"), + contributorSubResource(instanceId2, "type1"), + contributorSubResource(instanceId2, null), + contributorSubResource(instanceId1, "null")); var expected = new InstanceContributorBrowseItem() .isAnchor(false) @@ -38,7 +61,10 @@ void mapToBrowseResult_positive_withConsortiumFilter() { .name("name") .contributorNameTypeId("nameType") .authorityId("auth") - .totalRecords(1); + .totalRecords(2); + + when(consortiumSearchHelper.filterSubResourcesForConsortium(any(), any(), any())) + .thenReturn(contributorsSubResourcesMock); var browseResult = service.mapToBrowseResult(browseContext, searchResult, false); @@ -46,26 +72,34 @@ void mapToBrowseResult_positive_withConsortiumFilter() { .isEqualTo(expected); } + @ValueSource(booleans = {true, false}) + @ParameterizedTest + void getSearchQuery_positive(Boolean isBrowsingForward) { + var browseRequest = BrowseRequest.builder().targetField("test").build(); + var browseContext = BrowseContext.builder().anchor("test").succeedingLimit(1).precedingLimit(1).build(); + var queryMock = disMaxQuery(); + + when(consortiumSearchHelper.filterBrowseQueryForActiveAffiliation(eq(browseContext), any())) + .thenReturn(queryMock); + + var result = service.getSearchQuery(browseRequest, browseContext, isBrowsingForward); + + assertThat(result.query()).isEqualTo(queryMock); + } + @Test - void mapToBrowseResult_positive() { - var contributors = contributors(); - var searchResult = new SearchResult(); - searchResult.setTotalRecords(1); - searchResult.setRecords(contributors); - var browseContext = BrowseContext.builder().build(); + void getAnchorSearchQuery_positive() { + var browseRequest = BrowseRequest.builder().targetField("test").build(); + var browseContext = BrowseContext.builder() + .anchor("test").succeedingQuery(rangeQuery("test")).succeedingLimit(1).build(); + var queryMock = disMaxQuery(); - var expected = new InstanceContributorBrowseItem() - .isAnchor(false) - .contributorTypeId(List.of("type1", "type2")) - .name("name") - .contributorNameTypeId("nameType") - .authorityId("auth") - .totalRecords(2); + when(consortiumSearchHelper.filterBrowseQueryForActiveAffiliation(eq(browseContext), any())) + .thenReturn(queryMock); - var browseResult = service.mapToBrowseResult(browseContext, searchResult, false); + var result = service.getAnchorSearchQuery(browseRequest, browseContext); - assertThat(browseResult.getRecords().get(0)) - .isEqualTo(expected); + assertThat(result.query()).isEqualTo(queryMock); } private List contributors() { @@ -88,4 +122,11 @@ private InstanceSubResource contributorInstance(String instanceId, String typeId .tenantId(tenantId) .build(); } + + private InstanceSubResource contributorSubResource(String instanceId, String typeId) { + return InstanceSubResource.builder() + .instanceId(instanceId) + .typeId(typeId) + .build(); + } } diff --git a/src/test/java/org/folio/search/service/browse/SubjectBrowseServiceTest.java b/src/test/java/org/folio/search/service/browse/SubjectBrowseServiceTest.java index 48b5bb9ed..9f2d471b6 100644 --- a/src/test/java/org/folio/search/service/browse/SubjectBrowseServiceTest.java +++ b/src/test/java/org/folio/search/service/browse/SubjectBrowseServiceTest.java @@ -4,6 +4,9 @@ import static org.folio.search.utils.TestConstants.TENANT_ID; import static org.folio.search.utils.TestUtils.searchResult; import static org.folio.search.utils.TestUtils.subjectBrowseItem; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.opensearch.index.query.QueryBuilders.matchAllQuery; @@ -26,9 +29,11 @@ import org.folio.search.model.service.BrowseContext; import org.folio.search.model.service.BrowseRequest; import org.folio.search.repository.SearchRepository; +import org.folio.search.service.consortium.ConsortiumSearchHelper; import org.folio.search.service.converter.ElasticsearchDocumentConverter; import org.folio.search.service.setter.SearchResponsePostProcessor; import org.folio.spring.test.type.UnitTest; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; @@ -58,10 +63,20 @@ class SubjectBrowseServiceTest { @Mock private ElasticsearchDocumentConverter documentConverter; @Mock + private ConsortiumSearchHelper consortiumSearchHelper; + @Mock private SearchResponse searchResponse; @Mock private Map, SearchResponsePostProcessor> searchResponsePostProcessors = Collections.emptyMap(); + @BeforeEach + public void setUpMocks() { + doAnswer(invocation -> invocation.getArgument(1)) + .when(consortiumSearchHelper).filterBrowseQueryForActiveAffiliation(any(), any()); + lenient().doAnswer(invocation -> ((SubjectResource) invocation.getArgument(1)).getInstances()) + .when(consortiumSearchHelper).filterSubResourcesForConsortium(any(), any(), any()); + } + @Test void browse_positive_forward() { var query = "value > s0"; diff --git a/src/test/java/org/folio/search/service/consortium/ConsortiumSearchHelperTest.java b/src/test/java/org/folio/search/service/consortium/ConsortiumSearchHelperTest.java new file mode 100644 index 000000000..5da1ecf2e --- /dev/null +++ b/src/test/java/org/folio/search/service/consortium/ConsortiumSearchHelperTest.java @@ -0,0 +1,314 @@ +package org.folio.search.service.consortium; + +import static com.google.common.collect.Lists.newArrayList; +import static com.google.common.collect.Sets.newHashSet; +import static org.apache.commons.lang3.StringUtils.EMPTY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.folio.search.utils.TestConstants.CONSORTIUM_TENANT_ID; +import static org.folio.search.utils.TestConstants.TENANT_ID; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +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 java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import org.folio.search.model.index.InstanceSubResource; +import org.folio.search.model.index.SubjectResource; +import org.folio.search.model.service.BrowseContext; +import org.folio.spring.FolioExecutionContext; +import org.folio.spring.test.type.UnitTest; +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.NullSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +@UnitTest +@ExtendWith(MockitoExtension.class) +class ConsortiumSearchHelperTest { + + private static final String SUBRESOURCE_PREFIX = "instances."; + private static final String TENANT_ID_FIELD = SUBRESOURCE_PREFIX + "tenantId"; + private static final String SHARED_FIELD = SUBRESOURCE_PREFIX + "shared"; + + @Mock + private FolioExecutionContext context; + @Mock + private ConsortiumTenantService tenantService; + + @Spy + @InjectMocks + private ConsortiumSearchHelper consortiumSearchHelper; + + @Test + void filterQueryForActiveAffiliation_positive_basic() { + var query = matchAllQuery(); + when(context.getTenantId()).thenReturn(TENANT_ID); + when(tenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CONSORTIUM_TENANT_ID)); + + consortiumSearchHelper.filterQueryForActiveAffiliation(query); + + verify(consortiumSearchHelper).filterQueryForActiveAffiliation(query, TENANT_ID, CONSORTIUM_TENANT_ID); + } + + @Test + void filterQueryForActiveAffiliation_positive_basicNotConsortiumTenant() { + var query = matchAllQuery(); + when(context.getTenantId()).thenReturn(TENANT_ID); + when(tenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.empty()); + + var actual = consortiumSearchHelper.filterQueryForActiveAffiliation(query); + + assertThat(actual).isEqualTo(query); + verify(consortiumSearchHelper, times(0)).filterQueryForActiveAffiliation(any(), any(), any()); + } + + @Test + void filterQueryForActiveAffiliation_positive_basicConsortiumCentralTenant() { + var query = matchAllQuery(); + var expected = boolQuery() + .minimumShouldMatch(1) + .should(termQuery("shared", true)); + + when(context.getTenantId()).thenReturn(TENANT_ID); + when(tenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(TENANT_ID)); + + var actual = consortiumSearchHelper.filterQueryForActiveAffiliation(query); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void filterQueryForActiveAffiliation_positive_noPrefix() { + var query = matchAllQuery(); + var expected = boolQuery() + .minimumShouldMatch(1) + .should(termQuery("tenantId", TENANT_ID)) + .should(termQuery("shared", true)); + + var actual = consortiumSearchHelper.filterQueryForActiveAffiliation(query, TENANT_ID, CONSORTIUM_TENANT_ID); + + assertThat(actual).isEqualTo(expected); + verify(consortiumSearchHelper).filterQueryForActiveAffiliation(EMPTY, query, TENANT_ID, CONSORTIUM_TENANT_ID); + } + + @Test + void filterQueryForActiveAffiliation_positive() { + var query = matchAllQuery(); + var expected = boolQuery() + .minimumShouldMatch(1) + .should(termQuery(TENANT_ID_FIELD, TENANT_ID)) + .should(termQuery(SHARED_FIELD, true)); + + var actual = consortiumSearchHelper.filterQueryForActiveAffiliation(SUBRESOURCE_PREFIX, query, TENANT_ID, + CONSORTIUM_TENANT_ID); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void filterQueryForActiveAffiliation_positive_boolQuery() { + var query = boolQuery(); + var expected = boolQuery() + .minimumShouldMatch(1) + .should(termQuery(TENANT_ID_FIELD, TENANT_ID)) + .should(termQuery(SHARED_FIELD, true)); + + var actual = consortiumSearchHelper.filterQueryForActiveAffiliation(SUBRESOURCE_PREFIX, query, TENANT_ID, + CONSORTIUM_TENANT_ID); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void filterQueryForActiveAffiliation_positive_boolQueryWithShould() { + var query = boolQuery().should(termQuery("test", "test")); + var expected = boolQuery() + .minimumShouldMatch(1) + .should(termQuery("test", "test")) + .must(boolQuery() + .should(termQuery(TENANT_ID_FIELD, TENANT_ID)) + .should(termQuery(SHARED_FIELD, true))); + + var actual = consortiumSearchHelper.filterQueryForActiveAffiliation(SUBRESOURCE_PREFIX, query, TENANT_ID, + CONSORTIUM_TENANT_ID); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void filterQueryForActiveAffiliation_positive_otherQuery() { + var query = termQuery("test", "test"); + var expected = boolQuery() + .minimumShouldMatch(1) + .must(termQuery("test", "test")) + .should(termQuery(TENANT_ID_FIELD, TENANT_ID)) + .should(termQuery(SHARED_FIELD, true)); + + var actual = consortiumSearchHelper.filterQueryForActiveAffiliation(SUBRESOURCE_PREFIX, query, TENANT_ID, + CONSORTIUM_TENANT_ID); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void filterBrowseQueryForActiveAffiliation_positive_notConsortiumTenant() { + var browseContext = browseContext(false); + var query = matchAllQuery(); + + when(context.getTenantId()).thenReturn(TENANT_ID); + when(tenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.empty()); + + var actual = consortiumSearchHelper.filterBrowseQueryForActiveAffiliation(browseContext, query); + + assertThat(actual).isEqualTo(query); + assertThat(browseContext.getFilters()).isEmpty(); + } + + @Test + void filterBrowseQueryForActiveAffiliation_positive_consortiumCentralTenant() { + var browseContext = browseContext(false); + var query = matchAllQuery(); + var expected = boolQuery() + .must(termQuery(TENANT_ID_FIELD, TENANT_ID)); + + when(context.getTenantId()).thenReturn(TENANT_ID); + when(tenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.ofNullable(TENANT_ID)); + + var actual = consortiumSearchHelper.filterBrowseQueryForActiveAffiliation(browseContext, query); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void filterBrowseQueryForActiveAffiliation_positive_shared() { + var browseContext = browseContext(true); + var query = matchAllQuery(); + + when(context.getTenantId()).thenReturn(TENANT_ID); + when(tenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CONSORTIUM_TENANT_ID)); + + consortiumSearchHelper.filterBrowseQueryForActiveAffiliation(browseContext, query); + + verify(consortiumSearchHelper).filterQueryForActiveAffiliation("instances.", query, TENANT_ID, + CONSORTIUM_TENANT_ID); + } + + @Test + void filterBrowseQueryForActiveAffiliation_positive_local() { + var browseContext = browseContext(false); + var query = matchAllQuery(); + var expected = boolQuery() + .must(termQuery(TENANT_ID_FIELD, TENANT_ID)); + + when(context.getTenantId()).thenReturn(TENANT_ID); + when(tenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CONSORTIUM_TENANT_ID)); + + var actual = consortiumSearchHelper.filterBrowseQueryForActiveAffiliation(browseContext, query); + + assertThat(actual).isEqualTo(expected); + assertThat(browseContext.getFilters()).isNotEmpty(); + } + + @Test + void filterBrowseQueryForActiveAffiliation_positive_localWithShould() { + var browseContext = browseContext(false); + var query = boolQuery() + .should(termQuery("test", "test")); + var expected = boolQuery() + .should(termQuery("test", "test")) + .minimumShouldMatch(1) + .must(termQuery(TENANT_ID_FIELD, TENANT_ID)); + + when(context.getTenantId()).thenReturn(TENANT_ID); + when(tenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CONSORTIUM_TENANT_ID)); + + var actual = consortiumSearchHelper.filterBrowseQueryForActiveAffiliation(browseContext, query); + + assertThat(actual).isEqualTo(expected); + assertThat(browseContext.getFilters()).isNotEmpty(); + } + + @ParameterizedTest + @ValueSource(strings = TENANT_ID) + @NullSource + void filterSubResourcesForConsortium_positive_notConsortiumMemberTenant(String centralTenantId) { + when(context.getTenantId()).thenReturn(TENANT_ID); + when(tenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.ofNullable(centralTenantId)); + + var browseContext = browseContext(false); + var resource = new SubjectResource(); + resource.setInstances(subResources()); + + var actual = consortiumSearchHelper.filterSubResourcesForConsortium(browseContext, resource, + SubjectResource::getInstances); + + assertThat(actual).isEqualTo(resource.getInstances()); + } + + @Test + void filterSubResourcesForConsortium_positive_local() { + when(context.getTenantId()).thenReturn(TENANT_ID); + when(tenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CONSORTIUM_TENANT_ID)); + + var browseContext = browseContext(false); + var subResources = subResources(); + var resource = new SubjectResource(); + resource.setInstances(subResources); + var expected = subResources.stream().filter(s -> s.getTenantId().equals(TENANT_ID)).collect(Collectors.toSet()); + + var actual = consortiumSearchHelper.filterSubResourcesForConsortium(browseContext, resource, + SubjectResource::getInstances); + + assertThat(actual).isEqualTo(expected); + } + + @ParameterizedTest + @ValueSource(booleans = true) + @NullSource + void filterSubResourcesForConsortium_positive_shared(Boolean shared) { + when(context.getTenantId()).thenReturn(TENANT_ID); + when(tenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CONSORTIUM_TENANT_ID)); + + var browseContext = browseContext(shared); + var subResources = subResources(); + var resource = SubjectResource.builder().instances(subResources).build(); + var expected = newHashSet(subResources); + expected.removeIf(s -> s.getTenantId().equals(CONSORTIUM_TENANT_ID) && !s.getShared()); + + var actual = consortiumSearchHelper.filterSubResourcesForConsortium(browseContext, resource, + SubjectResource::getInstances); + + assertThat(actual).isEqualTo(expected); + } + + private BrowseContext browseContext(Boolean sharedFilter) { + var browseContext = BrowseContext.builder(); + + if (sharedFilter != null) { + browseContext.filters(newArrayList(termQuery(SHARED_FIELD, sharedFilter))); + } + + return browseContext.build(); + } + + private Set subResources() { + return newHashSet(subResource(TENANT_ID, false), + subResource(TENANT_ID, true), + subResource(CONSORTIUM_TENANT_ID, false), + subResource(CONSORTIUM_TENANT_ID, true)); + } + + private InstanceSubResource subResource(String tenantId, boolean shared) { + return InstanceSubResource.builder().tenantId(tenantId).shared(shared).build(); + } +}