From 705e6e8cc8604b38beacee4590f7b2235b7c9f14 Mon Sep 17 00:00:00 2001 From: Viacheslav Poliakov <159778173+viacheslavpoliakov@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:31:32 +0300 Subject: [PATCH] MSEARCH-762 Local Instances are not exported with the cql file in Member tenant (#600) * fix(mod-search): fix export of cql files - Add query filter for active affiliation - Add converter to consortia for member tenant Closes: MSEARCH-762 --- NEWS.md | 1 + .../search/cql/CqlSearchQueryConverter.java | 16 +++++++ .../search/service/ResourceIdService.java | 3 +- .../consortium/ConsortiumSearchHelper.java | 9 ++++ .../search/service/ResourceIdServiceTest.java | 12 ++--- .../ConsortiumSearchHelperTest.java | 44 ++++++++++++++++++- 6 files changed, 76 insertions(+), 9 deletions(-) diff --git a/NEWS.md b/NEWS.md index 73636b481..b167fe04a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -32,6 +32,7 @@ * Browse: Duplicate results in exact match with diacritics ([MSEARCH-751](https://folio-org.atlassian.net/browse/MSEARCH-751)) * Classification browse: Fix instances count for Shared Facet ([MSEARCH-761](https://folio-org.atlassian.net/browse/MSEARCH-761)) * Subjects/Contributors browse: Fix instances count for Shared Facet ([MSEARCH-782](https://folio-org.atlassian.net/browse/MSEARCH-782)) +* Search Resources IDs: Local instances are not searchable with requests from member tenants ([MSEARCH-762](https://folio-org.atlassian.net/browse/MSEARCH-762)) ### Tech Dept * Re-Index: delete all records from consortium_instance on full re-index ([MSEARCH-744](https://folio-org.atlassian.net/browse/MSEARCH-744)) diff --git a/src/main/java/org/folio/search/cql/CqlSearchQueryConverter.java b/src/main/java/org/folio/search/cql/CqlSearchQueryConverter.java index 13f90c9e6..507ffd7ba 100644 --- a/src/main/java/org/folio/search/cql/CqlSearchQueryConverter.java +++ b/src/main/java/org/folio/search/cql/CqlSearchQueryConverter.java @@ -71,6 +71,22 @@ public SearchSourceBuilder convertForConsortia(String query, String resource) { return convertForConsortia(query, resource, false); } + /** + * Converts given CQL search query value to the elasticsearch {@link SearchSourceBuilder} object. + * Wraps base 'convert' and adds active affiliation tenantId filter in case of consortia mode + * + * @param query cql query to parse + * @param resource resource name + * @param tenantId active affiliation member tenant name + * @return search source as {@link SearchSourceBuilder} object with query and sorting conditions + */ + public SearchSourceBuilder convertForConsortia(String query, String resource, String tenantId) { + var sourceBuilder = convert(query, resource); + var queryBuilder = consortiumSearchHelper + .filterQueryForActiveAffiliation(sourceBuilder.query(), resource, tenantId); + return sourceBuilder.query(queryBuilder); + } + public SearchSourceBuilder convertForConsortia(String query, String resource, boolean consortiumConsolidated) { var sourceBuilder = convert(query, resource); if (consortiumConsolidated) { diff --git a/src/main/java/org/folio/search/service/ResourceIdService.java b/src/main/java/org/folio/search/service/ResourceIdService.java index 426197cc0..6b7f12700 100644 --- a/src/main/java/org/folio/search/service/ResourceIdService.java +++ b/src/main/java/org/folio/search/service/ResourceIdService.java @@ -140,7 +140,8 @@ protected OutputStreamWriter createOutputStreamWriter(OutputStream outputStream) private void streamResourceIds(CqlResourceIdsRequest request, Consumer> idsConsumer) { log.info("streamResourceIds:: by [query: {}, resource: {}]", request.getQuery(), request.getResource()); - var searchSource = queryConverter.convertForConsortia(request.getQuery(), request.getResource()) + var searchSource = queryConverter + .convertForConsortia(request.getQuery(), request.getResource(), request.getTenantId()) .size(streamIdsProperties.getScrollQuerySize()) .fetchSource(new String[] {request.getSourceFieldPath()}, null) .sort(fieldSort("_doc")); diff --git a/src/main/java/org/folio/search/service/consortium/ConsortiumSearchHelper.java b/src/main/java/org/folio/search/service/consortium/ConsortiumSearchHelper.java index 843ef1bf0..375460053 100644 --- a/src/main/java/org/folio/search/service/consortium/ConsortiumSearchHelper.java +++ b/src/main/java/org/folio/search/service/consortium/ConsortiumSearchHelper.java @@ -42,6 +42,15 @@ public class ConsortiumSearchHelper { public QueryBuilder filterQueryForActiveAffiliation(QueryBuilder query, String resource) { var contextTenantId = folioExecutionContext.getTenantId(); + return filterQueryForActiveAffiliation(query, resource, contextTenantId); + } + + /** + * Modifies query to support both 'shared' filter and Active Affiliation. + * Active Affiliation have precedence over 'shared' filter so in case of member tenant + * modified query will have member 'tenantId' filter with shared=true). + */ + public QueryBuilder filterQueryForActiveAffiliation(QueryBuilder query, String resource, String contextTenantId) { var centralTenantId = consortiumTenantService.getCentralTenant(contextTenantId); if (centralTenantId.isEmpty()) { return query; diff --git a/src/test/java/org/folio/search/service/ResourceIdServiceTest.java b/src/test/java/org/folio/search/service/ResourceIdServiceTest.java index bbd63a77a..0518c7062 100644 --- a/src/test/java/org/folio/search/service/ResourceIdServiceTest.java +++ b/src/test/java/org/folio/search/service/ResourceIdServiceTest.java @@ -66,7 +66,7 @@ class ResourceIdServiceTest { @Test void streamResourceIds() throws IOException { - when(queryConverter.convertForConsortia(TEST_QUERY, RESOURCE_NAME)).thenReturn(searchSource()); + when(queryConverter.convertForConsortia(TEST_QUERY, RESOURCE_NAME, TENANT_ID)).thenReturn(searchSource()); when(properties.getScrollQuerySize()).thenReturn(QUERY_SIZE); mockSearchRepositoryCall(List.of(RANDOM_ID)); @@ -80,7 +80,7 @@ void streamResourceIds() throws IOException { @Test void streamResourceIdsAsText() { - when(queryConverter.convertForConsortia(TEST_QUERY, RESOURCE_NAME)).thenReturn(searchSource()); + when(queryConverter.convertForConsortia(TEST_QUERY, RESOURCE_NAME, TENANT_ID)).thenReturn(searchSource()); when(properties.getScrollQuerySize()).thenReturn(QUERY_SIZE); mockSearchRepositoryCall(List.of(RANDOM_ID)); @@ -125,7 +125,7 @@ void streamResourceIds_negative_throwExceptionOnWritingIdField() throws IOExcept mockSearchRepositoryCall(List.of(RANDOM_ID)); when(properties.getScrollQuerySize()).thenReturn(QUERY_SIZE); when(objectMapper.createGenerator(outputStream)).thenReturn(generator); - when(queryConverter.convertForConsortia(TEST_QUERY, RESOURCE_NAME)).thenReturn(searchSource()); + when(queryConverter.convertForConsortia(TEST_QUERY, RESOURCE_NAME, TENANT_ID)).thenReturn(searchSource()); doThrow(new IOException("Failed to write string field")).when(generator).writeStringField("id", RANDOM_ID); var request = request(); @@ -142,7 +142,7 @@ void streamResourceIdsAsText_negative_throwExceptionOnWritingIdField() throws IO mockSearchRepositoryCall(List.of(RANDOM_ID)); when(properties.getScrollQuerySize()).thenReturn(QUERY_SIZE); when(resourceIdService.createOutputStreamWriter(outputStream)).thenReturn(writer); - when(queryConverter.convertForConsortia(TEST_QUERY, RESOURCE_NAME)).thenReturn(searchSource()); + when(queryConverter.convertForConsortia(TEST_QUERY, RESOURCE_NAME, TENANT_ID)).thenReturn(searchSource()); doThrow(new IOException("Failed to write string field")).when(writer).write(RANDOM_ID + '\n'); var request = request(); @@ -154,7 +154,7 @@ void streamResourceIdsAsText_negative_throwExceptionOnWritingIdField() throws IO @Test void streamResourceIds_positive_emptyCollectionProvided() throws IOException { mockSearchRepositoryCall(emptyList()); - when(queryConverter.convertForConsortia(TEST_QUERY, RESOURCE_NAME)).thenReturn(searchSource()); + when(queryConverter.convertForConsortia(TEST_QUERY, RESOURCE_NAME, TENANT_ID)).thenReturn(searchSource()); when(properties.getScrollQuerySize()).thenReturn(QUERY_SIZE); var outputStream = new ByteArrayOutputStream(); @@ -167,7 +167,7 @@ void streamResourceIds_positive_emptyCollectionProvided() throws IOException { @Test void streamResourceIdsInTextTextType_positive_emptyCollectionProvided() { mockSearchRepositoryCall(emptyList()); - when(queryConverter.convertForConsortia(TEST_QUERY, RESOURCE_NAME)).thenReturn(searchSource()); + when(queryConverter.convertForConsortia(TEST_QUERY, RESOURCE_NAME, TENANT_ID)).thenReturn(searchSource()); when(properties.getScrollQuerySize()).thenReturn(QUERY_SIZE); var outputStream = new ByteArrayOutputStream(); diff --git a/src/test/java/org/folio/search/service/consortium/ConsortiumSearchHelperTest.java b/src/test/java/org/folio/search/service/consortium/ConsortiumSearchHelperTest.java index ad7cc33e6..7ca65bc3c 100644 --- a/src/test/java/org/folio/search/service/consortium/ConsortiumSearchHelperTest.java +++ b/src/test/java/org/folio/search/service/consortium/ConsortiumSearchHelperTest.java @@ -3,6 +3,7 @@ import static com.google.common.collect.Sets.newHashSet; import static org.assertj.core.api.Assertions.assertThat; import static org.folio.search.utils.SearchUtils.CONTRIBUTOR_RESOURCE; +import static org.folio.search.utils.SearchUtils.INSTANCE_RESOURCE; import static org.folio.search.utils.SearchUtils.INSTANCE_SUBJECT_RESOURCE; import static org.folio.search.utils.TestConstants.CENTRAL_TENANT_ID; import static org.folio.search.utils.TestConstants.TENANT_ID; @@ -37,8 +38,10 @@ 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"; + private static final String TENANT_ID_FIELD_NAME = "tenantId"; + private static final String TENANT_ID_FIELD = SUBRESOURCE_PREFIX + TENANT_ID_FIELD_NAME; + private static final String SHARED_FIELD_NAME = "shared"; + private static final String SHARED_FIELD = SUBRESOURCE_PREFIX + SHARED_FIELD_NAME; @Mock private FolioExecutionContext context; @@ -72,6 +75,43 @@ void filterQueryForActiveAffiliation_positive_basicNotConsortiumTenant() { verify(consortiumSearchHelper, times(0)).filterQueryForActiveAffiliation(any(), any(), any(), any()); } + @Test + void filterQueryActiveAffiliation_filteredByMemberTenant() { + var query = boolQuery() + .filter(termQuery("staffSuppress", false)) + .filter(termQuery(SHARED_FIELD_NAME, false)); + var expected = boolQuery() + .filter(termQuery("staffSuppress", false)) + .filter(termQuery(SHARED_FIELD_NAME, false)) + .minimumShouldMatch(1) + .should(termQuery(TENANT_ID_FIELD_NAME, TENANT_ID)) + .should(termQuery(SHARED_FIELD_NAME, true)); + + when(tenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CENTRAL_TENANT_ID)); + + var actual = consortiumSearchHelper.filterQueryForActiveAffiliation(query, INSTANCE_RESOURCE, TENANT_ID); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void filterQueryActiveAffiliation_notFilteredByMemberTenant() { + var query = boolQuery() + .filter(termQuery("staffSuppress", false)) + .filter(termQuery(SHARED_FIELD_NAME, false)); + var expected = boolQuery() + .filter(termQuery("staffSuppress", false)) + .filter(termQuery(SHARED_FIELD_NAME, false)) + .minimumShouldMatch(1) + .should(termQuery(SHARED_FIELD_NAME, true)); + + when(tenantService.getCentralTenant(CENTRAL_TENANT_ID)).thenReturn(Optional.of(CENTRAL_TENANT_ID)); + + var actual = consortiumSearchHelper.filterQueryForActiveAffiliation(query, INSTANCE_RESOURCE, CENTRAL_TENANT_ID); + + assertThat(actual).isEqualTo(expected); + } + @Test void filterQueryForActiveAffiliation_positive_basicConsortiumCentralTenant() { var query = matchAllQuery();