org.alfresco.repo.transfer.RepoPrimaryManifestProcessorImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.repo.transfer.RepoPrimaryManifestProcessorImpl.java

Source

/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */

package org.alfresco.repo.transfer;

import java.io.File;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
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.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.repo.transfer.CorrespondingNodeResolver.ResolvedParentChildPair;
import org.alfresco.repo.transfer.manifest.ManifestAccessControl;
import org.alfresco.repo.transfer.manifest.ManifestCategory;
import org.alfresco.repo.transfer.manifest.ManifestPermission;
import org.alfresco.repo.transfer.manifest.TransferManifestDeletedNode;
import org.alfresco.repo.transfer.manifest.TransferManifestHeader;
import org.alfresco.repo.transfer.manifest.TransferManifestNode;
import org.alfresco.repo.transfer.manifest.TransferManifestNormalNode;
import org.alfresco.service.cmr.dictionary.AspectDefinition;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
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.lock.LockType;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.Path;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.search.CategoryService;
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.tagging.TaggingService;
import org.alfresco.service.cmr.transfer.TransferReceiver;
import org.alfresco.service.namespace.QName;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * @author brian
 * 
 * The primary manifest processor is responsible for the first parsing the snapshot 
 * file and writing nodes into the receiving repository.
 * 
 * New nodes may be written into a "temporary" space if their primary parent node 
 * has not yet been transferred. 
 */
public class RepoPrimaryManifestProcessorImpl extends AbstractManifestProcessorBase {
    private static final Log log = LogFactory.getLog(RepoPrimaryManifestProcessorImpl.class);

    private static final String MSG_NO_PRIMARY_PARENT_SUPPLIED = "transfer_service.receiver.no_primary_parent_supplied";
    private static final String MSG_ORPHANS_EXIST = "transfer_service.receiver.orphans_exist";
    private static final String MSG_REFERENCED_CONTENT_FILE_MISSING = "transfer_service.receiver.content_file_missing";

    protected static final Set<QName> DEFAULT_LOCAL_PROPERTIES = new HashSet<QName>();

    static {
        DEFAULT_LOCAL_PROPERTIES.add(ContentModel.PROP_STORE_IDENTIFIER);
        DEFAULT_LOCAL_PROPERTIES.add(ContentModel.PROP_STORE_NAME);
        DEFAULT_LOCAL_PROPERTIES.add(ContentModel.PROP_STORE_PROTOCOL);
        DEFAULT_LOCAL_PROPERTIES.add(ContentModel.PROP_NODE_DBID);
        DEFAULT_LOCAL_PROPERTIES.add(ContentModel.PROP_NODE_REF);
        DEFAULT_LOCAL_PROPERTIES.add(ContentModel.PROP_NODE_UUID);
    }

    private NodeService nodeService;
    private PermissionService permissionService;
    private ContentService contentService;
    private DictionaryService dictionaryService;
    private CorrespondingNodeResolver nodeResolver;
    private AlienProcessor alienProcessor;
    private SearchService searchService;
    private CategoryService categoryService;
    private TaggingService taggingService;

    // State within this class
    /**
     * The header of the manifest
     */
    TransferManifestHeader header;

    /**
     * The list of orphans, during processing orphans are added and removed from this list.
     * If at the end of processing there are still orphans then an exception will be thrown.
     */
    private Map<NodeRef, List<ChildAssociationRef>> orphans = new HashMap<NodeRef, List<ChildAssociationRef>>(89);

    /**
     * node ref mapping from source to destination categories
     */
    private Map<NodeRef, NodeRef> categoryMap = new HashMap<NodeRef, NodeRef>();

    /**
     * @param receiver TransferReceiver
     * @param transferId String
     */
    public RepoPrimaryManifestProcessorImpl(TransferReceiver receiver, String transferId) {
        super(receiver, transferId);
    }

    /*
     * (non-Javadoc)
     * 
     * @seeorg.alfresco.repo.transfer.manifest.TransferManifestProcessor# endTransferManifest()
     */
    protected void endManifest() {
        if (!orphans.isEmpty()) {
            error(MSG_ORPHANS_EXIST);
        }
    }

    /**
     * 
     */
    protected void processNode(TransferManifestDeletedNode node) {
        // This is a deleted node. First we need to check whether it has already been deleted in this repo
        // too by looking in the local archive store. If we find it then we need not do anything.
        // If we can't find it in our archive store then we'll see if we can find a corresponding node in the
        // store in which its old parent lives.
        // If we can find a corresponding node then we'll delete it.
        // If we can't find a corresponding node then we'll do nothing.
        logComment("Primary Processing incoming deleted node: " + node.getNodeRef());

        ChildAssociationRef origPrimaryParent = node.getPrimaryParentAssoc();
        NodeRef origNodeRef = new NodeRef(origPrimaryParent.getParentRef().getStoreRef(),
                node.getNodeRef().getId());

        CorrespondingNodeResolver.ResolvedParentChildPair resolvedNodes = nodeResolver
                .resolveCorrespondingNode(origNodeRef, origPrimaryParent, node.getParentPath());

        // Does a corresponding node exist in this repo?
        if (resolvedNodes.resolvedChild != null) {
            NodeRef exNode = resolvedNodes.resolvedChild;
            // Yes, it does. Delete it.
            if (log.isDebugEnabled()) {
                log.debug("Incoming deleted noderef " + node.getNodeRef()
                        + " has been resolved to existing local noderef " + exNode + "  - deleting");
            }

            logDeleted(node.getNodeRef(), exNode, nodeService.getPath(exNode).toString());
            logSummaryDeleted(node.getNodeRef(), exNode, nodeService.getPath(exNode).toString());

            delete(node, exNode);
        } else {
            logComment("Unable to find corresponding node for incoming deleted node: " + node.getNodeRef());
            if (log.isDebugEnabled()) {
                log.debug("Incoming deleted noderef has no corresponding local noderef: " + node.getNodeRef()
                        + "  - ignoring");
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @seeorg.alfresco.repo.transfer.manifest.TransferManifestProcessor#
     * processTransferManifestNode(org.alfresco.repo.transfer .manifest.TransferManifestNode)
     */
    protected void processNode(TransferManifestNormalNode node) {
        if (log.isDebugEnabled()) {
            log.debug("Processing node with incoming noderef of " + node.getNodeRef());
        }
        logComment("Primary Processing incoming node: " + node.getNodeRef() + " --  Source path = "
                + node.getParentPath() + "/" + node.getPrimaryParentAssoc().getQName());

        ChildAssociationRef primaryParentAssoc = node.getPrimaryParentAssoc();
        if (primaryParentAssoc == null) {
            error(node, MSG_NO_PRIMARY_PARENT_SUPPLIED);
        }

        CorrespondingNodeResolver.ResolvedParentChildPair resolvedNodes = nodeResolver
                .resolveCorrespondingNode(node.getNodeRef(), primaryParentAssoc, node.getParentPath());

        // Does a corresponding node exist in this repo?
        if (resolvedNodes.resolvedChild != null) {
            if (log.isTraceEnabled()) {
                log.trace("REPO_PRIMARY_MANIFEST_PROCESSOR - node DOES exist!");
                logInvasionHierarchy(resolvedNodes.resolvedParent, resolvedNodes.resolvedChild, nodeService, log);
            }

            // Yes, the corresponding node does exist. Update it.
            if (log.isDebugEnabled()) {
                log.debug("Incoming noderef " + node.getNodeRef() + " has been resolved to existing local noderef "
                        + resolvedNodes.resolvedChild);
            }
            update(node, resolvedNodes, primaryParentAssoc);

            if (log.isTraceEnabled()) {
                log.trace("^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^");
            }
        } else {
            // No, there is no corresponding node. 
            NodeRef archiveNodeRef = new NodeRef(StoreRef.STORE_REF_ARCHIVE_SPACESSTORE, node.getNodeRef().getId());
            if (nodeService.exists(archiveNodeRef)) {
                // We have found a node in the archive store that has the same
                // UUID as the one that we've been sent.    If it remains it may cause problems later on
                // We delete from the archive store and treat the new node as a create.
                if (log.isInfoEnabled()) {
                    log.info("Located an archived node with UUID matching transferred node: " + archiveNodeRef);
                    log.info("Attempting to restore " + archiveNodeRef);
                }
                logComment("Delete node from archive: " + archiveNodeRef);
                nodeService.deleteNode(archiveNodeRef);
            }

            if (log.isDebugEnabled()) {
                log.debug("Incoming noderef has no corresponding local noderef: " + node.getNodeRef());
            }

            if (log.isTraceEnabled()) {
                log.trace("REPO_PRIMARY_MANIFEST_PROCESSOR - node DOES NOT esist yet! Name: '"
                        + node.getProperties().get(ContentModel.PROP_NAME) + "', parentPath: '"
                        + node.getParentPath() + "'");
            }

            create(node, resolvedNodes, primaryParentAssoc);

            if (log.isTraceEnabled()) {
                log.trace("^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^");
            }
        }
    }

    /**
     * Create new node.
     * 
     * @param node TransferManifestNormalNode
     * @param resolvedNodes ResolvedParentChildPair
     * @param primaryParentAssoc ChildAssociationRef
     */
    private void create(TransferManifestNormalNode node, ResolvedParentChildPair resolvedNodes,
            ChildAssociationRef primaryParentAssoc) {
        log.info("Creating new node with noderef " + node.getNodeRef());

        QName parentAssocType = primaryParentAssoc.getTypeQName();
        QName parentAssocName = primaryParentAssoc.getQName();
        NodeRef parentNodeRef = resolvedNodes.resolvedParent;
        if (parentNodeRef == null) {
            if (log.isDebugEnabled()) {
                log.debug("Unable to resolve parent for inbound noderef " + node.getNodeRef()
                        + ".\n  Supplied parent noderef is " + primaryParentAssoc.getParentRef()
                        + ".\n  Supplied parent path is " + node.getParentPath().toString());
            }
            // We can't find the node's parent.
            // We'll store the node in a temporary location and record it for
            // later processing
            ChildAssociationRef tempLocation = getTemporaryLocation(node.getNodeRef());
            parentNodeRef = tempLocation.getParentRef();
            parentAssocType = tempLocation.getTypeQName();
            parentAssocName = tempLocation.getQName();
            log.info("Recording orphaned transfer node: " + node.getNodeRef());
            logComment("Unable to resolve parent for new incoming node. Storing it in temp folder: "
                    + node.getNodeRef());
            storeOrphanNode(primaryParentAssoc);
        }
        // We now know that this is a new node, and we have found the
        // appropriate parent node in the
        // local repository.
        log.info("Resolved parent node to " + parentNodeRef);

        // We need to process content properties separately.
        // First, create a shallow copy of the supplied property map...
        Map<QName, Serializable> props = new HashMap<QName, Serializable>(node.getProperties());

        processCategories(props, node.getManifestCategories());

        injectTransferred(props);

        // Split out the content properties and sanitise the others
        Map<QName, Serializable> contentProps = processProperties(null, props, null);

        // Remove the invadedBy property since that is used by the transfer service 
        // and is local to this repository.
        props.remove(TransferModel.PROP_INVADED_BY);

        // Do we need to worry about locking this new node ?
        if (header.isReadOnly()) {
            log.debug("new node needs to be locked");
            props.put(ContentModel.PROP_LOCK_OWNER, AuthenticationUtil.getAdminUserName());
            props.put(ContentModel.PROP_LOCK_TYPE, LockType.NODE_LOCK.toString());
            props.put(ContentModel.PROP_EXPIRY_DATE, null);
        }

        // Create the corresponding node...
        ChildAssociationRef newNode = nodeService.createNode(parentNodeRef, parentAssocType, parentAssocName,
                node.getType(), props);

        if (log.isDebugEnabled()) {
            log.debug(
                    "Created new node (" + newNode.getChildRef() + ") parented by node " + newNode.getParentRef());
        }

        logCreated(node.getNodeRef(), newNode.getChildRef(), newNode.getParentRef(),
                nodeService.getPath(newNode.getChildRef()).toString(), false);
        logSummaryCreated(node.getNodeRef(), newNode.getChildRef(), newNode.getParentRef(),
                nodeService.getPath(newNode.getChildRef()).toString(), false);

        // Deal with the content properties
        writeContent(newNode.getChildRef(), contentProps);

        // Apply any aspects that are needed but haven't automatically been
        // applied
        Set<QName> aspects = new HashSet<QName>(node.getAspects());
        aspects.removeAll(nodeService.getAspects(newNode.getChildRef()));
        for (QName aspect : aspects) {
            nodeService.addAspect(newNode.getChildRef(), aspect, null);
        }

        ManifestAccessControl acl = node.getAccessControl();
        // Apply new ACL to this node
        if (acl != null) {
            permissionService.setInheritParentPermissions(newNode.getChildRef(), acl.isInherited());

            if (acl.getPermissions() != null) {
                for (ManifestPermission permission : acl.getPermissions()) {
                    log.debug("setting permission on node");
                    AccessStatus status = AccessStatus.valueOf(permission.getStatus());
                    // The node has its own access control list
                    permissionService.setPermission(newNode.getChildRef(), permission.getAuthority(),
                            permission.getPermission(), status == AccessStatus.ALLOWED);
                }
            }
        }

        /**
         * are we adding an alien node here? The transfer service has policies disabled 
         * so have to call the consequence of the policy directly.
         */
        if (nodeService.hasAspect(parentNodeRef, TransferModel.ASPECT_TRANSFERRED)
                || nodeService.hasAspect(parentNodeRef, TransferModel.ASPECT_ALIEN)) {
            alienProcessor.onCreateChild(newNode, header.getRepositoryId(), true);
        }

        // Is the node that we've just added the parent of any orphans that
        // we've found earlier?
        checkOrphans(newNode.getChildRef());
    }

    /**
     * Delete this node
     * @param node TransferManifestDeletedNode
     * @param nodeToDelete NodeRef
     */
    protected void delete(TransferManifestDeletedNode node, NodeRef nodeToDelete) {
        if (alienProcessor.isAlien(nodeToDelete)) {
            logComment("Node contains alien content and can't be deleted: " + nodeToDelete);
            if (log.isDebugEnabled()) {
                log.debug("Node to be deleted is alien prune rather than delete: " + nodeToDelete);
            }
            alienProcessor.pruneNode(nodeToDelete, header.getRepositoryId());
        } else {
            /**
             * Check that if the destination is "from" the transferring repo if it is "from" another repo then ignore
             */
            if (nodeService.hasAspect(nodeToDelete, TransferModel.ASPECT_TRANSFERRED)) {
                String fromRepository = (String) nodeService.getProperty(nodeToDelete,
                        TransferModel.PROP_FROM_REPOSITORY_ID);
                String transferringRepo = header.getRepositoryId();

                if (fromRepository != null && transferringRepo != null) {
                    if (!fromRepository.equalsIgnoreCase(transferringRepo)) {
                        logComment(
                                "Not deleting local node (not from the transferring repository): " + nodeToDelete);
                        return;
                    }
                }
            }

            // Not alien or from another repo - delete it.
            logDeleted(node.getNodeRef(), nodeToDelete, nodeService.getPath(nodeToDelete).toString());
            logSummaryDeleted(node.getNodeRef(), nodeToDelete, nodeService.getPath(nodeToDelete).toString());

            nodeService.deleteNode(nodeToDelete);
            if (log.isDebugEnabled()) {
                log.debug("Deleted local node: " + nodeToDelete);
            }
        }
    }

    private void checkOrphans(NodeRef parentNode) {
        List<ChildAssociationRef> orphansToClaim = orphans.get(parentNode);
        if (orphansToClaim != null) {
            // Yes, it is...
            for (ChildAssociationRef orphan : orphansToClaim) {
                logComment("Re-parenting previously orphaned node (" + orphan.getChildRef() + ") with found parent "
                        + orphan.getParentRef());
                ChildAssociationRef newRef = nodeService.moveNode(orphan.getChildRef(), orphan.getParentRef(),
                        orphan.getTypeQName(), orphan.getQName());

                /**
                 * We may be creating an alien node here and the policies are turned off.
                 */
                if (nodeService.hasAspect(newRef.getParentRef(), TransferModel.ASPECT_TRANSFERRED)) {
                    alienProcessor.onCreateChild(newRef, header.getRepositoryId(), true);
                }
            }
            // We can now remove the record of these orphans, as their parent
            // has been found
            orphans.remove(parentNode);
        }
    }

    /**
     * 
     * @param node TransferManifestNormalNode
     * @param resolvedNodes ResolvedParentChildPair
     * @param primaryParentAssoc ChildAssociationRef
     */
    private void update(TransferManifestNormalNode node, ResolvedParentChildPair resolvedNodes,
            ChildAssociationRef primaryParentAssoc) {
        NodeRef nodeToUpdate = resolvedNodes.resolvedChild;

        /**
         * Check that if the destination is "from" the transferring repo if it is "from" another repo then ignore
         */
        if (nodeService.hasAspect(nodeToUpdate, TransferModel.ASPECT_TRANSFERRED)) {
            String fromRepository = (String) nodeService.getProperty(nodeToUpdate,
                    TransferModel.PROP_FROM_REPOSITORY_ID);
            String transferringRepo = header.getRepositoryId();

            if (fromRepository != null && transferringRepo != null) {
                if (!fromRepository.equalsIgnoreCase(transferringRepo)) {
                    logComment(
                            "Not updating local node (not from the transferring repository): " + node.getNodeRef());
                    return;
                }
            }
        } else {
            logComment("Not updating local node - node is local to this repository): " + node.getNodeRef());
            // Not a transferred node.
            return;
        }

        QName parentAssocType = primaryParentAssoc.getTypeQName();
        QName parentAssocName = primaryParentAssoc.getQName();
        NodeRef parentNodeRef = resolvedNodes.resolvedParent;
        if (parentNodeRef == null) {
            // We can't find the node's parent.
            // We'll store the node in a temporary location and record it for
            // later processing
            ChildAssociationRef tempLocation = getTemporaryLocation(node.getNodeRef());
            parentNodeRef = tempLocation.getParentRef();
            parentAssocType = tempLocation.getTypeQName();
            parentAssocName = tempLocation.getQName();
            storeOrphanNode(primaryParentAssoc);
        }

        // First of all, do we need to move the node? If any aspect of the
        // primary parent association has changed
        // then the answer is "yes"
        ChildAssociationRef currentParent = nodeService.getPrimaryParent(nodeToUpdate);
        if (!currentParent.getParentRef().equals(parentNodeRef)
                || !currentParent.getTypeQName().equals(parentAssocType)
                || !currentParent.getQName().equals(parentAssocName)) {

            /**
             * Yes, the parent assoc has changed so we need to move the node
             */
            if (nodeService.hasAspect(currentParent.getParentRef(), TransferModel.ASPECT_ALIEN)) {
                // old parent node ref may be alien so treat as a delete
                alienProcessor.beforeDeleteAlien(currentParent.getChildRef(), null);
            }

            // Yes, we need to move the node
            ChildAssociationRef newNode = nodeService.moveNode(nodeToUpdate, parentNodeRef, parentAssocType,
                    parentAssocName);
            logMoved(node.getNodeRef(), nodeToUpdate, node.getParentPath().toString(), newNode.getParentRef(),
                    nodeService.getPath(newNode.getChildRef()).toString());
            logSummaryMoved(node.getNodeRef(), nodeToUpdate, node.getParentPath().toString(),
                    newNode.getParentRef(), nodeService.getPath(newNode.getChildRef()).toString());

            /**
             * are we adding an alien node here? The transfer service has policies disabled 
             * so have to call the consequence of the policy directly.
             */
            if (nodeService.hasAspect(newNode.getChildRef(), TransferModel.ASPECT_ALIEN)) {
                alienProcessor.afterMoveAlien(newNode);
            } else {
                /**
                 * are we adding an alien node here? The transfer service has policies disabled 
                 * so have to call the consequence of the policy directly.
                 */
                if (nodeService.hasAspect(parentNodeRef, TransferModel.ASPECT_TRANSFERRED)
                        || nodeService.hasAspect(parentNodeRef, TransferModel.ASPECT_ALIEN)) {
                    alienProcessor.onCreateChild(newNode, header.getRepositoryId(), true);
                }
            }
        }

        log.info("Resolved parent node to " + parentNodeRef);

        if (updateNeeded(node, nodeToUpdate)) {

            logUpdated(node.getNodeRef(), nodeToUpdate, nodeService.getPath(nodeToUpdate).toString());

            // We need to process content properties separately.
            // First, create a shallow copy of the supplied property map...
            Map<QName, Serializable> props = new HashMap<QName, Serializable>(node.getProperties());
            Map<QName, Serializable> existingProps = nodeService.getProperties(nodeToUpdate);

            processCategories(props, node.getManifestCategories());

            // inject transferred properties/aspect here
            injectTransferred(props);

            // Remove the invadedBy property since that is used by the transfer service 
            // and is local to this repository.
            props.remove(TransferModel.PROP_INVADED_BY);

            // Do we need to worry about locking this updated ?
            if (header.isReadOnly()) {
                props.put(ContentModel.PROP_LOCK_OWNER, AuthenticationUtil.getAdminUserName());
                props.put(ContentModel.PROP_LOCK_TYPE, LockType.NODE_LOCK.toString());
                props.put(ContentModel.PROP_EXPIRY_DATE, null);
                log.debug("updated node needs to be locked");
            }

            // Split out the content properties and sanitise the others
            Map<QName, Serializable> contentProps = processProperties(nodeToUpdate, props, existingProps);

            // If there was already a value for invadedBy then leave it alone rather than replacing it.
            if (existingProps.containsKey(TransferModel.PROP_INVADED_BY)) {
                props.put(TransferModel.PROP_INVADED_BY, existingProps.get(TransferModel.PROP_INVADED_BY));
            }

            // Update the non-content properties
            nodeService.setProperties(nodeToUpdate, props);

            // Deal with the content properties
            boolean contentUpdated = writeContent(nodeToUpdate, contentProps);
            if (contentUpdated) {
                logSummaryUpdated(node.getNodeRef(), nodeToUpdate, nodeService.getPath(nodeToUpdate).toString());
            }
            // Change the type of the content
            if (!nodeService.getType(nodeToUpdate).equals(node.getType())) {
                // The type has changed, check the dictionary to contain the model for that type
                TypeDefinition newTypeDef = dictionaryService.getType(node.getType());
                if (newTypeDef == null) {
                    log.warn("Failed to update the type: " + node.getType() + " for node: " + nodeToUpdate
                            + ", as there is no type definition for it");
                } else {
                    // Check the default properties
                    Map<QName, PropertyDefinition> typeProperties = newTypeDef.getProperties();
                    // Search if all the properties are in place
                    boolean fail = false;
                    for (QName key : typeProperties.keySet()) {
                        PropertyDefinition propDef = typeProperties.get(key);
                        if (!props.containsKey(key) && propDef.isMandatory()) {
                            log.warn("Failed to update the type: " + node.getType() + " for node: " + nodeToUpdate
                                    + ", as the mandatory property '" + propDef.getName()
                                    + "' was not transferred.");
                            fail = true;
                            break;
                        }
                    }
                    if (!fail) {
                        // Set the new type
                        nodeService.setType(nodeToUpdate, node.getType());
                    }
                }
            }

            // Blend the aspects together
            Set<QName> suppliedAspects = new HashSet<QName>(node.getAspects());
            Set<QName> existingAspects = nodeService.getAspects(nodeToUpdate);
            Set<QName> aspectsToRemove = new HashSet<QName>(existingAspects);

            // Add mandatory aspects to the supplied aspects (eg. should not explicitly remove auditable aspect from a folder - see also DMDeploymentTarget for similar)
            List<AspectDefinition> aspectDefs = dictionaryService.getType(nodeService.getType(nodeToUpdate))
                    .getDefaultAspects(true);
            for (AspectDefinition aspectDef : aspectDefs) {
                suppliedAspects.add(aspectDef.getName());
            }

            if (header.isReadOnly()) {
                suppliedAspects.add(ContentModel.ASPECT_LOCKABLE);
            }

            aspectsToRemove.removeAll(suppliedAspects);

            /**
             * Don't remove the aspects that the transfer service uses itself.
             */
            aspectsToRemove.remove(TransferModel.ASPECT_TRANSFERRED);
            aspectsToRemove.remove(TransferModel.ASPECT_ALIEN);

            suppliedAspects.removeAll(existingAspects);

            // Now aspectsToRemove contains the set of aspects to remove
            // and suppliedAspects contains the set of aspects to add
            for (QName aspect : suppliedAspects) {
                nodeService.addAspect(nodeToUpdate, aspect, null);
            }

            for (QName aspect : aspectsToRemove) {
                nodeService.removeAspect(nodeToUpdate, aspect);
            }

            // Check the ACL of this updated node

            ManifestAccessControl acl = node.getAccessControl();
            if (acl != null) {
                boolean existInherit = permissionService.getInheritParentPermissions(nodeToUpdate);
                if (existInherit != acl.isInherited()) {
                    log.debug("changed inherit permissions flag");
                    permissionService.setInheritParentPermissions(nodeToUpdate, acl.isInherited());
                }

                Set<AccessPermission> existingPermissions = permissionService.getAllSetPermissions(nodeToUpdate);
                List<ManifestPermission> newPermissions = acl.getPermissions();

                if (existingPermissions.size() > 0 || newPermissions != null) {
                    // Yes we have explicit permissions on this node.
                    log.debug("have to check permissions");

                    Set<ManifestPermission> work = new HashSet<ManifestPermission>();
                    for (AccessPermission permission : existingPermissions) {
                        if (permission.isSetDirectly()) {
                            ManifestPermission p = new ManifestPermission();
                            p.setAuthority(permission.getAuthority());
                            p.setPermission(permission.getPermission());
                            p.setStatus(permission.getAccessStatus().toString());
                            work.add(p);
                        }
                    }

                    // Do we need to check whether to add any permissions ?
                    if (newPermissions != null) {
                        // Do we need to add any permissions ?
                        for (ManifestPermission permission : acl.getPermissions()) {
                            if (!work.contains(permission)) {
                                log.debug("setting permission on node:" + permission);
                                AccessStatus status = AccessStatus.valueOf(permission.getStatus());
                                permissionService.setPermission(nodeToUpdate, permission.getAuthority(),
                                        permission.getPermission(), status == AccessStatus.ALLOWED);
                            }
                        }

                        // Remove permissions from "work" that should be there
                        work.removeAll(newPermissions);

                    }

                    // Do we need to remove any permissions
                    for (ManifestPermission permission : work) {
                        log.debug("removing permission on node:" + permission);
                        permissionService.deletePermission(nodeToUpdate, permission.getAuthority(),
                                permission.getPermission());
                    }
                }
            }
        }
    }

    /**
     * This method takes all the received properties and separates them into two parts. The content properties are
     * removed from the non-content properties such that the non-content properties remain in the "props" map and the
     * content properties are returned from this method Subsequently, any properties that are to be retained from the
     * local repository are copied over into the "props" map. The result of all this is that, upon return, "props"
     * contains all the non-content properties that are to be written to the local repo, and "contentProps" contains all
     * the content properties that are to be written to the local repo.
     * 
     * @param nodeToUpdate
     *            The noderef of the existing node in the local repo that is to be updated with these properties. May be
     *            null, indicating that these properties are destined for a brand new local node.
     * @param props the new properties
     * @param existingProps the existing properties, null if this is a create
     * @return A map containing the content properties which are going to be replaced from the supplied "props" map
     */
    private Map<QName, Serializable> processProperties(NodeRef nodeToUpdate, Map<QName, Serializable> props,
            Map<QName, Serializable> existingProps) {
        Map<QName, Serializable> contentProps = new HashMap<QName, Serializable>();
        // ...and copy any supplied content properties into this new map...
        for (Map.Entry<QName, Serializable> propEntry : props.entrySet()) {
            Serializable value = propEntry.getValue();
            QName key = propEntry.getKey();
            if (log.isDebugEnabled()) {
                if (value == null) {
                    log.debug("Received a null value for property " + propEntry.getKey());
                }
            }
            if ((value != null) && ContentData.class.isAssignableFrom(value.getClass())) {
                if (existingProps != null) {
                    // This is an update and we have content data 
                    File stagingDir = getStagingFolder();
                    ContentData contentData = (ContentData) propEntry.getValue();
                    String contentUrl = contentData.getContentUrl();
                    String fileName = TransferCommons.URLToPartName(contentUrl);
                    File stagedFile = new File(stagingDir, fileName);
                    if (stagedFile.exists()) {
                        if (log.isDebugEnabled()) {
                            log.debug("replace content for node:" + nodeToUpdate + ", " + key);
                        }
                        // Yes we are going to replace the content item
                        contentProps.put(propEntry.getKey(), propEntry.getValue());
                    } else {
                        // Staging file does not exist
                        if (props.containsKey(key)) {
                            if (log.isDebugEnabled()) {
                                log.debug("keep existing content for node:" + nodeToUpdate + ", " + key);
                            }
                            // keep the existing content value
                            props.put(propEntry.getKey(), existingProps.get(key));
                        }
                    }
                } else {
                    // This is a create so all content items are new
                    contentProps.put(propEntry.getKey(), propEntry.getValue());
                }
            }
        }

        // Now we can remove the content properties from amongst the other kinds
        // of properties
        // (no removeAll on a Map...)
        for (QName contentPropertyName : contentProps.keySet()) {
            props.remove(contentPropertyName);
        }

        if (existingProps != null) {
            // Finally, overlay the repo-specific properties from the existing
            // node (if there is one)
            for (QName localProperty : getLocalProperties()) {
                Serializable existingValue = existingProps.get(localProperty);
                if (existingValue != null) {
                    props.put(localProperty, existingValue);
                } else {
                    props.remove(localProperty);
                }
            }
        }
        return contentProps;
    }

    /**
     * @param nodeToUpdate NodeRef
     * @param contentProps Map<QName, Serializable>
     * @return true if any content property has been updated for the needToUpdate node
     */
    private boolean writeContent(NodeRef nodeToUpdate, Map<QName, Serializable> contentProps) {
        boolean contentUpdated = false;
        File stagingDir = getStagingFolder();
        for (Map.Entry<QName, Serializable> contentEntry : contentProps.entrySet()) {
            ContentData contentData = (ContentData) contentEntry.getValue();
            String contentUrl = contentData.getContentUrl();
            if (contentUrl == null || contentUrl.isEmpty()) {
                log.debug("content data is null or empty:" + nodeToUpdate);
                ContentData cd = new ContentData(null, null, 0, null);
                nodeService.setProperty(nodeToUpdate, contentEntry.getKey(), cd);
                contentUpdated = true;
            } else {
                String fileName = TransferCommons.URLToPartName(contentUrl);
                File stagedFile = new File(stagingDir, fileName);
                if (!stagedFile.exists()) {
                    error(MSG_REFERENCED_CONTENT_FILE_MISSING);
                }
                ContentWriter writer = contentService.getWriter(nodeToUpdate, contentEntry.getKey(), true);
                writer.setEncoding(contentData.getEncoding());
                writer.setMimetype(contentData.getMimetype());
                writer.setLocale(contentData.getLocale());
                writer.putContent(stagedFile);
                contentUpdated = true;
            }
        }
        return contentUpdated;
    }

    protected boolean updateNeeded(TransferManifestNormalNode node, NodeRef nodeToUpdate) {
        boolean updateNeeded = true;
        // Assumption: if the modified and modifier properties haven't changed, and the cm:content property
        // (if it exists) hasn't changed size then we can assume that properties don't need to be updated...
        //        Map<QName, Serializable> suppliedProps = node.getProperties();
        //        Date suppliedModifiedDate = (Date) suppliedProps.get(ContentModel.PROP_MODIFIED);
        //        String suppliedModifier = (String) suppliedProps.get(ContentModel.PROP_MODIFIER);
        //        ContentData suppliedContent = (ContentData) suppliedProps.get(ContentModel.PROP_CONTENT);
        //
        //        Map<QName, Serializable> existingProps = nodeService.getProperties(nodeToUpdate);
        //        Date existingModifiedDate = (Date) existingProps.get(ContentModel.PROP_MODIFIED);
        //        String existingModifier = (String) existingProps.get(ContentModel.PROP_MODIFIER);
        //        ContentData existingContent = (ContentData) existingProps.get(ContentModel.PROP_CONTENT);
        //
        //        updateNeeded = false;
        //        updateNeeded |= ((suppliedModifiedDate != null && !suppliedModifiedDate.equals(existingModifiedDate)) || 
        //                (existingModifiedDate != null && !existingModifiedDate.equals(suppliedModifiedDate)));
        //        updateNeeded |= ((suppliedContent != null && existingContent == null)
        //                || (suppliedContent == null && existingContent != null) || (suppliedContent != null
        //                && existingContent != null && suppliedContent.getSize() != existingContent.getSize()));
        //        updateNeeded |= ((suppliedModifier != null && !suppliedModifier.equals(existingModifier)) || 
        //                (existingModifier != null && !existingModifier.equals(suppliedModifier)));
        return updateNeeded;
    }

    /**
     */
    protected Set<QName> getLocalProperties() {
        return DEFAULT_LOCAL_PROPERTIES;
    }

    /**
     * @param primaryParentAssoc ChildAssociationRef
     */
    private void storeOrphanNode(ChildAssociationRef primaryParentAssoc) {
        List<ChildAssociationRef> orphansOfParent = orphans.get(primaryParentAssoc.getParentRef());
        if (orphansOfParent == null) {
            orphansOfParent = new ArrayList<ChildAssociationRef>();
            orphans.put(primaryParentAssoc.getParentRef(), orphansOfParent);
        }
        orphansOfParent.add(primaryParentAssoc);
    }

    /**
     * @param node TransferManifestNode
     * @param msgId String
     */
    private void error(TransferManifestNode node, String msgId) {
        TransferProcessingException ex = new TransferProcessingException(msgId);
        log.error(ex.getMessage(), ex);
        throw ex;
    }

    /**
     * @param msgId String
     */
    private void error(String msgId) {
        TransferProcessingException ex = new TransferProcessingException(msgId);
        log.error(ex.getMessage(), ex);
        throw ex;
    }

    protected void processHeader(TransferManifestHeader header) {
        // squirrel away the header for later use
        this.header = header;
    }

    /*
     * (non-Javadoc)
     * 
     * @seeorg.alfresco.repo.transfer.manifest.TransferManifestProcessor# startTransferManifest()
     */
    protected void startManifest() {
    }

    /**
     * @param nodeService
     *            the nodeService to set
     */
    public void setNodeService(NodeService nodeService) {
        this.nodeService = nodeService;
    }

    /**
     * @param contentService
     *            the contentService to set
     */
    public void setContentService(ContentService contentService) {
        this.contentService = contentService;
    }

    /**
     * @param dictionaryService
     *            the dictionaryService to set
     */
    public void setDictionaryService(DictionaryService dictionaryService) {
        this.dictionaryService = dictionaryService;
    }

    /**
     * @param nodeResolver
     *            the nodeResolver to set
     */
    public void setNodeResolver(CorrespondingNodeResolver nodeResolver) {
        this.nodeResolver = nodeResolver;
    }

    public void setPermissionService(PermissionService permissionService) {
        this.permissionService = permissionService;
    }

    public PermissionService getPermissionService() {
        return permissionService;
    }

    /**
     * Process categories.
     * 
     * CRUD of Categories and Tags - also maps noderefs of type d:content from source to target
     * 
     * 
     * @param properties Map<QName, Serializable>
     * @param manifestCategories Map<NodeRef, ManifestCategory>
     */
    private void processCategories(Map<QName, Serializable> properties,
            Map<NodeRef, ManifestCategory> manifestCategories) {
        if (manifestCategories != null) {
            for (Map.Entry<QName, Serializable> val : properties.entrySet()) {
                PropertyDefinition def = dictionaryService.getProperty(val.getKey());
                if (def != null) {
                    if (def.getDataType().getName().isMatch(DataTypeDefinition.CATEGORY)) {
                        Serializable thing = val.getValue();
                        if (thing != null) {
                            if (def.isMultiValued()) {
                                if (thing instanceof java.util.Collection) {
                                    List<NodeRef> newCategories = new ArrayList<NodeRef>();
                                    java.util.Collection<NodeRef> c = (java.util.Collection<NodeRef>) thing;
                                    for (NodeRef sourceCategoryNodeRef : c) {
                                        if (log.isDebugEnabled()) {
                                            log.debug("sourceCategoryNodeRef" + sourceCategoryNodeRef);
                                        }
                                        // substitute target node ref fot source node ref
                                        NodeRef targetNodeRef = processCategory(sourceCategoryNodeRef,
                                                manifestCategories);
                                        newCategories.add(targetNodeRef);
                                    }
                                    // substitute target node refs for source node refs
                                    properties.put(val.getKey(), (Serializable) newCategories);
                                } else {
                                    throw new AlfrescoRuntimeException(
                                            "Multi valued object is not a collection" + val.getKey());
                                }
                            } else {
                                NodeRef sourceCategoryNodeRef = (NodeRef) thing;
                                if (log.isDebugEnabled()) {
                                    log.debug("sourceCategoryNodeRef:" + sourceCategoryNodeRef);
                                }
                                NodeRef targetNodeRef = processCategory(sourceCategoryNodeRef, manifestCategories);
                                // substitute target node ref for source node ref
                                properties.put(val.getKey(), targetNodeRef);
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * process category - maps the category node ref from the source system to the target system.
     * 
     * It will lazily create any missing categories and tags as it executes.
     * 
     * @param sourceCategoryNodeRef NodeRef
     * @param manifestCategories Map<NodeRef, ManifestCategory>
     * @return targetNodeRef
     */
    private NodeRef processCategory(final NodeRef sourceCategoryNodeRef,
            final Map<NodeRef, ManifestCategory> manifestCategories) {

        // first check cache to see whether we have already mapped this category
        NodeRef destinationNodeRef = categoryMap.get(sourceCategoryNodeRef);
        if (destinationNodeRef != null) {
            return destinationNodeRef;
        }

        // No we havn't seen this category before, have we got the details in the manifest
        ManifestCategory category = manifestCategories.get(sourceCategoryNodeRef);
        if (category != null) {
            final String path = category.getPath();
            final Path catPath = PathHelper.stringToPath(path);

            final Path.Element aspectName = catPath.get(2);
            final QName aspectQName = QName.createQName(aspectName.getElementString());

            if (aspectQName.equals(ContentModel.ASPECT_TAGGABLE)) {
                Path.Element tagName = catPath.get(3);
                // Category is a tag
                QName tagQName = QName.createQName(tagName.getElementString());
                destinationNodeRef = taggingService.getTagNodeRef(sourceCategoryNodeRef.getStoreRef(),
                        tagQName.getLocalName());
                if (destinationNodeRef != null) {
                    log.debug("found existing tag" + tagQName.getLocalName());
                    categoryMap.put(sourceCategoryNodeRef, destinationNodeRef);
                    return destinationNodeRef;
                }
                destinationNodeRef = taggingService.createTag(sourceCategoryNodeRef.getStoreRef(),
                        tagQName.getLocalName());
                if (destinationNodeRef != null) {
                    log.debug("created new tag" + tagQName.getLocalName());
                    categoryMap.put(sourceCategoryNodeRef, destinationNodeRef);
                    return destinationNodeRef;
                }
            } else {
                // Categories are finniky about permissions, so run as system
                RunAsWork<NodeRef> processCategory = new RunAsWork<NodeRef>() {
                    @Override
                    public NodeRef doWork() throws Exception {
                        QName rootCatName = QName.createQName(catPath.get(3).getElementString());

                        Collection<ChildAssociationRef> roots = categoryService
                                .getRootCategories(sourceCategoryNodeRef.getStoreRef(), aspectQName);

                        /**
                         * Get the root category node ref
                         */
                        NodeRef rootCategoryNodeRef = null;
                        for (ChildAssociationRef ref : roots) {
                            if (ref.getQName().equals(rootCatName)) {
                                rootCategoryNodeRef = ref.getChildRef();
                                break;
                            }
                        }

                        if (rootCategoryNodeRef == null) {
                            // Root category does not exist
                            rootCategoryNodeRef = categoryService.createRootCategory(
                                    sourceCategoryNodeRef.getStoreRef(), aspectQName, rootCatName.getLocalName());
                        }

                        NodeRef workingNodeRef = rootCategoryNodeRef;
                        // Root category does already exist - step through any sub-categories
                        for (int i = 4; i < catPath.size(); i++) {
                            Path.Element element = catPath.get(i);
                            QName subCatName = QName.createQName(element.toString());
                            ChildAssociationRef child = categoryService.getCategory(workingNodeRef, aspectQName,
                                    subCatName.getLocalName());
                            if (child != null) {
                                workingNodeRef = child.getChildRef();
                            } else {
                                workingNodeRef = categoryService.createCategory(workingNodeRef,
                                        subCatName.getLocalName());
                            }
                        }
                        return workingNodeRef;
                    }

                };
                destinationNodeRef = AuthenticationUtil.runAs(processCategory, AuthenticationUtil.SYSTEM_USER_NAME);
                categoryMap.put(sourceCategoryNodeRef, destinationNodeRef);
                return destinationNodeRef;
            }
        } // if manifest category exists

        return sourceCategoryNodeRef;

    }

    /**
     * inject transferred
     */
    private void injectTransferred(Map<QName, Serializable> props) {
        if (!props.containsKey(TransferModel.PROP_REPOSITORY_ID)) {
            log.debug("injecting repositoryId property");
            props.put(TransferModel.PROP_REPOSITORY_ID, header.getRepositoryId());
        }
        props.put(TransferModel.PROP_FROM_REPOSITORY_ID, header.getRepositoryId());

        /**
         * For each property
         */
        List<String> contentProps = new ArrayList<String>();
        for (Serializable value : props.values()) {
            if ((value != null) && ContentData.class.isAssignableFrom(value.getClass())) {
                ContentData srcContent = (ContentData) value;

                if (srcContent.getContentUrl() != null && !srcContent.getContentUrl().isEmpty()) {
                    log.debug("adding part name to from content field");
                    contentProps.add(TransferCommons.URLToPartName(srcContent.getContentUrl()));
                }
            }
        }

        props.put(TransferModel.PROP_FROM_CONTENT, (Serializable) contentProps);
    }

    public void setAlienProcessor(AlienProcessor alienProcessor) {
        this.alienProcessor = alienProcessor;
    }

    public AlienProcessor getAlienProcessor() {
        return alienProcessor;
    }

    public CategoryService getCategoryService() {
        return categoryService;
    }

    public void setCategoryService(CategoryService categoryService) {
        this.categoryService = categoryService;
    }

    public TaggingService getTaggingService() {
        return taggingService;
    }

    public void setTaggingService(TaggingService taggingService) {
        this.taggingService = taggingService;
    }

}