From d39401a7ecdeb7e68923de160ee4503216123f68 Mon Sep 17 00:00:00 2001 From: George Evangelopoulos Date: Wed, 19 Apr 2023 11:41:33 +0100 Subject: [PATCH] ACS-4025: Support include count and orderBy count for GET /tags (#1806) * ACS-4025: Support include count and orderBy count for GET /tags * ACS-4025: add E2Es --- .../org/alfresco/rest/tags/GetTagsTests.java | 132 +++++- .../org/alfresco/rest/tags/TagsDataPrep.java | 5 +- .../org/alfresco/rest/api/impl/TagsImpl.java | 46 +- .../alfresco/rest/api/impl/TagsImplTest.java | 184 +++++++- .../alfresco/repo/jscript/Classification.java | 444 +++++++++--------- .../impl/AbstractCategoryServiceImpl.java | 78 +-- .../repo/tagging/TaggingServiceImpl.java | 101 +++- .../service/cmr/search/CategoryService.java | 13 +- .../service/cmr/tagging/TaggingService.java | 23 +- 9 files changed, 718 insertions(+), 308 deletions(-) diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/tags/GetTagsTests.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/tags/GetTagsTests.java index 1b42ea0e2d..2648289e60 100644 --- a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/tags/GetTagsTests.java +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/tags/GetTagsTests.java @@ -16,6 +16,11 @@ import org.alfresco.utility.testrail.annotation.TestRail; import org.springframework.http.HttpStatus; import org.testng.annotations.Test; +import java.util.Set; +import java.util.stream.IntStream; + +import static org.alfresco.utility.report.log.Step.STEP; + @Test(groups = {TestGroup.REQUIRE_SOLR}) public class GetTagsTests extends TagsDataPrep { @@ -72,6 +77,130 @@ public class GetTagsTests extends TagsDataPrep .and().entriesListContains("tag", documentTagValue2); } + /** + * Include count in the query parameters and ensure count is as expected for returned tags. + */ + @Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION }) + public void testGetTags_withIncludeCount() + { + STEP("Get tags including count filter and ensure count is as expected for returned tags"); + returnedCollection = restClient.authenticateUser(adminUserModel) + .withParams("include=count") + .withCoreAPI() + .getTags(); + restClient.assertStatusCodeIs(HttpStatus.OK); + + returnedCollection.getEntries().stream() + .filter(e -> e.onModel().getTag().equals(folderTagValue) || e.onModel().getTag().equals(documentTagValue)) + .forEach(e -> e.onModel().assertThat().field("count").is(2)); + + returnedCollection.getEntries().stream() + .filter(e -> e.onModel().getTag().equals(documentTagValue2)) + .forEach(e -> e.onModel().assertThat().field("count").is(1)); + } + + /** + * Get tags and order results by count. Default sort order should be ascending + */ + @Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION }) + public void testGetTags_withOrderByCountDefaultOrderShouldBeAsc() + { + + STEP("Get tags and order results by count. Default sort order should be ascending"); + returnedCollection = restClient.authenticateUser(adminUserModel) + .withParams("include=count&orderBy=count") + .withCoreAPI() + .getTags(); + + restClient.assertStatusCodeIs(HttpStatus.OK); + returnedCollection.assertThat().entriesListIsSortedAscBy("count"); + } + + /** + * Get tags and order results by count in ascending order + */ + @Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION }) + public void testGetTags_withOrderByCountAsc() + { + + STEP("Get tags and order results by count in ascending order"); + returnedCollection = restClient.authenticateUser(adminUserModel) + .withParams("include=count&orderBy=count ASC") + .withCoreAPI() + .getTags(); + + restClient.assertStatusCodeIs(HttpStatus.OK); + returnedCollection.assertThat().entriesListIsSortedAscBy("count"); + } + + /** + * Get tags and order results by count in descending order + */ + @Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION }) + public void testGetTags_withOrderByCountDesc() + { + + STEP("Get tags and order results by count in descending order"); + returnedCollection = restClient.authenticateUser(adminUserModel) + .withParams("include=count&orderBy=count DESC") + .withCoreAPI() + .getTags(); + + restClient.assertStatusCodeIs(HttpStatus.OK); + returnedCollection.assertThat().entriesListIsSortedDescBy("count"); + } + + /** + * Get tags and order results by tag name. Default sort order should be ascending + */ + @Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION }) + public void testGetTags_withOrderByTagDefaultOrderShouldBeAsc() + { + + STEP("Get tags and order results by tag name. Default sort order should be ascending"); + returnedCollection = restClient.authenticateUser(adminUserModel) + .withParams("orderBy=tag") + .withCoreAPI() + .getTags(); + + restClient.assertStatusCodeIs(HttpStatus.OK); + returnedCollection.assertThat().entriesListIsSortedAscBy("tag"); + } + + /** + * Get tags and order results by tag name in ascending order + */ + @Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION }) + public void testGetTags_withOrderByTagAsc() + { + + STEP("Get tags and order results by tag name in ascending order"); + returnedCollection = restClient.authenticateUser(adminUserModel) + .withParams("orderBy=tag ASC") + .withCoreAPI() + .getTags(); + + restClient.assertStatusCodeIs(HttpStatus.OK); + returnedCollection.assertThat().entriesListIsSortedAscBy("tag"); + } + + /** + * Get tags and order results by tag name in descending order + */ + @Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION }) + public void testGetTags_withOrderByTagDesc() + { + + STEP("Get tags and order results by tag name in descending order"); + returnedCollection = restClient.authenticateUser(adminUserModel) + .withParams("orderBy=tag DESC") + .withCoreAPI() + .getTags(); + + restClient.assertStatusCodeIs(HttpStatus.OK); + returnedCollection.assertThat().entriesListIsSortedDescBy("tag"); + } + @TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.SANITY, description = "Failed authentication get tags call returns status code 401 with Manager role") @Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.SANITY }) // @Bug(id="MNT-16904", description = "It fails only on environment with tenants") @@ -193,8 +322,7 @@ public class GetTagsTests extends TagsDataPrep .getPagination().assertThat().field("maxItems").is(100) .and().field("hasMoreItems").is("false") .and().field("count").is("0") - .and().field("skipCount").is(20000) - .and().field("totalItems").is(0); + .and().field("skipCount").is(20000); } @TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.REGRESSION, diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/tags/TagsDataPrep.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/tags/TagsDataPrep.java index 6fd9b3f1f4..caf3c923b3 100644 --- a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/tags/TagsDataPrep.java +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/tags/TagsDataPrep.java @@ -22,7 +22,8 @@ public class TagsDataPrep extends RestTest protected static ListUserWithRoles usersWithRoles; protected static SiteModel siteModel; protected static FileModel document; - protected static FolderModel folder; + protected static FolderModel folder, folder2; + protected static RestTagModelsCollection folder2tags; protected static String documentTagValue, documentTagValue2, folderTagValue; protected static RestTagModel documentTag, documentTag2, folderTag, orphanTag, returnedModel; protected static RestTagModelsCollection returnedCollection; @@ -38,6 +39,7 @@ public class TagsDataPrep extends RestTest usersWithRoles = dataUser.usingAdmin().addUsersWithRolesToSite(siteModel, UserRole.SiteManager, UserRole.SiteCollaborator, UserRole.SiteConsumer, UserRole.SiteContributor); document = dataContent.usingUser(adminUserModel).usingSite(siteModel).createContent(CMISUtil.DocumentType.TEXT_PLAIN); folder = dataContent.usingUser(adminUserModel).usingSite(siteModel).createFolder(); + folder2 = dataContent.usingUser(adminUserModel).usingSite(siteModel).createFolder(); documentTagValue = RandomData.getRandomName("tag").toLowerCase(); documentTagValue2 = RandomData.getRandomName("tag").toLowerCase(); @@ -48,6 +50,7 @@ public class TagsDataPrep extends RestTest documentTag2 = restClient.withCoreAPI().usingResource(document).addTag(documentTagValue2); folderTag = restClient.withCoreAPI().usingResource(folder).addTag(folderTagValue); orphanTag = restClient.withCoreAPI().createSingleTag(RestTagModel.builder().tag(RandomData.getRandomName("orphan-tag").toLowerCase()).create()); + folder2tags = restClient.withCoreAPI().usingResource(folder2).addTags(folderTagValue, documentTagValue); // Allow indexing to complete. Utility.sleep(500, 60000, () -> diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/TagsImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/TagsImpl.java index 49263d2af1..ad9992cf09 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/impl/TagsImpl.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/TagsImpl.java @@ -36,6 +36,7 @@ import static org.alfresco.service.cmr.tagging.TaggingService.TAG_ROOT_NODE_REF; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -44,6 +45,8 @@ import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; +import org.alfresco.model.ContentModel; +import org.alfresco.query.ListBackedPagingResults; import org.alfresco.query.PagingResults; import org.alfresco.repo.tagging.NonExistentTagException; import org.alfresco.repo.tagging.TagExistsException; @@ -60,10 +63,12 @@ import org.alfresco.rest.framework.core.exceptions.UnsupportedResourceOperationE import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; import org.alfresco.rest.framework.resource.parameters.Paging; import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.resource.parameters.SortColumn; import org.alfresco.rest.framework.resource.parameters.where.Query; import org.alfresco.rest.framework.resource.parameters.where.QueryHelper; import org.alfresco.rest.framework.resource.parameters.where.QueryImpl; import org.alfresco.service.Experimental; +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; @@ -166,36 +171,25 @@ public class TagsImpl implements Tags taggingService.deleteTag(storeRef, tagValue); } - @Override + @Override public CollectionWithPagingInfo getTags(StoreRef storeRef, Parameters params) { - Paging paging = params.getPaging(); - Map> namesFilters = resolveTagNamesQuery(params.getQuery()); - PagingResults> results = taggingService.getTags(storeRef, Util.getPagingRequest(paging), namesFilters.get(EQUALS), namesFilters.get(MATCHES)); + Paging paging = params.getPaging(); + Pair sorting = !params.getSorting().isEmpty() ? new Pair<>(params.getSorting().get(0).column, params.getSorting().get(0).asc) : null; + Map> namesFilters = resolveTagNamesQuery(params.getQuery()); - Integer totalItems = results.getTotalResultCount().getFirst(); - List> page = results.getPage(); - List tags = new ArrayList<>(page.size()); - for (Pair pair : page) - { - Tag selectedTag = new Tag(pair.getFirst(), pair.getSecond()); - tags.add(selectedTag); - } - if (params.getInclude().contains(PARAM_INCLUDE_COUNT)) - { - List> tagsByCount = taggingService.findTaggedNodesAndCountByTagName(storeRef); - Map tagsByCountMap = new HashMap<>(); - if (tagsByCount != null) - { - for (Pair tagByCountElem : tagsByCount) - { - tagsByCountMap.put(tagByCountElem.getFirst(), Long.valueOf(tagByCountElem.getSecond())); - } - } - tags.forEach(tag -> tag.setCount(Optional.ofNullable(tagsByCountMap.get(tag.getTag())).orElse(0L))); - } + Map results = taggingService.getTags(storeRef, params.getInclude(), sorting, namesFilters.get(EQUALS), namesFilters.get(MATCHES)); - return CollectionWithPagingInfo.asPaged(paging, tags, results.hasMoreItems(), totalItems); + List tagsList = results.entrySet().stream().map(entry -> new Tag(entry.getKey(), (String)nodeService.getProperty(entry.getKey(), ContentModel.PROP_NAME))).collect(Collectors.toList()); + + if (params.getInclude().contains(PARAM_INCLUDE_COUNT)) + { + tagsList.forEach(tag -> tag.setCount(results.get(tag.getNodeRef()))); + } + + ListBackedPagingResults listBackedPagingResults = new ListBackedPagingResults(tagsList, Util.getPagingRequest(params.getPaging())); + + return CollectionWithPagingInfo.asPaged(paging, listBackedPagingResults.getPage(), listBackedPagingResults.hasMoreItems(), (Integer) listBackedPagingResults.getTotalResultCount().getFirst()); } public NodeRef validateTag(String tagId) diff --git a/remote-api/src/test/java/org/alfresco/rest/api/impl/TagsImplTest.java b/remote-api/src/test/java/org/alfresco/rest/api/impl/TagsImplTest.java index e3b457a77b..e231903f04 100644 --- a/remote-api/src/test/java/org/alfresco/rest/api/impl/TagsImplTest.java +++ b/remote-api/src/test/java/org/alfresco/rest/api/impl/TagsImplTest.java @@ -29,7 +29,6 @@ import static java.util.stream.Collectors.toList; import static org.alfresco.rest.api.impl.TagsImpl.NOT_A_VALID_TAG; import static org.alfresco.rest.api.impl.TagsImpl.NO_PERMISSION_TO_MANAGE_A_TAG; -import static org.alfresco.rest.api.impl.TagsImpl.PARAM_INCLUDE_COUNT; import static org.alfresco.service.cmr.repository.StoreRef.STORE_REF_WORKSPACE_SPACESSTORE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; @@ -41,10 +40,14 @@ import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; +import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Set; +import org.alfresco.model.ContentModel; import org.alfresco.query.PagingRequest; import org.alfresco.query.PagingResults; import org.alfresco.rest.api.Nodes; @@ -56,6 +59,7 @@ import org.alfresco.rest.framework.core.exceptions.UnsupportedResourceOperationE import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; import org.alfresco.rest.framework.resource.parameters.Paging; import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.resource.parameters.SortColumn; import org.alfresco.rest.framework.resource.parameters.where.InvalidQueryException; import org.alfresco.rest.framework.tools.RecognizedParamsExtractor; import org.alfresco.service.cmr.repository.ChildAssociationRef; @@ -84,6 +88,8 @@ public class TagsImplTest private static final NodeRef TAG_PARENT_NODE_REF = new NodeRef(STORE_REF_WORKSPACE_SPACESSTORE, PARENT_NODE_ID); private static final String CONTENT_NODE_ID = "content-node-id"; private static final NodeRef CONTENT_NODE_REF = new NodeRef(STORE_REF_WORKSPACE_SPACESSTORE, CONTENT_NODE_ID); + private static final String PARAM_INCLUDE_COUNT = "count"; + private final RecognizedParamsExtractor queryExtractor = new RecognizedParamsExtractor() {}; @@ -123,13 +129,16 @@ public class TagsImplTest public void testGetTags() { given(parametersMock.getPaging()).willReturn(pagingMock); - given(taggingServiceMock.getTags(any(StoreRef.class), any(PagingRequest.class), any(), any())).willReturn(pagingResultsMock); - given(pagingResultsMock.getTotalResultCount()).willReturn(new Pair<>(Integer.MAX_VALUE, 0)); - given(pagingResultsMock.getPage()).willReturn(List.of(new Pair<>(TAG_NODE_REF, TAG_NAME))); + given(parametersMock.getSorting()).willReturn(new ArrayList<>()); + given(parametersMock.getInclude()).willReturn(new ArrayList<>()); + + //given(taggingServiceMock.getTags(eq(STORE_REF_WORKSPACE_SPACESSTORE), any(), any(), isNull(), isNull())).willReturn(List.of(new Pair<>(TAG_NODE_REF, null))); + given(taggingServiceMock.getTags(eq(STORE_REF_WORKSPACE_SPACESSTORE), any(), any(), isNull(), isNull())).willReturn(Map.of(TAG_NODE_REF, 0L)); + given(nodeServiceMock.getProperty(any(NodeRef.class), eq(ContentModel.PROP_NAME))).willReturn("tag-dummy-name"); final CollectionWithPagingInfo actualTags = objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock); - then(taggingServiceMock).should().getTags(eq(STORE_REF_WORKSPACE_SPACESSTORE), any(PagingRequest.class), isNull(), isNull()); + then(taggingServiceMock).should().getTags(eq(STORE_REF_WORKSPACE_SPACESSTORE), any(), any(), isNull(), isNull()); then(taggingServiceMock).shouldHaveNoMoreInteractions(); final List expectedTags = createTagsWithNodeRefs(List.of(TAG_NAME)); assertEquals(expectedTags, actualTags.getCollection()); @@ -139,14 +148,15 @@ public class TagsImplTest public void testGetTags_verifyIfCountIsZero() { given(parametersMock.getPaging()).willReturn(pagingMock); - given(taggingServiceMock.getTags(any(StoreRef.class), any(PagingRequest.class), any(), any())).willReturn(pagingResultsMock); - given(pagingResultsMock.getTotalResultCount()).willReturn(new Pair<>(Integer.MAX_VALUE, 0)); - given(pagingResultsMock.getPage()).willReturn(List.of(new Pair<>(TAG_NODE_REF, TAG_NAME))); + given(parametersMock.getSorting()).willReturn(new ArrayList<>()); + given(parametersMock.getInclude()).willReturn(List.of(PARAM_INCLUDE_COUNT)); + given(taggingServiceMock.getTags(any(StoreRef.class), any(), any(), any(), any())).willReturn(Map.of(TAG_NODE_REF, 0L)); + + given(nodeServiceMock.getProperty(any(NodeRef.class), eq(ContentModel.PROP_NAME))).willReturn("tag-dummy-name"); given(parametersMock.getInclude()).willReturn(List.of("count")); final CollectionWithPagingInfo actualTags = objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock); - then(taggingServiceMock).should().findTaggedNodesAndCountByTagName(STORE_REF_WORKSPACE_SPACESSTORE); final List expectedTags = createTagsWithNodeRefs(List.of(TAG_NAME)).stream() .peek(tag -> tag.setCount(0L)) .collect(toList()); @@ -159,36 +169,157 @@ public class TagsImplTest { NodeRef tagNodeA = new NodeRef("tag://A/"); NodeRef tagNodeB = new NodeRef("tag://B/"); - List> tagPairs = List.of(new Pair<>(tagNodeA, "taga"), new Pair<>(tagNodeB, "tagb")); + given(parametersMock.getSorting()).willReturn(Collections.emptyList()); given(parametersMock.getPaging()).willReturn(pagingMock); - given(taggingServiceMock.getTags(any(StoreRef.class), any(PagingRequest.class), any(), any())).willReturn(pagingResultsMock); - given(pagingResultsMock.getTotalResultCount()).willReturn(new Pair<>(Integer.MAX_VALUE, 0)); - given(pagingResultsMock.getPage()).willReturn(tagPairs); given(parametersMock.getInclude()).willReturn(List.of("count")); - // Only taga is included in the returned list since tagb is not in use. - given(taggingServiceMock.findTaggedNodesAndCountByTagName(STORE_REF_WORKSPACE_SPACESSTORE)).willReturn(List.of(new Pair<>("taga", 5))); + + final LinkedHashMap results = new LinkedHashMap<>(); + results.put(tagNodeA, 5L); + results.put(tagNodeB, 0L); + + given(taggingServiceMock.getTags(any(StoreRef.class), eq(List.of(PARAM_INCLUDE_COUNT)), isNull(), any(), any())).willReturn(results); + given(nodeServiceMock.getProperty(any(NodeRef.class), eq(ContentModel.PROP_NAME))).willReturn("taga", "tagb"); final CollectionWithPagingInfo actualTags = objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock); - then(taggingServiceMock).should().findTaggedNodesAndCountByTagName(STORE_REF_WORKSPACE_SPACESSTORE); + final List expectedTags = List.of(Tag.builder().tag("tagA").nodeRef(tagNodeA).count(5L).create(), + Tag.builder().tag("tagB").nodeRef(tagNodeB).count(0L).create()); + assertEquals(expectedTags, actualTags.getCollection()); + } + + @Test + public void testGetTags_orderByCountAscendingOrder() + { + NodeRef tagNodeA = new NodeRef("tag://A/"); + NodeRef tagNodeB = new NodeRef("tag://B/"); + NodeRef tagNodeC = new NodeRef("tag://C/"); + + given(parametersMock.getPaging()).willReturn(pagingMock); + given(parametersMock.getInclude()).willReturn(List.of("count")); + given(parametersMock.getSorting()).willReturn(List.of(new SortColumn("count", true))); + + final LinkedHashMap results = new LinkedHashMap<>(); + results.put(tagNodeB, 0L); + results.put(tagNodeC, 2L); + results.put(tagNodeA, 5L); + + given(taggingServiceMock.getTags(any(StoreRef.class), eq(List.of(PARAM_INCLUDE_COUNT)), eq(new Pair<>("count", true)), any(), any())).willReturn(results); + given(nodeServiceMock.getProperty(tagNodeA, ContentModel.PROP_NAME)).willReturn("taga"); + given(nodeServiceMock.getProperty(tagNodeB, ContentModel.PROP_NAME)).willReturn("tagb"); + given(nodeServiceMock.getProperty(tagNodeC, ContentModel.PROP_NAME)).willReturn("tagc"); + + //when + final CollectionWithPagingInfo actualTags = objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock); + + final List expectedTags = List.of(Tag.builder().tag("tagb").nodeRef(tagNodeB).count(0L).create(), + Tag.builder().tag("tagc").nodeRef(tagNodeC).count(2L).create(), + Tag.builder().tag("taga").nodeRef(tagNodeA).count(5L).create()); + assertEquals(expectedTags, actualTags.getCollection()); + } + + @Test + public void testGetTags_orderByCountDescendingOrder() + { + NodeRef tagNodeA = new NodeRef("tag://A/"); + NodeRef tagNodeB = new NodeRef("tag://B/"); + NodeRef tagNodeC = new NodeRef("tag://C/"); + + given(parametersMock.getPaging()).willReturn(pagingMock); + given(parametersMock.getInclude()).willReturn(List.of("count")); + given(parametersMock.getSorting()).willReturn(List.of(new SortColumn("count", false))); + + final LinkedHashMap results = new LinkedHashMap<>(); + results.put(tagNodeA, 5L); + results.put(tagNodeC, 2L); + results.put(tagNodeB, 0L); + + given(taggingServiceMock.getTags(any(StoreRef.class), eq(List.of(PARAM_INCLUDE_COUNT)), eq(new Pair<>("count", false)), any(), any())).willReturn(results); + given(nodeServiceMock.getProperty(tagNodeA, ContentModel.PROP_NAME)).willReturn("taga"); + given(nodeServiceMock.getProperty(tagNodeB, ContentModel.PROP_NAME)).willReturn("tagb"); + given(nodeServiceMock.getProperty(tagNodeC, ContentModel.PROP_NAME)).willReturn("tagc"); + + //when + final CollectionWithPagingInfo actualTags = objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock); + final List expectedTags = List.of(Tag.builder().tag("taga").nodeRef(tagNodeA).count(5L).create(), + Tag.builder().tag("tagc").nodeRef(tagNodeC).count(2L).create(), Tag.builder().tag("tagb").nodeRef(tagNodeB).count(0L).create()); assertEquals(expectedTags, actualTags.getCollection()); } + @Test + public void testGetTags_orderByTagAscendingOrder() + { + NodeRef tagApple = new NodeRef("tag://apple/"); + NodeRef tagBanana = new NodeRef("tag://banana/"); + NodeRef tagCoconut = new NodeRef("tag://coconut/"); + + given(parametersMock.getPaging()).willReturn(pagingMock); + given(parametersMock.getInclude()).willReturn(Collections.emptyList()); + given(parametersMock.getSorting()).willReturn(List.of(new SortColumn("tag", true))); + + final LinkedHashMap results = new LinkedHashMap<>(); + results.put(tagApple, 0L); + results.put(tagBanana, 0L); + results.put(tagCoconut, 0L); + + given(taggingServiceMock.getTags(any(StoreRef.class), any(), eq(new Pair<>("tag", true)), any(), any())).willReturn(results); + given(nodeServiceMock.getProperty(tagApple, ContentModel.PROP_NAME)).willReturn("apple"); + given(nodeServiceMock.getProperty(tagBanana, ContentModel.PROP_NAME)).willReturn("banana"); + given(nodeServiceMock.getProperty(tagCoconut, ContentModel.PROP_NAME)).willReturn("coconut"); + + //when + final CollectionWithPagingInfo actualTags = objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock); + + final List expectedTags = List.of(Tag.builder().tag("apple").nodeRef(tagApple).create(), + Tag.builder().tag("banana").nodeRef(tagBanana).create(), + Tag.builder().tag("coconut").nodeRef(tagCoconut).create()); + assertEquals(expectedTags, actualTags.getCollection()); + } + + @Test + public void testGetTags_orderByTagDescendingOrder() + { + NodeRef tagApple = new NodeRef("tag://apple/"); + NodeRef tagBanana = new NodeRef("tag://banana/"); + NodeRef tagCoconut = new NodeRef("tag://coconut/"); + + given(parametersMock.getPaging()).willReturn(pagingMock); + given(parametersMock.getInclude()).willReturn(Collections.emptyList()); + given(parametersMock.getSorting()).willReturn(List.of(new SortColumn("tag", false))); + + final LinkedHashMap results = new LinkedHashMap<>(); + results.put(tagCoconut, 0L); + results.put(tagBanana, 0L); + results.put(tagApple, 0L); + + given(taggingServiceMock.getTags(any(StoreRef.class), any(), eq(new Pair<>("tag", false)), any(), any())).willReturn(results); + given(nodeServiceMock.getProperty(tagApple, ContentModel.PROP_NAME)).willReturn("apple"); + given(nodeServiceMock.getProperty(tagBanana, ContentModel.PROP_NAME)).willReturn("banana"); + given(nodeServiceMock.getProperty(tagCoconut, ContentModel.PROP_NAME)).willReturn("coconut"); + + //when + final CollectionWithPagingInfo actualTags = objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock); + + final List expectedTags = List.of(Tag.builder().tag("coconut").nodeRef(tagCoconut).create(), + Tag.builder().tag("banana").nodeRef(tagBanana).create(), + Tag.builder().tag("apple").nodeRef(tagApple).create()); + assertEquals(expectedTags, actualTags.getCollection()); + } + @Test public void testGetTags_withEqualsClauseWhereQuery() { given(parametersMock.getPaging()).willReturn(pagingMock); given(parametersMock.getQuery()).willReturn(queryExtractor.getWhereClause("(tag=expectedName)")); - given(taggingServiceMock.getTags(any(StoreRef.class), any(PagingRequest.class), any(), any())).willReturn(pagingResultsMock); - given(pagingResultsMock.getTotalResultCount()).willReturn(new Pair<>(Integer.MAX_VALUE, 0)); + given(parametersMock.getSorting()).willReturn(new ArrayList<>()); + given(parametersMock.getInclude()).willReturn(new ArrayList<>()); //when final CollectionWithPagingInfo actualTags = objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock); - then(taggingServiceMock).should().getTags(eq(STORE_REF_WORKSPACE_SPACESSTORE), any(PagingRequest.class), eq(Set.of("expectedname")), isNull()); + then(taggingServiceMock).should().getTags(eq(STORE_REF_WORKSPACE_SPACESSTORE), eq(new ArrayList<>()), any(), eq(Set.of("expectedname")), isNull()); then(taggingServiceMock).shouldHaveNoMoreInteractions(); assertThat(actualTags).isNotNull(); } @@ -198,13 +329,13 @@ public class TagsImplTest { given(parametersMock.getPaging()).willReturn(pagingMock); given(parametersMock.getQuery()).willReturn(queryExtractor.getWhereClause("(tag IN (expectedName1, expectedName2))")); - given(taggingServiceMock.getTags(any(StoreRef.class), any(PagingRequest.class), any(), any())).willReturn(pagingResultsMock); - given(pagingResultsMock.getTotalResultCount()).willReturn(new Pair<>(Integer.MAX_VALUE, 0)); + given(parametersMock.getSorting()).willReturn(new ArrayList<>()); + given(parametersMock.getInclude()).willReturn(new ArrayList<>()); //when final CollectionWithPagingInfo actualTags = objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock); - then(taggingServiceMock).should().getTags(eq(STORE_REF_WORKSPACE_SPACESSTORE), any(PagingRequest.class), eq(Set.of("expectedname1", "expectedname2")), isNull()); + then(taggingServiceMock).should().getTags(eq(STORE_REF_WORKSPACE_SPACESSTORE),any(), any(), eq(Set.of("expectedname1", "expectedname2")), isNull()); then(taggingServiceMock).shouldHaveNoMoreInteractions(); assertThat(actualTags).isNotNull(); } @@ -214,13 +345,13 @@ public class TagsImplTest { given(parametersMock.getPaging()).willReturn(pagingMock); given(parametersMock.getQuery()).willReturn(queryExtractor.getWhereClause("(tag MATCHES ('expectedName*'))")); - given(taggingServiceMock.getTags(any(StoreRef.class), any(PagingRequest.class), any(), any())).willReturn(pagingResultsMock); - given(pagingResultsMock.getTotalResultCount()).willReturn(new Pair<>(Integer.MAX_VALUE, 0)); + given(parametersMock.getSorting()).willReturn(new ArrayList<>()); + given(parametersMock.getInclude()).willReturn(new ArrayList<>()); //when final CollectionWithPagingInfo actualTags = objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock); - then(taggingServiceMock).should().getTags(eq(STORE_REF_WORKSPACE_SPACESSTORE), any(PagingRequest.class), isNull(), eq(Set.of("expectedname*"))); + then(taggingServiceMock).should().getTags(eq(STORE_REF_WORKSPACE_SPACESSTORE), any(), any(), isNull(), eq(Set.of("expectedname*"))); then(taggingServiceMock).shouldHaveNoMoreInteractions(); assertThat(actualTags).isNotNull(); } @@ -230,6 +361,7 @@ public class TagsImplTest { given(parametersMock.getPaging()).willReturn(pagingMock); given(parametersMock.getQuery()).willReturn(queryExtractor.getWhereClause("(tag=expectedName AND tag IN (expectedName1, expectedName2))")); + given(parametersMock.getSorting()).willReturn(new ArrayList<>()); //when final Throwable actualException = catchThrowable(() -> objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock)); @@ -243,6 +375,7 @@ public class TagsImplTest { given(parametersMock.getPaging()).willReturn(pagingMock); given(parametersMock.getQuery()).willReturn(queryExtractor.getWhereClause("(tag BETWEEN ('expectedName', 'expectedName2'))")); + given(parametersMock.getSorting()).willReturn(new ArrayList<>()); //when final Throwable actualException = catchThrowable(() -> objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock)); @@ -256,6 +389,7 @@ public class TagsImplTest { given(parametersMock.getPaging()).willReturn(pagingMock); given(parametersMock.getQuery()).willReturn(queryExtractor.getWhereClause("(NOT tag=expectedName)")); + given(parametersMock.getSorting()).willReturn(new ArrayList<>()); //when final Throwable actualException = catchThrowable(() -> objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock)); diff --git a/repository/src/main/java/org/alfresco/repo/jscript/Classification.java b/repository/src/main/java/org/alfresco/repo/jscript/Classification.java index c2cc3f6c19..121cbf6f92 100644 --- a/repository/src/main/java/org/alfresco/repo/jscript/Classification.java +++ b/repository/src/main/java/org/alfresco/repo/jscript/Classification.java @@ -23,225 +23,225 @@ * along with Alfresco. If not, see . * #L% */ -package org.alfresco.repo.jscript; - -import java.util.Collection; -import java.util.List; - -import org.alfresco.model.ContentModel; -import org.alfresco.query.PagingRequest; -import org.alfresco.service.ServiceRegistry; -import org.alfresco.service.cmr.repository.ChildAssociationRef; -import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.repository.StoreRef; -import org.alfresco.service.cmr.search.CategoryService; -import org.alfresco.service.namespace.QName; -import org.alfresco.util.Pair; -import org.mozilla.javascript.Context; -import org.mozilla.javascript.Scriptable; - -/** - * Support class for finding categories, finding root nodes for categories and creating root categories. - * - * @author Andy Hind - */ -public final class Classification extends BaseScopableProcessorExtension -{ - private ServiceRegistry services; - - private StoreRef storeRef; - - /** - * Set the default store reference - * - * @param storeRef the default store reference - */ - public void setStoreUrl(String storeRef) - { - this.storeRef = new StoreRef(storeRef); - } - - /** - * Set the service registry - * - * @param services the service registry - */ - public void setServiceRegistry(ServiceRegistry services) - { - this.services = services; - } - - /** - * Find all the category nodes in a given classification. - * - * @param aspect String - * @return Scriptable - */ - public Scriptable getAllCategoryNodes(String aspect) - { - Object[] cats = buildCategoryNodes(services.getCategoryService().getCategories( - storeRef, createQName(aspect), CategoryService.Depth.ANY)); - return Context.getCurrentContext().newArray(getScope(), cats); - } - - /** - * Get all the aspects that define a classification. - * - * @return String[] - */ - public String[] getAllClassificationAspects() - { - Collection aspects = services.getCategoryService().getClassificationAspects(); - String[] answer = new String[aspects.size()]; - int i = 0; - for (QName qname : aspects) - { - answer[i++] = qname.toPrefixString(this.services.getNamespaceService()); - } - return answer; - } - - /** - * Create a root category in a classification. - * - * @param aspect String - * @param name String - */ - public CategoryNode createRootCategory(String aspect, String name) - { - NodeRef categoryNodeRef = services.getCategoryService().createRootCategory(storeRef, createQName(aspect), name); - CategoryNode categoryNode = new CategoryNode(categoryNodeRef, this.services, getScope()); - - return categoryNode; - } - - /** - * Get the category node from the category node reference. - * - * @param categoryRef category node reference - * @return {@link CategoryNode} category node - */ - public CategoryNode getCategory(String categoryRef) - { - CategoryNode result = null; - NodeRef categoryNodeRef = new NodeRef(categoryRef); - if (services.getNodeService().exists(categoryNodeRef) == true && - services.getDictionaryService().isSubClass(ContentModel.TYPE_CATEGORY, services.getNodeService().getType(categoryNodeRef)) == true) - { - result = new CategoryNode(categoryNodeRef, this.services, getScope()); - } - return result; - } - - /** - * Get the root categories in a classification. - * - * @param aspect String - * @return Scriptable - */ - public Scriptable getRootCategories(String aspect) - { - Object[] cats = buildCategoryNodes(services.getCategoryService().getRootCategories( - storeRef, createQName(aspect))); - return Context.getCurrentContext().newArray(getScope(), cats); - } - - /** - * Get ordered, filtered and paged root categories in a classification. - * - * @param aspect - * @param filter - * @param maxItems - * @param skipCount (offset) - * @return - */ - public Scriptable getRootCategories(String aspect, String filter, int maxItems, int skipCount) - { - PagingRequest pagingRequest = new PagingRequest(skipCount, maxItems); - List rootCategories = services.getCategoryService().getRootCategories(storeRef, createQName(aspect), pagingRequest, true, filter).getPage(); - Object[] cats = buildCategoryNodes(rootCategories); - return Context.getCurrentContext().newArray(getScope(), cats); - } - - /** - * Get the category usage count. - * - * @param aspect String - * @param maxCount int - * @return Scriptable - */ - public Scriptable getCategoryUsage(String aspect, int maxCount) - { - List> topCats = services.getCategoryService().getTopCategories(storeRef, createQName(aspect), maxCount); - Object[] tags = new Object[topCats.size()]; - int i = 0; - for (Pair topCat : topCats) - { - tags[i++] = new Tag(new CategoryNode(topCat.getFirst(), this.services, getScope()), topCat.getSecond()); - } - - return Context.getCurrentContext().newArray(getScope(), tags); - } - - /** - * Build category nodes. - * - * @param cars list of associations to category nodes - * @return {@link Object}[] array of category nodes - */ - private Object[] buildCategoryNodes(Collection cars) - { - Object[] categoryNodes = new Object[cars.size()]; - int i = 0; - for (ChildAssociationRef car : cars) - { - categoryNodes[i++] = new CategoryNode(car.getChildRef(), this.services, getScope()); - } - return categoryNodes; - } - - /** - * Create QName from string - * - * @param s QName string value - * @return {@link QName} qualified name object - */ - private QName createQName(String s) - { - QName qname; - if (s.indexOf(QName.NAMESPACE_BEGIN) != -1) - { - qname = QName.createQName(s); - } - else - { - qname = QName.createQName(s, this.services.getNamespaceService()); - } - return qname; - } - - /** - * Tag class returned from getCategoryUsage(). - */ - public final class Tag - { - private CategoryNode categoryNode; - private int frequency = 0; - - public Tag(CategoryNode categoryNode, int frequency) - { - this.categoryNode = categoryNode; - this.frequency = frequency; - } - - public CategoryNode getCategory() - { - return categoryNode; - } - - public int getFrequency() - { - return frequency; - } - } -} +package org.alfresco.repo.jscript; + +import java.util.Collection; +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.query.PagingRequest; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.CategoryService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.Pair; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.Scriptable; + +/** + * Support class for finding categories, finding root nodes for categories and creating root categories. + * + * @author Andy Hind + */ +public final class Classification extends BaseScopableProcessorExtension +{ + private ServiceRegistry services; + + private StoreRef storeRef; + + /** + * Set the default store reference + * + * @param storeRef the default store reference + */ + public void setStoreUrl(String storeRef) + { + this.storeRef = new StoreRef(storeRef); + } + + /** + * Set the service registry + * + * @param services the service registry + */ + public void setServiceRegistry(ServiceRegistry services) + { + this.services = services; + } + + /** + * Find all the category nodes in a given classification. + * + * @param aspect String + * @return Scriptable + */ + public Scriptable getAllCategoryNodes(String aspect) + { + Object[] cats = buildCategoryNodes(services.getCategoryService().getCategories( + storeRef, createQName(aspect), CategoryService.Depth.ANY)); + return Context.getCurrentContext().newArray(getScope(), cats); + } + + /** + * Get all the aspects that define a classification. + * + * @return String[] + */ + public String[] getAllClassificationAspects() + { + Collection aspects = services.getCategoryService().getClassificationAspects(); + String[] answer = new String[aspects.size()]; + int i = 0; + for (QName qname : aspects) + { + answer[i++] = qname.toPrefixString(this.services.getNamespaceService()); + } + return answer; + } + + /** + * Create a root category in a classification. + * + * @param aspect String + * @param name String + */ + public CategoryNode createRootCategory(String aspect, String name) + { + NodeRef categoryNodeRef = services.getCategoryService().createRootCategory(storeRef, createQName(aspect), name); + CategoryNode categoryNode = new CategoryNode(categoryNodeRef, this.services, getScope()); + + return categoryNode; + } + + /** + * Get the category node from the category node reference. + * + * @param categoryRef category node reference + * @return {@link CategoryNode} category node + */ + public CategoryNode getCategory(String categoryRef) + { + CategoryNode result = null; + NodeRef categoryNodeRef = new NodeRef(categoryRef); + if (services.getNodeService().exists(categoryNodeRef) == true && + services.getDictionaryService().isSubClass(ContentModel.TYPE_CATEGORY, services.getNodeService().getType(categoryNodeRef)) == true) + { + result = new CategoryNode(categoryNodeRef, this.services, getScope()); + } + return result; + } + + /** + * Get the root categories in a classification. + * + * @param aspect String + * @return Scriptable + */ + public Scriptable getRootCategories(String aspect) + { + Object[] cats = buildCategoryNodes(services.getCategoryService().getRootCategories( + storeRef, createQName(aspect))); + return Context.getCurrentContext().newArray(getScope(), cats); + } + + /** + * Get ordered, filtered and paged root categories in a classification. + * + * @param aspect + * @param filter + * @param maxItems + * @param skipCount (offset) + * @return + */ + public Scriptable getRootCategories(String aspect, String filter, int maxItems, int skipCount) + { + PagingRequest pagingRequest = new PagingRequest(skipCount, maxItems); + List rootCategories = services.getCategoryService().getRootCategories(storeRef, createQName(aspect), pagingRequest, true, filter).getPage(); + Object[] cats = buildCategoryNodes(rootCategories); + return Context.getCurrentContext().newArray(getScope(), cats); + } + + /** + * Get the category usage count. + * + * @param aspect String + * @param maxCount int + * @return Scriptable + */ + public Scriptable getCategoryUsage(String aspect, int maxCount) + { + List> topCats = services.getCategoryService().getTopCategories(storeRef, createQName(aspect), maxCount); + Object[] tags = new Object[topCats.size()]; + int i = 0; + for (Pair topCat : topCats) + { + tags[i++] = new Tag(new CategoryNode(topCat.getFirst(), this.services, getScope()), topCat.getSecond()); + } + + return Context.getCurrentContext().newArray(getScope(), tags); + } + + /** + * Build category nodes. + * + * @param cars list of associations to category nodes + * @return {@link Object}[] array of category nodes + */ + private Object[] buildCategoryNodes(Collection cars) + { + Object[] categoryNodes = new Object[cars.size()]; + int i = 0; + for (ChildAssociationRef car : cars) + { + categoryNodes[i++] = new CategoryNode(car.getChildRef(), this.services, getScope()); + } + return categoryNodes; + } + + /** + * Create QName from string + * + * @param s QName string value + * @return {@link QName} qualified name object + */ + private QName createQName(String s) + { + QName qname; + if (s.indexOf(QName.NAMESPACE_BEGIN) != -1) + { + qname = QName.createQName(s); + } + else + { + qname = QName.createQName(s, this.services.getNamespaceService()); + } + return qname; + } + + /** + * Tag class returned from getCategoryUsage(). + */ + public final class Tag + { + private CategoryNode categoryNode; + private int frequency = 0; + + public Tag(CategoryNode categoryNode, int frequency) + { + this.categoryNode = categoryNode; + this.frequency = frequency; + } + + public CategoryNode getCategory() + { + return categoryNode; + } + + public int getFrequency() + { + return frequency; + } + } +} diff --git a/repository/src/main/java/org/alfresco/repo/search/impl/AbstractCategoryServiceImpl.java b/repository/src/main/java/org/alfresco/repo/search/impl/AbstractCategoryServiceImpl.java index 760bbd2bb8..d0df945adf 100644 --- a/repository/src/main/java/org/alfresco/repo/search/impl/AbstractCategoryServiceImpl.java +++ b/repository/src/main/java/org/alfresco/repo/search/impl/AbstractCategoryServiceImpl.java @@ -414,34 +414,7 @@ public abstract class AbstractCategoryServiceImpl implements CategoryService int count = 0; boolean moreItems = false; - final Function> childNodesSupplier = (nodeRef) -> { - final Set childNodes = new HashSet<>(); - if (CollectionUtils.isEmpty(exactNamesFilter) && CollectionUtils.isEmpty(alikeNamesFilter)) - { - // lookup in DB without filtering - childNodes.addAll(nodeService.getChildAssocs(nodeRef, ContentModel.ASSOC_SUBCATEGORIES, RegexQNamePattern.MATCH_ALL)); - } - else - { - if (CollectionUtils.isNotEmpty(exactNamesFilter)) - { - // lookup in DB filtering by name - childNodes.addAll(nodeService.getChildrenByName(nodeRef, ContentModel.ASSOC_SUBCATEGORIES, exactNamesFilter)); - } - if (CollectionUtils.isNotEmpty(alikeNamesFilter)) - { - // lookup using search engin filtering by name - childNodes.addAll(getChildren(nodeRef, Mode.SUB_CATEGORIES, Depth.IMMEDIATE, sortByName, alikeNamesFilter, skipCount + maxItems + 1)); - } - } - - Stream childNodesStream = childNodes.stream(); - if (sortByName) - { - childNodesStream = childNodesStream.sorted(Comparator.comparing(tag -> tag.getQName().getLocalName())); - } - return childNodesStream.collect(Collectors.toList()); - }; + final Function> childNodesSupplier = getNodeRefCollectionFunction(sortByName, exactNamesFilter, alikeNamesFilter, skipCount, maxItems); OUTER_LOOP: for(NodeRef nodeRef : nodeRefs) { @@ -468,6 +441,55 @@ public abstract class AbstractCategoryServiceImpl implements CategoryService return new ListBackedPagingResults<>(associations, moreItems); } + public Collection getRootCategories(StoreRef storeRef, QName aspectName, Collection exactNamesFilter, Collection alikeNamesFilter) + { + final Set nodeRefs = getClassificationNodes(storeRef, aspectName); + final List associations = new LinkedList<>(); + + final Function> childNodesSupplier = getNodeRefCollectionFunction(false, exactNamesFilter, alikeNamesFilter, 0, 10000); + + for (NodeRef nodeRef : nodeRefs) + { + Collection children = childNodesSupplier.apply(nodeRef); + associations.addAll(children); + } + + return associations; + } + + private Function> getNodeRefCollectionFunction(boolean sortByName, Collection exactNamesFilter, Collection alikeNamesFilter, int skipCount, int maxItems) + { + final Function> childNodesSupplier = (nodeRef) -> { + final Set childNodes = new HashSet<>(); + if (CollectionUtils.isEmpty(exactNamesFilter) && CollectionUtils.isEmpty(alikeNamesFilter)) + { + // lookup in DB without filtering + childNodes.addAll(nodeService.getChildAssocs(nodeRef, ContentModel.ASSOC_SUBCATEGORIES, RegexQNamePattern.MATCH_ALL)); + } + else + { + if (CollectionUtils.isNotEmpty(exactNamesFilter)) + { + // lookup in DB filtering by name + childNodes.addAll(nodeService.getChildrenByName(nodeRef, ContentModel.ASSOC_SUBCATEGORIES, exactNamesFilter)); + } + if (CollectionUtils.isNotEmpty(alikeNamesFilter)) + { + // lookup using search engine filtering by name + childNodes.addAll(getChildren(nodeRef, Mode.SUB_CATEGORIES, Depth.IMMEDIATE, sortByName, alikeNamesFilter, skipCount + maxItems + 1)); + } + } + + Stream childNodesStream = childNodes.stream(); + if (sortByName) + { + childNodesStream = childNodesStream.sorted(Comparator.comparing(tag -> tag.getQName().getLocalName())); + } + return childNodesStream.collect(Collectors.toList()); + }; + return childNodesSupplier; + } + public Collection getRootCategories(StoreRef storeRef, QName aspectName) { return getRootCategories(storeRef, aspectName, null); diff --git a/repository/src/main/java/org/alfresco/repo/tagging/TaggingServiceImpl.java b/repository/src/main/java/org/alfresco/repo/tagging/TaggingServiceImpl.java index e06c1b7cd7..5ff3b6ac45 100644 --- a/repository/src/main/java/org/alfresco/repo/tagging/TaggingServiceImpl.java +++ b/repository/src/main/java/org/alfresco/repo/tagging/TaggingServiceImpl.java @@ -46,12 +46,16 @@ import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; +import java.util.TreeMap; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.alfresco.model.ContentModel; import org.alfresco.query.EmptyPagingResults; @@ -61,6 +65,7 @@ import org.alfresco.repo.audit.AuditComponent; import org.alfresco.repo.coci.CheckOutCheckInServicePolicies.OnCheckOut; import org.alfresco.repo.copy.CopyServicePolicies.BeforeCopyPolicy; import org.alfresco.repo.copy.CopyServicePolicies.OnCopyCompletePolicy; +import org.alfresco.repo.domain.query.QueryException; import org.alfresco.repo.event2.EventGenerator; import org.alfresco.repo.node.NodeServicePolicies.BeforeDeleteNodePolicy; import org.alfresco.repo.node.NodeServicePolicies.OnCreateNodePolicy; @@ -138,6 +143,9 @@ public class TaggingServiceImpl implements TaggingService, private static final String TAG_DETAILS_DELIMITER = "|"; /** Next tag delimiter */ private static final String NEXT_TAG_DELIMITER = "\n"; + /** Parameters Include count */ + private static final String PARAM_INCLUDE_COUNT = "count"; + private static Set FORBIDDEN_TAGS_SEQUENCES = new HashSet(Arrays.asList(new String[] {NEXT_TAG_DELIMITER, TAG_DETAILS_DELIMITER})); @@ -680,6 +688,21 @@ public class TaggingServiceImpl implements TaggingService, } } + public Map calculateCount(StoreRef storeRef) + { + List> tagsByCount = findTaggedNodesAndCountByTagName(storeRef); + Map tagsByCountMap = new HashMap<>(); + if (tagsByCount != null) + { + for (Pair tagByCountElem : tagsByCount) + { + tagsByCountMap.put(tagByCountElem.getFirst(), Long.valueOf(tagByCountElem.getSecond())); + } + } + return tagsByCountMap; + } + + /** * @see TaggingService#hasTag(NodeRef, String) */ @@ -956,7 +979,83 @@ public class TaggingServiceImpl implements TaggingService, exactNamesFilter, alikeNamesFilter); return mapPagingResult(rootCategories, - (childAssociation) -> new Pair<>(childAssociation.getChildRef(), childAssociation.getQName().getLocalName())); + (childAssociation) -> new Pair<>(childAssociation.getChildRef(), childAssociation.getQName().getLocalName())); + } + + public Map getTags(StoreRef storeRef, List parameterIncludes, Pair sorting, Collection exactNamesFilter, Collection alikeNamesFilter) + { + ParameterCheck.mandatory("storeRef", storeRef); + Collection rootCategories = categoryService.getRootCategories(storeRef, ContentModel.ASPECT_TAGGABLE, exactNamesFilter, alikeNamesFilter); + + Map tagsMap = new TreeMap<>(); + for (ChildAssociationRef childAssociation : rootCategories) + { + tagsMap.put(childAssociation.getQName().getLocalName(), 0L); + } + + Map tagsByCountMap = new HashMap<>(); + + if(parameterIncludes.contains(PARAM_INCLUDE_COUNT)) + { + tagsByCountMap = calculateCount(storeRef); + + for (Map.Entry entry : tagsMap.entrySet()) { + entry.setValue(Optional.ofNullable(tagsByCountMap.get(entry.getKey())).orElse(0L)); + } + } + + //check if we should sort results. Can only sort by one parameter, default order is ascending + if (sorting != null) + { + if (sorting.getFirst().equals("tag")) + { + if (!sorting.getSecond()) + { + Stream> sortedTags = + tagsMap.entrySet().stream() + .sorted(Collections.reverseOrder(Map.Entry.comparingByKey())); + tagsMap = sortedTags.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); + } + else + { + Stream> sortedTags = + tagsMap.entrySet().stream() + .sorted(Map.Entry.comparingByKey()); + tagsMap = sortedTags.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); + } + } + else if (sorting.getFirst().equals(PARAM_INCLUDE_COUNT)) + { + if (tagsByCountMap.isEmpty()) + { + throw new QueryException("Tag count should be included when ordering by count"); + } + + if (!sorting.getSecond()) + { + Stream> sortedTags = + tagsMap.entrySet().stream() + .sorted(Collections.reverseOrder(Map.Entry.comparingByValue())); + tagsMap = sortedTags.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); + } + else + { + Stream> sortedTags = + tagsMap.entrySet().stream() + .sorted(Map.Entry.comparingByValue()); + tagsMap = sortedTags.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); + } + } + } + + Map tagNodeRefMap = new LinkedHashMap<>(); + + for (Map.Entry entry : tagsMap.entrySet()) + { + tagNodeRefMap.put(getTagNodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, entry.getKey()), entry.getValue()); + } + + return tagNodeRefMap; } /** diff --git a/repository/src/main/java/org/alfresco/service/cmr/search/CategoryService.java b/repository/src/main/java/org/alfresco/service/cmr/search/CategoryService.java index 355d53cbf9..56eddcbfd2 100644 --- a/repository/src/main/java/org/alfresco/service/cmr/search/CategoryService.java +++ b/repository/src/main/java/org/alfresco/service/cmr/search/CategoryService.java @@ -26,6 +26,7 @@ package org.alfresco.service.cmr.search; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Optional; @@ -150,11 +151,21 @@ public interface CategoryService */ @Auditable(parameters = {"storeRef", "aspectName", "pagingRequest", "sortByName", "exactNamesFilter", "alikeNamesFilter"}) default PagingResults getRootCategories(StoreRef storeRef, QName aspectName, PagingRequest pagingRequest, boolean sortByName, - Collection exactNamesFilter, Collection alikeNamesFilter) + Collection exactNamesFilter, Collection alikeNamesFilter) { return new EmptyPagingResults<>(); } + /** + * Get a collection of the root categories for an aspect/classification supporting multiple name filters. + */ + @Auditable(parameters = {"storeRef", "aspectName", "exactNamesFilter", "alikeNamesFilter"}) + default Collection getRootCategories(StoreRef storeRef, QName aspectName, Collection exactNamesFilter, Collection alikeNamesFilter) + { + return Collections.emptyList(); + } + + /** * Get the root categories for an aspect/classification with names that start with filter * diff --git a/repository/src/main/java/org/alfresco/service/cmr/tagging/TaggingService.java b/repository/src/main/java/org/alfresco/service/cmr/tagging/TaggingService.java index 0c48c09817..d1cc890bc7 100644 --- a/repository/src/main/java/org/alfresco/service/cmr/tagging/TaggingService.java +++ b/repository/src/main/java/org/alfresco/service/cmr/tagging/TaggingService.java @@ -28,6 +28,7 @@ package org.alfresco.service.cmr.tagging; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; import org.alfresco.api.AlfrescoPublicApi; import org.alfresco.query.EmptyPagingResults; @@ -94,6 +95,18 @@ public interface TaggingService { return new EmptyPagingResults<>(); } + + /** + * Get a map of tag NodeRefs and their respective usage count filtered by name and sorted by tag name or count + * + * @param storeRef + * @param parameterIncludes + * @param sorting + * @param exactNamesFilter + * @param alikeNamesFilter + * @return + */ + Map getTags(StoreRef storeRef, ListparameterIncludes, Pair sorting, Collection exactNamesFilter, Collection alikeNamesFilter); /** * Get all the tags currently available that match the provided filter. @@ -327,8 +340,7 @@ public interface TaggingService */ @NotAuditable Pair, Integer> getPagedTags(StoreRef storeRef, String filter, int fromTag, int pageSize); - - + /** * Get tagged nodes and count of nodes group by tag name * @@ -362,6 +374,13 @@ public interface TaggingService { return Collections.emptyList(); } + + /** + * + * @param storeRef + * @return a map with each tag name and its usage count + */ + Map calculateCount(StoreRef storeRef); }