mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-07-24 17:32:48 +00:00
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:
@@ -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)
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user