[MNT-25111] Shared Links API with where clause does not work with Search Enterprise (#4296)

This commit is contained in:
Shreasi Khan
2026-08-24 14:56:42 +05:30
committed by GitHub
parent c6aaae7005
commit 69757afc96
8 changed files with 394 additions and 85 deletions
@@ -26,16 +26,24 @@
package org.alfresco.repo.search;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import org.alfresco.repo.search.impl.QueryParameterisationException;
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.repository.XPathException;
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
import org.alfresco.service.cmr.search.QueryParameter;
import org.alfresco.service.cmr.search.QueryParameterDefinition;
import org.alfresco.service.cmr.search.ResultSet;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.namespace.NamespacePrefixResolver;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.SearchLanguageConversion;
/**
@@ -73,4 +81,90 @@ public abstract class AbstractSearcherComponent implements SearchService
return selectProperties(contextNodeRef, xpath, parameters, namespacePrefixResolver, followAllParentLinks,
SearchService.LANGUAGE_XPATH);
}
@SuppressWarnings({"PMD.CognitiveComplexity", "PMD.CyclomaticComplexity", "PMD.NPathComplexity", "PMD.AssignmentInOperand"})
protected String parameterise(String unparameterised, Map<QName, QueryParameterDefinition> map, QueryParameter[] queryParameters, NamespacePrefixResolver nspr)
throws QueryParameterisationException
{
Map<QName, List<Serializable>> valueMap = new HashMap<>();
if (queryParameters != null)
{
for (QueryParameter parameter : queryParameters)
{
List<Serializable> list = valueMap.get(parameter.getQName());
if (list == null)
{
list = new ArrayList<>();
valueMap.put(parameter.getQName(), list);
}
list.add(parameter.getValue());
}
}
Map<QName, ListIterator<Serializable>> iteratorMap = new HashMap<>();
List<QName> missing = new ArrayList<>(1);
StringBuilder buffer = new StringBuilder(unparameterised);
int index = 0;
while ((index = buffer.indexOf("${", index)) != -1)
{
int endIndex = buffer.indexOf("}", index);
if (endIndex == -1)
{
throw new QueryParameterisationException("Unclosed query parameter placeholder starting at index " + index);
}
String qNameString = buffer.substring(index + 2, endIndex);
QName key = QName.createQName(qNameString, nspr);
QueryParameterDefinition parameterDefinition = map.get(key);
if (parameterDefinition == null)
{
missing.add(key);
buffer.replace(index, endIndex + 1, "");
}
else
{
ListIterator<Serializable> it = iteratorMap.get(key);
if ((it == null) || (!it.hasNext()))
{
List<Serializable> list = valueMap.get(key);
if ((list != null) && (!list.isEmpty()))
{
it = list.listIterator();
}
if (it != null)
{
iteratorMap.put(key, it);
}
}
String value;
if (it == null)
{
if (!parameterDefinition.hasDefaultValue())
{
throw new QueryParameterisationException("No value provided for query parameter: " + key);
}
value = parameterDefinition.getDefault();
}
else
{
value = DefaultTypeConverter.INSTANCE.convert(String.class, it.next());
}
buffer.replace(index, endIndex + 1, value);
}
}
if (!missing.isEmpty())
{
StringBuilder error = new StringBuilder();
error.append("The query uses the following parameters which are not defined: ");
for (QName qName : missing)
{
error.append(qName).append(", ");
}
error.delete(error.length() - 2, error.length());
throw new QueryParameterisationException(error.toString());
}
return buffer.toString();
}
}
@@ -28,6 +28,7 @@ package org.alfresco.repo.search.impl.elasticsearch;
import static org.alfresco.repo.search.adaptor.QueryConstants.FIELD_TAG;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
@@ -66,16 +67,19 @@ public class ElasticsearchSearchService extends AbstractSearcherComponent
private final Map<String, LuceneQueryLanguageSPI> queryLanguages;
private NodeService nodeService;
private DictionaryService dictionaryService;
private NamespacePrefixResolver namespacePrefixResolver;
public ElasticsearchSearchService(QueryRegisterComponent queryRegister,
Map<String, LuceneQueryLanguageSPI> queryLanguages,
NodeService nodeService,
DictionaryService dictionaryService)
DictionaryService dictionaryService,
NamespacePrefixResolver namespacePrefixResolver)
{
this.queryRegister = queryRegister;
this.queryLanguages = queryLanguages;
this.nodeService = nodeService;
this.dictionaryService = dictionaryService;
this.namespacePrefixResolver = namespacePrefixResolver;
}
@Override
@@ -115,6 +119,7 @@ public class ElasticsearchSearchService extends AbstractSearcherComponent
public ResultSet query(SearchParameters searchParameters)
{
adjustSearchParameters(searchParameters);
parameteriseQuery(searchParameters);
LuceneQueryLanguageSPI language = queryLanguages.get(searchParameters.getLanguage().toLowerCase());
if (language != null)
{
@@ -126,6 +131,25 @@ public class ElasticsearchSearchService extends AbstractSearcherComponent
}
}
private void parameteriseQuery(SearchParameters searchParameters)
{
if (searchParameters.getQuery() == null
|| searchParameters.getQueryParameterDefinitions() == null
|| searchParameters.getQueryParameterDefinitions().isEmpty())
{
return;
}
Map<QName, QueryParameterDefinition> map = new HashMap<>();
for (QueryParameterDefinition qpd : searchParameters.getQueryParameterDefinitions())
{
map.put(qpd.getQName(), qpd);
}
String parameterisedQueryString = parameterise(searchParameters.getQuery(), map, null, namespacePrefixResolver);
searchParameters.setQuery(parameterisedQueryString);
}
@Override
public List<NodeRef> selectNodes(NodeRef contextNodeRef, String xpath, QueryParameterDefinition[] parameters,
NamespacePrefixResolver namespacePrefixResolver, boolean followAllParentLinks)
@@ -35,6 +35,7 @@ import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.namespace.NamespacePrefixResolver;
/**
* Factory for Indexer and Searcher instances for Elasticsearch
@@ -44,6 +45,7 @@ public class ElasticsearchSearchServiceFactory extends AbstractIndexerAndSearche
private QueryRegisterComponent queryRegister;
private NodeService nodeService;
private DictionaryService dictionaryService;
private NamespacePrefixResolver namespacePrefixResolver;
/**
* Elasticsearch is indexing the repository by polling database or listening queue messages from an external service, so no Indexer is available for the Repository.
@@ -57,7 +59,7 @@ public class ElasticsearchSearchServiceFactory extends AbstractIndexerAndSearche
@Override
public SearchService getSearcher(StoreRef storeRef, boolean searchDelta) throws SearcherException
{
return new ElasticsearchSearchService(queryRegister, getQueryLanguages(), nodeService, dictionaryService);
return new ElasticsearchSearchService(queryRegister, getQueryLanguages(), nodeService, dictionaryService, namespacePrefixResolver);
}
@Override
@@ -80,4 +82,9 @@ public class ElasticsearchSearchServiceFactory extends AbstractIndexerAndSearche
{
this.dictionaryService = dictionaryService;
}
public void setNamespacePrefixResolver(NamespacePrefixResolver namespacePrefixResolver)
{
this.namespacePrefixResolver = namespacePrefixResolver;
}
}
@@ -30,10 +30,10 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import org.alfresco.repo.search.AbstractSearcherComponent;
import org.alfresco.repo.search.CannedQueryDef;
import org.alfresco.repo.search.QueryRegisterComponent;
import org.alfresco.repo.search.SearcherException;
@@ -61,7 +61,7 @@ import org.alfresco.util.SearchLanguageConversion;
/**
* @author Andy
*/
public class SolrSearchService implements SearchService
public class SolrSearchService extends AbstractSearcherComponent
{
private NodeService nodeService;
@@ -227,86 +227,6 @@ public class SolrSearchService implements SearchService
}
}
/* Parameterise the query string - not sure if it is required to escape lucence spacials chars The parameters could be used to build the query - the contents of parameters should alread have been escaped if required. ... mush better to provide the parameters and work out what to do TODO: conditional query escapement - may be we should have a parameter type that is not escaped */
private String parameterise(String unparameterised, Map<QName, QueryParameterDefinition> map, QueryParameter[] queryParameters, NamespacePrefixResolver nspr)
throws QueryParameterisationException
{
Map<QName, List<Serializable>> valueMap = new HashMap<QName, List<Serializable>>();
if (queryParameters != null)
{
for (QueryParameter parameter : queryParameters)
{
List<Serializable> list = valueMap.get(parameter.getQName());
if (list == null)
{
list = new ArrayList<Serializable>();
valueMap.put(parameter.getQName(), list);
}
list.add(parameter.getValue());
}
}
Map<QName, ListIterator<Serializable>> iteratorMap = new HashMap<QName, ListIterator<Serializable>>();
List<QName> missing = new ArrayList<QName>(1);
StringBuilder buffer = new StringBuilder(unparameterised);
int index = 0;
while ((index = buffer.indexOf("${", index)) != -1)
{
int endIndex = buffer.indexOf("}", index);
String qNameString = buffer.substring(index + 2, endIndex);
QName key = QName.createQName(qNameString, nspr);
QueryParameterDefinition parameterDefinition = map.get(key);
if (parameterDefinition == null)
{
missing.add(key);
buffer.replace(index, endIndex + 1, "");
}
else
{
ListIterator<Serializable> it = iteratorMap.get(key);
if ((it == null) || (!it.hasNext()))
{
List<Serializable> list = valueMap.get(key);
if ((list != null) && (list.size() > 0))
{
it = list.listIterator();
}
if (it != null)
{
iteratorMap.put(key, it);
}
}
String value;
if (it == null)
{
value = parameterDefinition.getDefault();
}
else
{
value = DefaultTypeConverter.INSTANCE.convert(String.class, it.next());
}
buffer.replace(index, endIndex + 1, value);
}
}
if (missing.size() > 0)
{
StringBuilder error = new StringBuilder();
error.append("The query uses the following parameters which are not defined: ");
for (QName qName : missing)
{
error.append(qName);
error.append(", ");
}
error.delete(error.length() - 1, error.length() - 1);
error.delete(error.length() - 1, error.length() - 1);
throw new QueryParameterisationException(error.toString());
}
return buffer.toString();
}
/* (non-Javadoc)
*
* @see org.alfresco.service.cmr.search.SearchService#query(org.alfresco.service.cmr.search.SearchParameters) */
@@ -164,6 +164,7 @@
<property name="queryRegister" ref="search.queryRegisterComponent" />
<property name="nodeService" ref="nodeService" />
<property name="dictionaryService" ref="DictionaryService" />
<property name="namespacePrefixResolver" ref="namespaceService" />
</bean>
<bean id="elasticsearchQueryExecutor" abstract="true" class="org.alfresco.repo.search.impl.elasticsearch.query.ElasticsearchQueryExecutor">
@@ -243,6 +243,7 @@ import org.alfresco.util.testing.category.NonBuildTests;
org.alfresco.repo.domain.schema.script.ScriptBundleExecutorImplTest.class,
org.alfresco.repo.search.MLAnaysisModeExpansionTest.class,
org.alfresco.repo.search.DocumentNavigatorTest.class,
org.alfresco.repo.search.AbstractSearcherComponentTest.class,
org.alfresco.util.NumericEncodingTest.class,
org.alfresco.repo.search.impl.parsers.CMIS_FTSTest.class,
org.alfresco.repo.search.impl.parsers.CMISTest.class,
@@ -0,0 +1,223 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2026 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.search;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.alfresco.repo.search.impl.QueryParameterisationException;
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.repository.XPathException;
import org.alfresco.service.cmr.search.QueryParameter;
import org.alfresco.service.cmr.search.QueryParameterDefinition;
import org.alfresco.service.cmr.search.ResultSet;
import org.alfresco.service.cmr.search.SearchParameters;
import org.alfresco.service.namespace.NamespacePrefixResolver;
import org.alfresco.service.namespace.QName;
@RunWith(MockitoJUnitRunner.class)
public class AbstractSearcherComponentTest
{
private static final String TEST_URI = "http://www.alfresco.org/test/1.0";
private static final String PREFIX = "test";
private final AbstractSearcherComponent searcher = new StubSearcherComponent();
@Mock
private NamespacePrefixResolver namespacePrefixResolver;
@Before
public void setUp()
{
lenient().when(namespacePrefixResolver.getNamespaceURI(PREFIX)).thenReturn(TEST_URI);
}
private static QName paramQName(String localName)
{
return QName.createQName(TEST_URI, localName);
}
@Test
public void parameterise_substitutesSuppliedParameterValue()
{
Map<QName, QueryParameterDefinition> map = new HashMap<>();
map.put(paramQName("param"), mock(QueryParameterDefinition.class));
QueryParameter parameter = mock(QueryParameter.class);
when(parameter.getQName()).thenReturn(paramQName("param"));
when(parameter.getValue()).thenReturn("hello");
String result = searcher.parameterise("value:${test:param}", map, new QueryParameter[]{parameter}, namespacePrefixResolver);
assertThat(result).isEqualTo("value:hello");
}
@Test
public void parameterise_substitutesDefaultValueWhenNoParameterSupplied()
{
QueryParameterDefinition qpd = mock(QueryParameterDefinition.class);
when(qpd.hasDefaultValue()).thenReturn(true);
when(qpd.getDefault()).thenReturn("admin");
Map<QName, QueryParameterDefinition> map = new HashMap<>();
map.put(paramQName("param"), qpd);
String result = searcher.parameterise("user:${test:param}", map, null, namespacePrefixResolver);
assertThat(result).isEqualTo("user:admin");
}
@Test
public void parameterise_iteratesSuppliedValuesInOrderForRepeatedPlaceholder()
{
Map<QName, QueryParameterDefinition> map = new HashMap<>();
map.put(paramQName("param"), mock(QueryParameterDefinition.class));
QueryParameter first = mock(QueryParameter.class);
when(first.getQName()).thenReturn(paramQName("param"));
when(first.getValue()).thenReturn("one");
QueryParameter second = mock(QueryParameter.class);
when(second.getQName()).thenReturn(paramQName("param"));
when(second.getValue()).thenReturn("two");
String result = searcher.parameterise("${test:param}-${test:param}", map, new QueryParameter[]{first, second}, namespacePrefixResolver);
assertThat(result).isEqualTo("one-two");
}
@Test
public void parameterise_returnsQueryUnchangedWhenNoPlaceholders()
{
String result = searcher.parameterise("TYPE:\"cm:content\"", new HashMap<>(), null, namespacePrefixResolver);
assertThat(result).isEqualTo("TYPE:\"cm:content\"");
}
@Test
public void parameterise_throwsWhenPlaceholderReferencesUndefinedParameter()
{
assertThatExceptionOfType(QueryParameterisationException.class)
.isThrownBy(() -> searcher.parameterise("value:${test:param}", new HashMap<>(), null, namespacePrefixResolver))
.withMessageContaining("not defined");
}
@Test
public void parameterise_throwsWhenPlaceholderIsUnclosed()
{
assertThatExceptionOfType(QueryParameterisationException.class)
.isThrownBy(() -> searcher.parameterise("value:${test:param", new HashMap<>(), null, namespacePrefixResolver))
.withMessageContaining("Unclosed");
}
@Test
public void parameterise_throwsWhenParameterHasNoValueAndNoDefault()
{
QueryParameterDefinition qpd = mock(QueryParameterDefinition.class);
when(qpd.hasDefaultValue()).thenReturn(false);
Map<QName, QueryParameterDefinition> map = new HashMap<>();
map.put(paramQName("param"), qpd);
assertThatExceptionOfType(QueryParameterisationException.class)
.isThrownBy(() -> searcher.parameterise("value:${test:param}", map, null, namespacePrefixResolver))
.withMessageContaining("No value provided");
}
/**
* Minimal concrete subclass so the shared {@code protected} parameterise implementation can be exercised in isolation. The remaining {@link org.alfresco.service.cmr.search.SearchService} operations are not needed here.
*/
private static final class StubSearcherComponent extends AbstractSearcherComponent
{
@Override
public ResultSet query(StoreRef store, String language, String query, QueryParameterDefinition[] queryParameterDefinitions)
{
throw new UnsupportedOperationException();
}
@Override
public ResultSet query(StoreRef store, QName queryId, QueryParameter[] queryParameters)
{
throw new UnsupportedOperationException();
}
@Override
public ResultSet query(SearchParameters searchParameters)
{
throw new UnsupportedOperationException();
}
@Override
public List<NodeRef> selectNodes(NodeRef contextNodeRef, String xpath, QueryParameterDefinition[] parameters,
NamespacePrefixResolver namespacePrefixResolver, boolean followAllParentLinks, String language)
throws InvalidNodeRefException, XPathException
{
throw new UnsupportedOperationException();
}
@Override
public List<Serializable> selectProperties(NodeRef contextNodeRef, String xpath, QueryParameterDefinition[] parameters,
NamespacePrefixResolver namespacePrefixResolver, boolean followAllParentLinks, String language)
throws InvalidNodeRefException, XPathException
{
throw new UnsupportedOperationException();
}
@Override
public boolean contains(NodeRef nodeRef, QName propertyQName, String googleLikePattern) throws InvalidNodeRefException
{
throw new UnsupportedOperationException();
}
@Override
public boolean contains(NodeRef nodeRef, QName propertyQName, String googleLikePattern, SearchParameters.Operator defaultOperator)
throws InvalidNodeRefException
{
throw new UnsupportedOperationException();
}
@Override
public boolean like(NodeRef nodeRef, QName propertyQName, String sqlLikePattern, boolean includeFTS) throws InvalidNodeRefException
{
throw new UnsupportedOperationException();
}
}
}
@@ -33,6 +33,7 @@ import static org.mockito.Mockito.*;
import static org.alfresco.service.cmr.search.SearchService.LANGUAGE_FTS_ALFRESCO;
import static org.alfresco.service.cmr.search.SearchService.LANGUAGE_LUCENE;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
@@ -49,8 +50,11 @@ import org.alfresco.repo.search.SearcherException;
import org.alfresco.repo.search.impl.lucene.LuceneQueryLanguageSPI;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.search.QueryParameterDefinition;
import org.alfresco.service.cmr.search.ResultSet;
import org.alfresco.service.cmr.search.SearchParameters;
import org.alfresco.service.namespace.NamespacePrefixResolver;
import org.alfresco.service.namespace.QName;
@RunWith(MockitoJUnitRunner.class)
public class ElasticsearchSearchServiceTest
@@ -71,6 +75,9 @@ public class ElasticsearchSearchServiceTest
@Mock
private DictionaryService dictionaryService;
@Mock
private NamespacePrefixResolver namespacePrefixResolver;
@Mock
private SearchParameters searchParameters;
@@ -83,9 +90,10 @@ public class ElasticsearchSearchServiceTest
Map<String, LuceneQueryLanguageSPI> languages = new HashMap<>();
languages.put(LANGUAGE.toLowerCase(Locale.getDefault()), queryLanguage);
searchService = new ElasticsearchSearchService(queryRegister, languages, nodeService, dictionaryService);
searchService = new ElasticsearchSearchService(queryRegister, languages, nodeService, dictionaryService, namespacePrefixResolver);
when(searchParameters.getLanguage()).thenReturn(LANGUAGE);
when(searchParameters.getQueryParameterDefinitions()).thenReturn(new ArrayList<>());
when(queryLanguage.executeQuery(any(SearchParameters.class))).thenReturn(resultSet);
}
@@ -152,6 +160,37 @@ public class ElasticsearchSearchServiceTest
verify(searchParameters, times(1)).setLanguage(LANGUAGE_FTS_ALFRESCO);
}
@Test
public void luceneQueryWithParameterDefinition_shouldSubstitutePlaceholderWithDefaultValue()
{
String qshareUri = "http://www.alfresco.org/model/qshare/1.0";
when(namespacePrefixResolver.getNamespaceURI("qshare")).thenReturn(qshareUri);
QName sharedBy = QName.createQName(qshareUri, "sharedBy");
QueryParameterDefinition qpd = mock(QueryParameterDefinition.class);
when(qpd.getQName()).thenReturn(sharedBy);
when(qpd.hasDefaultValue()).thenReturn(true);
when(qpd.getDefault()).thenReturn("admin");
when(searchParameters.getQuery()).thenReturn("ASPECT:\"qshare:shared\" +@qshare:sharedBy:\"${qshare:sharedBy}\"");
when(searchParameters.getQueryParameterDefinitions()).thenReturn(new ArrayList<>(List.of(qpd)));
searchService.query(searchParameters);
verify(searchParameters, times(1)).setQuery("ASPECT:\"qshare:shared\" +@qshare:sharedBy:\"admin\"");
}
@Test
public void luceneQueryWithoutParameterDefinitions_shouldNotSubstitute()
{
when(searchParameters.getQuery()).thenReturn("ASPECT:\"qshare:shared\"");
when(searchParameters.getQueryParameterDefinitions()).thenReturn(new ArrayList<>());
searchService.query(searchParameters);
verify(searchParameters, never()).setQuery(any());
}
@Test
public void luceneQuery_whenNotMatchingTagQuery_shouldNotBeModified()
{