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