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.
This commit is contained in:
Maciej Pichura
2022-12-08 10:43:22 +01:00
committed by GitHub
parent d04043b015
commit b701f9a994
12 changed files with 705 additions and 26 deletions

View File

@@ -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<Category> createSubcategories(String parentCategoryId, List<Category> categories, Parameters parameters);
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
* #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<Category>
{
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<Category> create(String parentCategoryId, List<Category> categoryList, Parameters parameters)
{
return categories.createSubcategories(parentCategoryId, categoryList, parameters);
}
}

View File

@@ -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<Category> createSubcategories(String parentCategoryId, List<Category> 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<NodeRef> 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<ChildAssociationRef> 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)

View File

@@ -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;
}
}
}

View File

@@ -830,6 +830,8 @@
</bean>
<bean id="categories" class="org.alfresco.rest.api.impl.CategoriesImpl">
<constructor-arg name="authorityService" ref="AuthorityService"/>
<constructor-arg name="categoryService" ref="CategoryService"/>
<constructor-arg name="nodes" ref="nodes"/>
<constructor-arg name="nodeService" ref="NodeService"/>
</bean>
@@ -1099,10 +1101,14 @@
<constructor-arg name="categories" ref="Categories"/>
</bean>
<bean class="org.alfresco.rest.api.categories.SubcategoriesRelation">
<constructor-arg name="categories" ref="Categories"/>
</bean>
<bean class="org.alfresco.rest.api.tags.TagsEntityResource">
<property name="tags" ref="Tags" />
</bean>
<bean class="org.alfresco.rest.api.people.PersonSitesRelation">
<property name="sites" ref="Sites" />
</bean>

View File

@@ -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<Category> 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<Category> 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<Category> prepareCategories()
{
return List.of(Category.builder()
.name(CATEGORY_NAME)
.create());
}
}