From fee7ef2ad4128e4ceeda60c6bbe6bf71ab6e1d46 Mon Sep 17 00:00:00 2001 From: Ramona Neamtu Date: Fri, 28 Oct 2016 12:09:59 +0000 Subject: [PATCH] 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 --- config/alfresco/core-services-context.xml | 1 + .../messages/activity-list.properties | 1 + .../messages/doclink-service.properties | 3 + .../model-specific-services-context.xml | 5 +- config/alfresco/model/applicationModel.xml | 4 + .../org/alfresco/model/ApplicationModel.java | 91 +-- .../repo/activities/ActivityType.java | 78 +-- .../repo/doclink/DocumentLinkServiceImpl.java | 567 ++++++++++------ .../cmr/repository/DocumentLinkService.java | 16 +- .../org/alfresco/Repository67TestSuite.java | 43 ++ .../doclink/DocumentLinkServiceImplTest.java | 625 +++++++++--------- 11 files changed, 820 insertions(+), 614 deletions(-) create mode 100644 config/alfresco/messages/doclink-service.properties create mode 100644 source/test-java/org/alfresco/Repository67TestSuite.java diff --git a/config/alfresco/core-services-context.xml b/config/alfresco/core-services-context.xml index bb8a4cd233..88a130d519 100644 --- a/config/alfresco/core-services-context.xml +++ b/config/alfresco/core-services-context.xml @@ -384,6 +384,7 @@ alfresco.messages.authentication alfresco.messages.file-folder-service alfresco.messages.custommodel-service + alfresco.messages.doclink-service diff --git a/config/alfresco/messages/activity-list.properties b/config/alfresco/messages/activity-list.properties index e7035eb3e9..2eb3618dce 100644 --- a/config/alfresco/messages/activity-list.properties +++ b/config/alfresco/messages/activity-list.properties @@ -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.subscribed={1} has subscribed to {2} org.alfresco.profile.status-changed={1}: {2} +org.alfresco.doclink.link-created={1} created link to {0} \ No newline at end of file diff --git a/config/alfresco/messages/doclink-service.properties b/config/alfresco/messages/doclink-service.properties new file mode 100644 index 0000000000..31a2daa7c6 --- /dev/null +++ b/config/alfresco/messages/doclink-service.properties @@ -0,0 +1,3 @@ +# link service externalised display strings + +doclink_service.link_to_label=Link to {0} \ No newline at end of file diff --git a/config/alfresco/model-specific-services-context.xml b/config/alfresco/model-specific-services-context.xml index 20b3dfba04..c389dfd7c0 100644 --- a/config/alfresco/model-specific-services-context.xml +++ b/config/alfresco/model-specific-services-context.xml @@ -94,11 +94,14 @@ - + + + + diff --git a/config/alfresco/model/applicationModel.xml b/config/alfresco/model/applicationModel.xml index 63ff978621..722ea2a722 100644 --- a/config/alfresco/model/applicationModel.xml +++ b/config/alfresco/model/applicationModel.xml @@ -146,6 +146,10 @@ + + Marker aspect to indicate that the node has been linked. + + \ No newline at end of file diff --git a/source/java/org/alfresco/model/ApplicationModel.java b/source/java/org/alfresco/model/ApplicationModel.java index 1de778dda4..27ddf61dce 100644 --- a/source/java/org/alfresco/model/ApplicationModel.java +++ b/source/java/org/alfresco/model/ApplicationModel.java @@ -23,47 +23,50 @@ * along with Alfresco. If not, see . * #L% */ -package org.alfresco.model; - -import org.alfresco.service.namespace.NamespaceService; -import org.alfresco.service.namespace.QName; - -/** - * @author Kevin Roast - */ -public interface ApplicationModel -{ - // workflow - static final QName ASPECT_SIMPLE_WORKFLOW = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "simpleworkflow"); - static final QName PROP_APPROVE_STEP = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "approveStep"); - static final QName PROP_APPROVE_FOLDER = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "approveFolder"); - static final QName PROP_APPROVE_MOVE = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "approveMove"); - static final QName PROP_REJECT_STEP = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "rejectStep"); - static final QName PROP_REJECT_FOLDER = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "rejectFolder"); - static final QName PROP_REJECT_MOVE = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "rejectMove"); - - // ui facets aspect - static final QName ASPECT_UIFACETS = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "uifacets"); - static final QName PROP_ICON = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "icon"); - - // inlineeditable aspect - static final QName ASPECT_INLINEEDITABLE = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "inlineeditable"); - static final QName PROP_EDITINLINE = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "editInline"); - - // configurable aspect - static final QName ASPECT_CONFIGURABLE = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "configurable"); - static final QName TYPE_CONFIGURATIONS = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "configurations"); - static final QName ASSOC_CONFIGURATIONS = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "configurations"); - - // object links - static final QName TYPE_FILELINK = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "filelink"); - static final QName TYPE_FOLDERLINK = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "folderlink"); - - // feed source aspect - static final QName ASPECT_FEEDSOURCE = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "feedsource"); - static final QName PROP_FEEDTEMPLATE = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "template"); - - // Default view config aspect - 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"); -} +package org.alfresco.model; + +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; + +/** + * @author Kevin Roast + */ +public interface ApplicationModel +{ + // workflow + static final QName ASPECT_SIMPLE_WORKFLOW = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "simpleworkflow"); + static final QName PROP_APPROVE_STEP = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "approveStep"); + static final QName PROP_APPROVE_FOLDER = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "approveFolder"); + static final QName PROP_APPROVE_MOVE = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "approveMove"); + static final QName PROP_REJECT_STEP = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "rejectStep"); + static final QName PROP_REJECT_FOLDER = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "rejectFolder"); + static final QName PROP_REJECT_MOVE = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "rejectMove"); + + // ui facets aspect + static final QName ASPECT_UIFACETS = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "uifacets"); + static final QName PROP_ICON = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "icon"); + + // inlineeditable aspect + static final QName ASPECT_INLINEEDITABLE = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "inlineeditable"); + static final QName PROP_EDITINLINE = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "editInline"); + + // configurable aspect + static final QName ASPECT_CONFIGURABLE = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "configurable"); + static final QName TYPE_CONFIGURATIONS = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "configurations"); + static final QName ASSOC_CONFIGURATIONS = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "configurations"); + + // object links + static final QName TYPE_FILELINK = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "filelink"); + static final QName TYPE_FOLDERLINK = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "folderlink"); + + // feed source aspect + static final QName ASPECT_FEEDSOURCE = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "feedsource"); + static final QName PROP_FEEDTEMPLATE = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "template"); + + // Default view config aspect + 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"); + + // Linked aspect + static final QName ASPECT_LINKED = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "linked"); +} diff --git a/source/java/org/alfresco/repo/activities/ActivityType.java b/source/java/org/alfresco/repo/activities/ActivityType.java index 60dabb84d3..c65fa2a809 100644 --- a/source/java/org/alfresco/repo/activities/ActivityType.java +++ b/source/java/org/alfresco/repo/activities/ActivityType.java @@ -23,41 +23,43 @@ * along with Alfresco. If not, see . * #L% */ -package org.alfresco.repo.activities; - -public interface ActivityType -{ - // pre-defined alfresco activity types - - // generic fallback (if specific template is missing) - public static final String GENERIC_FALLBACK = "org.alfresco.generic"; - - // site membership - public static final String SITE_USER_JOINED = "org.alfresco.site.user-joined"; - public static final String SITE_USER_REMOVED = "org.alfresco.site.user-left"; - public static final String SITE_USER_ROLE_UPDATE = "org.alfresco.site.user-role-changed"; - public static final String SITE_GROUP_ADDED = "org.alfresco.site.group-added"; - public static final String SITE_GROUP_REMOVED = "org.alfresco.site.group-removed"; - public static final String SITE_GROUP_ROLE_UPDATE = "org.alfresco.site.group-role-changed"; - public static final String SUBSCRIPTIONS_SUBSCRIBE = "org.alfresco.subscriptions.subscribed"; - public static final String SUBSCRIPTIONS_FOLLOW = "org.alfresco.subscriptions.followed"; - - public static final String FILE_ADDED = "org.alfresco.documentlibrary.file-added"; - public static final String FILE_UPDATED = "org.alfresco.documentlibrary.file-updated"; - public static final String FILE_DELETED = "org.alfresco.documentlibrary.file-deleted"; - - public static final String FILES_ADDED = "org.alfresco.documentlibrary.files-added"; - public static final String FILES_UPDATED = "org.alfresco.documentlibrary.files-updated"; - public static final String FILES_DELETED = "org.alfresco.documentlibrary.files-deleted"; - - public static final String FOLDER_ADDED = "org.alfresco.documentlibrary.folder-added"; - public static final String FOLDER_DELETED = "org.alfresco.documentlibrary.folder-deleted"; - - public static final String FOLDERS_ADDED = "org.alfresco.documentlibrary.folders-added"; - public static final String FOLDERS_DELETED = "org.alfresco.documentlibrary.folders-deleted"; - - public static final String FILE_LIKED = "org.alfresco.documentlibrary.file-liked"; - public static final String FOLDER_LIKED = "org.alfresco.documentlibrary.folder-liked"; - - public static final String COMMENT_CREATED = "org.alfresco.comments.comment-created"; -} +package org.alfresco.repo.activities; + +public interface ActivityType +{ + // pre-defined alfresco activity types + + // generic fallback (if specific template is missing) + public static final String GENERIC_FALLBACK = "org.alfresco.generic"; + + // site membership + public static final String SITE_USER_JOINED = "org.alfresco.site.user-joined"; + public static final String SITE_USER_REMOVED = "org.alfresco.site.user-left"; + public static final String SITE_USER_ROLE_UPDATE = "org.alfresco.site.user-role-changed"; + public static final String SITE_GROUP_ADDED = "org.alfresco.site.group-added"; + public static final String SITE_GROUP_REMOVED = "org.alfresco.site.group-removed"; + public static final String SITE_GROUP_ROLE_UPDATE = "org.alfresco.site.group-role-changed"; + public static final String SUBSCRIPTIONS_SUBSCRIBE = "org.alfresco.subscriptions.subscribed"; + public static final String SUBSCRIPTIONS_FOLLOW = "org.alfresco.subscriptions.followed"; + + public static final String FILE_ADDED = "org.alfresco.documentlibrary.file-added"; + public static final String FILE_UPDATED = "org.alfresco.documentlibrary.file-updated"; + public static final String FILE_DELETED = "org.alfresco.documentlibrary.file-deleted"; + + public static final String FILES_ADDED = "org.alfresco.documentlibrary.files-added"; + public static final String FILES_UPDATED = "org.alfresco.documentlibrary.files-updated"; + public static final String FILES_DELETED = "org.alfresco.documentlibrary.files-deleted"; + + public static final String FOLDER_ADDED = "org.alfresco.documentlibrary.folder-added"; + public static final String FOLDER_DELETED = "org.alfresco.documentlibrary.folder-deleted"; + + public static final String FOLDERS_ADDED = "org.alfresco.documentlibrary.folders-added"; + public static final String FOLDERS_DELETED = "org.alfresco.documentlibrary.folders-deleted"; + + public static final String FILE_LIKED = "org.alfresco.documentlibrary.file-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 DOCLINK_CREATED = "org.alfresco.doclink.link-created"; +} diff --git a/source/java/org/alfresco/repo/doclink/DocumentLinkServiceImpl.java b/source/java/org/alfresco/repo/doclink/DocumentLinkServiceImpl.java index 4391d3366b..5e7871ad13 100644 --- a/source/java/org/alfresco/repo/doclink/DocumentLinkServiceImpl.java +++ b/source/java/org/alfresco/repo/doclink/DocumentLinkServiceImpl.java @@ -23,220 +23,353 @@ * along with Alfresco. If not, see . * #L% */ -package org.alfresco.repo.doclink; - -import java.io.Serializable; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.alfresco.model.ApplicationModel; -import org.alfresco.model.ContentModel; -import org.alfresco.repo.model.filefolder.FileFolderServiceImpl; -import org.alfresco.repo.search.QueryParameterDefImpl; -import org.alfresco.repo.security.permissions.AccessDeniedException; -import org.alfresco.service.cmr.dictionary.DataTypeDefinition; -import org.alfresco.service.cmr.dictionary.DictionaryService; -import org.alfresco.service.cmr.repository.ChildAssociationRef; -import org.alfresco.service.cmr.repository.DeleteLinksStatusReport; -import org.alfresco.service.cmr.repository.DocumentLinkService; -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.QueryParameterDefinition; -import org.alfresco.service.cmr.search.SearchService; -import org.alfresco.service.namespace.NamespaceService; -import org.alfresco.service.namespace.QName; -import org.alfresco.util.ParameterCheck; -import org.alfresco.util.PropertyCheck; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -/** - * Implementation of the document link service - * - * @author Ana Bozianu - * @since 5.1 - */ -public class DocumentLinkServiceImpl implements DocumentLinkService -{ - private static Log logger = LogFactory.getLog(DocumentLinkServiceImpl.class); - - private NodeService nodeService; - private DictionaryService dictionaryService; - private SearchService searchService; - private NamespaceService namespaceService; - - /** Shallow search for nodes with a name pattern */ - private static final String XPATH_QUERY_NODE_NAME_MATCH = "./*[like(@cm:name, $cm:name, false)]"; - - /** Shallow search for links with a destination pattern */ - private static final String XPATH_QUERY_LINK_DEST_MATCH = ".//*[like(@cm:destination, $cm:destination, false)]"; - - @Override - public NodeRef createDocumentLink(NodeRef source, NodeRef destination) - { - if(logger.isDebugEnabled()) - { - logger.debug("Creating document link. source: " + source + ", destination: " + destination); - } - - /* Validate input */ - PropertyCheck.mandatory(this, "source", source); - PropertyCheck.mandatory(this, "destination", destination); - - // check if source node exists - if (!nodeService.exists(source)) - { - throw new IllegalArgumentException("Source NodeRef '" + source + "' does not exist"); - } - // check if destination node exists - if (!nodeService.exists(destination)) - { - throw new IllegalArgumentException("Destination NodeRef '" + destination + "' does not exist"); - } - // check if destination node is a directory - if (!dictionaryService.isSubClass(nodeService.getType(destination), ContentModel.TYPE_FOLDER)) - { - throw new IllegalArgumentException("Destination node NodeRef '" + source + "' must be of type " + ContentModel.TYPE_FOLDER); - } - - /* Create link */ - String sourceName = (String) nodeService.getProperty(source, ContentModel.PROP_NAME); - Map props = new HashMap(); - props.put(ContentModel.PROP_NAME, sourceName); - props.put(ContentModel.PROP_LINK_DESTINATION, source); - QName assocQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName(sourceName)); - - // check if the link node already exists - if (checkExists(sourceName, destination)) - { - throw new IllegalArgumentException("A file with the name '" + sourceName + "' already exists in the destination folder"); - } - - ChildAssociationRef childRef = null; - if (dictionaryService.isSubClass(nodeService.getType(source), ContentModel.TYPE_CONTENT)) - { - // create File Link node - childRef = nodeService.createNode(destination, ContentModel.ASSOC_CONTAINS, assocQName, ApplicationModel.TYPE_FILELINK, props); - - } - else if (dictionaryService.isSubClass(nodeService.getType(source), ContentModel.TYPE_FOLDER)) - { - // create Folder link node - childRef = nodeService.createNode(destination, ContentModel.ASSOC_CONTAINS, assocQName, ApplicationModel.TYPE_FOLDERLINK, props); - } - else - { - throw new IllegalArgumentException("unsupported source node type : " + nodeService.getType(source)); - } - - return childRef.getChildRef(); - } - - /** - * Check if node with specified name exists within a - * parent folder. - * - * @param name - * @param parent - * @return - */ - private boolean checkExists(String name, NodeRef parent) - { - QueryParameterDefinition[] params = new QueryParameterDefinition[1]; - params[0] = new QueryParameterDefImpl(ContentModel.PROP_NAME, dictionaryService.getDataType(DataTypeDefinition.TEXT), true, name); - - // execute the query - List nodeRefs = searchService.selectNodes(parent, XPATH_QUERY_NODE_NAME_MATCH, params, namespaceService, false); - - return (nodeRefs.size() != 0); - } - - @Override - public NodeRef getLinkDestination(NodeRef linkNodeRef) - { - /* Validate input */ - PropertyCheck.mandatory(this, "linkNodeRef", linkNodeRef); - - /* Check if the node exists */ - if (!nodeService.exists(linkNodeRef)) - { - throw new IllegalArgumentException("The provided node does not exist"); - } - - if (!nodeService.getType(linkNodeRef).equals(ApplicationModel.TYPE_FILELINK) - && !nodeService.getType(linkNodeRef).equals(ApplicationModel.TYPE_FOLDERLINK)) - { - throw new IllegalArgumentException("The provided node is not a document link"); - } - - return (NodeRef) nodeService.getProperty(linkNodeRef, ContentModel.PROP_LINK_DESTINATION); - } - - @Override - public DeleteLinksStatusReport deleteLinksToDocument(NodeRef document) - { - if(logger.isDebugEnabled()) - { - logger.debug("Deleting links of a document. document: " + document); - } - - /* Validate input */ - 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(); - for(StoreRef store : nodeService.getStores()) - { - /* Get the root node */ - NodeRef rootNodeRef = nodeService.getRootNode(store); - - /* Execute the query, retrieve links to the document*/ - List nodeRefs = searchService.selectNodes(rootNodeRef, XPATH_QUERY_LINK_DEST_MATCH, params, namespaceService, true); - report.addTotalLinksFoundCount(nodeRefs.size()); - - /* Delete the found nodes */ - for(NodeRef linkRef : nodeRefs) - { - try{ - nodeService.deleteNode(linkRef); - - /* if the node was successfully deleted increment the count */ - report.incrementDeletedLinksCount(); - } - catch(AccessDeniedException ex) - { - /* if the node could not be deleted add it to the report */ - report.addErrorDetail(linkRef, ex); - } - } - } - - return report; - } - - public void setNodeService(NodeService nodeService) - { - this.nodeService = nodeService; - } - - public void setDictionaryService(DictionaryService dictionaryService) - { - this.dictionaryService = dictionaryService; - } - - public void setSearchService(SearchService searchService) - { - this.searchService = searchService; - } - - public void setNamespaceService(NamespaceService namespaceService) - { - this.namespaceService = namespaceService; - } -} +package org.alfresco.repo.doclink; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ApplicationModel; +import org.alfresco.model.ContentModel; +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.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.DictionaryService; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.DeleteLinksStatusReport; +import org.alfresco.service.cmr.repository.DocumentLinkService; +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.QueryParameterDefinition; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.PropertyCheck; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * Implementation of the document link service + * In addition to the document link service, this class also provides a BeforeDeleteNodePolicy + * + * @author Ana Bozianu + * @since 5.1 + */ +public class DocumentLinkServiceImpl implements DocumentLinkService, NodeServicePolicies.BeforeDeleteNodePolicy +{ + private static Log logger = LogFactory.getLog(DocumentLinkServiceImpl.class); + + private NodeService nodeService; + private DictionaryService dictionaryService; + private SearchService searchService; + private NamespaceService namespaceService; + private CheckOutCheckInService checkOutCheckInService; + private PolicyComponent policyComponent; + private BehaviourFilter behaviourFilter; + + /** Shallow search for nodes with a name pattern */ + private static final String XPATH_QUERY_NODE_NAME_MATCH = "./*[like(@cm:name, $cm:name, false)]"; + + /** 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 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 + public NodeRef createDocumentLink(NodeRef source, NodeRef destination) + { + if(logger.isDebugEnabled()) + { + logger.debug("Creating document link. source: " + source + ", destination: " + destination); + } + + /* Validate input */ + PropertyCheck.mandatory(this, "source", source); + PropertyCheck.mandatory(this, "destination", destination); + + // check if source node exists + if (!nodeService.exists(source)) + { + throw new IllegalArgumentException("Source NodeRef '" + source + "' does not exist"); + } + // check if destination node exists + if (!nodeService.exists(destination)) + { + throw new IllegalArgumentException("Destination NodeRef '" + destination + "' does not exist"); + } + // check if destination node is a directory + if (!dictionaryService.isSubClass(nodeService.getType(destination), 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 */ + String sourceName = (String) nodeService.getProperty(source, ContentModel.PROP_NAME); + + String newName = sourceName + LINK_NODE_EXTENSION; + newName = I18NUtil.getMessage(LINK_TO_LABEL, newName); + + Map props = new HashMap(); + props.put(ContentModel.PROP_NAME, newName); + props.put(ContentModel.PROP_LINK_DESTINATION, source); + + QName assocQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName(newName)); + + // check if the link node already exists + if (checkExists(newName, destination)) + { + throw new IllegalArgumentException("A file with the name '" + newName + "' already exists in the destination folder"); + } + + ChildAssociationRef childRef = null; + QName sourceType = nodeService.getType(source); + + if (dictionaryService.isSubClass(sourceType, ContentModel.TYPE_CONTENT)) + { + // create File Link node + childRef = nodeService.createNode(destination, ContentModel.ASSOC_CONTAINS, assocQName, ApplicationModel.TYPE_FILELINK, props); + + } + else if (!dictionaryService.isSubClass(sourceType, SiteModel.TYPE_SITE) && dictionaryService.isSubClass(nodeService.getType(source), ContentModel.TYPE_FOLDER)) + { + // create Folder link node + childRef = nodeService.createNode(destination, ContentModel.ASSOC_CONTAINS, assocQName, ApplicationModel.TYPE_FOLDERLINK, props); + } + else + { + 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(); + } + + /** + * Check if node with specified name exists within a + * parent folder. + * + * @param name + * @param parent + * @return + */ + private boolean checkExists(String name, NodeRef parent) + { + QueryParameterDefinition[] params = new QueryParameterDefinition[1]; + params[0] = new QueryParameterDefImpl(ContentModel.PROP_NAME, dictionaryService.getDataType(DataTypeDefinition.TEXT), true, name); + + // execute the query + List nodeRefs = searchService.selectNodes(parent, XPATH_QUERY_NODE_NAME_MATCH, params, namespaceService, false); + + return (nodeRefs.size() != 0); + } + + @Override + public NodeRef getLinkDestination(NodeRef linkNodeRef) + { + /* Validate input */ + PropertyCheck.mandatory(this, "linkNodeRef", linkNodeRef); + + /* Check if the node exists */ + if (!nodeService.exists(linkNodeRef)) + { + throw new IllegalArgumentException("The provided node does not exist"); + } + + if (!nodeService.getType(linkNodeRef).equals(ApplicationModel.TYPE_FILELINK) + && !nodeService.getType(linkNodeRef).equals(ApplicationModel.TYPE_FOLDERLINK)) + { + throw new IllegalArgumentException("The provided node is not a document link"); + } + + return (NodeRef) nodeService.getProperty(linkNodeRef, ContentModel.PROP_LINK_DESTINATION); + } + + @Override + public List 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 nodeLinks = new ArrayList(); + List 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 + public DeleteLinksStatusReport deleteLinksToDocument(NodeRef document) + { + if (logger.isDebugEnabled()) + { + logger.debug("Deleting links of a document. document: " + document); + } + + /* Validate input */ + PropertyCheck.mandatory(this, "document", document); + + DeleteLinksStatusReport report = new DeleteLinksStatusReport(); + + List linkNodeRefs = getNodeLinks(document); + report.addTotalLinksFoundCount(linkNodeRefs.size()); + + for (NodeRef linkRef : linkNodeRefs) + { + try + { + nodeService.deleteNode(linkRef); + + /* if the node was successfully deleted increment the count */ + report.incrementDeletedLinksCount(); + } + catch (AccessDeniedException ex) + { + /* if the node could not be deleted add it to the report */ + report.addErrorDetail(linkRef, ex); + } + } + + // remove also the aspect app:linked + nodeService.removeAspect(document, ApplicationModel.ASPECT_LINKED); + + 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 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) + { + this.nodeService = nodeService; + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + public void setNamespaceService(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; + } +} diff --git a/source/java/org/alfresco/service/cmr/repository/DocumentLinkService.java b/source/java/org/alfresco/service/cmr/repository/DocumentLinkService.java index eb0fc7418d..68a4ab3aca 100644 --- a/source/java/org/alfresco/service/cmr/repository/DocumentLinkService.java +++ b/source/java/org/alfresco/service/cmr/repository/DocumentLinkService.java @@ -23,8 +23,10 @@ * along with Alfresco. If not, see . * #L% */ -package org.alfresco.service.cmr.repository; - +package org.alfresco.service.cmr.repository; + +import java.util.List; + /** * Provides methods specific to manipulating links of documents * @@ -52,7 +54,15 @@ public interface DocumentLinkService * The link node. * @return A reference to the destination of the provided link node */ - 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 getNodeLinks(NodeRef nodeRef); /** * Deletes all links having the provided node as destination. diff --git a/source/test-java/org/alfresco/Repository67TestSuite.java b/source/test-java/org/alfresco/Repository67TestSuite.java new file mode 100644 index 0000000000..bbf85be135 --- /dev/null +++ b/source/test-java/org/alfresco/Repository67TestSuite.java @@ -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 . + * #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; + } +} diff --git a/source/test-java/org/alfresco/repo/doclink/DocumentLinkServiceImplTest.java b/source/test-java/org/alfresco/repo/doclink/DocumentLinkServiceImplTest.java index ca7eb4c552..d7eeef586d 100644 --- a/source/test-java/org/alfresco/repo/doclink/DocumentLinkServiceImplTest.java +++ b/source/test-java/org/alfresco/repo/doclink/DocumentLinkServiceImplTest.java @@ -23,314 +23,317 @@ * along with Alfresco. If not, see . * #L% */ -package org.alfresco.repo.doclink; - -import java.io.Serializable; -import java.util.HashMap; -import java.util.Map; - -import javax.transaction.Status; -import javax.transaction.UserTransaction; - -import junit.framework.TestCase; - -import org.alfresco.error.AlfrescoRuntimeException; -import org.alfresco.model.ApplicationModel; -import org.alfresco.model.ContentModel; -import org.alfresco.repo.security.authentication.AuthenticationUtil; -import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; -import org.alfresco.repo.security.permissions.AccessDeniedException; -import org.alfresco.service.ServiceRegistry; -import org.alfresco.service.cmr.model.FileFolderService; -import org.alfresco.service.cmr.repository.DeleteLinksStatusReport; -import org.alfresco.service.cmr.repository.DocumentLinkService; -import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.repository.NodeService; -import org.alfresco.service.cmr.security.PermissionService; -import org.alfresco.service.cmr.security.PersonService; -import org.alfresco.service.cmr.site.SiteService; -import org.alfresco.service.cmr.site.SiteVisibility; -import org.alfresco.service.namespace.QName; -import org.alfresco.service.transaction.TransactionService; -import org.alfresco.util.ApplicationContextHelper; -import org.alfresco.util.GUID; -import org.springframework.context.ApplicationContext; - -/** - * Test cases for {@link DocumentLinkServiceImpl}. - * - * @author Ana Bozianu - * @since 5.1 - */ - -public class DocumentLinkServiceImplTest extends TestCase -{ - - private static final ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); - private static final String TEST_USER = DocumentLinkServiceImplTest.class.getSimpleName() + "_testuser"; - - private UserTransaction txn; - - private TransactionService transactionService; - private DocumentLinkService documentLinkService; - private PermissionService permissionService; - private PersonService personService; - private SiteService siteService; - private FileFolderService fileFolderService; - private NodeService nodeService; - - // nodes the test user has read/write permission to - private NodeRef site1File1; - private NodeRef site1File2; // do not create links of this file - private NodeRef site1Folder1; - private NodeRef site1Folder2; - private NodeRef site1Folder3; - - // nodes the test user has no permission to - private NodeRef site2File; - private NodeRef site2Folder1; - private NodeRef site2Folder2; - private NodeRef linkOfFile1Site2; - - private String site1File1Name = GUID.generate(); - private String site1Folder1Name = GUID.generate();; - - @Override - public void setUp() throws Exception - { - // Set up the services - ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean("ServiceRegistry"); - transactionService = serviceRegistry.getTransactionService(); - documentLinkService = serviceRegistry.getDocumentLinkService(); - permissionService = serviceRegistry.getPermissionService(); - personService = serviceRegistry.getPersonService(); - siteService = serviceRegistry.getSiteService(); - fileFolderService = serviceRegistry.getFileFolderService(); - nodeService = serviceRegistry.getNodeService(); - - // Start the transaction - txn = transactionService.getUserTransaction(); - txn.begin(); - - // Authenticate - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); - - /* Create the test user */ - Map props = new HashMap(); - props.put(ContentModel.PROP_USERNAME, TEST_USER); - personService.createPerson(props); - - /* - * Create the working test root 1 to which the user has read/write - * permission - */ - NodeRef site1 = siteService.createSite("site1", GUID.generate(), "myTitle", "myDescription", SiteVisibility.PUBLIC).getNodeRef(); - permissionService.setPermission(site1, TEST_USER, PermissionService.ALL_PERMISSIONS, true); - site1Folder1 = fileFolderService.create(site1, site1Folder1Name, ContentModel.TYPE_FOLDER).getNodeRef(); - site1File1 = fileFolderService.create(site1Folder1, site1File1Name, ContentModel.TYPE_CONTENT).getNodeRef(); - site1File2 = fileFolderService.create(site1Folder1, GUID.generate(), ContentModel.TYPE_CONTENT).getNodeRef(); - site1Folder2 = fileFolderService.create(site1, GUID.generate(), ContentModel.TYPE_FOLDER).getNodeRef(); - site1Folder3 = fileFolderService.create(site1, GUID.generate(), ContentModel.TYPE_FOLDER).getNodeRef(); - // create a link of site1File1 in site1Folder3 to test regular deletion - documentLinkService.createDocumentLink(site1File2, site1Folder3); - - /* Create the working test root 2 to which the user has no permission */ - NodeRef site2 = siteService.createSite("site2", GUID.generate(), "myTitle", "myDescription", SiteVisibility.PRIVATE).getNodeRef(); - permissionService.setPermission(site2, TEST_USER, PermissionService.ALL_PERMISSIONS, false); - site2File = fileFolderService.create(site2, GUID.generate(), ContentModel.TYPE_CONTENT).getNodeRef(); - site2Folder1 = fileFolderService.create(site2, GUID.generate(), ContentModel.TYPE_FOLDER).getNodeRef(); - site2Folder2 = fileFolderService.create(site2, GUID.generate(), ContentModel.TYPE_FOLDER).getNodeRef(); - // Create a link of site1File1 in site2Folder2 to test the deletion - // without permission - linkOfFile1Site2 = documentLinkService.createDocumentLink(site1File2, site2Folder2); - } - - @Override - protected void tearDown() throws Exception - { - try - { - if (txn.getStatus() != Status.STATUS_ROLLEDBACK && txn.getStatus() != Status.STATUS_COMMITTED) - { - txn.rollback(); - } - } - catch (Throwable e) - { - e.printStackTrace(); - } - } - - /** - * Tests the creation of a file link - * - * @throws Exception - */ - public void testCreateFileLink() throws Exception - { - // create a link of file site1File1_1 in folder site1Folder2 - NodeRef linkNodeRef = documentLinkService.createDocumentLink(site1File1, site1Folder2); - assertNotNull(linkNodeRef); - - // test if the link node is listed as a child of site1Folder2 - NodeRef linkNodeRef2 = fileFolderService.searchSimple(site1Folder2, site1File1Name); - assertNotNull(linkNodeRef2); - assertEquals(linkNodeRef, linkNodeRef2); - - // check the type of the link - assertEquals(nodeService.getType(linkNodeRef), ApplicationModel.TYPE_FILELINK); - - // check if the link destination is site1File1_1 - NodeRef linkDestination = documentLinkService.getLinkDestination(linkNodeRef); - assertNotNull(linkDestination); - assertEquals(linkDestination, site1File1); - } - - /** - * Tests the creation of a folder link - * - * @throws Exception - */ - public void testCreateFolderLink() throws Exception - { - // create a link of file site1File1_1 in folder site1Folder2 - NodeRef linkNodeRef = documentLinkService.createDocumentLink(site1Folder1, site1Folder2); - assertNotNull(linkNodeRef); - - // test if the link node is listed as a child of site1Folder2 - NodeRef linkNodeRef2 = fileFolderService.searchSimple(site1Folder2, site1Folder1Name); - assertNotNull(linkNodeRef2); - assertEquals(linkNodeRef, linkNodeRef2); - - // check the type of the link - assertEquals(nodeService.getType(linkNodeRef), ApplicationModel.TYPE_FOLDERLINK); - - // check if the link destination is site1File1_1 - NodeRef linkDestination = documentLinkService.getLinkDestination(linkNodeRef); - assertNotNull(linkDestination); - assertEquals(linkDestination, site1Folder1); - } - - /** - * Tests the behavior of createDocumentLink when providing null arguments - * - * @throws Exception - */ - public void testCreateDocumentWithNullArguments() throws Exception - { - try - { - documentLinkService.createDocumentLink(null, null); - fail("null arguments must generate AlfrescoRuntimeException."); - } - catch (AlfrescoRuntimeException e) - { - // Expected - } - } - - /** - * Test the behavior of createDocumentLink when provided a file as a - * destination - * - * @throws Exception - */ - public void testInvalidDestination() throws Exception - { - try - { - documentLinkService.createDocumentLink(site1Folder2, site1File1); - fail("invalid destination argument must generate IllegalArgumentException."); - } - catch (IllegalArgumentException e) - { - // Expected - } - } - - /** - * Tests the creation of a document link when the user doesn't have read - * permission on the source node - * - * @throws Exception - */ - public void testCreateDocLinkWithoutReadPermissionOnSource() throws Exception - { - try - { - AuthenticationUtil.runAs(new RunAsWork() - { - @Override - public Void doWork() throws Exception - { - documentLinkService.createDocumentLink(site2File, site1Folder2); - return null; - } - }, TEST_USER); - - fail("no read permission on the source node must generate AccessDeniedException."); - } - catch (AccessDeniedException ex) - { - // Expected - } - } - - /** - * Tests the creation of a document link when the user doesn't have write - * permission on the destination node - * - * @throws Exception - */ - public void testCreateDocLinkWithoutWritePermissionOnDestination() throws Exception - { - try - { - AuthenticationUtil.runAs(new RunAsWork() - { - @Override - public Void doWork() throws Exception - { - documentLinkService.createDocumentLink(site1File1, site2Folder1); - return null; - } - }, TEST_USER); - - fail("no write permission on the destination node must generate AccessDeniedException."); - } - catch (AccessDeniedException ex) - { - // Expected - } - } - - /** - * Tests the deletion of a document's links, with and without write - * permissions - * - * @throws Exception - */ - public void testDeleteLinks() throws Exception - { - DeleteLinksStatusReport report = AuthenticationUtil.runAs(new RunAsWork() - { - @Override - public DeleteLinksStatusReport doWork() throws Exception - { - return documentLinkService.deleteLinksToDocument(site1File2); - } - }, TEST_USER); - - // check if the service found 2 links of the document - assertEquals(report.getTotalLinksFoundCount(), 2); - - // check if the service successfully deleted one - assertEquals(report.getDeletedLinksCount(), 1); - - // check if the second one failed with access denied - Throwable ex = report.getErrorDetails().get(linkOfFile1Site2); - assertNotNull(ex); - assertEquals(ex.getClass(), AccessDeniedException.class); - } - -} +package org.alfresco.repo.doclink; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import javax.transaction.Status; +import javax.transaction.UserTransaction; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ApplicationModel; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.repository.DeleteLinksStatusReport; +import org.alfresco.service.cmr.repository.DocumentLinkService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.cmr.site.SiteVisibility; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.GUID; +import org.springframework.context.ApplicationContext; +import org.springframework.extensions.surf.util.I18NUtil; + +import junit.framework.TestCase; + +/** + * Test cases for {@link DocumentLinkServiceImpl}. + * + * @author Ana Bozianu + * @since 5.1 + */ + +public class DocumentLinkServiceImplTest extends TestCase +{ + + private static final ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + private static final String TEST_USER = DocumentLinkServiceImplTest.class.getSimpleName() + "_testuser"; + + private UserTransaction txn; + + private TransactionService transactionService; + private DocumentLinkService documentLinkService; + private PermissionService permissionService; + private PersonService personService; + private SiteService siteService; + private FileFolderService fileFolderService; + private NodeService nodeService; + + // nodes the test user has read/write permission to + private NodeRef site1File1; + private NodeRef site1File2; // do not create links of this file + private NodeRef site1Folder1; + private NodeRef site1Folder2; + private NodeRef site1Folder3; + + // nodes the test user has no permission to + private NodeRef site2File; + private NodeRef site2Folder1; + private NodeRef site2Folder2; + private NodeRef linkOfFile1Site2; + + private String site1File1Name = GUID.generate(); + private String site1Folder1Name = GUID.generate();; + + @Override + public void setUp() throws Exception + { + // Set up the services + ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean("ServiceRegistry"); + transactionService = serviceRegistry.getTransactionService(); + documentLinkService = serviceRegistry.getDocumentLinkService(); + permissionService = serviceRegistry.getPermissionService(); + personService = serviceRegistry.getPersonService(); + siteService = serviceRegistry.getSiteService(); + fileFolderService = serviceRegistry.getFileFolderService(); + nodeService = serviceRegistry.getNodeService(); + + // Start the transaction + txn = transactionService.getUserTransaction(); + txn.begin(); + + // Authenticate + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + /* Create the test user */ + Map props = new HashMap(); + props.put(ContentModel.PROP_USERNAME, TEST_USER); + personService.createPerson(props); + + /* + * Create the working test root 1 to which the user has read/write + * permission + */ + NodeRef site1 = siteService.createSite("site1", GUID.generate(), "myTitle", "myDescription", SiteVisibility.PUBLIC).getNodeRef(); + permissionService.setPermission(site1, TEST_USER, PermissionService.ALL_PERMISSIONS, true); + site1Folder1 = fileFolderService.create(site1, site1Folder1Name, ContentModel.TYPE_FOLDER).getNodeRef(); + site1File1 = fileFolderService.create(site1Folder1, site1File1Name, ContentModel.TYPE_CONTENT).getNodeRef(); + site1File2 = fileFolderService.create(site1Folder1, GUID.generate(), ContentModel.TYPE_CONTENT).getNodeRef(); + site1Folder2 = fileFolderService.create(site1, GUID.generate(), ContentModel.TYPE_FOLDER).getNodeRef(); + site1Folder3 = fileFolderService.create(site1, GUID.generate(), ContentModel.TYPE_FOLDER).getNodeRef(); + // create a link of site1File1 in site1Folder3 to test regular deletion + documentLinkService.createDocumentLink(site1File2, site1Folder3); + + /* Create the working test root 2 to which the user has no permission */ + NodeRef site2 = siteService.createSite("site2", GUID.generate(), "myTitle", "myDescription", SiteVisibility.PRIVATE).getNodeRef(); + permissionService.setPermission(site2, TEST_USER, PermissionService.ALL_PERMISSIONS, false); + site2File = fileFolderService.create(site2, GUID.generate(), ContentModel.TYPE_CONTENT).getNodeRef(); + site2Folder1 = fileFolderService.create(site2, GUID.generate(), ContentModel.TYPE_FOLDER).getNodeRef(); + site2Folder2 = fileFolderService.create(site2, GUID.generate(), ContentModel.TYPE_FOLDER).getNodeRef(); + // Create a link of site1File1 in site2Folder2 to test the deletion + // without permission + linkOfFile1Site2 = documentLinkService.createDocumentLink(site1File2, site2Folder2); + } + + @Override + protected void tearDown() throws Exception + { + try + { + if (txn.getStatus() != Status.STATUS_ROLLEDBACK && txn.getStatus() != Status.STATUS_COMMITTED) + { + txn.rollback(); + } + } + catch (Throwable e) + { + e.printStackTrace(); + } + } + + /** + * Tests the creation of a file link + * + * @throws Exception + */ + public void testCreateFileLink() throws Exception + { + // create a link of file site1File1_1 in folder site1Folder2 + NodeRef linkNodeRef = documentLinkService.createDocumentLink(site1File1, site1Folder2); + assertNotNull(linkNodeRef); + + // test if the link node is listed as a child of site1Folder2 + String site1File1LinkName = I18NUtil.getMessage("doclink_service.link_to_label", (site1File1Name + ".url")); + NodeRef linkNodeRef2 = fileFolderService.searchSimple(site1Folder2, site1File1LinkName); + assertNotNull(linkNodeRef2); + assertEquals(linkNodeRef, linkNodeRef2); + + // check the type of the link + assertEquals(nodeService.getType(linkNodeRef), ApplicationModel.TYPE_FILELINK); + + // check if the link destination is site1File1_1 + NodeRef linkDestination = documentLinkService.getLinkDestination(linkNodeRef); + assertNotNull(linkDestination); + assertEquals(linkDestination, site1File1); + } + + /** + * Tests the creation of a folder link + * + * @throws Exception + */ + public void testCreateFolderLink() throws Exception + { + // create a link of file site1File1_1 in folder site1Folder2 + NodeRef linkNodeRef = documentLinkService.createDocumentLink(site1Folder1, site1Folder2); + assertNotNull(linkNodeRef); + + // test if the link node is listed as a child of site1Folder2 + String site1Folder1LinkName = I18NUtil.getMessage("doclink_service.link_to_label", (site1Folder1Name + ".url")); + NodeRef linkNodeRef2 = fileFolderService.searchSimple(site1Folder2, site1Folder1LinkName); + assertNotNull(linkNodeRef2); + assertEquals(linkNodeRef, linkNodeRef2); + + // check the type of the link + assertEquals(nodeService.getType(linkNodeRef), ApplicationModel.TYPE_FOLDERLINK); + + // check if the link destination is site1File1_1 + NodeRef linkDestination = documentLinkService.getLinkDestination(linkNodeRef); + assertNotNull(linkDestination); + assertEquals(linkDestination, site1Folder1); + } + + /** + * Tests the behavior of createDocumentLink when providing null arguments + * + * @throws Exception + */ + public void testCreateDocumentWithNullArguments() throws Exception + { + try + { + documentLinkService.createDocumentLink(null, null); + fail("null arguments must generate AlfrescoRuntimeException."); + } + catch (AlfrescoRuntimeException e) + { + // Expected + } + } + + /** + * Test the behavior of createDocumentLink when provided a file as a + * destination + * + * @throws Exception + */ + public void testInvalidDestination() throws Exception + { + try + { + documentLinkService.createDocumentLink(site1Folder2, site1File1); + fail("invalid destination argument must generate IllegalArgumentException."); + } + catch (IllegalArgumentException e) + { + // Expected + } + } + + /** + * Tests the creation of a document link when the user doesn't have read + * permission on the source node + * + * @throws Exception + */ + public void testCreateDocLinkWithoutReadPermissionOnSource() throws Exception + { + try + { + AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public Void doWork() throws Exception + { + documentLinkService.createDocumentLink(site2File, site1Folder2); + return null; + } + }, TEST_USER); + + fail("no read permission on the source node must generate AccessDeniedException."); + } + catch (AccessDeniedException ex) + { + // Expected + } + } + + /** + * Tests the creation of a document link when the user doesn't have write + * permission on the destination node + * + * @throws Exception + */ + public void testCreateDocLinkWithoutWritePermissionOnDestination() throws Exception + { + try + { + AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public Void doWork() throws Exception + { + documentLinkService.createDocumentLink(site1File1, site2Folder1); + return null; + } + }, TEST_USER); + + fail("no write permission on the destination node must generate AccessDeniedException."); + } + catch (AccessDeniedException ex) + { + // Expected + } + } + + /** + * Tests the deletion of a document's links, with and without write + * permissions + * + * @throws Exception + */ + public void testDeleteLinks() throws Exception + { + DeleteLinksStatusReport report = AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public DeleteLinksStatusReport doWork() throws Exception + { + return documentLinkService.deleteLinksToDocument(site1File2); + } + }, TEST_USER); + + // check if the service found 2 links of the document + assertEquals(report.getTotalLinksFoundCount(), 2); + + // check if the service successfully deleted one + assertEquals(report.getDeletedLinksCount(), 1); + + // check if the second one failed with access denied + Throwable ex = report.getErrorDetails().get(linkOfFile1Site2); + assertNotNull(ex); + assertEquals(ex.getClass(), AccessDeniedException.class); + } + +}