ACS-4033: List linked categories for a particular node (#1672)

* ACS-4033: List linked categories for a particular node - GET /nodes/{nodeId}/category-links
This commit is contained in:
krdabrowski
2023-01-13 18:10:46 +01:00
committed by GitHub
parent d3d1aaeba1
commit 0197b0e221
14 changed files with 605 additions and 65 deletions

View File

@@ -54,7 +54,17 @@ public interface Categories
void deleteCategoryById(String id, Parameters parameters);
/**
* Link node to categories. Node types allowed for categorization are specified within {@link org.alfresco.util.TypeConstraint}.
* Get categories linked from node. Read permission on node is required.
* Node type is restricted to specified vales from: {@link org.alfresco.util.TypeConstraint}.
*
* @param nodeId Node ID.
* @return Categories linked from node.
*/
List<Category> listCategoriesForNode(String nodeId);
/**
* Link node to categories. Change permission on node is required.
* Node types allowed for categorization are specified within {@link org.alfresco.util.TypeConstraint}.
*
* @param nodeId Node ID.
* @param categoryLinks Category IDs to which content should be linked to.

View File

@@ -35,10 +35,12 @@ import org.alfresco.rest.api.nodes.NodesEntityResource;
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.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.ListPage;
import org.alfresco.rest.framework.resource.parameters.Parameters;
@RelationshipResource(name = "category-links", entityResource = NodesEntityResource.class, title = "Category links")
public class NodesCategoryLinksRelation implements RelationshipResourceAction.Create<Category>
public class NodesCategoryLinksRelation implements RelationshipResourceAction.Read<Category>, RelationshipResourceAction.Create<Category>
{
private final Categories categories;
@@ -48,12 +50,26 @@ public class NodesCategoryLinksRelation implements RelationshipResourceAction.Cr
this.categories = categories;
}
/**
* GET /nodes/{nodeId}/category-links
*/
@WebApiDescription(
title = "Get categories linked to by node",
description = "Get categories linked to by node",
successStatus = HttpServletResponse.SC_OK
)
@Override
public CollectionWithPagingInfo<Category> readAll(String nodeId, Parameters parameters)
{
return ListPage.of(categories.listCategoriesForNode(nodeId), parameters.getPaging());
}
/**
* POST /nodes/{nodeId}/category-links
*/
@WebApiDescription(
title = "Link content node to categories",
description = "Creates a link between a content node and categories",
title = "Link node to categories",
description = "Creates a link between a node and categories",
successStatus = HttpServletResponse.SC_CREATED
)
@Override

View File

@@ -32,6 +32,7 @@ import static org.alfresco.service.cmr.security.PermissionService.CHANGE_PERMISS
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -71,6 +72,7 @@ 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_MANAGE_A_CATEGORY = "Current user does not have permission to manage a category";
static final String NO_PERMISSION_TO_READ_CONTENT = "Current user does not have read permission to content";
static final String NO_PERMISSION_TO_CHANGE_CONTENT = "Current user does not have change permission to content";
static final String NOT_NULL_OR_EMPTY = "Category name must not be null or empty";
static final String INVALID_NODE_TYPE = "Cannot categorize this node type";
@@ -158,6 +160,23 @@ public class CategoriesImpl implements Categories
nodeService.deleteNode(nodeRef);
}
@Override
public List<Category> listCategoriesForNode(final String nodeId)
{
final NodeRef contentNodeRef = nodes.validateNode(nodeId);
verifyReadPermission(contentNodeRef);
verifyNodeType(contentNodeRef);
final Serializable currentCategories = nodeService.getProperty(contentNodeRef, ContentModel.PROP_CATEGORIES);
if (currentCategories == null)
{
return Collections.emptyList();
}
final Collection<NodeRef> actualCategories = DefaultTypeConverter.INSTANCE.getCollection(NodeRef.class, currentCategories);
return actualCategories.stream().map(this::mapToCategory).collect(Collectors.toList());
}
@Override
public List<Category> linkNodeToCategories(final String nodeId, final List<Category> categoryLinks)
{
@@ -195,11 +214,19 @@ public class CategoriesImpl implements Categories
}
}
private void verifyReadPermission(final NodeRef nodeRef)
{
if (permissionService.hasReadPermission(nodeRef) != ALLOWED)
{
throw new PermissionDeniedException(NO_PERMISSION_TO_READ_CONTENT);
}
}
private void verifyChangePermission(final NodeRef nodeRef)
{
if (permissionService.hasPermission(nodeRef, CHANGE_PERMISSIONS) != ALLOWED)
{
throw new PermissionDeniedException(NO_PERMISSION_TO_READ_CONTENT);
throw new PermissionDeniedException(NO_PERMISSION_TO_CHANGE_CONTENT);
}
}

View File

@@ -51,7 +51,6 @@ public interface SerializablePagedCollection<T>
/**
* Indicates the total number of items available.
*
* Can be greater than the number of items returned in the list.
*
*/

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2022 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -27,13 +27,11 @@
package org.alfresco.rest.framework.resource.parameters;
import org.alfresco.rest.api.search.context.SearchContext;
import org.alfresco.service.Experimental;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Experimental
public class ArrayListPage<E> extends ArrayList<E> implements ListPage<E>
{
private final Paging paging;

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2022 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -28,7 +28,6 @@ package org.alfresco.rest.framework.resource.parameters;
import org.alfresco.query.PagingResults;
import org.alfresco.rest.framework.resource.SerializablePagedCollection;
import org.alfresco.service.Experimental;
import org.alfresco.util.Pair;
import java.util.Collection;
@@ -38,10 +37,8 @@ import java.util.List;
/**
* List page with paging information.
*
*
* @param <E> - list element type
*/
@Experimental
public interface ListPage<E> extends List<E>, PagingResults<E>, SerializablePagedCollection<E>
{

View File

@@ -0,0 +1,91 @@
/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* 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 static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import java.util.List;
import org.alfresco.rest.api.Categories;
import org.alfresco.rest.api.model.Category;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class NodesCategoryLinksRelationTest
{
private static final String CONTENT_ID = "content-node-id";
@Mock
private Categories categoriesMock;
@Mock
private Category categoryMock;
@Mock
private Parameters parametersMock;
@InjectMocks
private NodesCategoryLinksRelation objectUnderTest;
@Test
public void testReadAll()
{
given(categoriesMock.listCategoriesForNode(any())).willReturn(List.of(categoryMock));
// when
final CollectionWithPagingInfo<Category> actualCategoriesPage = objectUnderTest.readAll(CONTENT_ID, parametersMock);
then(categoriesMock).should().listCategoriesForNode(CONTENT_ID);
then(categoriesMock).shouldHaveNoMoreInteractions();
assertThat(actualCategoriesPage)
.isNotNull()
.extracting(CollectionWithPagingInfo::getCollection)
.isEqualTo(List.of(categoryMock));
}
@Test
public void testCreate()
{
given(categoriesMock.linkNodeToCategories(any(), any())).willReturn(List.of(categoryMock));
// when
final List<Category> actualCategories = objectUnderTest.create(CONTENT_ID, List.of(categoryMock), parametersMock);
then(categoriesMock).should().linkNodeToCategories(CONTENT_ID, List.of(categoryMock));
then(categoriesMock).shouldHaveNoMoreInteractions();
assertThat(actualCategories)
.isNotNull()
.isEqualTo(List.of(categoryMock));
}
}

View File

@@ -30,6 +30,7 @@ import static org.alfresco.rest.api.Nodes.PATH_ROOT;
import static org.alfresco.rest.api.impl.CategoriesImpl.INVALID_NODE_TYPE;
import static org.alfresco.rest.api.impl.CategoriesImpl.NOT_A_VALID_CATEGORY;
import static org.alfresco.rest.api.impl.CategoriesImpl.NOT_NULL_OR_EMPTY;
import static org.alfresco.rest.api.impl.CategoriesImpl.NO_PERMISSION_TO_CHANGE_CONTENT;
import static org.alfresco.rest.api.impl.CategoriesImpl.NO_PERMISSION_TO_READ_CONTENT;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -39,7 +40,6 @@ import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
@@ -54,6 +54,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.alfresco.model.ContentModel;
import org.alfresco.rest.api.Nodes;
@@ -127,6 +128,7 @@ public class CategoriesImplTest
given(nodesMock.validateNode(CONTENT_NODE_ID)).willReturn(CONTENT_NODE_REF);
given(nodesMock.isSubClass(any(), any(), anyBoolean())).willReturn(true);
given(typeConstraint.matches(any())).willReturn(true);
given(permissionServiceMock.hasReadPermission(any())).willReturn(AccessStatus.ALLOWED);
given(permissionServiceMock.hasPermission(any(), any())).willReturn(AccessStatus.ALLOWED);
}
@@ -944,7 +946,7 @@ public class CategoriesImplTest
then(nodeServiceMock).shouldHaveNoInteractions();
assertThat(actualException)
.isInstanceOf(PermissionDeniedException.class)
.hasMessageContaining(NO_PERMISSION_TO_READ_CONTENT);
.hasMessageContaining(NO_PERMISSION_TO_CHANGE_CONTENT);
}
@Test
@@ -998,6 +1000,94 @@ public class CategoriesImplTest
.hasMessageContaining(NOT_A_VALID_CATEGORY);
}
@Test
public void testListCategoriesForNode()
{
final NodeRef categoryParentNodeRef = createNodeRefWithId(PARENT_ID);
final ChildAssociationRef parentAssociation = createAssociationOf(categoryParentNodeRef, CATEGORY_NODE_REF);
given(nodeServiceMock.getProperty(any(), eq(ContentModel.PROP_CATEGORIES))).willReturn((Serializable) List.of(CATEGORY_NODE_REF));
given(nodesMock.getNode(any())).willReturn(prepareCategoryNode());
given(nodeServiceMock.getPrimaryParent(any())).willReturn(parentAssociation);
// when
final List<Category> actualCategories = objectUnderTest.listCategoriesForNode(CONTENT_NODE_ID);
then(nodesMock).should().validateNode(CONTENT_NODE_ID);
then(permissionServiceMock).should().hasReadPermission(CONTENT_NODE_REF);
then(permissionServiceMock).shouldHaveNoMoreInteractions();
then(typeConstraint).should().matches(CONTENT_NODE_REF);
then(typeConstraint).shouldHaveNoMoreInteractions();
then(nodesMock).should().getNode(CATEGORY_ID);
then(nodesMock).shouldHaveNoMoreInteractions();
then(nodeServiceMock).should().getProperty(CONTENT_NODE_REF, ContentModel.PROP_CATEGORIES);
then(nodeServiceMock).should().getChildAssocs(CATEGORY_NODE_REF, RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL, false);
then(nodeServiceMock).should().getPrimaryParent(CATEGORY_NODE_REF);
then(nodeServiceMock).should().getParentAssocs(categoryParentNodeRef);
then(nodeServiceMock).shouldHaveNoMoreInteractions();
final List<Category> expectedCategories = List.of(CATEGORY);
assertThat(actualCategories)
.isNotNull().usingRecursiveComparison()
.isEqualTo(expectedCategories);
}
@Test
public void testListCategoriesForNode_withInvalidNodeId()
{
given(nodesMock.validateNode(CONTENT_NODE_ID)).willThrow(EntityNotFoundException.class);
// when
final Throwable actualException = catchThrowable(() -> objectUnderTest.listCategoriesForNode(CONTENT_NODE_ID));
then(nodesMock).should().validateNode(CONTENT_NODE_ID);
then(nodeServiceMock).shouldHaveNoInteractions();
assertThat(actualException)
.isInstanceOf(EntityNotFoundException.class);
}
@Test
public void testListCategoriesForNode_withoutPermission()
{
given(permissionServiceMock.hasReadPermission(any())).willReturn(AccessStatus.DENIED);
// when
final Throwable actualException = catchThrowable(() -> objectUnderTest.listCategoriesForNode(CONTENT_NODE_ID));
then(nodesMock).should().validateNode(CONTENT_NODE_ID);
then(permissionServiceMock).should().hasReadPermission(CONTENT_NODE_REF);
then(nodeServiceMock).shouldHaveNoInteractions();
assertThat(actualException)
.isInstanceOf(PermissionDeniedException.class)
.hasMessageContaining(NO_PERMISSION_TO_READ_CONTENT);
}
@Test
public void testListCategoriesForNode_withInvalidNodeType()
{
given(typeConstraint.matches(any())).willReturn(false);
// when
final Throwable actualException = catchThrowable(() -> objectUnderTest.listCategoriesForNode(CONTENT_NODE_ID));
then(typeConstraint).should().matches(CONTENT_NODE_REF);
then(nodeServiceMock).shouldHaveNoInteractions();
assertThat(actualException)
.isInstanceOf(UnsupportedResourceOperationException.class)
.hasMessageContaining(INVALID_NODE_TYPE);
}
@Test
public void testListCategoriesForNode_withoutLinkedCategories()
{
Stream.of(null, Collections.emptyList()).forEach(nullOrEmptyList -> {
given(nodeServiceMock.getProperty(any(), eq(ContentModel.PROP_CATEGORIES))).willReturn((Serializable) nullOrEmptyList);
// when
final List<Category> actualCategories = objectUnderTest.listCategoriesForNode(CONTENT_NODE_ID);
assertThat(actualCategories).isNotNull().isEmpty();
});
}
private Node prepareCategoryNode(final String name, final String id, final NodeRef parentNodeRef)
{
final Node categoryNode = new Node();

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2022 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -28,7 +28,6 @@ package org.alfresco.rest.framework.resource.parameters;
import junit.framework.TestCase;
import org.alfresco.rest.framework.resource.SerializablePagedCollection;
import org.alfresco.service.Experimental;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
@@ -42,7 +41,6 @@ import java.util.Random;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
@Experimental
@RunWith(MockitoJUnitRunner.class)
public class ArrayListPageTest extends TestCase
{