SHA-1629 : Creating a link to file in a different location

- Added support for multiple files in doclink.post webscript
   - Added unit test for api/node/doclink api
   - Added marker aspect app:linked for nodes that have links attached

git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/BRANCHES/DEV/5.2.N/root@131857 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
Ramona Neamtu
2016-10-28 12:09:59 +00:00
parent 7457fa5b13
commit fee7ef2ad4
11 changed files with 820 additions and 614 deletions

View File

@@ -384,6 +384,7 @@
<value>alfresco.messages.authentication</value> <value>alfresco.messages.authentication</value>
<value>alfresco.messages.file-folder-service</value> <value>alfresco.messages.file-folder-service</value>
<value>alfresco.messages.custommodel-service</value> <value>alfresco.messages.custommodel-service</value>
<value>alfresco.messages.doclink-service</value>
</list> </list>
</property> </property>
</bean> </bean>

View File

@@ -61,3 +61,4 @@ org.alfresco.datalists.list-deleted={1} deleted data list {0}
org.alfresco.subscriptions.followed={1} is now following {5} org.alfresco.subscriptions.followed={1} is now following {5}
org.alfresco.subscriptions.subscribed={1} has subscribed to {2} org.alfresco.subscriptions.subscribed={1} has subscribed to {2}
org.alfresco.profile.status-changed={1}: {2} org.alfresco.profile.status-changed={1}: {2}
org.alfresco.doclink.link-created={1} created link to {0}

View File

@@ -0,0 +1,3 @@
# link service externalised display strings
doclink_service.link_to_label=Link to {0}

View File

@@ -94,11 +94,14 @@
<property name="hiddenAspect" ref="hiddenAspect"/> <property name="hiddenAspect" ref="hiddenAspect"/>
</bean> </bean>
<bean name="documentLinkService" class="org.alfresco.repo.doclink.DocumentLinkServiceImpl"> <bean name="documentLinkService" class="org.alfresco.repo.doclink.DocumentLinkServiceImpl" init-method="init">
<property name="nodeService" ref="NodeService"/> <property name="nodeService" ref="NodeService"/>
<property name="dictionaryService" ref="dictionaryService"/> <property name="dictionaryService" ref="dictionaryService"/>
<property name="searchService" ref="admSearchService"/> <property name="searchService" ref="admSearchService"/>
<property name="namespaceService" ref="namespaceService"/> <property name="namespaceService" ref="namespaceService"/>
<property name="checkOutCheckInService" ref="checkOutCheckInService"/>
<property name="policyComponent" ref="policyComponent"/>
<property name="behaviourFilter" ref="policyBehaviourFilter" />
</bean> </bean>
<bean id="mlTranslationInterceptor" class="org.alfresco.repo.model.filefolder.MLTranslationInterceptor" > <bean id="mlTranslationInterceptor" class="org.alfresco.repo.model.filefolder.MLTranslationInterceptor" >

View File

@@ -146,6 +146,10 @@
</properties> </properties>
</aspect> </aspect>
<aspect name="app:linked">
<title>Marker aspect to indicate that the node has been linked.</title>
</aspect>
</aspects> </aspects>
</model> </model>

View File

@@ -66,4 +66,7 @@ public interface ApplicationModel
// Default view config aspect // Default view config aspect
static final QName ASPECT_DEFAULT_VIEW_CONFIG = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "defaultViewConfig"); static final QName ASPECT_DEFAULT_VIEW_CONFIG = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "defaultViewConfig");
static final QName PROP_DEFAULT_VIEW_ID = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "defaultViewId"); static final QName PROP_DEFAULT_VIEW_ID = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "defaultViewId");
// Linked aspect
static final QName ASPECT_LINKED = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "linked");
} }

View File

@@ -60,4 +60,6 @@ public interface ActivityType
public static final String FOLDER_LIKED = "org.alfresco.documentlibrary.folder-liked"; public static final String FOLDER_LIKED = "org.alfresco.documentlibrary.folder-liked";
public static final String COMMENT_CREATED = "org.alfresco.comments.comment-created"; public static final String COMMENT_CREATED = "org.alfresco.comments.comment-created";
public static final String DOCLINK_CREATED = "org.alfresco.doclink.link-created";
} }

View File

@@ -26,15 +26,22 @@
package org.alfresco.repo.doclink; package org.alfresco.repo.doclink;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.alfresco.model.ApplicationModel; import org.alfresco.model.ApplicationModel;
import org.alfresco.model.ContentModel; import org.alfresco.model.ContentModel;
import org.alfresco.repo.model.filefolder.FileFolderServiceImpl; import org.alfresco.repo.node.NodeServicePolicies;
import org.alfresco.repo.node.NodeServicePolicies.BeforeDeleteNodePolicy;
import org.alfresco.repo.policy.BehaviourFilter;
import org.alfresco.repo.policy.JavaBehaviour;
import org.alfresco.repo.policy.PolicyComponent;
import org.alfresco.repo.search.QueryParameterDefImpl; import org.alfresco.repo.search.QueryParameterDefImpl;
import org.alfresco.repo.security.permissions.AccessDeniedException; import org.alfresco.repo.security.permissions.AccessDeniedException;
import org.alfresco.repo.site.SiteModel;
import org.alfresco.service.cmr.coci.CheckOutCheckInService;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition; import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.ChildAssociationRef;
@@ -47,18 +54,19 @@ import org.alfresco.service.cmr.search.QueryParameterDefinition;
import org.alfresco.service.cmr.search.SearchService; import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.QName;
import org.alfresco.util.ParameterCheck;
import org.alfresco.util.PropertyCheck; import org.alfresco.util.PropertyCheck;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.extensions.surf.util.I18NUtil;
/** /**
* Implementation of the document link service * Implementation of the document link service
* In addition to the document link service, this class also provides a BeforeDeleteNodePolicy
* *
* @author Ana Bozianu * @author Ana Bozianu
* @since 5.1 * @since 5.1
*/ */
public class DocumentLinkServiceImpl implements DocumentLinkService public class DocumentLinkServiceImpl implements DocumentLinkService, NodeServicePolicies.BeforeDeleteNodePolicy
{ {
private static Log logger = LogFactory.getLog(DocumentLinkServiceImpl.class); private static Log logger = LogFactory.getLog(DocumentLinkServiceImpl.class);
@@ -66,6 +74,9 @@ public class DocumentLinkServiceImpl implements DocumentLinkService
private DictionaryService dictionaryService; private DictionaryService dictionaryService;
private SearchService searchService; private SearchService searchService;
private NamespaceService namespaceService; private NamespaceService namespaceService;
private CheckOutCheckInService checkOutCheckInService;
private PolicyComponent policyComponent;
private BehaviourFilter behaviourFilter;
/** Shallow search for nodes with a name pattern */ /** Shallow search for nodes with a name pattern */
private static final String XPATH_QUERY_NODE_NAME_MATCH = "./*[like(@cm:name, $cm:name, false)]"; private static final String XPATH_QUERY_NODE_NAME_MATCH = "./*[like(@cm:name, $cm:name, false)]";
@@ -73,6 +84,45 @@ public class DocumentLinkServiceImpl implements DocumentLinkService
/** Shallow search for links with a destination pattern */ /** Shallow search for links with a destination pattern */
private static final String XPATH_QUERY_LINK_DEST_MATCH = ".//*[like(@cm:destination, $cm:destination, false)]"; private static final String XPATH_QUERY_LINK_DEST_MATCH = ".//*[like(@cm:destination, $cm:destination, false)]";
private static final String LINK_NODE_EXTENSION = ".url";
/* I18N labels */
private static final String LINK_TO_LABEL = "doclink_service.link_to_label";
/**
* The initialise method. Register our policies.
*/
public void init()
{
PropertyCheck.mandatory(this, "nodeService", nodeService);
PropertyCheck.mandatory(this, "dictionaryService", dictionaryService);
PropertyCheck.mandatory(this, "searchService", searchService);
PropertyCheck.mandatory(this, "namespaceService", namespaceService);
PropertyCheck.mandatory(this, "checkOutCheckInService", checkOutCheckInService);
PropertyCheck.mandatory(this, "policyComponent", policyComponent);
PropertyCheck.mandatory(this, "behaviourFilter", behaviourFilter);
// Register interest in the beforeDeleteNode policy
//for nodes that have app:linked aspect
policyComponent.bindClassBehaviour(
BeforeDeleteNodePolicy.QNAME,
ApplicationModel.ASPECT_LINKED,
new JavaBehaviour(this, "beforeDeleteNode"));
//for app:filelink node types
policyComponent.bindClassBehaviour(
BeforeDeleteNodePolicy.QNAME,
ApplicationModel.TYPE_FILELINK,
new JavaBehaviour(this, "beforeDeleteLinkNode"));
//for app:folderlink node types
policyComponent.bindClassBehaviour(
BeforeDeleteNodePolicy.QNAME,
ApplicationModel.TYPE_FOLDERLINK,
new JavaBehaviour(this, "beforeDeleteLinkNode"));
}
@Override @Override
public NodeRef createDocumentLink(NodeRef source, NodeRef destination) public NodeRef createDocumentLink(NodeRef source, NodeRef destination)
{ {
@@ -100,28 +150,39 @@ public class DocumentLinkServiceImpl implements DocumentLinkService
{ {
throw new IllegalArgumentException("Destination node NodeRef '" + source + "' must be of type " + ContentModel.TYPE_FOLDER); throw new IllegalArgumentException("Destination node NodeRef '" + source + "' must be of type " + ContentModel.TYPE_FOLDER);
} }
//if file is working copy - create link to the original
if (checkOutCheckInService.isWorkingCopy(source))
{
source = checkOutCheckInService.getCheckedOut(source);
}
/* Create link */ /* Create link */
String sourceName = (String) nodeService.getProperty(source, ContentModel.PROP_NAME); String sourceName = (String) nodeService.getProperty(source, ContentModel.PROP_NAME);
String newName = sourceName + LINK_NODE_EXTENSION;
newName = I18NUtil.getMessage(LINK_TO_LABEL, newName);
Map<QName, Serializable> props = new HashMap<QName, Serializable>(); Map<QName, Serializable> props = new HashMap<QName, Serializable>();
props.put(ContentModel.PROP_NAME, sourceName); props.put(ContentModel.PROP_NAME, newName);
props.put(ContentModel.PROP_LINK_DESTINATION, source); props.put(ContentModel.PROP_LINK_DESTINATION, source);
QName assocQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName(sourceName));
QName assocQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName(newName));
// check if the link node already exists // check if the link node already exists
if (checkExists(sourceName, destination)) if (checkExists(newName, destination))
{ {
throw new IllegalArgumentException("A file with the name '" + sourceName + "' already exists in the destination folder"); throw new IllegalArgumentException("A file with the name '" + newName + "' already exists in the destination folder");
} }
ChildAssociationRef childRef = null; ChildAssociationRef childRef = null;
if (dictionaryService.isSubClass(nodeService.getType(source), ContentModel.TYPE_CONTENT)) QName sourceType = nodeService.getType(source);
if (dictionaryService.isSubClass(sourceType, ContentModel.TYPE_CONTENT))
{ {
// create File Link node // create File Link node
childRef = nodeService.createNode(destination, ContentModel.ASSOC_CONTAINS, assocQName, ApplicationModel.TYPE_FILELINK, props); childRef = nodeService.createNode(destination, ContentModel.ASSOC_CONTAINS, assocQName, ApplicationModel.TYPE_FILELINK, props);
} }
else if (dictionaryService.isSubClass(nodeService.getType(source), ContentModel.TYPE_FOLDER)) else if (!dictionaryService.isSubClass(sourceType, SiteModel.TYPE_SITE) && dictionaryService.isSubClass(nodeService.getType(source), ContentModel.TYPE_FOLDER))
{ {
// create Folder link node // create Folder link node
childRef = nodeService.createNode(destination, ContentModel.ASSOC_CONTAINS, assocQName, ApplicationModel.TYPE_FOLDERLINK, props); childRef = nodeService.createNode(destination, ContentModel.ASSOC_CONTAINS, assocQName, ApplicationModel.TYPE_FOLDERLINK, props);
@@ -131,6 +192,9 @@ public class DocumentLinkServiceImpl implements DocumentLinkService
throw new IllegalArgumentException("unsupported source node type : " + nodeService.getType(source)); throw new IllegalArgumentException("unsupported source node type : " + nodeService.getType(source));
} }
//add linked aspect to the sourceNode
nodeService.addAspect(source, ApplicationModel.ASPECT_LINKED, null);
return childRef.getChildRef(); return childRef.getChildRef();
} }
@@ -174,6 +238,31 @@ public class DocumentLinkServiceImpl implements DocumentLinkService
return (NodeRef) nodeService.getProperty(linkNodeRef, ContentModel.PROP_LINK_DESTINATION); return (NodeRef) nodeService.getProperty(linkNodeRef, ContentModel.PROP_LINK_DESTINATION);
} }
@Override
public List<NodeRef> getNodeLinks(NodeRef nodeRef)
{
/* Validate input */
PropertyCheck.mandatory(this, "nodeRef", nodeRef);
/* Get all links of the given nodeRef */
QueryParameterDefinition[] params = new QueryParameterDefinition[1];
params[0] = new QueryParameterDefImpl(ContentModel.PROP_LINK_DESTINATION, dictionaryService.getDataType(DataTypeDefinition.NODE_REF), true, nodeRef.toString());
List<NodeRef> nodeLinks = new ArrayList<NodeRef>();
List<NodeRef> nodeRefs;
/* Search for links in all stores */
for(StoreRef store : nodeService.getStores())
{
/* Get the root node */
NodeRef rootNodeRef = nodeService.getRootNode(store);
/* Execute the query, retrieve links to the document*/
nodeRefs = searchService.selectNodes(rootNodeRef, XPATH_QUERY_LINK_DEST_MATCH, params, namespaceService, true);
nodeLinks.addAll(nodeRefs);
}
return nodeLinks;
}
@Override @Override
public DeleteLinksStatusReport deleteLinksToDocument(NodeRef document) public DeleteLinksStatusReport deleteLinksToDocument(NodeRef document)
{ {
@@ -185,25 +274,15 @@ public class DocumentLinkServiceImpl implements DocumentLinkService
/* Validate input */ /* Validate input */
PropertyCheck.mandatory(this, "document", document); PropertyCheck.mandatory(this, "document", document);
/* Get all links of the given document */
QueryParameterDefinition[] params = new QueryParameterDefinition[1];
params[0] = new QueryParameterDefImpl(ContentModel.PROP_LINK_DESTINATION, dictionaryService.getDataType(DataTypeDefinition.NODE_REF), true, document.toString());
/* Search for links in all stores */
DeleteLinksStatusReport report = new DeleteLinksStatusReport(); DeleteLinksStatusReport report = new DeleteLinksStatusReport();
for(StoreRef store : nodeService.getStores())
{
/* Get the root node */
NodeRef rootNodeRef = nodeService.getRootNode(store);
/* Execute the query, retrieve links to the document*/ List<NodeRef> linkNodeRefs = getNodeLinks(document);
List<NodeRef> nodeRefs = searchService.selectNodes(rootNodeRef, XPATH_QUERY_LINK_DEST_MATCH, params, namespaceService, true); report.addTotalLinksFoundCount(linkNodeRefs.size());
report.addTotalLinksFoundCount(nodeRefs.size());
/* Delete the found nodes */ for (NodeRef linkRef : linkNodeRefs)
for(NodeRef linkRef : nodeRefs) {
try
{ {
try{
nodeService.deleteNode(linkRef); nodeService.deleteNode(linkRef);
/* if the node was successfully deleted increment the count */ /* if the node was successfully deleted increment the count */
@@ -215,11 +294,50 @@ public class DocumentLinkServiceImpl implements DocumentLinkService
report.addErrorDetail(linkRef, ex); report.addErrorDetail(linkRef, ex);
} }
} }
}
// remove also the aspect app:linked
nodeService.removeAspect(document, ApplicationModel.ASPECT_LINKED);
return report; return report;
} }
@Override
public void beforeDeleteNode(NodeRef nodeRef)
{
behaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE);
try
{
deleteLinksToDocument(nodeRef);
}
finally
{
behaviourFilter.enableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE);
}
}
public void beforeDeleteLinkNode(NodeRef linkNodeRef)
{
// NodeRef linkNodeRef = childAssocRef.getChildRef();
NodeRef nodeRef = getLinkDestination(linkNodeRef);
List<NodeRef> nodeRefLinks = getNodeLinks(nodeRef);
if (nodeRefLinks.size() == 1 && nodeRefLinks.contains(linkNodeRef))
{
behaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE);
try
{
// remove linked aspect to the sourceNode
nodeService.removeAspect(nodeRef, ApplicationModel.ASPECT_LINKED);
}
finally
{
behaviourFilter.enableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE);
}
}
}
public void setNodeService(NodeService nodeService) public void setNodeService(NodeService nodeService)
{ {
this.nodeService = nodeService; this.nodeService = nodeService;
@@ -239,4 +357,19 @@ public class DocumentLinkServiceImpl implements DocumentLinkService
{ {
this.namespaceService = namespaceService; this.namespaceService = namespaceService;
} }
public void setCheckOutCheckInService(CheckOutCheckInService checkOutCheckInService)
{
this.checkOutCheckInService = checkOutCheckInService;
}
public void setPolicyComponent(PolicyComponent policyComponent)
{
this.policyComponent = policyComponent;
}
public void setBehaviourFilter(BehaviourFilter behaviourFilter)
{
this.behaviourFilter = behaviourFilter;
}
} }

View File

@@ -25,6 +25,8 @@
*/ */
package org.alfresco.service.cmr.repository; package org.alfresco.service.cmr.repository;
import java.util.List;
/** /**
* Provides methods specific to manipulating links of documents * Provides methods specific to manipulating links of documents
* *
@@ -54,6 +56,14 @@ public interface DocumentLinkService
*/ */
public NodeRef getLinkDestination(NodeRef linkNodeRef); public NodeRef getLinkDestination(NodeRef linkNodeRef);
/**
* Returns the associated links for a node, from all stores
*
* @param nodeRef
* @return A list of link nodeRefs for given node
*/
public List<NodeRef> getNodeLinks(NodeRef nodeRef);
/** /**
* Deletes all links having the provided node as destination. * Deletes all links having the provided node as destination.
* *

View File

@@ -0,0 +1,43 @@
/*
* #%L
* Alfresco Repository
* %%
* 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;
import junit.framework.Test;
import junit.framework.TestSuite;
/**
* All Repository project UNIT test classes should be added to this test suite.
*/
public class Repository67TestSuite extends TestSuite
{
public static Test suite()
{
TestSuite suite = new TestSuite();
Repository01TestSuite.tests67(suite);
return suite;
}
}

View File

@@ -32,8 +32,6 @@ import java.util.Map;
import javax.transaction.Status; import javax.transaction.Status;
import javax.transaction.UserTransaction; import javax.transaction.UserTransaction;
import junit.framework.TestCase;
import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ApplicationModel; import org.alfresco.model.ApplicationModel;
import org.alfresco.model.ContentModel; import org.alfresco.model.ContentModel;
@@ -55,6 +53,9 @@ import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.ApplicationContextHelper; import org.alfresco.util.ApplicationContextHelper;
import org.alfresco.util.GUID; import org.alfresco.util.GUID;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.extensions.surf.util.I18NUtil;
import junit.framework.TestCase;
/** /**
* Test cases for {@link DocumentLinkServiceImpl}. * Test cases for {@link DocumentLinkServiceImpl}.
@@ -173,7 +174,8 @@ public class DocumentLinkServiceImplTest extends TestCase
assertNotNull(linkNodeRef); assertNotNull(linkNodeRef);
// test if the link node is listed as a child of site1Folder2 // test if the link node is listed as a child of site1Folder2
NodeRef linkNodeRef2 = fileFolderService.searchSimple(site1Folder2, site1File1Name); String site1File1LinkName = I18NUtil.getMessage("doclink_service.link_to_label", (site1File1Name + ".url"));
NodeRef linkNodeRef2 = fileFolderService.searchSimple(site1Folder2, site1File1LinkName);
assertNotNull(linkNodeRef2); assertNotNull(linkNodeRef2);
assertEquals(linkNodeRef, linkNodeRef2); assertEquals(linkNodeRef, linkNodeRef2);
@@ -198,7 +200,8 @@ public class DocumentLinkServiceImplTest extends TestCase
assertNotNull(linkNodeRef); assertNotNull(linkNodeRef);
// test if the link node is listed as a child of site1Folder2 // test if the link node is listed as a child of site1Folder2
NodeRef linkNodeRef2 = fileFolderService.searchSimple(site1Folder2, site1Folder1Name); String site1Folder1LinkName = I18NUtil.getMessage("doclink_service.link_to_label", (site1Folder1Name + ".url"));
NodeRef linkNodeRef2 = fileFolderService.searchSimple(site1Folder2, site1Folder1LinkName);
assertNotNull(linkNodeRef2); assertNotNull(linkNodeRef2);
assertEquals(linkNodeRef, linkNodeRef2); assertEquals(linkNodeRef, linkNodeRef2);