ACS-4160: Fixing and making GetTopCategories more re-usable (#1674)

* ACS-4160: Fixing and making GetTopCategories more re-usable

* Fixing a TYPO
This commit is contained in:
Maciej Pichura
2023-01-13 17:07:45 +01:00
committed by GitHub
parent 96384a289b
commit d3d1aaeba1
3 changed files with 251 additions and 51 deletions

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2020 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -32,6 +32,7 @@ import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -44,7 +45,10 @@ import org.alfresco.repo.search.IndexerAndSearcher;
import org.alfresco.repo.search.IndexerException;
import org.alfresco.repo.tenant.TenantService;
import org.alfresco.service.Experimental;
import org.alfresco.service.cmr.dictionary.AspectDefinition;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.dictionary.PropertyDefinition;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
import org.alfresco.service.cmr.repository.NodeRef;
@@ -546,6 +550,58 @@ public abstract class AbstractCategoryServiceImpl implements CategoryService
public abstract List<Pair<NodeRef, Integer>> getTopCategories(StoreRef storeRef, QName aspectName, int count);
/**
* Creates search query parameters used to get top categories.
* Can be used as a base both wih SOLR and ES.
* @param storeRef Node store reference
* @param aspectName Aspect name. "cm:generalclassifiable" aspect should be used for usual cases.
* It is possible to use a custom aspect but it must have valid category property
* @param count Will be used as faceted results limit, when system has very many categories this must be reflecting that number
* @return SearchParameters to perform search for top categories.
*/
protected SearchParameters createSearchTopCategoriesParameters(StoreRef storeRef, QName aspectName, int count) {
final AspectDefinition aspectDefinition = dictionaryService.getAspect(aspectName);
if(aspectDefinition == null)
{
throw new IllegalStateException("Unknown aspect");
}
final Map<QName, PropertyDefinition> aspectProperties = aspectDefinition.getProperties();
final Optional<QName> catProperty = aspectProperties.entrySet().stream()
//for backwards compatibility I'm leaving the part where we get custom category aspects
.filter(ap -> ContentModel.ASPECT_GEN_CLASSIFIABLE.isMatch(aspectName) || isValidCategoryTypeProperty(aspectName, ap))
.map(Map.Entry::getKey)
.findFirst();
return catProperty.map(cp -> {
final String field = "@" + cp;
final SearchParameters sp = new SearchParameters();
sp.addStore(storeRef);
sp.setQuery(cp + ":*");
//we only care about faceted results and don't need query results so we can limit them to minimum
sp.setMaxItems(1);
sp.setSkipCount(0);
final SearchParameters.FieldFacet ff = new SearchParameters.FieldFacet(field);
ff.setLimitOrNull(count < 0 ? null : count);
sp.addFieldFacet(ff);
return sp;
})
.orElseThrow(() -> new IllegalStateException("Aspect does not have category property mirroring the aspect name"));
}
/**
* Checks whether given aspect property definition is valid category property
* @param aspectName Aspect name
* @param propertyDef Aspect property definition.
* @return is valid category property
*/
private boolean isValidCategoryTypeProperty(QName aspectName, Map.Entry<QName, PropertyDefinition> propertyDef)
{
return propertyDef.getKey().getNamespaceURI().equals(aspectName.getNamespaceURI()) &&
propertyDef.getKey().getLocalName().equals(aspectName.getLocalName()) &&
DataTypeDefinition.CATEGORY.equals(propertyDef.getValue().getDataType().getName());
}
@Override
@Experimental
public Optional<NodeRef> getRootCategoryNodeRef(final StoreRef storeRef)

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2020 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -27,12 +27,8 @@ package org.alfresco.repo.search.impl.solr;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.alfresco.repo.search.impl.AbstractCategoryServiceImpl;
import org.alfresco.service.cmr.dictionary.AspectDefinition;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.dictionary.PropertyDefinition;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.search.ResultSet;
@@ -52,59 +48,30 @@ public class SolrCategoryServiceImpl extends AbstractCategoryServiceImpl
@Override
public List<Pair<NodeRef, Integer>> getTopCategories(StoreRef storeRef, QName aspectName, int count)
{
AspectDefinition definition = dictionaryService.getAspect(aspectName);
if(definition == null)
{
throw new IllegalStateException("Unknown aspect");
}
QName catProperty = null;
Map<QName, PropertyDefinition> properties = definition.getProperties();
for(QName pName : properties.keySet())
{
if(pName.getNamespaceURI().equals(aspectName.getNamespaceURI()))
{
if(pName.getLocalName().equalsIgnoreCase(aspectName.getLocalName()))
{
PropertyDefinition def = properties.get(pName);
if(def.getDataType().getName().equals(DataTypeDefinition.CATEGORY))
{
catProperty = pName;
}
}
}
}
if(catProperty == null)
{
throw new IllegalStateException("Aspect does not have category property mirroring the aspect name");
}
String field = "@" + catProperty;
SearchParameters sp = new SearchParameters();
sp.setLanguage(SearchService.LANGUAGE_INDEX_FTS_ALFRESCO);
sp.addStore(storeRef);
sp.setQuery(catProperty+":*");
FieldFacet ff = new FieldFacet(field);
ff.setLimitOrNull(count);
sp.addFieldFacet(ff);
final SearchParameters searchParameters = createSearchTopCategoriesParameters(storeRef, aspectName, count);
searchParameters.setLanguage(SearchService.LANGUAGE_INDEX_FTS_ALFRESCO);
final String field = searchParameters.getFieldFacets().stream()
.map(FieldFacet::getField)
.findFirst()
.orElse("");
ResultSet resultSet = null;
try
{
resultSet = indexerAndSearcher.getSearcher(storeRef, false).query(sp);
List<Pair<String, Integer>> facetCounts = resultSet.getFieldFacet(field);
List<Pair<NodeRef, Integer>> answer = new LinkedList<Pair<NodeRef, Integer>>();
resultSet = indexerAndSearcher.getSearcher(storeRef, false).query(searchParameters);
final List<Pair<String, Integer>> facetCounts = resultSet.getFieldFacet(field);
final List<Pair<NodeRef, Integer>> answer = new LinkedList<>();
for (Pair<String, Integer> term : facetCounts)
{
Pair<NodeRef, Integer> toAdd;
NodeRef nodeRef = new NodeRef(term.getFirst());
final NodeRef nodeRef = new NodeRef(term.getFirst());
if (nodeService.exists(nodeRef))
{
toAdd = new Pair<NodeRef, Integer>(nodeRef, term.getSecond());
toAdd = new Pair<>(nodeRef, term.getSecond());
}
else
{
toAdd = new Pair<NodeRef, Integer>(null, term.getSecond());
toAdd = new Pair<>(null, term.getSecond());
}
answer.add(toAdd);
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2022 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -26,20 +26,39 @@
package org.alfresco.repo.search.impl.solr;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.search.impl.noindex.NoIndexCategoryServiceImpl;
import org.alfresco.repo.search.IndexerAndSearcher;
import org.alfresco.service.cmr.dictionary.AspectDefinition;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.dictionary.PropertyDefinition;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.search.ResultSet;
import org.alfresco.service.cmr.search.SearchParameters;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.Pair;
import org.apache.commons.collections.CollectionUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
@@ -55,6 +74,8 @@ public class SolrCategoryServiceImplTest
{
private static final String PATH_ROOT = "-root-";
private static final String CAT_ROOT_NODE_ID = "cat-root-node-id";
private static final String NODE_ID_PREFIX = "node-id-";
private static final StoreRef STORE_REF = StoreRef.STORE_REF_WORKSPACE_SPACESSTORE;
@Mock
private NodeService nodeServiceMock;
@@ -62,9 +83,19 @@ public class SolrCategoryServiceImplTest
private ChildAssociationRef categoryRootChildAssociationRefMock;
@Mock
private ChildAssociationRef categoryChildAssociationRefMock;
@Mock
private IndexerAndSearcher indexerAndSearcherMock;
@Mock
private SearchService searcherMock;
@Mock
private DictionaryService dictionaryServiceMock;
@Mock
private AspectDefinition aspectDefinitionMock;
@Mock
private ResultSet resultSetMock;
@InjectMocks
private NoIndexCategoryServiceImpl objectUnderTest;
private SolrCategoryServiceImpl objectUnderTest;
@Test
public void testGetRootCategoryNodeRef()
@@ -90,4 +121,150 @@ public class SolrCategoryServiceImplTest
assertTrue(rooCategoryNodeRef.isPresent());
assertEquals(CAT_ROOT_NODE_ID, rooCategoryNodeRef.get().getId());
}
@Test
public void testGetTopCategories()
{
given(indexerAndSearcherMock.getSearcher(STORE_REF, false)).willReturn(searcherMock);
final QName aspectGenClassifiable = ContentModel.ASPECT_GEN_CLASSIFIABLE;
mockAspectDefinition(ContentModel.PROP_CATEGORIES, null);
given(dictionaryServiceMock.getAspect(aspectGenClassifiable)).willReturn(aspectDefinitionMock);
final QName categoryProperty = ContentModel.PROP_CATEGORIES;
final String field = getField(categoryProperty);
final int count = 100;
final SearchParameters searchParameters = prepareSearchParams(STORE_REF, categoryProperty, field, count);
final List<Integer> countList = List.of(11, 9, 8);
mockResultSet(field, countList);
given(searcherMock.query(searchParameters)).willReturn(resultSetMock);
given(nodeServiceMock.exists(any(NodeRef.class))).willReturn(true);
//when
final List<Pair<NodeRef, Integer>> topCategories = objectUnderTest.getTopCategories(STORE_REF, aspectGenClassifiable, count);
then(indexerAndSearcherMock).should().getSearcher(STORE_REF, false);
then(indexerAndSearcherMock).shouldHaveNoMoreInteractions();
then(dictionaryServiceMock).should().getAspect(aspectGenClassifiable);
then(dictionaryServiceMock).shouldHaveNoMoreInteractions();
then(searcherMock).should().query(searchParameters);
then(searcherMock).shouldHaveNoMoreInteractions();
then(nodeServiceMock).should(times(3)).exists(any(NodeRef.class));
then(nodeServiceMock).shouldHaveNoMoreInteractions();
IntStream.range(0, countList.size())
.forEach(i -> {
final NodeRef nodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, NODE_ID_PREFIX + i);
assertEquals(nodeRef, topCategories.get(i).getFirst());
assertEquals(countList.get(i), topCategories.get(i).getSecond());
});
}
@Test
public void testGetTopCategories_nonExistingAspect()
{
final QName aspectGenClassifiable = ContentModel.ASPECT_GEN_CLASSIFIABLE;
given(dictionaryServiceMock.getAspect(aspectGenClassifiable)).willReturn(null);
//when
assertThrows(IllegalStateException.class, () -> objectUnderTest.getTopCategories(STORE_REF, aspectGenClassifiable, 100));
then(indexerAndSearcherMock).shouldHaveNoInteractions();
then(dictionaryServiceMock).should().getAspect(aspectGenClassifiable);
then(dictionaryServiceMock).shouldHaveNoMoreInteractions();
then(searcherMock).shouldHaveNoInteractions();
then(nodeServiceMock).shouldHaveNoInteractions();
}
@Test
public void testGetTopCategories_customCategoryAspect()
{
given(indexerAndSearcherMock.getSearcher(STORE_REF, false)).willReturn(searcherMock);
final QName aspectCustomCategories = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "customcategories");
mockAspectDefinition(aspectCustomCategories, DataTypeDefinition.CATEGORY);
given(dictionaryServiceMock.getAspect(aspectCustomCategories)).willReturn(aspectDefinitionMock);
final String field = getField(aspectCustomCategories);
final int count = 100;
final SearchParameters searchParameters = prepareSearchParams(STORE_REF, aspectCustomCategories, field, count);
final List<Integer> countList = List.of(11, 9, 8);
mockResultSet(field, countList);
given(searcherMock.query(searchParameters)).willReturn(resultSetMock);
given(nodeServiceMock.exists(any(NodeRef.class))).willReturn(true);
//when
final List<Pair<NodeRef, Integer>> topCategories = objectUnderTest.getTopCategories(STORE_REF, aspectCustomCategories, count);
then(indexerAndSearcherMock).should().getSearcher(STORE_REF, false);
then(indexerAndSearcherMock).shouldHaveNoMoreInteractions();
then(dictionaryServiceMock).should().getAspect(aspectCustomCategories);
then(dictionaryServiceMock).shouldHaveNoMoreInteractions();
then(searcherMock).should().query(searchParameters);
then(searcherMock).shouldHaveNoMoreInteractions();
then(nodeServiceMock).should(times(3)).exists(any(NodeRef.class));
then(nodeServiceMock).shouldHaveNoMoreInteractions();
IntStream.range(0, countList.size())
.forEach(i -> {
final NodeRef nodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, NODE_ID_PREFIX + i);
assertEquals(nodeRef, topCategories.get(i).getFirst());
assertEquals(countList.get(i), topCategories.get(i).getSecond());
});
}
@Test
public void testGetTopCategories_invalidCustomCategoryAspect()
{
final QName aspectCustomCategories = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "customcategories");
mockAspectDefinition(aspectCustomCategories, DataTypeDefinition.QNAME);
given(dictionaryServiceMock.getAspect(aspectCustomCategories)).willReturn(aspectDefinitionMock);
final String field = getField(aspectCustomCategories);
final int count = 100;
//when
assertThrows(IllegalStateException.class, () -> objectUnderTest.getTopCategories(STORE_REF, aspectCustomCategories, count));
then(indexerAndSearcherMock).shouldHaveNoInteractions();
then(dictionaryServiceMock).should().getAspect(aspectCustomCategories);
then(dictionaryServiceMock).shouldHaveNoMoreInteractions();
then(searcherMock).shouldHaveNoInteractions();
then(nodeServiceMock).shouldHaveNoInteractions();
}
private String getField(QName categoryProperty)
{
return "@" + categoryProperty;
}
private SearchParameters prepareSearchParams(StoreRef storeRef, QName categoryProperty, String field, int count)
{
final SearchParameters sp = new SearchParameters();
sp.setLanguage(SearchService.LANGUAGE_INDEX_FTS_ALFRESCO);
sp.addStore(storeRef);
sp.setQuery(categoryProperty + ":*");
final SearchParameters.FieldFacet ff = new SearchParameters.FieldFacet(field);
ff.setLimitOrNull(count);
sp.addFieldFacet(ff);
sp.setMaxItems(1);
sp.setSkipCount(0);
return sp;
}
private void mockResultSet(final String field, List<Integer> countList)
{
final List<Pair<String, Integer>> facetedResults = IntStream.range(0, countList.size())
.mapToObj(i -> new Pair<>(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE + "/" + NODE_ID_PREFIX + i, countList.get(i)))
.collect(Collectors.toList());
given(resultSetMock.getFieldFacet(field)).willReturn(CollectionUtils.isEmpty(facetedResults) ? null : facetedResults);
}
private void mockAspectDefinition(final QName qName, final QName dataType)
{
final PropertyDefinition propertyDefinitionMock = mock(PropertyDefinition.class);
final DataTypeDefinition dataTypeMock = mock(DataTypeDefinition.class);
if (dataType != null)
{
given(propertyDefinitionMock.getDataType()).willReturn(dataTypeMock);
given(dataTypeMock.getName()).willReturn(dataType);
}
given(aspectDefinitionMock.getProperties()).willReturn(Map.of(qName, propertyDefinitionMock));
}
}