Merge branch 'feature/RM-4376_relativePath' into 'master'

Feature/rm 4376 relative path

RM-4376 - The categories specified in the relativePath that do not exist are not created before the record folder is created.

Override createNode and create the RM path before calling the core method. after the RM path is created the core method is called with the last element of the created path

See merge request !647
This commit is contained in:
Tuna Aksoy
2016-11-21 21:00:39 +00:00
3 changed files with 453 additions and 0 deletions

View File

@@ -28,6 +28,7 @@
<property name="filePlanService" ref="FilePlanService"/> <property name="filePlanService" ref="FilePlanService"/>
<property name="recordsManagementServiceRegistry" ref="RecordsManagementServiceRegistry"/> <property name="recordsManagementServiceRegistry" ref="RecordsManagementServiceRegistry"/>
<property name="capabilityService" ref="CapabilityService"/> <property name="capabilityService" ref="CapabilityService"/>
<property name="fileFolderService" ref="FileFolderService"/>
</bean> </bean>
<bean id="rm.Nodes" class="org.springframework.aop.framework.ProxyFactoryBean"> <bean id="rm.Nodes" class="org.springframework.aop.framework.ProxyFactoryBean">

View File

@@ -30,9 +30,11 @@ package org.alfresco.rm.rest.api.impl;
import java.security.InvalidParameterException; import java.security.InvalidParameterException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.StringTokenizer;
import org.alfresco.model.ContentModel; import org.alfresco.model.ContentModel;
import org.alfresco.module.org_alfresco_module_rm.RecordsManagementServiceRegistry; import org.alfresco.module.org_alfresco_module_rm.RecordsManagementServiceRegistry;
@@ -41,16 +43,22 @@ import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedul
import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService;
import org.alfresco.module.org_alfresco_module_rm.fileplan.FilePlanService; import org.alfresco.module.org_alfresco_module_rm.fileplan.FilePlanService;
import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.rest.api.Nodes;
import org.alfresco.rest.api.impl.NodesImpl; import org.alfresco.rest.api.impl.NodesImpl;
import org.alfresco.rest.api.model.Node; import org.alfresco.rest.api.model.Node;
import org.alfresco.rest.api.model.UserInfo; import org.alfresco.rest.api.model.UserInfo;
import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.rm.rest.api.RMNodes; import org.alfresco.rm.rest.api.RMNodes;
import org.alfresco.rm.rest.api.model.FileplanComponentNode; import org.alfresco.rm.rest.api.model.FileplanComponentNode;
import org.alfresco.rm.rest.api.model.RecordCategoryNode; import org.alfresco.rm.rest.api.model.RecordCategoryNode;
import org.alfresco.rm.rest.api.model.RecordFolderNode; import org.alfresco.rm.rest.api.model.RecordFolderNode;
import org.alfresco.rm.rest.api.model.RecordNode; import org.alfresco.rm.rest.api.model.RecordNode;
import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.model.FileFolderService;
import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.QName;
@@ -79,6 +87,7 @@ public class RMNodesImpl extends NodesImpl implements RMNodes
private DictionaryService dictionaryService; private DictionaryService dictionaryService;
private DispositionService dispositionService; private DispositionService dispositionService;
private CapabilityService capabilityService; private CapabilityService capabilityService;
private FileFolderService fileFolderService;
public void init() public void init()
{ {
@@ -103,6 +112,11 @@ public class RMNodesImpl extends NodesImpl implements RMNodes
this.capabilityService = capabilityService; this.capabilityService = capabilityService;
} }
public void setFileFolderService(FileFolderService fileFolderService)
{
this.fileFolderService = fileFolderService;
}
@Override @Override
public Node getFolderOrDocument(final NodeRef nodeRef, NodeRef parentNodeRef, QName nodeTypeQName, List<String> includeParam, Map<String, UserInfo> mapUserInfo) public Node getFolderOrDocument(final NodeRef nodeRef, NodeRef parentNodeRef, QName nodeTypeQName, List<String> includeParam, Map<String, UserInfo> mapUserInfo)
{ {
@@ -317,4 +331,138 @@ public class RMNodesImpl extends NodesImpl implements RMNodes
return new Pair<>(searchTypeQNames, ignoreAspectQNames); return new Pair<>(searchTypeQNames, ignoreAspectQNames);
} }
@Override
public Node createNode(String parentFolderNodeId, Node nodeInfo, Parameters parameters)
{
// create RM path if needed and call the super method with the last element of the created path
NodeRef parentNodeRef = getOrCreatePath(parentFolderNodeId, nodeInfo);
nodeInfo.setRelativePath(null);
return super.createNode(parentNodeRef.getId(), nodeInfo, parameters);
}
/**
* Gets or creates the relative path specified in nodeInfo.relativePath
* starting from the provided parent folder.
* The method decides the type of the created elements considering the
* parent container's type and the type of the node to be created.
* @param parentFolderNodeId the parent folder to start from
* @param nodeInfo information about the node to be created
* @return reference to the last element of the created path
*/
protected NodeRef getOrCreatePath(String parentFolderNodeId, Node nodeInfo)
{
NodeRef parentNodeRef = validateOrLookupNode(parentFolderNodeId, null);
String relativePath = nodeInfo.getRelativePath();
if (relativePath == null)
{
return parentNodeRef;
}
List<String> pathElements = getPathElements(relativePath);
if (pathElements.isEmpty())
{
return parentNodeRef;
}
/*
* Get the latest existing path element
*/
int i = 0;
for (; i < pathElements.size(); i++)
{
final String pathElement = pathElements.get(i);
final NodeRef contextParentNodeRef = parentNodeRef;
// Navigation should not check permissions
NodeRef child = AuthenticationUtil.runAsSystem(new RunAsWork<NodeRef>()
{
@Override
public NodeRef doWork() throws Exception
{
return nodeService.getChildByName(contextParentNodeRef, ContentModel.ASSOC_CONTAINS, pathElement);
}
});
if(child == null)
{
break;
}
parentNodeRef = child;
}
if(i == pathElements.size())
{
return parentNodeRef;
}
else
{
pathElements = pathElements.subList(i, pathElements.size());
}
/*
* Starting from the latest existing element create the rest of the elements
*/
QName parentNodeType = nodeService.getType(parentNodeRef);
if(dictionaryService.isSubClass(parentNodeType, RecordsManagementModel.TYPE_UNFILED_RECORD_FOLDER) ||
dictionaryService.isSubClass(parentNodeType, RecordsManagementModel.TYPE_UNFILED_RECORD_CONTAINER))
{
for (String pathElement : pathElements)
{
// Create unfiled record folder
parentNodeRef = fileFolderService.create(parentNodeRef, pathElement, RecordsManagementModel.TYPE_UNFILED_RECORD_FOLDER).getNodeRef();
}
}
else
{
// Get the type of the node to be created
String nodeType = nodeInfo.getNodeType();
if ((nodeType == null) || nodeType.isEmpty())
{
throw new InvalidArgumentException("Node type is expected: "+parentFolderNodeId+","+nodeInfo.getName());
}
QName nodeTypeQName = createQName(nodeType);
/* Outside the unfiled record container the path elements are record categories
* except the last element which is a record folder if the created node is of type content
*/
Iterator<String> iterator = pathElements.iterator();
while(iterator.hasNext())
{
String pathElement = iterator.next();
if(!iterator.hasNext() && dictionaryService.isSubClass(nodeTypeQName, ContentModel.TYPE_CONTENT))
{
// last element, create record folder if the node to be created is content
parentNodeRef = fileFolderService.create(parentNodeRef, pathElement, RecordsManagementModel.TYPE_RECORD_FOLDER).getNodeRef();
}
else
{
// create record category
parentNodeRef = filePlanService.createRecordCategory(parentNodeRef, pathElement);
}
}
}
return parentNodeRef;
}
/**
* Helper method that parses a string representing a file path and returns a list of element names
* @param path the file path represented as a string
* @return a list of file path element names
*/
private List<String> getPathElements(String path)
{
final List<String> pathElements = new ArrayList<>();
if (path != null && path.trim().length() > 0)
{
// There is no need to check for leading and trailing "/"
final StringTokenizer tokenizer = new StringTokenizer(path, "/");
while (tokenizer.hasMoreTokens())
{
pathElements.add(tokenizer.nextToken().trim());
}
}
return pathElements;
}
} }

View File

@@ -0,0 +1,304 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2016 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.rm.rest.api.impl;
import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.alfresco.model.ContentModel;
import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel;
import org.alfresco.module.org_alfresco_module_rm.test.util.AlfMock;
import org.alfresco.module.org_alfresco_module_rm.test.util.BaseUnitTest;
import org.alfresco.rest.api.model.Node;
import org.alfresco.service.cmr.model.FileInfo;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.namespace.NamespaceService;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.MockitoAnnotations;
/**
* Unit Test class for RMNodesImpl.getOrCreatePath method
*
* @author Ana Bozianu
* @since 2.6
*/
public class RMNodesImplRelativePathUnitTest extends BaseUnitTest
{
@InjectMocks
private RMNodesImpl rmNodesImpl;
@Before
public void before()
{
MockitoAnnotations.initMocks(this);
when(mockedDictionaryService.isSubClass(TYPE_RECORD_CATEGORY, RecordsManagementModel.TYPE_UNFILED_RECORD_FOLDER)).thenReturn(false);
when(mockedDictionaryService.isSubClass(TYPE_RECORD_CATEGORY, RecordsManagementModel.TYPE_UNFILED_RECORD_CONTAINER)).thenReturn(false);
when(mockedDictionaryService.isSubClass(ContentModel.TYPE_CONTENT, ContentModel.TYPE_CONTENT)).thenReturn(true);
when(mockedDictionaryService.isSubClass(TYPE_UNFILED_RECORD_CONTAINER, RecordsManagementModel.TYPE_UNFILED_RECORD_CONTAINER)).thenReturn(true);
when(mockedDictionaryService.isSubClass(TYPE_RECORD_FOLDER, ContentModel.TYPE_CONTENT)).thenReturn(false);
when(mockedNamespaceService.getNamespaceURI(NamespaceService.CONTENT_MODEL_PREFIX)).thenReturn(NamespaceService.CONTENT_MODEL_1_0_URI);
when(mockedNamespaceService.getNamespaceURI(RM_PREFIX)).thenReturn(RM_URI);
}
/**
* Given any parent node
* When trying to create a node in the parent node with no relative path
* Then the parent node is returned and no node is created
*/
@Test
public void testNoRelativePath() throws Exception
{
/*
* Given any parent node
*/
NodeRef parentNode = AlfMock.generateNodeRef(mockedNodeService);
/*
* When trying to create a node in the parent node with no relative path
*/
Node nodeInfo = mock(Node.class);
when(nodeInfo.getRelativePath()).thenReturn(null);
NodeRef returnedPath = rmNodesImpl.getOrCreatePath(parentNode.getId(), nodeInfo);
/*
* Then the parent node is returned and no node is created
*/
assertEquals(parentNode, returnedPath);
verify(mockedFileFolderService, never()).create(any(), any(), any());
verify(mockedFilePlanService, never()).createRecordCategory(any(), any());
}
/**
* Given a parent node and an existing path c1/f1 under it
* When trying to create a node in the parent node with the relative path c1/f1
* Then the node f1 is returned and no node is created
*/
@Test
public void testGetExistingRelativePath() throws Exception
{
/*
* Given a parent node and an existing path c1/f1 under it
*/
NodeRef parentNode = AlfMock.generateNodeRef(mockedNodeService);
String category = "c1";
NodeRef categoryNode = AlfMock.generateNodeRef(mockedNodeService);
when(mockedNodeService.getChildByName(parentNode, ContentModel.ASSOC_CONTAINS, category)).thenReturn(categoryNode);
String recordFolder = "f1";
NodeRef recordFolderNode = AlfMock.generateNodeRef(mockedNodeService);
when(mockedNodeService.getChildByName(categoryNode, ContentModel.ASSOC_CONTAINS, recordFolder)).thenReturn(recordFolderNode);
/*
* When trying to create a node in the parent node with the relative path c1/f1
*/
Node nodeInfo = mock(Node.class);
when(nodeInfo.getRelativePath()).thenReturn(category + "/" + recordFolder);
NodeRef returnedPath = rmNodesImpl.getOrCreatePath(parentNode.getId(), nodeInfo);
/*
* Then the node f1 is returned and no node is created
*/
assertEquals(recordFolderNode, returnedPath);
verify(mockedFileFolderService, never()).create(any(), any(), any());
verify(mockedFilePlanService, never()).createRecordCategory(any(), any());
}
/**
* Given the fileplan and an existing path c1/c2 under fileplan
* When creating a content node under fileplan with the relative path c1/c2/c3/f1
* Then the category c3 and the record folder f1 should be created and f1 should be returned
*
* @throws Exception
*/
@Test
public void testCreatePartiallyExistingRelativePath() throws Exception
{
/*
* Given the fileplan and an existing path c1/c2 under fileplan
*/
NodeRef fileplanNodeRef = AlfMock.generateNodeRef(mockedNodeService);
// create c1
String category1 = "c1";
NodeRef categoryNode1 = AlfMock.generateNodeRef(mockedNodeService);
when(mockedNodeService.getChildByName(fileplanNodeRef, ContentModel.ASSOC_CONTAINS, category1)).thenReturn(categoryNode1);
// create c2
String category2 = "c2";
NodeRef categoryNode2 = AlfMock.generateNodeRef(mockedNodeService);
when(mockedNodeService.getChildByName(categoryNode1, ContentModel.ASSOC_CONTAINS, category2)).thenReturn(categoryNode2);
when(mockedNodeService.getType(categoryNode2)).thenReturn(TYPE_RECORD_CATEGORY);
/*
* When trying to create a content node in the relative path c1/c2/c3/f1
*/
Node nodeInfo = mock(Node.class);
when(nodeInfo.getNodeType()).thenReturn("cm:content");
// c3
String category3 = "c3";
NodeRef categoryNode3 = AlfMock.generateNodeRef(mockedNodeService);
when(mockedFilePlanService.createRecordCategory(categoryNode2, category3)).thenReturn(categoryNode3);
// f1
String recordFolder = "f1";
NodeRef recordFolderNode = AlfMock.generateNodeRef(mockedNodeService);
FileInfo recordFolderFileInfo = mock(FileInfo.class);
when(recordFolderFileInfo.getNodeRef()).thenReturn(recordFolderNode);
when(mockedFileFolderService.create(categoryNode3, recordFolder, RecordsManagementModel.TYPE_RECORD_FOLDER)).thenReturn(recordFolderFileInfo);
// call the class under tests
when(nodeInfo.getRelativePath()).thenReturn(category1 + "/" + category2 + "/" + category3 + "/" + recordFolder);
NodeRef returnedPath = rmNodesImpl.getOrCreatePath(fileplanNodeRef.getId(), nodeInfo);
/*
* Then the category c1 and the record folder f1 should be created and f1 should be returned
*/
assertEquals(recordFolderNode, returnedPath);
verify(mockedFilePlanService, times(1)).createRecordCategory(categoryNode2, category3);
verify(mockedFileFolderService, times(1)).create(categoryNode3, recordFolder, RecordsManagementModel.TYPE_RECORD_FOLDER);
}
/**
* Given the unfiled record container
* When creating a content node under fileplan with the relative path f1/f2/f3
* Then the 3 unfiled record folders should be created and f3 should be returned
*/
@Test
public void testCreateRelativePathInUnfiledRecords() throws Exception
{
/*
* Given the unfiled record folder
*/
NodeRef unfiledRecordContainer = AlfMock.generateNodeRef(mockedNodeService);
when(mockedNodeService.getType(unfiledRecordContainer)).thenReturn(TYPE_UNFILED_RECORD_CONTAINER);
/*
* When trying to create a content node in the relative path f1/f2/f3
*/
// f1
String folder1 = "f1";
NodeRef folderNode1 = AlfMock.generateNodeRef(mockedNodeService);
FileInfo folderFileInfo1 = mock(FileInfo.class);
when(folderFileInfo1.getNodeRef()).thenReturn(folderNode1);
when(mockedFileFolderService.create(unfiledRecordContainer, folder1, RecordsManagementModel.TYPE_UNFILED_RECORD_FOLDER)).thenReturn(folderFileInfo1);
// f2
String folder2 = "f2";
NodeRef folderNode2 = AlfMock.generateNodeRef(mockedNodeService);
FileInfo folderFileInfo2 = mock(FileInfo.class);
when(folderFileInfo2.getNodeRef()).thenReturn(folderNode2);
when(mockedFileFolderService.create(folderNode1, folder2, RecordsManagementModel.TYPE_UNFILED_RECORD_FOLDER)).thenReturn(folderFileInfo2);
// f3
String folder3 = "f3";
NodeRef folderNode3 = AlfMock.generateNodeRef(mockedNodeService);
FileInfo folderFileInfo3 = mock(FileInfo.class);
when(folderFileInfo3.getNodeRef()).thenReturn(folderNode3);
when(mockedFileFolderService.create(folderNode2, folder3, RecordsManagementModel.TYPE_UNFILED_RECORD_FOLDER)).thenReturn(folderFileInfo3);
// call the class under tests
Node nodeInfo = mock(Node.class);
when(nodeInfo.getRelativePath()).thenReturn(folder1 + "/" + folder2 + "/" + folder3);
NodeRef returnedParentNode = rmNodesImpl.getOrCreatePath(unfiledRecordContainer.getId(), nodeInfo);
/*
* Then the category c1 and the record folder rf1 should be created
* and an instance to the record folder should be returned
*/
assertEquals(folderNode3, returnedParentNode);
verify(mockedFileFolderService, times(1)).create(unfiledRecordContainer, folder1, RecordsManagementModel.TYPE_UNFILED_RECORD_FOLDER);
verify(mockedFileFolderService, times(1)).create(folderNode1, folder2, RecordsManagementModel.TYPE_UNFILED_RECORD_FOLDER);
verify(mockedFileFolderService, times(1)).create(folderNode2, folder3, RecordsManagementModel.TYPE_UNFILED_RECORD_FOLDER);
//check no other node is created
verify(mockedFilePlanService, never()).createRecordCategory(any(), any());
verify(mockedFileFolderService, times(3)).create(any(), any(), any());
}
/**
* Given the fileplan
* When creating a record folder node under fileplan with the relative path c1/c2/c3
* Then the categories c1, c2 and c3 should be created and c3 should be returned
*/
@Test
public void testCreateRelativePathToRecordFolder() throws Exception
{
/*
* Given the fileplan
*/
NodeRef fileplanNodeRef = AlfMock.generateNodeRef(mockedNodeService);
when(mockedNodeService.getType(fileplanNodeRef)).thenReturn(TYPE_FILE_PLAN);
/*
* When trying to create a folder node in the relative path c1/c2/c3
*/
Node nodeInfo = mock(Node.class);
when(nodeInfo.getNodeType()).thenReturn("rma:recordFolder");
// c1
String category1 = "c1";
NodeRef categoryNode1 = AlfMock.generateNodeRef(mockedNodeService);
when(mockedFilePlanService.createRecordCategory(fileplanNodeRef, category1)).thenReturn(categoryNode1);
// c2
String category2 = "c2";
NodeRef categoryNode2 = AlfMock.generateNodeRef(mockedNodeService);
when(mockedFilePlanService.createRecordCategory(categoryNode1, category2)).thenReturn(categoryNode2);
// c3
String category3 = "c3";
NodeRef categoryNode3 = AlfMock.generateNodeRef(mockedNodeService);
when(mockedFilePlanService.createRecordCategory(categoryNode2, category3)).thenReturn(categoryNode3);
// call the class under tests
when(nodeInfo.getRelativePath()).thenReturn(category1 + "/" + category2 + "/" + category3);
NodeRef returnedParentNode = rmNodesImpl.getOrCreatePath(fileplanNodeRef.getId(), nodeInfo);
/*
* Then the categories c1, c2 and c3 should be created and c3 should be returned
*/
assertEquals(categoryNode3, returnedParentNode);
verify(mockedFilePlanService, times(1)).createRecordCategory(fileplanNodeRef, category1);
verify(mockedFilePlanService, times(1)).createRecordCategory(categoryNode1, category2);
verify(mockedFilePlanService, times(1)).createRecordCategory(categoryNode2, category3);
// check no other node is created
verify(mockedFilePlanService, times(3)).createRecordCategory(any(), any());
verify(mockedFileFolderService, never()).create(any(), any(), any());
}
}