/* * Copyright (C) 2005-2007 Alfresco Software Limited. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * This program 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 General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * As a special exception to the terms and conditions of version 2.0 of * the GPL, you may redistribute this Program in connection with Free/Libre * and Open Source Software ("FLOSS") applications as described in Alfresco's * FLOSS exception. You should have recieved a copy of the text describing * the FLOSS exception, and it is also available here: * http://www.alfresco.com/legal/licensing" */ package org.alfresco.repo.copy; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.i18n.I18NUtil; import org.alfresco.model.ContentModel; import org.alfresco.repo.action.ActionServiceImpl; import org.alfresco.repo.copy.CopyBehaviourCallback.ChildAssocCopyAction; import org.alfresco.repo.copy.CopyBehaviourCallback.ChildAssocRecurseAction; import org.alfresco.repo.copy.CopyBehaviourCallback.CopyChildAssociationDetails; import org.alfresco.repo.policy.ClassPolicyDelegate; import org.alfresco.repo.policy.JavaBehaviour; import org.alfresco.repo.policy.PolicyComponent; import org.alfresco.service.cmr.dictionary.AspectDefinition; import org.alfresco.service.cmr.dictionary.AssociationDefinition; import org.alfresco.service.cmr.dictionary.ChildAssociationDefinition; import org.alfresco.service.cmr.dictionary.ClassDefinition; import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.dictionary.PropertyDefinition; import org.alfresco.service.cmr.dictionary.TypeDefinition; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.CopyService; import org.alfresco.service.cmr.repository.CopyServiceException; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.rule.RuleService; import org.alfresco.service.cmr.search.ResultSet; import org.alfresco.service.cmr.search.SearchService; import org.alfresco.service.cmr.security.AccessPermission; import org.alfresco.service.cmr.security.AccessStatus; import org.alfresco.service.cmr.security.AuthenticationService; import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.RegexQNamePattern; import org.alfresco.util.GUID; import org.alfresco.util.ParameterCheck; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * Node operations service implmentation. * * @author Roy Wetherall */ public class CopyServiceImpl implements CopyService { /** * The logger */ private static Log logger = LogFactory.getLog(ActionServiceImpl.class); /** I18N labels */ private String COPY_OF_LABEL = "copy_service.copy_of_label"; /** The node service */ private NodeService nodeService; private NodeService internalNodeService; /** The dictionary service*/ private DictionaryService dictionaryService; /** The search service */ private SearchService searchService; /** Policy component */ private PolicyComponent policyComponent; /** Rule service */ private RuleService ruleService; /** Permission service */ private PermissionService permissionService; /** Authentication service */ private AuthenticationService authenticationService; /** Policy delegates */ private ClassPolicyDelegate onCopyNodeDelegate; private ClassPolicyDelegate onCopyCompleteDelegate; /** * Set the node service * * @param nodeService the node service */ public void setNodeService(NodeService nodeService) { this.nodeService = nodeService; } /** * Sets the internal node service * * @param internalNodeService the internal node service */ public void setInternalNodeService(NodeService internalNodeService) { this.internalNodeService = internalNodeService; } /** * Sets the dictionary service * * @param dictionaryService the dictionary service */ public void setDictionaryService(DictionaryService dictionaryService) { this.dictionaryService = dictionaryService; } /** * Sets the policy component * * @param policyComponent the policy component */ public void setPolicyComponent(PolicyComponent policyComponent) { this.policyComponent = policyComponent; } /** * Sets the search service * * @param searchService the search service */ public void setSearchService(SearchService searchService) { this.searchService = searchService; } /** * Set the rule service * * @param ruleService the rule service */ public void setRuleService(RuleService ruleService) { this.ruleService = ruleService; } /** * Set the permission service * * @param permissionService the permission service */ public void setPermissionService(PermissionService permissionService) { this.permissionService = permissionService; } /** * Sets the authentication service * * @param authenticationService the authentication service */ public void setAuthenticationService(AuthenticationService authenticationService) { this.authenticationService = authenticationService; } /** * Initialise method */ public void init() { // Register the policies onCopyNodeDelegate = policyComponent.registerClassPolicy(CopyServicePolicies.OnCopyNodePolicy.class); onCopyCompleteDelegate = policyComponent.registerClassPolicy(CopyServicePolicies.OnCopyCompletePolicy.class); // Register policy behaviours this.policyComponent.bindClassBehaviour( QName.createQName(NamespaceService.ALFRESCO_URI, "getCopyCallback"), ContentModel.ASPECT_COPIEDFROM, new JavaBehaviour(this, "getCallbackForCopiedFromAspect")); this.policyComponent.bindClassBehaviour( QName.createQName(NamespaceService.ALFRESCO_URI, "getCopyCallback"), ContentModel.TYPE_FOLDER, new JavaBehaviour(this, "getCallbackForFolderType")); this.policyComponent.bindClassBehaviour( QName.createQName(NamespaceService.ALFRESCO_URI, "getCopyCallback"), ContentModel.ASPECT_OWNABLE, new JavaBehaviour(this, "getCallbackForOwnableAspect")); this.policyComponent.bindClassBehaviour( QName.createQName(NamespaceService.ALFRESCO_URI, "getCopyCallback"), ContentModel.ASPECT_AUTHOR, new JavaBehaviour(this, "getCallbackForAuthorAspect")); } public NodeRef copy( NodeRef sourceNodeRef, NodeRef targetParentRef, QName assocTypeQName, QName assocQName, boolean copyChildren) { // Check that all the passed values are not null ParameterCheck.mandatory("sourceNodeRef", sourceNodeRef); ParameterCheck.mandatory("targetParentRef", targetParentRef); ParameterCheck.mandatory("assocTypeQName", assocTypeQName); ParameterCheck.mandatory("assocQName", assocQName); if (sourceNodeRef.getStoreRef().equals(targetParentRef.getStoreRef()) == false) { // TODO We need to create a new node in the other store with the same id as the source // Error - since at the moment we do not support cross store copying throw new UnsupportedOperationException("Copying nodes across stores is not currently supported."); } // Keep track of copied children Map copiesByOriginals = new HashMap(17); Set copies = new HashSet(17); NodeRef copiedNodeRef = copyImpl( sourceNodeRef, targetParentRef, assocTypeQName, assocQName, copyChildren, true, // Drop cm:name for top-level node copiesByOriginals, copies); // Check if the node was copied if (copiedNodeRef == null) { CopyDetails copyDetails = getCopyDetails(sourceNodeRef, targetParentRef, null, assocTypeQName, assocQName); // Denied! throw new CopyServiceException( "A bound policy denied copy: \n" + " " + copyDetails); } // Foreach of the newly created copies call the copy complete policy for (Map.Entry entry : copiesByOriginals.entrySet()) { NodeRef mappedSourceNodeRef = entry.getKey(); NodeRef mappedTargetNodeRef = entry.getValue(); invokeCopyComplete(mappedSourceNodeRef, mappedTargetNodeRef, true, copiesByOriginals); } // Done return copiedNodeRef; } public NodeRef copyAndRename( NodeRef sourceNodeRef, NodeRef destinationParent, QName assocTypeQName, QName assocQName, boolean copyChildren) { // To fix ETWOONE-224 issue it is necessary to change a QName of the new node accordingly to its name. NodeRef result = null; String sourceName = (String)this.internalNodeService.getProperty(sourceNodeRef, ContentModel.PROP_NAME); // Find a non-duplicate name String newName = sourceName; while (this.internalNodeService.getChildByName(destinationParent, assocTypeQName, newName) != null) { newName = I18NUtil.getMessage(COPY_OF_LABEL, newName); } if (assocQName == null) { // Change a QName of the new node accordingly to its name assocQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName(newName)); } // Make a copy result = copy(sourceNodeRef, destinationParent, assocTypeQName, assocQName, copyChildren); // Set name property this.internalNodeService.setProperty(result, ContentModel.PROP_NAME, newName); // Return new NodeRef return result; } /** * {@inheritDoc} * * Defer to the standard implementation with copyChildren set to false */ public NodeRef copy( NodeRef sourceNodeRef, NodeRef targetParentNodeRef, QName assocTypeQName, QName assocQName) { return copy( sourceNodeRef, targetParentNodeRef, assocTypeQName, assocQName, false); } public void copy(NodeRef sourceNodeRef, NodeRef targetNodeRef) { QName sourceNodeTypeQName = nodeService.getType(sourceNodeRef); QName targetNodeTypeQName = nodeService.getType(targetNodeRef); // Check that the source and destination node are the same type if (!sourceNodeTypeQName.equals(targetNodeTypeQName)) { // Error - can not copy objects that are of different types throw new CopyServiceException("The source and destination node must be the same type."); } Map sourceNodeProperties = nodeService.getProperties(sourceNodeRef); // Get the destinations node's details ChildAssociationRef destinationPrimaryAssocRef = nodeService.getPrimaryParent(targetNodeRef); NodeRef destinationParentNodeRef = destinationPrimaryAssocRef.getParentRef(); QName assocTypeQName = destinationPrimaryAssocRef.getTypeQName(); QName assocQName = destinationPrimaryAssocRef.getQName(); // Get the copy details CopyDetails copyDetails = getCopyDetails( sourceNodeRef, destinationParentNodeRef, targetNodeRef, assocTypeQName, assocQName); // Get callbacks Map callbacks = getCallbacks(copyDetails); // Remove the name property from the source properties to avoid having it copied sourceNodeProperties.remove(ContentModel.PROP_NAME); // Copy copyProperties(copyDetails, targetNodeRef, sourceNodeTypeQName, callbacks); copyAspects(copyDetails, targetNodeRef, Collections.emptySet(), callbacks); copyResidualProperties(copyDetails, targetNodeRef); // invoke the copy complete policy Map copiedNodeRefs = new HashMap(1); copiedNodeRefs.put(sourceNodeRef, targetNodeRef); invokeCopyComplete(sourceNodeRef, targetNodeRef, false, copiedNodeRefs); } public List getCopies(NodeRef nodeRef) { List copies = new ArrayList(); // Do a search to find the origional document ResultSet resultSet = null; try { resultSet = this.searchService.query( nodeRef.getStoreRef(), SearchService.LANGUAGE_LUCENE, "+@\\{http\\://www.alfresco.org/model/content/1.0\\}" + ContentModel.PROP_COPY_REFERENCE.getLocalName() + ":\"" + nodeRef.toString() + "\""); for (NodeRef copy : resultSet.getNodeRefs()) { copies.add(copy); } } finally { if (resultSet != null) { resultSet.close(); } } return copies; } /** * @return Returns null if the node was denied a copy */ private NodeRef copyImpl( NodeRef sourceNodeRef, NodeRef targetParentRef, QName assocTypeQName, QName assocQName, boolean copyChildren, boolean dropName, Map copiesByOriginals, Set copies) { // Build the top-level node's copy details CopyDetails copyDetails = getCopyDetails(sourceNodeRef, targetParentRef, null, assocTypeQName, assocQName); // Get the callbacks that will determine the copy behaviour Map callbacks = getCallbacks(copyDetails); // Check that the primary (type) callback allows copy QName sourceNodeTypeQName = copyDetails.getSourceNodeTypeQName(); CopyBehaviourCallback callback = callbacks.get(sourceNodeTypeQName); if (callback == null) { throw new IllegalStateException("Source node type has no callback: " + sourceNodeTypeQName); } if (!callback.getMustCopy(sourceNodeTypeQName, copyDetails)) { // Denied! return null; } // Recursive copies cannot have name conflicts, therefore copy names verbatim. NodeRef copiedNodeRef = recursiveCopy( copyDetails, copyChildren, dropName, copiesByOriginals, copies, callbacks); return copiedNodeRef; } /** * Recursive copy algorithm * * @param dropName drop the name property when associations don't allow duplicately named children */ private NodeRef recursiveCopy( CopyDetails copyDetails, boolean copyChildren, boolean dropName, Map copiesByOriginal, Set copies, Map callbacks) { NodeRef sourceNodeRef = copyDetails.getSourceNodeRef(); Set sourceNodeAspectQNames = copyDetails.getSourceNodeAspectQNames(); NodeRef targetParentNodeRef = copyDetails.getTargetParentNodeRef(); QName assocTypeQName = copyDetails.getAssocTypeQName(); QName assocQName = copyDetails.getAssocQName(); // Avoid duplicate and cyclic copies if (copies.contains(sourceNodeRef)) { throw new IllegalStateException( "Nested copy prevention has failed: \n" + " " + copyDetails + "\n" + " Copies by original: " + copiesByOriginal); } else if (copiesByOriginal.containsKey(sourceNodeRef)) { throw new IllegalStateException( "Multiple child assocs between same two nodes detected: \n" + " " + copyDetails + "\n" + " Copies by original: " + copiesByOriginal); } // Extract Type Definition QName sourceNodeTypeQName = copyDetails.getSourceNodeTypeQName(); // Does this node get copied at all? // The source node's type-bound behaviour has an effective veto. CopyBehaviourCallback sourceTypeBehaviour = callbacks.get(sourceNodeTypeQName); if (sourceTypeBehaviour == null) { throw new IllegalStateException("Source node type has no callback: " + sourceNodeTypeQName); } if (!sourceTypeBehaviour.getMustCopy(sourceNodeTypeQName, copyDetails)) { // Nothing to do return null; } // Get the type properties to copy Map targetNodeProperties = buildCopyProperties( copyDetails, Collections.singleton(sourceNodeTypeQName), callbacks); // Some aspects are going to be applied automatically. For efficiency, the initial node properties // for these aspects should be provided. Set defaultAspectQNames = getDefaultAspects(sourceNodeTypeQName); Map defaultAspectsProperties = buildCopyProperties( copyDetails, defaultAspectQNames, callbacks); targetNodeProperties.putAll(defaultAspectsProperties); // Drop the name property, if required. This prevents duplicate names and leaves it up to the client // to assign a new name. AssociationDefinition assocDef = dictionaryService.getAssociation(assocTypeQName); if (!assocDef.isChild()) { throw new AlfrescoRuntimeException("Association is not a child association: " + assocTypeQName); } else { ChildAssociationDefinition childAssocDef = (ChildAssociationDefinition) assocDef; if (dropName && !childAssocDef.getDuplicateChildNamesAllowed()) { // duplicate children are not allowed. targetNodeProperties.remove(ContentModel.PROP_NAME); } } // Lastly, make sure the the Node UUID is set correctly; after all, the contract // of the CopyDetails says that the targetNodeRef was already determined. String targetNodeUuid = copyDetails.getTargetNodeRef().getId(); targetNodeProperties.put(ContentModel.PROP_NODE_UUID, targetNodeUuid); // The initial node copy is good to go ChildAssociationRef targetChildAssocRef = this.nodeService.createNode( targetParentNodeRef, assocTypeQName, assocQName, sourceNodeTypeQName, targetNodeProperties); NodeRef targetNodeRef = targetChildAssocRef.getChildRef(); // Save the mapping for later copiesByOriginal.put(sourceNodeRef, targetNodeRef); copies.add(targetNodeRef); // Work out which aspects still need copying. The source aspects less the default aspects // will give this set. Set remainingAspectQNames = new HashSet(sourceNodeAspectQNames); remainingAspectQNames.removeAll(defaultAspectQNames); // Prevent any rules being fired on the new destination node this.ruleService.disableRules(targetNodeRef); try { // Apply the remaining aspects and properties for (QName remainingAspectQName : remainingAspectQNames) { copyProperties(copyDetails, targetNodeRef, remainingAspectQName, callbacks); } // Copy residual properties copyResidualProperties(copyDetails, targetNodeRef); // Apply the copy aspect to the new node Map copyProperties = new HashMap(); copyProperties.put(ContentModel.PROP_COPY_REFERENCE, sourceNodeRef); internalNodeService.addAspect(targetNodeRef, ContentModel.ASPECT_COPIEDFROM, copyProperties); // Copy permissions copyPermissions(sourceNodeRef, targetNodeRef); // We present the recursion option regardless of what the client chooses copyChildren( copyDetails, targetNodeRef, true, // We know that the node has been created copyChildren, copiesByOriginal, copies, callbacks); } finally { this.ruleService.enableRules(targetNodeRef); } return targetNodeRef; } private Set getDefaultAspects(QName sourceNodeTypeQName) { TypeDefinition sourceNodeTypeDef = dictionaryService.getType(sourceNodeTypeQName); if (sourceNodeTypeDef == null) { return Collections.emptySet(); } Set defaultAspectQNames = new HashSet(7); for (AspectDefinition aspectDef : sourceNodeTypeDef.getDefaultAspects()) { defaultAspectQNames.add(aspectDef.getName()); } // Done return defaultAspectQNames; } /** * Constructs the properties to copy that apply to the type and default aspects */ private Map buildCopyProperties( CopyDetails copyDetails, Set classQNames, Map callbacks) { Map sourceNodeProperties = copyDetails.getSourceNodeProperties(); Map copyProperties = new HashMap(sourceNodeProperties.size(), 1.0F); Map scratchProperties = new HashMap(11); // Each defined callback gets a chance to say which properties get copied // Only model-defined properties are considered for (QName classQName : classQNames) { CopyBehaviourCallback callback = callbacks.get(classQName); if (callback == null) { throw new IllegalStateException("Source node class has no callback: " + classQName); } // Ignore if not present or if not scheduled for a copy if (!callback.getMustCopy(classQName, copyDetails)) { continue; } // Get the dictionary definition ClassDefinition classDef = dictionaryService.getClass(classQName); if (classDef == null) { continue; } // Get the defined properties Map propertyDefs = classDef.getProperties(); // Extract these from the source nodes properties and store in a safe (modifiable) map scratchProperties.clear(); for (QName propertyQName : propertyDefs.keySet()) { Serializable value = sourceNodeProperties.get(propertyQName); if (value == null) { continue; } scratchProperties.put(propertyQName, value); } // What does the behaviour do with properties? scratchProperties = callback.getCopyProperties(classQName, copyDetails, scratchProperties); // Add to the final properties copyProperties.putAll(scratchProperties); } // Done return copyProperties; } /** * Invokes the copy complete policy for the node reference provided * * @param sourceNodeRef the source node reference * @param targetNodeRef the destination node reference * @param copiedNodeRefs the map of copied node references */ private void invokeCopyComplete( NodeRef sourceNodeRef, NodeRef targetNodeRef, boolean copyToNewNode, Map copiedNodeRefs) { QName sourceClassRef = internalNodeService.getType(sourceNodeRef); invokeCopyComplete(sourceClassRef, sourceNodeRef, targetNodeRef, copyToNewNode, copiedNodeRefs); // Get the source aspects Set sourceAspects = this.nodeService.getAspects(sourceNodeRef); for (QName sourceAspect : sourceAspects) { invokeCopyComplete(sourceAspect, sourceNodeRef, targetNodeRef, copyToNewNode, copiedNodeRefs); } } private void invokeCopyComplete( QName typeQName, NodeRef sourceNodeRef, NodeRef targetNodeRef, boolean copyToNewNode, Map copiedNodeRefs) { Collection policies = onCopyCompleteDelegate.getList(typeQName); for (CopyServicePolicies.OnCopyCompletePolicy policy : policies) { policy.onCopyComplete(typeQName, sourceNodeRef, targetNodeRef, copyToNewNode, copiedNodeRefs); } } /** * Copies the permissions of the source node reference onto the destination node reference * * @param sourceNodeRef the source node reference * @param destinationNodeRef the destination node reference */ private void copyPermissions(NodeRef sourceNodeRef, NodeRef destinationNodeRef) { if(this.permissionService.hasPermission(sourceNodeRef, PermissionService.READ_PERMISSIONS) == AccessStatus.ALLOWED) { // Get the permission details of the source node reference Set permissions = this.permissionService.getAllSetPermissions(sourceNodeRef); boolean includeInherited = this.permissionService.getInheritParentPermissions(sourceNodeRef); AccessStatus writePermission = permissionService.hasPermission(destinationNodeRef, PermissionService.CHANGE_PERMISSIONS); if (writePermission.equals(AccessStatus.ALLOWED) || this.authenticationService.isCurrentUserTheSystemUser() ) { // Set the permission values on the destination node for (AccessPermission permission : permissions) { if(permission.isSetDirectly()) { this.permissionService.setPermission( destinationNodeRef, permission.getAuthority(), permission.getPermission(), permission.getAccessStatus().equals(AccessStatus.ALLOWED)); } } this.permissionService.setInheritParentPermissions(destinationNodeRef, includeInherited); } } } /** * Gets the copy details. This calls the appropriate policies that have been registered * against the node and aspect types in order to pick-up any type specific copy behaviour. *

* The full {@link NodeService} is used for property retrieval. After this, read permission * can be assumed to have passed on the source node - at least w.r.t. properties and aspects. *

* NOTE: If a target node is not supplied, then one is created in the same store as the * target parent node. This allows behavioural code always know which node will * be copied to, even if the node does not exist. */ private CopyDetails getCopyDetails( NodeRef sourceNodeRef, NodeRef targetParentNodeRef, NodeRef targetNodeRef, QName assocTypeQName, QName assocQName) { // The first call will fail permissions, so there is no point doing permission checks with // the other calls Map sourceNodeProperties = nodeService.getProperties(sourceNodeRef); QName sourceNodeTypeQName = internalNodeService.getType(sourceNodeRef); Set sourceNodeAspectQNames = internalNodeService.getAspects(sourceNodeRef); // Create a target node, if necessary boolean targetNodeIsNew = false; if (targetNodeRef == null) { targetNodeRef = new NodeRef(targetParentNodeRef.getStoreRef(), GUID.generate()); targetNodeIsNew = true; } CopyDetails copyDetails = new CopyDetails( sourceNodeRef, sourceNodeTypeQName, sourceNodeAspectQNames, sourceNodeProperties, targetParentNodeRef, targetNodeRef, targetNodeIsNew, assocTypeQName, assocQName); // Done return copyDetails; } /** * @return Returns a map of all the copy behaviours keyed by type and aspect qualified names */ private Map getCallbacks(CopyDetails copyDetails) { QName sourceNodeTypeQName = copyDetails.getSourceNodeTypeQName(); Map callbacks = new HashMap(11); // Get the type-specific behaviour CopyBehaviourCallback callback = getCallback(sourceNodeTypeQName, copyDetails); callbacks.put(sourceNodeTypeQName, callback); // Get the source aspects for (QName sourceNodeAspectQName : copyDetails.getSourceNodeAspectQNames()) { callback = getCallback(sourceNodeAspectQName, copyDetails); callbacks.put(sourceNodeAspectQName, callback); } return callbacks; } /** * @return Returns the copy callback for the given criteria */ private CopyBehaviourCallback getCallback(QName sourceClassQName, CopyDetails copyDetails) { Collection policies = this.onCopyNodeDelegate.getList(sourceClassQName); ClassDefinition sourceClassDef = dictionaryService.getClass(sourceClassQName); CopyBehaviourCallback callback = null; if (sourceClassDef == null) { // Do nothing as the type is not in the dictionary callback = DoNothingCopyBehaviourCallback.getInstance(); } if (policies.isEmpty()) { // Default behaviour callback = DefaultCopyBehaviourCallback.getInstance(); } else if (policies.size() == 1) { callback = policies.iterator().next().getCopyCallback(sourceClassQName, copyDetails); } else { // There are multiple CompoundCopyBehaviourCallback compoundCallback = new CompoundCopyBehaviourCallback(sourceClassQName); for (CopyServicePolicies.OnCopyNodePolicy policy : policies) { CopyBehaviourCallback nestedCallback = policy.getCopyCallback(sourceClassQName, copyDetails); compoundCallback.addBehaviour(nestedCallback); } callback = compoundCallback; } // Done if (logger.isDebugEnabled()) { logger.debug( "Fetched copy callback: \n" + " Class: " + sourceClassQName + "\n" + " Details: " + copyDetails + "\n" + " Callback: " + callback); } return callback; } /** * Copies the properties for the node type or aspect onto the destination node. */ private void copyProperties( CopyDetails copyDetails, NodeRef targetNodeRef, QName classQName, Map callbacks) { ClassDefinition targetClassDef = dictionaryService.getClass(classQName); if (targetClassDef == null) { return; // Ignore unknown types } // First check if the aspect must be copied at all CopyBehaviourCallback callback = callbacks.get(classQName); if (callback == null) { throw new IllegalStateException("Source node class has no callback: " + classQName); } // Ignore if not present or if not scheduled for a copy if (!callback.getMustCopy(classQName, copyDetails)) { // Do nothing with this return; } // Compile the properties to copy, even if they are empty Map classProperties = buildCopyProperties( copyDetails, Collections.singleton(classQName), callbacks); // We don't need permissions as we've just created the node if (targetClassDef.isAspect()) { internalNodeService.addAspect(targetNodeRef, classQName, classProperties); } else { internalNodeService.addProperties(targetNodeRef, classProperties); } } /** * Copy properties that do not belong to the source node's type or any of the aspects. */ private void copyResidualProperties( CopyDetails copyDetails, NodeRef targetNodeRef) { Map residualProperties = new HashMap(); // Start with the full set residualProperties.putAll(copyDetails.getSourceNodeProperties()); QName sourceNodeTypeQName = copyDetails.getSourceNodeTypeQName(); Set knownClassQNames = new HashSet(13); // We add the default aspects, source-applied aspects and the source node type knownClassQNames.addAll(getDefaultAspects(sourceNodeTypeQName)); knownClassQNames.addAll(copyDetails.getSourceNodeAspectQNames()); knownClassQNames.add(sourceNodeTypeQName); for (QName knownClassQName : knownClassQNames) { ClassDefinition classDef = dictionaryService.getClass(knownClassQName); if (classDef == null) { continue; } // Remove defined properties form the residual list for (QName definedPropQName : classDef.getProperties().keySet()) { residualProperties.remove(definedPropQName); // We've removed them all, so shortcut out if (residualProperties.size() == 0) { break; } } } // Add the residual properties to the node if (residualProperties.size() > 0) { internalNodeService.addProperties(targetNodeRef, residualProperties); } } /** * Copies aspects from the source to the target node. */ private void copyAspects( CopyDetails copyDetails, NodeRef targetNodeRef, Set aspectsToIgnore, Map callbacks) { Set sourceAspectQNames = copyDetails.getSourceNodeAspectQNames(); for (QName aspectQName : sourceAspectQNames) { if (aspectsToIgnore.contains(aspectQName)) { continue; } // Double check that the aspect must be copied at all CopyBehaviourCallback callback = callbacks.get(aspectQName); if (callback == null) { throw new IllegalStateException("Source aspect class has no callback: " + aspectQName); } if (!callback.getMustCopy(aspectQName, copyDetails)) { continue; } copyProperties(copyDetails, targetNodeRef, aspectQName, callbacks); } } /** * @param copyChildren false if the client selected not to recurse */ private void copyChildren( CopyDetails copyDetails, NodeRef targetNodeRef, boolean targetNodeIsNew, boolean copyChildren, Map copiesByOriginals, Set copies, Map callbacks) { QName sourceNodeTypeQName = copyDetails.getSourceNodeTypeQName(); Set sourceNodeAspectQNames = copyDetails.getSourceNodeAspectQNames(); // First check associations on the type copyChildren( copyDetails, sourceNodeTypeQName, targetNodeRef, targetNodeIsNew, copyChildren, copiesByOriginals, copies, callbacks); // Check associations for the aspects for (QName aspectQName : sourceNodeAspectQNames) { AspectDefinition aspectDef = dictionaryService.getAspect(aspectQName); if (aspectDef == null) { continue; } copyChildren( copyDetails, aspectQName, targetNodeRef, targetNodeIsNew, copyChildren, copiesByOriginals, copies, callbacks); } } /** * @param copyChildren false if the client selected not to recurse */ private void copyChildren( CopyDetails copyDetails, QName classQName, NodeRef targetNodeRef, boolean targetNodeIsNew, boolean copyChildren, Map copiesByOriginals, Set copies, Map callbacks) { NodeRef sourceNodeRef = copyDetails.getSourceNodeRef(); ClassDefinition classDef = dictionaryService.getClass(classQName); if (classDef == null) { // Ignore missing types return; } // Check the behaviour CopyBehaviourCallback callback = callbacks.get(classQName); if (callback == null) { throw new IllegalStateException("Source node class has no callback: " + classQName); } for (Map.Entry entry : classDef.getChildAssociations().entrySet()) { QName assocTypeQName = entry.getKey(); ChildAssociationDefinition assocDef = entry.getValue(); if (!assocDef.isChild()) { continue; // Ignore non-child assocs } // Get the child associations List childAssocRefs = nodeService.getChildAssocs( sourceNodeRef, assocTypeQName, RegexQNamePattern.MATCH_ALL); for (ChildAssociationRef childAssocRef : childAssocRefs) { NodeRef childNodeRef = childAssocRef.getChildRef(); QName assocQName = childAssocRef.getQName(); CopyChildAssociationDetails childAssocCopyDetails = new CopyChildAssociationDetails( childAssocRef, targetNodeRef, targetNodeIsNew, copyChildren); // Handle nested copies if (copies.contains(childNodeRef)) { // The node was already copied i.e. we are seeing a copy produced by some earlier // copy process. // The first way this can occur is if a hierarchy is copied into some lower part // of the hierarchy. We avoid the copied part. // The other way this could occur is if there are multiple assocs between a // parent and child. Calls to this method are scoped by class, so the newly-created // node will not be found because it will have been created using a different assoc // type. // A final edge case is where there are multiple assocs between parent and child // of the same type. This is ignorable. continue; } // Check the callbacks ChildAssocCopyAction childAssocCopyAction = callback.getChildAssociationCopyAction( classQName, copyDetails, childAssocCopyDetails); switch (childAssocCopyAction) { case IGNORE: break; case COPY_ASSOC: nodeService.addChild(targetNodeRef, childNodeRef, assocTypeQName, assocQName); break; case COPY_CHILD: // Handle potentially cyclic relationships if (copiesByOriginals.containsKey(childNodeRef)) { // This is either a cyclic relationship or there are multiple different // types of associations between the same parent and child. // Just hook the child up with the association. nodeService.addChild(targetNodeRef, childNodeRef, assocTypeQName, assocQName); } else { // Find out whether to force a recursion ChildAssocRecurseAction childAssocRecurseAction = callback.getChildAssociationRecurseAction( classQName, copyDetails, childAssocCopyDetails); switch (childAssocRecurseAction) { case RESPECT_RECURSE_FLAG: // Keep child copy flag the same break; case FORCE_RECURSE: // Force recurse copyChildren = true; break; default: throw new IllegalStateException("Unrecognized enum"); } // This copy may fail silently copyImpl( childNodeRef, targetNodeRef, assocTypeQName, assocQName, copyChildren, false, // Keep child names for deep copies copiesByOriginals, copies); } break; default: throw new IllegalStateException("Unrecognized enum"); } } } } /** * Callback behaviour retrieval for the 'copiedFrom' aspect. * * @return Returns {@link DoNothingCopyBehaviourCallback} always */ public CopyBehaviourCallback getCallbackForCopiedFromAspect(QName classRef, CopyDetails copyDetails) { return DoNothingCopyBehaviourCallback.getInstance(); } /** * Callback behaviour retrieval for {@link ContentModel#TYPE_FOLDER} aspect. * * @return Returns {@link FolderTypeCopyBehaviourCallback} */ public CopyBehaviourCallback getCallbackForFolderType(QName classRef, CopyDetails copyDetails) { return FolderTypeCopyBehaviourCallback.INSTANCE; } /** * cm:folder behaviour * * @author Derek Hulley * @since 3.2 */ private static class FolderTypeCopyBehaviourCallback extends DefaultCopyBehaviourCallback { private static final CopyBehaviourCallback INSTANCE = new FolderTypeCopyBehaviourCallback(); /** * Respects the copyChildren flag. Child nodes are copied fully if the association * is primary otherwise secondary associations are duplicated. */ @Override public ChildAssocCopyAction getChildAssociationCopyAction( QName classQName, CopyDetails copyDetails, CopyChildAssociationDetails childAssocCopyDetails) { ChildAssociationRef childAssocRef = childAssocCopyDetails.getChildAssocRef(); boolean copyChildren = childAssocCopyDetails.isCopyChildren(); if (childAssocRef.getTypeQName().equals(ContentModel.ASSOC_CONTAINS)) { if (!copyChildren) { return ChildAssocCopyAction.IGNORE; } if (childAssocRef.isPrimary()) { return ChildAssocCopyAction.COPY_CHILD; } else { return ChildAssocCopyAction.COPY_ASSOC; } } else { throw new IllegalStateException( "Behaviour should have been invoked: \n" + " Aspect: " + this.getClass().getName() + "\n" + " Assoc: " + childAssocRef + "\n" + " " + copyDetails); } } } /** * Callback behaviour retrieval for the 'ownable' aspect. * * @return Returns {@link DoNothingCopyBehaviourCallback} always */ public CopyBehaviourCallback getCallbackForOwnableAspect(QName classRef, CopyDetails copyDetails) { return DoNothingCopyBehaviourCallback.getInstance(); } /** * Callback behaviour retrieval for the 'author' aspect. * * @return Returns {@link DoNothingCopyBehaviourCallback} always */ public CopyBehaviourCallback getCallbackForAuthorAspect(QName classRef, CopyDetails copyDetails) { return DoNothingCopyBehaviourCallback.getInstance(); } }