/* * Copyright (C) 2005-2010 Alfresco Software Limited. * * This file is part of Alfresco * * 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 . */ 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.List; import java.util.Map; import java.util.Set; import java.util.Stack; 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.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.index.NodeIndexer; import org.alfresco.repo.policy.BehaviourFilter; import org.alfresco.repo.security.authentication.AuthenticationUtil; 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.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.namespace.QName; import org.alfresco.service.namespace.QNamePattern; import org.alfresco.service.namespace.RegexQNamePattern; 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.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * Node service using database persistence layer to fulfill functionality * * @author Derek Hulley */ public class DbNodeServiceImpl extends AbstractNodeServiceImpl { private static Log logger = LogFactory.getLog(DbNodeServiceImpl.class); private QNameDAO qnameDAO; private NodeDAO nodeDAO; private StoreArchiveMap storeArchiveMap; private NodeService avmNodeService; private NodeIndexer nodeIndexer; private BehaviourFilter policyBehaviourFilter; private final static String KEY_PRE_COMMIT_ADD_NODE = "DbNodeServiceImpl.PreCommitAddNode"; private final static String KEY_DELETED_NODES = "DbNodeServiceImpl.DeletedNodes"; public DbNodeServiceImpl() { 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 setStoreArchiveMap(StoreArchiveMap storeArchiveMap) { this.storeArchiveMap = storeArchiveMap; } public void setAvmNodeService(NodeService avmNodeService) { this.avmNodeService = avmNodeService; } /** * @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; } /** * 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 getNodePairNotNull(NodeRef nodeRef) throws InvalidNodeRefException { ParameterCheck.mandatory("nodeRef", nodeRef); Pair unchecked = nodeDAO.getNodePair(nodeRef); if (unchecked == null) { throw new InvalidNodeRefException("Node does not exist: " + nodeRef, nodeRef); } return unchecked; } public boolean exists(StoreRef storeRef) { return nodeDAO.exists(storeRef); } public boolean exists(NodeRef nodeRef) { ParameterCheck.mandatory("nodeRef", nodeRef); return nodeDAO.exists(nodeRef); } public Status getNodeStatus(NodeRef nodeRef) { ParameterCheck.mandatory("nodeRef", nodeRef); NodeRef.Status status = nodeDAO.getNodeRefStatus(nodeRef); return status; } /** * {@inheritDoc} */ public List getStores() { // Get the ADM stores List> stores = nodeDAO.getStores(); List storeRefs = new ArrayList(50); for (Pair pair : stores) { StoreRef storeRef = pair.getSecond(); if (storeRef.getProtocol().equals(StoreRef.PROTOCOL_DELETED)) { // Ignore continue; } storeRefs.add(storeRef); } // Now get the AVMStores. List avmStores = avmNodeService.getStores(); storeRefs.addAll(avmStores); // Return them all. return storeRefs; } /** * Defers to the typed service * @see StoreDaoService#createWorkspace(String) */ 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 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 */ public void deleteStore(StoreRef storeRef) throws InvalidStoreRefException { // Delete the index nodeIndexer.indexDeleteStore(storeRef); // Rename the store 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); } } public NodeRef getRootNode(StoreRef storeRef) throws InvalidStoreRefException { Pair rootNodePair = nodeDAO.getRootNode(storeRef); if (rootNodePair == null) { throw new InvalidStoreRefException("Store does not exist: " + storeRef, storeRef); } // done return rootNodePair.getSecond(); } /** * @see #createNode(NodeRef, QName, QName, QName, Map) */ public ChildAssociationRef createNode( NodeRef parentRef, QName assocTypeQName, QName assocQName, QName nodeTypeQName) { return this.createNode(parentRef, assocTypeQName, assocQName, nodeTypeQName, null); } /** * {@inheritDoc} */ public ChildAssociationRef createNode( NodeRef parentRef, QName assocTypeQName, QName assocQName, QName nodeTypeQName, Map properties) { 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"); } // Get the parent node Pair 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); /** * Check the parent node has not been deleted in this txn. */ if(isDeletedNodeRef(parentRef)) { throw new InvalidNodeRefException("The parent node has been deleted", parentRef); } // 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); // create the node instance ChildAssocEntity assoc = nodeDAO.newNode( parentNodePair.getFirst(), assocTypeQName, assocQName, parentStoreRef, newUuid, nodeTypeQName, newName, properties); ChildAssociationRef childAssocRef = assoc.getRef(qnameDAO); Pair childNodePair = assoc.getChildNode().getNodePair(); addAspectsAndProperties( childNodePair, nodeTypeQName, null, Collections.emptySet(), Collections.emptyMap(), Collections.emptySet(), properties, true, false); Map propertiesAfter = nodeDAO.getNodeProperties(childNodePair.getFirst()); // Invoke policy behaviour invokeOnCreateNode(childAssocRef); invokeOnCreateChildAssociation(childAssocRef, true); Map propertiesBefore = PropertyMap.EMPTY_MAP; invokeOnUpdateProperties( childAssocRef.getChildRef(), propertiesBefore, propertiesAfter); untrackDeletedNodeRef(childAssocRef.getChildRef()); // Index nodeIndexer.indexCreateNode(childAssocRef); // Ensure that the parent node has the required aspects addAspectsAndPropertiesAssoc(parentNodePair, assocTypeQName, null, null, null, null, false); // done return childAssocRef; } /** * Track a deleted node * * The deleted node set is used to break an infinite loop which can happen when adding a new node into a path containing a * deleted node. This transactional list is used to detect and prevent that from * happening. * * @param nodeRef the deleted node to track * @return true if the node was not already tracked */ private boolean trackDeletedNodeRef(NodeRef deletedNodeRef) { Set deletedNodes = TransactionalResourceHelper.getSet(KEY_DELETED_NODES); return deletedNodes.add(deletedNodeRef); } /** * Untrack a deleted node ref * * Used when a deleted node is restored. * * @param deletedNodeRef */ private void untrackDeletedNodeRef(NodeRef deletedNodeRef) { Set deletedNodes = TransactionalResourceHelper.getSet(KEY_DELETED_NODES); if (deletedNodes.size() > 0) { deletedNodes.remove(deletedNodeRef); } } private boolean isDeletedNodeRef(NodeRef deletedNodeRef) { Set deletedNodes = TransactionalResourceHelper.getSet(KEY_DELETED_NODES); return deletedNodes.contains(deletedNodeRef); } /** * loose interest in tracking a node ref * * for example if its been deleted or moved * @param nodeRef the node ref to untrack */ private void untrackNewNodeRef(NodeRef nodeRef) { Set newNodes = TransactionalResourceHelper.getSet(KEY_PRE_COMMIT_ADD_NODE); if (newNodes.size() > 0) { newNodes.remove(nodeRef); } } /** * 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 null then properties and aspects are only applied * for 'extra' aspects and 'extra' properties. * @param existingAspects the existing aspects or null to have them fetched * @param existingProperties the existing properties or null to have them fetched * @param extraAspects any aspects that should be added to the 'missing' set (may be null) * @param extraProperties any properties that should be added the the 'missing' set (may be null) * @param overwriteExistingProperties true if the extra properties must completely overwrite * the existing properties * @return true if properties or aspects were added */ private boolean addAspectsAndProperties( Pair nodePair, QName classQName, Set existingAspects, Map existingProperties, Set extraAspects, Map extraProperties, boolean overwriteExistingProperties) { return addAspectsAndProperties(nodePair, classQName, null, existingAspects, existingProperties, extraAspects, extraProperties, overwriteExistingProperties, true); } private boolean addAspectsAndPropertiesAssoc( Pair nodePair, QName assocTypeQName, Set existingAspects, Map existingProperties, Set extraAspects, Map extraProperties, boolean overwriteExistingProperties) { return addAspectsAndProperties(nodePair, null, assocTypeQName, existingAspects, existingProperties, extraAspects, extraProperties, overwriteExistingProperties, true); } private boolean addAspectsAndProperties( Pair nodePair, QName classQName, QName assocTypeQName, Set existingAspects, Map existingProperties, Set extraAspects, Map 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 allProperties = new HashMap(37); allProperties.putAll(existingProperties); allProperties.putAll(extraProperties); // Copy incoming existing values so that we can modify appropriately existingAspects = new HashSet(existingAspects); // Get the 'missing' aspects and append the 'extra' aspects Set 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 allClassQNames = new HashSet(13); allClassQNames.add(classQName); allClassQNames.addAll(missingAspects); Map 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 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 getMissingAspectsAssoc( Set existingAspects, Map 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 does not 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 getMissingAspects( Set existingAspects, Map existingProperties, QName classQName) { // Copy incoming existing values so that we can modify appropriately existingAspects = new HashSet(existingAspects); ClassDefinition classDefinition = dictionaryService.getClass(classQName); if (classDefinition == null) { return Collections.emptySet(); } Set missingAspects = new HashSet(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 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 allTypesAndAspects = new HashSet(13); allTypesAndAspects.add(classQName); allTypesAndAspects.addAll(existingAspects); allTypesAndAspects.addAll(missingAspects); Set missingAspectsCopy = new HashSet(missingAspects); for (QName missingAspect : missingAspectsCopy) { Set furtherMissingAspects = getMissingAspects( allTypesAndAspects, Collections.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 getMissingProperties(Map existingProperties, Set classQNames) { Map allDefaultProperties = new HashMap(17); for (QName classQName : classQNames) { ClassDefinition classDefinition = dictionaryService.getClass(classQName); if (classDefinition == null) { continue; } // Get the default properties for this type/aspect Map defaultProperties = getDefaultProperties(classQName); if (defaultProperties.size() > 0) { allDefaultProperties.putAll(defaultProperties); } } // Work out what is missing Map missingProperties = new HashMap(allDefaultProperties); missingProperties.keySet().removeAll(existingProperties.keySet()); // Done return missingProperties; } public void setChildAssociationIndex(ChildAssociationRef childAssocRef, int index) { // get nodes Pair parentNodePair = getNodePairNotNull(childAssocRef.getParentRef()); Pair 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); } } public QName getType(NodeRef nodeRef) throws InvalidNodeRefException { Pair 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) */ public void setType(NodeRef nodeRef, QName typeQName) throws InvalidNodeRefException { // check the node type TypeDefinition nodeTypeDef = dictionaryService.getType(typeQName); if (nodeTypeDef == null) { throw new InvalidTypeException(typeQName); } Pair nodePair = getNodePairNotNull(nodeRef); // Invoke policies invokeBeforeUpdateNode(nodeRef); // Set the type nodeDAO.updateNode(nodePair.getFirst(), null, null, typeQName); // Add the default aspects and properties required for the given type. Existing values will not be overridden. addAspectsAndProperties(nodePair, typeQName, null, null, null, null, false); // Index nodeIndexer.indexUpdateNode(nodeRef); // Invoke policies invokeOnUpdateNode(nodeRef); } /** * @see Node#getAspects() */ public void addAspect( NodeRef nodeRef, QName aspectTypeQName, Map 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); } // 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 nodePair = getNodePairNotNull(nodeRef); boolean modified = addAspectsAndProperties( nodePair, aspectTypeQName, null, null, Collections.singleton(aspectTypeQName), aspectProperties, false); if (modified) { // Invoke policy behaviours invokeOnUpdateNode(nodeRef); // Index nodeIndexer.indexUpdateNode(nodeRef); } } public void removeAspect(NodeRef nodeRef, QName aspectTypeQName) throws InvalidNodeRefException, InvalidAspectException { /** * Note: Aspect and property removal is resilient to missing dictionary definitions */ // get the node final Pair 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 propertyDefs = aspectDef.getProperties(); Set 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> assocsToDelete = new ArrayList>(5); final List> nodesToDelete = new ArrayList>(5); NodeDAO.ChildAssocRefQueryCallback callback = new NodeDAO.ChildAssocRefQueryCallback() { public boolean handle( Pair childAssocPair, Pair parentNodePair, Pair childNodePair ) { // 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 boolean preLoadNodes() { return true; } public void done() { } }; // Get all the QNames to remove Set assocTypeQNamesToRemove = new HashSet(aspectDef.getChildAssociations().keySet()); nodeDAO.getChildAssocs(nodeId, assocTypeQNamesToRemove, callback); // Delete all the collected associations for (Pair 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 childNodePair : nodesToDelete) { NodeRef childNodeRef = childNodePair.getSecond(); this.deleteNode(childNodeRef); } // Remove regular associations Map nodeAssocDefs = aspectDef.getAssociations(); Set nodeAssocTypeQNamesToRemove = new HashSet(13); for (Map.Entry entry : nodeAssocDefs.entrySet()) { if (entry.getValue().isChild()) { // Not interested in child assocs continue; } nodeAssocTypeQNamesToRemove.add(entry.getKey()); } int assocsDeleted = nodeDAO.removeNodeAssocsToAndFrom(nodeId, nodeAssocTypeQNamesToRemove); 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 */ public boolean hasAspect(NodeRef nodeRef, QName aspectQName) throws InvalidNodeRefException, InvalidAspectException { Pair nodePair = getNodePairNotNull(nodeRef); return nodeDAO.hasNodeAspect(nodePair.getFirst(), aspectQName); } public Set getAspects(NodeRef nodeRef) throws InvalidNodeRefException { Pair nodePair = getNodePairNotNull(nodeRef); return nodeDAO.getNodeAspects(nodePair.getFirst()); } /** * Delete Node */ public void deleteNode(NodeRef nodeRef) { // Pair contains NodeId, NodeRef Pair nodePair = getNodePairNotNull(nodeRef); Long nodeId = nodePair.getFirst(); Boolean requiresDelete = null; // get the primary parent-child relationship before it is gone Pair childAssocPair = nodeDAO.getPrimaryParentAssoc(nodeId); ChildAssociationRef childAssocRef = childAssocPair.getSecond(); // get type and aspect QNames as they will be unavailable after the delete QName nodeTypeQName = nodeDAO.getNodeType(nodeId); Set nodeAspectQNames = nodeDAO.getNodeAspects(nodeId); StoreRef storeRef = nodeRef.getStoreRef(); StoreRef archiveStoreRef = storeArchiveMap.get(storeRef); /** * Work out whether we need to archive or delete the node. */ 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 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; } } } } /** * Now we have worked out whether to archive or delete, go ahead and do it */ if (requiresDelete == null || requiresDelete) { // remove the deleted node from the list of new nodes untrackNewNodeRef(nodeRef); // track the deletion of this node - so we can prevent new associations to it. trackDeletedNodeRef(nodeRef); // Invoke policy behaviours invokeBeforeDeleteNode(nodeRef); // Cascade delecte as required deletePrimaryChildrenNotArchived(nodePair); // perform a normal deletion nodeDAO.deleteNode(nodeId); // Invoke policy behaviours invokeOnDeleteNode(childAssocRef, nodeTypeQName, nodeAspectQNames, false); // Index nodeIndexer.indexDeleteNode(childAssocRef); } else { /* * Go ahead and archive the node * * Archiving will take responsibility for firing the policy behaviours on * the nodes it modifies. */ archiveNode(nodeRef, archiveStoreRef); } } /** * delete primary children - private method for deleteNode. * * recurses through children when deleting a node. Does not archive. */ private void deletePrimaryChildrenNotArchived(Pair nodePair) { Long nodeId = nodePair.getFirst(); // Get the node's primary children final List> childNodePairs = new ArrayList>(5); final Map childAssocRefsByChildId = new HashMap(5); NodeDAO.ChildAssocRefQueryCallback callback = new NodeDAO.ChildAssocRefQueryCallback() { public boolean handle( Pair childAssocPair, Pair parentNodePair, Pair childNodePair ) { // Add it childNodePairs.add(childNodePair); childAssocRefsByChildId.put(childNodePair.getFirst(), childAssocPair.getSecond()); // More results return true; } public boolean preLoadNodes() { return true; } public void done() { } }; // Get all the QNames to remove nodeDAO.getChildAssocs(nodeId, null, null, null, Boolean.TRUE, null, callback); // Each child must be deleted for (Pair childNodePair : childNodePairs) { // Fire node policies. This ensures that each node in the hierarchy gets a notification fired. Long childNodeId = childNodePair.getFirst(); NodeRef childNodeRef = childNodePair.getSecond(); QName childNodeType = nodeDAO.getNodeType(childNodeId); Set childNodeQNames = nodeDAO.getNodeAspects(childNodeId); ChildAssociationRef childParentAssocRef = childAssocRefsByChildId.get(childNodeId); // remove the deleted node from the list of new nodes untrackNewNodeRef(childNodeRef); // track the deletion of this node - so we can prevent new associations to it. trackDeletedNodeRef(childNodeRef); invokeBeforeDeleteNode(childNodeRef); // Cascade first // This ensures that the beforeDelete policy is fired for all nodes in the hierarchy before // the actual delete starts. deletePrimaryChildrenNotArchived(childNodePair); // Delete the child nodeDAO.deleteNode(childNodeId); invokeOnDeleteNode(childParentAssocRef, childNodeType, childNodeQNames, false); // lose interest in tracking this node ref untrackNewNodeRef(childNodeRef); } } public ChildAssociationRef addChild(NodeRef parentRef, NodeRef childRef, QName assocTypeQName, QName assocQName) { return addChild(Collections.singletonList(parentRef), childRef, assocTypeQName, assocQName).get(0); } public List addChild(Collection parentRefs, NodeRef childRef, QName assocTypeQName, QName assocQName) { // Get the node's name, if present Pair childNodePair = getNodePairNotNull(childRef); Long childNodeId = childNodePair.getFirst(); Map childNodeProperties = nodeDAO.getNodeProperties(childNodePair.getFirst()); String childNodeName = extractNameProperty(childNodeProperties); if (childNodeName == null) { childNodeName = childRef.getId(); } List childAssociationRefs = new ArrayList(parentRefs.size()); List> parentNodePairs = new ArrayList>(parentRefs.size()); for (NodeRef parentRef : parentRefs) { if (isDeletedNodeRef(parentRef)) { throw new InvalidNodeRefException("The parent node has been deleted", parentRef); } Pair parentNodePair = getNodePairNotNull(parentRef); Long parentNodeId = parentNodePair.getFirst(); parentNodePairs.add(parentNodePair); // make the association Pair 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 // this functionality is provided for free in getPath getPaths(childRef, false); // 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 parentNodePair : parentNodePairs) { addAspectsAndPropertiesAssoc(parentNodePair, assocTypeQName, null, null, null, null, false); } // Index for (ChildAssociationRef childAssocRef : childAssociationRefs) { nodeIndexer.indexCreateChildAssociation(childAssocRef); } return childAssociationRefs; } public void removeChild(NodeRef parentRef, NodeRef childRef) throws InvalidNodeRefException { final Pair parentNodePair = getNodePairNotNull(parentRef); final Long parentNodeId = parentNodePair.getFirst(); final Pair childNodePair = getNodePairNotNull(childRef); final Long childNodeId = childNodePair.getFirst(); // Get the primary parent association for the child Pair 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> assocsToDelete = new ArrayList>(5); NodeDAO.ChildAssocRefQueryCallback callback = new NodeDAO.ChildAssocRefQueryCallback() { public boolean handle( Pair childAssocPair, Pair parentNodePair, Pair 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 boolean preLoadNodes() { return false; } public void done() { } }; nodeDAO.getChildAssocs(parentNodeId, childNodeId, null, null, null, null, callback); // Delete all the collected associations for (Pair 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 } public boolean removeChildAssociation(ChildAssociationRef childAssocRef) { Long parentNodeId = getNodePairNotNull(childAssocRef.getParentRef()).getFirst(); Long childNodeId = getNodePairNotNull(childAssocRef.getChildRef()).getFirst(); QName assocTypeQName = childAssocRef.getTypeQName(); QName assocQName = childAssocRef.getQName(); Pair 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 public boolean removeSecondaryChildAssociation(ChildAssociationRef childAssocRef) { Long parentNodeId = getNodePairNotNull(childAssocRef.getParentRef()).getFirst(); Long childNodeId = getNodePairNotNull(childAssocRef.getChildRef()).getFirst(); QName assocTypeQName = childAssocRef.getTypeQName(); QName assocQName = childAssocRef.getQName(); Pair 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 nodeDAO.deleteChildAssoc(assocId); invokeOnDeleteChildAssociation(childAssocRef); // Index nodeIndexer.indexDeleteChildAssociation(childAssocRef); // Done return true; } 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; } public Map getProperties(NodeRef nodeRef) throws InvalidNodeRefException { Pair nodePair = getNodePairNotNull(nodeRef); return getPropertiesImpl(nodePair); } /** * Gets, converts and adds the intrinsic properties to the current node's properties */ private Map getPropertiesImpl(Pair nodePair) throws InvalidNodeRefException { Long nodeId = nodePair.getFirst(); Map nodeProperties = nodeDAO.getNodeProperties(nodeId); // done return nodeProperties; } public Long getNodeAclId(NodeRef nodeRef) throws InvalidNodeRefException { Pair nodePair = getNodePairNotNull(nodeRef); return getAclIDImpl(nodePair); } /** * Gets, converts and adds the intrinsic properties to the current node's properties */ private Long getAclIDImpl(Pair 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 true if any work was done by this method */ private boolean setPropertiesCommonWork(Pair nodePair, Map properties) { Long nodeId = nodePair.getFirst(); boolean changed = false; // cm:name special handling if (properties.containsKey(ContentModel.PROP_NAME)) { String name = extractNameProperty(properties); Pair 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. * * @see DbNodeServiceImpl.NullPropertyValue */ 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 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. * * @see Node#getProperties() */ 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; } 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) { // 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 preload; } 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.getChildAssocs(nodeId, null, typeQName, qname, null, null, callback); // sort the results List orderedList = reorderChildAssocs(results); // Done return orderedList; } 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 handle( Pair childAssocPair, Pair parentNodePair, Pair childNodePair) { results.add(childAssocPair.getSecond()); // More results return true; } public boolean preLoadNodes() { return true; } public void done() { } }; // Get all child associations with the specific qualified name nodeDAO.getChildAssocsByChildTypes(nodeId, childNodeTypeQNames, callback); // Sort the results List orderedList = reorderChildAssocs(results); // Done return orderedList; } private List reorderChildAssocs(Collection childAssocRefs) { // shortcut if there are no assocs if (childAssocRefs.size() == 0) { return Collections.emptyList(); } // sort results ArrayList orderedList = new ArrayList(childAssocRefs); Collections.sort(orderedList); // list of results int nthSibling = 0; Iterator iterator = orderedList.iterator(); while(iterator.hasNext()) { ChildAssociationRef childAssocRef = iterator.next(); childAssocRef.setNthSibling(nthSibling); nthSibling++; } // done return orderedList; } public NodeRef getChildByName(NodeRef nodeRef, QName assocTypeQName, String childName) { // 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 handle( Pair childAssocPair, Pair parentNodePair, Pair childNodePair) { results.add(childAssocPair.getSecond()); // More results return true; } public boolean preLoadNodes() { return true; } public void done() { } }; // Get all child associations with the specific qualified name nodeDAO.getChildAssocs(nodeId, assocTypeQName, childNames, callback); // Sort the results List orderedList = reorderChildAssocs(results); // Done return orderedList; } 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; } public AssociationRef createAssociation( NodeRef sourceRef, NodeRef targetRef, QName assocTypeQName, Long insertAfter) throws InvalidNodeRefException, AssociationExistsException { Pair sourceNodePair = getNodePairNotNull(sourceRef); long sourceNodeId = sourceNodePair.getFirst(); Pair targetNodePair = getNodePairNotNull(targetRef); long targetNodeId = targetNodePair.getFirst(); // Check if ordering is allowed if (insertAfter != null) { AssociationDefinition assocDef = dictionaryService.getAssociation(assocTypeQName); if (assocDef == null /*|| !assocDef.isOrdered()*/) { throw new IllegalArgumentException("Association type '" + assocTypeQName + "' is not ordered."); } } // we are sure that the association doesn't exist - make it Long assocId = nodeDAO.newNodeAssoc(sourceNodeId, targetNodeId, assocTypeQName, insertAfter); 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; } 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 handle(Pair childAssocPair, Pair parentNodePair, Pair childNodePair) { results.add(childAssocPair.getSecond()); // More results return true; } public boolean preLoadNodes() { return false; } 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; } 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); // sort the results List orderedList = reorderChildAssocs(results); // Done return orderedList; } public void removeAssociation(NodeRef sourceRef, NodeRef targetRef, QName assocTypeQName) throws InvalidNodeRefException { Pair sourceNodePair = getNodePairNotNull(sourceRef); long sourceNodeId = sourceNodePair.getFirst(); Pair targetNodePair = getNodePairNotNull(targetRef); long targetNodeId = targetNodePair.getFirst(); // delete it int assocsDeleted = nodeDAO.removeNodeAssoc(sourceNodeId, targetNodeId, assocTypeQName); if (assocsDeleted > 0) { AssociationRef assocRef = new AssociationRef(sourceRef, assocTypeQName, targetRef); // Invoke policy behaviours invokeOnDeleteAssociation(assocRef); } } public AssociationRef getAssoc(Long id) { return nodeDAO.getNodeAssoc(id).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) * @see #prependPaths(Node, Path, Collection, Stack, 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. * @see #prependPaths(Node, Path, Collection, Stack, boolean) */ 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 archiveNode(NodeRef nodeRef, StoreRef archiveStoreRef) { boolean wasDisabled = policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_AUDITABLE); try { archiveNodeImpl(nodeRef, archiveStoreRef); } finally { if (!wasDisabled) { policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_AUDITABLE); } } } private void archiveNodeImpl(NodeRef nodeRef, StoreRef archiveStoreRef) { Pair nodePair = getNodePairNotNull(nodeRef); Long nodeId = nodePair.getFirst(); Pair primaryParentAssocPair = nodeDAO.getPrimaryParentAssoc(nodeId); Set newAspects = new HashSet(5); Map existingProperties = nodeDAO.getNodeProperties(nodeId); Map newProperties = new HashMap(11); // add the aspect newAspects.add(ContentModel.ASPECT_ARCHIVED); newProperties.put(ContentModel.PROP_ARCHIVED_BY, AuthenticationUtil.getFullyAuthenticatedUser()); newProperties.put(ContentModel.PROP_ARCHIVED_DATE, new Date()); newProperties.put(ContentModel.PROP_ARCHIVED_ORIGINAL_PARENT_ASSOC, primaryParentAssocPair.getSecond()); Serializable originalOwner = existingProperties.get(ContentModel.PROP_OWNER); Serializable originalCreator = existingProperties.get(ContentModel.PROP_CREATOR); if (originalOwner != null || originalCreator != null) { newProperties.put( ContentModel.PROP_ARCHIVED_ORIGINAL_OWNER, originalOwner != null ? originalOwner : originalCreator); } // change the node ownership newAspects.add(ContentModel.ASPECT_OWNABLE); newProperties.put(ContentModel.PROP_OWNER, AuthenticationUtil.getFullyAuthenticatedUser()); // Set the aspects and properties nodeDAO.addNodeProperties(nodeId, newProperties); nodeDAO.addNodeAspects(nodeId, newAspects); // move the node Pair archiveStoreRootNodePair = nodeDAO.getRootNode(archiveStoreRef); moveNode( nodeRef, archiveStoreRootNodePair.getSecond(), ContentModel.ASSOC_CHILDREN, NodeArchiveService.QNAME_ARCHIVED_ITEM); } /** * {@inheritDoc} * * Archives the node without the cm:auditable aspect behaviour */ public NodeRef restoreNode(NodeRef archivedNodeRef, NodeRef destinationParentNodeRef, QName assocTypeQName, QName assocQName) { boolean wasDisabled = policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_AUDITABLE); try { return restoreNodeImpl(archivedNodeRef, destinationParentNodeRef, assocTypeQName, assocQName); } finally { if (!wasDisabled) { policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_AUDITABLE); } } } private NodeRef restoreNodeImpl(NodeRef archivedNodeRef, NodeRef destinationParentNodeRef, QName assocTypeQName, QName assocQName) { Pair archivedNodePair = getNodePairNotNull(archivedNodeRef); Long archivedNodeId = archivedNodePair.getFirst(); Set existingAspects = nodeDAO.getNodeAspects(archivedNodeId); Set newAspects = new HashSet(5); Map existingProperties = nodeDAO.getNodeProperties(archivedNodeId); Map newProperties = new HashMap(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"); } 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 removePropertyQNames = new HashSet(11); 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); nodeDAO.removeNodeProperties(archivedNodeId, removePropertyQNames); nodeDAO.removeNodeAspects(archivedNodeId, Collections.singleton(ContentModel.ASPECT_ARCHIVED)); // restore the original ownership if (originalOwner != null) { newAspects.add(ContentModel.ASPECT_OWNABLE); newProperties.put(ContentModel.PROP_OWNER, originalOwner); } 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 */ public ChildAssociationRef moveNode( NodeRef nodeToMoveRef, NodeRef newParentRef, QName assocTypeQName, QName assocQName) { if (isDeletedNodeRef(newParentRef)) { throw new InvalidNodeRefException("The parent node has been deleted", newParentRef); } Pair nodeToMovePair = getNodePairNotNull(nodeToMoveRef); Pair parentNodePair = getNodePairNotNull(newParentRef); Long nodeToMoveId = nodeToMovePair.getFirst(); QName nodeToMoveTypeQName = nodeDAO.getNodeType(nodeToMoveId); NodeRef oldNodeToMoveRef = nodeToMovePair.getSecond(); Long parentNodeId = parentNodePair.getFirst(); NodeRef parentNodeRef = parentNodePair.getSecond(); StoreRef oldStoreRef = oldNodeToMoveRef.getStoreRef(); StoreRef newStoreRef = parentNodeRef.getStoreRef(); NodeRef newNodeToMoveRef = new NodeRef(newStoreRef, oldNodeToMoveRef.getId()); Pair newNodeToMovePair = new Pair(nodeToMoveId, newNodeToMoveRef); // Get the primary parent association Pair 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(); // Shortcut this whole process if nothing has changed if (EqualsHelper.nullSafeEquals(oldParentAssocRef.getParentRef(), newParentRef) && EqualsHelper.nullSafeEquals(oldParentAssocRef.getTypeQName(), assocTypeQName) && EqualsHelper.nullSafeEquals(oldParentAssocRef.getQName(), assocQName)) { // It's all just the same return oldParentAssocRef; } boolean movingStore = !oldStoreRef.equals(newStoreRef); // Invoke "Before"policy behaviour if (movingStore) { // remove the deleted node from the list of new nodes untrackNewNodeRef(nodeToMoveRef); // track the deletion of this node - so we can prevent new associations to it. trackDeletedNodeRef(nodeToMoveRef); invokeBeforeDeleteNode(nodeToMoveRef); invokeBeforeCreateNode(newParentRef, assocTypeQName, assocQName, nodeToMoveTypeQName); } else { invokeBeforeDeleteChildAssociation(oldParentAssocRef); } // Move node under the new parent Pair newParentAssocPair = nodeDAO.moveNode( nodeToMoveId, parentNodeId, assocTypeQName, assocQName); ChildAssociationRef newParentAssocRef = newParentAssocPair.getSecond(); // Handle indexing differently if it is a store move if (movingStore) { // The association existed before and the node is moving to a new store nodeIndexer.indexDeleteNode(oldParentAssocRef); nodeIndexer.indexCreateNode(newParentAssocRef); } else { // The node is in the same store and is just having it's child association modified nodeIndexer.indexUpdateChildAssociation(oldParentAssocRef, newParentAssocRef); } // Call behaviours if (movingStore) { Set nodeToMoveAspectQNames = nodeDAO.getNodeAspects(nodeToMoveId); // 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, nodeToMoveTypeQName, nodeToMoveAspectQNames, true); invokeOnCreateNode(newParentAssocRef); // Pull children to the new store pullNodeChildrenToSameStore(newNodeToMovePair); } else { invokeOnCreateChildAssociation(newParentAssocRef, false); invokeOnDeleteChildAssociation(oldParentAssocRef); invokeOnMoveNode(oldParentAssocRef, newParentAssocRef); } // Done return newParentAssocRef; } /** * This process is less invasive than the move method as the child associations * do not need to be remade. */ private void pullNodeChildrenToSameStore(Pair nodePair) { Long nodeId = nodePair.getFirst(); // Get the node's children, but only one's that aren't in the same store final List> childNodePairs = new ArrayList>(5); NodeDAO.ChildAssocRefQueryCallback callback = new NodeDAO.ChildAssocRefQueryCallback() { public boolean handle( Pair childAssocPair, Pair parentNodePair, Pair childNodePair ) { // Add it childNodePairs.add(childNodePair); // More results return true; } public boolean preLoadNodes() { return true; } public void done() { } }; // We only need to move child nodes that are not already in the same store nodeDAO.getChildAssocs(nodeId, null, null, null, Boolean.TRUE, Boolean.FALSE, callback); // Each child must be moved to the same store as the parent for (Pair oldChildNodePair : childNodePairs) { Long childNodeId = oldChildNodePair.getFirst(); NodeRef childNodeRef = oldChildNodePair.getSecond(); NodeRef.Status childNodeStatus = nodeDAO.getNodeRefStatus(childNodeRef); if (childNodeStatus == null || childNodeStatus.isDeleted()) { // Node has already been deleted. continue; } QName childNodeTypeQName = nodeDAO.getNodeType(childNodeId); Set childNodeAspectQNames = nodeDAO.getNodeAspects(childNodeId); Pair oldParentAssocPair = nodeDAO.getPrimaryParentAssoc(childNodeId); Pair newChildNodePair = oldChildNodePair; Pair newParentAssocPair = oldParentAssocPair; ChildAssociationRef newParentAssocRef = newParentAssocPair.getSecond(); // remove the deleted node from the list of new nodes untrackNewNodeRef(childNodeRef); // track the deletion of this node - so we can prevent new associations to it. trackDeletedNodeRef(childNodeRef); // Fire node policies. This ensures that each node in the hierarchy gets a notification fired. invokeBeforeDeleteNode(childNodeRef); invokeBeforeCreateNode( newParentAssocRef.getParentRef(), newParentAssocRef.getTypeQName(), newParentAssocRef.getQName(), childNodeTypeQName); // Move the node as this gives back the primary parent association newParentAssocPair = nodeDAO.moveNode(childNodeId, nodeId, null,null); // Index nodeIndexer.indexCreateNode(newParentAssocPair.getSecond()); // Fire node policies. This ensures that each node in the hierarchy gets a notification fired. invokeOnDeleteNode(oldParentAssocPair.getSecond(), childNodeTypeQName, childNodeAspectQNames, true); invokeOnCreateNode(newParentAssocPair.getSecond()); // Cascade pullNodeChildrenToSameStore(newChildNodePair); } } 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 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 true if the child association cm:name was written */ private boolean setChildNameUnique(Pair 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; } } }