diff --git a/repository/src/main/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java b/repository/src/main/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java index 73731c4604..1a6ca88c59 100644 --- a/repository/src/main/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java +++ b/repository/src/main/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java @@ -1,3247 +1,3244 @@ -/* - * #%L - * Alfresco Repository - * %% - * Copyright (C) 2005 - 2023 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 . - * #L% - */ -package org.alfresco.repo.node.db; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; - -import org.alfresco.error.AlfrescoRuntimeException; -import org.alfresco.model.ContentModel; -import org.alfresco.repo.domain.node.ChildAssocEntity; -import org.alfresco.repo.domain.node.Node; -import org.alfresco.repo.domain.node.NodeDAO; -import org.alfresco.repo.domain.node.NodeDAO.ChildAssocRefQueryCallback; -import org.alfresco.repo.domain.node.NodeExistsException; -import org.alfresco.repo.domain.qname.QNameDAO; -import org.alfresco.repo.node.AbstractNodeServiceImpl; -import org.alfresco.repo.node.StoreArchiveMap; -import org.alfresco.repo.node.archive.NodeArchiveService; -import org.alfresco.repo.node.db.NodeHierarchyWalker.VisitedNode; -import org.alfresco.repo.node.db.traitextender.NodeServiceExtension; -import org.alfresco.repo.node.db.traitextender.NodeServiceTrait; -import org.alfresco.repo.policy.BehaviourFilter; -import org.alfresco.repo.security.authentication.AuthenticationUtil; -import org.alfresco.repo.transaction.AlfrescoTransactionSupport; -import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState; -import org.alfresco.repo.transaction.RetryingTransactionHelper; -import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; -import org.alfresco.repo.transaction.TransactionalResourceHelper; -import org.alfresco.service.cmr.dictionary.AspectDefinition; -import org.alfresco.service.cmr.dictionary.AssociationDefinition; -import org.alfresco.service.cmr.dictionary.ChildAssociationDefinition; -import org.alfresco.service.cmr.dictionary.ClassDefinition; -import org.alfresco.service.cmr.dictionary.InvalidAspectException; -import org.alfresco.service.cmr.dictionary.InvalidTypeException; -import org.alfresco.service.cmr.dictionary.PropertyDefinition; -import org.alfresco.service.cmr.dictionary.TypeDefinition; -import org.alfresco.service.cmr.repository.AssociationExistsException; -import org.alfresco.service.cmr.repository.AssociationRef; -import org.alfresco.service.cmr.repository.ChildAssociationRef; -import org.alfresco.service.cmr.repository.InvalidChildAssociationRefException; -import org.alfresco.service.cmr.repository.InvalidNodeRefException; -import org.alfresco.service.cmr.repository.InvalidStoreRefException; -import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.repository.NodeRef.Status; -import org.alfresco.service.cmr.repository.NodeService; -import org.alfresco.service.cmr.repository.Path; -import org.alfresco.service.cmr.repository.StoreRef; -import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; -import org.alfresco.service.cmr.security.AccessPermission; -import org.alfresco.service.cmr.security.AccessStatus; -import org.alfresco.service.cmr.security.OwnableService; -import org.alfresco.service.cmr.security.PermissionService; -import org.alfresco.service.namespace.NamespaceService; -import org.alfresco.service.namespace.QName; -import org.alfresco.service.namespace.QNamePattern; -import org.alfresco.service.namespace.RegexQNamePattern; -import org.alfresco.traitextender.AJProxyTrait; -import org.alfresco.traitextender.Extend; -import org.alfresco.traitextender.ExtendedTrait; -import org.alfresco.traitextender.Extensible; -import org.alfresco.traitextender.Trait; -import org.alfresco.util.EqualsHelper; -import org.alfresco.util.GUID; -import org.alfresco.util.Pair; -import org.alfresco.util.ParameterCheck; -import org.alfresco.util.PropertyMap; -import org.alfresco.util.transaction.TransactionListenerAdapter; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.extensions.surf.util.I18NUtil; - -/** - * Node service using database persistence layer to fulfill functionality - * - * @author Derek Hulley - */ -public class DbNodeServiceImpl extends AbstractNodeServiceImpl implements Extensible , NodeService -{ - public static final String KEY_PENDING_DELETE_NODES = "DbNodeServiceImpl.pendingDeleteNodes"; - - private static Log logger = LogFactory.getLog(DbNodeServiceImpl.class); - - private QNameDAO qnameDAO; - private NodeDAO nodeDAO; - private PermissionService permissionService; - private StoreArchiveMap storeArchiveMap; - private BehaviourFilter policyBehaviourFilter; - private boolean enableTimestampPropagation; - private final ExtendedTrait nodeServiceTrait; - - public DbNodeServiceImpl() - { - nodeServiceTrait = new ExtendedTrait(AJProxyTrait.create(this, NodeServiceTrait.class)); - storeArchiveMap = new StoreArchiveMap(); // in case it is not set - } - - public void setQnameDAO(QNameDAO qnameDAO) - { - this.qnameDAO = qnameDAO; - } - - public void setNodeDAO(NodeDAO nodeDAO) - { - this.nodeDAO = nodeDAO; - } - - public void setPermissionService(PermissionService permissionService) - { - this.permissionService = permissionService; - } - - public void setStoreArchiveMap(StoreArchiveMap storeArchiveMap) - { - this.storeArchiveMap = storeArchiveMap; - } - - /** - * - * @param policyBehaviourFilter component used to enable and disable behaviours - */ - public void setPolicyBehaviourFilter(BehaviourFilter policyBehaviourFilter) - { - this.policyBehaviourFilter = policyBehaviourFilter; - } - - /** - * Set whether cm:auditable timestamps should be propagated to parent nodes - * where the parent-child relationship has been marked using propagateTimestamps. - * - * @param enableTimestampPropagation true 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 getNodePairNotNull(NodeRef nodeRef) throws InvalidNodeRefException - { - ParameterCheck.mandatory("nodeRef", nodeRef); - - Pair 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 nodePair = nodeDAO.getNodePair(nodeId); - return nodePair == null ? null : nodePair.getSecond(); - } - - /** - * {@inheritDoc} - */ - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - 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); - } - // 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 rootNodePair = nodeDAO.newStore(storeRef); - NodeRef rootNodeRef = rootNodePair.getSecond(); - - // invoke policies - invokeOnCreateStore(rootNodeRef); - - // Done - return storeRef; - } - - /** - * @throws UnsupportedOperationException Always - */ - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - public void deleteStore(StoreRef storeRef) throws InvalidStoreRefException - { - // Cannot delete the root node but we can delete, without archive, all immediate children - NodeRef rootNodeRef = nodeDAO.getRootNode(storeRef).getSecond(); - List 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 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 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 getChildAssocs(NodeRef nodeRef) throws InvalidNodeRefException - { - return super.getChildAssocs(nodeRef); - } - - @Override - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - public List 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 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 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 childNodePair = assoc.getChildNode().getNodePair(); - - addAspectsAndProperties( - childNodePair, - nodeTypeQName, - null, - Collections.emptySet(), - Collections.emptyMap(), - Collections.emptySet(), - properties, - true, - false); - - Map propertiesAfter = nodeDAO.getNodeProperties(childNodePair.getFirst()); - - // Propagate timestamps - propagateTimeStamps(childAssocRef); - - // Invoke policy behaviour - invokeOnCreateNode(childAssocRef); - invokeOnCreateChildAssociation(childAssocRef, true); - Map propertiesBefore = PropertyMap.EMPTY_MAP; - invokeOnUpdateProperties( - childAssocRef.getChildRef(), - propertiesBefore, - propertiesAfter); - - // Ensure that the parent node has the required aspects - addAspectsAndPropertiesAssoc(parentNodePair, assocTypeQName, null, null, null, null, false); - - // done - return childAssocRef; - } - - - /** - * Adds all the aspects and properties required for the given node, along with mandatory aspects - * and related properties. - * Existing values will not be overridden. All required pre- and post-update notifications - * are sent for missing aspects. - * - * @param nodePair the node to which the details apply - * @param classQName the type or aspect QName for which the defaults must be applied. - * If this is 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; - } - - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - 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); - } - } - - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - 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) - */ - @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 nodePair = getNodePairNotNull(nodeRef); - - // Invoke policies - invokeBeforeUpdateNode(nodeRef); - QName oldType = nodeDAO.getNodeType(nodePair.getFirst()); - invokeBeforeSetType(nodeRef, oldType, typeQName); - - // Set the type - boolean updatedNode = nodeDAO.updateNode(nodePair.getFirst(), typeQName, null); - - // Add the default aspects and properties required for the given type. Existing values will not be overridden. - boolean updatedProps = addAspectsAndProperties(nodePair, typeQName, null, null, null, null, false); - - // Invoke policies - if (updatedNode || updatedProps) - { - // Invoke policies - invokeOnUpdateNode(nodeRef); - invokeOnSetType(nodeRef, oldType, typeQName); - } - } - - @Override - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - public void addAspect( - NodeRef nodeRef, - QName aspectTypeQName, - Map 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 nodePair = getNodePairNotNull(nodeRef); - // SetProperties common tasks - setPropertiesCommonWork(nodePair, aspectProperties); - boolean modified = addAspectsAndProperties( - nodePair, - aspectTypeQName, - null, - null, - Collections.singleton(aspectTypeQName), - aspectProperties, - false); - - if (modified) - { - // Invoke policy behaviours - invokeOnUpdateNode(nodeRef); - } - } - - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - public int countChildAssocs(NodeRef nodeRef, boolean isPrimary) throws InvalidNodeRefException - { - final Pair 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 nodePair = getNodePairNotNull(nodeRef); - final Long nodeId = nodePair.getFirst(); - - if (!nodeDAO.hasNodeAspect(nodeId, aspectTypeQName)) - { - return; - } - // Invoke policy behaviours - invokeBeforeUpdateNode(nodeRef); - 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 preLoadNodes() - { - return true; - } - - @Override public boolean orderResults() - { - return false; - } - - public boolean handle(Pair childAssocPair, Pair parentNodePair, - Pair 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 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); - } - - // Gather peer associations to delete - Map nodeAssocDefs = aspectDef.getAssociations(); - List nodeAssocIdsToRemove = new ArrayList(13); - List assocRefsRemoved = new ArrayList(13); - for (Map.Entry 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> targetAssocRefs = nodeDAO.getTargetNodeAssocs(nodeId, assocTypeQName); - for (Pair 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); - } - - invokeOnRemoveAspect(nodeRef, aspectTypeQName); - - } - - /** - * Performs a check on the set of node aspects - */ - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - public boolean hasAspect(NodeRef nodeRef, QName aspectQName) throws InvalidNodeRefException, InvalidAspectException - { - if (aspectQName.equals(ContentModel.ASPECT_PENDING_DELETE)) - { - return isPendingDelete(nodeRef); - } - Pair nodePair = getNodePairNotNull(nodeRef); - return nodeDAO.hasNodeAspect(nodePair.getFirst(), aspectQName); - } - - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - public Set getAspects(NodeRef nodeRef) throws InvalidNodeRefException - { - Pair nodePair = getNodePairNotNull(nodeRef); - Set aspectQNames = nodeDAO.getNodeAspects(nodePair.getFirst()); - if (isPendingDelete(nodeRef)) - { - aspectQNames.add(ContentModel.ASPECT_PENDING_DELETE); - } - return aspectQNames; - } - - /** - * @return Returns true 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 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 true if normal archival may occur or - * false 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 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 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 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 nodesPendingDelete = new HashSet(walker.getNodes(false).size()); - for (VisitedNode visitedNode : walker.getNodes(true)) - { - nodesPendingDelete.add(visitedNode.nodeRef); - } - Set 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 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 childAssocIds = new HashSet(23); // Prevents duplicate firing - Set peerAssocIds = new HashSet(23); // Prevents duplicate firing - List nodesToDelete = walker.getNodes(true); - for (VisitedNode nodeToDelete : nodesToDelete) - { - // Target associations - for (Pair targetAssocPair : nodeToDelete.targetAssocs) - { - if (!peerAssocIds.add(targetAssocPair.getFirst())) - { - continue; // Already fired - } - invokeBeforeDeleteAssociation(targetAssocPair.getSecond()); - } - // Source associations - for (Pair sourceAssocPair : nodeToDelete.sourceAssocs) - { - if (!peerAssocIds.add(sourceAssocPair.getFirst())) - { - continue; // Already fired - } - invokeBeforeDeleteAssociation(sourceAssocPair.getSecond()); - } - // Secondary child associations - for (Pair secondaryChildAssocPair : nodeToDelete.secondaryChildAssocs) - { - if (!childAssocIds.add(secondaryChildAssocPair.getFirst())) - { - continue; // Already fired - } - invokeBeforeDeleteChildAssociation(secondaryChildAssocPair.getSecond()); - } - // Secondary parent associations - for (Pair 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 targetAssocPair : nodeToDelete.targetAssocs) - { - if (!peerAssocIds.add(targetAssocPair.getFirst())) - { - continue; // Already fired - } - nodeDAO.removeNodeAssocs(Collections.singletonList(targetAssocPair.getFirst())); - invokeOnDeleteAssociation(targetAssocPair.getSecond()); - } - // Source associations - for (Pair 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 secondaryChildAssocPair : nodeToDelete.secondaryChildAssocs) - { - if (!childAssocIds.add(secondaryChildAssocPair.getFirst())) - { - continue; // Already fired - } - nodeDAO.deleteChildAssoc(secondaryChildAssocPair.getFirst()); - invokeOnDeleteChildAssociation(secondaryChildAssocPair.getSecond()); - } - // Secondary parent associations - for (Pair secondaryParentAssocPair : nodeToDelete.secondaryParentAssocs) - { - if (!childAssocIds.add(secondaryParentAssocPair.getFirst())) - { - continue; // Already fired - } - nodeDAO.deleteChildAssoc(secondaryParentAssocPair.getFirst()); - invokeOnDeleteChildAssociation(secondaryParentAssocPair.getSecond()); - } - QName childNodeTypeQName = nodeDAO.getNodeType(nodeToDelete.id); - Set childAspectQnames = nodeDAO.getNodeAspects(nodeToDelete.id); - // Delete the node - nodeDAO.deleteChildAssoc(nodeToDelete.primaryParentAssocPair.getFirst()); - nodeDAO.deleteNode(nodeToDelete.id); - invokeOnDeleteNode( - nodeToDelete.primaryParentAssocPair.getSecond(), - childNodeTypeQName, childAspectQnames, archive); - } - - // Clear out the list of nodes pending delete - nodesPendingDeleteTxn = TransactionalResourceHelper.getSet(KEY_PENDING_DELETE_NODES); - nodesPendingDeleteTxn.removeAll(nodesPendingDelete); - } - - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - public ChildAssociationRef addChild(NodeRef parentRef, NodeRef childRef, QName assocTypeQName, QName assocQName) - { - return addChild(Collections.singletonList(parentRef), childRef, assocTypeQName, assocQName).get(0); - } - - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - public List addChild(Collection 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 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) - { - // The node(s) involved may not be pending deletion - checkPendingDelete(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 - 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 parentNodePair : parentNodePairs) - { - addAspectsAndPropertiesAssoc(parentNodePair, assocTypeQName, null, null, null, null, false); - } - - return childAssociationRefs; - } - - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - public void removeChild(NodeRef parentRef, NodeRef childRef) throws InvalidNodeRefException - { - // The node(s) involved may not be pending deletion - checkPendingDelete(parentRef); - checkPendingDelete(childRef); - - final Pair 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 preLoadNodes() - { - return true; - } - - @Override - public boolean orderResults() - { - return false; - } - - 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 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); - } - - // 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 assocPair = nodeDAO.getChildAssoc( - parentNodeId, childNodeId, assocTypeQName, assocQName); - if (assocPair == null) - { - // No association exists - return false; - } - Long assocId = assocPair.getFirst(); - ChildAssociationRef assocRef = assocPair.getSecond(); - if (assocRef.isPrimary()) - { - NodeRef childNodeRef = assocRef.getChildRef(); - // Delete the child node - this.deleteNode(childNodeRef); - // Done - return true; - } - else - { - // Delete the association - invokeBeforeDeleteChildAssociation(childAssocRef); - nodeDAO.deleteChildAssoc(assocId); - invokeOnDeleteChildAssociation(childAssocRef); - // Done - return true; - } - } - - @Override - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - public boolean removeSecondaryChildAssociation(ChildAssociationRef childAssocRef) - { - // The node(s) involved may not be pending deletion - checkPendingDelete(childAssocRef.getParentRef()); - checkPendingDelete(childAssocRef.getChildRef()); - - Long parentNodeId = getNodePairNotNull(childAssocRef.getParentRef()).getFirst(); - Long childNodeId = getNodePairNotNull(childAssocRef.getChildRef()).getFirst(); - QName assocTypeQName = childAssocRef.getTypeQName(); - QName assocQName = childAssocRef.getQName(); - Pair assocPair = nodeDAO.getChildAssoc( - parentNodeId, childNodeId, assocTypeQName, assocQName); - if (assocPair == null) - { - // No association exists - return false; - } - Long assocId = assocPair.getFirst(); - ChildAssociationRef assocRef = assocPair.getSecond(); - if (assocRef.isPrimary()) - { - throw new IllegalArgumentException( - "removeSeconaryChildAssociation can not be applied to a primary association: \n" + - " Child Assoc: " + assocRef); - } - // Delete the secondary association - invokeBeforeDeleteChildAssociation(childAssocRef); - nodeDAO.deleteChildAssoc(assocId); - invokeOnDeleteChildAssociation(childAssocRef); - // Done - return true; - } - - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - public Serializable getProperty(NodeRef nodeRef, QName qname) throws InvalidNodeRefException - { - Long nodeId = getNodePairNotNull(nodeRef).getFirst(); - // Spoof referencable properties - if (qname.equals(ContentModel.PROP_STORE_PROTOCOL)) - { - return nodeRef.getStoreRef().getProtocol(); - } - else if (qname.equals(ContentModel.PROP_STORE_IDENTIFIER)) - { - return nodeRef.getStoreRef().getIdentifier(); - } - else if (qname.equals(ContentModel.PROP_NODE_UUID)) - { - return nodeRef.getId(); - } - else if (qname.equals(ContentModel.PROP_NODE_DBID)) - { - return nodeId; - } - - Serializable property = nodeDAO.getNodeProperty(nodeId, qname); - - // check if we need to provide a spoofed name - if (property == null && qname.equals(ContentModel.PROP_NAME)) - { - return nodeRef.getId(); - } - - // done - return property; - } - - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - public Map 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; - } - - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - 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. - * - */ - @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 nodePair = getNodePairNotNull(nodeRef); - - // Invoke policy behaviour - invokeBeforeUpdateNode(nodeRef); - - // cm:name special handling - setPropertiesCommonWork( - nodePair, - Collections.singletonMap(qname, value)); - - // Add the property and all required defaults - boolean changed = addAspectsAndProperties( - nodePair, null, - null, null, - null, Collections.singletonMap(qname, value), false); - - if (changed) - { - // Invoke policy behaviour - invokeOnUpdateNode(nodeRef); - } - } - - /** - * Ensures that all required properties are present on the node and copies the - * property values to the 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. - * - */ - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - 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); - } - } - - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - 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); - } - } - - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - 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); - } - - 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. - */ - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - 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; - } - - @Override - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - public List 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 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 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 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; - } - - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - 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; - } - - @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 nodePair = getNodePairNotNull(nodeRef); - Long nodeId = nodePair.getFirst(); - - Pair 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 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; - } - - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - 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 - @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 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 - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - 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); - } - } - - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - 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; - } - - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - public List findAssocsNotLinkedByTwoOtherAssocs(NodeRef parent) - { - // Get the parent node - Pair nodePair = getNodePairNotNull(parent); - Long parentNodeId = nodePair.getFirst(); - - return nodeDAO.selectAssocsNotLinkedByTwoOtherAssocs(parentNodeId); - } - /** - * 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 - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - 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; - } - - @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 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 - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - public AssociationRef getAssoc(Long id) - { - Pair nodeAssocPair = nodeDAO.getNodeAssocOrNull(id); - return nodeAssocPair == null ? null : nodeAssocPair.getSecond(); - } - - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - 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; - } - - @Override - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - public List getTargetAssocsByPropertyValue(NodeRef sourceRef, QNamePattern qnamePattern, QName propertyQName, Serializable propertyValue) - { - Pair 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> assocPairs = nodeDAO.getTargetAssocsByPropertyValue(sourceNodeId, qnameFilter, propertyQName, propertyValue); - 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; - } - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - 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) - */ - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - 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. - */ - @Extend(traitAPI=NodeServiceTrait.class,extensionAPI=NodeServiceExtension.class) - 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> archiveRecord = new HashMap>(walker.getNodes(false).size() * 2); - for (VisitedNode node : walker.getNodes(false)) - { - // Get node metadata - Map archiveProperties = nodeDAO.getNodeProperties(node.id); - Set archiveAspects = nodeDAO.getNodeAspects(node.id); - - // The first node gets special treatment as it contains the archival details - ChildAssociationRef archivePrimaryParentAssocRef = null; - final Pair 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 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 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 userArchiveNodeProps = Collections.singletonMap( - ContentModel.PROP_NAME, (Serializable) username); - userArchiveNodeRef = createNode( - archiveStoreRootNodeRef, - ContentModel.ASSOC_ARCHIVE_USER_LINK, - archiveUserAssocQName, - ContentModel.TYPE_ARCHIVE_USER, - userArchiveNodeProps).getChildRef(); - } - else - { - userArchiveNodeRef = userArchiveAssocPair.getSecond().getChildRef(); - } - // Link user node to archived item via secondary child association - String archiveNodeName = (String) archiveProperties.get(ContentModel.PROP_NAME); - if (archiveNodeName == null) - { - archiveNodeName = archiveNodeRef.getId(); - } - QName archiveAssocQName = QName.createQNameWithValidLocalName( - NamespaceService.SYSTEM_MODEL_1_0_URI, archiveNodeName); - addChild(userArchiveNodeRef, archiveNodeRef, ContentModel.ASSOC_ARCHIVED_LINK, archiveAssocQName); - } - - // Invoke behaviours - invokeOnCreateNode(archivePrimaryParentAssocRef); - - firstNode = false; - } - } - - /** - * {@inheritDoc} - * - * Archives the node without the cm:auditable 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 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"); - } - - // Remove the secondary link to the user that deleted the node - List 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 removePropertyQNames = new HashSet(11); - Set removeAspectQNames = new HashSet(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 nodeToMovePair = getNodePairNotNull(nodeToMoveRef); - Pair 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 nodesToRestoreAssociationsFor = new ArrayList(); - - // 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(); - - boolean movingStore = !oldStoreRef.equals(newStoreRef); - - if (movingStore) - { - // Recursively find primary children of the node to move - // TODO: Use NodeHierarchyWalker - List childAssocs = new LinkedList(); - Map oldChildNodeIds = new HashMap(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> movedNodePairs = new HashMap>(childAssocs.size() * 2 + 2); - QName childNodeTypeQName = nodeDAO.getNodeType(nodeToMoveId); - Set 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> moveNodeResult = nodeDAO.moveNode( - nodeToMoveId, - parentNodeId, - assocTypeQName, - assocQName); - Pair newParentAssocPair = moveNodeResult.getFirst(); - movedNodePairs.put(nodeToMoveRef, moveNodeResult.getSecond()); - ChildAssociationRef newParentAssocRef = newParentAssocPair.getSecond(); - - // Propagate timestamps - propagateTimeStamps(oldParentAssocRef); - propagateTimeStamps(newParentAssocRef); - - // The Node changes NodeRefs, so this is really the deletion of the old node and creation - // of a node in a new store as far as the clients are concerned. - invokeOnDeleteNode(oldParentAssocRef, childNodeTypeQName, childNodeAspectQNames, true); - invokeOnCreateNode(newParentAssocRef); - - // do the same to the children, preserving parents, types and qnames - for (ChildAssociationRef oldChildAssoc : childAssocs) - { - NodeRef oldChildNodeRef = oldChildAssoc.getChildRef(); - Long oldChildNodeId = oldChildNodeIds.get(oldChildNodeRef); - NodeRef oldParentNodeRef = oldChildAssoc.getParentRef(); - Pair newParentNodePair = movedNodePairs.get(oldParentNodeRef); - Long newParentNodeId = newParentNodePair.getFirst(); - - childNodeTypeQName = nodeDAO.getNodeType(oldChildNodeId); - childNodeAspectQNames = nodeDAO.getNodeAspects(oldChildNodeId); - - // Now that the new parent ref exists, invoke the before create policy - invokeBeforeCreateNode( - newParentNodePair.getSecond(), - oldChildAssoc.getTypeQName(), - oldChildAssoc.getQName(), - childNodeTypeQName); - - // Move the node as this gives back the primary parent association - try - { - moveNodeResult = nodeDAO.moveNode(oldChildNodeId, newParentNodeId, null,null); - } - catch (NodeExistsException e) - { - deleteNode(e.getNodePair().getSecond()); - moveNodeResult = nodeDAO.moveNode(oldChildNodeId, newParentNodeId, null,null); - } - // Move the node as this gives back the primary parent association - newParentAssocPair = moveNodeResult.getFirst(); - movedNodePairs.put(oldChildNodeRef, moveNodeResult.getSecond()); - ChildAssociationRef newChildAssoc = newParentAssocPair.getSecond(); - - // Propagate timestamps - propagateTimeStamps(newChildAssoc); - - // Fire node policies. This ensures that each node in the hierarchy gets a notification fired. - invokeOnDeleteNode(oldChildAssoc, childNodeTypeQName, childNodeAspectQNames, true); - invokeOnCreateNode(newChildAssoc); - - // collect working copy nodes that need to be updated; we need all nodes - // to be already moved when create association between nodes - if (hasAspect(newChildAssoc.getChildRef(), ContentModel.ASPECT_ARCHIVE_LOCKABLE)) - { - nodesToRestoreAssociationsFor.add(newChildAssoc); - } - } - - // invoke onRestoreNode for working copy nodes in order to restore original lock - for (ChildAssociationRef childAssoc : nodesToRestoreAssociationsFor) - { - invokeOnRestoreNode(childAssoc); - } - - return newParentAssocRef; - } - else - { - invokeBeforeMoveNode(oldParentAssocRef, newParentRef); - - invokeBeforeDeleteChildAssociation(oldParentAssocRef); - - // Move node under the new parent - Pair, Pair> moveNodeResult = nodeDAO.moveNode( - nodeToMoveId, - parentNodeId, - assocTypeQName, - assocQName); - Pair newParentAssocPair = moveNodeResult.getFirst(); - ChildAssociationRef newParentAssocRef = newParentAssocPair.getSecond(); - - // Propagate timestamps (watch out for moves within the same folder) - if (!oldParentAssocRef.getParentRef().equals(newParentAssocRef.getParentRef())) - { - propagateTimeStamps(oldParentAssocRef); - propagateTimeStamps(newParentAssocRef); - } - else - { - // Propagate timestamps for rename case, see ALF-10884 - propagateTimeStamps(newParentAssocRef); - } - - invokeOnCreateChildAssociation(newParentAssocRef, false); - invokeOnDeleteChildAssociation(oldParentAssocRef); - invokeOnMoveNode(oldParentAssocRef, newParentAssocRef); - - // Done - return newParentAssocRef; - } - } - - private void findNodeChildrenToMove(Long nodeId, final StoreRef storeRef, - final List childAssocsToMove, final Map nodeIds) - { - // Get the node's children, but only one's that aren't in the same store - final List childAssocs = new LinkedList(); - NodeDAO.ChildAssocRefQueryCallback callback = new NodeDAO.ChildAssocRefQueryCallback() - { - public boolean preLoadNodes() - { - return true; - } - - @Override - public boolean orderResults() - { - return false; - } - - public boolean handle( - Pair childAssocPair, - Pair parentNodePair, - Pair 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 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; - } - } - - /** - * Propagate, if necessary, a cm:modified timestamp change to the parent of the - * given association, along with the cm:modifier of who changed it. - * The parent node has to be cm:auditable 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 parentNodePair = getNodePairNotNull(parentNodeRef); - Long parentNodeId = parentNodePair.getFirst(); - - // Get the ID of the child that triggered this update - NodeRef childNodeRef = assocRef.getChildRef(); - Pair 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 cm:modified time and cm:modifier 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 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 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 cm:modified, and the updating child node from which to get the cm:modifier from - * @param modifiedDate the date to set - * @param useCurrentTxn true to use the current transaction - */ - private void process(final Map 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 cm:modifier from - * @param modifiedDate the date to set - * @param useCurrentTxn true 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 callback = new RetryingTransactionCallback() - { - @Override - public Void execute() throws Throwable - { - // Get the details of the parent, and check it's valid to update - Pair 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 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); - - Map propertiesBefore = nodeDAO.getNodeProperties(parentNodeId); - // Touch the node; it is cm:auditable - boolean changed = nodeDAO.setModifiedProperties(parentNodeId, modifiedDate, modifiedByToPropagate); - - if (changed) - { - Map propertiesAfter = nodeDAO.getNodeProperties(parentNodeId); - // Invoke policy behaviour - invokeOnUpdateNode(parentNodeRef); - invokeOnUpdateProperties(parentNodeRef, propertiesBefore, propertiesAfter); - } - - 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 ExtendedTrait getTrait(Class traitAPI) - { - return (ExtendedTrait) nodeServiceTrait; - } -} +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2023 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 . + * #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.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.I18NUtil; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.domain.node.ChildAssocEntity; +import org.alfresco.repo.domain.node.Node; +import org.alfresco.repo.domain.node.NodeDAO; +import org.alfresco.repo.domain.node.NodeDAO.ChildAssocRefQueryCallback; +import org.alfresco.repo.domain.node.NodeExistsException; +import org.alfresco.repo.domain.qname.QNameDAO; +import org.alfresco.repo.node.AbstractNodeServiceImpl; +import org.alfresco.repo.node.StoreArchiveMap; +import org.alfresco.repo.node.archive.NodeArchiveService; +import org.alfresco.repo.node.db.NodeHierarchyWalker.VisitedNode; +import org.alfresco.repo.node.db.traitextender.NodeServiceExtension; +import org.alfresco.repo.node.db.traitextender.NodeServiceTrait; +import org.alfresco.repo.policy.BehaviourFilter; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.repo.transaction.TransactionalResourceHelper; +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ChildAssociationDefinition; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.InvalidAspectException; +import org.alfresco.service.cmr.dictionary.InvalidTypeException; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.repository.AssociationExistsException; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.InvalidChildAssociationRefException; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.InvalidStoreRefException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeRef.Status; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.security.AccessPermission; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.OwnableService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.QNamePattern; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.traitextender.AJProxyTrait; +import org.alfresco.traitextender.Extend; +import org.alfresco.traitextender.ExtendedTrait; +import org.alfresco.traitextender.Extensible; +import org.alfresco.traitextender.Trait; +import org.alfresco.util.EqualsHelper; +import org.alfresco.util.GUID; +import org.alfresco.util.Pair; +import org.alfresco.util.ParameterCheck; +import org.alfresco.util.PropertyMap; +import org.alfresco.util.transaction.TransactionListenerAdapter; + +/** + * Node service using database persistence layer to fulfill functionality + * + * @author Derek Hulley + */ +public class DbNodeServiceImpl extends AbstractNodeServiceImpl implements Extensible, NodeService +{ + public static final String KEY_PENDING_DELETE_NODES = "DbNodeServiceImpl.pendingDeleteNodes"; + + private static Log logger = LogFactory.getLog(DbNodeServiceImpl.class); + + private QNameDAO qnameDAO; + private NodeDAO nodeDAO; + private PermissionService permissionService; + private StoreArchiveMap storeArchiveMap; + private BehaviourFilter policyBehaviourFilter; + private boolean enableTimestampPropagation; + private final ExtendedTrait nodeServiceTrait; + + public DbNodeServiceImpl() + { + nodeServiceTrait = new ExtendedTrait(AJProxyTrait.create(this, NodeServiceTrait.class)); + storeArchiveMap = new StoreArchiveMap(); // in case it is not set + } + + public void setQnameDAO(QNameDAO qnameDAO) + { + this.qnameDAO = qnameDAO; + } + + public void setNodeDAO(NodeDAO nodeDAO) + { + this.nodeDAO = nodeDAO; + } + + public void setPermissionService(PermissionService permissionService) + { + this.permissionService = permissionService; + } + + public void setStoreArchiveMap(StoreArchiveMap storeArchiveMap) + { + this.storeArchiveMap = storeArchiveMap; + } + + /** + * + * @param policyBehaviourFilter + * component used to enable and disable behaviours + */ + public void setPolicyBehaviourFilter(BehaviourFilter policyBehaviourFilter) + { + this.policyBehaviourFilter = policyBehaviourFilter; + } + + /** + * Set whether cm:auditable timestamps should be propagated to parent nodes where the parent-child relationship has been marked using propagateTimestamps. + * + * @param enableTimestampPropagation + * true 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 getNodePairNotNull(NodeRef nodeRef) throws InvalidNodeRefException + { + ParameterCheck.mandatory("nodeRef", nodeRef); + + Pair 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 nodePair = nodeDAO.getNodePair(nodeId); + return nodePair == null ? null : nodePair.getSecond(); + } + + /** + * {@inheritDoc} + */ + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + 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); + } + // 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 rootNodePair = nodeDAO.newStore(storeRef); + NodeRef rootNodeRef = rootNodePair.getSecond(); + + // invoke policies + invokeOnCreateStore(rootNodeRef); + + // Done + return storeRef; + } + + /** + * @throws UnsupportedOperationException + * Always + */ + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + public void deleteStore(StoreRef storeRef) throws InvalidStoreRefException + { + // Cannot delete the root node but we can delete, without archive, all immediate children + NodeRef rootNodeRef = nodeDAO.getRootNode(storeRef).getSecond(); + List 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 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 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 getChildAssocs(NodeRef nodeRef) throws InvalidNodeRefException + { + return super.getChildAssocs(nodeRef); + } + + @Override + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + public List 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 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 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 childNodePair = assoc.getChildNode().getNodePair(); + + addAspectsAndProperties( + childNodePair, + nodeTypeQName, + null, + Collections. emptySet(), + Collections. emptyMap(), + Collections. emptySet(), + properties, + true, + false); + + Map propertiesAfter = nodeDAO.getNodeProperties(childNodePair.getFirst()); + + // Propagate timestamps + propagateTimeStamps(childAssocRef); + + // Invoke policy behaviour + invokeOnCreateNode(childAssocRef); + invokeOnCreateChildAssociation(childAssocRef, true); + Map propertiesBefore = PropertyMap.EMPTY_MAP; + invokeOnUpdateProperties( + childAssocRef.getChildRef(), + propertiesBefore, + propertiesAfter); + + // Ensure that the parent node has the required aspects + addAspectsAndPropertiesAssoc(parentNodePair, assocTypeQName, null, null, null, null, false); + + // done + return childAssocRef; + } + + /** + * Adds all the aspects and properties required for the given node, along with mandatory aspects and related properties. Existing values will not be overridden. All required pre- and post-update notifications are sent for missing aspects. + * + * @param nodePair + * the node to which the details apply + * @param classQName + * the type or aspect QName for which the defaults must be applied. If this is 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; + } + + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + 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); + } + } + + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + 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) + */ + @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 nodePair = getNodePairNotNull(nodeRef); + + // Invoke policies + invokeBeforeUpdateNode(nodeRef); + QName oldType = nodeDAO.getNodeType(nodePair.getFirst()); + invokeBeforeSetType(nodeRef, oldType, typeQName); + + // Set the type + boolean updatedNode = nodeDAO.updateNode(nodePair.getFirst(), typeQName, null); + + // Add the default aspects and properties required for the given type. Existing values will not be overridden. + boolean updatedProps = addAspectsAndProperties(nodePair, typeQName, null, null, null, null, false); + + // Invoke policies + if (updatedNode || updatedProps) + { + // Invoke policies + invokeOnUpdateNode(nodeRef); + invokeOnSetType(nodeRef, oldType, typeQName); + } + } + + @Override + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + public void addAspect( + NodeRef nodeRef, + QName aspectTypeQName, + Map 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 nodePair = getNodePairNotNull(nodeRef); + // SetProperties common tasks + setPropertiesCommonWork(nodePair, aspectProperties); + boolean modified = addAspectsAndProperties( + nodePair, + aspectTypeQName, + null, + null, + Collections.singleton(aspectTypeQName), + aspectProperties, + false); + + if (modified) + { + // Invoke policy behaviours + invokeOnUpdateNode(nodeRef); + } + } + + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + public int countChildAssocs(NodeRef nodeRef, boolean isPrimary) throws InvalidNodeRefException + { + final Pair 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 nodePair = getNodePairNotNull(nodeRef); + final Long nodeId = nodePair.getFirst(); + + if (!nodeDAO.hasNodeAspect(nodeId, aspectTypeQName)) + { + return; + } + // Invoke policy behaviours + invokeBeforeUpdateNode(nodeRef); + invokeBeforeRemoveAspect(nodeRef, aspectTypeQName); + nodeDAO.removeNodeAspects(nodeId, Collections.singleton(aspectTypeQName)); + + AspectDefinition aspectDef = dictionaryService.getAspect(aspectTypeQName); + boolean updated = false; + if (aspectDef != null) + { + // Remove default properties + Map propsBefore = nodeDAO.getNodeProperties(nodeId); + Map propertyDefs = aspectDef.getProperties(); + Set propertyToRemoveQNames = propertyDefs.keySet(); + boolean propertiesRemoved = nodeDAO.removeNodeProperties(nodeId, propertyToRemoveQNames); + + if (propertiesRemoved) + { + invokeOnUpdateProperties( + nodeRef, + propsBefore, // before + nodeDAO.getNodeProperties(nodeId)); // after + } + + // 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 preLoadNodes() + { + return true; + } + + @Override + public boolean orderResults() + { + return false; + } + + public boolean handle(Pair childAssocPair, Pair parentNodePair, + Pair 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 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); + } + + // Gather peer associations to delete + Map nodeAssocDefs = aspectDef.getAssociations(); + List nodeAssocIdsToRemove = new ArrayList(13); + List assocRefsRemoved = new ArrayList(13); + for (Map.Entry 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> targetAssocRefs = nodeDAO.getTargetNodeAssocs(nodeId, assocTypeQName); + for (Pair 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); + } + + invokeOnRemoveAspect(nodeRef, aspectTypeQName); + + } + + /** + * Performs a check on the set of node aspects + */ + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + public boolean hasAspect(NodeRef nodeRef, QName aspectQName) throws InvalidNodeRefException, InvalidAspectException + { + if (aspectQName.equals(ContentModel.ASPECT_PENDING_DELETE)) + { + return isPendingDelete(nodeRef); + } + Pair nodePair = getNodePairNotNull(nodeRef); + return nodeDAO.hasNodeAspect(nodePair.getFirst(), aspectQName); + } + + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + public Set getAspects(NodeRef nodeRef) throws InvalidNodeRefException + { + Pair nodePair = getNodePairNotNull(nodeRef); + Set aspectQNames = nodeDAO.getNodeAspects(nodePair.getFirst()); + if (isPendingDelete(nodeRef)) + { + aspectQNames.add(ContentModel.ASPECT_PENDING_DELETE); + } + return aspectQNames; + } + + /** + * @return Returns true 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 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 + * true if normal archival may occur or false 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 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 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 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 nodesPendingDelete = new HashSet(walker.getNodes(false).size()); + for (VisitedNode visitedNode : walker.getNodes(true)) + { + nodesPendingDelete.add(visitedNode.nodeRef); + } + Set 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 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 childAssocIds = new HashSet(23); // Prevents duplicate firing + Set peerAssocIds = new HashSet(23); // Prevents duplicate firing + List nodesToDelete = walker.getNodes(true); + for (VisitedNode nodeToDelete : nodesToDelete) + { + // Target associations + for (Pair targetAssocPair : nodeToDelete.targetAssocs) + { + if (!peerAssocIds.add(targetAssocPair.getFirst())) + { + continue; // Already fired + } + invokeBeforeDeleteAssociation(targetAssocPair.getSecond()); + } + // Source associations + for (Pair sourceAssocPair : nodeToDelete.sourceAssocs) + { + if (!peerAssocIds.add(sourceAssocPair.getFirst())) + { + continue; // Already fired + } + invokeBeforeDeleteAssociation(sourceAssocPair.getSecond()); + } + // Secondary child associations + for (Pair secondaryChildAssocPair : nodeToDelete.secondaryChildAssocs) + { + if (!childAssocIds.add(secondaryChildAssocPair.getFirst())) + { + continue; // Already fired + } + invokeBeforeDeleteChildAssociation(secondaryChildAssocPair.getSecond()); + } + // Secondary parent associations + for (Pair 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 targetAssocPair : nodeToDelete.targetAssocs) + { + if (!peerAssocIds.add(targetAssocPair.getFirst())) + { + continue; // Already fired + } + nodeDAO.removeNodeAssocs(Collections.singletonList(targetAssocPair.getFirst())); + invokeOnDeleteAssociation(targetAssocPair.getSecond()); + } + // Source associations + for (Pair 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 secondaryChildAssocPair : nodeToDelete.secondaryChildAssocs) + { + if (!childAssocIds.add(secondaryChildAssocPair.getFirst())) + { + continue; // Already fired + } + nodeDAO.deleteChildAssoc(secondaryChildAssocPair.getFirst()); + invokeOnDeleteChildAssociation(secondaryChildAssocPair.getSecond()); + } + // Secondary parent associations + for (Pair secondaryParentAssocPair : nodeToDelete.secondaryParentAssocs) + { + if (!childAssocIds.add(secondaryParentAssocPair.getFirst())) + { + continue; // Already fired + } + nodeDAO.deleteChildAssoc(secondaryParentAssocPair.getFirst()); + invokeOnDeleteChildAssociation(secondaryParentAssocPair.getSecond()); + } + QName childNodeTypeQName = nodeDAO.getNodeType(nodeToDelete.id); + Set childAspectQnames = nodeDAO.getNodeAspects(nodeToDelete.id); + // Delete the node + nodeDAO.deleteChildAssoc(nodeToDelete.primaryParentAssocPair.getFirst()); + nodeDAO.deleteNode(nodeToDelete.id); + invokeOnDeleteNode( + nodeToDelete.primaryParentAssocPair.getSecond(), + childNodeTypeQName, childAspectQnames, archive); + } + + // Clear out the list of nodes pending delete + nodesPendingDeleteTxn = TransactionalResourceHelper.getSet(KEY_PENDING_DELETE_NODES); + nodesPendingDeleteTxn.removeAll(nodesPendingDelete); + } + + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + public ChildAssociationRef addChild(NodeRef parentRef, NodeRef childRef, QName assocTypeQName, QName assocQName) + { + return addChild(Collections.singletonList(parentRef), childRef, assocTypeQName, assocQName).get(0); + } + + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + public List addChild(Collection 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 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) + { + // The node(s) involved may not be pending deletion + checkPendingDelete(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 + 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 parentNodePair : parentNodePairs) + { + addAspectsAndPropertiesAssoc(parentNodePair, assocTypeQName, null, null, null, null, false); + } + + return childAssociationRefs; + } + + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + public void removeChild(NodeRef parentRef, NodeRef childRef) throws InvalidNodeRefException + { + // The node(s) involved may not be pending deletion + checkPendingDelete(parentRef); + checkPendingDelete(childRef); + + final Pair 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 preLoadNodes() + { + return true; + } + + @Override + public boolean orderResults() + { + return false; + } + + 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 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); + } + + // 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 assocPair = nodeDAO.getChildAssoc( + parentNodeId, childNodeId, assocTypeQName, assocQName); + if (assocPair == null) + { + // No association exists + return false; + } + Long assocId = assocPair.getFirst(); + ChildAssociationRef assocRef = assocPair.getSecond(); + if (assocRef.isPrimary()) + { + NodeRef childNodeRef = assocRef.getChildRef(); + // Delete the child node + this.deleteNode(childNodeRef); + // Done + return true; + } + else + { + // Delete the association + invokeBeforeDeleteChildAssociation(childAssocRef); + nodeDAO.deleteChildAssoc(assocId); + invokeOnDeleteChildAssociation(childAssocRef); + // Done + return true; + } + } + + @Override + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + public boolean removeSecondaryChildAssociation(ChildAssociationRef childAssocRef) + { + // The node(s) involved may not be pending deletion + checkPendingDelete(childAssocRef.getParentRef()); + checkPendingDelete(childAssocRef.getChildRef()); + + Long parentNodeId = getNodePairNotNull(childAssocRef.getParentRef()).getFirst(); + Long childNodeId = getNodePairNotNull(childAssocRef.getChildRef()).getFirst(); + QName assocTypeQName = childAssocRef.getTypeQName(); + QName assocQName = childAssocRef.getQName(); + Pair assocPair = nodeDAO.getChildAssoc( + parentNodeId, childNodeId, assocTypeQName, assocQName); + if (assocPair == null) + { + // No association exists + return false; + } + Long assocId = assocPair.getFirst(); + ChildAssociationRef assocRef = assocPair.getSecond(); + if (assocRef.isPrimary()) + { + throw new IllegalArgumentException( + "removeSeconaryChildAssociation can not be applied to a primary association: \n" + + " Child Assoc: " + assocRef); + } + // Delete the secondary association + invokeBeforeDeleteChildAssociation(childAssocRef); + nodeDAO.deleteChildAssoc(assocId); + invokeOnDeleteChildAssociation(childAssocRef); + // Done + return true; + } + + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + public Serializable getProperty(NodeRef nodeRef, QName qname) throws InvalidNodeRefException + { + Long nodeId = getNodePairNotNull(nodeRef).getFirst(); + // Spoof referencable properties + if (qname.equals(ContentModel.PROP_STORE_PROTOCOL)) + { + return nodeRef.getStoreRef().getProtocol(); + } + else if (qname.equals(ContentModel.PROP_STORE_IDENTIFIER)) + { + return nodeRef.getStoreRef().getIdentifier(); + } + else if (qname.equals(ContentModel.PROP_NODE_UUID)) + { + return nodeRef.getId(); + } + else if (qname.equals(ContentModel.PROP_NODE_DBID)) + { + return nodeId; + } + + Serializable property = nodeDAO.getNodeProperty(nodeId, qname); + + // check if we need to provide a spoofed name + if (property == null && qname.equals(ContentModel.PROP_NAME)) + { + return nodeRef.getId(); + } + + // done + return property; + } + + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + public Map 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; + } + + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + 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. + * + */ + @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 nodePair = getNodePairNotNull(nodeRef); + + // Invoke policy behaviour + invokeBeforeUpdateNode(nodeRef); + + // cm:name special handling + setPropertiesCommonWork( + nodePair, + Collections.singletonMap(qname, value)); + + // Add the property and all required defaults + boolean changed = addAspectsAndProperties( + nodePair, null, + null, null, + null, Collections.singletonMap(qname, value), false); + + if (changed) + { + // Invoke policy behaviour + invokeOnUpdateNode(nodeRef); + } + } + + /** + * Ensures that all required properties are present on the node and copies the property values to the 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. + * + */ + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + 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); + } + } + + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + 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); + } + } + + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + 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); + } + + 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. + */ + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + 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; + } + + @Override + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + public List 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 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 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 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; + } + + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + 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; + } + + @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 nodePair = getNodePairNotNull(nodeRef); + Long nodeId = nodePair.getFirst(); + + Pair 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 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; + } + + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + 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 + @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 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 + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + 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); + } + } + + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + 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; + } + + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + public List findAssocsNotLinkedByTwoOtherAssocs(NodeRef parent) + { + // Get the parent node + Pair nodePair = getNodePairNotNull(parent); + Long parentNodeId = nodePair.getFirst(); + + return nodeDAO.selectAssocsNotLinkedByTwoOtherAssocs(parentNodeId); + } + + /** + * 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 + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + 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; + } + + @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 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 + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + public AssociationRef getAssoc(Long id) + { + Pair nodeAssocPair = nodeDAO.getNodeAssocOrNull(id); + return nodeAssocPair == null ? null : nodeAssocPair.getSecond(); + } + + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + 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; + } + + @Override + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + public List getTargetAssocsByPropertyValue(NodeRef sourceRef, QNamePattern qnamePattern, QName propertyQName, Serializable propertyValue) + { + Pair 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> assocPairs = nodeDAO.getTargetAssocsByPropertyValue(sourceNodeId, qnameFilter, propertyQName, propertyValue); + 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; + } + + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + 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) + */ + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + 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. + */ + @Extend(traitAPI = NodeServiceTrait.class, extensionAPI = NodeServiceExtension.class) + 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> archiveRecord = new HashMap>(walker.getNodes(false).size() * 2); + for (VisitedNode node : walker.getNodes(false)) + { + // Get node metadata + Map archiveProperties = nodeDAO.getNodeProperties(node.id); + Set archiveAspects = nodeDAO.getNodeAspects(node.id); + + // The first node gets special treatment as it contains the archival details + ChildAssociationRef archivePrimaryParentAssocRef = null; + final Pair 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 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 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 userArchiveNodeProps = Collections.singletonMap( + ContentModel.PROP_NAME, (Serializable) username); + userArchiveNodeRef = createNode( + archiveStoreRootNodeRef, + ContentModel.ASSOC_ARCHIVE_USER_LINK, + archiveUserAssocQName, + ContentModel.TYPE_ARCHIVE_USER, + userArchiveNodeProps).getChildRef(); + } + else + { + userArchiveNodeRef = userArchiveAssocPair.getSecond().getChildRef(); + } + // Link user node to archived item via secondary child association + String archiveNodeName = (String) archiveProperties.get(ContentModel.PROP_NAME); + if (archiveNodeName == null) + { + archiveNodeName = archiveNodeRef.getId(); + } + QName archiveAssocQName = QName.createQNameWithValidLocalName( + NamespaceService.SYSTEM_MODEL_1_0_URI, archiveNodeName); + addChild(userArchiveNodeRef, archiveNodeRef, ContentModel.ASSOC_ARCHIVED_LINK, archiveAssocQName); + } + + // Invoke behaviours + invokeOnCreateNode(archivePrimaryParentAssocRef); + + firstNode = false; + } + } + + /** + * {@inheritDoc} + * + * Archives the node without the cm:auditable 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 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"); + } + + // Remove the secondary link to the user that deleted the node + List 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 removePropertyQNames = new HashSet(11); + Set removeAspectQNames = new HashSet(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 nodeToMovePair = getNodePairNotNull(nodeToMoveRef); + Pair 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 nodesToRestoreAssociationsFor = new ArrayList(); + + // 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(); + + boolean movingStore = !oldStoreRef.equals(newStoreRef); + + if (movingStore) + { + // Recursively find primary children of the node to move + // TODO: Use NodeHierarchyWalker + List childAssocs = new LinkedList(); + Map oldChildNodeIds = new HashMap(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> movedNodePairs = new HashMap>(childAssocs.size() * 2 + 2); + QName childNodeTypeQName = nodeDAO.getNodeType(nodeToMoveId); + Set 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> moveNodeResult = nodeDAO.moveNode( + nodeToMoveId, + parentNodeId, + assocTypeQName, + assocQName); + Pair newParentAssocPair = moveNodeResult.getFirst(); + movedNodePairs.put(nodeToMoveRef, moveNodeResult.getSecond()); + ChildAssociationRef newParentAssocRef = newParentAssocPair.getSecond(); + + // Propagate timestamps + propagateTimeStamps(oldParentAssocRef); + propagateTimeStamps(newParentAssocRef); + + // The Node changes NodeRefs, so this is really the deletion of the old node and creation + // of a node in a new store as far as the clients are concerned. + invokeOnDeleteNode(oldParentAssocRef, childNodeTypeQName, childNodeAspectQNames, true); + invokeOnCreateNode(newParentAssocRef); + + // do the same to the children, preserving parents, types and qnames + for (ChildAssociationRef oldChildAssoc : childAssocs) + { + NodeRef oldChildNodeRef = oldChildAssoc.getChildRef(); + Long oldChildNodeId = oldChildNodeIds.get(oldChildNodeRef); + NodeRef oldParentNodeRef = oldChildAssoc.getParentRef(); + Pair newParentNodePair = movedNodePairs.get(oldParentNodeRef); + Long newParentNodeId = newParentNodePair.getFirst(); + + childNodeTypeQName = nodeDAO.getNodeType(oldChildNodeId); + childNodeAspectQNames = nodeDAO.getNodeAspects(oldChildNodeId); + + // Now that the new parent ref exists, invoke the before create policy + invokeBeforeCreateNode( + newParentNodePair.getSecond(), + oldChildAssoc.getTypeQName(), + oldChildAssoc.getQName(), + childNodeTypeQName); + + // Move the node as this gives back the primary parent association + try + { + moveNodeResult = nodeDAO.moveNode(oldChildNodeId, newParentNodeId, null, null); + } + catch (NodeExistsException e) + { + deleteNode(e.getNodePair().getSecond()); + moveNodeResult = nodeDAO.moveNode(oldChildNodeId, newParentNodeId, null, null); + } + // Move the node as this gives back the primary parent association + newParentAssocPair = moveNodeResult.getFirst(); + movedNodePairs.put(oldChildNodeRef, moveNodeResult.getSecond()); + ChildAssociationRef newChildAssoc = newParentAssocPair.getSecond(); + + // Propagate timestamps + propagateTimeStamps(newChildAssoc); + + // Fire node policies. This ensures that each node in the hierarchy gets a notification fired. + invokeOnDeleteNode(oldChildAssoc, childNodeTypeQName, childNodeAspectQNames, true); + invokeOnCreateNode(newChildAssoc); + + // collect working copy nodes that need to be updated; we need all nodes + // to be already moved when create association between nodes + if (hasAspect(newChildAssoc.getChildRef(), ContentModel.ASPECT_ARCHIVE_LOCKABLE)) + { + nodesToRestoreAssociationsFor.add(newChildAssoc); + } + } + + // invoke onRestoreNode for working copy nodes in order to restore original lock + for (ChildAssociationRef childAssoc : nodesToRestoreAssociationsFor) + { + invokeOnRestoreNode(childAssoc); + } + + return newParentAssocRef; + } + else + { + invokeBeforeMoveNode(oldParentAssocRef, newParentRef); + + invokeBeforeDeleteChildAssociation(oldParentAssocRef); + + // Move node under the new parent + Pair, Pair> moveNodeResult = nodeDAO.moveNode( + nodeToMoveId, + parentNodeId, + assocTypeQName, + assocQName); + Pair newParentAssocPair = moveNodeResult.getFirst(); + ChildAssociationRef newParentAssocRef = newParentAssocPair.getSecond(); + + // Propagate timestamps (watch out for moves within the same folder) + if (!oldParentAssocRef.getParentRef().equals(newParentAssocRef.getParentRef())) + { + propagateTimeStamps(oldParentAssocRef); + propagateTimeStamps(newParentAssocRef); + } + else + { + // Propagate timestamps for rename case, see ALF-10884 + propagateTimeStamps(newParentAssocRef); + } + + invokeOnCreateChildAssociation(newParentAssocRef, false); + invokeOnDeleteChildAssociation(oldParentAssocRef); + invokeOnMoveNode(oldParentAssocRef, newParentAssocRef); + + // Done + return newParentAssocRef; + } + } + + private void findNodeChildrenToMove(Long nodeId, final StoreRef storeRef, + final List childAssocsToMove, final Map nodeIds) + { + // Get the node's children, but only one's that aren't in the same store + final List childAssocs = new LinkedList(); + NodeDAO.ChildAssocRefQueryCallback callback = new NodeDAO.ChildAssocRefQueryCallback() { + public boolean preLoadNodes() + { + return true; + } + + @Override + public boolean orderResults() + { + return false; + } + + public boolean handle( + Pair childAssocPair, + Pair parentNodePair, + Pair 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 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; + } + } + + /** + * Propagate, if necessary, a cm:modified timestamp change to the parent of the given association, along with the cm:modifier of who changed it. The parent node has to be cm:auditable 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 parentNodePair = getNodePairNotNull(parentNodeRef); + Long parentNodeId = parentNodePair.getFirst(); + + // Get the ID of the child that triggered this update + NodeRef childNodeRef = assocRef.getChildRef(); + Pair 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 cm:modified time and cm:modifier 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 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 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 cm:modified, and the updating child node from which to get the cm:modifier from + * @param modifiedDate + * the date to set + * @param useCurrentTxn + * true to use the current transaction + */ + private void process(final Map 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 cm:modifier from + * @param modifiedDate + * the date to set + * @param useCurrentTxn + * true 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 callback = new RetryingTransactionCallback() { + @Override + public Void execute() throws Throwable + { + // Get the details of the parent, and check it's valid to update + Pair 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 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); + + Map propertiesBefore = nodeDAO.getNodeProperties(parentNodeId); + // Touch the node; it is cm:auditable + boolean changed = nodeDAO.setModifiedProperties(parentNodeId, modifiedDate, modifiedByToPropagate); + + if (changed) + { + Map propertiesAfter = nodeDAO.getNodeProperties(parentNodeId); + // Invoke policy behaviour + invokeOnUpdateNode(parentNodeRef); + invokeOnUpdateProperties(parentNodeRef, propertiesBefore, propertiesAfter); + } + + 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 ExtendedTrait getTrait(Class traitAPI) + { + return (ExtendedTrait) nodeServiceTrait; + } +} diff --git a/repository/src/test/java/org/alfresco/repo/event2/UpdateRepoEventIT.java b/repository/src/test/java/org/alfresco/repo/event2/UpdateRepoEventIT.java index c6efe5ef5e..ddb5eceb08 100644 --- a/repository/src/test/java/org/alfresco/repo/event2/UpdateRepoEventIT.java +++ b/repository/src/test/java/org/alfresco/repo/event2/UpdateRepoEventIT.java @@ -26,6 +26,12 @@ package org.alfresco.repo.event2; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + import static org.alfresco.model.ContentModel.PROP_DESCRIPTION; import java.io.Serializable; @@ -35,6 +41,9 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; + +import org.junit.Test; + import org.alfresco.model.ContentModel; import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.dictionary.M2Model; @@ -53,7 +62,6 @@ import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.namespace.QName; import org.alfresco.util.GUID; import org.alfresco.util.Pair; -import org.junit.Test; /** * @author Iulian Aftene @@ -66,20 +74,20 @@ public class UpdateRepoEventIT extends AbstractContextAwareRepoEvent public void testUpdateNodeResourceContent() { ContentService contentService = (ContentService) applicationContext.getBean( - "contentService"); + "contentService"); final NodeRef nodeRef = createNode(ContentModel.TYPE_CONTENT); RepoEvent> resultRepoEvent = getRepoEvent(1); assertEquals("Wrong repo event type.", EventType.NODE_CREATED.getType(), - resultRepoEvent.getType()); + resultRepoEvent.getType()); NodeResource resource = getNodeResource(resultRepoEvent); assertNull("Content should have been null.", resource.getContent()); retryingTransactionHelper.doInTransaction(() -> { ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.TYPE_CONTENT, - true); + true); writer.setMimetype(MimetypeMap.MIMETYPE_PDF); writer.setEncoding("UTF-8"); writer.putContent("test content."); @@ -90,7 +98,7 @@ public class UpdateRepoEventIT extends AbstractContextAwareRepoEvent resultRepoEvent = getRepoEvent(2); assertEquals("Wrong repo event type.", EventType.NODE_UPDATED.getType(), - resultRepoEvent.getType()); + resultRepoEvent.getType()); resource = getNodeResource(resultRepoEvent); ContentInfo content = resource.getContent(); @@ -105,7 +113,7 @@ public class UpdateRepoEventIT extends AbstractContextAwareRepoEvent // Update the content again retryingTransactionHelper.doInTransaction(() -> { ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.TYPE_CONTENT, - true); + true); writer.setMimetype(MimetypeMap.MIMETYPE_PDF); writer.setEncoding("UTF-8"); writer.putContent("A quick brown fox jumps over the lazy dog."); @@ -370,7 +378,6 @@ public class UpdateRepoEventIT extends AbstractContextAwareRepoEvent assertEquals("new test title", title); assertEquals("new test title", getLocalizedProperty(resource, "cm:title", defaultLocale)); - resourceBefore = getNodeResourceBefore(3); title = getProperty(resourceBefore, "cm:title"); assertEquals("Wrong old property.", "test title", title); @@ -490,14 +497,14 @@ public class UpdateRepoEventIT extends AbstractContextAwareRepoEvent resource = getNodeResource(2); assertNotNull(resource.getAspectNames()); assertTrue(resource.getAspectNames().contains("cm:versionable")); - //Check all aspects + // Check all aspects Set expectedAspects = new HashSet<>(originalAspects); expectedAspects.add("cm:versionable"); assertEquals(expectedAspects, resource.getAspectNames()); // Check properties assertFalse(resource.getProperties().isEmpty()); - //Check resourceBefore + // Check resourceBefore NodeResource resourceBefore = getNodeResourceBefore(2); assertNotNull(resourceBefore.getAspectNames()); assertEquals(originalAspects, resourceBefore.getAspectNames()); @@ -544,21 +551,64 @@ public class UpdateRepoEventIT extends AbstractContextAwareRepoEvent assertEquals(aspectsBeforeRemove, resourceBefore.getAspectNames()); } + @Test + public void testRemoveAspectPropertiesTest() + { + final NodeRef nodeRef = createNode(ContentModel.TYPE_CONTENT); + NodeResource resource = getNodeResource(1); + final Set originalAspects = resource.getAspectNames(); + assertNotNull(originalAspects); + + // Add cm:geographic aspect with properties + retryingTransactionHelper.doInTransaction(() -> { + Map aspectProperties = new HashMap<>(); + aspectProperties.put(ContentModel.PROP_LATITUDE, "12.345678"); + aspectProperties.put(ContentModel.PROP_LONGITUDE, "12.345678"); + nodeService.addAspect(nodeRef, ContentModel.ASPECT_GEOGRAPHIC, aspectProperties); + return null; + }); + resource = getNodeResource(2); + Set aspectsBeforeRemove = resource.getAspectNames(); + assertNotNull(aspectsBeforeRemove); + assertTrue(aspectsBeforeRemove.contains("cm:geographic")); + + // Remove cm:geographic aspect - this automatically removes the properties from the node + retryingTransactionHelper.doInTransaction(() -> { + nodeService.removeAspect(nodeRef, ContentModel.ASPECT_GEOGRAPHIC); + return null; + }); + + resource = getNodeResource(3); + assertEquals(originalAspects, resource.getAspectNames()); + + NodeResource resourceBefore = getNodeResourceBefore(3); + assertNotNull(resourceBefore.getAspectNames()); + assertEquals(aspectsBeforeRemove, resourceBefore.getAspectNames()); + // Resource before should contain cm:latitude and cm:longitude properties + assertNotNull(resourceBefore.getProperties()); + assertTrue(resourceBefore.getProperties().containsKey("cm:latitude")); + assertTrue(resourceBefore.getProperties().containsKey("cm:longitude")); + // Resource after should NOT contain cm:latitude and cm:longitude properties + assertNotNull(resource.getProperties()); + assertFalse(resource.getProperties().containsKey("cm:latitude")); + assertFalse(resource.getProperties().containsKey("cm:longitude")); + } + @Test public void testCreateAndUpdateInTheSameTransaction() { retryingTransactionHelper.doInTransaction(() -> { NodeRef node1 = nodeService.createNode( - rootNodeRef, - ContentModel.ASSOC_CHILDREN, - QName.createQName(TEST_NAMESPACE, GUID.generate()), - ContentModel.TYPE_CONTENT).getChildRef(); + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE, GUID.generate()), + ContentModel.TYPE_CONTENT).getChildRef(); nodeService.setProperty(node1, PROP_DESCRIPTION, "test description"); return null; }); - //Create and update node are done in the same transaction so one event is expected + // Create and update node are done in the same transaction so one event is expected // to be generated checkNumOfEvents(1); } @@ -593,8 +643,8 @@ public class UpdateRepoEventIT extends AbstractContextAwareRepoEvent assertEquals("Incorrect node type was found", "cm:folder", nodeResource.getNodeType()); NodeResource resourceBefore = getNodeResourceBefore(2); - assertEquals("Incorrect node type was found","cm:content", resourceBefore.getNodeType()); - // assertNotNull(resourceBefore.getModifiedAt()); uncomment this when the issue will be fixed + assertEquals("Incorrect node type was found", "cm:content", resourceBefore.getNodeType()); + // assertNotNull(resourceBefore.getModifiedAt()); uncomment this when the issue will be fixed assertNull(resourceBefore.getId()); assertNull(resourceBefore.getContent()); assertNull(resourceBefore.isFile()); @@ -624,8 +674,7 @@ public class UpdateRepoEventIT extends AbstractContextAwareRepoEvent m2Type.setTitle("Test type title"); // Create active model - CustomModelDefinition modelDefinition = - retryingTransactionHelper.doInTransaction(() -> customModelService.createCustomModel(model, true)); + CustomModelDefinition modelDefinition = retryingTransactionHelper.doInTransaction(() -> customModelService.createCustomModel(model, true)); assertNotNull(modelDefinition); assertEquals(modelName, modelDefinition.getName().getLocalName()); @@ -655,7 +704,7 @@ public class UpdateRepoEventIT extends AbstractContextAwareRepoEvent assertEquals("Wrong repo event type.", EventType.NODE_CREATED.getType(), resultRepoEvent.getType()); assertEquals("cm:content node type was not found", "cm:content", nodeResource.getNodeType()); - QName typeQName = QName.createQName("{" + namespacePair.getFirst()+ "}" + typeName); + QName typeQName = QName.createQName("{" + namespacePair.getFirst() + "}" + typeName); retryingTransactionHelper.doInTransaction(() -> { nodeService.setType(nodeRef, typeQName); @@ -757,7 +806,7 @@ public class UpdateRepoEventIT extends AbstractContextAwareRepoEvent // we should have only 1 event, node.Created checkNumOfEvents(1); - RepoEvent> resultRepoEvent = getRepoEvent(1); + RepoEvent> resultRepoEvent = getRepoEvent(1); assertEquals("Wrong repo event type.", EventType.NODE_CREATED.getType(), resultRepoEvent.getType()); NodeResource nodeResource = getNodeResource(resultRepoEvent); assertEquals("Incorrect node type was found", "cm:folder", nodeResource.getNodeType()); @@ -783,10 +832,10 @@ public class UpdateRepoEventIT extends AbstractContextAwareRepoEvent retryingTransactionHelper.doInTransaction(() -> { nodeService.moveNode( - moveFile, - folder2, - ContentModel.ASSOC_CONTAINS, - QName.createQName(TEST_NAMESPACE)); + moveFile, + folder2, + ContentModel.ASSOC_CONTAINS, + QName.createQName(TEST_NAMESPACE)); return null; }); @@ -801,7 +850,7 @@ public class UpdateRepoEventIT extends AbstractContextAwareRepoEvent assertEquals("Wrong node parent.", folder1ID, moveFileParentBeforeMove); assertEquals("Wrong node parent.", folder2ID, moveFileParentAfterMove); assertEquals("Wrong repo event type.", EventType.NODE_UPDATED.getType(), - getRepoEvent(4).getType()); + getRepoEvent(4).getType()); assertNull(resourceBefore.getId()); assertNull(resourceBefore.getName()); @@ -833,10 +882,10 @@ public class UpdateRepoEventIT extends AbstractContextAwareRepoEvent retryingTransactionHelper.doInTransaction(() -> { nodeService.moveNode( - moveFolder, - grandParent, - ContentModel.ASSOC_CONTAINS, - QName.createQName(TEST_NAMESPACE)); + moveFolder, + grandParent, + ContentModel.ASSOC_CONTAINS, + QName.createQName(TEST_NAMESPACE)); return null; }); @@ -845,15 +894,13 @@ public class UpdateRepoEventIT extends AbstractContextAwareRepoEvent final String grandParentID = getNodeResource(1).getId(); final String parentID = getNodeResource(2).getId(); - final String moveFolderParentBeforeMove = - getNodeResourceBefore(4).getPrimaryHierarchy().get(0); - final String moveFolderParentAfterMove = - getNodeResource(4).getPrimaryHierarchy().get(0); + final String moveFolderParentBeforeMove = getNodeResourceBefore(4).getPrimaryHierarchy().get(0); + final String moveFolderParentAfterMove = getNodeResource(4).getPrimaryHierarchy().get(0); assertEquals("Wrong node parent.", parentID, moveFolderParentBeforeMove); assertEquals("Wrong node parent.", grandParentID, moveFolderParentAfterMove); assertEquals("Wrong repo event type.", EventType.NODE_UPDATED.getType(), - getRepoEventWithoutWait(4).getType()); + getRepoEventWithoutWait(4).getType()); } @Test @@ -867,28 +914,25 @@ public class UpdateRepoEventIT extends AbstractContextAwareRepoEvent retryingTransactionHelper.doInTransaction(() -> { nodeService.moveNode( - grandParent, - root2, - ContentModel.ASSOC_CONTAINS, - QName.createQName(TEST_NAMESPACE)); + grandParent, + root2, + ContentModel.ASSOC_CONTAINS, + QName.createQName(TEST_NAMESPACE)); return null; }); checkNumOfEvents(6); final String root2ID = getNodeResource(2).getId(); - final String grandParentParentAfterMove = - getNodeResource(6).getPrimaryHierarchy().get(0); + final String grandParentParentAfterMove = getNodeResource(6).getPrimaryHierarchy().get(0); assertEquals("Wrong node parent.", root2ID, grandParentParentAfterMove); final String grandParentID = getNodeResource(3).getId(); - final String parentIDOfTheParentFolder = - getNodeResource(4).getPrimaryHierarchy().get(0); + final String parentIDOfTheParentFolder = getNodeResource(4).getPrimaryHierarchy().get(0); assertEquals("Wrong node parent.", grandParentID, parentIDOfTheParentFolder); final String parentID = getNodeResource(4).getId(); - final String contentParentID = - getNodeResource(5).getPrimaryHierarchy().get(0); + final String contentParentID = getNodeResource(5).getPrimaryHierarchy().get(0); assertEquals("Wrong node parent.", parentID, contentParentID); } @@ -906,10 +950,10 @@ public class UpdateRepoEventIT extends AbstractContextAwareRepoEvent retryingTransactionHelper.doInTransaction(() -> { nodeService.moveNode( - moveFile, - folder2, - ContentModel.ASSOC_CONTAINS, - QName.createQName(TEST_NAMESPACE)); + moveFile, + folder2, + ContentModel.ASSOC_CONTAINS, + QName.createQName(TEST_NAMESPACE)); return null; }); @@ -918,8 +962,7 @@ public class UpdateRepoEventIT extends AbstractContextAwareRepoEvent assertTrue("Wrong aspect.", resource.getAspectNames().contains("cm:versionable")); final String folder2ID = getNodeResource(2).getId(); - final String moveFileParentAfterMove = - getNodeResource(5).getPrimaryHierarchy().get(0); + final String moveFileParentAfterMove = getNodeResource(5).getPrimaryHierarchy().get(0); assertEquals("Wrong node parent.", folder2ID, moveFileParentAfterMove); } @@ -935,10 +978,10 @@ public class UpdateRepoEventIT extends AbstractContextAwareRepoEvent nodeService.setProperty(moveFile, ContentModel.PROP_NAME, "test_new_name"); nodeService.moveNode( - moveFile, - folder2, - ContentModel.ASSOC_CONTAINS, - QName.createQName(TEST_NAMESPACE)); + moveFile, + folder2, + ContentModel.ASSOC_CONTAINS, + QName.createQName(TEST_NAMESPACE)); return null; }); @@ -946,8 +989,7 @@ public class UpdateRepoEventIT extends AbstractContextAwareRepoEvent assertEquals("test_new_name", resource.getName()); final String folder2ID = getNodeResource(2).getId(); - final String moveFileParentAfterMove = - getNodeResource(4).getPrimaryHierarchy().get(0); + final String moveFileParentAfterMove = getNodeResource(4).getPrimaryHierarchy().get(0); assertEquals("Wrong node parent.", folder2ID, moveFileParentAfterMove); } @@ -958,28 +1000,28 @@ public class UpdateRepoEventIT extends AbstractContextAwareRepoEvent retryingTransactionHelper.doInTransaction(() -> { NodeRef folder1 = nodeService.createNode( - rootNodeRef, - ContentModel.ASSOC_CHILDREN, - QName.createQName(TEST_NAMESPACE), - ContentModel.TYPE_FOLDER).getChildRef(); + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE), + ContentModel.TYPE_FOLDER).getChildRef(); NodeRef folder2 = nodeService.createNode( - rootNodeRef, - ContentModel.ASSOC_CHILDREN, - QName.createQName(TEST_NAMESPACE), - ContentModel.TYPE_FOLDER).getChildRef(); + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(TEST_NAMESPACE), + ContentModel.TYPE_FOLDER).getChildRef(); NodeRef fileToMove = nodeService.createNode( - folder1, - ContentModel.ASSOC_CONTAINS, - QName.createQName(TEST_NAMESPACE), - ContentModel.TYPE_CONTENT).getChildRef(); + folder1, + ContentModel.ASSOC_CONTAINS, + QName.createQName(TEST_NAMESPACE), + ContentModel.TYPE_CONTENT).getChildRef(); nodeService.moveNode( - fileToMove, - folder2, - ContentModel.ASSOC_CONTAINS, - QName.createQName(TEST_NAMESPACE)); + fileToMove, + folder2, + ContentModel.ASSOC_CONTAINS, + QName.createQName(TEST_NAMESPACE)); assertEquals(folder2, nodeService.getPrimaryParent(fileToMove).getParentRef()); @@ -989,8 +1031,7 @@ public class UpdateRepoEventIT extends AbstractContextAwareRepoEvent checkNumOfEvents(3); final String folder2ID = getNodeResource(2).getId(); - final String moveFileParentAfterMove = - getNodeResource(3).getPrimaryHierarchy().get(0); + final String moveFileParentAfterMove = getNodeResource(3).getPrimaryHierarchy().get(0); assertEquals("Wrong node parent.", folder2ID, moveFileParentAfterMove); } @@ -1003,7 +1044,6 @@ public class UpdateRepoEventIT extends AbstractContextAwareRepoEvent final Set originalAspects = resource.getAspectNames(); assertNotNull(originalAspects); - retryingTransactionHelper.doInTransaction(() -> { // Add cm:geographic aspect with default value nodeService.addAspect(nodeRef, ContentModel.ASPECT_GEOGRAPHIC, null);