/*
 * #%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.node.db;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.domain.node.ChildAssocEntity;
import org.alfresco.repo.domain.node.Node;
import org.alfresco.repo.domain.node.NodeDAO;
import org.alfresco.repo.domain.node.NodeDAO.ChildAssocRefQueryCallback;
import org.alfresco.repo.domain.node.NodeExistsException;
import org.alfresco.repo.domain.qname.QNameDAO;
import org.alfresco.repo.node.AbstractNodeServiceImpl;
import org.alfresco.repo.node.StoreArchiveMap;
import org.alfresco.repo.node.archive.NodeArchiveService;
import org.alfresco.repo.node.db.NodeHierarchyWalker.VisitedNode;
import org.alfresco.repo.node.db.traitextender.NodeServiceExtension;
import org.alfresco.repo.node.db.traitextender.NodeServiceTrait;
import org.alfresco.repo.policy.BehaviourFilter;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
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.InvalidAspectException;
import org.alfresco.service.cmr.dictionary.InvalidTypeException;
import org.alfresco.service.cmr.dictionary.PropertyDefinition;
import org.alfresco.service.cmr.dictionary.TypeDefinition;
import org.alfresco.service.cmr.repository.AssociationExistsException;
import org.alfresco.service.cmr.repository.AssociationRef;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.InvalidChildAssociationRefException;
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
import org.alfresco.service.cmr.repository.InvalidStoreRefException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeRef.Status;
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.repository.datatype.DefaultTypeConverter;
import org.alfresco.service.cmr.security.AccessPermission;
import org.alfresco.service.cmr.security.AccessStatus;
import org.alfresco.service.cmr.security.OwnableService;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.QNamePattern;
import org.alfresco.service.namespace.RegexQNamePattern;
import org.alfresco.traitextender.AJProxyTrait;
import org.alfresco.traitextender.Extend;
import org.alfresco.traitextender.ExtendedTrait;
import org.alfresco.traitextender.Extensible;
import org.alfresco.traitextender.Trait;
import org.alfresco.util.EqualsHelper;
import org.alfresco.util.GUID;
import org.alfresco.util.Pair;
import org.alfresco.util.ParameterCheck;
import org.alfresco.util.PropertyMap;
import org.alfresco.util.transaction.TransactionListenerAdapter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.extensions.surf.util.I18NUtil;

/**
 * Node service using database persistence layer to fulfill functionality
 * 
 * @author Derek Hulley
 */
public class DbNodeServiceImpl extends AbstractNodeServiceImpl implements Extensible , NodeService
{
    public static final String KEY_PENDING_DELETE_NODES = "DbNodeServiceImpl.pendingDeleteNodes";
    
    private static Log logger = LogFactory.getLog(DbNodeServiceImpl.class);
    
    private QNameDAO qnameDAO;
    private NodeDAO nodeDAO;
    private PermissionService permissionService;
    private StoreArchiveMap storeArchiveMap;
    private BehaviourFilter policyBehaviourFilter;
    private boolean enableTimestampPropagation;
    private final ExtendedTrait<NodeServiceTrait> nodeServiceTrait;
    
    public DbNodeServiceImpl()
    {
        nodeServiceTrait = new ExtendedTrait<NodeServiceTrait>(AJProxyTrait.create(this, NodeServiceTrait.class));
        storeArchiveMap = new StoreArchiveMap();        // in case it is not set
    }

    public void setQnameDAO(QNameDAO qnameDAO)
    {
        this.qnameDAO = qnameDAO;
    }

    public void setNodeDAO(NodeDAO nodeDAO)
    {
        this.nodeDAO = nodeDAO;
    }

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

    public void setStoreArchiveMap(StoreArchiveMap storeArchiveMap)
    {
        this.storeArchiveMap = storeArchiveMap;
    }

    /**
     * 
     * @param policyBehaviourFilter     component used to enable and disable behaviours
     */
    public void setPolicyBehaviourFilter(BehaviourFilter policyBehaviourFilter)
    {
        this.policyBehaviourFilter = policyBehaviourFilter;
    }

    /**
     * Set whether <b>cm:auditable</b> timestamps should be propagated to parent nodes
     * where the parent-child relationship has been marked using <b>propagateTimestamps</b>.
     * 
     * @param enableTimestampPropagation        <tt>true</tt> to propagate timestamps to the parent
     *                                          node where appropriate
     */
    public void setEnableTimestampPropagation(boolean enableTimestampPropagation)
    {
        this.enableTimestampPropagation = enableTimestampPropagation;
    }

    /**
     * Performs a null-safe get of the node
     * 
     * @param nodeRef the node to retrieve
     * @return Returns the node entity (never null)
     * @throws InvalidNodeRefException if the referenced node could not be found
     */
    private Pair<Long, NodeRef> getNodePairNotNull(NodeRef nodeRef) throws InvalidNodeRefException
    {
        ParameterCheck.mandatory("nodeRef", nodeRef);
        
        Pair<Long, NodeRef> unchecked = nodeDAO.getNodePair(nodeRef);
        if (unchecked == null)
        {
            Status nodeStatus = nodeDAO.getNodeRefStatus(nodeRef);
            throw new InvalidNodeRefException("Node does not exist: " + nodeRef + " (status:" + nodeStatus + ")", nodeRef);
        }
        return unchecked;
    }
    
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public boolean exists(StoreRef storeRef)
    {
        return nodeDAO.exists(storeRef);
    }
    
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public boolean exists(NodeRef nodeRef)
    {
        ParameterCheck.mandatory("nodeRef", nodeRef);
        return nodeDAO.exists(nodeRef);
    }
    
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public Status getNodeStatus(NodeRef nodeRef)
    {
        ParameterCheck.mandatory("nodeRef", nodeRef);
        NodeRef.Status status = nodeDAO.getNodeRefStatus(nodeRef);
        return status;
    }

    @Override
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public NodeRef getNodeRef(Long nodeId)
    {
        Pair<Long, NodeRef> nodePair = nodeDAO.getNodePair(nodeId);
        return nodePair == null ? null : nodePair.getSecond();
    }

    /**
     * {@inheritDoc}
     */
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public List<StoreRef> getStores()
    {
        // Get the ADM stores
        List<Pair<Long, StoreRef>> stores = nodeDAO.getStores();
        List<StoreRef> storeRefs = new ArrayList<StoreRef>(50);
        for (Pair<Long, StoreRef> pair : stores)
        {
            StoreRef storeRef = pair.getSecond();
            if (storeRef.getProtocol().equals(StoreRef.PROTOCOL_DELETED))
            {
                // Ignore
                continue;
            }
            storeRefs.add(storeRef);
        }
        // Return them all.
        return storeRefs;
    }
    
    /**
     * Defers to the typed service
     */
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public StoreRef createStore(String protocol, String identifier)
    {
        StoreRef storeRef = new StoreRef(protocol, identifier);
        
        // invoke policies
        invokeBeforeCreateStore(ContentModel.TYPE_STOREROOT, storeRef);
        
        // create a new one
        Pair<Long, NodeRef> rootNodePair = nodeDAO.newStore(storeRef);
        NodeRef rootNodeRef = rootNodePair.getSecond();
        
        // invoke policies
        invokeOnCreateStore(rootNodeRef);
        
        // Done
        return storeRef;
    }
    
    /**
     * @throws UnsupportedOperationException        Always
     */
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public void deleteStore(StoreRef storeRef) throws InvalidStoreRefException
    {
        // Cannot delete the root node but we can delete, without archive, all immediate children
        NodeRef rootNodeRef = nodeDAO.getRootNode(storeRef).getSecond();
        List<ChildAssociationRef> childAssocRefs = getChildAssocs(rootNodeRef);
        for (ChildAssociationRef childAssocRef : childAssocRefs)
        {
            NodeRef childNodeRef = childAssocRef.getChildRef();
            // We do NOT want to archive these, so mark them as temporary
            deleteNode(childNodeRef, false);
        }
        // Rename the store.  This takes all the nodes with it.
        StoreRef deletedStoreRef = new StoreRef(StoreRef.PROTOCOL_DELETED, GUID.generate());
        nodeDAO.moveStore(storeRef, deletedStoreRef);
        
        // Done
        if (logger.isDebugEnabled())
        {
            logger.debug("Marked store for deletion: " + storeRef + " --> " + deletedStoreRef);
        }
    }

    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public NodeRef getRootNode(StoreRef storeRef) throws InvalidStoreRefException
    {
        Pair<Long, NodeRef> rootNodePair = nodeDAO.getRootNode(storeRef);
        if (rootNodePair == null)
        {
            throw new InvalidStoreRefException("Store does not exist: " + storeRef, storeRef);
        }
        // done
        return rootNodePair.getSecond();
    }
    
    @Override
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public Set<NodeRef> getAllRootNodes(StoreRef storeRef)
    {
        return nodeDAO.getAllRootNodes(storeRef);
    }

    /**
     * @see #createNode(NodeRef, QName, QName, QName, Map)
     */
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public ChildAssociationRef createNode(
            NodeRef parentRef,
            QName assocTypeQName,
            QName assocQName,
            QName nodeTypeQName)
    {
        return this.createNode(parentRef, assocTypeQName, assocQName, nodeTypeQName, null);
    }

    @Override
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public List<ChildAssociationRef> getChildAssocs(NodeRef nodeRef) throws InvalidNodeRefException
    {
        return super.getChildAssocs(nodeRef);
    }
    
    @Override
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public List<NodeRef> findNodes(FindNodeParameters params)
    {
        return super.findNodes(params);
    }
    
    /**
     * {@inheritDoc}
     */
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public ChildAssociationRef createNode(
            NodeRef parentRef,
            QName assocTypeQName,
            QName assocQName,
            QName nodeTypeQName,
            Map<QName, Serializable> properties)
    {
        // The node(s) involved may not be pending deletion
        checkPendingDelete(parentRef);
        
        ParameterCheck.mandatory("parentRef", parentRef);
        ParameterCheck.mandatory("assocTypeQName", assocTypeQName);
        ParameterCheck.mandatory("assocQName", assocQName);
        ParameterCheck.mandatory("nodeTypeQName", nodeTypeQName);
        if(assocQName.getLocalName().length() > QName.MAX_LENGTH)
        {
            throw new IllegalArgumentException("Localname is too long. Length of " + 
                 assocQName.getLocalName().length() + " exceeds the maximum of " + QName.MAX_LENGTH);
        }
        
        // Get the parent node
        Pair<Long, NodeRef> parentNodePair = getNodePairNotNull(parentRef);
        StoreRef parentStoreRef = parentRef.getStoreRef();

        // null property map is allowed
        if (properties == null)
        {      
            properties = Collections.emptyMap();
        }
        
        // get an ID for the node
        String newUuid = generateGuid(properties);
        
        // Invoke policy behaviour
        invokeBeforeCreateNode(parentRef, assocTypeQName, assocQName, nodeTypeQName);
        
        // check the node type
        TypeDefinition nodeTypeDef = dictionaryService.getType(nodeTypeQName);
        if (nodeTypeDef == null)
        {
            throw new InvalidTypeException(nodeTypeQName);
        }
        
        // Ensure child uniqueness
        String newName = extractNameProperty(properties);
        
        // Get the thread's locale
        Locale locale = I18NUtil.getLocale();
        
        // create the node instance
        ChildAssocEntity assoc = nodeDAO.newNode(
                parentNodePair.getFirst(),
                assocTypeQName,
                assocQName,
                parentStoreRef,
                newUuid,
                nodeTypeQName,
                locale,
                newName,
                properties);
        ChildAssociationRef childAssocRef = assoc.getRef(qnameDAO);
        Pair<Long, NodeRef> childNodePair = assoc.getChildNode().getNodePair();
        
        addAspectsAndProperties(
                    childNodePair,
                    nodeTypeQName,
                    null,
                    Collections.<QName>emptySet(),
                    Collections.<QName, Serializable>emptyMap(),
                    Collections.<QName>emptySet(),
                    properties,
                    true,
                    false);
        
        Map<QName, Serializable> propertiesAfter = nodeDAO.getNodeProperties(childNodePair.getFirst());
        
        // Propagate timestamps
        propagateTimeStamps(childAssocRef);

        // Invoke policy behaviour
        invokeOnCreateNode(childAssocRef);
        invokeOnCreateChildAssociation(childAssocRef, true);
        Map<QName, Serializable> propertiesBefore = PropertyMap.EMPTY_MAP;
        invokeOnUpdateProperties(
                childAssocRef.getChildRef(),
                propertiesBefore,
                propertiesAfter);
        
        // Ensure that the parent node has the required aspects
        addAspectsAndPropertiesAssoc(parentNodePair, assocTypeQName, null, null, null, null, false);
        
        // done
        return childAssocRef;
    }
    
    
    /**
     * Adds all the aspects and properties required for the given node, along with mandatory aspects
     * and related properties.
     * Existing values will not be overridden.  All required pre- and post-update notifications
     * are sent for missing aspects.
     * 
     * @param nodePair              the node to which the details apply
     * @param classQName            the type or aspect QName for which the defaults must be applied.
     *                              If this is <tt>null</tt> then properties and aspects are only applied
     *                              for 'extra' aspects and 'extra' properties.
     * @param existingAspects       the existing aspects or <tt>null</tt> to have them fetched
     * @param existingProperties    the existing properties or <tt>null</tt> to have them fetched
     * @param extraAspects          any aspects that should be added to the 'missing' set (may be <tt>null</tt>)
     * @param extraProperties       any properties that should be added the the 'missing' set (may be <tt>null</tt>)
     * @param overwriteExistingProperties   <tt>true</tt> if the extra properties must completely overwrite
     *                              the existing properties
     * @return                      <tt>true</tt> if properties or aspects were added
     */
    private boolean addAspectsAndProperties(
            Pair<Long, NodeRef> nodePair,
            QName classQName,
            Set<QName> existingAspects,
            Map<QName, Serializable> existingProperties,
            Set<QName> extraAspects,
            Map<QName, Serializable> extraProperties,
            boolean overwriteExistingProperties)
    {
        return addAspectsAndProperties(nodePair, classQName, null, existingAspects, existingProperties, extraAspects, extraProperties, overwriteExistingProperties, true);
    }
    
    private boolean addAspectsAndPropertiesAssoc(
            Pair<Long, NodeRef> nodePair,
            QName assocTypeQName,
            Set<QName> existingAspects,
            Map<QName, Serializable> existingProperties,
            Set<QName> extraAspects,
            Map<QName, Serializable> extraProperties,
            boolean overwriteExistingProperties)
    {
        return addAspectsAndProperties(nodePair, null, assocTypeQName, existingAspects, existingProperties, extraAspects, extraProperties, overwriteExistingProperties, true);
    }
    
    private boolean addAspectsAndProperties(
                Pair<Long, NodeRef> nodePair,
                QName classQName,
                QName assocTypeQName,
                Set<QName> existingAspects,
                Map<QName, Serializable> existingProperties,
                Set<QName> extraAspects,
                Map<QName, Serializable> extraProperties,
                boolean overwriteExistingProperties,
                boolean invokeOnUpdateProperties)
    {
        ParameterCheck.mandatory("nodePair", nodePair);

        Long nodeId = nodePair.getFirst();
        NodeRef nodeRef = nodePair.getSecond();
        
        // Ensure that have a type that has no mandatory aspects or properties
        if (classQName == null)
        {
            classQName = ContentModel.TYPE_BASE;
        }
        
        // Ensure we have 'extra' aspects and properties to play with
        if (extraAspects == null)
        {
            extraAspects = Collections.emptySet();
        }
        if (extraProperties == null)
        {
            extraProperties = Collections.emptyMap();
        }
        
        // Get the existing aspects and properties, if necessary
        if (existingAspects == null)
        {
            existingAspects = nodeDAO.getNodeAspects(nodeId);
        }
        if (existingProperties == null)
        {
            existingProperties = nodeDAO.getNodeProperties(nodeId);
        }
        
        // To determine the 'missing' aspects, we need to determine the full set of properties
        Map<QName, Serializable> allProperties = new HashMap<QName, Serializable>(37);
        allProperties.putAll(existingProperties);
        allProperties.putAll(extraProperties);
        
        // Copy incoming existing values so that we can modify appropriately
        existingAspects = new HashSet<QName>(existingAspects);
        
        // Get the 'missing' aspects and append the 'extra' aspects
        Set<QName> missingAspects = getMissingAspects(existingAspects, allProperties, classQName);
        missingAspects.addAll(extraAspects);
        
        if (assocTypeQName != null)
        {
            missingAspects.addAll(getMissingAspectsAssoc(existingAspects, allProperties, assocTypeQName));
        }
        
        // Notify 'before' adding aspect
        for (QName missingAspect : missingAspects)
        {
            invokeBeforeAddAspect(nodeRef, missingAspect);
        }
        
        // Get all missing properties for aspects that are missing.
        //   This will include the type if the type was passed in.
        Set<QName> allClassQNames = new HashSet<QName>(13);
        allClassQNames.add(classQName);
        allClassQNames.addAll(missingAspects);
        Map<QName, Serializable> missingProperties = getMissingProperties(existingProperties, allClassQNames);
        missingProperties.putAll(extraProperties);
        
        // Bulk-add the properties
        boolean changedProperties = false;
        if (overwriteExistingProperties)
        {
            // Overwrite properties
            changedProperties = nodeDAO.setNodeProperties(nodeId, missingProperties);
        }
        else
        {
            // Append properties
            changedProperties = nodeDAO.addNodeProperties(nodeId, missingProperties);
        }
        if (changedProperties && invokeOnUpdateProperties)
        {
            Map<QName, Serializable> propertiesAfter = nodeDAO.getNodeProperties(nodeId);
            invokeOnUpdateProperties(nodeRef, existingProperties, propertiesAfter);
        }
        // Bulk-add the aspects
        boolean changedAspects = nodeDAO.addNodeAspects(nodeId, missingAspects);
        if (changedAspects)
        {
            for (QName missingAspect : missingAspects)
            {
                invokeOnAddAspect(nodeRef, missingAspect);
            }
        }
        // Done
        return changedAspects || changedProperties;
    }
    
    private Set<QName> getMissingAspectsAssoc(
            Set<QName> existingAspects,
            Map<QName, Serializable> existingProperties,
            QName assocTypeQName)
    {
            AssociationDefinition assocDef = dictionaryService.getAssociation(assocTypeQName);
            if (assocDef == null)
            {
                return Collections.emptySet();
            }
            ClassDefinition classDefinition = assocDef.getSourceClass();
            return getMissingAspects(existingAspects, existingProperties, classDefinition.getName());
    }
    
    /**
     * Get any aspects that should be added given the type, properties and existing aspects.
     * Note that this <b>does not</b> included a search for properties required for the missing
     * aspects.
     * 
     * @param classQName    the type, aspect or association
     * @return              Returns any aspects that should be added
     */
    private Set<QName> getMissingAspects(
            Set<QName> existingAspects,
            Map<QName, Serializable> existingProperties,
            QName classQName)
    {
        // Copy incoming existing values so that we can modify appropriately
        existingAspects = new HashSet<QName>(existingAspects);
        
        ClassDefinition classDefinition = dictionaryService.getClass(classQName);
        if (classDefinition == null)
        {
            return Collections.emptySet();
        }

        Set<QName> missingAspects = new HashSet<QName>(7);
        // Check that the aspect itself is present (only applicable for aspects)
        if (classDefinition.isAspect() && !existingAspects.contains(classQName))
        {
            missingAspects.add(classQName);
        }
        
        // Find all aspects that should be present on the class
        List<AspectDefinition> defaultAspectDefs = classDefinition.getDefaultAspects();
        for (AspectDefinition defaultAspectDef : defaultAspectDefs)
        {
            QName defaultAspect = defaultAspectDef.getName();
            if (!existingAspects.contains(defaultAspect))
            {
                missingAspects.add(defaultAspect);
            }
        }
        // Find all aspects that should be present given the existing properties
        for (QName existingPropQName : existingProperties.keySet())
        {
            PropertyDefinition existingPropDef = dictionaryService.getProperty(existingPropQName);
            if (existingPropDef == null || !existingPropDef.getContainerClass().isAspect())
            {
                continue;           // Property is undefined or belongs to a class
            }
            QName existingPropDefiningType = existingPropDef.getContainerClass().getName();
            if (!existingAspects.contains(existingPropDefiningType))
            {
                missingAspects.add(existingPropDefiningType);
            }
        }
        // If there were missing aspects, recurse to find further missing aspects
        //    Don't re-add ones we know about or we can end in infinite recursion.
        //    Don't send any properties because we don't want to reprocess them each time
        Set<QName> allTypesAndAspects = new HashSet<QName>(13);
        allTypesAndAspects.add(classQName);
        allTypesAndAspects.addAll(existingAspects);
        allTypesAndAspects.addAll(missingAspects);
        Set<QName> missingAspectsCopy = new HashSet<QName>(missingAspects);
        for (QName missingAspect : missingAspectsCopy)
        {
            Set<QName> furtherMissingAspects = getMissingAspects(
                        allTypesAndAspects,
                        Collections.<QName, Serializable>emptyMap(),
                        missingAspect);
            missingAspects.addAll(furtherMissingAspects);
            allTypesAndAspects.addAll(furtherMissingAspects);
        }
        // Done
        return missingAspects;
    }
    
    /**
     * @param existingProperties    existing node properties
     * @param classQNames           the types or aspects to introspect
     * @return                      Returns any properties that should be added
     */
    private Map<QName, Serializable> getMissingProperties(Map<QName, Serializable> existingProperties, Set<QName> classQNames)
    {
        Map<QName, Serializable> allDefaultProperties = new HashMap<QName, Serializable>(17);
        for (QName classQName : classQNames)
        {
            ClassDefinition classDefinition = dictionaryService.getClass(classQName);
            if (classDefinition == null)
            {
                continue;
            }
            // Get the default properties for this type/aspect
            Map<QName, Serializable> defaultProperties = getDefaultProperties(classQName);
            if (defaultProperties.size() > 0)
            {
                allDefaultProperties.putAll(defaultProperties);
            }
        }
        // Work out what is missing
        Map<QName, Serializable> missingProperties = new HashMap<QName, Serializable>(allDefaultProperties);
        missingProperties.keySet().removeAll(existingProperties.keySet());
        // Done
        return missingProperties;
    }
    
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public void setChildAssociationIndex(ChildAssociationRef childAssocRef, int index)
    {
        // get nodes
        Pair<Long, NodeRef> parentNodePair = getNodePairNotNull(childAssocRef.getParentRef());
        Pair<Long, NodeRef> childNodePair = getNodePairNotNull(childAssocRef.getChildRef());
        
        Long parentNodeId = parentNodePair.getFirst();
        Long childNodeId = childNodePair.getFirst();
        QName assocTypeQName = childAssocRef.getTypeQName();
        QName assocQName = childAssocRef.getQName();
        
        // set the index
        int updated = nodeDAO.setChildAssocIndex(
                parentNodeId, childNodeId, assocTypeQName, assocQName, index);
        if (updated < 1)
        {
            throw new InvalidChildAssociationRefException(
                    "Unable to set child association index: \n" +
                    "   assoc: " + childAssocRef + "\n" +
                    "   index: " + index,
                    childAssocRef);
        }
    }

    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public QName getType(NodeRef nodeRef) throws InvalidNodeRefException
    {
        Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
        return nodeDAO.getNodeType(nodePair.getFirst());
    }
    
    /**
     * @see org.alfresco.service.cmr.repository.NodeService#setType(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName)
     */
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public void setType(NodeRef nodeRef, QName typeQName) throws InvalidNodeRefException
    {
        // The node(s) involved may not be pending deletion
        checkPendingDelete(nodeRef);
        
        // check the node type
        TypeDefinition nodeTypeDef = dictionaryService.getType(typeQName);
        if (nodeTypeDef == null)
        {
            throw new InvalidTypeException(typeQName);
        }
        Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
        
        // Invoke policies
        invokeBeforeUpdateNode(nodeRef);
        QName oldType = nodeDAO.getNodeType(nodePair.getFirst());
        invokeBeforeSetType(nodeRef, oldType, typeQName);
        
        // Set the type
        boolean updatedNode = nodeDAO.updateNode(nodePair.getFirst(), typeQName, null);
        
        // Add the default aspects and properties required for the given type. Existing values will not be overridden.
        boolean updatedProps = addAspectsAndProperties(nodePair, typeQName, null, null, null, null, false);
        
        // Invoke policies
        if (updatedNode || updatedProps)
        {
            // Invoke policies
            invokeOnUpdateNode(nodeRef);
            invokeOnSetType(nodeRef, oldType, typeQName);
        }
    }
    
    @Override
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public void addAspect(
            NodeRef nodeRef,
            QName aspectTypeQName,
            Map<QName, Serializable> aspectProperties)
            throws InvalidNodeRefException, InvalidAspectException
    {
        // check that the aspect is legal
        AspectDefinition aspectDef = dictionaryService.getAspect(aspectTypeQName);
        if (aspectDef == null)
        {
            throw new InvalidAspectException("The aspect is invalid: " + aspectTypeQName, aspectTypeQName);
        }
        
        // Don't allow spoofed aspect(s) to be added
        if (aspectTypeQName.equals(ContentModel.ASPECT_PENDING_DELETE))
        {
            throw new IllegalArgumentException("The aspect is reserved for system use: " + aspectTypeQName);
        }
        
        // Check the properties
        if (aspectProperties == null)
        {
            // Make a map
            aspectProperties = Collections.emptyMap();
        }
        // Make the properties immutable to be sure that they are not used incorrectly
        aspectProperties = Collections.unmodifiableMap(aspectProperties);
        
        // Invoke policy behaviours
        invokeBeforeUpdateNode(nodeRef);

        // Add aspect and defaults
        Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
        // SetProperties common tasks
        setPropertiesCommonWork(nodePair, aspectProperties);
        boolean modified = addAspectsAndProperties(
                    nodePair,
                    aspectTypeQName,
                    null,
                    null,
                    Collections.singleton(aspectTypeQName),
                    aspectProperties,
                    false);
        
        if (modified)
        {                                
            // Invoke policy behaviours
            invokeOnUpdateNode(nodeRef);
        }
    }

    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public int countChildAssocs(NodeRef nodeRef, boolean isPrimary) throws InvalidNodeRefException
    {    
        final Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
        final Long nodeId = nodePair.getFirst();
        return nodeDAO.countChildAssocsByParent(nodeId, isPrimary);
    }
    
    @Override
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public void removeAspect(NodeRef nodeRef, QName aspectTypeQName)
            throws InvalidNodeRefException, InvalidAspectException
    {
        // Don't allow spoofed aspect(s) to be removed
        if (aspectTypeQName.equals(ContentModel.ASPECT_PENDING_DELETE))
        {
            throw new IllegalArgumentException("The aspect is reserved for system use: " + aspectTypeQName);
        }
        
        /*
         * Note: Aspect and property removal is resilient to missing dictionary definitions
         */
        // get the node
        final Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
        final Long nodeId = nodePair.getFirst();
        
        boolean hadAspect = nodeDAO.hasNodeAspect(nodeId, aspectTypeQName);
        
        // Invoke policy behaviours
        invokeBeforeUpdateNode(nodeRef);
        if (hadAspect)
        {
            invokeBeforeRemoveAspect(nodeRef, aspectTypeQName);
            nodeDAO.removeNodeAspects(nodeId, Collections.singleton(aspectTypeQName));
        }
        
        AspectDefinition aspectDef = dictionaryService.getAspect(aspectTypeQName);
        boolean updated = false;
        if (aspectDef != null)
        {
            // Remove default properties
            Map<QName,PropertyDefinition> propertyDefs = aspectDef.getProperties();
            Set<QName> propertyToRemoveQNames = propertyDefs.keySet();
            nodeDAO.removeNodeProperties(nodeId, propertyToRemoveQNames);
            
            // Remove child associations
            // We have to iterate over the associations and remove all those between the parent and child
            final List<Pair<Long, ChildAssociationRef>> assocsToDelete = new ArrayList<Pair<Long, ChildAssociationRef>>(5);
            final List<Pair<Long, NodeRef>> nodesToDelete = new ArrayList<Pair<Long, NodeRef>>(5);
            NodeDAO.ChildAssocRefQueryCallback callback = new NodeDAO.ChildAssocRefQueryCallback()
            {
                public boolean preLoadNodes()
                {
                    return true;
                }

                @Override
                public boolean orderResults()
                {
                    return false;
                }

                public boolean handle(
                        Pair<Long, ChildAssociationRef> childAssocPair,
                        Pair<Long, NodeRef> parentNodePair,
                        Pair<Long, NodeRef> childNodePair
                        )
                {
                    if (isPendingDelete(parentNodePair.getSecond()) || isPendingDelete(childNodePair.getSecond()))
                    {
                        if (logger.isTraceEnabled())
                        {
                            logger.trace(
                                    "Aspect-triggered association removal: " +
                                    "Ignoring child associations where one of the nodes is pending delete: " + childAssocPair);
                        }
                        return true;
                    }
                    
                    // Double check that it's not a primary association.  If so, we can't delete it and
                    //    have to delete the child node directly and with full archival.
                    if (childAssocPair.getSecond().isPrimary())
                    {
                        nodesToDelete.add(childNodePair);
                    }
                    else
                    {
                        assocsToDelete.add(childAssocPair);
                    }
                    // More results
                    return true;
                }

                public void done()
                {
                }                               
            };
            // Get all the QNames to remove
            Set<QName> assocTypeQNamesToRemove = new HashSet<QName>(aspectDef.getChildAssociations().keySet());
            nodeDAO.getChildAssocs(nodeId, assocTypeQNamesToRemove, callback);
            // Delete all the collected associations
            for (Pair<Long, ChildAssociationRef> assocPair : assocsToDelete)
            {
                updated = true;
                Long assocId = assocPair.getFirst();
                ChildAssociationRef assocRef = assocPair.getSecond();
                // delete the association instance - it is not primary
                invokeBeforeDeleteChildAssociation(assocRef);
                nodeDAO.deleteChildAssoc(assocId);
                invokeOnDeleteChildAssociation(assocRef);
            }
            
            // Cascade-delete any nodes that were attached to primary associations
            for (Pair<Long, NodeRef> childNodePair : nodesToDelete)
            {
                NodeRef childNodeRef = childNodePair.getSecond();
                this.deleteNode(childNodeRef);
            }
            
            // Gather peer associations to delete
            Map<QName, AssociationDefinition> nodeAssocDefs = aspectDef.getAssociations();
            List<Long> nodeAssocIdsToRemove = new ArrayList<Long>(13);
            List<AssociationRef> assocRefsRemoved = new ArrayList<AssociationRef>(13);
            for (Map.Entry<QName, AssociationDefinition> entry : nodeAssocDefs.entrySet())
            {
                if (isPendingDelete(nodeRef))
                {
                    if (logger.isTraceEnabled())
                    {
                        logger.trace(
                                "Aspect-triggered association removal: " +
                                "Ignoring peer associations where one of the nodes is pending delete: " + nodeRef);
                    }
                    continue;
                }
                if (entry.getValue().isChild())
                {
                    // Not interested in child assocs
                    continue;
                }
                QName assocTypeQName = entry.getKey();
                Collection<Pair<Long, AssociationRef>> targetAssocRefs = nodeDAO.getTargetNodeAssocs(nodeId, assocTypeQName);
                for (Pair<Long, AssociationRef> assocPair : targetAssocRefs)
                {
                    if (isPendingDelete(assocPair.getSecond().getTargetRef()))
                    {
                        if (logger.isTraceEnabled())
                        {
                            logger.trace(
                                    "Aspect-triggered association removal: " +
                                    "Ignoring peer associations where one of the nodes is pending delete: " + assocPair);
                        }
                        continue;
                    }
                    nodeAssocIdsToRemove.add(assocPair.getFirst());
                    assocRefsRemoved.add(assocPair.getSecond());
                }
                // MNT-9580: Daisy chained cm:original associations are cascade-deleted when the first original is deleted
                //           As a side-effect of the investigation of MNT-9446, it was dicovered that inbound associations (ones pointing *to* this aspect)
                //           were also being removed.  This is incorrect because the aspect being removed here has no say over who points at it.
                //           Therefore, do not remove inbound associations because we only define outbound associations on types and aspects.
                //           Integrity checking will ensure that the correct behaviours are in place to maintain model integrity.
            }
            // Now delete peer associations
            int assocsDeleted = nodeDAO.removeNodeAssocs(nodeAssocIdsToRemove);
            for (AssociationRef assocRefRemoved : assocRefsRemoved)
            {
                invokeOnDeleteAssociation(assocRefRemoved);
            }
            updated = updated || assocsDeleted > 0;
        }
        
        // Invoke policy behaviours
        if (updated)
        {
            invokeOnUpdateNode(nodeRef);
        }
        if (hadAspect)
        {
            invokeOnRemoveAspect(nodeRef, aspectTypeQName);
        }
    }

    /**
     * Performs a check on the set of node aspects
     */
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public boolean hasAspect(NodeRef nodeRef, QName aspectQName) throws InvalidNodeRefException, InvalidAspectException
    {
        if (aspectQName.equals(ContentModel.ASPECT_PENDING_DELETE))
        {
            return isPendingDelete(nodeRef);
        }
        Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
        return nodeDAO.hasNodeAspect(nodePair.getFirst(), aspectQName);
    }

    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public Set<QName> getAspects(NodeRef nodeRef) throws InvalidNodeRefException
    {
        Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
        Set<QName> aspectQNames = nodeDAO.getNodeAspects(nodePair.getFirst());
        if (isPendingDelete(nodeRef))
        {
            aspectQNames.add(ContentModel.ASPECT_PENDING_DELETE);
        }
        return aspectQNames;
    }

    /**
     * @return      Returns <tt>true</tt> if the node is being deleted
     * 
     * @see #KEY_PENDING_DELETE_NODES
     */
    private boolean isPendingDelete(NodeRef nodeRef)
    {
        // Avoid creating a Set if the transaction is read-only
        if (AlfrescoTransactionSupport.getTransactionReadState() != TxnReadState.TXN_READ_WRITE)
        {
            return false;
        }
        Set<NodeRef> nodesPendingDelete = TransactionalResourceHelper.getSet(KEY_PENDING_DELETE_NODES);
        return nodesPendingDelete.contains(nodeRef);
    }
    
    /**
     * @throws      IllegalStateException   if the node is pending delete
     * 
     * @see #KEY_PENDING_DELETE_NODES
     */
    private void checkPendingDelete(NodeRef nodeRef)
    {
        if (isPendingDelete(nodeRef))
        {
            throw new IllegalStateException(
                    "Operation not allowed against node pending deletion." +
                    "  Check the node for aspect " + ContentModel.ASPECT_PENDING_DELETE);
        }
    }
    
    /**
     * Delete Node
     */
    @Override
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public void deleteNode(NodeRef nodeRef)
    {
        deleteNode(nodeRef, true);
    }
    
    /**
     * Delete a node
     * 
     * @param nodeRef           the node to delete
     * @param allowArchival     <tt>true</tt> if normal archival may occur or
     *                          <tt>false</tt> if the node must be forcibly deleted
     */
    private void deleteNode(NodeRef nodeRef, boolean allowArchival)
    {
        // The node(s) involved may not be pending deletion
        checkPendingDelete(nodeRef);
        
        // Pair contains NodeId, NodeRef
        Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
        Long nodeId = nodePair.getFirst();

        Boolean requiresDelete = null;

        // get type and aspect QNames as they will be unavailable after the delete
        QName nodeTypeQName = nodeDAO.getNodeType(nodeId);
        Set<QName> nodeAspectQNames = nodeDAO.getNodeAspects(nodeId);

        // Have we been asked to delete a store?
        if (nodeTypeQName.equals(ContentModel.TYPE_STOREROOT))
        {
            throw new IllegalArgumentException("A store root node cannot be deleted: " + nodeRef);
        }

        // get the primary parent-child relationship before it is gone
        Pair<Long, ChildAssociationRef> childAssocPair = nodeDAO.getPrimaryParentAssoc(nodeId);
        ChildAssociationRef childAssocRef = childAssocPair.getSecond();

        // Is this store 
        StoreRef storeRef = nodeRef.getStoreRef();
        StoreRef archiveStoreRef = storeArchiveMap.get(storeRef);

        // Gather information about the hierarchy
        NodeHierarchyWalker walker = new NodeHierarchyWalker(nodeDAO);
        walker.walkHierarchy(nodePair, childAssocPair);
        
        // Protect the nodes from being link/unlinked for the remainder of the process
        Set<NodeRef> nodesPendingDelete = new HashSet<NodeRef>(walker.getNodes(false).size());
        for (VisitedNode visitedNode : walker.getNodes(true))
        {
            nodesPendingDelete.add(visitedNode.nodeRef);
        }
        Set<NodeRef> nodesPendingDeleteTxn = TransactionalResourceHelper.getSet(KEY_PENDING_DELETE_NODES);
        nodesPendingDeleteTxn.addAll(nodesPendingDelete);           // We need to remove these later, again
        
        // Work out whether we need to archive or delete the node.
        if (!allowArchival)
        {
            // No archival allowed
            requiresDelete = true;
        }
        else if (archiveStoreRef == null)
        {
            // The store does not specify archiving
            requiresDelete = true;
        }
        else
        {
            // get the type and check if we need archiving.
            TypeDefinition typeDef = dictionaryService.getType(nodeTypeQName);
            if (typeDef != null)
            {
                Boolean requiresArchive = typeDef.getArchive();
                if (requiresArchive != null)
                {
                    requiresDelete = !requiresArchive;
                }
            }

            // If the type hasn't asked for deletion, check whether any applied aspects have
            Iterator<QName> i = nodeAspectQNames.iterator();
            while ((requiresDelete == null || !requiresDelete) && i.hasNext())
            {
                QName nodeAspectQName = i.next();
                AspectDefinition aspectDef = dictionaryService.getAspect(nodeAspectQName);
                if (aspectDef != null)
                {
                    Boolean requiresArchive = aspectDef.getArchive();
                    if (requiresArchive != null)
                    {
                        requiresDelete = !requiresArchive;
                    }
                }
            }
        }
        
        // Propagate timestamps
        propagateTimeStamps(childAssocRef);

        // Archive, if necessary
        boolean archive = requiresDelete != null && !requiresDelete.booleanValue();

        // Fire pre-delete events
        Set<Long> childAssocIds = new HashSet<Long>(23);            // Prevents duplicate firing
        Set<Long> peerAssocIds = new HashSet<Long>(23);            // Prevents duplicate firing
        List<VisitedNode> nodesToDelete = walker.getNodes(true);
        for (VisitedNode nodeToDelete : nodesToDelete)
        {
            // Target associations
            for (Pair<Long, AssociationRef> targetAssocPair : nodeToDelete.targetAssocs)
            {
                if (!peerAssocIds.add(targetAssocPair.getFirst()))
                {
                    continue;           // Already fired
                }
                invokeBeforeDeleteAssociation(targetAssocPair.getSecond());
            }
            // Source associations
            for (Pair<Long, AssociationRef> sourceAssocPair : nodeToDelete.sourceAssocs)
            {
                if (!peerAssocIds.add(sourceAssocPair.getFirst()))
                {
                    continue;           // Already fired
                }
                invokeBeforeDeleteAssociation(sourceAssocPair.getSecond());
            }
            // Secondary child associations
            for (Pair<Long, ChildAssociationRef> secondaryChildAssocPair : nodeToDelete.secondaryChildAssocs)
            {
                if (!childAssocIds.add(secondaryChildAssocPair.getFirst()))
                {
                    continue;           // Already fired
                }
                invokeBeforeDeleteChildAssociation(secondaryChildAssocPair.getSecond());
            }
            // Secondary parent associations
            for (Pair<Long, ChildAssociationRef> secondaryParentAssocPair : nodeToDelete.secondaryParentAssocs)
            {
                if (!childAssocIds.add(secondaryParentAssocPair.getFirst()))
                {
                    continue;           // Already fired
                }
                invokeBeforeDeleteChildAssociation(secondaryParentAssocPair.getSecond());
            }
            
            // Primary child associations
            if (archive)
            {
                invokeBeforeArchiveNode(nodeToDelete.nodeRef);
            }
            invokeBeforeDeleteNode(nodeToDelete.nodeRef);
        }
        
        // Archive, if necessary
        if (archive)
        {
            // Archive node
            archiveHierarchy(walker, archiveStoreRef);
        }

        // Delete/Archive and fire post-delete events incl. updating indexes
        childAssocIds.clear();                                    // Prevents duplicate firing
        peerAssocIds.clear();                                     // Prevents duplicate firing
        for (VisitedNode nodeToDelete : nodesToDelete)
        {
            // Target associations
            for (Pair<Long, AssociationRef> targetAssocPair : nodeToDelete.targetAssocs)
            {
                if (!peerAssocIds.add(targetAssocPair.getFirst()))
                {
                    continue;           // Already fired
                }
                nodeDAO.removeNodeAssocs(Collections.singletonList(targetAssocPair.getFirst()));
                invokeOnDeleteAssociation(targetAssocPair.getSecond());
            }
            // Source associations
            for (Pair<Long, AssociationRef> sourceAssocPair : nodeToDelete.sourceAssocs)
            {
                if (!peerAssocIds.add(sourceAssocPair.getFirst()))
                {
                    continue;           // Already fired
                }
                nodeDAO.removeNodeAssocs(Collections.singletonList(sourceAssocPair.getFirst()));
                invokeOnDeleteAssociation(sourceAssocPair.getSecond());
            }
            // Secondary child associations
            for (Pair<Long, ChildAssociationRef> secondaryChildAssocPair : nodeToDelete.secondaryChildAssocs)
            {
                if (!childAssocIds.add(secondaryChildAssocPair.getFirst()))
                {
                    continue;           // Already fired
                }
                nodeDAO.deleteChildAssoc(secondaryChildAssocPair.getFirst());
                invokeOnDeleteChildAssociation(secondaryChildAssocPair.getSecond());
            }
            // Secondary parent associations
            for (Pair<Long, ChildAssociationRef> secondaryParentAssocPair : nodeToDelete.secondaryParentAssocs)
            {
                if (!childAssocIds.add(secondaryParentAssocPair.getFirst()))
                {
                    continue;           // Already fired
                }
                nodeDAO.deleteChildAssoc(secondaryParentAssocPair.getFirst());
                invokeOnDeleteChildAssociation(secondaryParentAssocPair.getSecond());
            }
            QName childNodeTypeQName = nodeDAO.getNodeType(nodeToDelete.id);
            Set<QName> childAspectQnames = nodeDAO.getNodeAspects(nodeToDelete.id);
            // Delete the node
            nodeDAO.deleteChildAssoc(nodeToDelete.primaryParentAssocPair.getFirst());
            nodeDAO.deleteNode(nodeToDelete.id);
            invokeOnDeleteNode(
                    nodeToDelete.primaryParentAssocPair.getSecond(),
                    childNodeTypeQName, childAspectQnames, archive);
        }
        
        // Clear out the list of nodes pending delete
        nodesPendingDeleteTxn = TransactionalResourceHelper.getSet(KEY_PENDING_DELETE_NODES);
        nodesPendingDeleteTxn.removeAll(nodesPendingDelete);
    }
    
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public ChildAssociationRef addChild(NodeRef parentRef, NodeRef childRef, QName assocTypeQName, QName assocQName)
    {
        return addChild(Collections.singletonList(parentRef), childRef, assocTypeQName, assocQName).get(0);
    }

    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public List<ChildAssociationRef> addChild(Collection<NodeRef> parentRefs, NodeRef childRef, QName assocTypeQName, QName assocQName)
    {
        // The node(s) involved may not be pending deletion
        checkPendingDelete(childRef);
        
        // Get the node's name, if present
        Pair<Long, NodeRef> childNodePair = getNodePairNotNull(childRef);
        Long childNodeId = childNodePair.getFirst();
        Map<QName, Serializable> childNodeProperties = nodeDAO.getNodeProperties(childNodePair.getFirst());
        String childNodeName = extractNameProperty(childNodeProperties);
        if (childNodeName == null)
        {
            childNodeName = childRef.getId();
        }

        List <ChildAssociationRef> childAssociationRefs = new ArrayList<ChildAssociationRef>(parentRefs.size());
        List<Pair<Long, NodeRef>> parentNodePairs = new ArrayList<Pair<Long, NodeRef>>(parentRefs.size());
        for (NodeRef parentRef : parentRefs)
        {
            // The node(s) involved may not be pending deletion
            checkPendingDelete(parentRef);
            
            Pair<Long, NodeRef> parentNodePair = getNodePairNotNull(parentRef);
            Long parentNodeId = parentNodePair.getFirst();
            parentNodePairs.add(parentNodePair);

            // make the association
            Pair<Long, ChildAssociationRef> childAssocPair = nodeDAO.newChildAssoc(
                    parentNodeId, childNodeId,
                    assocTypeQName, assocQName,
                    childNodeName);

            childAssociationRefs.add(childAssocPair.getSecond());
        }
        
        // check that the child addition of the child has not created a cyclic relationship
        nodeDAO.cycleCheck(childNodeId);

        // Invoke policy behaviours
        for (ChildAssociationRef childAssocRef : childAssociationRefs)
        {
            invokeOnCreateChildAssociation(childAssocRef, false);
        }
        
        // Get the type associated with the association
        // The association may be sourced on an aspect, which may itself mandate further aspects
        for (Pair<Long, NodeRef> parentNodePair : parentNodePairs)
        {
            addAspectsAndPropertiesAssoc(parentNodePair, assocTypeQName, null, null, null, null, false);
        }

        return childAssociationRefs;
    }

    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public void removeChild(NodeRef parentRef, NodeRef childRef) throws InvalidNodeRefException
    {
        // The node(s) involved may not be pending deletion
        checkPendingDelete(parentRef);
        checkPendingDelete(childRef);
        
        final Pair<Long, NodeRef> parentNodePair = getNodePairNotNull(parentRef);
        final Long parentNodeId = parentNodePair.getFirst();
        final Pair<Long, NodeRef> childNodePair = getNodePairNotNull(childRef);
        final Long childNodeId = childNodePair.getFirst();
        
        // Get the primary parent association for the child
        Pair<Long, ChildAssociationRef> primaryChildAssocPair = nodeDAO.getPrimaryParentAssoc(childNodeId);
        // We can shortcut if our parent is also the primary parent
        if (primaryChildAssocPair != null)
        {
            NodeRef primaryParentNodeRef = primaryChildAssocPair.getSecond().getParentRef();
            if (primaryParentNodeRef.equals(parentRef))
            {
                // Shortcut - just delete the child node
                deleteNode(childRef);
                return;
            }
        }
        
        // We have to iterate over the associations and remove all those between the parent and child
        final List<Pair<Long, ChildAssociationRef>> assocsToDelete = new ArrayList<Pair<Long, ChildAssociationRef>>(5);
        NodeDAO.ChildAssocRefQueryCallback callback = new NodeDAO.ChildAssocRefQueryCallback()
        {
            public boolean preLoadNodes()
            {
                return true;
            }

            @Override
            public boolean orderResults()
            {
                return false;
            }

            public boolean handle(
                    Pair<Long, ChildAssociationRef> childAssocPair,
                    Pair<Long, NodeRef> parentNodePair,
                    Pair<Long, NodeRef> childNodePair)
            {
                // Ignore if the child is not ours (redundant check)
                if (!childNodePair.getFirst().equals(childNodeId))
                {
                    return false;
                }
                // Add it
                assocsToDelete.add(childAssocPair);
                // More results
                return true;
            }

            public void done()
            {
            }                               
        };
        nodeDAO.getChildAssocs(parentNodeId, childNodeId, null, null, null, null, callback);
        
        // Delete all the collected associations
        for (Pair<Long, ChildAssociationRef> assocPair : assocsToDelete)
        {
            Long assocId = assocPair.getFirst();
            ChildAssociationRef assocRef = assocPair.getSecond();
            // delete the association instance - it is not primary
            invokeBeforeDeleteChildAssociation(assocRef);
            nodeDAO.deleteChildAssoc(assocId);
            invokeOnDeleteChildAssociation(assocRef);
        }

        // Done
    }
    
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public boolean removeChildAssociation(ChildAssociationRef childAssocRef)
    {
        // The node(s) involved may not be pending deletion
        checkPendingDelete(childAssocRef.getParentRef());
        checkPendingDelete(childAssocRef.getChildRef());
        
        Long parentNodeId = getNodePairNotNull(childAssocRef.getParentRef()).getFirst();
        Long childNodeId = getNodePairNotNull(childAssocRef.getChildRef()).getFirst();
        QName assocTypeQName = childAssocRef.getTypeQName();
        QName assocQName = childAssocRef.getQName();
        Pair<Long, ChildAssociationRef> assocPair = nodeDAO.getChildAssoc(
                parentNodeId, childNodeId, assocTypeQName, assocQName);
        if (assocPair == null)
        {
            // No association exists
            return false;
        }
        Long assocId = assocPair.getFirst();
        ChildAssociationRef assocRef = assocPair.getSecond();
        if (assocRef.isPrimary())
        {
            NodeRef childNodeRef = assocRef.getChildRef();
            // Delete the child node
            this.deleteNode(childNodeRef);
            // Done
            return true;
        }
        else
        {
            // Delete the association
            invokeBeforeDeleteChildAssociation(childAssocRef);
            nodeDAO.deleteChildAssoc(assocId);
            invokeOnDeleteChildAssociation(childAssocRef);
            // Done
            return true;
        }
    }

    @Override
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public boolean removeSecondaryChildAssociation(ChildAssociationRef childAssocRef)
    {
        // The node(s) involved may not be pending deletion
        checkPendingDelete(childAssocRef.getParentRef());
        checkPendingDelete(childAssocRef.getChildRef());
        
        Long parentNodeId = getNodePairNotNull(childAssocRef.getParentRef()).getFirst();
        Long childNodeId = getNodePairNotNull(childAssocRef.getChildRef()).getFirst();
        QName assocTypeQName = childAssocRef.getTypeQName();
        QName assocQName = childAssocRef.getQName();
        Pair<Long, ChildAssociationRef> assocPair = nodeDAO.getChildAssoc(
                parentNodeId, childNodeId, assocTypeQName, assocQName);
        if (assocPair == null)
        {
            // No association exists
            return false;
        }
        Long assocId = assocPair.getFirst();
        ChildAssociationRef assocRef = assocPair.getSecond();
        if (assocRef.isPrimary())
        {
            throw new IllegalArgumentException(
                    "removeSeconaryChildAssociation can not be applied to a primary association: \n" +
                    "   Child Assoc: " + assocRef);
        }
        // Delete the secondary association
        invokeBeforeDeleteChildAssociation(childAssocRef);
        nodeDAO.deleteChildAssoc(assocId);
        invokeOnDeleteChildAssociation(childAssocRef);
        // Done
        return true;
    }

    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public Serializable getProperty(NodeRef nodeRef, QName qname) throws InvalidNodeRefException
    {
        Long nodeId = getNodePairNotNull(nodeRef).getFirst();
        // Spoof referencable properties
        if (qname.equals(ContentModel.PROP_STORE_PROTOCOL))
        {
            return nodeRef.getStoreRef().getProtocol();
        }
        else if (qname.equals(ContentModel.PROP_STORE_IDENTIFIER))
        {
            return nodeRef.getStoreRef().getIdentifier();
        }
        else if (qname.equals(ContentModel.PROP_NODE_UUID))
        {
            return nodeRef.getId();
        }
        else if (qname.equals(ContentModel.PROP_NODE_DBID))
        {
            return nodeId;
        }
        
        Serializable property = nodeDAO.getNodeProperty(nodeId, qname);
        
        // check if we need to provide a spoofed name
        if (property == null && qname.equals(ContentModel.PROP_NAME))
        {
            return nodeRef.getId();
        }
        
        // done
        return property;
    }

    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public Map<QName, Serializable> getProperties(NodeRef nodeRef) throws InvalidNodeRefException
    {
        Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
        return getPropertiesImpl(nodePair);
    }

    /**
     * Gets, converts and adds the intrinsic properties to the current node's properties
     */
    private Map<QName, Serializable> getPropertiesImpl(Pair<Long, NodeRef> nodePair) throws InvalidNodeRefException
    {
        Long nodeId = nodePair.getFirst();
        Map<QName, Serializable> nodeProperties = nodeDAO.getNodeProperties(nodeId);
        // done
        return nodeProperties;
    }
    
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public Long getNodeAclId(NodeRef nodeRef) throws InvalidNodeRefException
    {
        Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
        return getAclIDImpl(nodePair);
    }

    /**
     * Gets, converts and adds the intrinsic properties to the current node's properties
     */
    private Long getAclIDImpl(Pair<Long, NodeRef> nodePair) throws InvalidNodeRefException
    {
        Long nodeId = nodePair.getFirst();
        Long aclID = nodeDAO.getNodeAclId(nodeId);
        // done
        return aclID;
    }
    
    /**
     * Performs additional tasks associated with setting a property.
     * 
     * @return      Returns <tt>true</tt> if any work was done by this method
     */
    private boolean setPropertiesCommonWork(Pair<Long, NodeRef> nodePair, Map<QName, Serializable> properties)
    {
        Long nodeId = nodePair.getFirst();

        boolean changed = false;
        // cm:name special handling
        if (properties.containsKey(ContentModel.PROP_NAME))
        {
            String name = extractNameProperty(properties);
            Pair<Long, ChildAssociationRef> primaryParentAssocPair = nodeDAO.getPrimaryParentAssoc(nodeId);
            if (primaryParentAssocPair != null)
            {
                String oldName = extractNameProperty(nodeDAO.getNodeProperties(nodeId));
                String newName = DefaultTypeConverter.INSTANCE.convert(String.class, name);
                changed = setChildNameUnique(nodePair, newName, oldName);
            }
        }
        // Done
        return changed;
    }
    
    /**
     * Gets the properties map, sets the value (null is allowed) and checks that the new set
     * of properties is valid.
     * 
     */
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public void setProperty(NodeRef nodeRef, QName qname, Serializable value) throws InvalidNodeRefException
    {
        ParameterCheck.mandatory("nodeRef", nodeRef);
        ParameterCheck.mandatory("qname", qname);
        
        // The UUID cannot be explicitly changed
        if (qname.equals(ContentModel.PROP_NODE_UUID))
        {
            throw new IllegalArgumentException("The node UUID cannot be changed.");
        }

        // get the node
        Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
        
        // Invoke policy behaviour
        invokeBeforeUpdateNode(nodeRef);
        
        // cm:name special handling
        setPropertiesCommonWork(
                    nodePair,
                    Collections.singletonMap(qname, value));

        // Add the property and all required defaults
        boolean changed = addAspectsAndProperties(
                    nodePair, null,
                    null, null,
                    null, Collections.singletonMap(qname, value), false);
        
        if (changed)
        {
            // Invoke policy behaviour
            invokeOnUpdateNode(nodeRef);
        }
    }
    
    /**
     * Ensures that all required properties are present on the node and copies the
     * property values to the <code>Node</code>.
     * <p>
     * To remove a property, <b>remove it from the map</b> before calling this method.
     * Null-valued properties are allowed.
     * <p>
     * If any of the values are null, a marker object is put in to mimic nulls.  They will be turned back into
     * a real nulls when the properties are requested again.
     * 
     */
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public void setProperties(NodeRef nodeRef, Map<QName, Serializable> properties) throws InvalidNodeRefException
    {
        Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
        
        // Invoke policy behaviours
        invokeBeforeUpdateNode(nodeRef);

        // SetProperties common tasks
        setPropertiesCommonWork(nodePair, properties);
        
        // Set properties and defaults, overwriting the existing properties
        boolean changed = addAspectsAndProperties(nodePair, null, null, null, null, properties, true);
        
        if (changed)
        {
            // Invoke policy behaviours
            invokeOnUpdateNode(nodeRef);
        }
    }
    
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public void addProperties(NodeRef nodeRef, Map<QName, Serializable> properties) throws InvalidNodeRefException
    {
        Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
        
        // Invoke policy behaviours
        invokeBeforeUpdateNode(nodeRef);
        
        // cm:name special handling
        setPropertiesCommonWork(nodePair, properties);

        // Add properties and defaults
        boolean changed = addAspectsAndProperties(nodePair, null, null, null, null, properties, false);
        
        if (changed)
        {
            // Invoke policy behaviours
            invokeOnUpdateNode(nodeRef);
        }
    }
    
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public void removeProperty(NodeRef nodeRef, QName qname) throws InvalidNodeRefException
    {
        // Get the node
        Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
        Long nodeId = nodePair.getFirst();
        
        // Invoke policy behaviours
        invokeBeforeUpdateNode(nodeRef);
        
        // Get the values before
        Map<QName, Serializable> propertiesBefore = getPropertiesImpl(nodePair);
        
        // cm:name special handling
        if (qname.equals(ContentModel.PROP_NAME))
        {
            String oldName = extractNameProperty(nodeDAO.getNodeProperties(nodeId));
            String newName = null;
            setChildNameUnique(nodePair, newName, oldName);
        }

        // Remove
        nodeDAO.removeNodeProperties(nodeId, Collections.singleton(qname));
        
        // Invoke policy behaviours
        Map<QName, Serializable> propertiesAfter = getPropertiesImpl(nodePair);
        invokeOnUpdateNode(nodeRef);
        invokeOnUpdateProperties(nodeRef, propertiesBefore, propertiesAfter);
    }

    public Collection<NodeRef> getParents(NodeRef nodeRef) throws InvalidNodeRefException
    {
        List<ChildAssociationRef> parentAssocs = getParentAssocs(
                nodeRef,
                RegexQNamePattern.MATCH_ALL,
                RegexQNamePattern.MATCH_ALL);
        
        // Copy into the set to avoid duplicates
        Set<NodeRef> parentNodeRefs = new HashSet<NodeRef>(parentAssocs.size());
        for (ChildAssociationRef parentAssoc : parentAssocs)
        {
            NodeRef parentNodeRef = parentAssoc.getParentRef();
            parentNodeRefs.add(parentNodeRef);
        }
        // Done
        return new ArrayList<NodeRef>(parentNodeRefs);
    }

    /**
     * Filters out any associations if their qname is not a match to the given pattern.
     */
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public List<ChildAssociationRef> getParentAssocs(
            final NodeRef nodeRef,
            final QNamePattern typeQNamePattern,
            final QNamePattern qnamePattern)
    {
        // Get the node
        Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
        Long nodeId = nodePair.getFirst();
        
        final List<ChildAssociationRef> results = new ArrayList<ChildAssociationRef>(10);
        // We have a callback handler to filter results
        ChildAssocRefQueryCallback callback = new ChildAssocRefQueryCallback()
        {
            public boolean preLoadNodes()
            {
                return false;
            }
            
            @Override
            public boolean orderResults()
            {
                return false;
            }

            public boolean handle(
                    Pair<Long, ChildAssociationRef> childAssocPair,
                    Pair<Long, NodeRef> parentNodePair,
                    Pair<Long, NodeRef> childNodePair)
            {
                if (!typeQNamePattern.isMatch(childAssocPair.getSecond().getTypeQName()))
                {
                    return true;
                }
                if (!qnamePattern.isMatch(childAssocPair.getSecond().getQName()))
                {
                    return true;
                }
                results.add(childAssocPair.getSecond());
                return true;
            }

            public void done()
            {
            }                               
        };
        
        // Get the assocs pointing to it
        QName typeQName = (typeQNamePattern instanceof QName) ? (QName) typeQNamePattern : null;
        QName qname = (qnamePattern instanceof QName) ? (QName) qnamePattern : null;
        
        nodeDAO.getParentAssocs(nodeId, typeQName, qname, null, callback);
        // done
        return results;
    }

    @Override
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public List<ChildAssociationRef> getParentAssocs(NodeRef nodeRef) throws InvalidNodeRefException
    {
        return super.getParentAssocs(nodeRef);
    }
    
    /**
     * Filters out any associations if their qname is not a match to the given pattern.
     */
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public List<ChildAssociationRef> getChildAssocs(NodeRef nodeRef, final QNamePattern typeQNamePattern, final QNamePattern qnamePattern)
    {
       return getChildAssocs(nodeRef, typeQNamePattern, qnamePattern, true) ;
    }

    /**
     * Filters out any associations if their qname is not a match to the given pattern.
     */
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public List<ChildAssociationRef> getChildAssocs(
            NodeRef nodeRef,
            final QNamePattern typeQNamePattern,
            final QNamePattern qnamePattern,
            final boolean preload)
    {
        return getChildAssocs(nodeRef, typeQNamePattern, qnamePattern, Integer.MAX_VALUE, preload);
            }
            
    /**
     * Fetches the first n child associations in an efficient manner
     */
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public List<ChildAssociationRef> getChildAssocs(
            NodeRef nodeRef,
            final QNamePattern typeQNamePattern,
            final QNamePattern qnamePattern,
            final int maxResults,
            final boolean preload)
    {
        // Get the node
        Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
        
        // We have a callback handler to filter results
        final List<ChildAssociationRef> results = new ArrayList<ChildAssociationRef>(10);
        ChildAssocRefQueryCallback callback = new ChildAssocRefQueryCallback()
        {
            public boolean preLoadNodes()
            {
                return preload;
            }
            
            @Override
            public boolean orderResults()
            {
                return true;
            }

            public boolean handle(
                    Pair<Long, ChildAssociationRef> childAssocPair,
                    Pair<Long, NodeRef> parentNodePair,
                    Pair<Long, NodeRef> childNodePair)
            {
                if (typeQNamePattern != null && !typeQNamePattern.isMatch(childAssocPair.getSecond().getTypeQName()))
                {
                    return true;
                }
                if (qnamePattern != null && !qnamePattern.isMatch(childAssocPair.getSecond().getQName()))
                {
                    return true;
                }
                results.add(childAssocPair.getSecond());
                return true;
            }

            public void done()
            {
            }                               
        };
        // Get the assocs pointing to it
        QName typeQName = (typeQNamePattern instanceof QName) ? (QName) typeQNamePattern : null;
        QName qname = (qnamePattern instanceof QName) ? (QName) qnamePattern : null;

        nodeDAO.getChildAssocs(nodePair.getFirst(), typeQName, qname, maxResults, callback);
        // Done
        return results;
    }

    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public List<ChildAssociationRef> getChildAssocs(NodeRef nodeRef, Set<QName> childNodeTypeQNames)
    {
        // Get the node
        Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
        Long nodeId = nodePair.getFirst();

        final List<ChildAssociationRef> results = new ArrayList<ChildAssociationRef>(100);
        
        NodeDAO.ChildAssocRefQueryCallback callback = new NodeDAO.ChildAssocRefQueryCallback()
        {
            public boolean preLoadNodes()
            {
                return true;
            }

            @Override
            public boolean orderResults()
            {
                return true;
            }

            public boolean handle(
                    Pair<Long, ChildAssociationRef> childAssocPair,
                    Pair<Long, NodeRef> parentNodePair,
                    Pair<Long, NodeRef> childNodePair)
            {
                results.add(childAssocPair.getSecond());
                // More results
                return true;
            }

            public void done()
            {
            }                               
        };
        // Get all child associations with the specific qualified name
        nodeDAO.getChildAssocsByChildTypes(nodeId, childNodeTypeQNames, callback);
        // Done
        return results;
    }

    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public NodeRef getChildByName(NodeRef nodeRef, QName assocTypeQName, String childName)
    {
        ParameterCheck.mandatory("childName", childName);
        ParameterCheck.mandatory("nodeRef", nodeRef);
        ParameterCheck.mandatory("assocTypeQName", assocTypeQName);
        
        // Get the node
        Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
        Long nodeId = nodePair.getFirst();

        Pair<Long, ChildAssociationRef> childAssocPair = nodeDAO.getChildAssoc(nodeId, assocTypeQName, childName);
        if (childAssocPair != null)
        {
            return childAssocPair.getSecond().getChildRef();
        }
        else
        {
            return null;
        }
    }

    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public List<ChildAssociationRef> getChildrenByName(NodeRef nodeRef, QName assocTypeQName, Collection<String> childNames)
    {
        // Get the node
        Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
        Long nodeId = nodePair.getFirst();

        final List<ChildAssociationRef> results = new ArrayList<ChildAssociationRef>(100);
        
        NodeDAO.ChildAssocRefQueryCallback callback = new NodeDAO.ChildAssocRefQueryCallback()
        {
            public boolean preLoadNodes()
            {
                return true;
            }            

            @Override
            public boolean orderResults()
            {
                return true;
            }

            public boolean handle(
                    Pair<Long, ChildAssociationRef> childAssocPair,
                    Pair<Long, NodeRef> parentNodePair,
                    Pair<Long, NodeRef> childNodePair)
            {
                results.add(childAssocPair.getSecond());
                // More results
                return true;
            }

            public void done()
            {
            }                               
        };
        // Get all child associations with the specific qualified name
        nodeDAO.getChildAssocs(nodeId, assocTypeQName, childNames, callback);
        // Done
        return results;
    }

    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public ChildAssociationRef getPrimaryParent(NodeRef nodeRef) throws InvalidNodeRefException
    {
        // Get the node
        Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
        Long nodeId = nodePair.getFirst();

        // get the primary parent assoc
        Pair<Long, ChildAssociationRef> assocPair = nodeDAO.getPrimaryParentAssoc(nodeId);

        // done - the assoc may be null for a root node
        ChildAssociationRef assocRef = null;
        if (assocPair == null)
        {
            assocRef = new ChildAssociationRef(null, null, null, nodeRef);
        }
        else
        {
            assocRef = assocPair.getSecond();
        }
        return assocRef;
    }

    @Override
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public AssociationRef createAssociation(NodeRef sourceRef, NodeRef targetRef, QName assocTypeQName)
            throws InvalidNodeRefException, AssociationExistsException
    {
        // The node(s) involved may not be pending deletion
        checkPendingDelete(sourceRef);
        checkPendingDelete(targetRef);
        
        Pair<Long, NodeRef> sourceNodePair = getNodePairNotNull(sourceRef);
        long sourceNodeId = sourceNodePair.getFirst();
        Pair<Long, NodeRef> targetNodePair = getNodePairNotNull(targetRef);
        long targetNodeId = targetNodePair.getFirst();

        // we are sure that the association doesn't exist - make it
        Long assocId = nodeDAO.newNodeAssoc(sourceNodeId, targetNodeId, assocTypeQName, -1);
        AssociationRef assocRef = new AssociationRef(assocId, sourceRef, assocTypeQName, targetRef);

        // Invoke policy behaviours
        invokeOnCreateAssociation(assocRef);
        
        // Add missing aspects
        addAspectsAndPropertiesAssoc(sourceNodePair, assocTypeQName, null, null, null, null, false);

        return assocRef;
    }   
    
    @Override
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public void setAssociations(NodeRef sourceRef, QName assocTypeQName, List<NodeRef> targetRefs)
    {
        // The node(s) involved may not be pending deletion
        checkPendingDelete(sourceRef);
        
        Pair<Long, NodeRef> sourceNodePair = getNodePairNotNull(sourceRef);
        Long sourceNodeId = sourceNodePair.getFirst();
        // First get the existing associations
        Collection<Pair<Long, AssociationRef>> assocsBefore = nodeDAO.getTargetNodeAssocs(sourceNodeId, assocTypeQName);
        Map<NodeRef, Long> targetRefsBefore = new HashMap<NodeRef, Long>(assocsBefore.size());
        Map<NodeRef, Long> toRemoveMap = new HashMap<NodeRef, Long>(assocsBefore.size());
        for (Pair<Long, AssociationRef> assocBeforePair : assocsBefore)
        {
            Long id = assocBeforePair.getFirst();
            NodeRef nodeRef = assocBeforePair.getSecond().getTargetRef();
            targetRefsBefore.put(nodeRef, id);
            toRemoveMap.put(nodeRef, id);
        }
        // Work out which associations need to be removed
        toRemoveMap.keySet().removeAll(targetRefs);
        // Fire policies for redundant assocs
        for (NodeRef targetRef : toRemoveMap.keySet())
        {
            AssociationRef assocRef = new AssociationRef(sourceRef, assocTypeQName, targetRef);
            invokeBeforeDeleteAssociation(assocRef);
        }
        // Remove reduncant assocs
        List<Long> toRemoveIds = new ArrayList<Long>(toRemoveMap.values());
        nodeDAO.removeNodeAssocs(toRemoveIds);
        
        // Work out which associations need to be added
        Set<NodeRef> toAdd = new HashSet<NodeRef>(targetRefs);
        toAdd.removeAll(targetRefsBefore.keySet());
        
        // Iterate over the desired result and create new or reset indexes
        int assocIndex = 1;
        for (NodeRef targetNodeRef : targetRefs)
        {
            // The node(s) involved may not be pending deletion
            checkPendingDelete(targetNodeRef);
            
            Long id = targetRefsBefore.get(targetNodeRef);
            // Is this an existing assoc?
            if (id != null)
            {
                // Update it
                nodeDAO.setNodeAssocIndex(id, assocIndex);
            }
            else
            {
                Long targetNodeId = getNodePairNotNull(targetNodeRef).getFirst();
                nodeDAO.newNodeAssoc(sourceNodeId, targetNodeId, assocTypeQName, assocIndex);
            }
            assocIndex++;
        }
        
        // Invoke policy behaviours
        for (NodeRef targetNodeRef : toAdd)
        {
            AssociationRef assocRef = new AssociationRef(sourceRef, assocTypeQName, targetNodeRef);
            invokeOnCreateAssociation(assocRef);
        }
    }

    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public Collection<ChildAssociationRef> getChildAssocsWithoutParentAssocsOfType(NodeRef parent, QName assocTypeQName)
    {
        // Get the parent node
        Pair<Long, NodeRef> nodePair = getNodePairNotNull(parent);
        Long parentNodeId = nodePair.getFirst();

        final List<ChildAssociationRef> results = new ArrayList<ChildAssociationRef>(100);

        NodeDAO.ChildAssocRefQueryCallback callback = new NodeDAO.ChildAssocRefQueryCallback()
        {
            public boolean preLoadNodes()
            {
                return false;
            }

            @Override
            public boolean orderResults()
            {
                return false;
            }

            public boolean handle(Pair<Long, ChildAssociationRef> childAssocPair, Pair<Long, NodeRef> parentNodePair,
                    Pair<Long, NodeRef> childNodePair)
            {
                results.add(childAssocPair.getSecond());
                // More results
                return true;
            }

            public void done()
            {
            }                               
        };
        // Get the child associations that meet the criteria
        nodeDAO.getChildAssocsWithoutParentAssocsOfType(parentNodeId, assocTypeQName, callback);
        // done
        return results;
    }
    
    /**
     * Specific properties <b>not</b> supported by {@link #getChildAssocsByPropertyValue(NodeRef, QName, Serializable)}
     */
    private static List<QName> getChildAssocsByPropertyValueBannedProps = new ArrayList<QName>();
    static 
    {
        getChildAssocsByPropertyValueBannedProps.add(ContentModel.PROP_NODE_DBID);
        getChildAssocsByPropertyValueBannedProps.add(ContentModel.PROP_NODE_UUID);
        getChildAssocsByPropertyValueBannedProps.add(ContentModel.PROP_NAME);
        getChildAssocsByPropertyValueBannedProps.add(ContentModel.PROP_MODIFIED);
        getChildAssocsByPropertyValueBannedProps.add(ContentModel.PROP_MODIFIER);
        getChildAssocsByPropertyValueBannedProps.add(ContentModel.PROP_CREATED);
        getChildAssocsByPropertyValueBannedProps.add(ContentModel.PROP_CREATOR);
    }
    
    @Override
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public List<ChildAssociationRef> getChildAssocsByPropertyValue(
            NodeRef nodeRef,
            QName propertyQName, 
            Serializable value)
    {
        // Get the node
        Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
        Long nodeId = nodePair.getFirst();

        // Check the QName is not one of the "special" system maintained ones.
        
        if (getChildAssocsByPropertyValueBannedProps.contains(propertyQName))
        {
            throw new IllegalArgumentException(
                    "getChildAssocsByPropertyValue does not allow search of system maintained properties: " + propertyQName);
        }
                
        final List<ChildAssociationRef> results = new ArrayList<ChildAssociationRef>(10);
        // We have a callback handler to filter results
        ChildAssocRefQueryCallback callback = new ChildAssocRefQueryCallback()
        {
            public boolean preLoadNodes()
            {
                return false;
            }
            
            @Override
            public boolean orderResults()
            {
                return true;
            }

            public boolean handle(
                    Pair<Long, ChildAssociationRef> childAssocPair,
                    Pair<Long, NodeRef> parentNodePair,
                    Pair<Long, NodeRef> childNodePair)
            {
                results.add(childAssocPair.getSecond());
                return true;
            }

            public void done()
            {
            }                               
        };
        // Get the assocs pointing to it
        nodeDAO.getChildAssocsByPropertyValue(nodeId, propertyQName, value, callback);
        // Done
        return results;
    }

    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public void removeAssociation(NodeRef sourceRef, NodeRef targetRef, QName assocTypeQName)
            throws InvalidNodeRefException
    {
        // The node(s) involved may not be pending deletion
        checkPendingDelete(sourceRef);
        checkPendingDelete(targetRef);
        
        Pair<Long, NodeRef> sourceNodePair = getNodePairNotNull(sourceRef);
        Long sourceNodeId = sourceNodePair.getFirst();
        Pair<Long, NodeRef> targetNodePair = getNodePairNotNull(targetRef);
        Long targetNodeId = targetNodePair.getFirst();

        AssociationRef assocRef = new AssociationRef(sourceRef, assocTypeQName, targetRef);
        // Invoke policy behaviours
        invokeBeforeDeleteAssociation(assocRef);

        // delete it
        int assocsDeleted = nodeDAO.removeNodeAssoc(sourceNodeId, targetNodeId, assocTypeQName);
        
        if (assocsDeleted > 0)
        {
            // Invoke policy behaviours
            invokeOnDeleteAssociation(assocRef);
        }
    }
    
    @Override
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public AssociationRef getAssoc(Long id)
    {
        Pair<Long, AssociationRef> nodeAssocPair = nodeDAO.getNodeAssocOrNull(id);
        return nodeAssocPair == null ? null : nodeAssocPair.getSecond();
    }

    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public List<AssociationRef> getTargetAssocs(NodeRef sourceRef, QNamePattern qnamePattern)
    {
        Pair<Long, NodeRef> sourceNodePair = getNodePairNotNull(sourceRef);
        Long sourceNodeId = sourceNodePair.getFirst();

        QName qnameFilter = null;
        if (qnamePattern instanceof QName)
        {
            qnameFilter = (QName) qnamePattern;
        }
        Collection<Pair<Long, AssociationRef>> assocPairs = nodeDAO.getTargetNodeAssocs(sourceNodeId, qnameFilter);
        List<AssociationRef> nodeAssocRefs = new ArrayList<AssociationRef>(assocPairs.size());
        for (Pair<Long, AssociationRef> assocPair : assocPairs)
        {
            AssociationRef assocRef = assocPair.getSecond();
            // check qname pattern, if not already filtered
            if (qnameFilter == null && !qnamePattern.isMatch(assocRef.getTypeQName()))
            {
                continue;   // the assoc name doesn't match the pattern given 
            }
            nodeAssocRefs.add(assocRef);
        }
        // done
        return nodeAssocRefs;
    }

    @Override
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public List<AssociationRef> getTargetAssocsByPropertyValue(NodeRef sourceRef, QNamePattern qnamePattern, QName propertyQName, Serializable propertyValue)
    {
        Pair<Long, NodeRef> sourceNodePair = getNodePairNotNull(sourceRef);
        Long sourceNodeId = sourceNodePair.getFirst();

        QName qnameFilter = null;
        if (qnamePattern instanceof QName)
        {
            qnameFilter = (QName) qnamePattern;
        }

         // Check the QName is not one of the "special" system maintained ones.
        if (getChildAssocsByPropertyValueBannedProps.contains(propertyQName))
        {
            throw new IllegalArgumentException(
                    "getTargetAssocsByPropertyValue does not allow search of system maintained properties: " + propertyQName);
        }

        Collection<Pair<Long, AssociationRef>> assocPairs = nodeDAO.getTargetAssocsByPropertyValue(sourceNodeId, qnameFilter, propertyQName, propertyValue);
        List<AssociationRef> nodeAssocRefs = new ArrayList<AssociationRef>(assocPairs.size());
        for (Pair<Long, AssociationRef> assocPair : assocPairs)
        {
            AssociationRef assocRef = assocPair.getSecond();
            // check qname pattern, if not already filtered
            if (qnameFilter == null && !qnamePattern.isMatch(assocRef.getTypeQName()))
            {
                continue;   // the assoc name doesn't match the pattern given 
            }
            nodeAssocRefs.add(assocRef);
        }
        // done
        return nodeAssocRefs;
    }
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public List<AssociationRef> getSourceAssocs(NodeRef targetRef, QNamePattern qnamePattern)
    {
        Pair<Long, NodeRef> targetNodePair = getNodePairNotNull(targetRef);
        Long targetNodeId = targetNodePair.getFirst();
        
        QName qnameFilter = null;
        if (qnamePattern instanceof QName)
        {
            qnameFilter = (QName) qnamePattern;
        }
        Collection<Pair<Long, AssociationRef>> assocPairs = nodeDAO.getSourceNodeAssocs(targetNodeId, qnameFilter);
        List<AssociationRef> nodeAssocRefs = new ArrayList<AssociationRef>(assocPairs.size());
        for (Pair<Long, AssociationRef> assocPair : assocPairs)
        {
            AssociationRef assocRef = assocPair.getSecond();
            // check qname pattern, if not already filtered
            if (qnameFilter == null && !qnamePattern.isMatch(assocRef.getTypeQName()))
            {
                continue;   // the assoc name doesn't match the pattern given 
            }
            nodeAssocRefs.add(assocRef);
        }
        // done
        return nodeAssocRefs;
    }
    
    /**
     * @see #getPaths(NodeRef, boolean)
     */
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public Path getPath(NodeRef nodeRef) throws InvalidNodeRefException
    {
        List<Path> paths = getPaths(nodeRef, true);   // checks primary path count
        if (paths.size() == 1)
        {
            return paths.get(0);   // we know there is only one
        }
        throw new RuntimeException("Primary path count not checked");  // checked by getPaths()
    }

    /**
     * When searching for <code>primaryOnly == true</code>, checks that there is exactly
     * one path.
     */
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public List<Path> getPaths(NodeRef nodeRef, boolean primaryOnly) throws InvalidNodeRefException
    {
        // get the starting node
        Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
        
        return nodeDAO.getPaths(nodePair, primaryOnly);
    }
    
    /**
     * Archives the node without the <b>cm:auditable</b> aspect behaviour
     */
    private void archiveHierarchy(NodeHierarchyWalker walker, StoreRef archiveStoreRef)
    {
        policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_AUDITABLE);
        try
        {
            archiveHierarchyImpl(walker, archiveStoreRef);
        }
        finally
        {
            policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_AUDITABLE);
        }
    }
    
    /**
     * Archive (direct copy) a node hierarchy
     * 
     * @param walker                the node hierarchy to archive
     * @param archiveStoreRef StoreRef
     */
    private void archiveHierarchyImpl(NodeHierarchyWalker walker, StoreRef archiveStoreRef)
    {
        // Start with the node we are archiving to
        Pair<Long, NodeRef> archiveStoreRootNodePair = nodeDAO.getRootNode(archiveStoreRef);
        
        // Work through the hierarchy from the top down and archive all the nodes
        boolean firstNode = true;
        Map<Long, Pair<Long, NodeRef>> archiveRecord = new HashMap<Long, Pair<Long, NodeRef>>(walker.getNodes(false).size() * 2);
        for (VisitedNode node : walker.getNodes(false))
        {
            // Get node metadata
            Map<QName, Serializable> archiveProperties = nodeDAO.getNodeProperties(node.id);
            Set<QName> archiveAspects = nodeDAO.getNodeAspects(node.id);

            // The first node gets special treatment as it contains the archival details
            ChildAssociationRef archivePrimaryParentAssocRef = null;
            final Pair<Long, NodeRef> archiveParentNodePair;
            if (firstNode)
            {
                // Attach top-level archival details
                ChildAssociationRef primaryParentAssocRef = node.primaryParentAssocPair.getSecond();
                archiveAspects.add(ContentModel.ASPECT_ARCHIVED);
                archiveProperties.put(ContentModel.PROP_ARCHIVED_BY, AuthenticationUtil.getFullyAuthenticatedUser());
                archiveProperties.put(ContentModel.PROP_ARCHIVED_DATE, new Date());
                archiveProperties.put(ContentModel.PROP_ARCHIVED_ORIGINAL_PARENT_ASSOC, primaryParentAssocRef);
                Serializable originalOwner = archiveProperties.get(ContentModel.PROP_OWNER);
                archiveProperties.put(ContentModel.PROP_ARCHIVED_ORIGINAL_OWNER, originalOwner != null ? originalOwner : OwnableService.NO_OWNER);

                // change the node ownership
                archiveAspects.add(ContentModel.ASPECT_OWNABLE);
                archiveProperties.put(ContentModel.PROP_OWNER, AuthenticationUtil.getFullyAuthenticatedUser());
                // Create new primary association
                archivePrimaryParentAssocRef = new ChildAssociationRef(
                        ContentModel.ASSOC_CHILDREN,
                        archiveStoreRootNodePair.getSecond(),
                        NodeArchiveService.QNAME_ARCHIVED_ITEM,
                        new NodeRef(archiveStoreRef, node.nodeRef.getId()),
                        true,
                        -1);
                archiveParentNodePair = archiveStoreRootNodePair;
            }
            else
            {
                ChildAssociationRef primaryParentAssocRef = node.primaryParentAssocPair.getSecond();
                NodeRef parentNodeRef = primaryParentAssocRef.getParentRef();
                // Look it up
                VisitedNode parentNode = walker.getNode(parentNodeRef);
                if (parentNode == null)
                {
                    throw new IllegalStateException("Expected that a child has a visited primary parent: " + primaryParentAssocRef);
                }
                // This needs to have been mapped to a new parent
                archiveParentNodePair = archiveRecord.get(parentNode.id);
                if (archiveParentNodePair == null)
                {
                    throw new IllegalStateException("Expected to have archived primary parent: " + primaryParentAssocRef);
                }
                // Build the primary association details
                archivePrimaryParentAssocRef = new ChildAssociationRef(
                        primaryParentAssocRef.getTypeQName(),
                        archiveParentNodePair.getSecond(),
                        primaryParentAssocRef.getQName(),
                        new NodeRef(archiveStoreRef, node.nodeRef.getId()),
                        true,
                        primaryParentAssocRef.getNthSibling());
            }
            
            // Invoke behaviours
            invokeBeforeCreateNode(
                    archivePrimaryParentAssocRef.getParentRef(),
                    archivePrimaryParentAssocRef.getTypeQName(),
                    archivePrimaryParentAssocRef.getQName(),
                    node.nodeType);
                    
            // Create a new node
            boolean attempted = false;
            Node archiveNode = null;
            while (true)
            {
                try
                {
                    ChildAssocEntity archiveChildAssocEntity = nodeDAO.newNode(
                            archiveParentNodePair.getFirst(),
                            archivePrimaryParentAssocRef.getTypeQName(),
                            archivePrimaryParentAssocRef.getQName(),
                            archiveStoreRef,
                            node.nodeRef.getId(),
                            node.nodeType,
                            (Locale) archiveProperties.get(ContentModel.PROP_LOCALE),
                            (String) archiveProperties.get(ContentModel.PROP_NAME),
                            archiveProperties);
                    archiveNode = archiveChildAssocEntity.getChildNode();
                    // Store the archive mapping for this node
                    archiveRecord.put(node.id, archiveNode.getNodePair());
                    break;
                }
                catch (NodeExistsException e)
                {
                    if (!attempted)
                    {
                        // There is a conflict, so delete the currently-archived node
                        NodeRef conflictingNodeRef = e.getNodePair().getSecond();
                        deleteNode(conflictingNodeRef);
                        attempted = true;
                    }
                    else
                    {
                        throw e;
                    }
                }
            }
          
            // Carry any explicit permissions over to the new node
            Set<AccessPermission> originalNodePermissions = permissionService.getAllSetPermissions(node.nodeRef);
            for (AccessPermission originalPermission : originalNodePermissions)
            {
                if (originalPermission.isInherited())
                {
                    // Ignore inherited permissions
                    continue;
                }
                NodeRef archiveNodeRef = archiveNode.getNodeRef();
                permissionService.setPermission(
                        archiveNodeRef,
                        originalPermission.getAuthority(),
                        originalPermission.getPermission(),
                        originalPermission.getAccessStatus() == AccessStatus.ALLOWED);

            }

            // Check if it inherits permissions or not
            if (!permissionService.getInheritParentPermissions(node.nodeRef))
            {
                permissionService.setInheritParentPermissions(archiveNode.getNodeRef(), false);
            }
            
            // Add properties and aspects
            Long archiveNodeId = archiveNode.getId();
            NodeRef archiveNodeRef = archiveNode.getNodeRef();
            nodeDAO.addNodeAspects(archiveNodeId, archiveAspects);
            nodeDAO.addNodeProperties(archiveNodeId, archiveProperties);
            // TODO: archive other associations
            
            // If we are have just handled the top-level node in the hierarchy, then ensure that the
            // username is linked to the document
            if (firstNode)
            {
                // Attach archiveRoot aspect to root
                // TODO: In time, this can be moved into a patch
                Long archiveStoreRootNodeId = archiveStoreRootNodePair.getFirst();
                NodeRef archiveStoreRootNodeRef = archiveStoreRootNodePair.getSecond();
                if (!nodeDAO.hasNodeAspect(archiveStoreRootNodeId, ContentModel.ASPECT_ARCHIVE_ROOT))
                {
                    addAspect(archiveStoreRootNodeRef, ContentModel.ASPECT_ARCHIVE_ROOT, null);
                }
                // Ensure that the user has a folder for archival
                String username = AuthenticationUtil.getFullyAuthenticatedUser();
                if (username == null)
                {
                    username = AuthenticationUtil.getAdminUserName();
                }
                Pair<Long, ChildAssociationRef> userArchiveAssocPair = nodeDAO.getChildAssoc(
                        archiveStoreRootNodeId,
                        ContentModel.ASSOC_ARCHIVE_USER_LINK,
                        username);
                NodeRef userArchiveNodeRef = null;
                if (userArchiveAssocPair == null)
                {
                    // User has no node entry.  Create a new one.
                    QName archiveUserAssocQName = QName.createQName(
                                NamespaceService.CONTENT_MODEL_1_0_URI,
                                QName.createValidLocalName(username));
                    Map<QName, Serializable> userArchiveNodeProps = Collections.singletonMap(
                            ContentModel.PROP_NAME, (Serializable) username);
                    userArchiveNodeRef = createNode(
                            archiveStoreRootNodeRef,
                            ContentModel.ASSOC_ARCHIVE_USER_LINK,
                            archiveUserAssocQName,
                            ContentModel.TYPE_ARCHIVE_USER,
                            userArchiveNodeProps).getChildRef();
                }
                else
                {
                    userArchiveNodeRef = userArchiveAssocPair.getSecond().getChildRef();
                }
                // Link user node to archived item via secondary child association
                String archiveNodeName = (String) archiveProperties.get(ContentModel.PROP_NAME);
                if (archiveNodeName == null)
                {
                    archiveNodeName = archiveNodeRef.getId();
                }
                QName archiveAssocQName = QName.createQNameWithValidLocalName(
                        NamespaceService.SYSTEM_MODEL_1_0_URI, archiveNodeName);
                addChild(userArchiveNodeRef, archiveNodeRef, ContentModel.ASSOC_ARCHIVED_LINK, archiveAssocQName);
            }
            
            // Invoke behaviours
            invokeOnCreateNode(archivePrimaryParentAssocRef);

            firstNode = false;
        }
    }
    
    /**
     * {@inheritDoc}
     * 
     * Archives the node without the <b>cm:auditable</b> aspect behaviour
     */
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public NodeRef restoreNode(NodeRef archivedNodeRef, NodeRef destinationParentNodeRef, QName assocTypeQName, QName assocQName)
    {
        policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_AUDITABLE);
        try
        {
            return restoreNodeImpl(archivedNodeRef, destinationParentNodeRef, assocTypeQName, assocQName);
        }
        finally
        {
            policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_AUDITABLE);
        }
    }
    
    private NodeRef restoreNodeImpl(NodeRef archivedNodeRef, NodeRef destinationParentNodeRef, QName assocTypeQName, QName assocQName)
    {
        Pair<Long, NodeRef> archivedNodePair = getNodePairNotNull(archivedNodeRef);
        Long archivedNodeId = archivedNodePair.getFirst();
        Set<QName> existingAspects = nodeDAO.getNodeAspects(archivedNodeId);
        Set<QName> newAspects = new HashSet<QName>(5);
        Map<QName, Serializable> existingProperties = nodeDAO.getNodeProperties(archivedNodeId);
        Map<QName, Serializable> newProperties = new HashMap<QName, Serializable>(11);
        
        // the node must be a top-level archive node
        if (!existingAspects.contains(ContentModel.ASPECT_ARCHIVED))
        {
            throw new AlfrescoRuntimeException("The node to restore is not an archive node");
        }
        
        // Remove the secondary link to the user that deleted the node
        List<ChildAssociationRef> parentAssocsToRemove = getParentAssocs(
                archivedNodeRef,
                ContentModel.ASSOC_ARCHIVED_LINK,
                RegexQNamePattern.MATCH_ALL);
        for (ChildAssociationRef parentAssocToRemove : parentAssocsToRemove)
        {
            this.removeSecondaryChildAssociation(parentAssocToRemove);
        }
        
        ChildAssociationRef originalPrimaryParentAssocRef = (ChildAssociationRef) existingProperties.get(
                ContentModel.PROP_ARCHIVED_ORIGINAL_PARENT_ASSOC);
        Serializable originalOwner = existingProperties.get(ContentModel.PROP_ARCHIVED_ORIGINAL_OWNER);
        // remove the archived aspect
        Set<QName> removePropertyQNames = new HashSet<QName>(11);
        Set<QName> removeAspectQNames = new HashSet<QName>(3);
        removePropertyQNames.add(ContentModel.PROP_ARCHIVED_ORIGINAL_PARENT_ASSOC);
        removePropertyQNames.add(ContentModel.PROP_ARCHIVED_BY);
        removePropertyQNames.add(ContentModel.PROP_ARCHIVED_DATE);
        removePropertyQNames.add(ContentModel.PROP_ARCHIVED_ORIGINAL_OWNER);
        removeAspectQNames.add(ContentModel.ASPECT_ARCHIVED);
        
        // restore the original ownership
        if (originalOwner == null || originalOwner.equals(OwnableService.NO_OWNER))
        {
            // The ownable aspect was not present before
            removeAspectQNames.add(ContentModel.ASPECT_OWNABLE);
            removePropertyQNames.add(ContentModel.PROP_OWNER);
        }
        else
        {
            newAspects.add(ContentModel.ASPECT_OWNABLE);
            newProperties.put(ContentModel.PROP_OWNER, originalOwner);
        }
        
        // Prepare the node for restoration: remove old aspects and properties; add new aspects and properties
        nodeDAO.removeNodeProperties(archivedNodeId, removePropertyQNames);
        nodeDAO.removeNodeAspects(archivedNodeId, removeAspectQNames);
        nodeDAO.addNodeProperties(archivedNodeId, newProperties);
        nodeDAO.addNodeAspects(archivedNodeId, newAspects);

        if (destinationParentNodeRef == null)
        {
            // we must restore to the original location
            destinationParentNodeRef = originalPrimaryParentAssocRef.getParentRef();
        }
        // check the associations
        if (assocTypeQName == null)
        {
            assocTypeQName = originalPrimaryParentAssocRef.getTypeQName();
        }
        if (assocQName == null)
        {
            assocQName = originalPrimaryParentAssocRef.getQName();
        }

        // move the node to the target parent, which may or may not be the original parent
        ChildAssociationRef newChildAssocRef = moveNode(
                archivedNodeRef,
                destinationParentNodeRef,
                assocTypeQName,
                assocQName);

        // the node reference has changed due to the store move
        NodeRef restoredNodeRef = newChildAssocRef.getChildRef();
        invokeOnRestoreNode(newChildAssocRef);
        // done
        if (logger.isDebugEnabled())
        {
            logger.debug("Restored node: \n" +
                    "   original noderef: " + archivedNodeRef + "\n" +
                    "   restored noderef: " + restoredNodeRef + "\n" +
                    "   new parent: " + destinationParentNodeRef);
        }
        return restoredNodeRef;
    }

    /**
     * Move Node
     * 
     * Drops the old primary association and creates a new one
     */
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public ChildAssociationRef moveNode(
            NodeRef nodeToMoveRef,
            NodeRef newParentRef,
            QName assocTypeQName,
            QName assocQName)
    {
        // The node(s) involved may not be pending deletion
        checkPendingDelete(nodeToMoveRef);
        checkPendingDelete(newParentRef);
        
        Pair<Long, NodeRef> nodeToMovePair = getNodePairNotNull(nodeToMoveRef);
        Pair<Long, NodeRef> parentNodePair = getNodePairNotNull(newParentRef);
        
        Long nodeToMoveId = nodeToMovePair.getFirst();
        NodeRef oldNodeToMoveRef = nodeToMovePair.getSecond();
        Long parentNodeId = parentNodePair.getFirst();
        NodeRef parentNodeRef = parentNodePair.getSecond();
        StoreRef oldStoreRef = oldNodeToMoveRef.getStoreRef();
        StoreRef newStoreRef = parentNodeRef.getStoreRef();
        
        List<ChildAssociationRef> nodesToRestoreAssociationsFor = new ArrayList<ChildAssociationRef>();
        
        // Get the primary parent association
        Pair<Long, ChildAssociationRef> oldParentAssocPair = nodeDAO.getPrimaryParentAssoc(nodeToMoveId);
        if (oldParentAssocPair == null)
        {
            // The node doesn't have parent.  Moving it is not possible.
            throw new IllegalArgumentException("Node " + nodeToMoveId + " doesn't have a parent.  Use 'addChild' instead of move.");
        }
        ChildAssociationRef oldParentAssocRef = oldParentAssocPair.getSecond();
        
        boolean movingStore = !oldStoreRef.equals(newStoreRef);

        if (movingStore)
        {
            // Recursively find primary children of the node to move
            // TODO: Use NodeHierarchyWalker
            List<ChildAssociationRef> childAssocs = new LinkedList<ChildAssociationRef>();
            Map<NodeRef, Long> oldChildNodeIds = new HashMap<NodeRef, Long>(97);
            findNodeChildrenToMove(nodeToMoveId, newStoreRef, childAssocs, oldChildNodeIds);
        
            // Invoke "Before Delete" policy behaviour
            invokeBeforeDeleteNode(nodeToMoveRef);

            // do the same to the children, preserving parents, types and qnames
            for (ChildAssociationRef oldChildAssoc : childAssocs)
            {
                // Fire before delete policy. Before create policy needs the new parent ref to exist, so will be fired later
                invokeBeforeDeleteNode(oldChildAssoc.getChildRef());
            }
            
            // Now do the moving and remaining policy firing
            Map<NodeRef, Pair<Long, NodeRef>> movedNodePairs = new HashMap<NodeRef, Pair<Long,NodeRef>>(childAssocs.size() * 2 + 2);
            QName childNodeTypeQName = nodeDAO.getNodeType(nodeToMoveId);
            Set<QName> childNodeAspectQNames = nodeDAO.getNodeAspects(nodeToMoveId);
            
            // Fire before create immediately before moving with all parents in place
            invokeBeforeCreateNode(newParentRef, assocTypeQName, assocQName, childNodeTypeQName);

            // Move node under the new parent
            Pair<Pair<Long, ChildAssociationRef>, Pair<Long, NodeRef>> moveNodeResult = nodeDAO.moveNode(
                        nodeToMoveId,
                        parentNodeId,
                        assocTypeQName,
                        assocQName);
            Pair<Long, ChildAssociationRef> newParentAssocPair = moveNodeResult.getFirst();
            movedNodePairs.put(nodeToMoveRef, moveNodeResult.getSecond());
            ChildAssociationRef newParentAssocRef = newParentAssocPair.getSecond();
            
            // Propagate timestamps
            propagateTimeStamps(oldParentAssocRef);
            propagateTimeStamps(newParentAssocRef);

            // The Node changes NodeRefs, so this is really the deletion of the old node and creation
            // of a node in a new store as far as the clients are concerned.
            invokeOnDeleteNode(oldParentAssocRef, childNodeTypeQName, childNodeAspectQNames, true);
            invokeOnCreateNode(newParentAssocRef);
            
            // do the same to the children, preserving parents, types and qnames
            for (ChildAssociationRef oldChildAssoc : childAssocs)
            {
                NodeRef oldChildNodeRef = oldChildAssoc.getChildRef();
                Long oldChildNodeId = oldChildNodeIds.get(oldChildNodeRef);
                NodeRef oldParentNodeRef = oldChildAssoc.getParentRef();
                Pair<Long, NodeRef> newParentNodePair = movedNodePairs.get(oldParentNodeRef);
                Long newParentNodeId = newParentNodePair.getFirst();

                childNodeTypeQName = nodeDAO.getNodeType(oldChildNodeId);
                childNodeAspectQNames = nodeDAO.getNodeAspects(oldChildNodeId);

                // Now that the new parent ref exists, invoke the before create policy
                invokeBeforeCreateNode(
                        newParentNodePair.getSecond(),
                        oldChildAssoc.getTypeQName(),
                        oldChildAssoc.getQName(),
                        childNodeTypeQName);

                // Move the node as this gives back the primary parent association
                try
                {
                    moveNodeResult = nodeDAO.moveNode(oldChildNodeId, newParentNodeId, null,null);
                }
                catch (NodeExistsException e)
                {
                    deleteNode(e.getNodePair().getSecond());
                    moveNodeResult = nodeDAO.moveNode(oldChildNodeId, newParentNodeId, null,null);
                }
                // Move the node as this gives back the primary parent association
                newParentAssocPair = moveNodeResult.getFirst();
                movedNodePairs.put(oldChildNodeRef, moveNodeResult.getSecond());
                ChildAssociationRef newChildAssoc = newParentAssocPair.getSecond();
                
                // Propagate timestamps
                propagateTimeStamps(newChildAssoc);

                // Fire node policies.  This ensures that each node in the hierarchy gets a notification fired.
                invokeOnDeleteNode(oldChildAssoc, childNodeTypeQName, childNodeAspectQNames, true);
                invokeOnCreateNode(newChildAssoc);

                // collect working copy nodes that need to be updated; we need all nodes 
                // to be already moved when create association between nodes
                if (hasAspect(newChildAssoc.getChildRef(), ContentModel.ASPECT_ARCHIVE_LOCKABLE))
                {
                    nodesToRestoreAssociationsFor.add(newChildAssoc);
                }
            }
            
            // invoke onRestoreNode for working copy nodes in order to restore original lock
            for (ChildAssociationRef childAssoc : nodesToRestoreAssociationsFor)
            {
                invokeOnRestoreNode(childAssoc);
            }

            return newParentAssocRef;
        }
        else
        {
            invokeBeforeMoveNode(oldParentAssocRef, newParentRef);

            invokeBeforeDeleteChildAssociation(oldParentAssocRef);

            // Move node under the new parent
            Pair<Pair<Long, ChildAssociationRef>, Pair<Long, NodeRef>> moveNodeResult = nodeDAO.moveNode(
                    nodeToMoveId,
                    parentNodeId,
                    assocTypeQName,
                    assocQName);
            Pair<Long, ChildAssociationRef> newParentAssocPair = moveNodeResult.getFirst();
            ChildAssociationRef newParentAssocRef = newParentAssocPair.getSecond();

            // Propagate timestamps (watch out for moves within the same folder)
            if (!oldParentAssocRef.getParentRef().equals(newParentAssocRef.getParentRef()))
            {
                propagateTimeStamps(oldParentAssocRef);
                propagateTimeStamps(newParentAssocRef);
            }
            else
            {
                // Propagate timestamps for rename case, see ALF-10884
                propagateTimeStamps(newParentAssocRef);
            }

            invokeOnCreateChildAssociation(newParentAssocRef, false);
            invokeOnDeleteChildAssociation(oldParentAssocRef);
            invokeOnMoveNode(oldParentAssocRef, newParentAssocRef);            

            // Done
            return newParentAssocRef;
        }
    }
    
    private void findNodeChildrenToMove(Long nodeId, final StoreRef storeRef,
            final List<ChildAssociationRef> childAssocsToMove, final Map<NodeRef, Long> nodeIds)
    {
        // Get the node's children, but only one's that aren't in the same store
        final List<ChildAssociationRef> childAssocs = new LinkedList<ChildAssociationRef>();
        NodeDAO.ChildAssocRefQueryCallback callback = new NodeDAO.ChildAssocRefQueryCallback()
        {
            public boolean preLoadNodes()
            {
                return true;
            }

            @Override
            public boolean orderResults()
            {
                return false;
            }

            public boolean handle(
                    Pair<Long, ChildAssociationRef> childAssocPair,
                    Pair<Long, NodeRef> parentNodePair,
                    Pair<Long, NodeRef> childNodePair
                    )
            {
                // Add it if it's not in the target store
                NodeRef childNodeRef = childNodePair.getSecond(); 
                if (!childNodeRef.getStoreRef().equals(storeRef))
                {
                    childAssocs.add(childAssocPair.getSecond());
                    nodeIds.put(childNodeRef, childNodePair.getFirst());
                }
                // More results
                return true;
            }

            public void done()
            {
            }                               
        };
        // We need to get all primary children and do the store filtering ourselves
        nodeDAO.getChildAssocs(nodeId, null, null, null, Boolean.TRUE, null, callback);
        
        // Each child must be moved to the same store as the parent
        for (ChildAssociationRef oldChildAssoc : childAssocs)
        {
            NodeRef childNodeRef = oldChildAssoc.getChildRef();
            Long childNodeId = nodeIds.get(childNodeRef);
            NodeRef.Status childNodeStatus = nodeDAO.getNodeRefStatus(childNodeRef);
            if (childNodeStatus == null || childNodeStatus.isDeleted())
            {
                // Node has already been deleted.
                continue;
            } 
            childAssocsToMove.add(oldChildAssoc);
            // Cascade
            findNodeChildrenToMove(childNodeId, storeRef, childAssocsToMove, nodeIds);
        }
    }
    
    @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class)
    public NodeRef getStoreArchiveNode(StoreRef storeRef)
    {
        StoreRef archiveStoreRef = storeArchiveMap.get(storeRef);
        if (archiveStoreRef == null)
        {
            // no mapping for the given store
            return null;
        }
        else
        {
            return getRootNode(archiveStoreRef);
        }
    }

    private String extractNameProperty(Map<QName, Serializable> properties)
    {
        Serializable nameValue = properties.get(ContentModel.PROP_NAME);
        String name = (String) DefaultTypeConverter.INSTANCE.convert(String.class, nameValue);
        return name;
    }

    /**
     * Ensures name uniqueness for the child and the child association.  Note that nothing is done if the
     * association type doesn't enforce name uniqueness.
     * 
     * @return          Returns <tt>true</tt> if the child association <b>cm:name</b> was written
     */
    private boolean setChildNameUnique(Pair<Long, NodeRef> childNodePair, String newName, String oldName)
    {
        if (newName == null)
        {
            newName = childNodePair.getSecond().getId();            // Use the node's GUID
        }
        Long childNodeId = childNodePair.getFirst();
        
        if (EqualsHelper.nullSafeEquals(newName, oldName))
        {
            // The name has not changed
            return false;
        }
        else
        {
            nodeDAO.setChildAssocsUniqueName(childNodeId, newName);
            return true;
        }
    }

    /**
     * Propagate, if necessary, a <b>cm:modified</b> timestamp change to the parent of the
     * given association, along with the <b>cm:modifier</b> of who changed it.  
     * The parent node has to be <b>cm:auditable</b> and the association
     * has to be marked for propagation as well.
     * 
     * @param assocRef          the association to propagate along
     */
    private void propagateTimeStamps(ChildAssociationRef assocRef)
    {
        if (!enableTimestampPropagation)
        {
            return;         // Bypassed on a system-wide basis
        }
        // First check if the association type warrants propagation in the first place
        AssociationDefinition assocDef = dictionaryService.getAssociation(assocRef.getTypeQName());
        if (assocDef == null || !assocDef.isChild())
        {
            if (logger.isDebugEnabled())
            {
                logger.debug("Not propagating cm:auditable for unknown association type " + assocRef.getTypeQName());
            }
            return;
        }
        ChildAssociationDefinition childAssocDef = (ChildAssociationDefinition) assocDef;
        if (!childAssocDef.getPropagateTimestamps())
        {
            if (logger.isDebugEnabled())
            {
                logger.debug("Not propagating cm:auditable for association type " + childAssocDef.getName());
            }
            return;
        }
        
        // The dictionary says propagate.  Now get the parent node and prompt the touch.
        NodeRef parentNodeRef = assocRef.getParentRef();
        
        // Do not propagate if the cm:auditable behaviour is off
        if (!policyBehaviourFilter.isEnabled(parentNodeRef, ContentModel.ASPECT_AUDITABLE))
        {
            if (logger.isDebugEnabled())
            {
                logger.debug("Not propagating cm:auditable for non-auditable parent on " + assocRef);
            }
            return;
        }
        
        Pair<Long, NodeRef> parentNodePair = getNodePairNotNull(parentNodeRef);
        Long parentNodeId = parentNodePair.getFirst();
        
        // Get the ID of the child that triggered this update
        NodeRef childNodeRef = assocRef.getChildRef();
        Pair<Long, NodeRef> childNodePair = getNodePairNotNull(childNodeRef);
        Long childNodeId = childNodePair.getFirst();
        
        // If we have already modified a particular parent node in the current txn,
        // it is not necessary to start a new transaction to tweak the cm:modified date.
        // But if the parent node was NOT touched, then doing so in this transaction would
        // create excessive concurrency and retries; in latter case we defer to a small,
        // post-commit isolated transaction.
        if (TransactionalResourceHelper.getMap(KEY_AUDITABLE_PROPAGATION_PRE).containsKey(parentNodeId))
        {
            // It is already registered in the current transaction.
            // Modified By will be taken from the previous node to touch it
            if (logger.isDebugEnabled())
            {
                logger.debug("Update of cm:auditable already requested for " + parentNodePair);
            }
            return;
        }
        
        if (nodeDAO.isInCurrentTxn(parentNodeId))
        {
            // The parent and child are in the same transaction
            TransactionalResourceHelper.getMap(KEY_AUDITABLE_PROPAGATION_PRE).put(parentNodeId, childNodeId);
            // Make sure that it is not processed after the transaction
            TransactionalResourceHelper.getMap(KEY_AUDITABLE_PROPAGATION_POST).remove(parentNodeId);
            
            if (logger.isDebugEnabled())
            {
                logger.debug("Performing in-transaction cm:auditable update for " + parentNodePair + " from " + childNodePair);
            }
        }
        else
        {
            TransactionalResourceHelper.getMap(KEY_AUDITABLE_PROPAGATION_POST).put(parentNodeId, childNodeId);
            
            if (logger.isDebugEnabled())
            {
                logger.debug("Requesting later cm:auditable update for " + parentNodePair + " from " + childNodePair);
            }
        }
        
        // Bind a listener for post-transaction manipulation
        AlfrescoTransactionSupport.bindListener(auditableTransactionListener);
    }
    
    private static final String KEY_AUDITABLE_PROPAGATION_PRE = "node.auditable.propagation.pre";
    private static final String KEY_AUDITABLE_PROPAGATION_POST = "node.auditable.propagation.post";
    private AuditableTransactionListener auditableTransactionListener = new AuditableTransactionListener();
    /**
     * Wrapper to set the <b>cm:modified</b> time and <b>cm:modifier</b> on 
     * individual nodes.
     * 
     * @author Derek Hulley
     * @since 3.4.6
     */
    private class AuditableTransactionListener extends TransactionListenerAdapter
    {
        @Override
        public void beforeCommit(boolean readOnly)
        {
            // An error in prior code if it's read only
            if (readOnly)
            {
                throw new IllegalStateException("Attempting to modify parent cm:modified in read-only txn.");
            }
            
            Map<Long,Long> parentNodeIds = TransactionalResourceHelper.getMap(KEY_AUDITABLE_PROPAGATION_PRE);
            if (parentNodeIds.size() == 0)
            {
                return;
            }
            // Process parents, but use the current txn
            Date modifiedDate = new Date();
            process(parentNodeIds, modifiedDate, true);
        }

        @Override
        public void afterCommit()
        {
            Map<Long,Long> parentNodeIds = TransactionalResourceHelper.getMap(KEY_AUDITABLE_PROPAGATION_POST);
            if (parentNodeIds.size() == 0)
            {
                return;
            }
            Date modifiedDate = new Date();
            process(parentNodeIds, modifiedDate, false);
        }

        /**
         * @param parentNodeIds         the parent node IDs that need to be touched for <b>cm:modified</b>, and the updating child node from which to get the <b>cm:modifier</b> from
         * @param modifiedDate          the date to set
         * @param useCurrentTxn         <tt>true</tt> to use the current transaction
         */
        private void process(final Map<Long,Long> parentNodeIds, Date modifiedDate, boolean useCurrentTxn)
        {
            // Walk through the IDs
            for (Long parentNodeId: parentNodeIds.keySet())
            {
                processSingle(parentNodeId, parentNodeIds.get(parentNodeId), modifiedDate, useCurrentTxn);
            }
        }
        
        /**
         * Touch a single node in a new, writable txn
         * 
         * @param parentNodeId          the parent node to touch
         * @param childNodeId           the child node from which to get the <b>cm:modifier</b> from
         * @param modifiedDate          the date to set
         * @param useCurrentTxn         <tt>true</tt> to use the current transaction
         */
        private void processSingle(final Long parentNodeId, final Long childNodeId, final Date modifiedDate, boolean useCurrentTxn)
        {
            RetryingTransactionHelper txnHelper = transactionService.getRetryingTransactionHelper();
            txnHelper.setMaxRetries(1);
            RetryingTransactionCallback<Void> callback = new RetryingTransactionCallback<Void>()
            {
                @Override
                public Void execute() throws Throwable
                {
                    // Get the details of the parent, and check it's valid to update
                    Pair<Long, NodeRef> parentNodePair = nodeDAO.getNodePair(parentNodeId);
                    if (parentNodePair == null)
                    {
                        return null;                            // Parent has gone away
                    }
                    else if (!nodeDAO.hasNodeAspect(parentNodeId, ContentModel.ASPECT_AUDITABLE))
                    {
                        return null;                            // Not auditable
                    }
                    NodeRef parentNodeRef = parentNodePair.getSecond();
                    
                    // Fetch the modification details from the child, as best we can
                    Pair<Long, NodeRef> childNodePair = nodeDAO.getNodePair(childNodeId);
                    String modifiedByToPropagate = null;
                    Date modifiedDateToPropagate = modifiedDate;
                    if (childNodePair == null)
                    {
                        // Child has gone away, can't fetch details from children's properties
                        modifiedByToPropagate = AuthenticationUtil.getFullyAuthenticatedUser();
                    }
                    else if (!nodeDAO.hasNodeAspect(childNodeId, ContentModel.ASPECT_AUDITABLE))
                    {
                        // Child isn't auditable, can't fetch details
                        return null;
                    }
                    else
                    {
                        // Get the child's modification details
                        modifiedByToPropagate = (String)nodeDAO.getNodeProperty(childNodeId, ContentModel.PROP_MODIFIER);
                        modifiedDateToPropagate = (Date)nodeDAO.getNodeProperty(childNodeId, ContentModel.PROP_MODIFIED);
                    }
                    
                    // Did another child get there first?
                    Date parentModifiedAt = (Date)nodeDAO.getNodeProperty(parentNodeId, ContentModel.PROP_MODIFIED);
                    if (parentModifiedAt != null && modifiedDateToPropagate != null 
                            && parentModifiedAt.getTime() > modifiedDateToPropagate.getTime())
                    {
                        // Parent was modified more recently, don't update
                        if(logger.isDebugEnabled())
                        {
                            logger.debug("Parent " + parentNodeRef + " was modified more recently than child " + 
                                         childNodePair + " so not propogating auditable details");
                        }
                        return null;
                    }

                    // Invoke policy behaviour
                    invokeBeforeUpdateNode(parentNodeRef);

                    // Touch the node; it is cm:auditable
                    boolean changed = nodeDAO.setModifiedProperties(parentNodeId, modifiedDate, modifiedByToPropagate);

                    if (changed)
                    {
                        // Invoke policy behaviour
                        invokeOnUpdateNode(parentNodeRef);
                    }

                    return null;
                }
            };
            try
            {
                txnHelper.doInTransaction(callback, false, !useCurrentTxn);
                if (logger.isDebugEnabled())
                {
                    logger.debug(
                            "Touched cm:modified date for node " + parentNodeId +
                            " (" + modifiedDate + ")" +
                            (useCurrentTxn ? " in txn " : " in new txn ") +
                            nodeDAO.getCurrentTransactionId(false));
                }
            }
            catch (Throwable e)
            {
                logger.info("Failed to update cm:modified date for node: " + parentNodeId);
            }
        }
    }
    

    @SuppressWarnings("unchecked")
    @Override
    public <M extends Trait> ExtendedTrait<M> getTrait(Class<? extends M> traitAPI)
    {
        return (ExtendedTrait<M>) nodeServiceTrait;
    }
}
