From b701f9a994f78e08cbf99a8b9d8158c730afd128 Mon Sep 17 00:00:00 2001 From: Maciej Pichura <41297682+mpichura@users.noreply.github.com> Date: Thu, 8 Dec 2022 10:43:22 +0100 Subject: [PATCH] ACS-4032 Create category (POST) (#1606) * ACS-4028 Get category by id (#1591) * ACS-4028: Get category by id endpoint. * ACS-4028: Get category by id endpoint. * ACS-4028: Get category by id endpoint - integration TAS tests. * ACS-4028: Get category by id endpoint - refactoring. * ACS-4028: Adding test to test suite. * ACS-4028: Fixes after code review. * ACS-4032: Initial code for POST category endpoint. * ACS-4032: Full implementation for POST category endpoint + tests. * ACS-4032: Some fixes and refactors after code review. --- .../rest/model/RestCategoryModel.java | 34 ++- .../model/RestCategoryModelsCollection.java | 35 +++ .../alfresco/rest/requests/Categories.java | 30 ++- .../categories/CreateCategoriesTests.java | 206 +++++++++++++++++ .../rest/categories/GetCategoriesTests.java | 14 ++ .../org/alfresco/rest/api/Categories.java | 4 + .../api/categories/SubcategoriesRelation.java | 59 +++++ .../rest/api/impl/CategoriesImpl.java | 65 +++++- .../org/alfresco/rest/api/model/Category.java | 66 ++++++ .../alfresco/public-rest-context.xml | 8 +- .../rest/api/impl/CategoriesImplTest.java | 209 +++++++++++++++++- .../public-services-security-context.xml | 1 + 12 files changed, 705 insertions(+), 26 deletions(-) create mode 100644 packaging/tests/tas-restapi/src/main/java/org/alfresco/rest/model/RestCategoryModelsCollection.java create mode 100644 packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/categories/CreateCategoriesTests.java create mode 100644 remote-api/src/main/java/org/alfresco/rest/api/categories/SubcategoriesRelation.java diff --git a/packaging/tests/tas-restapi/src/main/java/org/alfresco/rest/model/RestCategoryModel.java b/packaging/tests/tas-restapi/src/main/java/org/alfresco/rest/model/RestCategoryModel.java index 805b20cf4f..5c94132075 100644 --- a/packaging/tests/tas-restapi/src/main/java/org/alfresco/rest/model/RestCategoryModel.java +++ b/packaging/tests/tas-restapi/src/main/java/org/alfresco/rest/model/RestCategoryModel.java @@ -1,5 +1,7 @@ package org.alfresco.rest.model; +import java.util.Objects; + import com.fasterxml.jackson.annotation.JsonProperty; import org.alfresco.rest.core.IRestModel; import org.alfresco.utility.model.TestModel; @@ -47,8 +49,7 @@ This must be unique within the parent category. private boolean hasChildren; /** The number of nodes that are assigned to this category. - */ - + */ private long count; public String getId() @@ -99,6 +100,33 @@ This must be unique within the parent category. public void setCount(long count) { this.count = count; - } + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RestCategoryModel that = (RestCategoryModel) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() + { + return Objects.hash(id); + } + + @Override + public String toString() + { + return "RestCategoryModel{" + + "id='" + id + '\'' + + ", name='" + name + '\'' + + ", parentId='" + parentId + '\'' + + ", hasChildren=" + hasChildren + + ", count=" + count + + '}'; + } } diff --git a/packaging/tests/tas-restapi/src/main/java/org/alfresco/rest/model/RestCategoryModelsCollection.java b/packaging/tests/tas-restapi/src/main/java/org/alfresco/rest/model/RestCategoryModelsCollection.java new file mode 100644 index 0000000000..7d328a4a2a --- /dev/null +++ b/packaging/tests/tas-restapi/src/main/java/org/alfresco/rest/model/RestCategoryModelsCollection.java @@ -0,0 +1,35 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 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 . + * #L% + */ + +package org.alfresco.rest.model; + +import org.alfresco.rest.core.RestModels; + +public class RestCategoryModelsCollection extends RestModels +{ + +} + diff --git a/packaging/tests/tas-restapi/src/main/java/org/alfresco/rest/requests/Categories.java b/packaging/tests/tas-restapi/src/main/java/org/alfresco/rest/requests/Categories.java index 87f5ba0da5..460ae87a3f 100644 --- a/packaging/tests/tas-restapi/src/main/java/org/alfresco/rest/requests/Categories.java +++ b/packaging/tests/tas-restapi/src/main/java/org/alfresco/rest/requests/Categories.java @@ -25,9 +25,15 @@ */ package org.alfresco.rest.requests; +import static org.alfresco.rest.core.JsonBodyGenerator.arrayToJson; + +import java.util.List; + import org.alfresco.rest.core.RestRequest; import org.alfresco.rest.core.RestWrapper; import org.alfresco.rest.model.RestCategoryModel; +import org.alfresco.rest.model.RestCategoryModelsCollection; +import org.alfresco.rest.model.RestRuleModelsCollection; import org.springframework.http.HttpMethod; public class Categories extends ModelRequest @@ -43,7 +49,7 @@ public class Categories extends ModelRequest /** * Retrieves a category with ID using GET call on using GET call on "/tags/{tagId}" * - * @return + * @return RestCategoryModel */ public RestCategoryModel getCategory() { @@ -52,4 +58,26 @@ public class Categories extends ModelRequest return restWrapper.processModel(RestCategoryModel.class, request); } + /** + * Create several categories in one request. + * + * @param restCategoryModels The list of categories to create. + * @return The list of created categories with additional data populated by the repository. + */ + public RestCategoryModelsCollection createCategoriesList(List restCategoryModels) { + RestRequest request = RestRequest.requestWithBody(HttpMethod.POST, arrayToJson(restCategoryModels), "categories/{categoryId}/subcategories", category.getId()); + return restWrapper.processModels(RestCategoryModelsCollection.class, request); + } + + /** + * Create single category. + * + * @param restCategoryModel The categories to create. + * @return Created category with additional data populated by the repository. + */ + public RestCategoryModel createSingleCategory(RestCategoryModel restCategoryModel) { + RestRequest request = RestRequest.requestWithBody(HttpMethod.POST, restCategoryModel.toJson(), "categories/{categoryId}/subcategories", category.getId()); + return restWrapper.processModel(RestCategoryModel.class, request); + } + } diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/categories/CreateCategoriesTests.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/categories/CreateCategoriesTests.java new file mode 100644 index 0000000000..edfc95d36f --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/categories/CreateCategoriesTests.java @@ -0,0 +1,206 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 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 . + * #L% + */ + +package org.alfresco.rest.categories; + +import static org.alfresco.utility.report.log.Step.STEP; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.CREATED; +import static org.springframework.http.HttpStatus.FORBIDDEN; +import static org.springframework.http.HttpStatus.NOT_FOUND; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.alfresco.rest.RestTest; +import org.alfresco.rest.model.RestCategoryModel; +import org.alfresco.rest.model.RestCategoryModelsCollection; +import org.alfresco.utility.data.RandomData; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.model.UserModel; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +public class CreateCategoriesTests extends RestTest +{ + private static final String FIELD_NAME = "name"; + private static final String FIELD_PARENT_ID = "parentId"; + private static final String FIELD_HAS_CHILDREN = "hasChildren"; + private static final String FIELD_ID = "id"; + private UserModel user; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() throws Exception + { + STEP("Create a user"); + user = dataUser.createRandomTestUser(); + } + + /** + * Check we can create a category as direct child of root category + */ + @Test(groups = {TestGroup.REST_API}) + public void testCreateCategoryUnderRoot() + { + STEP("Create a category under root category (as admin)"); + final RestCategoryModel rootCategory = new RestCategoryModel(); + rootCategory.setId("-root-"); + final RestCategoryModel aCategory = new RestCategoryModel(); + aCategory.setName(RandomData.getRandomName("Category")); + final RestCategoryModel createdCategory = restClient.authenticateUser(dataUser.getAdminUser()) + .withCoreAPI() + .usingCategory(rootCategory) + .createSingleCategory(aCategory); + restClient.assertStatusCodeIs(CREATED); + + createdCategory.assertThat().field(FIELD_NAME).is(aCategory.getName()); + createdCategory.assertThat().field(FIELD_PARENT_ID).is(rootCategory.getId()); + createdCategory.assertThat().field(FIELD_HAS_CHILDREN).is(false); + } + + /** + * Check we can create several categories as children of a created category + */ + @Test(groups = {TestGroup.REST_API}) + public void testCreateSeveralSubCategories() + { + STEP("Create a category under root category (as admin)"); + final RestCategoryModel rootCategory = new RestCategoryModel(); + rootCategory.setId("-root-"); + final RestCategoryModel aCategory = new RestCategoryModel(); + aCategory.setName(RandomData.getRandomName("Category")); + final RestCategoryModel createdCategory = restClient.authenticateUser(dataUser.getAdminUser()) + .withCoreAPI() + .usingCategory(rootCategory) + .createSingleCategory(aCategory); + restClient.assertStatusCodeIs(CREATED); + + createdCategory.assertThat().field(FIELD_NAME).is(aCategory.getName()) + .assertThat().field(FIELD_PARENT_ID).is(rootCategory.getId()) + .assertThat().field(FIELD_HAS_CHILDREN).is(false) + .assertThat().field(FIELD_ID).isNotEmpty(); + + STEP("Create two categories under the previously created (as admin)"); + final int categoriesNumber = 2; + final List categoriesToCreate = getCategoriesToCreate(categoriesNumber); + final RestCategoryModelsCollection createdSubCategories = restClient.authenticateUser(dataUser.getAdminUser()) + .withCoreAPI() + .usingCategory(createdCategory) + .createCategoriesList(categoriesToCreate); + restClient.assertStatusCodeIs(CREATED); + + createdSubCategories.assertThat() + .entriesListCountIs(categoriesToCreate.size()); + IntStream.range(0, categoriesNumber) + .forEach(i -> createdSubCategories.getEntries().get(i).onModel() + .assertThat().field(FIELD_NAME).is(categoriesToCreate.get(i).getName()) + .assertThat().field(FIELD_PARENT_ID).is(createdCategory.getId()) + .assertThat().field(FIELD_HAS_CHILDREN).is(false) + .assertThat().field(FIELD_ID).isNotEmpty() + ); + + STEP("Get the parent category and check if it now has children (as regular user)"); + final RestCategoryModel parentCategoryFromGet = restClient.authenticateUser(user) + .withCoreAPI() + .usingCategory(createdCategory) + .getCategory(); + + parentCategoryFromGet.assertThat().field(FIELD_HAS_CHILDREN).is(true); + } + + /** + * Check we cannot create a category as direct child of root category as non-admin user + */ + @Test(groups = {TestGroup.REST_API}) + public void testCreateCategoryUnderRootAsRegularUser_andFail() + { + STEP("Create a category under root category (as user)"); + final RestCategoryModel rootCategory = new RestCategoryModel(); + rootCategory.setId("-root-"); + final RestCategoryModel aCategory = new RestCategoryModel(); + aCategory.setName(RandomData.getRandomName("Category")); + restClient.authenticateUser(user) + .withCoreAPI() + .usingCategory(rootCategory) + .createSingleCategory(aCategory); + restClient.assertStatusCodeIs(FORBIDDEN).assertLastError().containsSummary("Current user does not have permission to create a category"); + } + + /** + * Check we cannot create a category under non existing parent node + */ + @Test(groups = {TestGroup.REST_API}) + public void testCreateCategoryUnderNonExistingParent_andFail() + { + STEP("Create a category under non existing category node (as admin)"); + final RestCategoryModel rootCategory = new RestCategoryModel(); + final String id = "non-existing-node-id"; + rootCategory.setId(id); + final RestCategoryModel aCategory = new RestCategoryModel(); + aCategory.setName(RandomData.getRandomName("Category")); + restClient.authenticateUser(dataUser.getAdminUser()) + .withCoreAPI() + .usingCategory(rootCategory) + .createSingleCategory(aCategory); + restClient.assertStatusCodeIs(NOT_FOUND).assertLastError().containsSummary("The entity with id: " + id + " was not found"); + } + + /** + * Check we cannot create a category under a node which is not a category + */ + @Test(groups = {TestGroup.REST_API}) + public void testCreateCategoryUnderFolderNode_andFail() + { + STEP("Create a site and a folder inside it"); + final SiteModel site = dataSite.usingUser(user).createPublicRandomSite(); + final FolderModel folder = dataContent.usingUser(user).usingSite(site).createFolder(); + + STEP("Create a category under folder node (as admin)"); + final RestCategoryModel rootCategory = new RestCategoryModel(); + rootCategory.setId(folder.getNodeRef()); + final RestCategoryModel aCategory = new RestCategoryModel(); + aCategory.setName(RandomData.getRandomName("Category")); + restClient.authenticateUser(dataUser.getAdminUser()) + .withCoreAPI() + .usingCategory(rootCategory) + .createSingleCategory(aCategory); + restClient.assertStatusCodeIs(BAD_REQUEST).assertLastError().containsSummary("Node id does not refer to a valid category"); + } + + private List getCategoriesToCreate(final int count) + { + return IntStream.range(0, count) + .mapToObj(i -> { + final RestCategoryModel aSubCategory = new RestCategoryModel(); + aSubCategory.setName((RandomData.getRandomName("SubCategory"))); + return aSubCategory; + }) + .collect(Collectors.toList()); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/categories/GetCategoriesTests.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/categories/GetCategoriesTests.java index d7ca0a70f3..d02312442b 100644 --- a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/categories/GetCategoriesTests.java +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/categories/GetCategoriesTests.java @@ -29,6 +29,7 @@ package org.alfresco.rest.categories; import static org.alfresco.utility.report.log.Step.STEP; import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.NOT_FOUND; +import static org.springframework.http.HttpStatus.OK; import org.alfresco.rest.RestTest; import org.alfresco.rest.model.RestCategoryModel; @@ -50,6 +51,19 @@ public class GetCategoriesTests extends RestTest user = dataUser.createRandomTestUser(); } + /** + * Check we can get a category which we just created in as direct child of root category + */ + @Test(groups = {TestGroup.REST_API}) + public void testGetCategoryById() + { + STEP("Get category with -root- as id (which does not exist)"); + final RestCategoryModel rootCategory = new RestCategoryModel(); + rootCategory.setId("-root-"); + restClient.authenticateUser(user).withCoreAPI().usingCategory(rootCategory).getCategory(); + restClient.assertStatusCodeIs(NOT_FOUND); + } + /** * Check we get an error when passing -root- as category id */ diff --git a/remote-api/src/main/java/org/alfresco/rest/api/Categories.java b/remote-api/src/main/java/org/alfresco/rest/api/Categories.java index f138450851..6c18d06fd2 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/Categories.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/Categories.java @@ -26,6 +26,8 @@ package org.alfresco.rest.api; +import java.util.List; + import org.alfresco.rest.api.model.Category; import org.alfresco.rest.framework.resource.parameters.Parameters; import org.alfresco.service.Experimental; @@ -35,4 +37,6 @@ import org.alfresco.service.cmr.repository.NodeRef; public interface Categories { Category getCategoryById(String id, Parameters params); + + List createSubcategories(String parentCategoryId, List categories, Parameters parameters); } diff --git a/remote-api/src/main/java/org/alfresco/rest/api/categories/SubcategoriesRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/categories/SubcategoriesRelation.java new file mode 100644 index 0000000000..1516ca44b8 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/categories/SubcategoriesRelation.java @@ -0,0 +1,59 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 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 . + * #L% + */ + +package org.alfresco.rest.api.categories; + +import java.util.List; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.rest.api.Categories; +import org.alfresco.rest.api.model.Category; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.Parameters; + +@RelationshipResource(name = "subcategories", entityResource = CategoriesEntityResource.class, title = "Subcategories") +public class SubcategoriesRelation implements RelationshipResourceAction.Create +{ + + private final Categories categories; + + public SubcategoriesRelation(Categories categories) + { + this.categories = categories; + } + + @WebApiDescription(title = "Create a category", + description = "Creates one or more categories under a parent category", + successStatus = HttpServletResponse.SC_CREATED) + @Override + public List create(String parentCategoryId, List categoryList, Parameters parameters) + { + return categories.createSubcategories(parentCategoryId, categoryList, parameters); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/CategoriesImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/CategoriesImpl.java index a8ae22f76a..28f452ea31 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/impl/CategoriesImpl.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/CategoriesImpl.java @@ -29,18 +29,24 @@ package org.alfresco.rest.api.impl; import static org.alfresco.rest.api.Nodes.PATH_ROOT; import java.util.List; +import java.util.stream.Collectors; import org.alfresco.model.ContentModel; import org.alfresco.rest.api.Categories; import org.alfresco.rest.api.Nodes; import org.alfresco.rest.api.model.Category; import org.alfresco.rest.api.model.Node; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; import org.alfresco.rest.framework.resource.parameters.Parameters; 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; +import org.alfresco.service.cmr.search.CategoryService; +import org.alfresco.service.cmr.security.AuthorityService; import org.alfresco.service.namespace.RegexQNamePattern; import org.apache.commons.collections.CollectionUtils; @@ -48,12 +54,17 @@ import org.apache.commons.collections.CollectionUtils; public class CategoriesImpl implements Categories { static final String NOT_A_VALID_CATEGORY = "Node id does not refer to a valid category"; + static final String NO_PERMISSION_TO_CREATE_A_CATEGORY = "Current user does not have permission to create a category"; + private final AuthorityService authorityService; + private final CategoryService categoryService; private final Nodes nodes; private final NodeService nodeService; - public CategoriesImpl(Nodes nodes, NodeService nodeService) + public CategoriesImpl(AuthorityService authorityService, CategoryService categoryService, Nodes nodes, NodeService nodeService) { + this.authorityService = authorityService; + this.categoryService = categoryService; this.nodes = nodes; this.nodeService = nodeService; } @@ -62,27 +73,59 @@ public class CategoriesImpl implements Categories public Category getCategoryById(final String id, final Parameters params) { final NodeRef nodeRef = nodes.validateNode(id); - final boolean isCategory = nodes.isSubClass(nodeRef, ContentModel.TYPE_CATEGORY, false); - if (!isCategory || isRootCategory(nodeRef)) + if (isNotACategory(nodeRef) || isRootCategory(nodeRef)) { throw new InvalidArgumentException(NOT_A_VALID_CATEGORY, new String[]{id}); } + + return mapToCategory(nodeRef); + } + + @Override + public List createSubcategories(String parentCategoryId, List categories, Parameters parameters) + { + if (!authorityService.hasAdminAuthority()) + { + throw new PermissionDeniedException(NO_PERMISSION_TO_CREATE_A_CATEGORY); + } + final NodeRef parentNodeRef = PATH_ROOT.equals(parentCategoryId) ? + categoryService.getRootCategoryNodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE) + .orElseThrow(() -> new EntityNotFoundException(parentCategoryId)) : + nodes.validateNode(parentCategoryId); + if (isNotACategory(parentNodeRef)) + { + throw new InvalidArgumentException(NOT_A_VALID_CATEGORY, new String[]{parentCategoryId}); + } + final List categoryNodeRefs = categories.stream() + .map(c -> categoryService.createCategory(parentNodeRef, c.getName())) + .collect(Collectors.toList()); + return categoryNodeRefs.stream() + .map(this::mapToCategory) + .collect(Collectors.toList()); + } + + private boolean isNotACategory(NodeRef nodeRef) + { + return !nodes.isSubClass(nodeRef, ContentModel.TYPE_CATEGORY, false); + } + + private Category mapToCategory(NodeRef nodeRef) + { final Node categoryNode = nodes.getNode(nodeRef.getId()); - final Category category = new Category(); - category.setId(nodeRef.getId()); - category.setName(categoryNode.getName()); - category.setParentId(getParentId(nodeRef)); final boolean hasChildren = CollectionUtils .isNotEmpty(nodeService.getChildAssocs(nodeRef, RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL, false)); - category.setHasChildren(hasChildren); - - return category; + return Category.builder() + .id(nodeRef.getId()) + .name(categoryNode.getName()) + .parentId(getParentId(nodeRef)) + .hasChildren(hasChildren) + .create(); } private boolean isRootCategory(final NodeRef nodeRef) { final List parentAssocs = nodeService.getParentAssocs(nodeRef); - return parentAssocs.stream().anyMatch(pa -> pa.getQName().equals(ContentModel.ASPECT_GEN_CLASSIFIABLE)); + return parentAssocs.stream().anyMatch(pa -> ContentModel.ASPECT_GEN_CLASSIFIABLE.equals(pa.getQName())); } private String getParentId(final NodeRef nodeRef) diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/Category.java b/remote-api/src/main/java/org/alfresco/rest/api/model/Category.java index e1baf15b08..d9b3b6d959 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/model/Category.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/Category.java @@ -26,6 +26,8 @@ package org.alfresco.rest.api.model; +import java.util.Objects; + public class Category { private String id; @@ -72,4 +74,68 @@ public class Category { this.hasChildren = hasChildren; } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Category category = (Category) o; + return hasChildren == category.hasChildren && Objects.equals(id, category.id) && name.equals(category.name) && + Objects.equals(parentId, category.parentId); + } + + @Override + public int hashCode() + { + return Objects.hash(id, name, parentId, hasChildren); + } + + public static Builder builder() + { + return new Builder(); + } + + public static class Builder + { + private String id; + private String name; + private String parentId; + private boolean hasChildren; + + public Builder id(String id) + { + this.id = id; + return this; + } + + public Builder name(String name) + { + this.name = name; + return this; + } + + public Builder parentId(String parentId) + { + this.parentId = parentId; + return this; + } + + public Builder hasChildren(boolean hasChildren) + { + this.hasChildren = hasChildren; + return this; + } + + public Category create() + { + final Category category = new Category(); + category.setId(id); + category.setName(name); + category.setParentId(parentId); + category.setHasChildren(hasChildren); + return category; + } + } + } diff --git a/remote-api/src/main/resources/alfresco/public-rest-context.xml b/remote-api/src/main/resources/alfresco/public-rest-context.xml index 6f702951f1..1899a35444 100644 --- a/remote-api/src/main/resources/alfresco/public-rest-context.xml +++ b/remote-api/src/main/resources/alfresco/public-rest-context.xml @@ -830,6 +830,8 @@ + + @@ -1099,10 +1101,14 @@ + + + + - + diff --git a/remote-api/src/test/java/org/alfresco/rest/api/impl/CategoriesImplTest.java b/remote-api/src/test/java/org/alfresco/rest/api/impl/CategoriesImplTest.java index ad01d33da8..7787e19812 100644 --- a/remote-api/src/test/java/org/alfresco/rest/api/impl/CategoriesImplTest.java +++ b/remote-api/src/test/java/org/alfresco/rest/api/impl/CategoriesImplTest.java @@ -26,15 +26,15 @@ package org.alfresco.rest.api.impl; +import static org.alfresco.rest.api.Nodes.PATH_ROOT; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import java.util.Collections; import java.util.List; +import java.util.Optional; import org.alfresco.model.ContentModel; import org.alfresco.rest.api.Nodes; @@ -42,11 +42,14 @@ import org.alfresco.rest.api.model.Category; import org.alfresco.rest.api.model.Node; import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; import org.alfresco.rest.framework.resource.parameters.Parameters; 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.CategoryService; +import org.alfresco.service.cmr.security.AuthorityService; import org.alfresco.service.namespace.RegexQNamePattern; import org.junit.Test; import org.junit.runner.RunWith; @@ -69,6 +72,10 @@ public class CategoriesImplTest @Mock private Parameters parametersMock; @Mock + private AuthorityService authorityServiceMock; + @Mock + private CategoryService categoryServiceMock; + @Mock private ChildAssociationRef dummyChildAssociationRefMock; @Mock private ChildAssociationRef categoryChildAssociationRefMock; @@ -93,6 +100,8 @@ public class CategoriesImplTest then(nodesMock).shouldHaveNoMoreInteractions(); then(nodeServiceMock).should().getParentAssocs(categoryRootNodeRef); then(nodeServiceMock).shouldHaveNoMoreInteractions(); + then(categoryServiceMock).shouldHaveNoInteractions(); + then(authorityServiceMock).shouldHaveNoMoreInteractions(); } @Test @@ -126,10 +135,16 @@ public class CategoriesImplTest then(nodeServiceMock).should().getChildAssocs(categoryNodeRef, RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL, false); then(nodeServiceMock).shouldHaveNoMoreInteractions(); - assertEquals(categoryNode.getName(), category.getName()); - assertEquals(CATEGORY_ID, category.getId()); - assertEquals(PARENT_ID, category.getParentId()); - assertTrue(category.getHasChildren()); + then(categoryServiceMock).shouldHaveNoInteractions(); + then(authorityServiceMock).shouldHaveNoInteractions(); + + final Category expectedCategory = Category.builder() + .id(CATEGORY_ID) + .name(categoryNode.getName()) + .hasChildren(true) + .parentId(PARENT_ID) + .create(); + assertEquals(expectedCategory, category); } @Test @@ -162,10 +177,16 @@ public class CategoriesImplTest then(nodeServiceMock).should().getChildAssocs(categoryNodeRef, RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL, false); then(nodeServiceMock).shouldHaveNoMoreInteractions(); - assertEquals(categoryNode.getName(), category.getName()); - assertEquals(CATEGORY_ID, category.getId()); - assertEquals(PARENT_ID, category.getParentId()); - assertFalse(category.getHasChildren()); + then(categoryServiceMock).shouldHaveNoInteractions(); + then(authorityServiceMock).shouldHaveNoInteractions(); + + final Category expectedCategory = Category.builder() + .id(CATEGORY_ID) + .name(categoryNode.getName()) + .hasChildren(false) + .parentId(PARENT_ID) + .create(); + assertEquals(expectedCategory, category); } @Test @@ -183,6 +204,8 @@ public class CategoriesImplTest then(nodesMock).shouldHaveNoMoreInteractions(); then(nodeServiceMock).shouldHaveNoInteractions(); + then(categoryServiceMock).shouldHaveNoInteractions(); + then(authorityServiceMock).shouldHaveNoInteractions(); } @Test @@ -197,5 +220,171 @@ public class CategoriesImplTest then(nodesMock).shouldHaveNoMoreInteractions(); then(nodeServiceMock).shouldHaveNoInteractions(); + then(categoryServiceMock).shouldHaveNoInteractions(); + then(authorityServiceMock).shouldHaveNoInteractions(); + } + + @Test + public void testCreateCategoryUnderRoot() + { + given(authorityServiceMock.hasAdminAuthority()).willReturn(true); + final NodeRef parentCategoryNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, PATH_ROOT); + given(categoryServiceMock.getRootCategoryNodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE)) + .willReturn(Optional.of(parentCategoryNodeRef)); + given(nodesMock.isSubClass(parentCategoryNodeRef, ContentModel.TYPE_CATEGORY, false)).willReturn(true); + final NodeRef categoryNodeRef = prepareCategoryNodeRef(); + given(categoryServiceMock.createCategory(parentCategoryNodeRef, CATEGORY_NAME)).willReturn(categoryNodeRef); + given(nodesMock.getNode(CATEGORY_ID)).willReturn(prepareCategoryNode()); + final ChildAssociationRef parentAssoc = new ChildAssociationRef(null, parentCategoryNodeRef, null, categoryNodeRef); + given(nodeServiceMock.getPrimaryParent(categoryNodeRef)).willReturn(parentAssoc); + given(nodeServiceMock.getParentAssocs(parentCategoryNodeRef)).willReturn(List.of(parentAssoc)); + given(nodeServiceMock.getChildAssocs(categoryNodeRef, RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL, false)) + .willReturn(Collections.emptyList()); + + //when + final List createdCategories = objectUnderTest.createSubcategories(PATH_ROOT, prepareCategories(), parametersMock); + + then(authorityServiceMock).should().hasAdminAuthority(); + then(authorityServiceMock).shouldHaveNoMoreInteractions(); + then(nodesMock).should().isSubClass(parentCategoryNodeRef, ContentModel.TYPE_CATEGORY, false); + then(nodesMock).should().getNode(CATEGORY_ID); + then(nodesMock).shouldHaveNoMoreInteractions(); + then(nodeServiceMock).should().getPrimaryParent(categoryNodeRef); + then(nodeServiceMock).should().getParentAssocs(parentCategoryNodeRef); + then(nodeServiceMock).should().getChildAssocs(categoryNodeRef, RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL, false); + then(nodeServiceMock).shouldHaveNoMoreInteractions(); + then(categoryServiceMock).should().getRootCategoryNodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE); + then(categoryServiceMock).should().createCategory(parentCategoryNodeRef, CATEGORY_NAME); + then(categoryServiceMock).shouldHaveNoMoreInteractions(); + + assertEquals(1, createdCategories.size()); + final Category expectedCategory = Category.builder() + .id(CATEGORY_ID) + .name(CATEGORY_NAME) + .hasChildren(false) + .parentId(PATH_ROOT) + .create(); + final Category createdCategory = createdCategories.iterator().next(); + assertEquals(expectedCategory, createdCategory); + } + + @Test + public void testCreateCategory() + { + given(authorityServiceMock.hasAdminAuthority()).willReturn(true); + final NodeRef parentCategoryNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, PARENT_ID); + given(nodesMock.validateNode(PARENT_ID)).willReturn(parentCategoryNodeRef); + given(nodesMock.isSubClass(parentCategoryNodeRef, ContentModel.TYPE_CATEGORY, false)).willReturn(true); + final NodeRef categoryNodeRef = prepareCategoryNodeRef(); + given(categoryServiceMock.createCategory(parentCategoryNodeRef, CATEGORY_NAME)).willReturn(categoryNodeRef); + given(nodesMock.getNode(CATEGORY_ID)).willReturn(prepareCategoryNode()); + final ChildAssociationRef parentAssoc = new ChildAssociationRef(null, parentCategoryNodeRef, null, categoryNodeRef); + given(nodeServiceMock.getPrimaryParent(categoryNodeRef)).willReturn(parentAssoc); + given(nodeServiceMock.getParentAssocs(parentCategoryNodeRef)).willReturn(List.of(parentAssoc)); + given(nodeServiceMock.getChildAssocs(categoryNodeRef, RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL, false)) + .willReturn(Collections.emptyList()); + + //when + final List createdCategories = objectUnderTest.createSubcategories(PARENT_ID, prepareCategories(), parametersMock); + + then(authorityServiceMock).should().hasAdminAuthority(); + then(authorityServiceMock).shouldHaveNoMoreInteractions(); + then(nodesMock).should().validateNode(PARENT_ID); + then(nodesMock).should().isSubClass(parentCategoryNodeRef, ContentModel.TYPE_CATEGORY, false); + then(nodesMock).should().getNode(CATEGORY_ID); + then(nodesMock).shouldHaveNoMoreInteractions(); + then(nodeServiceMock).should().getPrimaryParent(categoryNodeRef); + then(nodeServiceMock).should().getParentAssocs(parentCategoryNodeRef); + then(nodeServiceMock).should().getChildAssocs(categoryNodeRef, RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL, false); + then(nodeServiceMock).shouldHaveNoMoreInteractions(); + then(categoryServiceMock).should().createCategory(parentCategoryNodeRef, CATEGORY_NAME); + then(categoryServiceMock).shouldHaveNoMoreInteractions(); + + assertEquals(1, createdCategories.size()); + final Category expectedCategory = Category.builder() + .id(CATEGORY_ID) + .name(CATEGORY_NAME) + .hasChildren(false) + .parentId(PARENT_ID) + .create(); + final Category createdCategory = createdCategories.iterator().next(); + assertEquals(expectedCategory, createdCategory); + } + + @Test + public void testCreateCategories_noPermissions() + { + given(authorityServiceMock.hasAdminAuthority()).willReturn(false); + + //when + assertThrows(PermissionDeniedException.class, + () -> objectUnderTest.createSubcategories(PARENT_ID, prepareCategories(), parametersMock)); + + then(authorityServiceMock).should().hasAdminAuthority(); + then(authorityServiceMock).shouldHaveNoMoreInteractions(); + then(nodesMock).shouldHaveNoInteractions(); + then(nodeServiceMock).shouldHaveNoInteractions(); + then(categoryServiceMock).shouldHaveNoInteractions(); + } + + @Test + public void testCreateCategories_wrongParentNodeType() + { + given(authorityServiceMock.hasAdminAuthority()).willReturn(true); + final NodeRef parentCategoryNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, PARENT_ID); + given(nodesMock.validateNode(PARENT_ID)).willReturn(parentCategoryNodeRef); + given(nodesMock.isSubClass(parentCategoryNodeRef, ContentModel.TYPE_CATEGORY, false)).willReturn(false); + + //when + assertThrows(InvalidArgumentException.class, + () -> objectUnderTest.createSubcategories(PARENT_ID, prepareCategories(), parametersMock)); + + then(authorityServiceMock).should().hasAdminAuthority(); + then(authorityServiceMock).shouldHaveNoMoreInteractions(); + then(nodesMock).should().validateNode(PARENT_ID); + then(nodesMock).should().isSubClass(parentCategoryNodeRef, ContentModel.TYPE_CATEGORY, false); + then(nodesMock).shouldHaveNoMoreInteractions(); + then(nodeServiceMock).shouldHaveNoInteractions(); + then(categoryServiceMock).shouldHaveNoInteractions(); + } + + @Test + public void testCreateCategories_nonExistingParentNode() + { + given(authorityServiceMock.hasAdminAuthority()).willReturn(true); + given(nodesMock.validateNode(PARENT_ID)).willThrow(EntityNotFoundException.class); + + //when + assertThrows(EntityNotFoundException.class, + () -> objectUnderTest.createSubcategories(PARENT_ID, prepareCategories(), parametersMock)); + + then(authorityServiceMock).should().hasAdminAuthority(); + then(authorityServiceMock).shouldHaveNoMoreInteractions(); + then(nodesMock).should().validateNode(PARENT_ID); + then(nodesMock).shouldHaveNoMoreInteractions(); + then(nodeServiceMock).shouldHaveNoInteractions(); + then(categoryServiceMock).shouldHaveNoInteractions(); + } + + private Node prepareCategoryNode() + { + final Node categoryNode = new Node(); + categoryNode.setName(CATEGORY_NAME); + categoryNode.setNodeId(CATEGORY_ID); + final NodeRef parentNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, PARENT_ID); + categoryNode.setParentId(parentNodeRef); + return categoryNode; + } + + private NodeRef prepareCategoryNodeRef() + { + return new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, CATEGORY_ID); + } + + private List prepareCategories() + { + return List.of(Category.builder() + .name(CATEGORY_NAME) + .create()); } } diff --git a/repository/src/main/resources/alfresco/public-services-security-context.xml b/repository/src/main/resources/alfresco/public-services-security-context.xml index a1887929ff..b78cb659c0 100644 --- a/repository/src/main/resources/alfresco/public-services-security-context.xml +++ b/repository/src/main/resources/alfresco/public-services-security-context.xml @@ -573,6 +573,7 @@ org.alfresco.service.cmr.search.CategoryService.deleteClassification=ACL_ALLOW org.alfresco.service.cmr.search.CategoryService.deleteCategory=ACL_ALLOW org.alfresco.service.cmr.search.CategoryService.getTopCategories=ACL_ALLOW + org.alfresco.service.cmr.search.CategoryService.getRootCategoryNodeRef=ACL_ALLOW org.alfresco.service.cmr.search.CategoryService.*=ACL_DENY