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 Node.
     * 
     * To remove a property, remove it from the map before calling this method.
     * Null-valued properties are allowed.
     * 
     * 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.
     * 
     */
    public void setProperties(NodeRef nodeRef, Map properties) throws InvalidNodeRefException
    {
        Pair 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);
        }
    }
    
    public void addProperties(NodeRef nodeRef, Map properties) throws InvalidNodeRefException
    {
        Pair 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);
        }
    }
    
    public void removeProperty(NodeRef nodeRef, QName qname) throws InvalidNodeRefException
    {
        // Get the node
        Pair nodePair = getNodePairNotNull(nodeRef);
        Long nodeId = nodePair.getFirst();
        
        // Invoke policy behaviours
        invokeBeforeUpdateNode(nodeRef);
        
        // Get the values before
        Map 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 propertiesAfter = getPropertiesImpl(nodePair);
        invokeOnUpdateNode(nodeRef);
        invokeOnUpdateProperties(nodeRef, propertiesBefore, propertiesAfter);
        
        // Index
        nodeIndexer.indexUpdateNode(nodeRef);
    }
    public Collection getParents(NodeRef nodeRef) throws InvalidNodeRefException
    {
        List parentAssocs = getParentAssocs(
                nodeRef,
                RegexQNamePattern.MATCH_ALL,
                RegexQNamePattern.MATCH_ALL);
        
        // Copy into the set to avoid duplicates
        Set parentNodeRefs = new HashSet(parentAssocs.size());
        for (ChildAssociationRef parentAssoc : parentAssocs)
        {
            NodeRef parentNodeRef = parentAssoc.getParentRef();
            parentNodeRefs.add(parentNodeRef);
        }
        // Done
        return new ArrayList(parentNodeRefs);
    }
    /**
     * Filters out any associations if their qname is not a match to the given pattern.
     */
    public List getParentAssocs(
            final NodeRef nodeRef,
            final QNamePattern typeQNamePattern,
            final QNamePattern qnamePattern)
    {
        // Get the node
        Pair nodePair = getNodePairNotNull(nodeRef);
        Long nodeId = nodePair.getFirst();
        
        final List results = new ArrayList(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 childAssocPair,
                    Pair parentNodePair,
                    Pair 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;
    }
    /**
     * Filters out any associations if their qname is not a match to the given pattern.
     */
    public List 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.
     */
    public List 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
     */
    public List getChildAssocs(
            NodeRef nodeRef,
            final QNamePattern typeQNamePattern,
            final QNamePattern qnamePattern,
            final int maxResults,
            final boolean preload)
    {
        // Get the node
        Pair nodePair = getNodePairNotNull(nodeRef);
        
        // We have a callback handler to filter results
        final List results = new ArrayList(10);
        ChildAssocRefQueryCallback callback = new ChildAssocRefQueryCallback()
        {
            public boolean preLoadNodes()
            {
                return preload;
            }
            
            @Override
            public boolean orderResults()
            {
                return true;
            }
            public boolean handle(
                    Pair childAssocPair,
                    Pair parentNodePair,
                    Pair 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;
    }
    public List getChildAssocs(NodeRef nodeRef, Set childNodeTypeQNames)
    {
        // Get the node
        Pair nodePair = getNodePairNotNull(nodeRef);
        Long nodeId = nodePair.getFirst();
        final List results = new ArrayList(100);
        
        NodeDAO.ChildAssocRefQueryCallback callback = new NodeDAO.ChildAssocRefQueryCallback()
        {
            public boolean preLoadNodes()
            {
                return true;
            }
            @Override
            public boolean orderResults()
            {
                return true;
            }
            public boolean handle(
                    Pair childAssocPair,
                    Pair parentNodePair,
                    Pair 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;
    }
    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 nodePair = getNodePairNotNull(nodeRef);
        Long nodeId = nodePair.getFirst();
        Pair childAssocPair = nodeDAO.getChildAssoc(nodeId, assocTypeQName, childName);
        if (childAssocPair != null)
        {
            return childAssocPair.getSecond().getChildRef();
        }
        else
        {
            return null;
        }
    }
    public List getChildrenByName(NodeRef nodeRef, QName assocTypeQName, Collection childNames)
    {
        // Get the node
        Pair nodePair = getNodePairNotNull(nodeRef);
        Long nodeId = nodePair.getFirst();
        final List results = new ArrayList(100);
        
        NodeDAO.ChildAssocRefQueryCallback callback = new NodeDAO.ChildAssocRefQueryCallback()
        {
            public boolean preLoadNodes()
            {
                return true;
            }            
            @Override
            public boolean orderResults()
            {
                return true;
            }
            public boolean handle(
                    Pair childAssocPair,
                    Pair parentNodePair,
                    Pair 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;
    }
    public ChildAssociationRef getPrimaryParent(NodeRef nodeRef) throws InvalidNodeRefException
    {
        // Get the node
        Pair nodePair = getNodePairNotNull(nodeRef);
        Long nodeId = nodePair.getFirst();
        // get the primary parent assoc
        Pair 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
    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 sourceNodePair = getNodePairNotNull(sourceRef);
        long sourceNodeId = sourceNodePair.getFirst();
        Pair 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
    public void setAssociations(NodeRef sourceRef, QName assocTypeQName, List targetRefs)
    {
        // The node(s) involved may not be pending deletion
        checkPendingDelete(sourceRef);
        
        Pair sourceNodePair = getNodePairNotNull(sourceRef);
        Long sourceNodeId = sourceNodePair.getFirst();
        // First get the existing associations
        Collection> assocsBefore = nodeDAO.getTargetNodeAssocs(sourceNodeId, assocTypeQName);
        Map targetRefsBefore = new HashMap(assocsBefore.size());
        Map toRemoveMap = new HashMap(assocsBefore.size());
        for (Pair 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 toRemoveIds = new ArrayList(toRemoveMap.values());
        nodeDAO.removeNodeAssocs(toRemoveIds);
        
        // Work out which associations need to be added
        Set toAdd = new HashSet(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);
        }
    }
    public Collection getChildAssocsWithoutParentAssocsOfType(NodeRef parent, QName assocTypeQName)
    {
        // Get the parent node
        Pair nodePair = getNodePairNotNull(parent);
        Long parentNodeId = nodePair.getFirst();
        final List results = new ArrayList(100);
        NodeDAO.ChildAssocRefQueryCallback callback = new NodeDAO.ChildAssocRefQueryCallback()
        {
            public boolean preLoadNodes()
            {
                return false;
            }
            @Override
            public boolean orderResults()
            {
                return false;
            }
            public boolean handle(Pair childAssocPair, Pair parentNodePair,
                    Pair 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 not supported by {@link #getChildAssocsByPropertyValue(NodeRef, QName, Serializable)}
     */
    private static List getChildAssocsByPropertyValueBannedProps = new ArrayList();
    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
    public List getChildAssocsByPropertyValue(
            NodeRef nodeRef,
            QName propertyQName, 
            Serializable value)
    {
        // Get the node
        Pair 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 results = new ArrayList(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 childAssocPair,
                    Pair parentNodePair,
                    Pair 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;
    }
    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 sourceNodePair = getNodePairNotNull(sourceRef);
        Long sourceNodeId = sourceNodePair.getFirst();
        Pair 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
    public AssociationRef getAssoc(Long id)
    {
        Pair nodeAssocPair = nodeDAO.getNodeAssocOrNull(id);
        return nodeAssocPair == null ? null : nodeAssocPair.getSecond();
    }
    public List getTargetAssocs(NodeRef sourceRef, QNamePattern qnamePattern)
    {
        Pair sourceNodePair = getNodePairNotNull(sourceRef);
        Long sourceNodeId = sourceNodePair.getFirst();
        QName qnameFilter = null;
        if (qnamePattern instanceof QName)
        {
            qnameFilter = (QName) qnamePattern;
        }
        Collection> assocPairs = nodeDAO.getTargetNodeAssocs(sourceNodeId, qnameFilter);
        List nodeAssocRefs = new ArrayList(assocPairs.size());
        for (Pair 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;
    }
    public List getSourceAssocs(NodeRef targetRef, QNamePattern qnamePattern)
    {
        Pair targetNodePair = getNodePairNotNull(targetRef);
        Long targetNodeId = targetNodePair.getFirst();
        
        QName qnameFilter = null;
        if (qnamePattern instanceof QName)
        {
            qnameFilter = (QName) qnamePattern;
        }
        Collection> assocPairs = nodeDAO.getSourceNodeAssocs(targetNodeId, qnameFilter);
        List nodeAssocRefs = new ArrayList(assocPairs.size());
        for (Pair 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)
     */
    public Path getPath(NodeRef nodeRef) throws InvalidNodeRefException
    {
        List 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 primaryOnly == true, checks that there is exactly
     * one path.
     */
    public List getPaths(NodeRef nodeRef, boolean primaryOnly) throws InvalidNodeRefException
    {
        // get the starting node
        Pair nodePair = getNodePairNotNull(nodeRef);
        
        return nodeDAO.getPaths(nodePair, primaryOnly);
    }
    
    /**
     * Archives the node without the cm:auditable 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 archiveStoreRootNodePair = nodeDAO.getRootNode(archiveStoreRef);
        
        // Work through the hierarchy from the top down and archive all the nodes
        boolean firstNode = true;
        Map