diff --git a/repository/src/main/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java b/repository/src/main/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java
index 9b223788c5..41156e9734 100644
--- a/repository/src/main/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java
+++ b/repository/src/main/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java
@@ -1,5113 +1,5159 @@
-/*
- * #%L
- * Alfresco Repository
- * %%
- * Copyright (C) 2005 - 2023 Alfresco Software Limited
- * %%
- * This file is part of the Alfresco software.
- * If the software was purchased under a paid Alfresco license, the terms of
- * the paid license agreement will prevail. Otherwise, the software is
- * provided under the following open source license terms:
- *
- * Alfresco is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Alfresco is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with Alfresco. If not, see .
- * #L%
- */
-package org.alfresco.repo.domain.node;
-
-import java.io.Serializable;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-import java.sql.Savepoint;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
-import java.util.SortedSet;
-import java.util.Stack;
-import java.util.TreeSet;
-import java.util.concurrent.locks.ReadWriteLock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
-
-import org.alfresco.error.AlfrescoRuntimeException;
-import org.alfresco.ibatis.BatchingDAO;
-import org.alfresco.ibatis.RetryingCallbackHelper;
-import org.alfresco.ibatis.RetryingCallbackHelper.RetryingCallback;
-import org.alfresco.model.ContentModel;
-import org.alfresco.repo.cache.NullCache;
-import org.alfresco.repo.cache.SimpleCache;
-import org.alfresco.repo.cache.TransactionalCache;
-import org.alfresco.repo.cache.lookup.EntityLookupCache;
-import org.alfresco.repo.cache.lookup.EntityLookupCache.EntityLookupCallbackDAOAdaptor;
-import org.alfresco.repo.domain.contentdata.ContentDataDAO;
-import org.alfresco.repo.domain.control.ControlDAO;
-import org.alfresco.repo.domain.locale.LocaleDAO;
-import org.alfresco.repo.domain.permissions.AccessControlListDAO;
-import org.alfresco.repo.domain.permissions.AclDAO;
-import org.alfresco.repo.domain.qname.QNameDAO;
-import org.alfresco.repo.domain.usage.UsageDAO;
-import org.alfresco.repo.policy.BehaviourFilter;
-import org.alfresco.repo.security.permissions.AccessControlListProperties;
-import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
-import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
-import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
-import org.alfresco.repo.transaction.TransactionAwareSingleton;
-import org.alfresco.repo.transaction.TransactionalDao;
-import org.alfresco.repo.transaction.TransactionalResourceHelper;
-import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
-import org.alfresco.service.cmr.dictionary.DictionaryService;
-import org.alfresco.service.cmr.dictionary.InvalidTypeException;
-import org.alfresco.service.cmr.dictionary.PropertyDefinition;
-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.ContentData;
-import org.alfresco.service.cmr.repository.CyclicChildRelationshipException;
-import org.alfresco.service.cmr.repository.DuplicateChildNodeNameException;
-import org.alfresco.service.cmr.repository.InvalidNodeRefException;
-import org.alfresco.service.cmr.repository.InvalidStoreRefException;
-import org.alfresco.service.cmr.repository.NodeRef;
-import org.alfresco.service.cmr.repository.NodeRef.Status;
-import org.alfresco.service.cmr.repository.Path;
-import org.alfresco.service.cmr.repository.StoreRef;
-import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
-import org.alfresco.service.namespace.QName;
-import org.alfresco.service.transaction.ReadOnlyServerException;
-import org.alfresco.service.transaction.TransactionService;
-import org.alfresco.util.EqualsHelper;
-import org.alfresco.util.EqualsHelper.MapValueComparison;
-import org.alfresco.util.GUID;
-import org.alfresco.util.Pair;
-import org.alfresco.util.PropertyCheck;
-import org.alfresco.util.ReadWriteLockExecuter;
-import org.alfresco.util.ValueProtectingMap;
-import org.alfresco.util.transaction.TransactionListenerAdapter;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-import org.springframework.dao.ConcurrencyFailureException;
-import org.springframework.dao.DataIntegrityViolationException;
-import org.springframework.util.Assert;
-
-/**
- * Abstract implementation for Node DAO.
- *
- * This provides basic services such as caching, but defers to the underlying implementation
- * for CRUD operations.
- *
- * @author Derek Hulley
- * @since 3.4
- */
-public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
-{
- private static final String CACHE_REGION_ROOT_NODES = "N.RN";
- public static final String CACHE_REGION_NODES = "N.N";
- private static final String CACHE_REGION_ASPECTS = "N.A";
- private static final String CACHE_REGION_PROPERTIES = "N.P";
-
- private static final String KEY_LOST_NODE_PAIRS = AbstractNodeDAOImpl.class.getName() + ".lostNodePairs";
- private static final String KEY_DELETED_ASSOCS = AbstractNodeDAOImpl.class.getName() + ".deletedAssocs";
-
- protected Log logger = LogFactory.getLog(getClass());
- private Log loggerPaths = LogFactory.getLog(getClass().getName() + ".paths");
-
- protected final boolean isDebugEnabled = logger.isDebugEnabled();
- private NodePropertyHelper nodePropertyHelper;
- private UpdateTransactionListener updateTransactionListener = new UpdateTransactionListener();
- private RetryingCallbackHelper childAssocRetryingHelper;
-
- private TransactionService transactionService;
- private DictionaryService dictionaryService;
- private BehaviourFilter policyBehaviourFilter;
- private AclDAO aclDAO;
- private AccessControlListDAO accessControlListDAO;
- private ControlDAO controlDAO;
- private QNameDAO qnameDAO;
- private ContentDataDAO contentDataDAO;
- private LocaleDAO localeDAO;
- private UsageDAO usageDAO;
-
- private int cachingThreshold = 10;
-
- /**
- * Cache for the Store root nodes by StoreRef:
- * KEY: StoreRef
- * VALUE: Node representing the root node
- * VALUE KEY: IGNORED
- */
- private EntityLookupCache rootNodesCache;
-
-
- /**
- * Cache for nodes with the root aspect by StoreRef:
- * KEY: StoreRef
- * VALUE: A set of nodes with the root aspect
- */
- private SimpleCache> allRootNodesCache;
-
- /**
- * Bidirectional cache for the Node ID to Node lookups:
- * KEY: Node ID
- * VALUE: Node
- * VALUE KEY: The Node's NodeRef
- */
- private EntityLookupCache nodesCache;
- /**
- * Backing transactional cache to allow read-through requests to be honoured
- */
- private TransactionalCache nodesTransactionalCache;
- /**
- * Cache for the QName values:
- * KEY: NodeVersionKey
- * VALUE: Set<QName>
- * VALUE KEY: None
- */
- private EntityLookupCache, Serializable> aspectsCache;
- /**
- * Cache for the Node properties:
- * KEY: NodeVersionKey
- * VALUE: Map<QName, Serializable>
- * VALUE KEY: None
- */
- private EntityLookupCache, Serializable> propertiesCache;
- /**
- * Non-clustered cache for the Node parent assocs:
- * KEY: (nodeId, txnId) pair
- * VALUE: ParentAssocs
- */
- private ParentAssocsCache parentAssocsCache;
- private int parentAssocsCacheSize;
- private int parentAssocsCacheLimitFactor = 8;
-
- /**
- * Cache for fast lookups of child nodes by cm:name.
- */
- private SimpleCache childByNameCache;
-
- /**
- * Constructor. Set up various instance-specific members such as caches and locks.
- */
- public AbstractNodeDAOImpl()
- {
- childAssocRetryingHelper = new RetryingCallbackHelper();
- childAssocRetryingHelper.setRetryWaitMs(10);
- childAssocRetryingHelper.setMaxRetries(5);
- // Caches
- rootNodesCache = new EntityLookupCache(new RootNodesCacheCallbackDAO());
- nodesCache = new EntityLookupCache(new NodesCacheCallbackDAO());
- aspectsCache = new EntityLookupCache, Serializable>(new AspectsCallbackDAO());
- propertiesCache = new EntityLookupCache, Serializable>(new PropertiesCallbackDAO());
- childByNameCache = new NullCache();
- }
-
- /**
- * @param transactionService the service to start post-txn processes
- */
- public void setTransactionService(TransactionService transactionService)
- {
- this.transactionService = transactionService;
- }
-
- /**
- * @param dictionaryService the service help determine cm:auditable characteristics
- */
- public void setDictionaryService(DictionaryService dictionaryService)
- {
- this.dictionaryService = dictionaryService;
- }
-
- public void setCachingThreshold(int cachingThreshold)
- {
- this.cachingThreshold = cachingThreshold;
- }
-
- /**
- * @param policyBehaviourFilter the service to determine the behaviour for cm:auditable and
- * other inherent capabilities.
- */
- public void setPolicyBehaviourFilter(BehaviourFilter policyBehaviourFilter)
- {
- this.policyBehaviourFilter = policyBehaviourFilter;
- }
-
- /**
- * @param aclDAO used to update permissions during certain operations
- */
- public void setAclDAO(AclDAO aclDAO)
- {
- this.aclDAO = aclDAO;
- }
-
- /**
- * @param accessControlListDAO used to update ACL inheritance during node moves
- */
- public void setAccessControlListDAO(AccessControlListDAO accessControlListDAO)
- {
- this.accessControlListDAO = accessControlListDAO;
- }
-
- /**
- * @param controlDAO create Savepoints
- */
- public void setControlDAO(ControlDAO controlDAO)
- {
- this.controlDAO = controlDAO;
- }
-
- /**
- * @param qnameDAO translates QName IDs into QName instances and vice-versa
- */
- public void setQnameDAO(QNameDAO qnameDAO)
- {
- this.qnameDAO = qnameDAO;
- }
-
- /**
- * @param contentDataDAO used to create and delete content references
- */
- public void setContentDataDAO(ContentDataDAO contentDataDAO)
- {
- this.contentDataDAO = contentDataDAO;
- }
-
- /**
- * @param localeDAO used to handle MLText properties
- */
- public void setLocaleDAO(LocaleDAO localeDAO)
- {
- this.localeDAO = localeDAO;
- }
-
- /**
- * @param usageDAO used to keep content usage calculations in line
- */
- public void setUsageDAO(UsageDAO usageDAO)
- {
- this.usageDAO = usageDAO;
- }
-
- /**
- * Set the cache that maintains the Store root node data
- *
- * @param cache the cache
- */
- public void setRootNodesCache(SimpleCache cache)
- {
- this.rootNodesCache = new EntityLookupCache(
- cache,
- CACHE_REGION_ROOT_NODES,
- new RootNodesCacheCallbackDAO());
- }
-
- /**
- * Set the cache that maintains the extended Store root node data
- *
- * @param allRootNodesCache the cache
- */
- public void setAllRootNodesCache(SimpleCache> allRootNodesCache)
- {
- this.allRootNodesCache = allRootNodesCache;
- }
-
- /**
- * Set the cache that maintains node ID-NodeRef cross referencing data
- *
- * @param cache the cache
- */
- public void setNodesCache(SimpleCache cache)
- {
- this.nodesCache = new EntityLookupCache(
- cache,
- CACHE_REGION_NODES,
- new NodesCacheCallbackDAO());
- if (cache instanceof TransactionalCache)
- {
- this.nodesTransactionalCache = (TransactionalCache) cache;
- }
- }
-
- /**
- * Set the cache that maintains the Node QName IDs
- *
- * @param aspectsCache the cache
- */
- public void setAspectsCache(SimpleCache> aspectsCache)
- {
- this.aspectsCache = new EntityLookupCache, Serializable>(
- aspectsCache,
- CACHE_REGION_ASPECTS,
- new AspectsCallbackDAO());
- }
-
- /**
- * Set the cache that maintains the Node property values
- *
- * @param propertiesCache the cache
- */
- public void setPropertiesCache(SimpleCache> propertiesCache)
- {
- this.propertiesCache = new EntityLookupCache, Serializable>(
- propertiesCache,
- CACHE_REGION_PROPERTIES,
- new PropertiesCallbackDAO());
- }
-
- /**
- * Sets the maximum capacity of the parent assocs cache
- *
- * @param parentAssocsCacheSize the cache size
- */
- public void setParentAssocsCacheSize(int parentAssocsCacheSize)
- {
- this.parentAssocsCacheSize = parentAssocsCacheSize;
- }
-
- /**
- * Sets the average number of parents expected per cache entry. This parameter is multiplied by the
- * {@link #setParentAssocsCacheSize(int)} parameter to compute a limit on the total number of cached parents, which
- * will be proportional to the cache's memory usage. The cache will be pruned when this limit is exceeded to avoid
- * excessive memory usage.
- *
- * @param parentAssocsCacheLimitFactor
- * the parentAssocsCacheLimitFactor to set
- */
- public void setParentAssocsCacheLimitFactor(int parentAssocsCacheLimitFactor)
- {
- this.parentAssocsCacheLimitFactor = parentAssocsCacheLimitFactor;
- }
-
- /**
- * Set the cache that maintains lookups by child cm:name
- *
- * @param childByNameCache the cache
- */
- public void setChildByNameCache(SimpleCache childByNameCache)
- {
- this.childByNameCache = childByNameCache;
- }
-
- /*
- * Initialize
- */
-
- public void init()
- {
- PropertyCheck.mandatory(this, "transactionService", transactionService);
- PropertyCheck.mandatory(this, "dictionaryService", dictionaryService);
- PropertyCheck.mandatory(this, "aclDAO", aclDAO);
- PropertyCheck.mandatory(this, "accessControlListDAO", accessControlListDAO);
- PropertyCheck.mandatory(this, "qnameDAO", qnameDAO);
- PropertyCheck.mandatory(this, "contentDataDAO", contentDataDAO);
- PropertyCheck.mandatory(this, "localeDAO", localeDAO);
- PropertyCheck.mandatory(this, "usageDAO", usageDAO);
-
- this.nodePropertyHelper = new NodePropertyHelper(dictionaryService, qnameDAO, localeDAO, contentDataDAO);
- this.parentAssocsCache = new ParentAssocsCache(this.parentAssocsCacheSize, this.parentAssocsCacheLimitFactor);
- }
-
- /*
- * Cache helpers
- */
-
- private void clearCaches()
- {
- nodesCache.clear();
- aspectsCache.clear();
- propertiesCache.clear();
- parentAssocsCache.clear();
- }
-
- /**
- * Invalidate cache entries for all children of a give node. This usually applies
- * where the child associations or nodes are modified en-masse.
- *
- * @param parentNodeId the parent node of all child nodes to be invalidated (may be null)
- * @param touchNodes true to also touch the nodes
- * @return the number of child associations found (might be capped)
- */
- private int invalidateNodeChildrenCaches(Long parentNodeId, boolean primary, boolean touchNodes)
- {
- Long txnId = getCurrentTransaction().getId();
-
- int count = 0;
- List childNodeIds = new ArrayList(256);
- Long minChildNodeIdInclusive = Long.MIN_VALUE;
- while (minChildNodeIdInclusive != null)
- {
- childNodeIds.clear();
- List childAssocs = selectChildNodeIds(
- parentNodeId,
- Boolean.valueOf(primary),
- minChildNodeIdInclusive,
- 256);
- // Remove the cache entries as we go
- for (ChildAssocEntity childAssoc : childAssocs)
- {
- Long childNodeId = childAssoc.getChildNode().getId();
- if (childNodeId.compareTo(minChildNodeIdInclusive) < 0)
- {
- throw new RuntimeException("Query results did not increase for child node id ID");
- }
- else
- {
- minChildNodeIdInclusive = Long.valueOf(childNodeId.longValue() + 1L);
- }
- // Invalidate the node cache
- childNodeIds.add(childNodeId);
- invalidateNodeCaches(childNodeId);
- count++;
- }
- // Bring all the nodes into the transaction, if required
- if (touchNodes)
- {
- updateNodes(txnId, childNodeIds);
- }
- // Now break out if we didn't have the full set of results
- if (childAssocs.size() < 256)
- {
- break;
- }
- }
- // Done
- return count;
- }
-
- /**
- * Invalidates all cached artefacts for a particular node, forcing a refresh.
- *
- * @param nodeId the node ID
- */
- private void invalidateNodeCaches(Long nodeId)
- {
- // Take the current value from the nodesCache and use that to invalidate the other caches
- Node node = nodesCache.getValue(nodeId);
- if (node != null)
- {
- invalidateNodeCaches(node, true, true, true);
- }
- // Finally remove the node reference
- nodesCache.removeByKey(nodeId);
- }
-
- /**
- * Invalidate specific node caches using an exact key
- *
- * @param node the node in question
- */
- private void invalidateNodeCaches(Node node, boolean invalidateNodeAspectsCache,
- boolean invalidateNodePropertiesCache, boolean invalidateParentAssocsCache)
- {
- NodeVersionKey nodeVersionKey = node.getNodeVersionKey();
- if (invalidateNodeAspectsCache)
- {
- aspectsCache.removeByKey(nodeVersionKey);
- }
- if (invalidateNodePropertiesCache)
- {
- propertiesCache.removeByKey(nodeVersionKey);
- }
- if (invalidateParentAssocsCache)
- {
- invalidateParentAssocsCached(node);
- }
- }
-
- /*
- * Transactions
- */
-
- private static final String KEY_TRANSACTION = "node.transaction.id";
-
- /**
- * Wrapper to update the current transaction to get the change time correct
- *
- * @author Derek Hulley
- * @since 3.4
- */
- private class UpdateTransactionListener implements TransactionalDao
- {
- /**
- * Checks for the presence of a written DB transaction entry
- */
- @Override
- public boolean isDirty()
- {
- Long txnId = AbstractNodeDAOImpl.this.getCurrentTransactionId(false);
- return txnId != null;
- }
-
- @Override
- public void beforeCommit(boolean readOnly)
- {
- if (readOnly)
- {
- return;
- }
- TransactionEntity txn = AlfrescoTransactionSupport.getResource(KEY_TRANSACTION);
- Long txnId = txn.getId();
- // Update it
- Long now = System.currentTimeMillis();
- txn.setCommitTimeMs(now);
- updateTransaction(txnId, now);
- }
- }
-
- /**
- * @return Returns a new transaction or an existing one if already active
- */
- private TransactionEntity getCurrentTransaction()
- {
- TransactionEntity txn = AlfrescoTransactionSupport.getResource(KEY_TRANSACTION);
- if (txn != null)
- {
- // We have been busy here before
- return txn;
- }
- // Check that this is a writable txn
- if (AlfrescoTransactionSupport.getTransactionReadState() != TxnReadState.TXN_READ_WRITE)
- {
- throw new ReadOnlyServerException();
- }
- // Have to create a new transaction entry
- Long now = System.currentTimeMillis();
- String changeTxnId = AlfrescoTransactionSupport.getTransactionId();
- Long txnId = insertTransaction(changeTxnId, now);
- // Store it for later
- if (isDebugEnabled)
- {
- logger.debug("Create txn: " + txnId);
- }
- txn = new TransactionEntity();
- txn.setId(txnId);
- txn.setChangeTxnId(changeTxnId);
- txn.setCommitTimeMs(now);
-
- AlfrescoTransactionSupport.bindResource(KEY_TRANSACTION, txn);
- // Listen for the end of the transaction
- AlfrescoTransactionSupport.bindDaoService(updateTransactionListener);
- // Done
- return txn;
- }
-
- public Long getCurrentTransactionCommitTime()
- {
- Long commitTime = null;
- TransactionEntity resource = AlfrescoTransactionSupport.getResource(KEY_TRANSACTION);
- if(resource != null)
- {
- commitTime = resource.getCommitTimeMs();
- }
- return commitTime;
- }
-
- public Long getCurrentTransactionId(boolean ensureNew)
- {
- TransactionEntity txn;
- if (ensureNew)
- {
- txn = getCurrentTransaction();
- }
- else
- {
- txn = AlfrescoTransactionSupport.getResource(KEY_TRANSACTION);
- }
- return txn == null ? null : txn.getId();
- }
-
- /*
- * Stores
- */
-
- @Override
- public Pair getStore(StoreRef storeRef)
- {
- Pair rootNodePair = rootNodesCache.getByKey(storeRef);
- if (rootNodePair == null)
- {
- return null;
- }
- else
- {
- return new Pair(rootNodePair.getSecond().getStore().getId(), rootNodePair.getFirst());
- }
- }
-
- @Override
- public List> getStores()
- {
- List storeEntities = selectAllStores();
- List> storeRefs = new ArrayList>(storeEntities.size());
- for (StoreEntity storeEntity : storeEntities)
- {
- storeRefs.add(new Pair(storeEntity.getId(), storeEntity.getStoreRef()));
- }
- return storeRefs;
- }
-
- /**
- * @throws InvalidStoreRefException if the store is invalid
- */
- private StoreEntity getStoreNotNull(StoreRef storeRef)
- {
- Pair rootNodePair = rootNodesCache.getByKey(storeRef);
- if (rootNodePair == null)
- {
- throw new InvalidStoreRefException(storeRef);
- }
- else
- {
- return rootNodePair.getSecond().getStore();
- }
- }
-
- @Override
- public boolean exists(StoreRef storeRef)
- {
- Pair rootNodePair = rootNodesCache.getByKey(storeRef);
- return rootNodePair != null;
- }
-
- @Override
- public Pair getRootNode(StoreRef storeRef)
- {
- Pair rootNodePair = rootNodesCache.getByKey(storeRef);
- if (rootNodePair == null)
- {
- throw new InvalidStoreRefException(storeRef);
- }
- else
- {
- return rootNodePair.getSecond().getNodePair();
- }
- }
-
- @Override
- public Set getAllRootNodes(StoreRef storeRef)
- {
- Set rootNodes = allRootNodesCache.get(storeRef);
- if (rootNodes == null)
- {
- final Map> allRootNodes = new HashMap>(97);
- getNodesWithAspects(Collections.singleton(ContentModel.ASPECT_ROOT), 0L, Long.MAX_VALUE, new NodeRefQueryCallback()
- {
- @Override
- public boolean handle(Pair nodePair)
- {
- NodeRef nodeRef = nodePair.getSecond();
- StoreRef storeRef = nodeRef.getStoreRef();
- Set rootNodes = allRootNodes.get(storeRef);
- if (rootNodes == null)
- {
- rootNodes = new HashSet(97);
- allRootNodes.put(storeRef, rootNodes);
- }
- rootNodes.add(nodeRef);
- return true;
- }
- });
- rootNodes = allRootNodes.get(storeRef);
- if (rootNodes == null)
- {
- rootNodes = Collections.emptySet();
- allRootNodes.put(storeRef, rootNodes);
- }
- for (Map.Entry> entry : allRootNodes.entrySet())
- {
- StoreRef entryStoreRef = entry.getKey();
- // Prevent unnecessary cross-invalidation
- if (!allRootNodesCache.contains(entryStoreRef))
- {
- allRootNodesCache.put(entryStoreRef, entry.getValue());
- }
- }
- }
- return rootNodes;
- }
-
- @Override
- public Pair newStore(StoreRef storeRef)
- {
- // Create the store
- StoreEntity store = new StoreEntity();
- store.setProtocol(storeRef.getProtocol());
- store.setIdentifier(storeRef.getIdentifier());
-
- Long storeId = insertStore(store);
- store.setId(storeId);
-
- // Get an ACL for the root node
- Long aclId = aclDAO.createAccessControlList();
-
- // Create a root node
- Long nodeTypeQNameId = qnameDAO.getOrCreateQName(ContentModel.TYPE_STOREROOT).getFirst();
- NodeEntity rootNode = newNodeImpl(store, null, nodeTypeQNameId, null, aclId, null, true);
- Long rootNodeId = rootNode.getId();
- addNodeAspects(rootNodeId, Collections.singleton(ContentModel.ASPECT_ROOT));
-
- // Now update the store with the root node ID
- store.setRootNode(rootNode);
- updateStoreRoot(store);
-
- // Push the value into the caches
- rootNodesCache.setValue(storeRef, rootNode);
-
- if (isDebugEnabled)
- {
- logger.debug("Created store: \n" + " " + store);
- }
- return new Pair(rootNode.getId(), rootNode.getNodeRef());
- }
-
- @Override
- public void moveStore(StoreRef oldStoreRef, StoreRef newStoreRef)
- {
- StoreEntity store = getStoreNotNull(oldStoreRef);
- store.setProtocol(newStoreRef.getProtocol());
- store.setIdentifier(newStoreRef.getIdentifier());
- // Update it
- int count = updateStore(store);
- if (count != 1)
- {
- throw new ConcurrencyFailureException("Store not updated: " + oldStoreRef);
- }
- // Bring all the associated nodes into the current transaction
- Long txnId = getCurrentTransaction().getId();
- Long storeId = store.getId();
- updateNodesInStore(txnId, storeId);
-
- // All the NodeRef-based caches are invalid. ID-based caches are fine.
- rootNodesCache.removeByKey(oldStoreRef);
- allRootNodesCache.remove(oldStoreRef);
- nodesCache.clear();
-
- if (isDebugEnabled)
- {
- logger.debug("Moved store: " + oldStoreRef + " --> " + newStoreRef);
- }
- }
-
- /**
- * Callback to cache store root nodes by {@link StoreRef}.
- *
- * @author Derek Hulley
- * @since 3.4
- */
- private class RootNodesCacheCallbackDAO extends EntityLookupCallbackDAOAdaptor
- {
- /**
- * @throws UnsupportedOperationException Stores must be created externally
- */
- public Pair createValue(Node value)
- {
- throw new UnsupportedOperationException("Root node creation is done externally: " + value);
- }
-
- /**
- * @param storeRef the store ID
- */
- public Pair findByKey(StoreRef storeRef)
- {
- NodeEntity node = selectStoreRootNode(storeRef);
- return node == null ? null : new Pair(storeRef, node);
- }
- }
-
- /*
- * Nodes
- */
-
- /**
- * Callback to cache nodes by ID and {@link NodeRef}. When looking up objects based on the
- * value key, only the referencing properties need be populated. ALL nodes are cached,
- * not just live nodes.
- *
- * @see NodeEntity
- *
- * @author Derek Hulley
- * @since 3.4
- */
- private class NodesCacheCallbackDAO extends EntityLookupCallbackDAOAdaptor
- {
- /**
- * @throws UnsupportedOperationException Nodes are created externally
- */
- public Pair createValue(Node value)
- {
- throw new UnsupportedOperationException("Node creation is done externally: " + value);
- }
-
- /**
- * @param nodeId the key node ID
- */
- public Pair findByKey(Long nodeId)
- {
- NodeEntity node = selectNodeById(nodeId);
- if (node != null)
- {
- // Lock it to prevent 'accidental' modification
- node.lock();
- return new Pair(nodeId, node);
- }
- else
- {
- return null;
- }
- }
-
- /**
- * @return Returns the Node's NodeRef
- */
- @Override
- public NodeRef getValueKey(Node value)
- {
- return value.getNodeRef();
- }
-
- /**
- * Looks the node up based on the NodeRef of the given node
- */
- @Override
- public Pair findByValue(Node node)
- {
- NodeRef nodeRef = node.getNodeRef();
- node = selectNodeByNodeRef(nodeRef);
- if (node != null)
- {
- // Lock it to prevent 'accidental' modification
- node.lock();
- return new Pair(node.getId(), node);
- }
- else
- {
- return null;
- }
- }
- }
-
- public boolean exists(Long nodeId)
- {
- Pair pair = nodesCache.getByKey(nodeId);
- return pair != null && !pair.getSecond().getDeleted(qnameDAO);
- }
-
- public boolean exists(NodeRef nodeRef)
- {
- NodeEntity node = new NodeEntity(nodeRef);
- Pair pair = nodesCache.getByValue(node);
- return pair != null && !pair.getSecond().getDeleted(qnameDAO);
- }
-
- @Override
- public boolean isInCurrentTxn(Long nodeId)
- {
- Long currentTxnId = getCurrentTransactionId(false);
- if (currentTxnId == null)
- {
- // No transactional changes have been made to any nodes, therefore the node cannot
- // be part of the current transaction
- return false;
- }
- Node node = getNodeNotNull(nodeId, false);
- Long nodeTxnId = node.getTransaction().getId();
- return nodeTxnId.equals(currentTxnId);
- }
-
- @Override
- public Status getNodeRefStatus(NodeRef nodeRef)
- {
- Node node = new NodeEntity(nodeRef);
- Pair nodePair = nodesCache.getByValue(node);
- // The nodesCache gets both live and deleted nodes.
- if (nodePair == null)
- {
- return null;
- }
- else
- {
- return nodePair.getSecond().getNodeStatus(qnameDAO);
- }
- }
-
- @Override
- public Status getNodeIdStatus(Long nodeId)
- {
- Pair nodePair = nodesCache.getByKey(nodeId);
- // The nodesCache gets both live and deleted nodes.
- if (nodePair == null)
- {
- return null;
- }
- else
- {
- return nodePair.getSecond().getNodeStatus(qnameDAO);
- }
- }
-
- @Override
- public Pair getNodePair(NodeRef nodeRef)
- {
- NodeEntity node = new NodeEntity(nodeRef);
- Pair pair = nodesCache.getByValue(node);
- // Check it
- if (pair == null || pair.getSecond().getDeleted(qnameDAO))
- {
- // The cache says that the node is not there or is deleted.
- // We double check by going to the DB
- Node dbNode = selectNodeByNodeRef(nodeRef);
- if (dbNode == null)
- {
- // The DB agrees. This is an invalid noderef. Why are you trying to use it?
- return null;
- }
- else if (dbNode.getDeleted(qnameDAO))
- {
- // We may have reached this deleted node via an invalid association; trigger a post transaction prune of
- // any associations that point to this deleted one
- pruneDanglingAssocs(dbNode.getId());
-
- // The DB agrees. This is a deleted noderef.
- return null;
- }
- else
- {
- // The cache was wrong, possibly due to it caching negative results earlier.
- if (isDebugEnabled)
- {
- logger.debug("Repairing stale cache entry for node: " + nodeRef);
- }
- Long nodeId = dbNode.getId();
- invalidateNodeCaches(nodeId);
- dbNode.lock(); // Prevent unexpected edits of values going into the cache
- nodesCache.setValue(nodeId, dbNode);
- return dbNode.getNodePair();
- }
- }
- return pair.getSecond().getNodePair();
- }
-
- /**
- * Trigger a post transaction prune of any associations that point to this deleted one.
- * @param nodeId Long
- */
- private void pruneDanglingAssocs(Long nodeId)
- {
- selectChildAssocs(nodeId, null, null, null, null, null, new ChildAssocRefQueryCallback()
- {
- @Override
- public boolean preLoadNodes()
- {
- return false;
- }
-
- @Override
- public boolean orderResults()
- {
- return false;
- }
-
- @Override
- public boolean handle(Pair childAssocPair, Pair parentNodePair,
- Pair childNodePair)
- {
- bindFixAssocAndCollectLostAndFound(childNodePair, "childNodeWithDeletedParent", childAssocPair.getFirst(), childAssocPair.getSecond().isPrimary() && exists(childAssocPair.getFirst()));
- return true;
- }
-
- @Override
- public void done()
- {
- }
- });
- selectParentAssocs(nodeId, null, null, null, new ChildAssocRefQueryCallback()
- {
- @Override
- public boolean preLoadNodes()
- {
- return false;
- }
-
- @Override
- public boolean orderResults()
- {
- return false;
- }
-
- @Override
- public boolean handle(Pair childAssocPair, Pair parentNodePair,
- Pair childNodePair)
- {
- bindFixAssocAndCollectLostAndFound(childNodePair, "deletedChildWithParents", childAssocPair.getFirst(), false);
- return true;
- }
-
- @Override
- public void done()
- {
- }
- });
- }
-
- @Override
- public Pair getNodePair(Long nodeId)
- {
- Pair pair = nodesCache.getByKey(nodeId);
- // Check it
- if (pair == null || pair.getSecond().getDeleted(qnameDAO))
- {
- // The cache says that the node is not there or is deleted.
- // We double check by going to the DB
- Node dbNode = selectNodeById(nodeId);
- if (dbNode == null)
- {
- // The DB agrees. This is an invalid noderef. Why are you trying to use it?
- return null;
- }
- else if (dbNode.getDeleted(qnameDAO))
- {
- // We may have reached this deleted node via an invalid association; trigger a post transaction prune of
- // any associations that point to this deleted one
- pruneDanglingAssocs(dbNode.getId());
-
- // The DB agrees. This is a deleted noderef.
- return null;
- }
- else
- {
- // The cache was wrong, possibly due to it caching negative results earlier.
- if (isDebugEnabled)
- {
- logger.debug("Repairing stale cache entry for node: " + nodeId);
- }
- invalidateNodeCaches(nodeId);
- dbNode.lock(); // Prevent unexpected edits of values going into the cache
- nodesCache.setValue(nodeId, dbNode);
- return dbNode.getNodePair();
- }
- }
- else
- {
- return pair.getSecond().getNodePair();
- }
- }
-
- /**
- * Get a node instance regardless of whether it is considered live or deleted
- *
- * @param nodeId the node ID to look for
- * @param liveOnly true to ensure that only live nodes are retrieved
- * @return a node that will be live if requested
- * @throws ConcurrencyFailureException if a valid node is not found
- */
- private Node getNodeNotNull(Long nodeId, boolean liveOnly)
- {
- Pair pair = nodesCache.getByKey(nodeId);
-
- if (pair == null)
- {
- // The node has no entry in the database
- NodeEntity dbNode = selectNodeById(nodeId);
- nodesCache.removeByKey(nodeId);
- throw new ConcurrencyFailureException(
- "No node row exists: \n" +
- " ID: " + nodeId + "\n" +
- " DB row: " + dbNode);
- }
- else if (pair.getSecond().getDeleted(qnameDAO) && liveOnly)
- {
- // The node is not 'live' as was requested
- NodeEntity dbNode = selectNodeById(nodeId);
- nodesCache.removeByKey(nodeId);
- // Make absolutely sure that the node is not referenced by any associations
- pruneDanglingAssocs(nodeId);
- // Force a retry on the transaction
- throw new ConcurrencyFailureException(
- "No live node exists: \n" +
- " ID: " + nodeId + "\n" +
- " DB row: " + dbNode);
- }
- else
- {
- return pair.getSecond();
- }
- }
-
- @Override
- public QName getNodeType(Long nodeId)
- {
- Node node = getNodeNotNull(nodeId, false);
- Long nodeTypeQNameId = node.getTypeQNameId();
- return qnameDAO.getQName(nodeTypeQNameId).getSecond();
- }
-
- @Override
- public Long getNodeAclId(Long nodeId)
- {
- Node node = getNodeNotNull(nodeId, true);
- return node.getAclId();
- }
-
- @Override
- public ChildAssocEntity newNode(
- Long parentNodeId,
- QName assocTypeQName,
- QName assocQName,
- StoreRef storeRef,
- String uuid,
- QName nodeTypeQName,
- Locale nodeLocale,
- String childNodeName,
- Map auditableProperties) throws InvalidTypeException
- {
- Assert.notNull(parentNodeId, "parentNodeId");
- Assert.notNull(assocTypeQName, "assocTypeQName");
- Assert.notNull(assocQName, "assocQName");
- Assert.notNull(storeRef, "storeRef");
-
- if (auditableProperties == null)
- {
- auditableProperties = Collections.emptyMap();
- }
-
- // Get the parent node
- Node parentNode = getNodeNotNull(parentNodeId, true);
-
- // Find an initial ACL for the node
- Long parentAclId = parentNode.getAclId();
- AccessControlListProperties inheritedAcl = null;
- Long childAclId = null;
- if (parentAclId != null)
- {
- try
- {
- Long inheritedACL = aclDAO.getInheritedAccessControlList(parentAclId);
- inheritedAcl = aclDAO.getAccessControlListProperties(inheritedACL);
- if (inheritedAcl != null)
- {
- childAclId = inheritedAcl.getId();
- }
- }
- catch (RuntimeException e)
- {
- // The get* calls above actually do writes. So pessimistically get rid of the
- // parent node from the cache in case it was wrong somehow.
- invalidateNodeCaches(parentNodeId);
- // Rethrow for a retry (ALF-17286)
- throw new RuntimeException(
- "Failure while 'getting' inherited ACL or ACL properties: \n" +
- " parent ACL ID: " + parentAclId + "\n" +
- " inheritied ACL: " + inheritedAcl,
- e);
- }
- }
- // Build the cm:auditable properties
- AuditablePropertiesEntity auditableProps = new AuditablePropertiesEntity();
- boolean setAuditProps = auditableProps.setAuditValues(null, null, auditableProperties);
- if (!setAuditProps)
- {
- // No cm:auditable properties were supplied
- auditableProps = null;
- }
-
- // Get the store
- StoreEntity store = getStoreNotNull(storeRef);
- // Create the node (it is not a root node)
- Long nodeTypeQNameId = qnameDAO.getOrCreateQName(nodeTypeQName).getFirst();
- Long nodeLocaleId = localeDAO.getOrCreateLocalePair(nodeLocale).getFirst();
- NodeEntity node = newNodeImpl(store, uuid, nodeTypeQNameId, nodeLocaleId, childAclId, auditableProps, true);
- Long nodeId = node.getId();
-
- // Protect the node's cm:auditable if it was explicitly set
- if (setAuditProps)
- {
- NodeRef nodeRef = node.getNodeRef();
- policyBehaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE);
- }
-
- // Now create a primary association for it
- if (childNodeName == null)
- {
- childNodeName = node.getUuid();
- }
- ChildAssocEntity assoc = newChildAssocImpl(
- parentNodeId, nodeId, true, assocTypeQName, assocQName, childNodeName, false);
-
- // There will be no other parent assocs
- boolean isRoot = false;
- boolean isStoreRoot = nodeTypeQName.equals(ContentModel.TYPE_STOREROOT);
- ParentAssocsInfo parentAssocsInfo = new ParentAssocsInfo(isRoot, isStoreRoot, assoc);
- setParentAssocsCached(nodeId, parentAssocsInfo);
-
- if (isDebugEnabled)
- {
- logger.debug(
- "Created new node: \n" +
- " Node: " + node + "\n" +
- " Assoc: " + assoc);
- }
- return assoc;
- }
-
- /**
- * @param uuid the node UUID, or null to auto-generate
- * @param nodeTypeQNameId the node's type
- * @param nodeLocaleId the node's locale or null to use the default locale
- * @param aclId an ACL ID if available
- * @param auditableProps null to auto-generate or provide a value to explicitly set
- * @param allowAuditableAspect Should we override the behaviour by potentially not adding the auditable aspect
- * @throws NodeExistsException if the target reference is already taken by a live node
- */
- private NodeEntity newNodeImpl(
- StoreEntity store,
- String uuid,
- Long nodeTypeQNameId,
- Long nodeLocaleId,
- Long aclId,
- AuditablePropertiesEntity auditableProps,
- boolean allowAuditableAspect) throws InvalidTypeException
- {
- NodeEntity node = new NodeEntity();
- // Store
- node.setStore(store);
- // UUID
- if (uuid == null)
- {
- node.setUuid(GUID.generate());
- }
- else
- {
- node.setUuid(uuid);
- }
- // QName
- node.setTypeQNameId(nodeTypeQNameId);
- QName nodeTypeQName = qnameDAO.getQName(nodeTypeQNameId).getSecond();
- // Locale
- if (nodeLocaleId == null)
- {
- nodeLocaleId = localeDAO.getOrCreateDefaultLocalePair().getFirst();
- }
- node.setLocaleId(nodeLocaleId);
- // ACL (may be null)
- node.setAclId(aclId);
- // Transaction
- TransactionEntity txn = getCurrentTransaction();
- node.setTransaction(txn);
-
- // Audit
- boolean addAuditableAspect = false;
- if (auditableProps != null)
- {
- // Client-supplied cm:auditable values
- node.setAuditableProperties(auditableProps);
- addAuditableAspect = true;
- }
- else if (AuditablePropertiesEntity.hasAuditableAspect(nodeTypeQName, dictionaryService))
- {
- // Automatically-generated cm:auditable values
- auditableProps = new AuditablePropertiesEntity();
- auditableProps.setAuditValues(null, null, true, 0L);
- node.setAuditableProperties(auditableProps);
- addAuditableAspect = true;
- }
-
- if (!allowAuditableAspect) addAuditableAspect = false;
-
- Long id = newNodeImplInsert(node);
- node.setId(id);
-
- Set nodeAspects = null;
- if (addAuditableAspect)
- {
- Long auditableAspectQNameId = qnameDAO.getOrCreateQName(ContentModel.ASPECT_AUDITABLE).getFirst();
- insertNodeAspect(id, auditableAspectQNameId);
- nodeAspects = Collections.singleton(ContentModel.ASPECT_AUDITABLE);
- }
- else
- {
- nodeAspects = Collections.emptySet();
- }
-
- // Lock the node and cache
- node.lock();
- nodesCache.setValue(id, node);
- // Pre-populate some of the other caches so that we don't immediately query
- setNodeAspectsCached(id, nodeAspects);
- setNodePropertiesCached(id, Collections.emptyMap());
-
- if (isDebugEnabled)
- {
- logger.debug("Created new node: \n" + " " + node);
- }
- return node;
- }
-
- protected Long newNodeImplInsert(NodeEntity node)
- {
- Long id = null;
- Savepoint savepoint = controlDAO.createSavepoint("newNodeImpl");
- try
- {
- // First try a straight insert and risk the constraint violation if the node exists
- id = insertNode(node);
- controlDAO.releaseSavepoint(savepoint);
- }
- catch (Throwable e)
- {
- controlDAO.rollbackToSavepoint(savepoint);
- // This is probably because there is an existing node. We can handle existing deleted nodes.
- NodeRef targetNodeRef = node.getNodeRef();
- Node dbTargetNode = selectNodeByNodeRef(targetNodeRef);
- if (dbTargetNode == null)
- {
- // There does not appear to be any row that could prevent an insert
- throw new AlfrescoRuntimeException("Failed to insert new node: " + node, e);
- }
- else if (dbTargetNode.getDeleted(qnameDAO))
- {
- Long dbTargetNodeId = dbTargetNode.getId();
- // This is OK. It happens when we create a node that existed in the past.
- // Remove the row completely
- deleteNodeProperties(dbTargetNodeId, (Set) null);
- deleteNodeById(dbTargetNodeId);
- // Now repeat the insert but let any further problems just be thrown out
- id = insertNode(node);
- }
- else
- {
- // A live node exists.
- throw new NodeExistsException(dbTargetNode.getNodePair(), e);
- }
- }
-
- return id;
- }
-
- @Override
- public Pair, Pair> moveNode(
- final Long childNodeId,
- final Long newParentNodeId,
- final QName assocTypeQName,
- final QName assocQName)
- {
- final Node newParentNode = getNodeNotNull(newParentNodeId, true);
- final StoreEntity newParentStore = newParentNode.getStore();
- final Node childNode = getNodeNotNull(childNodeId, true);
- final StoreEntity childStore = childNode.getStore();
- final ChildAssocEntity primaryParentAssoc = getPrimaryParentAssocImpl(childNodeId);
- final Long oldParentAclId;
- final Long oldParentNodeId;
- if (primaryParentAssoc == null)
- {
- oldParentAclId = null;
- oldParentNodeId = null;
- }
- else
- {
- if (primaryParentAssoc.getParentNode() == null)
- {
- oldParentAclId = null;
- oldParentNodeId = null;
- }
- else
- {
- oldParentNodeId = primaryParentAssoc.getParentNode().getId();
- oldParentAclId = getNodeNotNull(oldParentNodeId, true).getAclId();
- }
- }
-
- // Need the child node's name here in case it gets removed
- final String childNodeName = (String) getNodeProperty(childNodeId, ContentModel.PROP_NAME);
-
- // First attempt to move the node, which may rollback to a savepoint
- Node newChildNode = childNode;
- // Store
- if (!childStore.getId().equals(newParentStore.getId()))
- {
-
- //Delete the ASPECT_AUDITABLE from the source node so it doesn't get copied across
- //A new aspect would have already been created in the newNodeImpl method.
- // ... make sure we have the cm:auditable data from the originating node
- AuditablePropertiesEntity auditableProps = childNode.getAuditableProperties();
-
- // Create a new node
- newChildNode = newNodeImpl(
- newParentStore,
- childNode.getUuid(),
- childNode.getTypeQNameId(),
- childNode.getLocaleId(),
- childNode.getAclId(),
- auditableProps,
- false);
- Long newChildNodeId = newChildNode.getId();
-
- //copy all the data over to new node
- moveNodeData(childNode.getId(), newChildNodeId);
-
- // The new node will have new data not present in the cache, yet
- invalidateNodeCaches(newChildNodeId);
- invalidateNodeChildrenCaches(newChildNodeId, true, true);
- invalidateNodeChildrenCaches(newChildNodeId, false, true);
- // Completely delete the original node but keep the ACL as it's reused
- deleteNodeImpl(childNodeId, false);
- }
- else
- {
- // Touch the node; make sure parent assocs are invalidated
- touchNode(childNodeId, null, null, false, false, true);
- }
-
- final Long newChildNodeId = newChildNode.getId();
-
- // Now update the primary parent assoc
- updatePrimaryParentAssocs(primaryParentAssoc,
- newParentNode,
- childNode,
- newChildNodeId,
- childNodeName,
- oldParentNodeId,
- assocTypeQName,
- assocQName);
-
- // Optimize for rename case
- if (!EqualsHelper.nullSafeEquals(newParentNodeId, oldParentNodeId))
- {
- // Check for cyclic relationships
- // TODO: This adds a lot of overhead when moving hierarchies.
- // While getPaths is faster, it would be better to avoid the parentAssocsCache
- // completely.
- getPaths(newChildNode.getNodePair(), false);
-// cycleCheck(newChildNodeId);
-
- // Update ACLs for moved tree
- Long newParentAclId = newParentNode.getAclId();
-
- // Verify if parent has aspect applied and ACL's are pending
- if (hasNodeAspect(oldParentNodeId, ContentModel.ASPECT_PENDING_FIX_ACL))
- {
- Long oldParentSharedAclId = (Long) this.getNodeProperty(oldParentNodeId, ContentModel.PROP_SHARED_ACL_TO_REPLACE);
- accessControlListDAO.updateInheritance(newChildNodeId, oldParentSharedAclId, newParentAclId);
- }
- else
- {
- accessControlListDAO.updateInheritance(newChildNodeId, oldParentAclId, newParentAclId);
- }
- }
-
- // Done
- Pair assocPair = getPrimaryParentAssoc(newChildNode.getId());
- Pair nodePair = newChildNode.getNodePair();
- if (isDebugEnabled)
- {
- logger.debug("Moved node: " + assocPair + " ... " + nodePair);
- }
- return new Pair, Pair>(assocPair, nodePair);
- }
-
- protected void updatePrimaryParentAssocs(
- final ChildAssocEntity primaryParentAssoc,
- final Node newParentNode,
- final Node childNode,
- final Long newChildNodeId,
- final String childNodeName,
- final Long oldParentNodeId,
- final QName assocTypeQName,
- final QName assocQName)
- {
- // Because we are retrying in-transaction i.e. absorbing exceptions, we need partial rollback &/or via savepoint if needed (eg. PostgreSQL)
- RetryingCallback callback = new RetryingCallback()
- {
- public Integer execute() throws Throwable
- {
- return updatePrimaryParentAssocsImpl(primaryParentAssoc,
- newParentNode,
- childNode,
- newChildNodeId,
- childNodeName,
- oldParentNodeId,
- assocTypeQName,
- assocQName);
- }
- };
- childAssocRetryingHelper.doWithRetry(callback);
- }
-
- protected int updatePrimaryParentAssocsImpl(
- ChildAssocEntity primaryParentAssoc,
- Node newParentNode,
- Node childNode,
- Long newChildNodeId,
- String childNodeName,
- Long oldParentNodeId,
- QName assocTypeQName,
- QName assocQName)
- {
- Long newParentNodeId = newParentNode.getId();
- Long childNodeId = childNode.getId();
-
- Savepoint savepoint = controlDAO.createSavepoint("DuplicateChildNodeNameException");
- // We use the child node's UUID if there is no cm:name
- String childNodeNameToUse = childNodeName == null ? childNode.getUuid() : childNodeName;
-
- try
- {
- int updated = updatePrimaryParentAssocs(
- newChildNodeId,
- newParentNodeId,
- assocTypeQName,
- assocQName,
- childNodeNameToUse);
- controlDAO.releaseSavepoint(savepoint);
- // Ensure we invalidate the name cache (the child version key might not have been 'bumped' by the last
- // 'touch')
- if (updated > 0 && primaryParentAssoc != null)
- {
- Pair oldTypeQnamePair = qnameDAO.getQName(
- primaryParentAssoc.getTypeQNameId());
- if (oldTypeQnamePair != null)
- {
- childByNameCache.remove(new ChildByNameKey(oldParentNodeId, oldTypeQnamePair.getSecond(),
- primaryParentAssoc.getChildNodeName()));
- }
- }
- return updated;
- }
- catch (Throwable e)
- {
- controlDAO.rollbackToSavepoint(savepoint);
- // DuplicateChildNodeNameException implements DoNotRetryException.
- // There are some cases - FK violations, specifically - where we DO actually want to retry.
- // Detecting this is done by looking for the related FK names, 'fk_alf_cass_*' in the error message
- String lowerMsg = e.getMessage().toLowerCase();
- if (lowerMsg.contains("fk_alf_cass_"))
- {
- throw new ConcurrencyFailureException("FK violation updating primary parent association for " + childNodeId, e);
- }
- // We assume that this is from the child cm:name constraint violation
- throw new DuplicateChildNodeNameException(
- newParentNode.getNodeRef(),
- assocTypeQName,
- childNodeName,
- e);
- }
- }
-
- @Override
- public boolean updateNode(Long nodeId, QName nodeTypeQName, Locale nodeLocale)
- {
- // Get the existing node; we need to check for a change in store or UUID
- Node oldNode = getNodeNotNull(nodeId, true);
- final Long nodeTypeQNameId;
- if (nodeTypeQName == null)
- {
- nodeTypeQNameId = oldNode.getTypeQNameId();
- }
- else
- {
- nodeTypeQNameId = qnameDAO.getOrCreateQName(nodeTypeQName).getFirst();
- }
- final Long nodeLocaleId;
- if (nodeLocale == null)
- {
- nodeLocaleId = oldNode.getLocaleId();
- }
- else
- {
- nodeLocaleId = localeDAO.getOrCreateLocalePair(nodeLocale).getFirst();
- }
-
- // Wrap all the updates into one
- NodeUpdateEntity nodeUpdate = new NodeUpdateEntity();
- nodeUpdate.setId(nodeId);
- nodeUpdate.setStore(oldNode.getStore()); // Need node reference
- nodeUpdate.setUuid(oldNode.getUuid()); // Need node reference
- // TypeQName (if necessary)
- if (!nodeTypeQNameId.equals(oldNode.getTypeQNameId()))
- {
- nodeUpdate.setTypeQNameId(nodeTypeQNameId);
- nodeUpdate.setUpdateTypeQNameId(true);
- }
- // Locale (if necessary)
- if (!nodeLocaleId.equals(oldNode.getLocaleId()))
- {
- nodeUpdate.setLocaleId(nodeLocaleId);
- nodeUpdate.setUpdateLocaleId(true);
- }
-
- return updateNodeImpl(oldNode, nodeUpdate, null);
- }
-
-
- @Override
- public int touchNodes(Long txnId, List nodeIds)
- {
- // limit in clause to 1000 node ids
- int batchSize = 1000;
-
- int touched = 0;
- ArrayList batch = new ArrayList(batchSize);
- for(Long nodeId : nodeIds)
- {
- invalidateNodeCaches(nodeId);
- batch.add(nodeId);
- if(batch.size() % batchSize == 0)
- {
- touched += updateNodes(txnId, batch);
- batch.clear();
- }
- }
- if(batch.size() > 0)
- {
- touched += updateNodes(txnId, batch);
- }
- return touched;
- }
-
- /**
- * Updates the node's transaction and cm:auditable properties while
- * providing a convenient method to control cache entry invalidation.
- *
- * Not all 'touch' signals actually produce a change: the node may already have been touched
- * in the current transaction. In this case, the required caches are explicitly invalidated
- * as requested.
- * It is more complicated when the node is modified. If the node is modified against a previous
- * transaction then all cache entries are left untrusted and not pulled forward. But if the
- * node is modified but in the same transaction, then the cache entries are considered good and
- * pull forward against the current version of the node ... unless the cache was specicially
- * tagged for invalidation.
- *
- * It is sometime necessary to provide the node's current aspects, particularly during
- * changes to the aspect list. If not provided, they will be looked up.
- *
- * @param nodeId the ID of the node (must refer to a live node)
- * @param auditableProps optionally override the cm:auditable values
- * @param nodeAspects the node's aspects or null to look them up
- * @param invalidateNodeAspectsCache true if the node's cached aspects are unreliable
- * @param invalidateNodePropertiesCache true if the node's cached properties are unreliable
- * @param invalidateParentAssocsCache true if the node's cached parent assocs are unreliable
- *
- * @see #updateNodeImpl(Node, NodeUpdateEntity, Set)
- */
- private boolean touchNode(
- Long nodeId, AuditablePropertiesEntity auditableProps, Set nodeAspects,
- boolean invalidateNodeAspectsCache,
- boolean invalidateNodePropertiesCache,
- boolean invalidateParentAssocsCache)
- {
- Node node = null;
- try
- {
- node = getNodeNotNull(nodeId, false);
- }
- catch (DataIntegrityViolationException e)
- {
- // The ID doesn't reference a live node.
- // We do nothing w.r.t. touching
- return false;
- }
-
- NodeUpdateEntity nodeUpdate = new NodeUpdateEntity();
- nodeUpdate.setId(nodeId);
- nodeUpdate.setAuditableProperties(auditableProps);
- // Update it
- boolean updatedNode = updateNodeImpl(node, nodeUpdate, nodeAspects);
- // Handle the cache invalidation requests
- NodeVersionKey nodeVersionKey = node.getNodeVersionKey();
- if (updatedNode)
- {
- Node newNode = getNodeNotNull(nodeId, false);
- NodeVersionKey newNodeVersionKey = newNode.getNodeVersionKey();
- // The version will have moved on, effectively rendering our caches invalid.
- // Copy over caches that DON'T need invalidating
- if (!invalidateNodeAspectsCache)
- {
- copyNodeAspectsCached(nodeVersionKey, newNodeVersionKey);
- }
- if (!invalidateNodePropertiesCache)
- {
- copyNodePropertiesCached(nodeVersionKey, newNodeVersionKey);
- }
- if (invalidateParentAssocsCache)
- {
- // Because we cache parent assocs by transaction, we must manually invalidate on this version change
- invalidateParentAssocsCached(node);
- }
- else
- {
- copyParentAssocsCached(node);
- }
- }
- else
- {
- // The node was not touched. By definition it MUST be in the current transaction.
- // We invalidate the caches as specifically requested
- invalidateNodeCaches(
- node,
- invalidateNodeAspectsCache,
- invalidateNodePropertiesCache,
- invalidateParentAssocsCache);
- }
-
- return updatedNode;
- }
-
- /**
- * Helper method that updates the node, bringing it into the current transaction with
- * the appropriate cm:auditable and transaction behaviour.
- *
- * If the NodeRef of the node is changing (usually a store move) then deleted
- * nodes are cleaned out where they might exist.
- *
- * @param oldNode the existing node, fully populated
- * @param nodeUpdate the node update with all update elements populated
- * @param nodeAspects the node's aspects or null to look them up
- * @return true if any updates were made
- */
- private boolean updateNodeImpl(Node oldNode, NodeUpdateEntity nodeUpdate, Set nodeAspects)
- {
- Long nodeId = oldNode.getId();
-
- // Make sure that the ID has been populated
- if (!EqualsHelper.nullSafeEquals(nodeId, nodeUpdate.getId()))
- {
- throw new IllegalArgumentException("NodeUpdateEntity node ID is not correct: " + nodeUpdate);
- }
-
- // Copy of the reference data
- nodeUpdate.setStore(oldNode.getStore());
- nodeUpdate.setUuid(oldNode.getUuid());
-
- // Ensure that other values are set for completeness when caching
- if (!nodeUpdate.isUpdateTypeQNameId())
- {
- nodeUpdate.setTypeQNameId(oldNode.getTypeQNameId());
- }
- if (!nodeUpdate.isUpdateLocaleId())
- {
- nodeUpdate.setLocaleId(oldNode.getLocaleId());
- }
- if (!nodeUpdate.isUpdateAclId())
- {
- nodeUpdate.setAclId(oldNode.getAclId());
- }
-
- nodeUpdate.setVersion(oldNode.getVersion());
- // Update the transaction
- TransactionEntity txn = getCurrentTransaction();
- nodeUpdate.setTransaction(txn);
- if (!txn.getId().equals(oldNode.getTransaction().getId()))
- {
- // Only update if the txn has changed
- nodeUpdate.setUpdateTransaction(true);
- }
- // Update auditable
- if (nodeAspects == null)
- {
- nodeAspects = getNodeAspects(nodeId);
- }
- if (nodeAspects.contains(ContentModel.ASPECT_AUDITABLE))
- {
- NodeRef oldNodeRef = oldNode.getNodeRef();
- if (policyBehaviourFilter.isEnabled(oldNodeRef, ContentModel.ASPECT_AUDITABLE))
- {
- // Make sure that auditable properties are present
- AuditablePropertiesEntity auditableProps = oldNode.getAuditableProperties();
- if (auditableProps == null)
- {
- auditableProps = new AuditablePropertiesEntity();
- }
- else
- {
- auditableProps = new AuditablePropertiesEntity(auditableProps);
- }
- long modifiedDateToleranceMs = 1000L;
-
- if (nodeUpdate.isUpdateTransaction())
- {
- // allow update cm:modified property for new transaction
- modifiedDateToleranceMs = 0L;
- }
-
- boolean updateAuditableProperties = auditableProps.setAuditValues(null, null, false, modifiedDateToleranceMs);
- nodeUpdate.setAuditableProperties(auditableProps);
- nodeUpdate.setUpdateAuditableProperties(updateAuditableProperties);
- }
- else if (nodeUpdate.getAuditableProperties() == null)
- {
- // cache the explicit setting of auditable properties when creating node (note: auditable aspect is not yet present)
- AuditablePropertiesEntity auditableProps = oldNode.getAuditableProperties();
- if (auditableProps != null)
- {
- nodeUpdate.setAuditableProperties(auditableProps); // Can reuse the locked instance
- nodeUpdate.setUpdateAuditableProperties(true);
- }
- }
- else
- {
- // ALF-4117: NodeDAO: Allow cm:auditable to be set
- // The nodeUpdate had auditable properties set, so we just use that directly
- nodeUpdate.setUpdateAuditableProperties(true);
- }
- }
- else
- {
- // Make sure that any auditable properties are removed
- AuditablePropertiesEntity auditableProps = oldNode.getAuditableProperties();
- if (auditableProps != null)
- {
- nodeUpdate.setAuditableProperties(null);
- nodeUpdate.setUpdateAuditableProperties(true);
- }
- }
-
- // Just bug out if nothing has changed
- if (!nodeUpdate.isUpdateAnything())
- {
- return false;
- }
-
- // The node is remaining in the current store
- int count = 0;
- Throwable concurrencyException = null;
- try
- {
- count = updateNode(nodeUpdate);
- }
- catch (Throwable e)
- {
- concurrencyException = e;
- }
- // Do concurrency check
- if (count != 1)
- {
- // Drop the value from the cache in case the cache is stale
- nodesCache.removeByKey(nodeId);
- nodesCache.removeByValue(nodeUpdate);
-
- throw new ConcurrencyFailureException("Failed to update node " + nodeId, concurrencyException);
- }
- else
- {
- // Check for wrap-around in the version number
- if (nodeUpdate.getVersion().equals(LONG_ZERO))
- {
- // The version was wrapped back to zero
- // The caches that are keyed by version are now unreliable
- propertiesCache.clear();
- aspectsCache.clear();
- parentAssocsCache.clear();
- }
- // Update the caches
- nodeUpdate.lock();
- nodesCache.setValue(nodeId, nodeUpdate);
- // The node's version has moved on so no need to invalidate caches
- }
-
- // Done
- if (isDebugEnabled)
- {
- logger.debug(
- "Updated Node: \n" +
- " OLD: " + oldNode + "\n" +
- " NEW: " + nodeUpdate);
- }
- return true;
- }
-
- @Override
- public void setNodeAclId(Long nodeId, Long aclId)
- {
- Node oldNode = getNodeNotNull(nodeId, true);
- NodeUpdateEntity nodeUpdateEntity = new NodeUpdateEntity();
- nodeUpdateEntity.setId(nodeId);
- nodeUpdateEntity.setAclId(aclId);
- nodeUpdateEntity.setUpdateAclId(true);
- updateNodeImpl(oldNode, nodeUpdateEntity, null);
- }
-
- public void setPrimaryChildrenSharedAclId(
- Long primaryParentNodeId,
- Long optionalOldSharedAlcIdInAdditionToNull,
- Long newSharedAclId)
- {
- Long txnId = getCurrentTransaction().getId();
- updatePrimaryChildrenSharedAclId(
- txnId,
- primaryParentNodeId,
- optionalOldSharedAlcIdInAdditionToNull,
- newSharedAclId);
- invalidateNodeChildrenCaches(primaryParentNodeId, true, false);
- }
-
- @Override
- public void deleteNode(Long nodeId)
- {
- // Delete and take the ACLs to the grave
- deleteNodeImpl(nodeId, true);
- }
-
- /**
- * Physical deletion of the node
- *
- * @param nodeId the node to delete
- * @param deleteAcl true to delete any associated ACLs otherwise
- * false if the ACLs get reused elsewhere
- */
- private void deleteNodeImpl(Long nodeId, boolean deleteAcl)
- {
- Node node = getNodeNotNull(nodeId, true);
- // Gather data for later
- Long aclId = node.getAclId();
- Set nodeAspects = getNodeAspects(nodeId);
-
- // Clean up content data
- Set contentQNames = new HashSet(dictionaryService.getAllProperties(DataTypeDefinition.CONTENT));
- Set contentQNamesToRemoveIds = qnameDAO.convertQNamesToIds(contentQNames, false);
- contentDataDAO.deleteContentDataForNode(nodeId, contentQNamesToRemoveIds);
-
- // Delete content usage deltas
- usageDAO.deleteDeltas(nodeId);
-
- // Handle sys:aspect_root
- if (nodeAspects.contains(ContentModel.ASPECT_ROOT))
- {
- StoreRef storeRef = node.getStore().getStoreRef();
- allRootNodesCache.remove(storeRef);
- }
-
- // Remove child associations (invalidate children)
- invalidateNodeChildrenCaches(nodeId, true, true);
- invalidateNodeChildrenCaches(nodeId, false, true);
-
- // Remove aspects
- deleteNodeAspects(nodeId, null);
-
- // Remove properties
- deleteNodeProperties(nodeId, (Set) null);
-
- // Remove subscriptions
- deleteSubscriptions(nodeId);
-
- // Delete the row completely:
- // ALF-12358: Concurrency: Possible to create association references to deleted nodes
- // There will be no way that any references can be made to a deleted node because we
- // are really going to delete it. However, for tracking purposes we need to maintain
- // a list of nodes deleted in the transaction. We store that information against a
- // new node of type 'sys:deleted'. This means that 'deleted' nodes are really just
- // orphaned (read standalone) nodes that remain invisible outside of the DAO.
- int deleted = deleteNodeById(nodeId);
- // We will always have to invalidate the cache for the node
- invalidateNodeCaches(nodeId);
- // Concurrency check
- if (deleted != 1)
- {
- // We thought that the row existed
- throw new ConcurrencyFailureException(
- "Failed to delete node: \n" +
- " Node: " + node);
- }
-
- // Remove ACLs
- if (deleteAcl && aclId != null)
- {
- aclDAO.deleteAclForNode(aclId);
- }
-
- // The node has been cleaned up. Now we recreate the node for index tracking purposes.
- // Use a 'deleted' type QName
- StoreEntity store = node.getStore();
- String uuid = node.getUuid();
- Long deletedQNameId = qnameDAO.getOrCreateQName(ContentModel.TYPE_DELETED).getFirst();
- Long defaultLocaleId = localeDAO.getOrCreateDefaultLocalePair().getFirst();
- Node deletedNode = newNodeImpl(store, uuid, deletedQNameId, defaultLocaleId, null, null, true);
- Long deletedNodeId = deletedNode.getId();
- // Store the original ID as a property
- Map trackingProps = Collections.singletonMap(ContentModel.PROP_ORIGINAL_ID, (Serializable) nodeId);
- setNodePropertiesImpl(deletedNodeId, trackingProps, true);
- }
-
- @Override
- public int purgeNodes(long fromTxnCommitTimeMs, long toTxnCommitTimeMs)
- {
- return deleteNodesByCommitTime(fromTxnCommitTimeMs, toTxnCommitTimeMs);
- }
-
- /*
- * Node Properties
- */
-
- public Map getNodeProperties(Long nodeId)
- {
- Map props = getNodePropertiesCached(nodeId);
- // Create a shallow copy to allow additions
- props = new HashMap(props);
-
- Node node = getNodeNotNull(nodeId, false);
- // Handle sys:referenceable
- ReferenceablePropertiesEntity.addReferenceableProperties(node, props);
- // Handle sys:localized
- LocalizedPropertiesEntity.addLocalizedProperties(localeDAO, node, props);
- // Handle cm:auditable
- if (hasNodeAspect(nodeId, ContentModel.ASPECT_AUDITABLE))
- {
- AuditablePropertiesEntity auditableProperties = node.getAuditableProperties();
- if (auditableProperties == null)
- {
- auditableProperties = new AuditablePropertiesEntity();
- }
- props.putAll(auditableProperties.getAuditableProperties());
- }
-
- // Wrap to ensure that we only clone values if the client attempts to modify
- // the map or retrieve values that might, themselves, be mutable
- props = new ValueProtectingMap(props, NodePropertyValue.IMMUTABLE_CLASSES);
-
- // Done
- if (isDebugEnabled)
- {
- logger.debug("Fetched properties for Node: \n" +
- " Node: " + nodeId + "\n" +
- " Props: " + props);
- }
- return props;
- }
-
- @Override
- public Serializable getNodeProperty(Long nodeId, QName propertyQName)
- {
- Serializable value = null;
- // We have to load the node for cm:auditable
- if (AuditablePropertiesEntity.isAuditableProperty(propertyQName))
- {
- Node node = getNodeNotNull(nodeId, false);
- AuditablePropertiesEntity auditableProperties = node.getAuditableProperties();
- if (auditableProperties != null)
- {
- value = auditableProperties.getAuditableProperty(propertyQName);
- }
- }
- else if (ReferenceablePropertiesEntity.isReferenceableProperty(propertyQName)) // sys:referenceable
- {
- Node node = getNodeNotNull(nodeId, false);
- value = ReferenceablePropertiesEntity.getReferenceableProperty(node, propertyQName);
- }
- else if (LocalizedPropertiesEntity.isLocalizedProperty(propertyQName)) // sys:localized
- {
- Node node = getNodeNotNull(nodeId, false);
- value = LocalizedPropertiesEntity.getLocalizedProperty(localeDAO, node, propertyQName);
- }
- else
- {
- Map props = getNodePropertiesCached(nodeId);
- // Wrap to ensure that we only clone values if the client attempts to modify
- // the map or retrieve values that might, themselves, be mutable
- props = new ValueProtectingMap(props, NodePropertyValue.IMMUTABLE_CLASSES);
- // The 'get' here will clone the value if it is mutable
- value = props.get(propertyQName);
- }
- // Done
- if (isDebugEnabled)
- {
- logger.debug("Fetched property for Node: \n" +
- " Node: " + nodeId + "\n" +
- " QName: " + propertyQName + "\n" +
- " Value: " + value);
- }
- return value;
- }
-
- /**
- * Does differencing to add and/or remove properties. Internally, the existing properties
- * will be retrieved and a difference performed to work out which properties need to be
- * created, updated or deleted.
- *
- * Note: The cached properties are not updated
- *
- * @param nodeId the node ID
- * @param newProps the properties to add or update
- * @param isAddOnly true if the new properties are just an update or
- * false if the properties are a complete set
- * @return Returns true if any properties were changed
- */
- private boolean setNodePropertiesImpl(
- Long nodeId,
- Map newProps,
- boolean isAddOnly)
- {
- if (isAddOnly && newProps.size() == 0)
- {
- return false; // No point adding nothing
- }
-
- // Get the current node
- Node node = getNodeNotNull(nodeId, false);
- // Create an update node
- NodeUpdateEntity nodeUpdate = new NodeUpdateEntity();
- nodeUpdate.setId(nodeId);
-
- // Copy inbound values
- newProps = new HashMap(newProps);
-
- // Copy cm:auditable
- if (!policyBehaviourFilter.isEnabled(node.getNodeRef(), ContentModel.ASPECT_AUDITABLE))
- {
- // Only bother if cm:auditable properties are present
- if (AuditablePropertiesEntity.hasAuditableProperty(newProps.keySet()))
- {
- AuditablePropertiesEntity auditableProps = node.getAuditableProperties();
- if (auditableProps == null)
- {
- auditableProps = new AuditablePropertiesEntity();
- }
- else
- {
- auditableProps = new AuditablePropertiesEntity(auditableProps); // Unlocked instance
- }
- boolean containedAuditProperties = auditableProps.setAuditValues(null, null, newProps);
- if (!containedAuditProperties)
- {
- // Double-check (previous hasAuditableProperty should cover it)
- // The behaviour is disabled, but no audit properties were passed in
- auditableProps = null;
- }
- nodeUpdate.setAuditableProperties(auditableProps);
- nodeUpdate.setUpdateAuditableProperties(true);
- }
- }
-
- // Remove cm:auditable
- newProps.keySet().removeAll(AuditablePropertiesEntity.getAuditablePropertyQNames());
-
- // Check if the sys:localized property is being changed
- Long oldNodeLocaleId = node.getLocaleId();
- Locale newLocale = DefaultTypeConverter.INSTANCE.convert(
- Locale.class,
- newProps.get(ContentModel.PROP_LOCALE));
- if (newLocale != null)
- {
- Long newNodeLocaleId = localeDAO.getOrCreateLocalePair(newLocale).getFirst();
- if (!newNodeLocaleId.equals(oldNodeLocaleId))
- {
- nodeUpdate.setLocaleId(newNodeLocaleId);
- nodeUpdate.setUpdateLocaleId(true);
- }
- }
- // else: a 'null' new locale is completely ignored. This is the behaviour we choose.
-
- // Remove sys:localized
- LocalizedPropertiesEntity.removeLocalizedProperties(node, newProps);
-
- // Remove sys:referenceable
- ReferenceablePropertiesEntity.removeReferenceableProperties(node, newProps);
- // Load the current properties.
- // This means that we have to go to the DB during cold-write operations,
- // but usually a write occurs after a node has been fetched of viewed in
- // some way by the client code. Loading the existing properties has the
- // advantage that the differencing code can eliminate unnecessary writes
- // completely.
- Map oldPropsCached = getNodePropertiesCached(nodeId); // Keep pristine for caching
- Map oldProps = new HashMap(oldPropsCached);
- // If we're adding, remove current properties that are not of interest
- if (isAddOnly)
- {
- oldProps.keySet().retainAll(newProps.keySet());
- }
- // We need to convert the new properties to our internally-used format,
- // which is compatible with model i.e. people may have passed in data
- // which needs to be converted to a model-compliant format. We do this
- // before comparisons to avoid false negatives.
- Map newPropsRaw = nodePropertyHelper.convertToPersistentProperties(newProps);
- newProps = nodePropertyHelper.convertToPublicProperties(newPropsRaw);
- // Now find out what's changed
- Map diff = EqualsHelper.getMapComparison(
- oldProps,
- newProps);
- // Keep track of properties to delete and add
- Set propsToDelete = new HashSet(oldProps.size()*2);
- Map propsToAdd = new HashMap(newProps.size() * 2);
- Set contentQNamesToDelete = new HashSet(5);
- for (Map.Entry entry : diff.entrySet())
- {
- QName qname = entry.getKey();
-
- PropertyDefinition removePropDef = dictionaryService.getProperty(qname);
- boolean isContent = (removePropDef != null &&
- removePropDef.getDataType().getName().equals(DataTypeDefinition.CONTENT));
-
- switch (entry.getValue())
- {
- case EQUAL:
- // Ignore
- break;
- case LEFT_ONLY:
- // Not in the new properties
- propsToDelete.add(qname);
- if (isContent)
- {
- contentQNamesToDelete.add(qname);
- }
- break;
- case NOT_EQUAL:
- // Must remove from the LHS
- propsToDelete.add(qname);
- if (isContent)
- {
- contentQNamesToDelete.add(qname);
- }
- // Fall through to load up the RHS
- case RIGHT_ONLY:
- // We're adding this
- Serializable value = newProps.get(qname);
- if (isContent && value != null)
- {
- ContentData newContentData = (ContentData) value;
- Long newContentDataId = contentDataDAO.createContentData(newContentData).getFirst();
- value = new ContentDataWithId(newContentData, newContentDataId);
- }
- propsToAdd.put(qname, value);
- break;
- default:
- throw new IllegalStateException("Unknown MapValueComparison: " + entry.getValue());
- }
- }
-
- boolean modifyProps = propsToDelete.size() > 0 || propsToAdd.size() > 0;
- boolean updated = modifyProps || nodeUpdate.isUpdateAnything();
-
- // Bring the node into the current transaction
- if (nodeUpdate.isUpdateAnything())
- {
- // We have to explicitly update the node (sys:locale or cm:auditable)
- if (updateNodeImpl(node, nodeUpdate, null))
- {
- // Copy the caches across
- NodeVersionKey nodeVersionKey = node.getNodeVersionKey();
- NodeVersionKey newNodeVersionKey = getNodeNotNull(nodeId, false).getNodeVersionKey();
- copyNodeAspectsCached(nodeVersionKey, newNodeVersionKey);
- copyNodePropertiesCached(nodeVersionKey, newNodeVersionKey);
- copyParentAssocsCached(node);
- }
- }
- else if (modifyProps)
- {
- // Touch the node; all caches are fine
- touchNode(nodeId, null, null, false, false, false);
- }
-
- // Touch to bring into current txn
- if (modifyProps)
- {
- // Clean up content properties
- try
- {
- if (contentQNamesToDelete.size() > 0)
- {
- Set contentQNameIdsToDelete = qnameDAO.convertQNamesToIds(contentQNamesToDelete, false);
- contentDataDAO.deleteContentDataForNode(nodeId, contentQNameIdsToDelete);
- }
- }
- catch (Throwable e)
- {
- throw new AlfrescoRuntimeException(
- "Failed to delete content properties: \n" +
- " Node: " + nodeId + "\n" +
- " Delete Tried: " + contentQNamesToDelete,
- e);
- }
-
- try
- {
- // Apply deletes
- Set propQNameIdsToDelete = qnameDAO.convertQNamesToIds(propsToDelete, true);
- deleteNodeProperties(nodeId, propQNameIdsToDelete);
- // Now create the raw properties for adding
- newPropsRaw = nodePropertyHelper.convertToPersistentProperties(propsToAdd);
- insertNodeProperties(nodeId, newPropsRaw);
- }
- catch (Throwable e)
- {
- // Don't trust the caches for the node
- invalidateNodeCaches(nodeId);
- // Focused error
- throw new AlfrescoRuntimeException(
- "Failed to write property deltas: \n" +
- " Node: " + nodeId + "\n" +
- " Old: " + oldProps + "\n" +
- " New: " + newProps + "\n" +
- " Diff: " + diff + "\n" +
- " Delete Tried: " + propsToDelete + "\n" +
- " Add Tried: " + propsToAdd,
- e);
- }
-
- // Build the properties to cache based on whether this is an append or replace
- Map propsToCache = null;
- if (isAddOnly)
- {
- // Copy cache properties for additions
- propsToCache = new HashMap(oldPropsCached);
- // Combine the old and new properties
- propsToCache.putAll(propsToAdd);
- }
- else
- {
- // Replace old properties
- propsToCache = newProps;
- propsToCache.putAll(propsToAdd); // Ensure correct types
- }
- // Update cache
- setNodePropertiesCached(nodeId, propsToCache);
- }
-
- // Done
- if (isDebugEnabled && updated)
- {
- logger.debug(
- "Modified node properties: " + nodeId + "\n" +
- " Removed: " + propsToDelete + "\n" +
- " Added: " + propsToAdd + "\n" +
- " Node Update: " + nodeUpdate);
- }
- return updated;
- }
-
- @Override
- public boolean setNodeProperties(Long nodeId, Map properties)
- {
- // Merge with current values
- boolean modified = setNodePropertiesImpl(nodeId, properties, false);
-
- // Done
- return modified;
- }
-
- @Override
- public boolean addNodeProperty(Long nodeId, QName qname, Serializable value)
- {
- // Copy inbound values
- Map newProps = new HashMap(3);
- newProps.put(qname, value);
- // Merge with current values
- boolean modified = setNodePropertiesImpl(nodeId, newProps, true);
-
- // Done
- return modified;
- }
-
- @Override
- public boolean addNodeProperties(Long nodeId, Map properties)
- {
- // Merge with current values
- boolean modified = setNodePropertiesImpl(nodeId, properties, true);
-
- // Done
- return modified;
- }
-
- @Override
- public boolean removeNodeProperties(Long nodeId, Set propertyQNames)
- {
- propertyQNames = new HashSet(propertyQNames);
- ReferenceablePropertiesEntity.removeReferenceableProperties(propertyQNames);
- if (propertyQNames.size() == 0)
- {
- return false; // sys:referenceable properties cannot be removed
- }
- LocalizedPropertiesEntity.removeLocalizedProperties(propertyQNames);
- if (propertyQNames.size() == 0)
- {
- return false; // sys:localized properties cannot be removed
- }
- Set qnameIds = qnameDAO.convertQNamesToIds(propertyQNames, false);
- int deleteCount = deleteNodeProperties(nodeId, qnameIds);
-
- if (deleteCount > 0)
- {
- // Touch the node; all caches are fine
- touchNode(nodeId, null, null, false, false, false);
- // Get cache props
- Map cachedProps = getNodePropertiesCached(nodeId);
- // Remove deleted properties
- Map props = new HashMap(cachedProps);
- props.keySet().removeAll(propertyQNames);
- // Update cache
- setNodePropertiesCached(nodeId, props);
- }
- // Done
- return deleteCount > 0;
- }
-
- @Override
- public boolean setModifiedDate(Long nodeId, Date modifiedDate)
- {
- return setModifiedProperties(nodeId, modifiedDate, null);
- }
-
- @Override
- public boolean setModifiedProperties(Long nodeId, Date modifiedDate, String modifiedBy) {
- // Do nothing if the node is not cm:auditable
- if (!hasNodeAspect(nodeId, ContentModel.ASPECT_AUDITABLE))
- {
- return false;
- }
- // Get the node
- Node node = getNodeNotNull(nodeId, false);
- NodeRef nodeRef = node.getNodeRef();
- // Get the existing auditable values
- AuditablePropertiesEntity auditableProps = node.getAuditableProperties();
- boolean dateChanged = false;
- if (auditableProps == null)
- {
- // The properties should be present
- auditableProps = new AuditablePropertiesEntity();
- auditableProps.setAuditValues(modifiedBy, modifiedDate, true, 1000L);
- dateChanged = true;
- }
- else
- {
- auditableProps = new AuditablePropertiesEntity(auditableProps);
- dateChanged = auditableProps.setAuditModified(modifiedDate, 1000L);
- if (dateChanged)
- {
- auditableProps.setAuditModifier(modifiedBy);
- }
- }
- if (dateChanged)
- {
- try
- {
- policyBehaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE);
- // Touch the node; all caches are fine
- return touchNode(nodeId, auditableProps, null, false, false, false);
- }
- finally
- {
- policyBehaviourFilter.enableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE);
- }
- }
- else
- {
- // Date did not advance
- return false;
- }
- }
-
- /**
- * @return Returns the read-only cached property map
- */
- private Map getNodePropertiesCached(Long nodeId)
- {
- NodeVersionKey nodeVersionKey = getNodeNotNull(nodeId, false).getNodeVersionKey();
- Pair> cacheEntry = propertiesCache.getByKey(nodeVersionKey);
- if (cacheEntry == null)
- {
- invalidateNodeCaches(nodeId);
- throw new DataIntegrityViolationException("Invalid node ID: " + nodeId);
- }
- // We have the properties from the cache
- Map cachedProperties = cacheEntry.getSecond();
- return cachedProperties;
- }
-
- /**
- * Update the node properties cache. The incoming properties will be wrapped to be
- * unmodifiable.
- *
- * NOTE: Incoming properties must exclude the cm:auditable properties
- */
- private void setNodePropertiesCached(Long nodeId, Map properties)
- {
- NodeVersionKey nodeVersionKey = getNodeNotNull(nodeId, false).getNodeVersionKey();
- propertiesCache.setValue(nodeVersionKey, Collections.unmodifiableMap(properties));
- }
-
- /**
- * Helper method to copy cache values from one key to another
- */
- private void copyNodePropertiesCached(NodeVersionKey from, NodeVersionKey to)
- {
- Map cacheEntry = propertiesCache.getValue(from);
- if (cacheEntry != null)
- {
- propertiesCache.setValue(to, cacheEntry);
- }
- }
-
- /**
- * Callback to cache node properties. The DAO callback only does the simple {@link #findByKey(Serializable)}.
- *
- * @author Derek Hulley
- * @since 3.4
- */
- private class PropertiesCallbackDAO extends EntityLookupCallbackDAOAdaptor, Serializable>
- {
- public Pair> createValue(Map value)
- {
- throw new UnsupportedOperationException("A node always has a 'map' of properties.");
- }
-
- public Pair> findByKey(NodeVersionKey nodeVersionKey)
- {
- Long nodeId = nodeVersionKey.getNodeId();
- Map> propsRawByNodeVersionKey = selectNodeProperties(nodeId);
- Map propsRaw = propsRawByNodeVersionKey.get(nodeVersionKey);
- if (propsRaw == null)
- {
- // Didn't find a match. Is this because there are none?
- if (propsRawByNodeVersionKey.size() == 0)
- {
- // This is OK. The node has no properties
- propsRaw = Collections.emptyMap();
- }
- else
- {
- // We found properties associated with a different node ID and version
- invalidateNodeCaches(nodeId);
- throw new DataIntegrityViolationException(
- "Detected stale node entry: " + nodeVersionKey +
- " (now " + propsRawByNodeVersionKey.keySet() + ")");
- }
- }
- // Convert to public properties
- Map props = nodePropertyHelper.convertToPublicProperties(propsRaw);
- // Done
- return new Pair>(nodeVersionKey, Collections.unmodifiableMap(props));
- }
- }
-
- /*
- * Aspects
- */
-
- @Override
- public Set getNodeAspects(Long nodeId)
- {
- Set nodeAspects = getNodeAspectsCached(nodeId);
- // Nodes are always referenceable
- nodeAspects.add(ContentModel.ASPECT_REFERENCEABLE);
- // Nodes are always localized
- nodeAspects.add(ContentModel.ASPECT_LOCALIZED);
- return nodeAspects;
- }
-
- @Override
- public boolean hasNodeAspect(Long nodeId, QName aspectQName)
- {
- if (aspectQName.equals(ContentModel.ASPECT_REFERENCEABLE))
- {
- // Nodes are always referenceable
- return true;
- }
- if (aspectQName.equals(ContentModel.ASPECT_LOCALIZED))
- {
- // Nodes are always localized
- return true;
- }
- Set nodeAspects = getNodeAspectsCached(nodeId);
- return nodeAspects.contains(aspectQName);
- }
-
- @Override
- public boolean addNodeAspects(Long nodeId, Set aspectQNames)
- {
- if (aspectQNames.size() == 0)
- {
- return false;
- }
- // Copy the inbound set
- Set aspectQNamesToAdd = new HashSet(aspectQNames);
- // Get existing
- Set existingAspectQNames = getNodeAspectsCached(nodeId);
- // Find out what needs adding
- aspectQNamesToAdd.removeAll(existingAspectQNames);
- aspectQNamesToAdd.remove(ContentModel.ASPECT_REFERENCEABLE); // Implicit
- aspectQNamesToAdd.remove(ContentModel.ASPECT_LOCALIZED); // Implicit
- if (aspectQNamesToAdd.isEmpty())
- {
- // Nothing to do
- return false;
- }
- // Add them
- Set aspectQNameIds = qnameDAO.convertQNamesToIds(aspectQNamesToAdd, true);
- startBatch();
- try
- {
- for (Long aspectQNameId : aspectQNameIds)
- {
- insertNodeAspect(nodeId, aspectQNameId);
- }
- }
- catch (RuntimeException e)
- {
- // This could be because the cache is out of date
- invalidateNodeCaches(nodeId);
- throw e;
- }
- finally
- {
- executeBatch();
- }
-
- // Collate the new aspect set, so that touch recognizes the addtion of cm:auditable
- Set newAspectQNames = new HashSet(existingAspectQNames);
- newAspectQNames.addAll(aspectQNamesToAdd);
-
- // Handle sys:aspect_root
- if (aspectQNames.contains(ContentModel.ASPECT_ROOT))
- {
- // invalidate root nodes cache for the store
- StoreRef storeRef = getNodeNotNull(nodeId, false).getStore().getStoreRef();
- allRootNodesCache.remove(storeRef);
- // Touch the node; parent assocs need invalidation
- touchNode(nodeId, null, newAspectQNames, false, false, true);
- }
- else
- {
- // Touch the node; all caches are fine
- touchNode(nodeId, null, newAspectQNames, false, false, false);
- }
-
- // Manually update the cache
- setNodeAspectsCached(nodeId, newAspectQNames);
-
- // Done
- return true;
- }
-
- public boolean removeNodeAspects(Long nodeId)
- {
- Set newAspectQNames = Collections.emptySet();
-
- // Touch the node; all caches are fine
- touchNode(nodeId, null, newAspectQNames, false, false, false);
-
- // Just delete all the node's aspects
- int deleteCount = deleteNodeAspects(nodeId, null);
-
- // Manually update the cache
- setNodeAspectsCached(nodeId, newAspectQNames);
-
- // Done
- return deleteCount > 0;
- }
-
- @Override
- public boolean removeNodeAspects(Long nodeId, Set aspectQNames)
- {
- if (aspectQNames.size() == 0)
- {
- return false;
- }
- // Get the current aspects
- Set existingAspectQNames = getNodeAspects(nodeId);
-
- // Collate the new set of aspects so that touch works correctly against cm:auditable
- Set newAspectQNames = new HashSet(existingAspectQNames);
- newAspectQNames.removeAll(aspectQNames);
-
- // Touch the node; all caches are fine
- touchNode(nodeId, null, newAspectQNames, false, false, false);
-
- // Now remove each aspect
- Set aspectQNameIdsToRemove = qnameDAO.convertQNamesToIds(aspectQNames, false);
- int deleteCount = deleteNodeAspects(nodeId, aspectQNameIdsToRemove);
- if (deleteCount == 0)
- {
- return false;
- }
-
- // Handle sys:aspect_root
- if (aspectQNames.contains(ContentModel.ASPECT_ROOT))
- {
- // invalidate root nodes cache for the store
- StoreRef storeRef = getNodeNotNull(nodeId, false).getStore().getStoreRef();
- allRootNodesCache.remove(storeRef);
- // Touch the node; parent assocs need invalidation
- touchNode(nodeId, null, newAspectQNames, false, false, true);
- }
- else
- {
- // Touch the node; all caches are fine
- touchNode(nodeId, null, newAspectQNames, false, false, false);
- }
-
- // Manually update the cache
- setNodeAspectsCached(nodeId, newAspectQNames);
-
- // Done
- return deleteCount > 0;
- }
-
- @Override
- public void getNodesWithAspects(
- Set aspectQNames,
- Long minNodeId, Long maxNodeId,
- NodeRefQueryCallback resultsCallback)
- {
- Set qnameIdsSet = qnameDAO.convertQNamesToIds(aspectQNames, false);
- if (qnameIdsSet.size() == 0)
- {
- // No point running a query
- return;
- }
- List qnameIds = new ArrayList(qnameIdsSet);
- selectNodesWithAspects(qnameIds, minNodeId, maxNodeId, resultsCallback);
- }
-
- @Override
- public void getNodesWithAspects(
- Set aspectQNames,
- Long minNodeId, Long maxNodeId, boolean ordered,
- NodeRefQueryCallback resultsCallback)
- {
- Set qnameIdsSet = qnameDAO.convertQNamesToIds(aspectQNames, false);
- if (qnameIdsSet.size() == 0)
- {
- // No point running a query
- return;
- }
- List qnameIds = new ArrayList(qnameIdsSet);
- selectNodesWithAspects(qnameIds, minNodeId, maxNodeId, ordered, resultsCallback);
- }
-
- @Override
- public void getNodesWithAspects(
- Set aspectQNames,
- Long minNodeId, Long maxNodeId, boolean ordered,
- int maxResults,
- NodeRefQueryCallback resultsCallback)
- {
- Set qnameIdsSet = qnameDAO.convertQNamesToIds(aspectQNames, false);
- if (qnameIdsSet.isEmpty())
- {
- // No point running a query
- return;
- }
- List qnameIds = new ArrayList<>(qnameIdsSet);
- selectNodesWithAspects(qnameIds, minNodeId, maxNodeId, ordered, maxResults, resultsCallback);
- }
-
- /**
- * @return Returns a writable copy of the cached aspects set
- */
- private Set getNodeAspectsCached(Long nodeId)
- {
- NodeVersionKey nodeVersionKey = getNodeNotNull(nodeId, false).getNodeVersionKey();
- Pair> cacheEntry = aspectsCache.getByKey(nodeVersionKey);
- if (cacheEntry == null)
- {
- invalidateNodeCaches(nodeId);
- throw new DataIntegrityViolationException("Invalid node ID: " + nodeId);
- }
- return new HashSet(cacheEntry.getSecond());
- }
-
- /**
- * Update the node aspects cache. The incoming set will be wrapped to be unmodifiable.
- */
- private void setNodeAspectsCached(Long nodeId, Set aspects)
- {
- NodeVersionKey nodeVersionKey = getNodeNotNull(nodeId, false).getNodeVersionKey();
- aspectsCache.setValue(nodeVersionKey, Collections.unmodifiableSet(aspects));
- }
-
- /**
- * Helper method to copy cache values from one key to another
- */
- private void copyNodeAspectsCached(NodeVersionKey from, NodeVersionKey to)
- {
- Set cacheEntry = aspectsCache.getValue(from);
- if (cacheEntry != null)
- {
- aspectsCache.setValue(to, cacheEntry);
- }
- }
-
- /**
- * Callback to cache node aspects. The DAO callback only does the simple {@link #findByKey(Serializable)}.
- *
- * @author Derek Hulley
- * @since 3.4
- */
- private class AspectsCallbackDAO extends EntityLookupCallbackDAOAdaptor, Serializable>
- {
- public Pair> createValue(Set value)
- {
- throw new UnsupportedOperationException("A node always has a 'set' of aspects.");
- }
-
- public Pair> findByKey(NodeVersionKey nodeVersionKey)
- {
- Long nodeId = nodeVersionKey.getNodeId();
- Set nodeIds = Collections.singleton(nodeId);
- Map> nodeAspectQNameIdsByVersionKey = selectNodeAspects(nodeIds);
- Set nodeAspectQNames = nodeAspectQNameIdsByVersionKey.get(nodeVersionKey);
- if (nodeAspectQNames == null)
- {
- // Didn't find a match. Is this because there are none?
- if (nodeAspectQNameIdsByVersionKey.size() == 0)
- {
- // This is OK. The node has no properties
- nodeAspectQNames = Collections.emptySet();
- }
- else
- {
- // We found properties associated with a different node ID and version
- invalidateNodeCaches(nodeId);
- throw new DataIntegrityViolationException(
- "Detected stale node entry: " + nodeVersionKey +
- " (now " + nodeAspectQNameIdsByVersionKey.keySet() + ")");
- }
- }
- // Done
- return new Pair>(nodeVersionKey, Collections.unmodifiableSet(nodeAspectQNames));
- }
- }
-
- /*
- * Node assocs
- */
-
- @Override
- public Long newNodeAssoc(Long sourceNodeId, Long targetNodeId, QName assocTypeQName, int assocIndex)
- {
- if (assocIndex == 0)
- {
- throw new IllegalArgumentException("Index is 1-based, or -1 to indicate 'next value'.");
- }
-
- // Touch the node; all caches are fine
- touchNode(sourceNodeId, null, null, false, false, false);
-
- // Resolve type QName
- Long assocTypeQNameId = qnameDAO.getOrCreateQName(assocTypeQName).getFirst();
-
- // Get the current max; we will need this no matter what
- if (assocIndex <= 0)
- {
- int maxIndex = selectNodeAssocMaxIndex(sourceNodeId, assocTypeQNameId);
- assocIndex = maxIndex + 1;
- }
-
- Long result = null;
- Savepoint savepoint = controlDAO.createSavepoint("NodeService.newNodeAssoc");
- try
- {
- result = insertNodeAssoc(sourceNodeId, targetNodeId, assocTypeQNameId, assocIndex);
- controlDAO.releaseSavepoint(savepoint);
- return result;
- }
- catch (Throwable e)
- {
- controlDAO.rollbackToSavepoint(savepoint);
- if (isDebugEnabled)
- {
- logger.debug(
- "Failed to insert node association: \n" +
- " sourceNodeId: " + sourceNodeId + "\n" +
- " targetNodeId: " + targetNodeId + "\n" +
- " assocTypeQName: " + assocTypeQName + "\n" +
- " assocIndex: " + assocIndex,
- e);
- }
- throw new AssociationExistsException(sourceNodeId, targetNodeId, assocTypeQName);
- }
- }
-
- @Override
- public void setNodeAssocIndex(Long id, int assocIndex)
- {
- int updated = updateNodeAssoc(id, assocIndex);
- if (updated != 1)
- {
- throw new ConcurrencyFailureException("Expected to update exactly one row: " + id);
- }
- }
-
- @Override
- public int removeNodeAssoc(Long sourceNodeId, Long targetNodeId, QName assocTypeQName)
- {
- Pair assocTypeQNamePair = qnameDAO.getQName(assocTypeQName);
- if (assocTypeQNamePair == null)
- {
- // Never existed
- return 0;
- }
-
- Long assocTypeQNameId = assocTypeQNamePair.getFirst();
- int deleted = deleteNodeAssoc(sourceNodeId, targetNodeId, assocTypeQNameId);
- if (deleted > 0)
- {
- // Touch the node; all caches are fine
- touchNode(sourceNodeId, null, null, false, false, false);
- }
- return deleted;
- }
-
- @Override
- public int removeNodeAssocs(List ids)
- {
- int toDelete = ids.size();
- if (toDelete == 0)
- {
- return 0;
- }
- int deleted = deleteNodeAssocs(ids);
- if (toDelete != deleted)
- {
- throw new ConcurrencyFailureException("Deleted " + deleted + " but expected " + toDelete);
- }
- return deleted;
- }
-
- @Override
- public Collection> getNodeAssocsToAndFrom(Long nodeId)
- {
- List nodeAssocEntities = selectNodeAssocs(nodeId);
- List> results = new ArrayList>(nodeAssocEntities.size());
- for (NodeAssocEntity nodeAssocEntity : nodeAssocEntities)
- {
- Long assocId = nodeAssocEntity.getId();
- AssociationRef assocRef = nodeAssocEntity.getAssociationRef(qnameDAO);
- results.add(new Pair(assocId, assocRef));
- }
- return results;
- }
-
- @Override
- public Collection> getSourceNodeAssocs(Long targetNodeId, QName typeQName)
- {
- Long typeQNameId = null;
- if (typeQName != null)
- {
- Pair typeQNamePair = qnameDAO.getQName(typeQName);
- if (typeQNamePair == null)
- {
- // No such QName
- return Collections.emptyList();
- }
- typeQNameId = typeQNamePair.getFirst();
- }
- List nodeAssocEntities = selectNodeAssocsByTarget(targetNodeId, typeQNameId);
- List> results = new ArrayList>(nodeAssocEntities.size());
- for (NodeAssocEntity nodeAssocEntity : nodeAssocEntities)
- {
- Long assocId = nodeAssocEntity.getId();
- AssociationRef assocRef = nodeAssocEntity.getAssociationRef(qnameDAO);
- results.add(new Pair(assocId, assocRef));
- }
- return results;
- }
-
- @Override
- public Collection> getTargetNodeAssocs(Long sourceNodeId, QName typeQName)
- {
- Long typeQNameId = null;
- if (typeQName != null)
- {
- Pair typeQNamePair = qnameDAO.getQName(typeQName);
- if (typeQNamePair == null)
- {
- // No such QName
- return Collections.emptyList();
- }
- typeQNameId = typeQNamePair.getFirst();
- }
- List nodeAssocEntities = selectNodeAssocsBySource(sourceNodeId, typeQNameId);
- List> results = new ArrayList>(nodeAssocEntities.size());
- for (NodeAssocEntity nodeAssocEntity : nodeAssocEntities)
- {
- Long assocId = nodeAssocEntity.getId();
- AssociationRef assocRef = nodeAssocEntity.getAssociationRef(qnameDAO);
- results.add(new Pair(assocId, assocRef));
- }
- return results;
- }
-
- @Override
- public Collection> getTargetAssocsByPropertyValue(Long sourceNodeId, QName typeQName, QName propertyQName, Serializable propertyValue)
- {
- Long typeQNameId = null;
- if (typeQName != null)
- {
- Pair typeQNamePair = qnameDAO.getQName(typeQName);
- if (typeQNamePair == null)
- {
- // No such QName
- return Collections.emptyList();
- }
- typeQNameId = typeQNamePair.getFirst();
- }
-
- Long propertyQNameId = null;
- NodePropertyValue nodeValue = null;
- if (propertyQName != null)
- {
-
- Pair propQNamePair = qnameDAO.getQName(propertyQName);
- if (propQNamePair == null)
- {
- // No such QName
- return Collections.emptyList();
- }
-
- propertyQNameId = propQNamePair.getFirst();
-
- PropertyDefinition propertyDef = dictionaryService.getProperty(propertyQName);
-
- nodeValue = nodePropertyHelper.makeNodePropertyValue(propertyDef, propertyValue);
- if (nodeValue != null)
- {
- switch (nodeValue.getPersistedType())
- {
- case 1: // Boolean
- case 3: // long
- case 5: // double
- case 6: // string
- // no floats due to the range errors testing equality on a float.
- break;
- default:
- throw new IllegalArgumentException("method not supported for persisted value type " + nodeValue.getPersistedType());
- }
- }
- }
-
- List nodeAssocEntities = selectNodeAssocsBySourceAndPropertyValue(sourceNodeId, typeQNameId, propertyQNameId, nodeValue);
-
- // Create custom result
- List> results = new ArrayList>(nodeAssocEntities.size());
- for (NodeAssocEntity nodeAssocEntity : nodeAssocEntities)
- {
- Long assocId = nodeAssocEntity.getId();
- AssociationRef assocRef = nodeAssocEntity.getAssociationRef(qnameDAO);
- results.add(new Pair(assocId, assocRef));
- }
- return results;
- }
-
- @Override
- public Pair getNodeAssocOrNull(Long assocId)
- {
- NodeAssocEntity nodeAssocEntity = selectNodeAssocById(assocId);
- if (nodeAssocEntity == null)
- {
- return null;
- }
- else
- {
- AssociationRef assocRef = nodeAssocEntity.getAssociationRef(qnameDAO);
- return new Pair(assocId, assocRef);
- }
- }
-
- @Override
- public Pair getNodeAssoc(Long assocId)
- {
- Pair ret = getNodeAssocOrNull(assocId);
- if (ret == null)
- {
- throw new ConcurrencyFailureException("Assoc ID does not point to a valid association: " + assocId);
- }
- else
- {
- return ret;
- }
- }
-
- /*
- * Child assocs
- */
-
- private ChildAssocEntity newChildAssocImpl(
- Long parentNodeId,
- Long childNodeId,
- boolean isPrimary,
- final QName assocTypeQName,
- QName assocQName,
- final String childNodeName,
- boolean allowDeletedChild)
- {
- Assert.notNull(parentNodeId, "parentNodeId");
- Assert.notNull(childNodeId, "childNodeId");
- Assert.notNull(assocTypeQName, "assocTypeQName");
- Assert.notNull(assocQName, "assocQName");
- Assert.notNull(childNodeName, "childNodeName");
-
- // Get parent and child nodes. We need them later, so just get them now.
- final Node parentNode = getNodeNotNull(parentNodeId, true);
- final Node childNode = getNodeNotNull(childNodeId, !allowDeletedChild);
-
- final ChildAssocEntity assoc = new ChildAssocEntity();
- // Parent node
- assoc.setParentNode(new NodeEntity(parentNode));
- // Child node
- assoc.setChildNode(new NodeEntity(childNode));
- // Type QName
- assoc.setTypeQNameAll(qnameDAO, assocTypeQName, true);
- // Child node name
- assoc.setChildNodeNameAll(dictionaryService, assocTypeQName, childNodeName);
- // QName
- assoc.setQNameAll(qnameDAO, assocQName, true);
- // Primary
- assoc.setPrimary(isPrimary);
- // Index
- assoc.setAssocIndex(-1);
-
- Long assocId = newChildAssocInsert(assoc, assocTypeQName, childNodeName);
-
- // Persist it
- assoc.setId(assocId);
-
- // Primary associations accompany new nodes, so we only have to bring the
- // node into the current transaction for secondary associations
- if (!isPrimary)
- {
- updateNode(childNodeId, null, null);
- }
-
- // Done
- if (isDebugEnabled)
- {
- logger.debug("Created child association: " + assoc);
- }
- return assoc;
- }
-
- protected Long newChildAssocInsert(final ChildAssocEntity assoc, final QName assocTypeQName, final String childNodeName)
- {
- // Because we are retrying in-transaction i.e. absorbing exceptions, we need partial rollback &/or via savepoint if needed (eg. PostgreSQL)
- RetryingCallback callback = new RetryingCallback()
- {
- public Long execute() throws Throwable
- {
- return newChildAssocInsertImpl(assoc, assocTypeQName, childNodeName);
- }
- };
- Long assocId = childAssocRetryingHelper.doWithRetry(callback);
- return assocId;
- }
-
- protected Long newChildAssocInsertImpl(final ChildAssocEntity assoc, final QName assocTypeQName, final String childNodeName)
- {
- Savepoint savepoint = controlDAO.createSavepoint("DuplicateChildNodeNameException");
- try
- {
- Long id = insertChildAssoc(assoc);
- controlDAO.releaseSavepoint(savepoint);
- return id;
- }
- catch (Throwable e)
- {
- controlDAO.rollbackToSavepoint(savepoint);
- // DuplicateChildNodeNameException implements DoNotRetryException.
-
- // Allow real DB concurrency issues (e.g. DeadlockLoserDataAccessException) straight through for a retry
- if (e instanceof ConcurrencyFailureException)
- {
- throw e;
- }
-
- // There are some cases - FK violations, specifically - where we DO actually want to retry.
- // Detecting this is done by looking for the related FK names, 'fk_alf_cass_*' in the error message
- String lowerMsg = e.getMessage().toLowerCase();
- if (lowerMsg.contains("fk_alf_cass_"))
- {
- throw new ConcurrencyFailureException("FK violation updating primary parent association:" + assoc, e);
- }
-
- // We assume that this is from the child cm:name constraint violation
- throw new DuplicateChildNodeNameException(
- assoc.getParentNode().getNodeRef(),
- assocTypeQName,
- childNodeName,
- e);
- }
- }
-
- @Override
- public Pair newChildAssoc(
- Long parentNodeId,
- Long childNodeId,
- QName assocTypeQName,
- QName assocQName,
- String childNodeName)
- {
- ParentAssocsInfo parentAssocInfo = getParentAssocsCached(childNodeId);
- // Create it
- ChildAssocEntity assoc = newChildAssocImpl(
- parentNodeId, childNodeId, false, assocTypeQName, assocQName, childNodeName, false);
- Long assocId = assoc.getId();
- // Touch the node; parent assocs have been updated
- touchNode(childNodeId, null, null, false, false, true);
- // update cache
- parentAssocInfo = parentAssocInfo.addAssoc(assocId, assoc);
- setParentAssocsCached(childNodeId, parentAssocInfo);
- // Done
- return assoc.getPair(qnameDAO);
- }
-
- @Override
- public void deleteChildAssoc(Long assocId)
- {
- ChildAssocEntity assoc = selectChildAssoc(assocId);
- if (assoc == null)
- {
- throw new ConcurrencyFailureException(
- "Child association not found: " + assocId + ". A concurrency violation is likely.\n" +
- "This can also occur if code reacts to 'beforeDelete' callbacks and pre-emptively deletes associations \n" +
- "that are about to be cascade-deleted. The 'onDelete' phase then fails to delete the association.\n" +
- "See links on issue ALF-12358."); // TODO: Get docs URL
- }
- // Update cache
- Long childNodeId = assoc.getChildNode().getId();
- ParentAssocsInfo parentAssocInfo = getParentAssocsCached(childNodeId);
- // Delete it
- List assocIds = Collections.singletonList(assocId);
- int count = deleteChildAssocs(assocIds);
- if (count != 1)
- {
- throw new ConcurrencyFailureException("Child association not deleted: " + assocId);
- }
- // Touch the node; parent assocs have been updated
- touchNode(childNodeId, null, null, false, false, true);
- // Update cache
- parentAssocInfo = parentAssocInfo.removeAssoc(assocId);
- setParentAssocsCached(childNodeId, parentAssocInfo);
- }
-
- @Override
- public int setChildAssocIndex(Long parentNodeId, Long childNodeId, QName assocTypeQName, QName assocQName, int index)
- {
- int count = updateChildAssocIndex(parentNodeId, childNodeId, assocTypeQName, assocQName, index);
- if (count > 0)
- {
- // Touch the node; parent assocs are out of sync
- touchNode(childNodeId, null, null, false, false, true);
- }
- return count;
- }
-
- /**
- * TODO: See about pulling automatic cm:name update logic into this DAO
- */
- @Override
- public void setChildAssocsUniqueName(Long childNodeId, String childName)
- {
- Integer count = setChildAssocsUniqueNameImpl(childNodeId, childName);
-
- if (count > 0)
- {
- // Touch the node; parent assocs are out of sync
- touchNode(childNodeId, null, null, false, false, true);
- }
-
- if (isDebugEnabled)
- {
- logger.debug(
- "Updated cm:name to parent assocs: \n" +
- " Node: " + childNodeId + "\n" +
- " Name: " + childName + "\n" +
- " Updated: " + count);
- }
- }
-
- protected int setChildAssocsUniqueNameImpl(final Long childNodeId, final String childName)
- {
- // Because we are retrying in-transaction i.e. absorbing exceptions, we need partial rollback &/or via savepoint if needed (eg. PostgreSQL)
- RetryingCallback callback = new RetryingCallback()
- {
- public Integer execute() throws Throwable
- {
- return updateChildAssocUniqueNameImpl(childNodeId, childName);
- }
- };
- return childAssocRetryingHelper.doWithRetry(callback);
- }
-
- protected int updateChildAssocUniqueNameImpl(final Long childNodeId, final String childName)
- {
- int total = 0;
- Savepoint savepoint = controlDAO.createSavepoint("DuplicateChildNodeNameException");
- try
- {
- for (ChildAssocEntity parentAssoc : getParentAssocsCached(childNodeId).getParentAssocs().values())
- {
- // Subtlety: We only update those associations for which name uniqueness checking is enforced.
- // Such associations have a positive CRC
- if (parentAssoc.getChildNodeNameCrc() <= 0)
- {
- continue;
- }
- Pair oldTypeQnamePair = qnameDAO.getQName(parentAssoc.getTypeQNameId());
- // Ensure we invalidate the name cache (the child version key might not be 'bumped' by the next
- // 'touch')
- if (oldTypeQnamePair != null)
- {
- childByNameCache.remove(new ChildByNameKey(parentAssoc.getParentNode().getId(),
- oldTypeQnamePair.getSecond(), parentAssoc.getChildNodeName()));
- }
- int count = updateChildAssocUniqueName(parentAssoc.getId(), childName);
- if (count <= 0)
- {
- // Should not be attempting to delete a deleted node
- throw new ConcurrencyFailureException("Failed to update an existing parent association "
- + parentAssoc.getId());
- }
- total += count;
- }
- controlDAO.releaseSavepoint(savepoint);
- return total;
- }
- catch (Throwable e)
- {
- controlDAO.rollbackToSavepoint(savepoint);
- // We assume that this is from the child cm:name constraint violation
- throw new DuplicateChildNodeNameException(null, null, childName, e);
- }
- }
-
- @Override
- public Pair getChildAssoc(Long assocId)
- {
- ChildAssocEntity assoc = selectChildAssoc(assocId);
- if (assoc == null)
- {
- throw new ConcurrencyFailureException("Child association not found: " + assocId);
- }
- return assoc.getPair(qnameDAO);
- }
-
- @Override
- public List getPrimaryChildrenAcls(Long nodeId)
- {
- return selectPrimaryChildAcls(nodeId);
- }
-
- @Override
- public Pair getChildAssoc(
- Long parentNodeId,
- Long childNodeId,
- QName assocTypeQName,
- QName assocQName)
- {
- List assocs = selectChildAssoc(parentNodeId, childNodeId, assocTypeQName, assocQName);
- if (assocs.size() == 0)
- {
- return null;
- }
- else if (assocs.size() == 1)
- {
- return assocs.get(0).getPair(qnameDAO);
- }
- // Keep the primary association or, if there isn't one, the association with the smallest ID
- Map assocsToDeleteById = new HashMap(assocs.size() * 2);
- Long minId = null;
- Long primaryId = null;
- for (ChildAssocEntity assoc : assocs)
- {
- // First store it
- Long assocId = assoc.getId();
- assocsToDeleteById.put(assocId, assoc);
- if (minId == null || minId.compareTo(assocId) > 0)
- {
- minId = assocId;
- }
- if (assoc.isPrimary())
- {
- primaryId = assocId;
- }
- }
- // Remove either the primary or min assoc
- Long assocToKeepId = primaryId == null ? minId : primaryId;
- ChildAssocEntity assocToKeep = assocsToDeleteById.remove(assocToKeepId);
- // If the current transaction allows, remove the other associations
- if (AlfrescoTransactionSupport.getTransactionReadState() == TxnReadState.TXN_READ_WRITE)
- {
- for (Long assocIdToDelete : assocsToDeleteById.keySet())
- {
- deleteChildAssoc(assocIdToDelete);
- }
- }
- // Done
- return assocToKeep.getPair(qnameDAO);
- }
-
- /**
- * Callback that applies node preloading if required.
- *
- * Instances must be used and discarded per query.
- *
- * @author Derek Hulley
- * @since 3.4
- */
- private class ChildAssocRefBatchingQueryCallback implements ChildAssocRefQueryCallback
- {
- private final ChildAssocRefQueryCallback callback;
- private final boolean preload;
- private final List nodeRefs;
- /**
- * @param callback the callback to batch around
- */
- private ChildAssocRefBatchingQueryCallback(ChildAssocRefQueryCallback callback)
- {
- this.callback = callback;
- this.preload = callback.preLoadNodes();
- if (preload)
- {
- nodeRefs = new LinkedList(); // No memory required
- }
- else
- {
- nodeRefs = null; // No list needed
- }
- }
- /**
- * @throws UnsupportedOperationException always
- */
- public boolean preLoadNodes()
- {
- throw new UnsupportedOperationException("Expected to be used internally only.");
- }
- /**
- * Defers to delegate
- */
- @Override
- public boolean orderResults()
- {
- return callback.orderResults();
- }
- /**
- * {@inheritDoc}
- */
- public boolean handle(
- Pair childAssocPair,
- Pair parentNodePair,
- Pair childNodePair)
- {
- if (preload)
- {
- nodeRefs.add(childNodePair.getSecond());
- }
- return callback.handle(childAssocPair, parentNodePair, childNodePair);
- }
- public void done()
- {
- // Finish the batch
- if (preload && nodeRefs.size() > 0)
- {
- cacheNodes(nodeRefs);
- nodeRefs.clear();
- }
- // Done
- callback.done();
- }
- }
-
- @Override
- public void getChildAssocs(
- Long parentNodeId,
- Long childNodeId,
- QName assocTypeQName,
- QName assocQName,
- Boolean isPrimary,
- Boolean sameStore,
- ChildAssocRefQueryCallback resultsCallback)
- {
- selectChildAssocs(
- parentNodeId, childNodeId,
- assocTypeQName, assocQName, isPrimary, sameStore,
- new ChildAssocRefBatchingQueryCallback(resultsCallback));
- }
-
- @Override
- public void getChildAssocs(
- Long parentNodeId,
- QName assocTypeQName,
- QName assocQName,
- int maxResults,
- ChildAssocRefQueryCallback resultsCallback)
- {
- selectChildAssocs(
- parentNodeId,
- assocTypeQName,
- assocQName,
- maxResults,
- new ChildAssocRefBatchingQueryCallback(resultsCallback));
- }
-
- @Override
- public void getChildAssocs(Long parentNodeId, Set assocTypeQNames, ChildAssocRefQueryCallback resultsCallback)
- {
- switch (assocTypeQNames.size())
- {
- case 0:
- return; // No results possible
- case 1:
- QName assocTypeQName = assocTypeQNames.iterator().next();
- selectChildAssocs(
- parentNodeId, null, assocTypeQName, (QName) null, null, null,
- new ChildAssocRefBatchingQueryCallback(resultsCallback));
- break;
- default:
- selectChildAssocs(
- parentNodeId, assocTypeQNames,
- new ChildAssocRefBatchingQueryCallback(resultsCallback));
- }
- }
-
- /**
- * Checks a cache and then queries.
- *
- * Note: If we were to cach misses, then we would have to ensure that the cache is
- * kept up to date whenever any affection association is changed. This is actually
- * not possible without forcing the cache to be fully clustered. So to
- * avoid clustering the cache, we instead watch the node child version,
- * which relies on a cache that is already clustered.
- */
- @Override
- public Pair getChildAssoc(Long parentNodeId, QName assocTypeQName, String childName)
- {
- ChildByNameKey key = new ChildByNameKey(parentNodeId, assocTypeQName, childName);
- ChildAssocEntity assoc = childByNameCache.get(key);
- boolean query = false;
- if (assoc == null)
- {
- query = true;
- }
- else
- {
- // Check that the resultant child node has not moved on
- Node childNode = assoc.getChildNode();
- Long childNodeId = childNode.getId();
- NodeVersionKey childNodeVersionKey = childNode.getNodeVersionKey();
- Pair childNodeFromCache = nodesCache.getByKey(childNodeId);
- if (childNodeFromCache == null)
- {
- // Child node no longer exists (or never did)
- query = true;
- }
- else
- {
- NodeVersionKey childNodeFromCacheVersionKey = childNodeFromCache.getSecond().getNodeVersionKey();
- if (!childNodeFromCacheVersionKey.equals(childNodeVersionKey))
- {
- // The child node has moved on. We don't know why, but must query again.
- query = true;
- }
- }
- }
- if (query)
- {
- assoc = selectChildAssoc(parentNodeId, assocTypeQName, childName);
- if (assoc != null)
- {
- childByNameCache.put(key, assoc);
- }
- else
- {
- // We do not cache misses. See javadoc.
- }
- }
- // Now return, checking the assoc's ID for null
- return assoc == null ? null : assoc.getPair(qnameDAO);
- }
-
- @Override
- public void getChildAssocs(
- Long parentNodeId,
- QName assocTypeQName,
- Collection childNames,
- ChildAssocRefQueryCallback resultsCallback)
- {
- selectChildAssocs(
- parentNodeId, assocTypeQName, childNames,
- new ChildAssocRefBatchingQueryCallback(resultsCallback));
- }
-
- @Override
- public void getChildAssocsByPropertyValue(
- Long parentNodeId,
- QName propertyQName,
- Serializable value,
- ChildAssocRefQueryCallback resultsCallback)
- {
- PropertyDefinition propertyDef = dictionaryService.getProperty(propertyQName);
- NodePropertyValue nodeValue = nodePropertyHelper.makeNodePropertyValue(propertyDef, value);
-
- if(nodeValue != null)
- {
- switch (nodeValue.getPersistedType())
- {
- case 1: // Boolean
- case 3: // long
- case 5: // double
- case 6: // string
- // no floats due to the range errors testing equality on a float.
- break;
-
- default:
- throw new IllegalArgumentException("method not supported for persisted value type " + nodeValue.getPersistedType());
- }
-
- selectChildAssocsByPropertyValue(parentNodeId,
- propertyQName,
- nodeValue,
- new ChildAssocRefBatchingQueryCallback(resultsCallback));
- }
- }
-
- @Override
- public void getChildAssocsByChildTypes(
- Long parentNodeId,
- Set childNodeTypeQNames,
- ChildAssocRefQueryCallback resultsCallback)
- {
- selectChildAssocsByChildTypes(
- parentNodeId, childNodeTypeQNames,
- new ChildAssocRefBatchingQueryCallback(resultsCallback));
- }
-
- @Override
- public void getChildAssocsWithoutParentAssocsOfType(
- Long parentNodeId,
- QName assocTypeQName,
- ChildAssocRefQueryCallback resultsCallback)
- {
- selectChildAssocsWithoutParentAssocsOfType(
- parentNodeId, assocTypeQName,
- new ChildAssocRefBatchingQueryCallback(resultsCallback));
- }
-
- @Override
- public Pair getPrimaryParentAssoc(Long childNodeId)
- {
- ChildAssocEntity childAssocEntity = getPrimaryParentAssocImpl(childNodeId);
- if(childAssocEntity == null)
- {
- return null;
- }
- else
- {
- return childAssocEntity.getPair(qnameDAO);
- }
- }
-
- private ChildAssocEntity getPrimaryParentAssocImpl(Long childNodeId)
- {
- ParentAssocsInfo parentAssocs = getParentAssocsCached(childNodeId);
- return parentAssocs.getPrimaryParentAssoc();
- }
-
- private static final int PARENT_ASSOCS_CACHE_FILTER_THRESHOLD = 2000;
-
- @Override
- public void getParentAssocs(
- Long childNodeId,
- QName assocTypeQName,
- QName assocQName,
- Boolean isPrimary,
- ChildAssocRefQueryCallback resultsCallback)
- {
- if (assocTypeQName == null && assocQName == null && isPrimary == null)
- {
- // Go for the cache (and return all)
- ParentAssocsInfo parentAssocs = getParentAssocsCached(childNodeId);
- for (ChildAssocEntity assoc : parentAssocs.getParentAssocs().values())
- {
- resultsCallback.handle(
- assoc.getPair(qnameDAO),
- assoc.getParentNode().getNodePair(),
- assoc.getChildNode().getNodePair());
- }
- resultsCallback.done();
- }
- else
- {
- // Decide whether we query or filter
- ParentAssocsInfo parentAssocs = getParentAssocsCached(childNodeId);
- if (parentAssocs.getParentAssocs().size() > PARENT_ASSOCS_CACHE_FILTER_THRESHOLD)
- {
- // Query
- selectParentAssocs(childNodeId, assocTypeQName, assocQName, isPrimary, resultsCallback);
- }
- else
- {
- // Go for the cache (and filter)
- for (ChildAssocEntity assoc : parentAssocs.getParentAssocs().values())
- {
- Pair assocPair = assoc.getPair(qnameDAO);
- if (((assocTypeQName == null) || (assocPair.getSecond().getTypeQName().equals(assocTypeQName))) &&
- ((assocQName == null) || (assocPair.getSecond().getQName().equals(assocQName))))
- {
- resultsCallback.handle(
- assocPair,
- assoc.getParentNode().getNodePair(),
- assoc.getChildNode().getNodePair());
- }
- }
- resultsCallback.done();
- }
-
- }
- }
-
- /**
- * Potentially cheaper than evaluating all of a node's paths to check for child association cycles
- *
- * TODO: When is it cheaper to go up and when is it cheaper to go down?
- * Look at using direct queries to pass through layers both up and down.
- *
- * @param nodeId the node to start with
- */
- @Override
- public void cycleCheck(Long nodeId)
- {
- CycleCallBack callback = new CycleCallBack();
- callback.cycleCheck(nodeId);
- if (callback.toThrow != null)
- {
- throw callback.toThrow;
- }
- }
-
- private class CycleCallBack implements ChildAssocRefQueryCallback
- {
- final Set nodeIds = new HashSet(97);
- CyclicChildRelationshipException toThrow;
-
- @Override
- public void done()
- {
- }
-
- @Override
- public boolean handle(
- Pair childAssocPair,
- Pair parentNodePair,
- Pair childNodePair)
- {
- Long nodeId = childNodePair.getFirst();
- if (!nodeIds.add(nodeId))
- {
- ChildAssociationRef childAssociationRef = childAssocPair.getSecond();
- // Remember exception we want to throw and exit. If we throw within here, it will be wrapped by IBatis
- toThrow = new CyclicChildRelationshipException(
- "Child Association Cycle detected hitting nodes: " + nodeIds,
- childAssociationRef);
- return false;
- }
- cycleCheck(nodeId);
- nodeIds.remove(nodeId);
- return toThrow == null;
- }
-
- /**
- * No preloading required
- */
- @Override
- public boolean preLoadNodes()
- {
- return false;
- }
-
- /**
- * No ordering required
- */
- @Override
- public boolean orderResults()
- {
- return false;
- }
-
- public void cycleCheck(Long nodeId)
- {
- getChildAssocs(nodeId, null, null, null, null, null, this);
- }
- };
-
-
- @Override
- public List getPaths(Pair