org.alfresco.repo.node.db.DbNodeServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.repo.node.db.DbNodeServiceImpl.java

Source

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

    /**
     * 
     * @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);

        // Index
        ChildAssociationRef assocRef = new ChildAssociationRef(null, null, null, rootNodeRef);
        nodeIndexer.indexCreateNode(assocRef);

        // Done
        return storeRef;
    }

    /**
     * @throws UnsupportedOperationException        Always
     */
    @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class)
    public void deleteStore(StoreRef storeRef) throws InvalidStoreRefException {
        // Delete the index
        nodeIndexer.indexDeleteStore(storeRef);
        // 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);

        // Index
        nodeIndexer.indexCreateNode(childAssocRef);

        // 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);

            // Index
            nodeIndexer.indexUpdateNode(nodeRef);
        }
    }

    @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);
            // Index
            nodeIndexer.indexUpdateNode(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);
        }

        // Index
        nodeIndexer.indexUpdateNode(nodeRef);
    }

    /**
     * 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());
                nodeIndexer.indexDeleteChildAssociation(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());
                nodeIndexer.indexDeleteChildAssociation(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);
            nodeIndexer.indexDeleteNode(nodeToDelete.primaryParentAssocPair.getSecond());
        }

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

        // Index
        for (ChildAssociationRef childAssocRef : childAssociationRefs) {
            nodeIndexer.indexCreateChildAssociation(childAssocRef);
        }

        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);

            // Index
            nodeIndexer.indexDeleteChildAssociation(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);
            // Index
            nodeIndexer.indexDeleteChildAssociation(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);
        // Index
        nodeIndexer.indexDeleteChildAssociation(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);
            // Index
            nodeIndexer.indexUpdateNode(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);
            // Index
            nodeIndexer.indexUpdateNode(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);
            // Index
            nodeIndexer.indexUpdateNode(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);

        // Index
        nodeIndexer.indexUpdateNode(nodeRef);
    }

    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
            nodeIndexer.indexCreateNode(archivePrimaryParentAssocRef);
            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();

            // Index
            nodeIndexer.indexDeleteNode(oldParentAssocRef);
            nodeIndexer.indexCreateNode(newParentAssocRef);

            // 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();

                // Index
                nodeIndexer.indexDeleteNode(oldChildAssoc);
                nodeIndexer.indexCreateNode(newChildAssoc);

                // 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();

            // The node is in the same store and is just having its child association modified
            nodeIndexer.indexUpdateChildAssociation(oldParentAssocRef, newParentAssocRef);

            // 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);
                        // Index
                        nodeIndexer.indexUpdateNode(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;
    }
}