com.surevine.alfresco.repo.action.SafeMoveCopyServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.surevine.alfresco.repo.action.SafeMoveCopyServiceImpl.java

Source

/*
 * Copyright (C) 2008-2010 Surevine Limited.
 *   
 * Although intended for deployment and use alongside Alfresco this module should
 * be considered 'Not a Contribution' as defined in Alfresco'sstandard contribution agreement, see
 * http://www.alfresco.org/resource/AlfrescoContributionAgreementv2.pdf
 * 
 * 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.
*/
package com.surevine.alfresco.repo.action;

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.model.ContentModel;
import org.alfresco.repo.action.ActionServiceImpl;
import org.alfresco.repo.copy.CompoundCopyBehaviourCallback;
import org.alfresco.repo.copy.CopyBehaviourCallback;
import org.alfresco.repo.copy.CopyBehaviourCallback.AssocCopySourceAction;
import org.alfresco.repo.copy.CopyBehaviourCallback.AssocCopyTargetAction;
import org.alfresco.repo.copy.CopyBehaviourCallback.ChildAssocRecurseAction;
import org.alfresco.repo.copy.CopyBehaviourCallback.CopyAssociationDetails;
import org.alfresco.repo.copy.CopyBehaviourCallback.CopyChildAssociationDetails;
import org.alfresco.repo.copy.CopyDetails;
import org.alfresco.repo.copy.CopyServicePolicies;
import org.alfresco.repo.copy.DefaultCopyBehaviourCallback;
import org.alfresco.repo.copy.DoNothingCopyBehaviourCallback;
import org.alfresco.repo.policy.ClassPolicyDelegate;
import org.alfresco.repo.policy.JavaBehaviour;
import org.alfresco.repo.policy.PolicyComponent;
import org.alfresco.repo.transaction.TransactionalResourceHelper;
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.AssociationRef;
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.PermissionService;
import org.alfresco.service.cmr.security.PublicServiceAccessService;
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.Pair;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.extensions.surf.util.I18NUtil;
import org.springframework.extensions.surf.util.ParameterCheck;

/**
 * Service implementation of copy operations.
 * 
 * Duplicated and altered by Surevine to facilitate copying of item discussion child nodes (for use by safe move service)
 * 
 * @author Roy Wetherall
 * @author Derek Hulley
 * @author Jonny Heavey
 */
public class SafeMoveCopyServiceImpl 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;

    private PermissionService permissionService;

    private PublicServiceAccessService publicServiceAccessService;

    /** Policy delegates */
    private ClassPolicyDelegate<CopyServicePolicies.OnCopyNodePolicy> onCopyNodeDelegate;
    private ClassPolicyDelegate<CopyServicePolicies.OnCopyCompletePolicy> onCopyCompleteDelegate;
    private ClassPolicyDelegate<CopyServicePolicies.BeforeCopyPolicy> beforeCopyDelegate;

    /**
     * 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;
    }

    /**
     * @param permissionService the permissionService to set
     */
    public void setPermissionService(PermissionService permissionService) {
        this.permissionService = permissionService;
    }

    /**
     * @param publicServiceAccessService the publicServiceAccessService to set
     */
    public void setPublicServiceAccessService(PublicServiceAccessService publicServiceAccessService) {
        this.publicServiceAccessService = publicServiceAccessService;
    }

    /**
     * Initialise method
     */
    public void init() {
        // Register the policies
        onCopyNodeDelegate = policyComponent.registerClassPolicy(CopyServicePolicies.OnCopyNodePolicy.class);
        onCopyCompleteDelegate = policyComponent
                .registerClassPolicy(CopyServicePolicies.OnCopyCompletePolicy.class);
        beforeCopyDelegate = policyComponent.registerClassPolicy(CopyServicePolicies.BeforeCopyPolicy.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"));
    }

    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) {
            // Error - since at the moment we do not support cross store copying
            throw new UnsupportedOperationException("Copying nodes across stores is not currently supported.");
        }

        // Clear out any record of copied associations
        TransactionalResourceHelper.getList(KEY_POST_COPY_ASSOCS).clear();

        // Keep track of copied children
        Map<NodeRef, NodeRef> copiesByOriginals = new HashMap<NodeRef, NodeRef>(17);
        Set<NodeRef> copies = new HashSet<NodeRef>(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);
        }

        // Copy an associations that were left until now
        copyPendingAssociations(copiesByOriginals);

        // Foreach of the newly created copies call the copy complete policy
        for (Map.Entry<NodeRef, NodeRef> 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);
    }

    /**
     * {@inheritDoc}
     * 
     * Defer to the standard implementation with copyChildren set to 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.");
        }

        // 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<QName, CopyBehaviourCallback> callbacks = getCallbacks(copyDetails);

        // invoke the before copy policy
        invokeBeforeCopy(sourceNodeRef, targetNodeRef);

        // Clear out any record of copied associations
        TransactionalResourceHelper.getList(KEY_POST_COPY_ASSOCS).clear();

        // Copy 
        copyProperties(copyDetails, targetNodeRef, sourceNodeTypeQName, callbacks);
        copyAspects(copyDetails, targetNodeRef, Collections.<QName>emptySet(), callbacks);
        copyResidualProperties(copyDetails, targetNodeRef);

        Map<NodeRef, NodeRef> copiedNodeRefs = new HashMap<NodeRef, NodeRef>(1);
        copiedNodeRefs.put(sourceNodeRef, targetNodeRef);

        Set<NodeRef> copies = new HashSet<NodeRef>(5);
        copyChildren(copyDetails, targetNodeRef, false, // We know that the node has been created
                true, copiedNodeRefs, copies, callbacks);

        // Copy an associations that were left until now
        copyPendingAssociations(copiedNodeRefs);
        // invoke the copy complete policy
        invokeCopyComplete(sourceNodeRef, targetNodeRef, false, copiedNodeRefs);
    }

    public List<NodeRef> getCopies(NodeRef nodeRef) {
        List<NodeRef> copies = new ArrayList<NodeRef>();

        // 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 <tt>null</tt> if the node was denied a copy
     */
    private NodeRef copyImpl(NodeRef sourceNodeRef, NodeRef targetParentRef, QName assocTypeQName, QName assocQName,
            boolean copyChildren, boolean dropName, Map<NodeRef, NodeRef> copiesByOriginals, Set<NodeRef> 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<QName, CopyBehaviourCallback> 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<NodeRef, NodeRef> copiesByOriginal, Set<NodeRef> copies,
            Map<QName, CopyBehaviourCallback> callbacks) {
        NodeRef sourceNodeRef = copyDetails.getSourceNodeRef();
        Set<QName> 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<QName, Serializable> 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<QName> defaultAspectQNames = getDefaultAspects(sourceNodeTypeQName);
        Map<QName, Serializable> 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 copyTarget = targetChildAssocRef.getChildRef();
        // Save the mapping for later
        copiesByOriginal.put(sourceNodeRef, copyTarget);
        copies.add(copyTarget);

        // We now have a node, so fire the BeforeCopyPolicy
        invokeBeforeCopy(sourceNodeRef, copyTarget);

        // Work out which aspects still need copying.  The source aspects less the default aspects
        // will give this set.
        Set<QName> remainingAspectQNames = new HashSet<QName>(sourceNodeAspectQNames);
        remainingAspectQNames.removeAll(defaultAspectQNames);

        // Prevent any rules being fired on the new destination node
        this.ruleService.disableRules(copyTarget);
        try {
            // Apply the remaining aspects and properties
            for (QName remainingAspectQName : remainingAspectQNames) {
                copyProperties(copyDetails, copyTarget, remainingAspectQName, callbacks);
            }

            // Copy residual properties
            copyResidualProperties(copyDetails, copyTarget);

            //  Apply the copy aspect to the new node   
            Map<QName, Serializable> copyProperties = new HashMap<QName, Serializable>();
            copyProperties.put(ContentModel.PROP_COPY_REFERENCE, sourceNodeRef);
            internalNodeService.addAspect(copyTarget, ContentModel.ASPECT_COPIEDFROM, copyProperties);

            // Copy permissions
            copyPermissions(sourceNodeRef, copyTarget);

            // We present the recursion option regardless of what the client chooses
            copyChildren(copyDetails, copyTarget, true, // We know that the node has been created
                    copyChildren, copiesByOriginal, copies, callbacks);
        } finally {
            this.ruleService.enableRules(copyTarget);
        }

        return copyTarget;
    }

    private Set<QName> getDefaultAspects(QName sourceNodeTypeQName) {
        TypeDefinition sourceNodeTypeDef = dictionaryService.getType(sourceNodeTypeQName);
        if (sourceNodeTypeDef == null) {
            return Collections.emptySet();
        }
        Set<QName> defaultAspectQNames = new HashSet<QName>(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<QName, Serializable> buildCopyProperties(CopyDetails copyDetails, Set<QName> classQNames,
            Map<QName, CopyBehaviourCallback> callbacks) {
        Map<QName, Serializable> sourceNodeProperties = copyDetails.getSourceNodeProperties();
        Map<QName, Serializable> copyProperties = new HashMap<QName, Serializable>(sourceNodeProperties.size(),
                1.0F);
        Map<QName, Serializable> scratchProperties = new HashMap<QName, Serializable>(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<QName, PropertyDefinition> 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?
            Map<QName, Serializable> propsToCopy = callback.getCopyProperties(classQName, copyDetails,
                    scratchProperties);

            // Add to the final properties
            copyProperties.putAll(propsToCopy);
        }
        // Done
        return copyProperties;
    }

    /**
     * Invokes the before copy policy for the node reference provided
     * 
     * @param sourceNodeRef         the source node reference
     * @param targetNodeRef         the destination node reference
     */
    private void invokeBeforeCopy(NodeRef sourceNodeRef, NodeRef targetNodeRef) {
        // By Type
        QName targetClassRef = internalNodeService.getType(targetNodeRef);
        invokeBeforeCopy(targetClassRef, sourceNodeRef, targetNodeRef);

        // And by Aspect
        Set<QName> targetAspects = this.nodeService.getAspects(targetNodeRef);
        for (QName targetAspect : targetAspects) {
            invokeBeforeCopy(targetAspect, sourceNodeRef, targetNodeRef);
        }
    }

    private void invokeBeforeCopy(QName typeQName, NodeRef sourceNodeRef, NodeRef targetNodeRef) {
        Collection<CopyServicePolicies.BeforeCopyPolicy> policies = beforeCopyDelegate.getList(typeQName);
        for (CopyServicePolicies.BeforeCopyPolicy policy : policies) {
            policy.beforeCopy(typeQName, sourceNodeRef, targetNodeRef);
        }
    }

    /**
     * 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<NodeRef, NodeRef> copiedNodeRefs) {
        // By Type
        QName sourceClassRef = internalNodeService.getType(sourceNodeRef);
        invokeCopyComplete(sourceClassRef, sourceNodeRef, targetNodeRef, copyToNewNode, copiedNodeRefs);

        // Get the source aspects
        Set<QName> 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<NodeRef, NodeRef> copiedNodeRefs) {
        Collection<CopyServicePolicies.OnCopyCompletePolicy> policies = onCopyCompleteDelegate.getList(typeQName);
        for (CopyServicePolicies.OnCopyCompletePolicy policy : policies) {
            policy.onCopyComplete(typeQName, sourceNodeRef, targetNodeRef, copyToNewNode, copiedNodeRefs);
        }
    }

    /**
     * Copy any remaining associations that could not be copied or ignored during the copy process.
     * See <a href=http://issues.alfresco.com/jira/browse/ALF-958>ALF-958: Target associations aren't copied</a>.
     */
    private void copyPendingAssociations(Map<NodeRef, NodeRef> copiedNodeRefs) {
        // Prepare storage for post-copy association handling
        List<Pair<AssociationRef, AssocCopyTargetAction>> postCopyAssocs = TransactionalResourceHelper
                .getList(KEY_POST_COPY_ASSOCS);
        for (Pair<AssociationRef, AssocCopyTargetAction> pair : postCopyAssocs) {
            AssociationRef assocRef = pair.getFirst();
            AssocCopyTargetAction action = pair.getSecond();
            // Was the original target copied?
            NodeRef newSourceForAssoc = copiedNodeRefs.get(assocRef.getSourceRef());
            if (newSourceForAssoc == null) {
                // Developer #fail
                throw new IllegalStateException("Post-copy association has a source that was NOT copied.");
            }
            NodeRef oldTargetForAssoc = assocRef.getTargetRef();
            NodeRef newTargetForAssoc = copiedNodeRefs.get(oldTargetForAssoc); // May be null
            QName assocTypeQName = assocRef.getTypeQName();
            switch (action) {
            case USE_ORIGINAL_TARGET:
                internalNodeService.createAssociation(newSourceForAssoc, oldTargetForAssoc, assocTypeQName);
                break;
            case USE_COPIED_TARGET:
                // Do nothing if the target was not copied
                if (newTargetForAssoc != null) {
                    internalNodeService.createAssociation(newSourceForAssoc, newTargetForAssoc, assocTypeQName);
                }
                break;
            case USE_COPIED_OTHERWISE_ORIGINAL_TARGET:
                if (newTargetForAssoc == null) {
                    internalNodeService.createAssociation(newSourceForAssoc, oldTargetForAssoc, assocTypeQName);
                } else {
                    internalNodeService.createAssociation(newSourceForAssoc, newTargetForAssoc, assocTypeQName);
                }
                break;
            default:
                throw new IllegalStateException("Unknown association action: " + action);
            }
        }

    }

    /**
     * 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(final NodeRef sourceNodeRef, final NodeRef destinationNodeRef) {
        if ((publicServiceAccessService.hasAccess("PermissionService", "getAllSetPermissions",
                sourceNodeRef) == AccessStatus.ALLOWED)
                && (publicServiceAccessService.hasAccess("PermissionService", "getInheritParentPermissions",
                        sourceNodeRef) == AccessStatus.ALLOWED)) {
            // Get the permission details of the source node reference
            Set<AccessPermission> permissions = permissionService.getAllSetPermissions(sourceNodeRef);
            boolean includeInherited = permissionService.getInheritParentPermissions(sourceNodeRef);

            if ((publicServiceAccessService.hasAccess("PermissionService", "setPermission", destinationNodeRef,
                    "dummyAuth", "dummyPermission", true) == AccessStatus.ALLOWED)
                    && (publicServiceAccessService.hasAccess("PermissionService", "setInheritParentPermissions",
                            destinationNodeRef, includeInherited) == AccessStatus.ALLOWED)) {
                // Set the permission values on the destination node        
                for (AccessPermission permission : permissions) {
                    if (permission.isSetDirectly()) {
                        permissionService.setPermission(destinationNodeRef, permission.getAuthority(),
                                permission.getPermission(),
                                permission.getAccessStatus().equals(AccessStatus.ALLOWED));
                    }
                }
                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.
     * <p>
     * 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.
     * <p>
     * <b>NOTE:</b> 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
        QName sourceNodeTypeQName = nodeService.getType(sourceNodeRef);
        // ALF-730: MLText is not fully carried during cut-paste or copy-paste
        //          Use the internalNodeService to fetch the properties.  It should be mlAwareNodeService.
        Map<QName, Serializable> sourceNodeProperties = internalNodeService.getProperties(sourceNodeRef);
        Set<QName> 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<QName, CopyBehaviourCallback> getCallbacks(CopyDetails copyDetails) {
        QName sourceNodeTypeQName = copyDetails.getSourceNodeTypeQName();

        Map<QName, CopyBehaviourCallback> callbacks = new HashMap<QName, CopyBehaviourCallback>(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<CopyServicePolicies.OnCopyNodePolicy> 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<QName, CopyBehaviourCallback> 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<QName, Serializable> 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<QName, Serializable> residualProperties = new HashMap<QName, Serializable>();
        // Start with the full set
        residualProperties.putAll(copyDetails.getSourceNodeProperties());

        QName sourceNodeTypeQName = copyDetails.getSourceNodeTypeQName();
        Set<QName> knownClassQNames = new HashSet<QName>(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<QName> aspectsToIgnore,
            Map<QName, CopyBehaviourCallback> callbacks) {
        Set<QName> 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              <tt>false</tt> if the client selected not to recurse
     */
    private void copyChildren(CopyDetails copyDetails, NodeRef copyTarget, boolean copyTargetIsNew,
            boolean copyChildren, Map<NodeRef, NodeRef> copiesByOriginals, Set<NodeRef> copies,
            Map<QName, CopyBehaviourCallback> callbacks) {
        QName sourceNodeTypeQName = copyDetails.getSourceNodeTypeQName();
        Set<QName> sourceNodeAspectQNames = copyDetails.getSourceNodeAspectQNames();
        // First check associations on the type
        copyChildren(copyDetails, sourceNodeTypeQName, copyTarget, copyTargetIsNew, 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, copyTarget, copyTargetIsNew, copyChildren, copiesByOriginals,
                    copies, callbacks);
        }
    }

    private static final String KEY_POST_COPY_ASSOCS = "CopyServiceImpl.postCopyAssocs";

    /**
     * @param copyChildren              <tt>false</tt> if the client selected not to recurse
     */
    private void copyChildren(CopyDetails copyDetails, QName classQName, NodeRef copyTarget,
            boolean copyTargetIsNew, boolean copyChildren, Map<NodeRef, NodeRef> copiesByOriginals,
            Set<NodeRef> copies, Map<QName, CopyBehaviourCallback> 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);
        }

        // Prepare storage for post-copy association handling
        List<Pair<AssociationRef, AssocCopyTargetAction>> postCopyAssocs = TransactionalResourceHelper
                .getList(KEY_POST_COPY_ASSOCS);

        // Handle peer associations.
        for (Map.Entry<QName, AssociationDefinition> entry : classDef.getAssociations().entrySet()) {
            QName assocTypeQName = entry.getKey();
            AssociationDefinition assocDef = entry.getValue();
            if (assocDef.isChild()) {
                continue; // Ignore child assocs
            }
            boolean haveRemovedFromCopyTarget = false;
            // Get the associations
            List<AssociationRef> assocRefs = nodeService.getTargetAssocs(sourceNodeRef, assocTypeQName);
            for (AssociationRef assocRef : assocRefs) {
                // Get the copy action for the association instance
                CopyAssociationDetails assocCopyDetails = new CopyAssociationDetails(assocRef, copyTarget,
                        copyTargetIsNew);
                Pair<AssocCopySourceAction, AssocCopyTargetAction> assocCopyAction = callback
                        .getAssociationCopyAction(classQName, copyDetails, assocCopyDetails);

                // Consider the source side first
                switch (assocCopyAction.getFirst()) {
                case IGNORE:
                    continue; // Do nothing
                case COPY_REMOVE_EXISTING:
                    if (!copyTargetIsNew && !haveRemovedFromCopyTarget) {
                        // Only do this if we are copying over an existing node and we have NOT
                        // already cleaned up for this association type
                        haveRemovedFromCopyTarget = true;
                        for (AssociationRef assocToRemoveRef : internalNodeService.getTargetAssocs(copyTarget,
                                assocTypeQName)) {
                            internalNodeService.removeAssociation(assocToRemoveRef.getSourceRef(),
                                    assocToRemoveRef.getTargetRef(), assocTypeQName);
                        }
                    }
                    // Fall through to copy
                case COPY:
                    // Record the type of target behaviour that is expected
                    switch (assocCopyAction.getSecond()) {
                    case USE_ORIGINAL_TARGET:
                    case USE_COPIED_TARGET:
                    case USE_COPIED_OTHERWISE_ORIGINAL_TARGET:
                        // Have to save for later to see if the target node is copied, too
                        postCopyAssocs.add(new Pair<AssociationRef, AssocCopyTargetAction>(assocRef,
                                assocCopyAction.getSecond()));
                        break;
                    default:
                        throw new IllegalStateException(
                                "Unknown association target copy action: " + assocCopyAction);
                    }
                    break;
                default:
                    throw new IllegalStateException("Unknown association source copy action: " + assocCopyAction);
                }
            }
        }

        // Handle child associations.  These need special attention due to their recursive nature.
        for (Map.Entry<QName, ChildAssociationDefinition> childEntry : classDef.getChildAssociations().entrySet()) {
            QName childAssocTypeQName = childEntry.getKey();
            ChildAssociationDefinition childAssocDef = childEntry.getValue();
            if (!childAssocDef.isChild()) {
                continue; // Ignore non-child assocs
            }
            // Get the child associations
            List<ChildAssociationRef> childAssocRefs = nodeService.getChildAssocs(sourceNodeRef,
                    childAssocTypeQName, RegexQNamePattern.MATCH_ALL);
            for (ChildAssociationRef childAssocRef : childAssocRefs) {
                NodeRef childNodeRef = childAssocRef.getChildRef();
                QName assocQName = childAssocRef.getQName();

                CopyChildAssociationDetails childAssocCopyDetails = new CopyChildAssociationDetails(childAssocRef,
                        copyTarget, copyTargetIsNew, 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;
                }

                // 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(copyTarget, childNodeRef, childAssocTypeQName, 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, copyTarget, childAssocTypeQName, assocQName, copyChildren, false, // Keep child names for deep copies
                            copiesByOriginals, copies);
                }
            }
        }
    }

    /**
     * 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;
    }

    /**
     * <b>cm:folder</b> behaviour
     * 
     * @author Derek Hulley
     * @since 3.2
     */
    private static class FolderTypeCopyBehaviourCallback extends DefaultCopyBehaviourCallback {
        private static final CopyBehaviourCallback INSTANCE = new FolderTypeCopyBehaviourCallback();

        /**
         * Respects the <code>copyChildren</code> 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();
    }
}