mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-06-30 18:15:39 +00:00
16780: Fix failing unit test - HeartBeat now needs to be constructed inside a transaction. 16765: Merged DEV/BELARUS/V3.2-2009_10_05 to V3.2 16754: ETHREEOH-2534: SPP does not authenticate when authentication chain contains both alfrescoNtlm and passthru types. - NTLM Authentication handler for Sharepoint module was implemented as singleton. But after it was integrated into Alfresco Authentication Subsystem, instance of this object is created for each type of NTLM authentication. As result static field with NTLM flags was rewrited for each instance. Bug was resolved by removing static indicator. 16751: LDAP sync improvements - Correction to the way retried transactional errors are reported - Addition of unit test for synchronization with a mock user registry generating a large volume of users, groups and associations 16749: Removed UserUsageBootstrapJob from scheduled jobs and moved UserUsageTrackingComponent to bootstrap - files missed from CHK-9619 16748: User Usage Tracking Component bootstrapped synchronously to avoid its expensive queries across all users 'stepping on top of' other bootstrap activity such as LDAP synchronization - Its startup messages are no longer masked out by log4j.properties - Logged ETHREEOH-3009 regarding upgrade impact of new faster queries 16747: Lower impact of HeartBeat service on server performance - More efficient AuthorityService APIs used to determine the total number of groups and users more efficiently - Queries of all users and groups done synchronously at startup only 16746: Improvements for faster user and group lookup and association on a large repository (unfortunately intertwined) - NodeService getChildAssocRefsByTypeQNames query rewritten to use a subquery to force a more logical evaluation order on MySQL - NodeService getChildAssocs method made to use more efficient getChildAssocRefsByTypeQNames DAO call when a type qname but no assoc qname is specified - NodeService getUsersWithoutUsage / getUsersWithUsage queries rewritten to avoid an expensive outer join on all users - PersonService getPersonIgnoreCase query corrected to include the type QName ID of the child associations it is querying (thus avoiding unnecessarily triggering duplicate person removal) - PersonService now supports an optional boolean argument to getPerson that indicates whether the auto-create + home folder creation behaviour should be triggered. - AuthorityDAOImpl now uses false argument to getPerson call to avoid lazy home folder creation during creation of group associations - AuthorityDAOImpl now specifies assoc type to getChildAssocs in getAllAuthoritiesInZone and findAuthorities calls so that the more efficient query variant is used - Redundant personExists() call removed from authorityServiceImpl git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@16914 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
2563 lines
106 KiB
Java
2563 lines
106 KiB
Java
/*
|
|
* Copyright (C) 2005-2007 Alfresco Software Limited.
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 2
|
|
* of the License, or (at your option) any later version.
|
|
|
|
* This program 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 General Public License for more details.
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
* As a special exception to the terms and conditions of version 2.0 of
|
|
* the GPL, you may redistribute this Program in connection with Free/Libre
|
|
* and Open Source Software ("FLOSS") applications as described in Alfresco's
|
|
* FLOSS exception. You should have recieved a copy of the text describing
|
|
* the FLOSS exception, and it is also available here:
|
|
* http://www.alfresco.com/legal/licensing"
|
|
*/
|
|
package org.alfresco.repo.node.db;
|
|
|
|
import java.io.Serializable;
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.Collections;
|
|
import java.util.Date;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.Stack;
|
|
|
|
import org.alfresco.error.AlfrescoRuntimeException;
|
|
import org.alfresco.model.ContentModel;
|
|
import org.alfresco.repo.domain.Node;
|
|
import org.alfresco.repo.node.AbstractNodeServiceImpl;
|
|
import org.alfresco.repo.node.StoreArchiveMap;
|
|
import org.alfresco.repo.node.cleanup.AbstractNodeCleanupWorker;
|
|
import org.alfresco.repo.node.db.NodeDaoService.NodeRefQueryCallback;
|
|
import org.alfresco.repo.node.index.NodeIndexer;
|
|
import org.alfresco.repo.security.authentication.AuthenticationUtil;
|
|
import org.alfresco.repo.transaction.RetryingTransactionHelper;
|
|
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
|
|
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.CyclicChildRelationshipException;
|
|
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.NodeService;
|
|
import org.alfresco.service.cmr.repository.Path;
|
|
import org.alfresco.service.cmr.repository.StoreRef;
|
|
import org.alfresco.service.cmr.repository.NodeRef.Status;
|
|
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
|
|
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.util.EqualsHelper;
|
|
import org.alfresco.util.GUID;
|
|
import org.alfresco.util.Pair;
|
|
import org.alfresco.util.ParameterCheck;
|
|
import org.alfresco.util.PropertyMap;
|
|
import org.apache.commons.logging.Log;
|
|
import org.apache.commons.logging.LogFactory;
|
|
import org.springframework.util.Assert;
|
|
|
|
/**
|
|
* Node service using database persistence layer to fulfill functionality
|
|
*
|
|
* @author Derek Hulley
|
|
*/
|
|
public class DbNodeServiceImpl extends AbstractNodeServiceImpl
|
|
{
|
|
private static Log logger = LogFactory.getLog(DbNodeServiceImpl.class);
|
|
private static Log loggerPaths = LogFactory.getLog(DbNodeServiceImpl.class.getName() + ".paths");
|
|
|
|
private NodeDaoService nodeDaoService;
|
|
private StoreArchiveMap storeArchiveMap;
|
|
private NodeService avmNodeService;
|
|
private NodeIndexer nodeIndexer;
|
|
private boolean cascadeInTransaction;
|
|
|
|
public DbNodeServiceImpl()
|
|
{
|
|
storeArchiveMap = new StoreArchiveMap(); // in case it is not set
|
|
cascadeInTransaction = true;
|
|
}
|
|
|
|
public void setNodeDaoService(NodeDaoService nodeDaoService)
|
|
{
|
|
this.nodeDaoService = nodeDaoService;
|
|
}
|
|
|
|
public void setStoreArchiveMap(StoreArchiveMap storeArchiveMap)
|
|
{
|
|
this.storeArchiveMap = storeArchiveMap;
|
|
}
|
|
|
|
public void setAvmNodeService(NodeService avmNodeService)
|
|
{
|
|
this.avmNodeService = avmNodeService;
|
|
}
|
|
|
|
/**
|
|
* @param nodeIndexer the indexer that will be notified of node additions,
|
|
* modifications and deletions
|
|
*/
|
|
public void setNodeIndexer(NodeIndexer nodeIndexer)
|
|
{
|
|
this.nodeIndexer = nodeIndexer;
|
|
}
|
|
|
|
/**
|
|
* Set whether store delete and archive operations must cascade to all children
|
|
* in the same transaction.
|
|
*
|
|
* @param cascadeInTransaction <tt>true</tt> (default) to cascade during
|
|
* delete and archive
|
|
*/
|
|
public void setCascadeInTransaction(boolean cascadeInTransaction)
|
|
{
|
|
this.cascadeInTransaction = cascadeInTransaction;
|
|
}
|
|
|
|
/**
|
|
* Performs a null-safe get of the node
|
|
*
|
|
* @param nodeRef the node to retrieve
|
|
* @return Returns the node entity (never null)
|
|
* @throws InvalidNodeRefException if the referenced node could not be found
|
|
*/
|
|
private Pair<Long, NodeRef> getNodePairNotNull(NodeRef nodeRef) throws InvalidNodeRefException
|
|
{
|
|
ParameterCheck.mandatory("nodeRef", nodeRef);
|
|
|
|
Pair<Long, NodeRef> unchecked = nodeDaoService.getNodePair(nodeRef);
|
|
if (unchecked == null)
|
|
{
|
|
throw new InvalidNodeRefException("Node does not exist: " + nodeRef, nodeRef);
|
|
}
|
|
return unchecked;
|
|
}
|
|
|
|
public boolean exists(StoreRef storeRef)
|
|
{
|
|
return (nodeDaoService.getRootNode(storeRef) != null);
|
|
}
|
|
|
|
public boolean exists(NodeRef nodeRef)
|
|
{
|
|
ParameterCheck.mandatory("nodeRef", nodeRef);
|
|
|
|
Pair<Long, NodeRef> nodePair = nodeDaoService.getNodePair(nodeRef);
|
|
boolean exists = (nodePair != null);
|
|
// done
|
|
return exists;
|
|
}
|
|
|
|
public Status getNodeStatus(NodeRef nodeRef)
|
|
{
|
|
ParameterCheck.mandatory("nodeRef", nodeRef);
|
|
NodeRef.Status status = nodeDaoService.getNodeRefStatus(nodeRef);
|
|
return status;
|
|
}
|
|
|
|
/**
|
|
* @see NodeDaoService#getStores()
|
|
*/
|
|
public List<StoreRef> getStores()
|
|
{
|
|
// Get the ADM stores
|
|
List<Pair<Long, StoreRef>> stores = nodeDaoService.getStores();
|
|
List<StoreRef> storeRefs = new ArrayList<StoreRef>(50);
|
|
for (Pair<Long, StoreRef> pair : stores)
|
|
{
|
|
storeRefs.add(pair.getSecond());
|
|
}
|
|
// Now get the AVMStores.
|
|
List<StoreRef> avmStores = avmNodeService.getStores();
|
|
storeRefs.addAll(avmStores);
|
|
// Return them all.
|
|
return storeRefs;
|
|
}
|
|
|
|
/**
|
|
* Defers to the typed service
|
|
* @see StoreDaoService#createWorkspace(String)
|
|
*/
|
|
public StoreRef createStore(String protocol, String identifier)
|
|
{
|
|
StoreRef storeRef = new StoreRef(protocol, identifier);
|
|
|
|
// invoke policies
|
|
invokeBeforeCreateStore(ContentModel.TYPE_STOREROOT, storeRef);
|
|
|
|
// create a new one
|
|
Pair<Long, NodeRef> rootNodePair = nodeDaoService.createStore(storeRef);
|
|
NodeRef rootNodeRef = rootNodePair.getSecond();
|
|
|
|
// invoke policies
|
|
invokeOnCreateStore(rootNodeRef);
|
|
|
|
// Index
|
|
ChildAssociationRef assocRef = new ChildAssociationRef(null, null, null, rootNodeRef);
|
|
nodeIndexer.indexCreateNode(assocRef);
|
|
|
|
// Done
|
|
return storeRef;
|
|
}
|
|
|
|
/**
|
|
* @throws UnsupportedOperationException Always
|
|
*/
|
|
public void deleteStore(StoreRef storeRef) throws InvalidStoreRefException
|
|
{
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
public NodeRef getRootNode(StoreRef storeRef) throws InvalidStoreRefException
|
|
{
|
|
Pair<Long, NodeRef> rootNodePair = nodeDaoService.getRootNode(storeRef);
|
|
if (rootNodePair == null)
|
|
{
|
|
throw new InvalidStoreRefException("Store does not exist: " + storeRef, storeRef);
|
|
}
|
|
// done
|
|
return rootNodePair.getSecond();
|
|
}
|
|
|
|
/**
|
|
* @see #createNode(NodeRef, QName, QName, QName, Map)
|
|
*/
|
|
public ChildAssociationRef createNode(
|
|
NodeRef parentRef,
|
|
QName assocTypeQName,
|
|
QName assocQName,
|
|
QName nodeTypeQName)
|
|
{
|
|
return this.createNode(parentRef, assocTypeQName, assocQName, nodeTypeQName, null);
|
|
}
|
|
|
|
/**
|
|
* @see org.alfresco.service.cmr.repository.NodeService#createNode(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, org.alfresco.service.namespace.QName, org.alfresco.service.namespace.QName, java.util.Map)
|
|
*/
|
|
public ChildAssociationRef createNode(
|
|
NodeRef parentRef,
|
|
QName assocTypeQName,
|
|
QName assocQName,
|
|
QName nodeTypeQName,
|
|
Map<QName, Serializable> properties)
|
|
{
|
|
Assert.notNull(parentRef);
|
|
Assert.notNull(assocTypeQName);
|
|
Assert.notNull(assocQName);
|
|
|
|
// Get the parent node
|
|
Pair<Long, NodeRef> parentNodePair = getNodePairNotNull(parentRef);
|
|
StoreRef parentStoreRef = parentRef.getStoreRef();
|
|
|
|
// null property map is allowed
|
|
if (properties == null)
|
|
{
|
|
properties = Collections.emptyMap();
|
|
}
|
|
|
|
// get/generate an ID for the node
|
|
String newUuid = generateGuid(properties);
|
|
|
|
// Remove any system properties
|
|
extractIntrinsicProperties(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);
|
|
}
|
|
|
|
// create the node instance
|
|
Pair<Long, NodeRef> childNodePair = nodeDaoService.newNode(parentStoreRef, newUuid, nodeTypeQName);
|
|
|
|
// Add defaults
|
|
addDefaults(childNodePair, nodeTypeQName);
|
|
|
|
// set the properties passed in
|
|
if (properties.size() > 0)
|
|
{
|
|
nodeDaoService.addNodeProperties(childNodePair.getFirst(), properties);
|
|
}
|
|
|
|
Map<QName, Serializable> propertiesAfter = nodeDaoService.getNodeProperties(childNodePair.getFirst());
|
|
|
|
// We now have enough to declare the child association creation
|
|
invokeBeforeCreateChildAssociation(parentRef, childNodePair.getSecond(), assocTypeQName, assocQName, true);
|
|
|
|
// Ensure child uniqueness
|
|
String newName = extractNameProperty(propertiesAfter);
|
|
// Create the association
|
|
Pair<Long, ChildAssociationRef> childAssocPair = nodeDaoService.newChildAssoc(
|
|
parentNodePair.getFirst(),
|
|
childNodePair.getFirst(),
|
|
true,
|
|
assocTypeQName,
|
|
assocQName,
|
|
newName);
|
|
ChildAssociationRef childAssocRef = childAssocPair.getSecond();
|
|
|
|
// Invoke policy behaviour
|
|
invokeOnCreateNode(childAssocRef);
|
|
invokeOnCreateChildAssociation(childAssocRef, true);
|
|
addIntrinsicProperties(childNodePair, propertiesAfter);
|
|
Map<QName, Serializable> propertiesBefore = PropertyMap.EMPTY_MAP;
|
|
invokeOnUpdateProperties(
|
|
childAssocRef.getChildRef(),
|
|
propertiesBefore,
|
|
propertiesAfter);
|
|
|
|
// Add missing aspects
|
|
addMissingAspects(childNodePair, propertiesBefore, propertiesAfter);
|
|
addMissingAspects(parentNodePair, assocTypeQName);
|
|
|
|
// Index
|
|
nodeIndexer.indexCreateNode(childAssocRef);
|
|
|
|
// done
|
|
return childAssocRef;
|
|
}
|
|
|
|
/**
|
|
* Adds all the default aspects and properties required for the given type.
|
|
* Existing values will not be overridden.
|
|
*/
|
|
private void addDefaults(Pair<Long, NodeRef> nodePair, QName typeQName)
|
|
{
|
|
addDefaultProperties(nodePair, typeQName);
|
|
addDefaultAspects(nodePair, typeQName);
|
|
}
|
|
|
|
/**
|
|
* Add the default aspects to a given node
|
|
* @return Returns <tt>true</tt> if any aspects were added
|
|
*/
|
|
private boolean addDefaultAspects(Pair<Long, NodeRef> nodePair, QName typeQName)
|
|
{
|
|
ClassDefinition classDefinition = dictionaryService.getClass(typeQName);
|
|
if (classDefinition == null)
|
|
{
|
|
return false;
|
|
}
|
|
// Get the existing values
|
|
Long nodeId = nodePair.getFirst();
|
|
Map<QName, Serializable> existingProperties = nodeDaoService.getNodeProperties(nodeId);
|
|
Set<QName> existingAspects = nodeDaoService.getNodeAspects(nodeId);
|
|
return addDefaultAspects(nodePair, existingAspects, existingProperties, typeQName);
|
|
}
|
|
|
|
/**
|
|
* Add the default aspects to a given node
|
|
* @return Returns <tt>true</tt> if any aspects were added
|
|
*/
|
|
private boolean addDefaultAspects(
|
|
Pair<Long, NodeRef> nodePair,
|
|
Set<QName> existingAspects,
|
|
Map<QName, Serializable> existingProperties,
|
|
QName typeQName)
|
|
{
|
|
ClassDefinition classDefinition = dictionaryService.getClass(typeQName);
|
|
if (classDefinition == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Long nodeId = nodePair.getFirst();
|
|
NodeRef nodeRef = nodePair.getSecond();
|
|
|
|
// get the mandatory aspects for the node type
|
|
List<AspectDefinition> defaultAspectDefs = classDefinition.getDefaultAspects();
|
|
|
|
// add all the aspects to the node
|
|
boolean added = false;
|
|
for (AspectDefinition typeDefinition : defaultAspectDefs)
|
|
{
|
|
QName aspectQName = typeDefinition.getName();
|
|
boolean existingAspect = existingAspects.contains(aspectQName);
|
|
// Only add the aspect if it isn't there
|
|
if (!existingAspect)
|
|
{
|
|
invokeBeforeAddAspect(nodeRef, aspectQName);
|
|
nodeDaoService.addNodeAspects(nodeId, Collections.singleton(aspectQName));
|
|
added = true;
|
|
}
|
|
// Set default properties for the aspect
|
|
addDefaultProperties(nodePair, aspectQName);
|
|
if (!existingAspect)
|
|
{
|
|
// Fire policy
|
|
invokeOnAddAspect(nodeRef, aspectQName);
|
|
}
|
|
|
|
// Now add any default aspects for this aspect
|
|
boolean moreAdded = addDefaultAspects(nodePair, aspectQName);
|
|
added = (added || moreAdded);
|
|
}
|
|
// Done
|
|
return added;
|
|
}
|
|
|
|
/**
|
|
* @return Returns <tt>true</tt> if any properties were added
|
|
*/
|
|
private boolean addDefaultProperties(Pair<Long, NodeRef> nodePair, QName typeQName)
|
|
{
|
|
ClassDefinition classDefinition = dictionaryService.getClass(typeQName);
|
|
if (classDefinition == null)
|
|
{
|
|
return false;
|
|
}
|
|
// Get the existing values
|
|
Long nodeId = nodePair.getFirst();
|
|
Map<QName, Serializable> existingProperties = nodeDaoService.getNodeProperties(nodeId);
|
|
return addDefaultProperties(nodePair, existingProperties, typeQName);
|
|
}
|
|
|
|
/**
|
|
* Adds default properties for the given type to the node. Default values will not be set if there are existing values.
|
|
*/
|
|
private boolean addDefaultProperties(Pair<Long, NodeRef> nodePair, Map<QName, Serializable> existingProperties, QName typeQName)
|
|
{
|
|
Long nodeId = nodePair.getFirst();
|
|
// Get the default properties for this aspect
|
|
Map<QName, Serializable> defaultProperties = getDefaultProperties(typeQName);
|
|
// Remove all default values where a value already exists
|
|
for (Map.Entry<QName, Serializable> entry : existingProperties.entrySet())
|
|
{
|
|
QName existingPropertyQName = entry.getKey();
|
|
Serializable existingProperty = entry.getValue();
|
|
if (existingProperty != null)
|
|
{
|
|
defaultProperties.remove(existingPropertyQName);
|
|
}
|
|
}
|
|
// Add the properties to the node - but only if there is anything to set
|
|
if (defaultProperties.size() > 0)
|
|
{
|
|
nodeDaoService.addNodeProperties(nodeId, defaultProperties);
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public void setChildAssociationIndex(ChildAssociationRef childAssocRef, int index)
|
|
{
|
|
// get nodes
|
|
Pair<Long, NodeRef> parentNodePair = getNodePairNotNull(childAssocRef.getParentRef());
|
|
Pair<Long, NodeRef> childNodePair = getNodePairNotNull(childAssocRef.getChildRef());
|
|
|
|
Long parentNodeId = parentNodePair.getFirst();
|
|
Long childNodeId = childNodePair.getFirst();
|
|
QName assocTypeQName = childAssocRef.getTypeQName();
|
|
QName assocQName = childAssocRef.getQName();
|
|
|
|
Pair<Long, ChildAssociationRef> assocPair = nodeDaoService.getChildAssoc(
|
|
parentNodeId,
|
|
childNodeId,
|
|
assocTypeQName,
|
|
assocQName);
|
|
if (assocPair == null)
|
|
{
|
|
throw new InvalidChildAssociationRefException("Unable to set child association index: \n" +
|
|
" assoc: " + childAssocRef + "\n" +
|
|
" index: " + index,
|
|
childAssocRef);
|
|
}
|
|
// Get the child node name
|
|
Map<QName, Serializable> childNodeProperties = nodeDaoService.getNodeProperties(childNodeId);
|
|
String childNodeName = extractNameProperty(childNodeProperties);
|
|
// set the index
|
|
nodeDaoService.updateChildAssoc(
|
|
assocPair.getFirst(),
|
|
parentNodeId,
|
|
childNodeId,
|
|
assocTypeQName,
|
|
assocQName,
|
|
index,
|
|
childNodeName);
|
|
}
|
|
|
|
public QName getType(NodeRef nodeRef) throws InvalidNodeRefException
|
|
{
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
return nodeDaoService.getNodeType(nodePair.getFirst());
|
|
}
|
|
|
|
/**
|
|
* @see org.alfresco.service.cmr.repository.NodeService#setType(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName)
|
|
*/
|
|
public void setType(NodeRef nodeRef, QName typeQName) throws InvalidNodeRefException
|
|
{
|
|
// check the node type
|
|
TypeDefinition nodeTypeDef = dictionaryService.getType(typeQName);
|
|
if (nodeTypeDef == null)
|
|
{
|
|
throw new InvalidTypeException(typeQName);
|
|
}
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
|
|
// Invoke policies
|
|
invokeBeforeUpdateNode(nodeRef);
|
|
|
|
// Set the type
|
|
nodeDaoService.updateNode(nodePair.getFirst(), null, null, typeQName);
|
|
|
|
// Add the default aspects to the node (update the properties with any new default values)
|
|
addDefaultAspects(nodePair, typeQName);
|
|
|
|
// Index
|
|
nodeIndexer.indexUpdateNode(nodeRef);
|
|
|
|
// Invoke policies
|
|
invokeOnUpdateNode(nodeRef);
|
|
}
|
|
|
|
/**
|
|
* @see Node#getAspects()
|
|
*/
|
|
public void addAspect(
|
|
NodeRef nodeRef,
|
|
QName aspectTypeQName,
|
|
Map<QName, Serializable> aspectProperties)
|
|
throws InvalidNodeRefException, InvalidAspectException
|
|
{
|
|
// check that the aspect is legal
|
|
AspectDefinition aspectDef = dictionaryService.getAspect(aspectTypeQName);
|
|
if (aspectDef == null)
|
|
{
|
|
throw new InvalidAspectException("The aspect is invalid: " + aspectTypeQName, aspectTypeQName);
|
|
}
|
|
|
|
// Check the properties
|
|
if (aspectProperties != null)
|
|
{
|
|
// Remove any system properties
|
|
extractIntrinsicProperties(aspectProperties);
|
|
}
|
|
else
|
|
{
|
|
// Make a map
|
|
aspectProperties = Collections.emptyMap();
|
|
}
|
|
// Make the properties immutable to be sure that they are not used incorrectly
|
|
aspectProperties = Collections.unmodifiableMap(aspectProperties);
|
|
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
Long nodeId = nodePair.getFirst();
|
|
|
|
// Invoke policy behaviours
|
|
invokeBeforeUpdateNode(nodeRef);
|
|
invokeBeforeAddAspect(nodeRef, aspectTypeQName);
|
|
|
|
// Add defaults
|
|
addDefaults(nodePair, aspectTypeQName);
|
|
|
|
if (aspectProperties.size() > 0)
|
|
{
|
|
nodeDaoService.addNodeProperties(nodeId, aspectProperties);
|
|
}
|
|
|
|
if (!nodeDaoService.hasNodeAspect(nodeId, aspectTypeQName))
|
|
{
|
|
// Invoke policy behaviours
|
|
invokeOnUpdateNode(nodeRef);
|
|
invokeOnAddAspect(nodeRef, aspectTypeQName);
|
|
nodeDaoService.addNodeAspects(nodeId, Collections.singleton(aspectTypeQName));
|
|
}
|
|
|
|
// Index
|
|
nodeIndexer.indexUpdateNode(nodeRef);
|
|
}
|
|
|
|
public void removeAspect(NodeRef nodeRef, QName aspectTypeQName)
|
|
throws InvalidNodeRefException, InvalidAspectException
|
|
{
|
|
/**
|
|
* Note: Aspect and property removal is resilient to missing dictionary definitions
|
|
*/
|
|
// get the node
|
|
final Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
final Long nodeId = nodePair.getFirst();
|
|
|
|
boolean hadAspect = nodeDaoService.hasNodeAspect(nodeId, aspectTypeQName);
|
|
|
|
// Invoke policy behaviours
|
|
invokeBeforeUpdateNode(nodeRef);
|
|
if (hadAspect)
|
|
{
|
|
invokeBeforeRemoveAspect(nodeRef, aspectTypeQName);
|
|
nodeDaoService.removeNodeAspects(nodeId, Collections.singleton(aspectTypeQName));
|
|
}
|
|
|
|
AspectDefinition aspectDef = dictionaryService.getAspect(aspectTypeQName);
|
|
boolean updated = false;
|
|
if (aspectDef != null)
|
|
{
|
|
// Remove default properties
|
|
Map<QName,PropertyDefinition> propertyDefs = aspectDef.getProperties();
|
|
Set<QName> propertyToRemoveQNames = propertyDefs.keySet();
|
|
nodeDaoService.removeNodeProperties(nodeId, propertyToRemoveQNames);
|
|
|
|
// Remove child associations
|
|
// We have to iterate over the associations and remove all those between the parent and child
|
|
final List<Pair<Long, ChildAssociationRef>> assocsToDelete = new ArrayList<Pair<Long, ChildAssociationRef>>(5);
|
|
final List<Pair<Long, NodeRef>> nodesToDelete = new ArrayList<Pair<Long, NodeRef>>(5);
|
|
NodeDaoService.ChildAssocRefQueryCallback callback = new NodeDaoService.ChildAssocRefQueryCallback()
|
|
{
|
|
public boolean handle(
|
|
Pair<Long, ChildAssociationRef> childAssocPair,
|
|
Pair<Long, NodeRef> parentNodePair,
|
|
Pair<Long, NodeRef> childNodePair
|
|
)
|
|
{
|
|
// Double check that it's not a primary association. If so, we can't delete it and
|
|
// have to delete the child node directly and with full archival.
|
|
if (childAssocPair.getSecond().isPrimary())
|
|
{
|
|
nodesToDelete.add(childNodePair);
|
|
}
|
|
else
|
|
{
|
|
assocsToDelete.add(childAssocPair);
|
|
}
|
|
// No recurse
|
|
return false;
|
|
}
|
|
};
|
|
// Get all the QNames to remove
|
|
List<QName> assocTypeQNamesToRemove = new ArrayList<QName>(aspectDef.getChildAssociations().keySet());
|
|
nodeDaoService.getChildAssocsByTypeQNames(nodeId, assocTypeQNamesToRemove, callback);
|
|
// Delete all the collected associations
|
|
for (Pair<Long, ChildAssociationRef> assocPair : assocsToDelete)
|
|
{
|
|
updated = true;
|
|
Long assocId = assocPair.getFirst();
|
|
ChildAssociationRef assocRef = assocPair.getSecond();
|
|
// delete the association instance - it is not primary
|
|
invokeBeforeDeleteChildAssociation(assocRef);
|
|
nodeDaoService.deleteChildAssoc(assocId);
|
|
invokeOnDeleteChildAssociation(assocRef);
|
|
}
|
|
|
|
// Cascade-delete any nodes that were attached to primary associations
|
|
for (Pair<Long, NodeRef> childNodePair : nodesToDelete)
|
|
{
|
|
NodeRef childNodeRef = childNodePair.getSecond();
|
|
this.deleteNode(childNodeRef);
|
|
}
|
|
|
|
// Remove regular associations
|
|
Map<QName, AssociationDefinition> nodeAssocDefs = aspectDef.getAssociations();
|
|
Collection<Pair<Long, AssociationRef>> nodeAssocPairs = nodeDaoService.getNodeAssocsToAndFrom(nodeId);
|
|
for (Pair<Long, AssociationRef> nodeAssocPair : nodeAssocPairs)
|
|
{
|
|
updated = true;
|
|
QName nodeAssocTypeQName = nodeAssocPair.getSecond().getTypeQName();
|
|
// Ignore if the association type is not defined by the aspect
|
|
if (!nodeAssocDefs.containsKey(nodeAssocTypeQName))
|
|
{
|
|
continue;
|
|
}
|
|
updated = true;
|
|
// It has to be removed
|
|
nodeDaoService.deleteNodeAssoc(nodeAssocPair.getFirst());
|
|
}
|
|
}
|
|
|
|
// Invoke policy behaviours
|
|
if (updated)
|
|
{
|
|
invokeOnUpdateNode(nodeRef);
|
|
}
|
|
if (hadAspect)
|
|
{
|
|
invokeOnRemoveAspect(nodeRef, aspectTypeQName);
|
|
}
|
|
|
|
// Index
|
|
nodeIndexer.indexUpdateNode(nodeRef);
|
|
}
|
|
|
|
/**
|
|
* Performs a check on the set of node aspects
|
|
*/
|
|
public boolean hasAspect(NodeRef nodeRef, QName aspectQName) throws InvalidNodeRefException, InvalidAspectException
|
|
{
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
return nodeDaoService.hasNodeAspect(nodePair.getFirst(), aspectQName);
|
|
}
|
|
|
|
public Set<QName> getAspects(NodeRef nodeRef) throws InvalidNodeRefException
|
|
{
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
return nodeDaoService.getNodeAspects(nodePair.getFirst());
|
|
}
|
|
|
|
public void deleteNode(NodeRef nodeRef)
|
|
{
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
Long nodeId = nodePair.getFirst();
|
|
|
|
Boolean requiresDelete = null;
|
|
|
|
// Invoke policy behaviours
|
|
invokeBeforeDeleteNode(nodeRef);
|
|
|
|
// get the primary parent-child relationship before it is gone
|
|
Pair<Long, ChildAssociationRef> childAssocPair = nodeDaoService.getPrimaryParentAssoc(nodeId);
|
|
ChildAssociationRef childAssocRef = childAssocPair.getSecond();
|
|
// get type and aspect QNames as they will be unavailable after the delete
|
|
QName nodeTypeQName = nodeDaoService.getNodeType(nodeId);
|
|
Set<QName> nodeAspectQNames = nodeDaoService.getNodeAspects(nodeId);
|
|
|
|
// check if we need to archive the node
|
|
StoreRef storeRef = nodeRef.getStoreRef();
|
|
StoreRef archiveStoreRef = storeArchiveMap.get(storeRef);
|
|
|
|
if (archiveStoreRef == null)
|
|
{
|
|
requiresDelete = true;
|
|
}
|
|
else
|
|
{
|
|
// get the type and check if we need archiving.
|
|
TypeDefinition typeDef = dictionaryService.getType(nodeTypeQName);
|
|
if (typeDef != null)
|
|
{
|
|
Boolean requiresArchive = typeDef.getArchive();
|
|
if (requiresArchive != null)
|
|
{
|
|
requiresDelete = !requiresArchive;
|
|
}
|
|
}
|
|
|
|
// If the type hasn't asked for deletion, check whether any applied aspects have
|
|
Iterator<QName> i = nodeAspectQNames.iterator();
|
|
while ((requiresDelete == null || !requiresDelete) && i.hasNext())
|
|
{
|
|
QName nodeAspectQName = i.next();
|
|
AspectDefinition aspectDef = dictionaryService.getAspect(nodeAspectQName);
|
|
if (aspectDef != null)
|
|
{
|
|
Boolean requiresArchive = aspectDef.getArchive();
|
|
if (requiresArchive != null)
|
|
{
|
|
requiresDelete = !requiresArchive;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (requiresDelete == null || requiresDelete)
|
|
{
|
|
// Cascade as required
|
|
if (cascadeInTransaction)
|
|
{
|
|
deletePrimaryChildren(nodePair, true);
|
|
}
|
|
// perform a normal deletion
|
|
nodeDaoService.deleteNode(nodeId);
|
|
// Invoke policy behaviours
|
|
invokeOnDeleteNode(childAssocRef, nodeTypeQName, nodeAspectQNames, false);
|
|
|
|
// Index
|
|
nodeIndexer.indexDeleteNode(childAssocRef);
|
|
}
|
|
else
|
|
{
|
|
// archive it
|
|
archiveNode(nodeRef, archiveStoreRef);
|
|
// The archive performs a move, which will fire the appropriate OnDeleteNode
|
|
invokeOnDeleteNode(childAssocRef, nodeTypeQName, nodeAspectQNames, true);
|
|
}
|
|
}
|
|
|
|
private void deletePrimaryChildren(Pair<Long, NodeRef> nodePair, boolean cascade)
|
|
{
|
|
Long nodeId = nodePair.getFirst();
|
|
// Get the node's primary children
|
|
final List<Pair<Long, NodeRef>> childNodePairs = new ArrayList<Pair<Long, NodeRef>>(5);
|
|
// TODO: Fix issues when invoking onDeleteNode
|
|
// final Map<Long, ChildAssociationRef> childAssocRefsByChildId = new HashMap<Long, ChildAssociationRef>(5);
|
|
NodeDaoService.ChildAssocRefQueryCallback callback = new NodeDaoService.ChildAssocRefQueryCallback()
|
|
{
|
|
public boolean handle(
|
|
Pair<Long, ChildAssociationRef> childAssocPair,
|
|
Pair<Long, NodeRef> parentNodePair,
|
|
Pair<Long, NodeRef> childNodePair
|
|
)
|
|
{
|
|
// Add it
|
|
childNodePairs.add(childNodePair);
|
|
// childAssocRefsByChildId.put(childNodePair.getFirst(), childAssocPair.getSecond());
|
|
// No recurse
|
|
return false;
|
|
}
|
|
};
|
|
// Get all the QNames to remove
|
|
nodeDaoService.getPrimaryChildAssocs(nodeId, callback);
|
|
// Each child must be deleted
|
|
for (Pair<Long, NodeRef> childNodePair : childNodePairs)
|
|
{
|
|
// Fire node policies. This ensures that each node in the hierarchy gets a notification fired.
|
|
Long childNodeId = childNodePair.getFirst();
|
|
NodeRef childNodeRef = childNodePair.getSecond();
|
|
// QName childNodeType = nodeDaoService.getNodeType(childNodeId);
|
|
// Set<QName> childNodeQNames = nodeDaoService.getNodeAspects(childNodeId);
|
|
// ChildAssociationRef childParentAssocRef = childAssocRefsByChildId.get(childNodeId);
|
|
|
|
invokeBeforeDeleteNode(childNodeRef);
|
|
|
|
// Cascade first, if required.
|
|
// This ensures that the beforeDelete policy is fired for all nodes in the hierarchy before
|
|
// the actual delete starts.
|
|
if (cascade)
|
|
{
|
|
deletePrimaryChildren(childNodePair, true);
|
|
}
|
|
// Delete the child
|
|
nodeDaoService.deleteNode(childNodeId);
|
|
// invokeOnDeleteNode(childParentAssocRef, childNodeType, childNodeQNames, true);
|
|
}
|
|
}
|
|
|
|
public ChildAssociationRef addChild(NodeRef parentRef, NodeRef childRef, QName assocTypeQName, QName assocQName)
|
|
{
|
|
Pair<Long, NodeRef> parentNodePair = getNodePairNotNull(parentRef);
|
|
Long parentNodeId = parentNodePair.getFirst();
|
|
Pair<Long, NodeRef> childNodePair = getNodePairNotNull(childRef);
|
|
Long childNodeId = childNodePair.getFirst();
|
|
|
|
// Invoke policy behaviours
|
|
invokeBeforeCreateChildAssociation(parentRef, childRef, assocTypeQName, assocQName, false);
|
|
|
|
// Get the node's name, if present
|
|
Map<QName, Serializable> childNodeProperties = nodeDaoService.getNodeProperties(childNodePair.getFirst());
|
|
String childNodeName = extractNameProperty(childNodeProperties);
|
|
|
|
// make the association
|
|
Pair<Long, ChildAssociationRef> childAssocPair = nodeDaoService.newChildAssoc(
|
|
parentNodeId,
|
|
childNodeId,
|
|
false,
|
|
assocTypeQName,
|
|
assocQName,
|
|
childNodeName);
|
|
ChildAssociationRef childAssocRef = childAssocPair.getSecond();
|
|
// ensure name uniqueness
|
|
setChildNameUnique(childAssocPair, childNodePair);
|
|
NodeRef childNodeRef = childAssocRef.getChildRef();
|
|
|
|
// check that the child addition of the child has not created a cyclic relationship
|
|
// this functionality is provided for free in getPath
|
|
getPaths(childNodeRef, false);
|
|
|
|
// Invoke policy behaviours
|
|
invokeOnCreateChildAssociation(childAssocRef, false);
|
|
|
|
// Add missing aspects
|
|
addMissingAspects(parentNodePair, assocTypeQName);
|
|
|
|
// Index
|
|
nodeIndexer.indexCreateChildAssociation(childAssocRef);
|
|
|
|
return childAssocRef;
|
|
}
|
|
|
|
public void removeChild(NodeRef parentRef, NodeRef childRef) throws InvalidNodeRefException
|
|
{
|
|
final Pair<Long, NodeRef> parentNodePair = getNodePairNotNull(parentRef);
|
|
final Long parentNodeId = parentNodePair.getFirst();
|
|
final Pair<Long, NodeRef> childNodePair = getNodePairNotNull(childRef);
|
|
final Long childNodeId = childNodePair.getFirst();
|
|
|
|
// Get the primary parent association for the child
|
|
Pair<Long, ChildAssociationRef> primaryChildAssocPair = nodeDaoService.getPrimaryParentAssoc(childNodeId);
|
|
// We can shortcut if our parent is also the primary parent
|
|
if (primaryChildAssocPair != null)
|
|
{
|
|
NodeRef primaryParentNodeRef = primaryChildAssocPair.getSecond().getParentRef();
|
|
if (primaryParentNodeRef.equals(parentRef))
|
|
{
|
|
// Shortcut - just delete the child node
|
|
deleteNode(childRef);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// We have to iterate over the associations and remove all those between the parent and child
|
|
final List<Pair<Long, ChildAssociationRef>> assocsToDelete = new ArrayList<Pair<Long, ChildAssociationRef>>(5);
|
|
NodeDaoService.ChildAssocRefQueryCallback callback = new NodeDaoService.ChildAssocRefQueryCallback()
|
|
{
|
|
public boolean handle(
|
|
Pair<Long, ChildAssociationRef> childAssocPair,
|
|
Pair<Long, NodeRef> parentNodePair,
|
|
Pair<Long, NodeRef> childNodePair)
|
|
{
|
|
// Ignore if the child is not ours
|
|
if (!childNodePair.getFirst().equals(childNodeId))
|
|
{
|
|
return false;
|
|
}
|
|
// Add it
|
|
assocsToDelete.add(childAssocPair);
|
|
// No recurse
|
|
return false;
|
|
}
|
|
};
|
|
nodeDaoService.getChildAssocs(parentNodeId, callback, false);
|
|
|
|
// Delete all the collected associations
|
|
for (Pair<Long, ChildAssociationRef> assocPair : assocsToDelete)
|
|
{
|
|
Long assocId = assocPair.getFirst();
|
|
ChildAssociationRef assocRef = assocPair.getSecond();
|
|
// delete the association instance - it is not primary
|
|
invokeBeforeDeleteChildAssociation(assocRef);
|
|
nodeDaoService.deleteChildAssoc(assocId);
|
|
invokeOnDeleteChildAssociation(assocRef);
|
|
|
|
// Index
|
|
nodeIndexer.indexDeleteChildAssociation(assocRef);
|
|
}
|
|
|
|
// Done
|
|
}
|
|
|
|
public boolean removeChildAssociation(ChildAssociationRef childAssocRef)
|
|
{
|
|
Long parentNodeId = getNodePairNotNull(childAssocRef.getParentRef()).getFirst();
|
|
Long childNodeId = getNodePairNotNull(childAssocRef.getChildRef()).getFirst();
|
|
QName assocTypeQName = childAssocRef.getTypeQName();
|
|
QName assocQName = childAssocRef.getQName();
|
|
Pair<Long, ChildAssociationRef> assocPair = nodeDaoService.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);
|
|
nodeDaoService.deleteChildAssoc(assocId);
|
|
invokeOnDeleteChildAssociation(childAssocRef);
|
|
// Index
|
|
nodeIndexer.indexDeleteChildAssociation(childAssocRef);
|
|
// Done
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public boolean removeSeconaryChildAssociation(ChildAssociationRef childAssocRef)
|
|
{
|
|
Long parentNodeId = getNodePairNotNull(childAssocRef.getParentRef()).getFirst();
|
|
Long childNodeId = getNodePairNotNull(childAssocRef.getChildRef()).getFirst();
|
|
QName assocTypeQName = childAssocRef.getTypeQName();
|
|
QName assocQName = childAssocRef.getQName();
|
|
Pair<Long, ChildAssociationRef> assocPair = nodeDaoService.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
|
|
nodeDaoService.deleteChildAssoc(assocId);
|
|
invokeOnDeleteChildAssociation(childAssocRef);
|
|
// Index
|
|
nodeIndexer.indexDeleteChildAssociation(childAssocRef);
|
|
// Done
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Remove properties that should not be persisted as general properties. Where necessary, the
|
|
* properties are set on the node.
|
|
*
|
|
* @param node the node to set properties on
|
|
* @param properties properties to change
|
|
*/
|
|
private void extractIntrinsicProperties(Map<QName, Serializable> properties)
|
|
{
|
|
properties.remove(ContentModel.PROP_STORE_PROTOCOL);
|
|
properties.remove(ContentModel.PROP_STORE_IDENTIFIER);
|
|
properties.remove(ContentModel.PROP_NODE_UUID);
|
|
properties.remove(ContentModel.PROP_NODE_DBID);
|
|
}
|
|
|
|
/**
|
|
* Adds all properties used by the
|
|
* {@link ContentModel#ASPECT_REFERENCEABLE referencable aspect}.
|
|
* <p>
|
|
* This method can be used to ensure that the values used by the aspect
|
|
* are present as node properties.
|
|
* <p>
|
|
* This method also ensures that the {@link ContentModel#PROP_NAME name property}
|
|
* is always present as a property on a node.
|
|
*
|
|
* @param node the node with the values
|
|
* @param nodeRef the node reference containing the values required
|
|
* @param properties the node properties
|
|
*/
|
|
private void addIntrinsicProperties(Pair<Long, NodeRef> nodePair, Map<QName, Serializable> properties)
|
|
{
|
|
Long nodeId = nodePair.getFirst();
|
|
NodeRef nodeRef = nodePair.getSecond();
|
|
properties.put(ContentModel.PROP_STORE_PROTOCOL, nodeRef.getStoreRef().getProtocol());
|
|
properties.put(ContentModel.PROP_STORE_IDENTIFIER, nodeRef.getStoreRef().getIdentifier());
|
|
properties.put(ContentModel.PROP_NODE_UUID, nodeRef.getId());
|
|
properties.put(ContentModel.PROP_NODE_DBID, nodeId);
|
|
// add the ID as the name, if required
|
|
if (properties.get(ContentModel.PROP_NAME) == null)
|
|
{
|
|
properties.put(ContentModel.PROP_NAME, nodeRef.getId());
|
|
}
|
|
}
|
|
|
|
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 = nodeDaoService.getNodeProperty(nodeId, qname);
|
|
|
|
// check if we need to provide a spoofed name
|
|
if (property == null && qname.equals(ContentModel.PROP_NAME))
|
|
{
|
|
return nodeRef.getId();
|
|
}
|
|
|
|
// done
|
|
return property;
|
|
}
|
|
|
|
public Map<QName, Serializable> getProperties(NodeRef nodeRef) throws InvalidNodeRefException
|
|
{
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
return getPropertiesImpl(nodePair);
|
|
}
|
|
|
|
/**
|
|
* Gets, converts and adds the intrinsic properties to the current node's properties
|
|
*/
|
|
private Map<QName, Serializable> getPropertiesImpl(Pair<Long, NodeRef> nodePair) throws InvalidNodeRefException
|
|
{
|
|
Long nodeId = nodePair.getFirst();
|
|
Map<QName, Serializable> nodeProperties = nodeDaoService.getNodeProperties(nodeId);
|
|
// spoof referencable properties
|
|
addIntrinsicProperties(nodePair, nodeProperties);
|
|
// done
|
|
return nodeProperties;
|
|
}
|
|
|
|
/**
|
|
* Find any aspects that are missing for the node, given the properties before and after an update.
|
|
*/
|
|
private void addMissingAspects(
|
|
Pair<Long, NodeRef> nodePair,
|
|
Map<QName, Serializable> propertiesBefore,
|
|
Map<QName, Serializable> propertiesAfter)
|
|
{
|
|
Long nodeId = nodePair.getFirst();
|
|
NodeRef nodeRef = nodePair.getSecond();
|
|
Set<QName> aspectQNamesToAdd = new HashSet<QName>(5);
|
|
Set<QName> newProperties = new HashSet<QName>(propertiesAfter.keySet());
|
|
newProperties.removeAll(propertiesBefore.entrySet());
|
|
Set<QName> existingAspectsQNames = nodeDaoService.getNodeAspects(nodeId);
|
|
for (QName newPropertyQName : newProperties)
|
|
{
|
|
PropertyDefinition propDef = dictionaryService.getProperty(newPropertyQName);
|
|
if (propDef == null)
|
|
{
|
|
continue; // Ignore undefined properties
|
|
}
|
|
if (!propDef.getContainerClass().isAspect())
|
|
{
|
|
continue;
|
|
}
|
|
QName containerClassQName = propDef.getContainerClass().getName();
|
|
// Remove this aspect - it is there
|
|
if (existingAspectsQNames.contains(containerClassQName))
|
|
{
|
|
// Already there
|
|
continue;
|
|
}
|
|
aspectQNamesToAdd.add(containerClassQName);
|
|
}
|
|
// Add the aspects and any missing, default properties
|
|
if (aspectQNamesToAdd.size() > 0)
|
|
{
|
|
for (QName aspectQNameToAdd : aspectQNamesToAdd)
|
|
{
|
|
invokeBeforeAddAspect(nodeRef, aspectQNameToAdd);
|
|
}
|
|
nodeDaoService.addNodeAspects(nodeId, aspectQNamesToAdd);
|
|
// Add the aspects and then their appropriate default values.
|
|
for (QName aspectQNameToAdd : aspectQNamesToAdd)
|
|
{
|
|
addDefaultProperties(nodePair, propertiesAfter, aspectQNameToAdd);
|
|
addDefaultAspects(nodePair, aspectQNameToAdd);
|
|
}
|
|
for (QName aspectQNameToAdd : aspectQNamesToAdd)
|
|
{
|
|
invokeOnAddAspect(nodeRef, aspectQNameToAdd);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find any aspects that are missing for the node, given the association type.
|
|
*/
|
|
private void addMissingAspects(
|
|
Pair<Long, NodeRef> nodePair,
|
|
QName assocTypeQName)
|
|
{
|
|
Long nodeId = nodePair.getFirst();
|
|
NodeRef nodeRef = nodePair.getSecond();
|
|
Set<QName> existingAspectsQNames = nodeDaoService.getNodeAspects(nodeId);
|
|
AssociationDefinition assocDef = dictionaryService.getAssociation(assocTypeQName);
|
|
if (assocDef == null)
|
|
{
|
|
return; // Ignore undefined properties
|
|
}
|
|
if (!assocDef.getSourceClass().isAspect())
|
|
{
|
|
return;
|
|
}
|
|
QName aspectQNameToAdd = assocDef.getSourceClass().getName();
|
|
// Remove this aspect - it is there
|
|
if (existingAspectsQNames.contains(aspectQNameToAdd))
|
|
{
|
|
// Already there
|
|
return;
|
|
}
|
|
// Add the aspects and any missing, default properties
|
|
invokeBeforeAddAspect(nodeRef, aspectQNameToAdd);
|
|
nodeDaoService.addNodeAspects(nodeId, Collections.singleton(aspectQNameToAdd));
|
|
// Add the aspects and then their appropriate default values.
|
|
addDefaultProperties(nodePair, aspectQNameToAdd);
|
|
addDefaultAspects(nodePair, aspectQNameToAdd);
|
|
invokeOnAddAspect(nodeRef, aspectQNameToAdd);
|
|
}
|
|
|
|
/**
|
|
* Gets the properties map, sets the value (null is allowed) and checks that the new set
|
|
* of properties is valid.
|
|
*
|
|
* @see DbNodeServiceImpl.NullPropertyValue
|
|
*/
|
|
public void setProperty(NodeRef nodeRef, QName qname, Serializable value) throws InvalidNodeRefException
|
|
{
|
|
Assert.notNull(qname);
|
|
|
|
// get the node
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
Long nodeId = nodePair.getFirst();
|
|
|
|
// Ensure that we are not setting intrinsic properties
|
|
Map<QName, Serializable> properties = new HashMap<QName, Serializable>(1, 1.0F);
|
|
properties.put(qname, value);
|
|
extractIntrinsicProperties(properties);
|
|
|
|
// Shortcut if nothing is left
|
|
if (properties.size() == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get the properties from before
|
|
Map<QName, Serializable> propertiesBefore = getPropertiesImpl(nodePair);
|
|
|
|
invokeBeforeUpdateNode(nodeRef);
|
|
// Update the properties
|
|
setPropertyImpl(nodeId, qname, value);
|
|
// Policy callbacks
|
|
Map<QName, Serializable> propertiesAfter = getPropertiesImpl(nodePair);
|
|
invokeOnUpdateNode(nodeRef);
|
|
invokeOnUpdateProperties(nodeRef, propertiesBefore, propertiesAfter);
|
|
|
|
// Add any missing aspects
|
|
addMissingAspects(nodePair, propertiesBefore, propertiesAfter);
|
|
|
|
// Index
|
|
nodeIndexer.indexUpdateNode(nodeRef);
|
|
}
|
|
|
|
/**
|
|
* Sets the property, taking special care to handle intrinsic properties and <b>cm:name</b> properly
|
|
*/
|
|
private void setPropertyImpl(Long nodeId, QName qname, Serializable value)
|
|
{
|
|
if (qname.equals(ContentModel.PROP_NODE_UUID))
|
|
{
|
|
throw new IllegalArgumentException("The node UUID cannot be changed.");
|
|
}
|
|
else
|
|
{
|
|
// cm:name special handling
|
|
if (qname.equals(ContentModel.PROP_NAME))
|
|
{
|
|
Pair<Long, ChildAssociationRef> primaryParentAssocPair = nodeDaoService.getPrimaryParentAssoc(nodeId);
|
|
if (primaryParentAssocPair != null)
|
|
{
|
|
String oldName = extractNameProperty(nodeDaoService.getNodeProperties(nodeId));
|
|
String newName = DefaultTypeConverter.INSTANCE.convert(String.class, value);
|
|
setChildNameUnique(primaryParentAssocPair, newName, oldName);
|
|
}
|
|
}
|
|
// Set the property
|
|
nodeDaoService.addNodeProperty(nodeId, qname, value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ensures that all required properties are present on the node and copies the
|
|
* property values to the <code>Node</code>.
|
|
* <p>
|
|
* To remove a property, <b>remove it from the map</b> before calling this method.
|
|
* Null-valued properties are allowed.
|
|
* <p>
|
|
* If any of the values are null, a marker object is put in to mimic nulls. They will be turned back into
|
|
* a real nulls when the properties are requested again.
|
|
*
|
|
* @see Node#getProperties()
|
|
*/
|
|
public void setProperties(NodeRef nodeRef, Map<QName, Serializable> properties) throws InvalidNodeRefException
|
|
{
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
Long nodeId = nodePair.getFirst();
|
|
|
|
extractIntrinsicProperties(properties);
|
|
|
|
// Invoke policy behaviours
|
|
Map<QName, Serializable> propertiesBefore = getPropertiesImpl(nodePair);
|
|
invokeBeforeUpdateNode(nodeRef);
|
|
|
|
// Do the set properties
|
|
setPropertiesImpl(nodeId, properties);
|
|
|
|
// Invoke policy behaviours
|
|
Map<QName, Serializable> propertiesAfter = getPropertiesImpl(nodePair);
|
|
invokeOnUpdateNode(nodeRef);
|
|
invokeOnUpdateProperties(nodeRef, propertiesBefore, propertiesAfter);
|
|
|
|
// Add any missing aspects
|
|
addMissingAspects(nodePair, propertiesBefore, propertiesAfter);
|
|
|
|
// Index
|
|
nodeIndexer.indexUpdateNode(nodeRef);
|
|
}
|
|
|
|
public void addProperties(NodeRef nodeRef, Map<QName, Serializable> properties) throws InvalidNodeRefException
|
|
{
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
Long nodeId = nodePair.getFirst();
|
|
|
|
extractIntrinsicProperties(properties);
|
|
|
|
// Invoke policy behaviours
|
|
Map<QName, Serializable> propertiesBefore = getPropertiesImpl(nodePair);
|
|
invokeBeforeUpdateNode(nodeRef);
|
|
|
|
// Change each property
|
|
for (Map.Entry<QName, Serializable> entry : properties.entrySet())
|
|
{
|
|
QName propertyQName = entry.getKey();
|
|
Serializable propertyValue = entry.getValue();
|
|
setPropertyImpl(nodeId, propertyQName, propertyValue);
|
|
}
|
|
|
|
// Invoke policy behaviours
|
|
Map<QName, Serializable> propertiesAfter = getPropertiesImpl(nodePair);
|
|
invokeOnUpdateNode(nodeRef);
|
|
invokeOnUpdateProperties(nodeRef, propertiesBefore, propertiesAfter);
|
|
|
|
// Add any missing aspects
|
|
addMissingAspects(nodePair, propertiesBefore, propertiesAfter);
|
|
|
|
// Index
|
|
nodeIndexer.indexUpdateNode(nodeRef);
|
|
}
|
|
|
|
private void setPropertiesImpl(Long nodeId, Map<QName, Serializable> properties)
|
|
{
|
|
// Get the cm:name and uuid for special handling
|
|
if (properties.containsKey(ContentModel.PROP_NAME))
|
|
{
|
|
Serializable name = properties.get(ContentModel.PROP_NAME);
|
|
setPropertyImpl(nodeId, ContentModel.PROP_NAME, name);
|
|
}
|
|
if (properties.containsKey(ContentModel.PROP_NODE_UUID))
|
|
{
|
|
throw new IllegalArgumentException("The node UUID cannot be set");
|
|
}
|
|
// Now remove special properties
|
|
extractIntrinsicProperties(properties);
|
|
// Update the node
|
|
nodeDaoService.setNodeProperties(nodeId, properties);
|
|
}
|
|
|
|
public void removeProperty(NodeRef nodeRef, QName qname) throws InvalidNodeRefException
|
|
{
|
|
// Get the node
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
Long nodeId = nodePair.getFirst();
|
|
|
|
// Invoke policy behaviours
|
|
invokeBeforeUpdateNode(nodeRef);
|
|
|
|
// Get the values before
|
|
Map<QName, Serializable> propertiesBefore = getPropertiesImpl(nodePair);
|
|
|
|
// cm:name special handling
|
|
if (qname.equals(ContentModel.PROP_NAME))
|
|
{
|
|
Pair<Long, ChildAssociationRef> primaryParentAssocPair = nodeDaoService.getPrimaryParentAssoc(nodeId);
|
|
String oldName = extractNameProperty(nodeDaoService.getNodeProperties(nodeId));
|
|
String newName = null;
|
|
setChildNameUnique(primaryParentAssocPair, newName, oldName);
|
|
}
|
|
|
|
// Remove
|
|
nodeDaoService.removeNodeProperties(nodeId, Collections.singleton(qname));
|
|
|
|
// Invoke policy behaviours
|
|
Map<QName, Serializable> propertiesAfter = getPropertiesImpl(nodePair);
|
|
invokeOnUpdateNode(nodeRef);
|
|
invokeOnUpdateProperties(nodeRef, propertiesBefore, propertiesAfter);
|
|
|
|
// Index
|
|
nodeIndexer.indexUpdateNode(nodeRef);
|
|
}
|
|
|
|
public Collection<NodeRef> getParents(NodeRef nodeRef) throws InvalidNodeRefException
|
|
{
|
|
// Get the node
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
Long nodeId = nodePair.getFirst();
|
|
|
|
// Get the assocs pointing to it
|
|
Collection<Pair<Long, ChildAssociationRef>> parentAssocPairs = nodeDaoService.getParentAssocs(nodeId);
|
|
// list of results
|
|
Collection<NodeRef> results = new ArrayList<NodeRef>(parentAssocPairs.size());
|
|
for (Pair<Long, ChildAssociationRef> assocPair : parentAssocPairs)
|
|
{
|
|
NodeRef parentNodeRef = assocPair.getSecond().getParentRef();
|
|
results.add(parentNodeRef);
|
|
}
|
|
// done
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Filters out any associations if their qname is not a match to the given pattern.
|
|
*/
|
|
public List<ChildAssociationRef> getParentAssocs(NodeRef nodeRef, QNamePattern typeQNamePattern, QNamePattern qnamePattern)
|
|
{
|
|
// Get the node
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
Long nodeId = nodePair.getFirst();
|
|
|
|
// Get the assocs pointing to it
|
|
Collection<Pair<Long, ChildAssociationRef>> parentAssocPairs = nodeDaoService.getParentAssocs(nodeId);
|
|
// list of results
|
|
List<ChildAssociationRef> results = new ArrayList<ChildAssociationRef>(parentAssocPairs.size());
|
|
for (Pair<Long, ChildAssociationRef> assocPair : parentAssocPairs)
|
|
{
|
|
ChildAssociationRef assocRef = assocPair.getSecond();
|
|
QName assocTypeQName = assocRef.getTypeQName();
|
|
QName assocQName = assocRef.getQName();
|
|
if (!qnamePattern.isMatch(assocQName) || !typeQNamePattern.isMatch(assocTypeQName))
|
|
{
|
|
// No match
|
|
continue;
|
|
}
|
|
results.add(assocRef);
|
|
}
|
|
// done
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Filters out any associations if their qname is not a match to the given pattern.
|
|
*/
|
|
public List<ChildAssociationRef> getChildAssocs(NodeRef nodeRef, final QNamePattern typeQNamePattern, final QNamePattern qnamePattern)
|
|
{
|
|
// Get the node
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
Long nodeId = nodePair.getFirst();
|
|
|
|
final List<ChildAssociationRef> results = new ArrayList<ChildAssociationRef>(100);
|
|
|
|
if (qnamePattern instanceof QName)
|
|
{
|
|
// Both explicit QNames
|
|
if (typeQNamePattern instanceof QName)
|
|
{
|
|
NodeDaoService.ChildAssocRefQueryCallback callback = new NodeDaoService.ChildAssocRefQueryCallback()
|
|
{
|
|
public boolean handle(Pair<Long, ChildAssociationRef> childAssocPair,
|
|
Pair<Long, NodeRef> parentNodePair, Pair<Long, NodeRef> childNodePair)
|
|
{
|
|
results.add(childAssocPair.getSecond());
|
|
return false;
|
|
}
|
|
};
|
|
// Get all child associations with the specific qualified name
|
|
nodeDaoService.getChildAssocsByTypeQNameAndQName(nodeId, (QName) typeQNamePattern,
|
|
(QName) qnamePattern, callback);
|
|
}
|
|
// Type is explicit, local qname is pattern
|
|
else
|
|
{
|
|
NodeDaoService.ChildAssocRefQueryCallback callback;
|
|
if (typeQNamePattern.equals(RegexQNamePattern.MATCH_ALL))
|
|
{
|
|
callback = new NodeDaoService.ChildAssocRefQueryCallback()
|
|
{
|
|
public boolean handle(Pair<Long, ChildAssociationRef> childAssocPair,
|
|
Pair<Long, NodeRef> parentNodePair, Pair<Long, NodeRef> childNodePair)
|
|
{
|
|
results.add(childAssocPair.getSecond());
|
|
return false;
|
|
}
|
|
};
|
|
}
|
|
else
|
|
{
|
|
callback = new NodeDaoService.ChildAssocRefQueryCallback()
|
|
{
|
|
public boolean handle(Pair<Long, ChildAssociationRef> childAssocPair,
|
|
Pair<Long, NodeRef> parentNodePair, Pair<Long, NodeRef> childNodePair)
|
|
{
|
|
ChildAssociationRef assocRef = childAssocPair.getSecond();
|
|
QName assocTypeQName = assocRef.getTypeQName();
|
|
if (!typeQNamePattern.isMatch(assocTypeQName))
|
|
{
|
|
// No match
|
|
return false;
|
|
}
|
|
results.add(assocRef);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
}
|
|
|
|
// Get all child associations with the specific qualified name
|
|
nodeDaoService.getChildAssocs(nodeId, (QName) qnamePattern, callback);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Local qname is pattern, type name is explicit
|
|
if (typeQNamePattern instanceof QName)
|
|
{
|
|
NodeDaoService.ChildAssocRefQueryCallback callback;
|
|
// if the type is the wildcard type, and the qname is not a search, then use a shortcut query
|
|
if (qnamePattern.equals(RegexQNamePattern.MATCH_ALL))
|
|
{
|
|
callback = new NodeDaoService.ChildAssocRefQueryCallback()
|
|
{
|
|
public boolean handle(Pair<Long, ChildAssociationRef> childAssocPair,
|
|
Pair<Long, NodeRef> parentNodePair, Pair<Long, NodeRef> childNodePair)
|
|
{
|
|
results.add(childAssocPair.getSecond());
|
|
return false;
|
|
}
|
|
};
|
|
}
|
|
else
|
|
{
|
|
|
|
callback = new NodeDaoService.ChildAssocRefQueryCallback()
|
|
{
|
|
public boolean handle(Pair<Long, ChildAssociationRef> childAssocPair,
|
|
Pair<Long, NodeRef> parentNodePair, Pair<Long, NodeRef> childNodePair)
|
|
{
|
|
ChildAssociationRef assocRef = childAssocPair.getSecond();
|
|
QName assocQName = assocRef.getQName();
|
|
if (!qnamePattern.isMatch(assocQName))
|
|
{
|
|
// No match
|
|
return false;
|
|
}
|
|
results.add(assocRef);
|
|
return false;
|
|
}
|
|
};
|
|
}
|
|
|
|
// Get all child associations with the specific type qualified name
|
|
nodeDaoService.getChildAssocsByTypeQNames(nodeId, Collections.singletonList((QName) typeQNamePattern),
|
|
callback);
|
|
|
|
}
|
|
// Local qname is pattern, type name is pattern
|
|
else
|
|
{
|
|
NodeDaoService.ChildAssocRefQueryCallback callback = new NodeDaoService.ChildAssocRefQueryCallback()
|
|
{
|
|
public boolean handle(Pair<Long, ChildAssociationRef> childAssocPair,
|
|
Pair<Long, NodeRef> parentNodePair, Pair<Long, NodeRef> childNodePair)
|
|
{
|
|
ChildAssociationRef assocRef = childAssocPair.getSecond();
|
|
QName assocTypeQName = assocRef.getTypeQName();
|
|
QName assocQName = assocRef.getQName();
|
|
if (!qnamePattern.isMatch(assocQName) || !typeQNamePattern.isMatch(assocTypeQName))
|
|
{
|
|
// No match
|
|
return false;
|
|
}
|
|
results.add(assocRef);
|
|
return false;
|
|
}
|
|
};
|
|
// Get all child associations
|
|
nodeDaoService.getChildAssocs(nodeId, callback, false);
|
|
}
|
|
}
|
|
|
|
// sort the results
|
|
List<ChildAssociationRef> orderedList = reorderChildAssocs(results);
|
|
// done
|
|
return orderedList;
|
|
}
|
|
|
|
public List<ChildAssociationRef> getChildAssocs(NodeRef nodeRef, Set<QName> childNodeTypeQNames)
|
|
{
|
|
// Get the node
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
Long nodeId = nodePair.getFirst();
|
|
|
|
final List<ChildAssociationRef> results = new ArrayList<ChildAssociationRef>(100);
|
|
|
|
NodeDaoService.ChildAssocRefQueryCallback callback = new NodeDaoService.ChildAssocRefQueryCallback()
|
|
{
|
|
public boolean handle(
|
|
Pair<Long, ChildAssociationRef> childAssocPair,
|
|
Pair<Long, NodeRef> parentNodePair,
|
|
Pair<Long, NodeRef> childNodePair)
|
|
{
|
|
results.add(childAssocPair.getSecond());
|
|
return false;
|
|
}
|
|
};
|
|
// Get all child associations with the specific qualified name
|
|
nodeDaoService.getChildAssocsByChildTypes(nodeId, childNodeTypeQNames, callback);
|
|
// Sort the results
|
|
List<ChildAssociationRef> orderedList = reorderChildAssocs(results);
|
|
// Done
|
|
return orderedList;
|
|
}
|
|
|
|
private List<ChildAssociationRef> reorderChildAssocs(Collection<ChildAssociationRef> childAssocRefs)
|
|
{
|
|
// shortcut if there are no assocs
|
|
if (childAssocRefs.size() == 0)
|
|
{
|
|
return Collections.emptyList();
|
|
}
|
|
// sort results
|
|
ArrayList<ChildAssociationRef> orderedList = new ArrayList<ChildAssociationRef>(childAssocRefs);
|
|
Collections.sort(orderedList);
|
|
|
|
// list of results
|
|
int nthSibling = 0;
|
|
Iterator<ChildAssociationRef> iterator = orderedList.iterator();
|
|
while(iterator.hasNext())
|
|
{
|
|
ChildAssociationRef childAssocRef = iterator.next();
|
|
childAssocRef.setNthSibling(nthSibling);
|
|
nthSibling++;
|
|
}
|
|
// done
|
|
return orderedList;
|
|
}
|
|
|
|
public NodeRef getChildByName(NodeRef nodeRef, QName assocTypeQName, String childName)
|
|
{
|
|
// Get the node
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
Long nodeId = nodePair.getFirst();
|
|
|
|
Pair<Long, ChildAssociationRef> childAssocPair = nodeDaoService.getChildAssoc(nodeId, assocTypeQName, childName);
|
|
if (childAssocPair != null)
|
|
{
|
|
return childAssocPair.getSecond().getChildRef();
|
|
}
|
|
else
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public List<ChildAssociationRef> getChildrenByName(NodeRef nodeRef, QName assocTypeQName, Collection<String> childNames)
|
|
{
|
|
// Get the node
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
Long nodeId = nodePair.getFirst();
|
|
|
|
final List<ChildAssociationRef> results = new ArrayList<ChildAssociationRef>(100);
|
|
|
|
NodeDaoService.ChildAssocRefQueryCallback callback = new NodeDaoService.ChildAssocRefQueryCallback()
|
|
{
|
|
public boolean handle(
|
|
Pair<Long, ChildAssociationRef> childAssocPair,
|
|
Pair<Long, NodeRef> parentNodePair,
|
|
Pair<Long, NodeRef> childNodePair)
|
|
{
|
|
results.add(childAssocPair.getSecond());
|
|
return false;
|
|
}
|
|
};
|
|
// Get all child associations with the specific qualified name
|
|
nodeDaoService.getChildAssocs(nodeId, assocTypeQName, childNames, callback);
|
|
// Sort the results
|
|
List<ChildAssociationRef> orderedList = reorderChildAssocs(results);
|
|
// Done
|
|
return orderedList;
|
|
}
|
|
|
|
public ChildAssociationRef getPrimaryParent(NodeRef nodeRef) throws InvalidNodeRefException
|
|
{
|
|
// Get the node
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
Long nodeId = nodePair.getFirst();
|
|
|
|
// get the primary parent assoc
|
|
Pair<Long, ChildAssociationRef> assocPair = nodeDaoService.getPrimaryParentAssoc(nodeId);
|
|
|
|
// done - the assoc may be null for a root node
|
|
ChildAssociationRef assocRef = null;
|
|
if (assocPair == null)
|
|
{
|
|
assocRef = new ChildAssociationRef(null, null, null, nodeRef);
|
|
}
|
|
else
|
|
{
|
|
assocRef = assocPair.getSecond();
|
|
}
|
|
return assocRef;
|
|
}
|
|
|
|
public AssociationRef createAssociation(NodeRef sourceRef, NodeRef targetRef, QName assocTypeQName)
|
|
throws InvalidNodeRefException, AssociationExistsException
|
|
{
|
|
Pair<Long, NodeRef> sourceNodePair = getNodePairNotNull(sourceRef);
|
|
long sourceNodeId = sourceNodePair.getFirst();
|
|
Pair<Long, NodeRef> targetNodePair = getNodePairNotNull(targetRef);
|
|
long targetNodeId = targetNodePair.getFirst();
|
|
|
|
// we are sure that the association doesn't exist - make it
|
|
Pair<Long, AssociationRef> assocPair = nodeDaoService.newNodeAssoc(sourceNodeId, targetNodeId, assocTypeQName);
|
|
AssociationRef assocRef = assocPair.getSecond();
|
|
|
|
// Invoke policy behaviours
|
|
invokeOnCreateAssociation(assocRef);
|
|
|
|
// Add missing aspects
|
|
addMissingAspects(sourceNodePair, assocTypeQName);
|
|
|
|
return assocRef;
|
|
}
|
|
|
|
public Collection<ChildAssociationRef> getChildAssocsWithoutParentAssocsOfType(NodeRef parent, QName assocTypeQName)
|
|
{
|
|
// Get the parent node
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(parent);
|
|
Long parentNodeId = nodePair.getFirst();
|
|
|
|
final List<ChildAssociationRef> results = new ArrayList<ChildAssociationRef>(100);
|
|
|
|
NodeDaoService.ChildAssocRefQueryCallback callback = new NodeDaoService.ChildAssocRefQueryCallback()
|
|
{
|
|
public boolean handle(Pair<Long, ChildAssociationRef> childAssocPair, Pair<Long, NodeRef> parentNodePair,
|
|
Pair<Long, NodeRef> childNodePair)
|
|
{
|
|
results.add(childAssocPair.getSecond());
|
|
return true;
|
|
}
|
|
};
|
|
|
|
// Get the child associations that meet the criteria
|
|
nodeDaoService.getChildAssocsWithoutParentAssocsOfType(parentNodeId, assocTypeQName, callback);
|
|
|
|
// done
|
|
return results;
|
|
}
|
|
|
|
public void removeAssociation(NodeRef sourceRef, NodeRef targetRef, QName assocTypeQName)
|
|
throws InvalidNodeRefException
|
|
{
|
|
Pair<Long, NodeRef> sourceNodePair = getNodePairNotNull(sourceRef);
|
|
long sourceNodeId = sourceNodePair.getFirst();
|
|
Pair<Long, NodeRef> targetNodePair = getNodePairNotNull(targetRef);
|
|
long targetNodeId = targetNodePair.getFirst();
|
|
|
|
// get the association
|
|
Pair<Long, AssociationRef> assocPair = nodeDaoService.getNodeAssoc(sourceNodeId, targetNodeId, assocTypeQName);
|
|
if (assocPair == null)
|
|
{
|
|
// nothing to remove
|
|
return;
|
|
}
|
|
AssociationRef assocRef = assocPair.getSecond();
|
|
|
|
// delete it
|
|
nodeDaoService.deleteNodeAssoc(assocPair.getFirst());
|
|
|
|
// Invoke policy behaviours
|
|
invokeOnDeleteAssociation(assocRef);
|
|
}
|
|
|
|
public List<AssociationRef> getTargetAssocs(NodeRef sourceRef, QNamePattern qnamePattern)
|
|
{
|
|
Pair<Long, NodeRef> sourceNodePair = getNodePairNotNull(sourceRef);
|
|
long sourceNodeId = sourceNodePair.getFirst();
|
|
|
|
// get all assocs to target
|
|
Collection<Pair<Long, AssociationRef>> assocPairs = nodeDaoService.getTargetNodeAssocs(sourceNodeId);
|
|
List<AssociationRef> nodeAssocRefs = new ArrayList<AssociationRef>(assocPairs.size());
|
|
for (Pair<Long, AssociationRef> assocPair : assocPairs)
|
|
{
|
|
AssociationRef assocRef = assocPair.getSecond();
|
|
// check qname pattern
|
|
if (!qnamePattern.isMatch(assocRef.getTypeQName()))
|
|
{
|
|
continue; // the assoc name doesn't match the pattern given
|
|
}
|
|
nodeAssocRefs.add(assocRef);
|
|
}
|
|
// done
|
|
return nodeAssocRefs;
|
|
}
|
|
|
|
public List<AssociationRef> getSourceAssocs(NodeRef targetRef, QNamePattern qnamePattern)
|
|
{
|
|
Pair<Long, NodeRef> targetNodePair = getNodePairNotNull(targetRef);
|
|
long targetNodeId = targetNodePair.getFirst();
|
|
|
|
// get all assocs to target
|
|
Collection<Pair<Long, AssociationRef>> assocPairs = nodeDaoService.getSourceNodeAssocs(targetNodeId);
|
|
List<AssociationRef> nodeAssocRefs = new ArrayList<AssociationRef>(assocPairs.size());
|
|
for (Pair<Long, AssociationRef> assocPair : assocPairs)
|
|
{
|
|
AssociationRef assocRef = assocPair.getSecond();
|
|
// check qname pattern
|
|
if (!qnamePattern.isMatch(assocRef.getTypeQName()))
|
|
{
|
|
continue; // the assoc name doesn't match the pattern given
|
|
}
|
|
nodeAssocRefs.add(assocRef);
|
|
}
|
|
// done
|
|
return nodeAssocRefs;
|
|
}
|
|
|
|
/**
|
|
* Recursive method used to build up paths from a given node to the root.
|
|
* <p>
|
|
* Whilst walking up the hierarchy to the root, some nodes may have a <b>root</b> aspect.
|
|
* Everytime one of these is encountered, a new path is farmed off, but the method
|
|
* continues to walk up the hierarchy.
|
|
*
|
|
* @param currentNode the node to start from, i.e. the child node to work upwards from
|
|
* @param currentPath the path from the current node to the descendent that we started from
|
|
* @param completedPaths paths that have reached the root are added to this collection
|
|
* @param assocStack the parent-child relationships traversed whilst building the path.
|
|
* Used to detected cyclic relationships.
|
|
* @param primaryOnly true if only the primary parent association must be traversed.
|
|
* If this is true, then the only root is the top level node having no parents.
|
|
* @throws CyclicChildRelationshipException
|
|
*/
|
|
private void prependPaths(
|
|
Pair<Long, NodeRef> currentNodePair,
|
|
Pair<StoreRef, NodeRef> currentRootNodePair,
|
|
Path currentPath,
|
|
Collection<Path> completedPaths,
|
|
Stack<Long> assocIdStack,
|
|
boolean primaryOnly)
|
|
throws CyclicChildRelationshipException
|
|
{
|
|
Long currentNodeId = currentNodePair.getFirst();
|
|
NodeRef currentNodeRef = currentNodePair.getSecond();
|
|
|
|
// Check if we have changed root nodes
|
|
StoreRef currentStoreRef = currentNodeRef.getStoreRef();
|
|
if (currentRootNodePair == null || !currentStoreRef.equals(currentRootNodePair.getFirst()))
|
|
{
|
|
// We've changed stores
|
|
Pair<Long, NodeRef> rootNodePair = nodeDaoService.getRootNode(currentStoreRef);
|
|
currentRootNodePair = new Pair<StoreRef, NodeRef>(currentStoreRef, rootNodePair.getSecond());
|
|
}
|
|
|
|
// get the parent associations of the given node
|
|
Collection<Pair<Long, ChildAssociationRef>> parentAssocPairs = nodeDaoService.getParentAssocs(currentNodeId);
|
|
// does the node have parents
|
|
boolean hasParents = parentAssocPairs.size() > 0;
|
|
// does the current node have a root aspect?
|
|
boolean isRoot = nodeDaoService.hasNodeAspect(currentNodeId, ContentModel.ASPECT_ROOT);
|
|
boolean isStoreRoot = nodeDaoService.getNodeType(currentNodeId).equals(ContentModel.TYPE_STOREROOT);
|
|
|
|
// look for a root. If we only want the primary root, then ignore all but the top-level root.
|
|
if (isRoot && !(primaryOnly && hasParents)) // exclude primary search with parents present
|
|
{
|
|
// create a one-sided assoc ref for the root node and prepend to the stack
|
|
// this effectively spoofs the fact that the current node is not below the root
|
|
// - we put this assoc in as the first assoc in the path must be a one-sided
|
|
// reference pointing to the root node
|
|
ChildAssociationRef assocRef = new ChildAssociationRef(
|
|
null,
|
|
null,
|
|
null,
|
|
currentRootNodePair.getSecond());
|
|
// create a path to save and add the 'root' assoc
|
|
Path pathToSave = new Path();
|
|
Path.ChildAssocElement first = null;
|
|
for (Path.Element element: currentPath)
|
|
{
|
|
if (first == null)
|
|
{
|
|
first = (Path.ChildAssocElement) element;
|
|
}
|
|
else
|
|
{
|
|
pathToSave.append(element);
|
|
}
|
|
}
|
|
if (first != null)
|
|
{
|
|
// mimic an association that would appear if the current node was below the root node
|
|
// or if first beneath the root node it will make the real thing
|
|
ChildAssociationRef updateAssocRef = new ChildAssociationRef(
|
|
isStoreRoot ? ContentModel.ASSOC_CHILDREN : first.getRef().getTypeQName(),
|
|
currentRootNodePair.getSecond(),
|
|
first.getRef().getQName(),
|
|
first.getRef().getChildRef());
|
|
Path.Element newFirst = new Path.ChildAssocElement(updateAssocRef);
|
|
pathToSave.prepend(newFirst);
|
|
}
|
|
|
|
Path.Element element = new Path.ChildAssocElement(assocRef);
|
|
pathToSave.prepend(element);
|
|
|
|
// store the path just built
|
|
completedPaths.add(pathToSave);
|
|
}
|
|
|
|
if (parentAssocPairs.size() == 0 && !isRoot)
|
|
{
|
|
throw new RuntimeException("Node without parents does not have root aspect: " +
|
|
currentNodeRef);
|
|
}
|
|
// walk up each parent association
|
|
for (Pair<Long, ChildAssociationRef> assocPair : parentAssocPairs)
|
|
{
|
|
Long assocId = assocPair.getFirst();
|
|
ChildAssociationRef assocRef = assocPair.getSecond();
|
|
// do we consider only primary assocs?
|
|
if (primaryOnly && !assocRef.isPrimary())
|
|
{
|
|
continue;
|
|
}
|
|
// Ordering is meaningless here as we are constructing a path upwards
|
|
// and have no idea where the node comes in the sibling order or even
|
|
// if there are like-pathed siblings.
|
|
assocRef.setNthSibling(-1);
|
|
// build a path element
|
|
Path.Element element = new Path.ChildAssocElement(assocRef);
|
|
// create a new path that builds on the current path
|
|
Path path = new Path();
|
|
path.append(currentPath);
|
|
// prepend element
|
|
path.prepend(element);
|
|
// get parent node
|
|
NodeRef parentRef = assocRef.getParentRef();
|
|
Pair<Long, NodeRef> parentNodePair = getNodePairNotNull(parentRef);
|
|
// does the association already exist in the stack
|
|
if (assocIdStack.contains(assocId))
|
|
{
|
|
// the association was present already
|
|
throw new CyclicChildRelationshipException(
|
|
"Cyclic parent-child relationship detected: \n" +
|
|
" current node: " + currentNodeId + "\n" +
|
|
" current path: " + currentPath + "\n" +
|
|
" next assoc: " + assocId,
|
|
assocRef);
|
|
}
|
|
|
|
// push the assoc stack, recurse and pop
|
|
assocIdStack.push(assocId);
|
|
prependPaths(parentNodePair, currentRootNodePair, path, completedPaths, assocIdStack, primaryOnly);
|
|
assocIdStack.pop();
|
|
}
|
|
// done
|
|
}
|
|
|
|
/**
|
|
* @see #getPaths(NodeRef, boolean)
|
|
* @see #prependPaths(Node, Path, Collection, Stack, boolean)
|
|
*/
|
|
public Path getPath(NodeRef nodeRef) throws InvalidNodeRefException
|
|
{
|
|
List<Path> paths = getPaths(nodeRef, true); // checks primary path count
|
|
if (paths.size() == 1)
|
|
{
|
|
return paths.get(0); // we know there is only one
|
|
}
|
|
throw new RuntimeException("Primary path count not checked"); // checked by getPaths()
|
|
}
|
|
|
|
/**
|
|
* When searching for <code>primaryOnly == true</code>, checks that there is exactly
|
|
* one path.
|
|
* @see #prependPaths(Node, Path, Collection, Stack, boolean)
|
|
*/
|
|
public List<Path> getPaths(NodeRef nodeRef, boolean primaryOnly) throws InvalidNodeRefException
|
|
{
|
|
// get the starting node
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
// create storage for the paths - only need 1 bucket if we are looking for the primary path
|
|
List<Path> paths = new ArrayList<Path>(primaryOnly ? 1 : 10);
|
|
// create an empty current path to start from
|
|
Path currentPath = new Path();
|
|
// create storage for touched associations
|
|
Stack<Long> assocIdStack = new Stack<Long>();
|
|
// call recursive method to sort it out
|
|
prependPaths(nodePair, null, currentPath, paths, assocIdStack, primaryOnly);
|
|
|
|
// check that for the primary only case we have exactly one path
|
|
if (primaryOnly && paths.size() != 1)
|
|
{
|
|
throw new RuntimeException("Node has " + paths.size() + " primary paths: " + nodeRef);
|
|
}
|
|
|
|
// done
|
|
if (loggerPaths.isDebugEnabled())
|
|
{
|
|
StringBuilder sb = new StringBuilder(256);
|
|
if (primaryOnly)
|
|
{
|
|
sb.append("Primary paths");
|
|
}
|
|
else
|
|
{
|
|
sb.append("Paths");
|
|
}
|
|
sb.append(" for node ").append(nodeRef);
|
|
for (Path path : paths)
|
|
{
|
|
sb.append("\n").append(" ").append(path);
|
|
}
|
|
loggerPaths.debug(sb);
|
|
}
|
|
return paths;
|
|
}
|
|
|
|
private void archiveNode(NodeRef nodeRef, StoreRef archiveStoreRef)
|
|
{
|
|
Pair<Long, NodeRef> nodePair = getNodePairNotNull(nodeRef);
|
|
Long nodeId = nodePair.getFirst();
|
|
Pair<Long, ChildAssociationRef> primaryParentAssocPair = nodeDaoService.getPrimaryParentAssoc(nodeId);
|
|
Set<QName> newAspects = new HashSet<QName>(5);
|
|
Map<QName, Serializable> existingProperties = nodeDaoService.getNodeProperties(nodeId);
|
|
Map<QName, Serializable> newProperties = new HashMap<QName, Serializable>(11);
|
|
|
|
// add the aspect
|
|
newAspects.add(ContentModel.ASPECT_ARCHIVED);
|
|
newProperties.put(ContentModel.PROP_ARCHIVED_BY, AuthenticationUtil.getFullyAuthenticatedUser());
|
|
newProperties.put(ContentModel.PROP_ARCHIVED_DATE, new Date());
|
|
newProperties.put(ContentModel.PROP_ARCHIVED_ORIGINAL_PARENT_ASSOC, primaryParentAssocPair.getSecond());
|
|
Serializable originalOwner = existingProperties.get(ContentModel.PROP_OWNER);
|
|
Serializable originalCreator = existingProperties.get(ContentModel.PROP_CREATOR);
|
|
if (originalOwner != null || originalCreator != null)
|
|
{
|
|
newProperties.put(
|
|
ContentModel.PROP_ARCHIVED_ORIGINAL_OWNER,
|
|
originalOwner != null ? originalOwner : originalCreator);
|
|
}
|
|
|
|
// change the node ownership
|
|
newAspects.add(ContentModel.ASPECT_OWNABLE);
|
|
newProperties.put(ContentModel.PROP_OWNER, AuthenticationUtil.getFullyAuthenticatedUser());
|
|
|
|
// Set the aspects and properties
|
|
nodeDaoService.addNodeProperties(nodeId, newProperties);
|
|
nodeDaoService.addNodeAspects(nodeId, newAspects);
|
|
|
|
// move the node
|
|
Pair<Long, NodeRef> archiveStoreRootNodePair = nodeDaoService.getRootNode(archiveStoreRef);
|
|
moveNode(
|
|
nodeRef,
|
|
archiveStoreRootNodePair.getSecond(),
|
|
ContentModel.ASSOC_CHILDREN,
|
|
QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "archivedItem"));
|
|
}
|
|
|
|
public NodeRef restoreNode(NodeRef archivedNodeRef, NodeRef destinationParentNodeRef, QName assocTypeQName, QName assocQName)
|
|
{
|
|
Pair<Long, NodeRef> archivedNodePair = getNodePairNotNull(archivedNodeRef);
|
|
Long archivedNodeId = archivedNodePair.getFirst();
|
|
Set<QName> existingAspects = nodeDaoService.getNodeAspects(archivedNodeId);
|
|
Set<QName> newAspects = new HashSet<QName>(5);
|
|
Map<QName, Serializable> existingProperties = nodeDaoService.getNodeProperties(archivedNodeId);
|
|
Map<QName, Serializable> newProperties = new HashMap<QName, Serializable>(11);
|
|
|
|
// the node must be a top-level archive node
|
|
if (!existingAspects.contains(ContentModel.ASPECT_ARCHIVED))
|
|
{
|
|
throw new AlfrescoRuntimeException("The node to restore is not an archive node");
|
|
}
|
|
ChildAssociationRef originalPrimaryParentAssocRef = (ChildAssociationRef) existingProperties.get(
|
|
ContentModel.PROP_ARCHIVED_ORIGINAL_PARENT_ASSOC);
|
|
Serializable originalOwner = existingProperties.get(ContentModel.PROP_ARCHIVED_ORIGINAL_OWNER);
|
|
// remove the archived aspect
|
|
Set<QName> removePropertyQNames = new HashSet<QName>(11);
|
|
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);
|
|
nodeDaoService.removeNodeProperties(archivedNodeId, removePropertyQNames);
|
|
nodeDaoService.removeNodeAspects(archivedNodeId, Collections.singleton(ContentModel.ASPECT_ARCHIVED));
|
|
|
|
// restore the original ownership
|
|
if (originalOwner != null)
|
|
{
|
|
newAspects.add(ContentModel.ASPECT_OWNABLE);
|
|
newProperties.put(ContentModel.PROP_OWNER, originalOwner);
|
|
}
|
|
|
|
if (destinationParentNodeRef == null)
|
|
{
|
|
// we must restore to the original location
|
|
destinationParentNodeRef = originalPrimaryParentAssocRef.getParentRef();
|
|
}
|
|
// check the associations
|
|
if (assocTypeQName == null)
|
|
{
|
|
assocTypeQName = originalPrimaryParentAssocRef.getTypeQName();
|
|
}
|
|
if (assocQName == null)
|
|
{
|
|
assocQName = originalPrimaryParentAssocRef.getQName();
|
|
}
|
|
|
|
// move the node to the target parent, which may or may not be the original parent
|
|
ChildAssociationRef newChildAssocRef = moveNode(
|
|
archivedNodeRef,
|
|
destinationParentNodeRef,
|
|
assocTypeQName,
|
|
assocQName);
|
|
|
|
// the node reference has changed due to the store move
|
|
NodeRef restoredNodeRef = newChildAssocRef.getChildRef();
|
|
|
|
// done
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("Restored node: \n" +
|
|
" original noderef: " + archivedNodeRef + "\n" +
|
|
" restored noderef: " + restoredNodeRef + "\n" +
|
|
" new parent: " + destinationParentNodeRef);
|
|
}
|
|
return restoredNodeRef;
|
|
}
|
|
|
|
/**
|
|
* Drops the old primary association and creates a new one
|
|
*/
|
|
public ChildAssociationRef moveNode(
|
|
NodeRef nodeToMoveRef,
|
|
NodeRef newParentRef,
|
|
QName assocTypeQName,
|
|
QName assocQName)
|
|
{
|
|
Pair<Long, NodeRef> nodeToMovePair = getNodePairNotNull(nodeToMoveRef);
|
|
Pair<Long, NodeRef> parentNodePair = getNodePairNotNull(newParentRef);
|
|
|
|
Long nodeToMoveId = nodeToMovePair.getFirst();
|
|
QName nodeToMoveTypeQName = nodeDaoService.getNodeType(nodeToMoveId);
|
|
NodeRef oldNodeToMoveRef = nodeToMovePair.getSecond();
|
|
Long parentNodeId = parentNodePair.getFirst();
|
|
NodeRef parentNodeRef = parentNodePair.getSecond();
|
|
StoreRef oldStoreRef = oldNodeToMoveRef.getStoreRef();
|
|
StoreRef newStoreRef = parentNodeRef.getStoreRef();
|
|
NodeRef newNodeToMoveRef = new NodeRef(newStoreRef, oldNodeToMoveRef.getId());
|
|
Pair<Long, NodeRef> newNodeToMovePair = new Pair<Long, NodeRef>(nodeToMoveId, newNodeToMoveRef);
|
|
|
|
// Get the primary parent association
|
|
Pair<Long, ChildAssociationRef> oldParentAssocPair = nodeDaoService.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.");
|
|
}
|
|
Long oldParentAssocId = oldParentAssocPair.getFirst();
|
|
ChildAssociationRef oldParentAssocRef = oldParentAssocPair.getSecond();
|
|
|
|
// Shortcut this whole process if nothing has changed
|
|
if (EqualsHelper.nullSafeEquals(oldParentAssocRef.getParentRef(), newParentRef) &&
|
|
EqualsHelper.nullSafeEquals(oldParentAssocRef.getTypeQName(), assocTypeQName) &&
|
|
EqualsHelper.nullSafeEquals(oldParentAssocRef.getQName(), assocQName))
|
|
{
|
|
// It's all just the same
|
|
return oldParentAssocRef;
|
|
}
|
|
|
|
boolean movingStore = !oldStoreRef.equals(newStoreRef);
|
|
// Handle store conflicts
|
|
if (movingStore)
|
|
{
|
|
handleStoreMoveConflicts(nodeToMovePair, newStoreRef);
|
|
}
|
|
|
|
// Invoke policy behaviour
|
|
if (movingStore)
|
|
{
|
|
invokeBeforeDeleteNode(nodeToMoveRef);
|
|
invokeBeforeCreateNode(newParentRef, assocTypeQName, assocQName, nodeToMoveTypeQName);
|
|
}
|
|
else
|
|
{
|
|
invokeBeforeDeleteChildAssociation(oldParentAssocRef);
|
|
invokeBeforeCreateChildAssociation(newParentRef, nodeToMoveRef, assocTypeQName, assocQName, false);
|
|
}
|
|
|
|
// Handle store moves
|
|
if (movingStore)
|
|
{
|
|
Pair<Long, NodeRef> newNodePair = nodeDaoService.moveNodeToStore(nodeToMoveId, newStoreRef);
|
|
if (!newNodePair.equals(newNodeToMovePair))
|
|
{
|
|
throw new RuntimeException("Store-moved pair isn't expected: " + newNodePair + " != " + newNodeToMovePair);
|
|
}
|
|
}
|
|
|
|
// Get the new node's cm:name
|
|
Map<QName, Serializable> newNodeProperties = nodeDaoService.getNodeProperties(nodeToMoveId);
|
|
String newNodeChildName = extractNameProperty(newNodeProperties);
|
|
// Modify the association directly. We do this AFTER the change of the node's store so that
|
|
// the association reference returned is correct.
|
|
Pair<Long, ChildAssociationRef> newParentAssocPair = nodeDaoService.updateChildAssoc(
|
|
oldParentAssocId,
|
|
parentNodeId,
|
|
nodeToMoveId,
|
|
assocTypeQName,
|
|
assocQName,
|
|
-1,
|
|
newNodeChildName);
|
|
ChildAssociationRef newParentAssocRef = newParentAssocPair.getSecond();
|
|
|
|
// Handle indexing differently if it is a store move
|
|
if (movingStore)
|
|
{
|
|
// The association existed before and the node is moving to a new store
|
|
nodeIndexer.indexDeleteNode(oldParentAssocRef);
|
|
nodeIndexer.indexCreateNode(newParentAssocRef);
|
|
}
|
|
else
|
|
{
|
|
// The node is in the same store and is just having it's child association modified
|
|
nodeIndexer.indexUpdateChildAssociation(oldParentAssocRef, newParentAssocRef);
|
|
}
|
|
|
|
// Ensure name uniqueness
|
|
setChildNameUnique(newParentAssocPair, newNodeToMovePair);
|
|
|
|
// Check that there is not a cyclic relationship
|
|
getPaths(newNodeToMoveRef, false);
|
|
|
|
// Call behaviours
|
|
if (movingStore)
|
|
{
|
|
Set<QName> nodeToMoveAspectQNames = nodeDaoService.getNodeAspects(nodeToMoveId);
|
|
// The Node changes NodeRefs, so this is really the deletion of the old node and creation
|
|
// of a node in a new store as far as the clients are concerned.
|
|
invokeOnDeleteNode(oldParentAssocRef, nodeToMoveTypeQName, nodeToMoveAspectQNames, true);
|
|
invokeOnCreateNode(newParentAssocRef);
|
|
}
|
|
else
|
|
{
|
|
invokeOnCreateChildAssociation(newParentAssocRef, false);
|
|
invokeOnDeleteChildAssociation(oldParentAssocRef);
|
|
invokeOnMoveNode(oldParentAssocRef, newParentAssocRef);
|
|
}
|
|
|
|
// If we have to cascade in the transaction, then pull the children over to the new store
|
|
if (cascadeInTransaction)
|
|
{
|
|
// Pull children to the new store
|
|
pullNodeChildrenToSameStore(newNodeToMovePair, true, true);
|
|
}
|
|
|
|
// Done
|
|
return newParentAssocRef;
|
|
}
|
|
|
|
/**
|
|
* Silently gives any clashing target nodes a new UUID
|
|
* @param nodeToMovePair the node that will be moved
|
|
* @param newStoreRef the store that the node will be moved to
|
|
*/
|
|
private void handleStoreMoveConflicts(Pair<Long, NodeRef> nodeToMovePair, StoreRef newStoreRef)
|
|
{
|
|
NodeRef oldNodeToMoveRef = nodeToMovePair.getSecond();
|
|
NodeRef newNodeToMoveRef = new NodeRef(newStoreRef, oldNodeToMoveRef.getId());
|
|
// If the new node reference is already taken, then give it a new uuid
|
|
Pair<Long, NodeRef> conflictingNodePair = nodeDaoService.getNodePair(newNodeToMoveRef);
|
|
if (conflictingNodePair != null)
|
|
{
|
|
// We are creating a new node. This noderef will be reused, so will be an update
|
|
nodeDaoService.updateNode(conflictingNodePair.getFirst(), null, GUID.generate(), null);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This process is less invasive than the <b>move</b> method as the child associations
|
|
* do not need to be remade. If the children are in the same store, only the <code>indexChildren</code>
|
|
* value is needed.
|
|
*/
|
|
private void pullNodeChildrenToSameStore(Pair<Long, NodeRef> nodePair, boolean cascade, boolean indexChildren)
|
|
{
|
|
Long nodeId = nodePair.getFirst();
|
|
NodeRef nodeRef = nodePair.getSecond();
|
|
StoreRef storeRef = nodeRef.getStoreRef();
|
|
// Get the node's children, but only one's that aren't in the same store
|
|
final List<Pair<Long, NodeRef>> childNodePairs = new ArrayList<Pair<Long, NodeRef>>(5);
|
|
NodeDaoService.ChildAssocRefQueryCallback callback = new NodeDaoService.ChildAssocRefQueryCallback()
|
|
{
|
|
public boolean handle(
|
|
Pair<Long, ChildAssociationRef> childAssocPair,
|
|
Pair<Long, NodeRef> parentNodePair,
|
|
Pair<Long, NodeRef> childNodePair
|
|
)
|
|
{
|
|
// Add it
|
|
childNodePairs.add(childNodePair);
|
|
return false;
|
|
}
|
|
};
|
|
// We only need to move child nodes that are not already in the same store
|
|
nodeDaoService.getPrimaryChildAssocsNotInSameStore(nodeId, callback);
|
|
// Each child must be moved to the same store as the parent
|
|
for (Pair<Long, NodeRef> oldChildNodePair : childNodePairs)
|
|
{
|
|
Long childNodeId = oldChildNodePair.getFirst();
|
|
NodeRef childNodeRef = oldChildNodePair.getSecond();
|
|
QName childNodeTypeQName = nodeDaoService.getNodeType(childNodeId);
|
|
Set<QName> childNodeAspectQNames = nodeDaoService.getNodeAspects(childNodeId);
|
|
Pair<Long, ChildAssociationRef> oldParentAssocPair = nodeDaoService.getPrimaryParentAssoc(childNodeId);
|
|
Pair<Long, NodeRef> newChildNodePair = oldChildNodePair;
|
|
Pair<Long, ChildAssociationRef> newParentAssocPair = oldParentAssocPair;
|
|
ChildAssociationRef newParentAssocRef = newParentAssocPair.getSecond();
|
|
// Fire node policies. This ensures that each node in the hierarchy gets a notification fired.
|
|
invokeBeforeDeleteNode(childNodeRef);
|
|
invokeBeforeCreateNode(
|
|
newParentAssocRef.getParentRef(),
|
|
newParentAssocRef.getTypeQName(),
|
|
newParentAssocRef.getQName(),
|
|
childNodeTypeQName);
|
|
// Move the node
|
|
handleStoreMoveConflicts(oldChildNodePair, storeRef);
|
|
// Change the store
|
|
newChildNodePair = nodeDaoService.moveNodeToStore(oldChildNodePair.getFirst(), storeRef);
|
|
// Get the new parent assoc
|
|
newParentAssocPair = nodeDaoService.getPrimaryParentAssoc(childNodeId);
|
|
// Index
|
|
if (indexChildren)
|
|
{
|
|
nodeIndexer.indexDeleteNode(oldParentAssocPair.getSecond());
|
|
nodeIndexer.indexCreateNode(newParentAssocPair.getSecond());
|
|
}
|
|
else
|
|
{
|
|
// The node we have just moved doesn't have it's children indexed, so tag it
|
|
nodeDaoService.addNodeAspects(childNodeId, Collections.singleton(ContentModel.ASPECT_INDEX_CHILDREN));
|
|
}
|
|
// Fire node policies. This ensures that each node in the hierarchy gets a notification fired.
|
|
invokeOnDeleteNode(oldParentAssocPair.getSecond(), childNodeTypeQName, childNodeAspectQNames, true);
|
|
invokeOnCreateNode(newParentAssocPair.getSecond());
|
|
// Cascade, if required
|
|
if (cascade)
|
|
{
|
|
pullNodeChildrenToSameStore(newChildNodePair, cascade, indexChildren);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void indexChildren(Pair<Long, NodeRef> nodePair, boolean cascade)
|
|
{
|
|
Long nodeId = nodePair.getFirst();
|
|
// Get the node's children, but only one's that aren't in the same store
|
|
final List<Pair<Long, NodeRef>> childNodePairs = new ArrayList<Pair<Long, NodeRef>>(5);
|
|
NodeDaoService.ChildAssocRefQueryCallback callback = new NodeDaoService.ChildAssocRefQueryCallback()
|
|
{
|
|
public boolean handle(
|
|
Pair<Long, ChildAssociationRef> childAssocPair,
|
|
Pair<Long, NodeRef> parentNodePair,
|
|
Pair<Long, NodeRef> childNodePair
|
|
)
|
|
{
|
|
// Add it
|
|
childNodePairs.add(childNodePair);
|
|
return false;
|
|
}
|
|
};
|
|
nodeDaoService.getPrimaryChildAssocs(nodeId, callback);
|
|
// Each child must be moved to the same store as the parent
|
|
for (Pair<Long, NodeRef> oldChildNodePair : childNodePairs)
|
|
{
|
|
Long childNodeId = oldChildNodePair.getFirst();
|
|
NodeRef oldChildNodeRef = oldChildNodePair.getSecond();
|
|
Pair<Long, NodeRef> newChildNodePair = oldChildNodePair;
|
|
// Touch the node child node so that index tracking will work
|
|
nodeDaoService.setNodeStatus(childNodeId);
|
|
// Index
|
|
nodeIndexer.indexUpdateNode(oldChildNodeRef);
|
|
// Cascade, if required
|
|
if (cascade)
|
|
{
|
|
indexChildren(newChildNodePair, cascade);
|
|
}
|
|
else
|
|
{
|
|
// We didn't cascade to the children, so tag the node to index the children later
|
|
nodeDaoService.addNodeAspects(childNodeId, Collections.singleton(ContentModel.ASPECT_INDEX_CHILDREN));
|
|
}
|
|
}
|
|
// We have indexed the children, so remove the tagging aspect
|
|
nodeDaoService.removeNodeAspects(nodeId, Collections.singleton(ContentModel.ASPECT_INDEX_CHILDREN));
|
|
}
|
|
|
|
public NodeRef getStoreArchiveNode(StoreRef storeRef)
|
|
{
|
|
StoreRef archiveStoreRef = storeArchiveMap.get(storeRef);
|
|
if (archiveStoreRef == null)
|
|
{
|
|
// no mapping for the given store
|
|
return null;
|
|
}
|
|
else
|
|
{
|
|
return getRootNode(archiveStoreRef);
|
|
}
|
|
}
|
|
|
|
private String extractNameProperty(Map<QName, Serializable> properties)
|
|
{
|
|
Serializable nameValue = properties.get(ContentModel.PROP_NAME);
|
|
String name = (String) DefaultTypeConverter.INSTANCE.convert(String.class, nameValue);
|
|
return name;
|
|
}
|
|
|
|
private void setChildNameUnique(Pair<Long, ChildAssociationRef> childAssocPair, Pair<Long, NodeRef> childNodePair)
|
|
{
|
|
// Get the node's existing name
|
|
Serializable nameValue = nodeDaoService.getNodeProperty(childNodePair.getFirst(), ContentModel.PROP_NAME);
|
|
String name = (String) DefaultTypeConverter.INSTANCE.convert(String.class, nameValue);
|
|
setChildNameUnique(childAssocPair, name, null);
|
|
}
|
|
|
|
/**
|
|
* Ensures name uniqueness for the child and the child association. Note that nothing is done if the
|
|
* association type doesn't enforce name uniqueness.
|
|
*/
|
|
private void setChildNameUnique(Pair<Long, ChildAssociationRef> childAssocPair, String newName, String oldName)
|
|
{
|
|
if (childAssocPair == null)
|
|
{
|
|
// This happens if the node is a root node
|
|
return;
|
|
}
|
|
else if (EqualsHelper.nullSafeEquals(newName, oldName))
|
|
{
|
|
// The name has not changed
|
|
return;
|
|
}
|
|
Long assocId = childAssocPair.getFirst();
|
|
QName assocTypeQName = childAssocPair.getSecond().getTypeQName();
|
|
AssociationDefinition assocDef = dictionaryService.getAssociation(assocTypeQName);
|
|
if (!assocDef.isChild())
|
|
{
|
|
throw new IllegalArgumentException("Child association has non-child type: " + assocId);
|
|
}
|
|
ChildAssociationDefinition childAssocDef = (ChildAssociationDefinition) assocDef;
|
|
if (!childAssocDef.getDuplicateChildNamesAllowed())
|
|
{
|
|
nodeDaoService.setChildNameUnique(assocId, newName);
|
|
}
|
|
}
|
|
|
|
public static class MoveChildrenToCorrectStore extends AbstractNodeCleanupWorker
|
|
{
|
|
@Override
|
|
protected List<String> doCleanInternal() throws Throwable
|
|
{
|
|
return dbNodeService.moveChildrenToCorrectStore();
|
|
}
|
|
};
|
|
|
|
private List<String> moveChildrenToCorrectStore()
|
|
{
|
|
List<String> results = new ArrayList<String>(1000);
|
|
// Repeat the process for each store
|
|
List<Pair<Long, StoreRef>> storePairs = nodeDaoService.getStores();
|
|
for (Pair<Long, StoreRef> storePair : storePairs)
|
|
{
|
|
List<String> storeResults = moveChildrenToCorrectStore(storePair.getFirst());
|
|
results.addAll(storeResults);
|
|
}
|
|
return results;
|
|
}
|
|
|
|
private List<String> moveChildrenToCorrectStore(final Long storeId)
|
|
{
|
|
final List<Pair<Long, NodeRef>> parentNodePairs = new ArrayList<Pair<Long, NodeRef>>(100);
|
|
final NodeRefQueryCallback callback = new NodeRefQueryCallback()
|
|
{
|
|
public boolean handle(Pair<Long, NodeRef> nodePair)
|
|
{
|
|
parentNodePairs.add(nodePair);
|
|
return true;
|
|
}
|
|
};
|
|
RetryingTransactionCallback<Object> getNodesCallback = new RetryingTransactionCallback<Object>()
|
|
{
|
|
public Object execute() throws Throwable
|
|
{
|
|
nodeDaoService.getNodesWithChildrenInDifferentStore(storeId, Long.MIN_VALUE, 100, callback);
|
|
// Done
|
|
return null;
|
|
}
|
|
};
|
|
transactionService.getRetryingTransactionHelper().doInTransaction(getNodesCallback, true, true);
|
|
// Process the nodes in random order
|
|
Collections.shuffle(parentNodePairs);
|
|
// Iterate and operate
|
|
List<String> results = new ArrayList<String>(100);
|
|
for (final Pair<Long, NodeRef> parentNodePair : parentNodePairs)
|
|
{
|
|
RetryingTransactionCallback<String> fixNodesCallback = new RetryingTransactionCallback<String>()
|
|
{
|
|
public String execute() throws Throwable
|
|
{
|
|
// Pull the children to the same store with full indexing - but don't cascade.
|
|
pullNodeChildrenToSameStore(parentNodePair, true, true);
|
|
// Done
|
|
return null;
|
|
}
|
|
};
|
|
RetryingTransactionHelper txnHelper = transactionService.getRetryingTransactionHelper();
|
|
txnHelper.setMaxRetries(1);
|
|
try
|
|
{
|
|
txnHelper.doInTransaction(fixNodesCallback, false, true);
|
|
String msg =
|
|
"Moved child nodes to parent node's store: \n" +
|
|
" Parent node: " + parentNodePair.getFirst();
|
|
results.add(msg);
|
|
}
|
|
catch (Throwable e)
|
|
{
|
|
String msg =
|
|
"Failed to move child nodes to parent node's store." +
|
|
" Set log level to WARN for this class to get exception log: \n" +
|
|
" Parent node: " + parentNodePair.getFirst() + "\n" +
|
|
" Error: " + e.getMessage();
|
|
// It failed; do a full log in WARN mode
|
|
if (logger.isWarnEnabled())
|
|
{
|
|
logger.warn(msg, e);
|
|
}
|
|
else
|
|
{
|
|
logger.error(msg);
|
|
}
|
|
results.add(msg);
|
|
}
|
|
}
|
|
// Done
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
StringBuilder sb = new StringBuilder(256);
|
|
sb.append("Moved children to correct stores: \n")
|
|
.append(" Results:\n");
|
|
for (String msg : results)
|
|
{
|
|
sb.append(" ").append(msg).append("\n");
|
|
}
|
|
logger.debug(sb.toString());
|
|
}
|
|
return results;
|
|
}
|
|
}
|