diff --git a/config/alfresco/ibatis/org.hibernate.dialect.Dialect/node-common-SqlMap.xml b/config/alfresco/ibatis/org.hibernate.dialect.Dialect/node-common-SqlMap.xml
index a311997916..59129e25e2 100644
--- a/config/alfresco/ibatis/org.hibernate.dialect.Dialect/node-common-SqlMap.xml
+++ b/config/alfresco/ibatis/org.hibernate.dialect.Dialect/node-common-SqlMap.xml
@@ -381,8 +381,6 @@
update alf_node set
version = #{version}
- , store_id = #{store.id}
- , uuid = #{uuid} , type_qname_id = #{typeQNameId} , locale_id = #{localeId} , acl_id = #{aclId,jdbcType=BIGINT}
@@ -450,6 +448,43 @@
is_primary = #{isPrimary}
+
+ update alf_child_assoc set
+ child_node_id = #{idTwo}
+ where
+ child_node_id = #{idOne}
+
+
+ update alf_child_assoc set
+ parent_node_id = #{idTwo}
+ where
+ parent_node_id = #{idOne}
+
+
+ update alf_node_assoc set
+ source_node_id = #{idTwo}
+ where
+ source_node_id = #{idOne}
+
+
+ update alf_node_assoc set
+ target_node_id = #{idTwo}
+ where
+ target_node_id = #{idOne}
+
+
+ update alf_node_properties set
+ node_id = #{idTwo}
+ where
+ node_id = #{idOne}
+
+
+ update alf_node_aspects set
+ node_id = #{idTwo}
+ where
+ node_id = #{idOne}
+
+
diff --git a/source/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java b/source/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java
index 838e627f23..07a9f32fb6 100644
--- a/source/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java
+++ b/source/java/org/alfresco/repo/domain/node/AbstractNodeDAOImpl.java
@@ -112,10 +112,10 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
private static final String CACHE_REGION_PROPERTIES = "N.P";
private static final String CACHE_REGION_PARENT_ASSOCS = "N.PA";
- private Log logger = LogFactory.getLog(getClass());
+ protected Log logger = LogFactory.getLog(getClass());
private Log loggerPaths = LogFactory.getLog(getClass().getName() + ".paths");
- private boolean isDebugEnabled = logger.isDebugEnabled();
+ protected final boolean isDebugEnabled = logger.isDebugEnabled();
private NodePropertyHelper nodePropertyHelper;
private ServerIdCallback serverIdCallback = new ServerIdCallback();
private UpdateTransactionListener updateTransactionListener = new UpdateTransactionListener();
@@ -670,7 +670,8 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
Long aclId = aclDAO.createAccessControlList();
// Create a root node
- NodeEntity rootNode = newNodeImpl(store, null, ContentModel.TYPE_STOREROOT, null, aclId, false, null);
+ Long nodeTypeQNameId = qnameDAO.getOrCreateQName(ContentModel.TYPE_STOREROOT).getFirst();
+ NodeEntity rootNode = newNodeImpl(store, null, nodeTypeQNameId, null, aclId, false, null);
Long rootNodeId = rootNode.getId();
addNodeAspects(rootNodeId, Collections.singleton(ContentModel.ASPECT_ROOT));
@@ -950,7 +951,9 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
// Get the store
StoreEntity store = getStoreNotNull(storeRef);
// Create the node (it is not a root node)
- NodeEntity node = newNodeImpl(store, uuid, nodeTypeQName, nodeLocale, childAclId, false, auditableProps);
+ Long nodeTypeQNameId = qnameDAO.getOrCreateQName(nodeTypeQName).getFirst();
+ Long nodeLocaleId = localeDAO.getOrCreateLocalePair(nodeLocale).getFirst();
+ NodeEntity node = newNodeImpl(store, uuid, nodeTypeQNameId, nodeLocaleId, childAclId, false, auditableProps);
Long nodeId = node.getId();
// Protect the node's cm:auditable if it was explicitly set
@@ -993,17 +996,18 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
/**
* @param uuid the node UUID, or null to auto-generate
- * @param nodeTypeQName the node's type
- * @param nodeLocale the node's locale or null to use the default locale
+ * @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 deleted true to create an already-deleted node (used for leaving trails of moved nodes)
+ * @throws NodeExistsException if the target reference is already taken by a live node
*/
private NodeEntity newNodeImpl(
StoreEntity store,
String uuid,
- QName nodeTypeQName,
- Locale nodeLocale,
+ Long nodeTypeQNameId,
+ Long nodeLocaleId,
Long aclId,
boolean deleted,
AuditablePropertiesEntity auditableProps) throws InvalidTypeException
@@ -1021,11 +1025,14 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
node.setUuid(uuid);
}
// QName
- Long typeQNameId = qnameDAO.getOrCreateQName(nodeTypeQName).getFirst();
- node.setTypeQNameId(typeQNameId);
+ node.setTypeQNameId(nodeTypeQNameId);
+ QName nodeTypeQName = qnameDAO.getQName(nodeTypeQNameId).getSecond();
// Locale
- final Long localeId = localeDAO.getOrCreateLocalePair(nodeLocale).getFirst();
- node.setLocaleId(localeId);
+ if (nodeLocaleId == null)
+ {
+ nodeLocaleId = localeDAO.getOrCreateDefaultLocalePair().getFirst();
+ }
+ node.setLocaleId(nodeLocaleId);
// ACL (may be null)
node.setAclId(aclId);
// Deleted
@@ -1064,6 +1071,11 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
controlDAO.rollbackToSavepoint(savepoint);
// This is probably because there is an existing node. We can handle existing deleted nodes.
NodeRef targetNodeRef = node.getNodeRef();
+ NodeEntity liveNode = selectNodeByNodeRef(targetNodeRef, false); // Only look for live nodes
+ if (liveNode != null)
+ {
+ throw new NodeExistsException(liveNode.getNodePair(), e);
+ }
NodeEntity deletedNode = selectNodeByNodeRef(targetNodeRef, true); // Only look for deleted nodes
if (deletedNode != null)
{
@@ -1105,7 +1117,7 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
return node;
}
- public Pair moveNode(
+ public Pair, Pair> moveNode(
final Long childNodeId,
final Long newParentNodeId,
final QName assocTypeQName,
@@ -1116,27 +1128,84 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
final Node childNode = getNodeNotNull(childNodeId);
final StoreEntity childStore = childNode.getStore();
ChildAssocEntity primaryParentAssoc = getPrimaryParentAssocImpl(childNodeId);
- final Long oldParentNodeId;
- if(primaryParentAssoc == null)
+ final Long oldParentAclId;
+ if (primaryParentAssoc == null)
{
- oldParentNodeId = null;
+ oldParentAclId = null;
}
else
{
- if(primaryParentAssoc.getParentNode() == null)
+ if (primaryParentAssoc.getParentNode() == null)
{
- oldParentNodeId = null;
+ oldParentAclId = null;
}
else
{
- oldParentNodeId = primaryParentAssoc.getParentNode().getId();
+ Long oldParentNodeId = primaryParentAssoc.getParentNode().getId();
+ oldParentAclId = getNodeNotNull(oldParentNodeId).getAclId();
// Update the parent node, if required
propagateTimestamps(childNodeId);
}
}
-
+
+ // First attempt to move the node, which may rollback to a savepoint
+ Node newChildNode = childNode;
+ // Store
+ if (!childStore.getId().equals(newParentStore.getId()))
+ {
+ // Clear out parent assocs cache; make sure child nodes are updated, too
+ invalidateCachesByNodeId(childNodeId, childNodeId, parentAssocsCache);
+ // Remove the cm:auditable aspect from the source node
+ // Remove the cm:auditable aspect from the old node as the new one will get new values as required
+ Set aspectIdsToDelete = qnameDAO.convertQNamesToIds(
+ Collections.singleton(ContentModel.ASPECT_AUDITABLE),
+ true);
+ deleteNodeAspects(childNodeId, aspectIdsToDelete);
+ // ... but make sure we copy over the cm:auditable data from the originating node
+ AuditablePropertiesEntity auditableProps = childNode.getAuditableProperties();
+ // Create a new node and copy all the data over to it
+ newChildNode = newNodeImpl(
+ newParentStore,
+ childNode.getUuid(),
+ childNode.getTypeQNameId(),
+ childNode.getLocaleId(),
+ childNode.getAclId(),
+ false,
+ auditableProps);
+ moveNodeData(
+ childNode.getId(),
+ newChildNode.getId());
+ // The new node has cache entries that will need updating to the new values
+ invalidateNodeCaches(newChildNode.getId());
+ // Now update the original to be 'deleted'
+ NodeUpdateEntity childNodeUpdate = new NodeUpdateEntity();
+ childNodeUpdate.setId(childNodeId);
+ childNodeUpdate.setAclId(null);
+ childNodeUpdate.setUpdateAclId(true);
+ childNodeUpdate.setTypeQNameId(qnameDAO.getOrCreateQName(ContentModel.TYPE_CMOBJECT).getFirst());
+ childNodeUpdate.setUpdateTypeQNameId(true);
+ childNodeUpdate.setLocaleId(localeDAO.getOrCreateDefaultLocalePair().getFirst());
+ childNodeUpdate.setUpdateLocaleId(true);
+ childNodeUpdate.setDeleted(Boolean.TRUE);
+ childNodeUpdate.setUpdateDeleted(true);
+ // Update the entity.
+ // Note: We don't use delete here because that will attempt to clean everything up again.
+ updateNodeImpl(childNode, childNodeUpdate);
+ }
+ else
+ {
+ // Ensure that the child node reflects the current txn and auditable data
+ touchNodeImpl(childNodeId);
+
+ // The moved node's reference has not changed, so just remove the cache entry to
+ // it's immediate parent. All children of the moved node will still point to the
+ // correct node.
+ invalidateCachesByNodeId(null, childNodeId, parentAssocsCache);
+ }
+
+ final Long newChildNodeId = newChildNode.getId();
// Now update the primary parent assoc
RetryingCallback callback = new RetryingCallback()
{
@@ -1154,7 +1223,7 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
try
{
int updated = updatePrimaryParentAssocs(
- childNodeId,
+ newChildNodeId,
newParentNodeId,
assocTypeQName,
assocQName,
@@ -1174,61 +1243,30 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
}
}
};
- Integer updateCount = childAssocRetryingHelper.doWithRetry(callback);
- if (updateCount > 0)
- {
- NodeUpdateEntity nodeUpdate = new NodeUpdateEntity();
- // ID
- nodeUpdate.setId(childNodeId);
- // Store
- if (!childStore.getId().equals(newParentStore.getId()))
- {
- nodeUpdate.setStore(newParentNode.getStore());
- nodeUpdate.setUpdateStore(true);
- }
-
- // Update. This takes care of the store move, auditable and transaction
- updateNodeImpl(childNode, nodeUpdate);
-
- // Clear out parent assocs cache
- invalidateCachesByNodeId(null, childNodeId, parentAssocsCache);
-
- // Check that there is not a cyclic relationship
- getPaths(nodeUpdate.getNodePair(), false);
-
- // Update ACLs for moved tree
- accessControlListDAO.updateInheritance(childNodeId, oldParentNodeId, newParentNodeId);
- }
- else
- {
- // Clear out parent assocs cache
- invalidateCachesByNodeId(null, childNodeId, parentAssocsCache);
- }
+ childAssocRetryingHelper.doWithRetry(callback);
- Pair assocPair = getPrimaryParentAssoc(childNodeId);
+ // Check for cyclic relationships
+ getPaths(newChildNode.getNodePair(), false);
+
+ // Update ACLs for moved tree
+ Long newParentAclId = newParentNode.getAclId();
+ accessControlListDAO.updateInheritance(newChildNodeId, oldParentAclId, newParentAclId);
// Done
+ Pair assocPair = getPrimaryParentAssoc(newChildNode.getId());
+ Pair nodePair = newChildNode.getNodePair();
if (isDebugEnabled)
{
- logger.debug("Moved node: " + assocPair);
+ logger.debug("Moved node: " + assocPair + " ... " + nodePair);
}
- return assocPair;
+ return new Pair, Pair>(assocPair, nodePair);
}
@Override
- public void updateNode(Long nodeId, StoreRef storeRef, String uuid, QName nodeTypeQName, Locale nodeLocale)
+ public void 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);
- // Use existing values, where necessary
- if (storeRef == null)
- {
- storeRef = oldNode.getStore().getStoreRef();
- }
- if (uuid == null)
- {
- uuid = oldNode.getUuid();
- }
final Long nodeTypeQNameId;
if (nodeTypeQName == null)
{
@@ -1251,27 +1289,8 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
// Wrap all the updates into one
NodeUpdateEntity nodeUpdate = new NodeUpdateEntity();
nodeUpdate.setId(nodeId);
- // Store (if necessary)
- if (!storeRef.equals(oldNode.getStore().getStoreRef()))
- {
- StoreEntity store = getStoreNotNull(storeRef);
- nodeUpdate.setStore(store);
- nodeUpdate.setUpdateStore(true);
- }
- else
- {
- nodeUpdate.setStore(oldNode.getStore()); // Need node reference
- }
- // UUID (if necessary)
- if (!uuid.equals(oldNode.getUuid()))
- {
- nodeUpdate.setUuid(uuid);
- nodeUpdate.setUpdateUuid(true);
- }
- else
- {
- nodeUpdate.setUuid(oldNode.getUuid()); // Need node reference
- }
+ nodeUpdate.setStore(oldNode.getStore()); // Need node reference
+ nodeUpdate.setUuid(oldNode.getUuid()); // Need node reference
// TypeQName (if necessary)
if (!nodeTypeQNameId.equals(oldNode.getTypeQNameId()))
{
@@ -1319,6 +1338,7 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
*
* @param oldNode the existing node, fully populated
* @param nodeUpdate the node update with all update elements populated
+ * @return the updated node ID and reference
*/
private void updateNodeImpl(Node oldNode, NodeUpdateEntity nodeUpdate)
{
@@ -1330,16 +1350,10 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
throw new IllegalArgumentException("NodeUpdateEntity node ID is not correct: " + nodeUpdate);
}
- // Copy the Store and UUID to the updated node, but leave the update flags.
- // The NodeRef may be required when resolving the duplicate NodeRef issues.
- if (!nodeUpdate.isUpdateStore())
- {
- nodeUpdate.setStore(oldNode.getStore());
- }
- if (!nodeUpdate.isUpdateUuid())
- {
- nodeUpdate.setUuid(oldNode.getUuid());
- }
+ // 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())
{
@@ -1358,9 +1372,6 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
nodeUpdate.setDeleted(oldNode.getDeleted());
}
- // Check the update values of the reference elements
- boolean updateReference = nodeUpdate.isUpdateStore() || nodeUpdate.isUpdateUuid();
-
nodeUpdate.setVersion(oldNode.getVersion());
// Update the transaction
TransactionEntity txn = getCurrentTransaction();
@@ -1421,72 +1432,8 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
return;
}
- // Do the update
- int count = 0;
- Savepoint savepoint = controlDAO.createSavepoint("updateNode");
- try
- {
- count = updateNode(nodeUpdate);
- controlDAO.releaseSavepoint(savepoint);
- }
- catch (Throwable e)
- {
- controlDAO.rollbackToSavepoint(savepoint);
- NodeRef targetNodeRef = nodeUpdate.getNodeRef();
- // Wipe the node ID from the caches just in case we have stale caches
- // The TransactionalCache will propagate removals to the shared cache on rollback
- nodesCache.removeByKey(nodeId);
- nodesCache.removeByValue(nodeUpdate);
-
- if (updateReference)
- {
- // This is the first error. Clean out deleted nodes that might be in the way and
- // move away live nodes.
- try
- {
- // Look for live nodes first as they will leave a trail of deleted nodes
- // that we will have to deal with subsequently.
- NodeEntity liveNode = selectNodeByNodeRef(targetNodeRef, false); // Only look for live nodes
- if (liveNode != null)
- {
- Long liveNodeId = liveNode.getId();
- String liveNodeUuid = GUID.generate();
- updateNode(liveNodeId, null, liveNodeUuid, null, null);
- }
- NodeEntity deletedNode = selectNodeByNodeRef(targetNodeRef, true); // Only look for deleted nodes
- if (deletedNode != null)
- {
- Long deletedNodeId = deletedNode.getId();
- deleteNodeById(deletedNodeId, true);
- }
- if (isDebugEnabled)
- {
- logger.debug("Cleaned up target references for reference update: " + targetNodeRef);
- }
- }
- catch (Throwable ee)
- {
- // We don't want to mask the original problem
- logger.error("Failed to clean up target nodes for new reference: " + targetNodeRef, ee);
- throw new RuntimeException("Failed to update node:" + nodeUpdate, e);
- }
- // Now repeat
- try
- {
- // The version number will have been incremented. Undo that.
- nodeUpdate.setVersion(nodeUpdate.getVersion() - 1L);
- count = updateNode(nodeUpdate);
- }
- catch (Throwable ee)
- {
- throw new RuntimeException("Failed to update Node: " + nodeUpdate, e);
- }
- }
- else // There is no reference change, so the error must just be propagated
- {
- throw new RuntimeException("Failed to update Node: " + nodeUpdate, e);
- }
- }
+ // The node is remaining in the current store
+ int count = updateNode(nodeUpdate);
// Do concurrency check
if (count != 1)
{
@@ -1496,15 +1443,18 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
throw new ConcurrencyFailureException("Failed to update node " + nodeId);
}
-
- // We need to leave a trail of deleted nodes
- if (updateReference)
+ else
{
- StoreEntity oldStore = oldNode.getStore();
- String oldUuid = oldNode.getUuid();
- newNodeImpl(oldStore, oldUuid, ContentModel.TYPE_CMOBJECT, null, null, true, null);
+ // Update the caches
+ nodeUpdate.lock();
+ nodesCache.setValue(nodeId, nodeUpdate);
+ if (nodeUpdate.isUpdateTypeQNameId() || nodeUpdate.isUpdateDeleted())
+ {
+ // The association references will all be wrong
+ invalidateCachesByNodeId(nodeId, nodeId, parentAssocsCache);
+ }
}
-
+
// Ensure that cm:auditable values are propagated, if required
if (enableTimestampPropagation &&
nodeUpdate.isUpdateAuditableProperties() &&
@@ -1513,15 +1463,6 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
propagateTimestamps(nodeId);
}
- // Update the caches
- nodeUpdate.lock();
- nodesCache.setValue(nodeId, nodeUpdate);
- if (updateReference || nodeUpdate.isUpdateTypeQNameId())
- {
- // The association references will all be wrong
- invalidateCachesByNodeId(nodeId, nodeId, parentAssocsCache);
- }
-
// Done
if (isDebugEnabled)
{
@@ -2646,7 +2587,7 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
// node into the current transaction for secondary associations
if (!isPrimary)
{
- updateNode(childNodeId, null, null, null, null);
+ updateNode(childNodeId, null, null);
}
// Done
@@ -3138,6 +3079,14 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
Stack assocIdStack,
boolean primaryOnly) throws CyclicChildRelationshipException
{
+ if (isDebugEnabled)
+ {
+ logger.debug("\n" +
+ "Prepending paths: \n" +
+ " Current node: " + currentNodePair + "\n" +
+ " Current root: " + currentRootNodePair + "\n" +
+ " Current path: " + currentPath);
+ }
Long currentNodeId = currentNodePair.getFirst();
NodeRef currentNodeRef = currentNodePair.getSecond();
@@ -3258,6 +3207,13 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
throw new CyclicChildRelationshipException("Node has been pasted into its own tree.", assocRef);
}
+ if (isDebugEnabled)
+ {
+ logger.debug("\n" +
+ " Prepending path parent: \n" +
+ " Parent node: " + parentNodePair);
+ }
+
// push the assoc stack, recurse and pop
assocIdStack.push(assocId);
prependPaths(parentNodePair, currentRootNodePair, path, completedPaths, assocIdStack, primaryOnly);
@@ -3894,6 +3850,14 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
QName assocTypeQName,
QName assocQName,
String childNodeName);
+ /**
+ * Moves all node-linked data from one node to another. The source node will be left
+ * in an orphaned state and without any attached data other than the current transaction.
+ *
+ * @param fromNodeId the source node
+ * @param toNodeId the target node
+ */
+ protected abstract void moveNodeData(Long fromNodeId, Long toNodeId);
protected abstract void deleteSubscriptions(Long nodeId);
diff --git a/source/java/org/alfresco/repo/domain/node/ChildAssocEntity.java b/source/java/org/alfresco/repo/domain/node/ChildAssocEntity.java
index 173a5ee186..b13dfedf1e 100644
--- a/source/java/org/alfresco/repo/domain/node/ChildAssocEntity.java
+++ b/source/java/org/alfresco/repo/domain/node/ChildAssocEntity.java
@@ -194,14 +194,14 @@ public class ChildAssocEntity
StringBuilder sb = new StringBuilder(512);
sb.append("ChildAssocEntity")
.append("[ ID=").append(id)
- .append(", parentNode=").append(parentNode)
- .append(", childNode=").append(childNode)
.append(", typeQNameId=").append(typeQNameId)
.append(", childNodeNameCrc=").append(childNodeNameCrc)
.append(", childNodeName=").append(childNodeName)
.append(", qnameNamespaceId=").append(qnameNamespaceId)
.append(", qnameLocalName=").append(qnameLocalName)
.append(", qnameCrc=").append(qnameCrc)
+ .append(", parentNode=").append(parentNode)
+ .append(", childNode=").append(childNode)
.append("]");
return sb.toString();
}
diff --git a/source/java/org/alfresco/repo/domain/node/NodeDAO.java b/source/java/org/alfresco/repo/domain/node/NodeDAO.java
index 0d1721e748..4d3a17e765 100644
--- a/source/java/org/alfresco/repo/domain/node/NodeDAO.java
+++ b/source/java/org/alfresco/repo/domain/node/NodeDAO.java
@@ -157,6 +157,7 @@ public interface NodeDAO extends NodeBulkLoader
* @return Returns the details of the child association created
* @throws InvalidTypeException if the node type is invalid or if the node type
* is not a valid real node
+ * @throws NodeExistsException if the target reference is already taken by a live node
*/
public ChildAssocEntity newNode(
Long parentNodeId,
@@ -167,35 +168,32 @@ public interface NodeDAO extends NodeBulkLoader
QName nodeTypeQName,
Locale nodeLocale,
String childNodeName,
- Map auditableProperties/*,
- Map ownableProperties*/) throws InvalidTypeException;
+ Map auditableProperties) throws InvalidTypeException;
/**
* Update a node's primary association, giving it a new parent and new association parameters.
*
- * **NEW**: If the parent node's store differs from the child node's store, then the
- * child node's store is updated. Store move conflicts are automatically handled by assigning
- * new UUIDs to the existing target node.
+ * **NEW**: If the parent node's store differs from the child node's store, then a new
+ * child node's is created.
*
* @param childNodeId the child node that is moving
* @param newParentNodeId the new parent node (may not be null)
* @param assocTypeQName the new association type or null to keep the existing type
* @param assocQName the new association qname or null to keep the existing name
- * @return Returns the new association reference
+ * @return Returns the (first) new association reference and new child reference (second)
+ * @throws NodeExistsException if the target UUID of the move (in case of a store move) already exists
*/
- public Pair moveNode(
+ public Pair, Pair> moveNode(
Long childNodeId,
Long newParentNodeId,
QName assocTypeQName,
QName assocQName);
/**
- * @param storeRef the new store or null to keep the existing one
- * @param uuid the new UUID for the node or null to keep it the same
* @param nodeTypeQName the new type QName for the node or null to keep the existing one
* @param nodeLocale the new locale for the node or null to keep the existing one
*/
- public void updateNode(Long nodeId, StoreRef storeRef, String uuid, QName nodeTypeQName, Locale nodeLocale);
+ public void updateNode(Long nodeId, QName nodeTypeQName, Locale nodeLocale);
public void setNodeAclId(Long nodeId, Long aclId);
diff --git a/source/java/org/alfresco/repo/domain/node/NodeExistsException.java b/source/java/org/alfresco/repo/domain/node/NodeExistsException.java
new file mode 100644
index 0000000000..31e4e85f60
--- /dev/null
+++ b/source/java/org/alfresco/repo/domain/node/NodeExistsException.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.repo.domain.node;
+
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.util.Pair;
+
+/**
+ * Exception generated when a live node already exists for a given {@link NodeRef node reference}.
+ *
+ * @author Derek Hulley
+ * @since 4.0
+ */
+public class NodeExistsException extends AlfrescoRuntimeException
+{
+ private static final long serialVersionUID = -2122408334209855947L;
+
+ private final Pair nodePair;
+
+ public NodeExistsException(Pair nodePair, Throwable e)
+ {
+ super("Node already exists: " + nodePair, e);
+ this.nodePair = nodePair;
+ }
+
+ public Pair getNodePair()
+ {
+ return nodePair;
+ }
+}
diff --git a/source/java/org/alfresco/repo/domain/node/NodeUpdateEntity.java b/source/java/org/alfresco/repo/domain/node/NodeUpdateEntity.java
index 435e02671f..fe06190b29 100644
--- a/source/java/org/alfresco/repo/domain/node/NodeUpdateEntity.java
+++ b/source/java/org/alfresco/repo/domain/node/NodeUpdateEntity.java
@@ -27,8 +27,6 @@ package org.alfresco.repo.domain.node;
*/
public class NodeUpdateEntity extends NodeEntity
{
- private boolean updateStore;
- private boolean updateUuid;
private boolean updateTypeQNameId;
private boolean updateLocaleId;
private boolean updateAclId;
@@ -49,27 +47,7 @@ public class NodeUpdateEntity extends NodeEntity
public boolean isUpdateAnything()
{
return updateAuditableProperties || updateTransaction || updateDeleted
- || updateLocaleId || updateAclId || updateStore || updateUuid || updateTypeQNameId;
- }
-
- public boolean isUpdateStore()
- {
- return updateStore;
- }
-
- public void setUpdateStore(boolean updateStore)
- {
- this.updateStore = updateStore;
- }
-
- public boolean isUpdateUuid()
- {
- return updateUuid;
- }
-
- public void setUpdateUuid(boolean updateUuid)
- {
- this.updateUuid = updateUuid;
+ || updateLocaleId || updateAclId || updateTypeQNameId;
}
public boolean isUpdateTypeQNameId()
diff --git a/source/java/org/alfresco/repo/domain/node/ibatis/NodeDAOImpl.java b/source/java/org/alfresco/repo/domain/node/ibatis/NodeDAOImpl.java
index 485aa14c9b..61f2f72fba 100644
--- a/source/java/org/alfresco/repo/domain/node/ibatis/NodeDAOImpl.java
+++ b/source/java/org/alfresco/repo/domain/node/ibatis/NodeDAOImpl.java
@@ -123,6 +123,14 @@ public class NodeDAOImpl extends AbstractNodeDAOImpl
private static final String SELECT_PARENT_ASSOCS_OF_CHILD = "alfresco.node.select_ParentAssocsOfChild";
private static final String UPDATE_PARENT_ASSOCS_OF_CHILD = "alfresco.node.update_ParentAssocsOfChild";
private static final String DELETE_SUBSCRIPTIONS = "alfresco.node.delete_Subscriptions";
+
+ private static final String UPDATE_MOVE_PARENT_ASSOCS = "alfresco.node.update_MoveParentAssocs";
+ private static final String UPDATE_MOVE_CHILD_ASSOCS = "alfresco.node.update_MoveChildAssocs";
+ private static final String UPDATE_MOVE_SOURCE_ASSOCS = "alfresco.node.update_MoveSourceAssocs";
+ private static final String UPDATE_MOVE_TARGET_ASSOCS = "alfresco.node.update_MoveTargetAssocs";
+ private static final String UPDATE_MOVE_PROPERTIES = "alfresco.node.update_MoveProperties";
+ private static final String UPDATE_MOVE_ASPECTS = "alfresco.node.update_MoveAspects";
+
private static final String SELECT_TXN_LAST = "alfresco.node.select_TxnLast";
private static final String SELECT_TXN_NODES = "alfresco.node.select_TxnNodes";
private static final String SELECT_TXNS = "alfresco.node.select_Txns";
@@ -1315,6 +1323,35 @@ public class NodeDAOImpl extends AbstractNodeDAOImpl
return template.update(UPDATE_PARENT_ASSOCS_OF_CHILD, assoc);
}
+ @Override
+ protected void moveNodeData(Long fromNodeId, Long toNodeId)
+ {
+ IdsEntity params = new IdsEntity();
+ params.setIdOne(fromNodeId);
+ params.setIdTwo(toNodeId);
+
+
+ int countPA = template.update(UPDATE_MOVE_PARENT_ASSOCS, params);
+ int countCA = template.update(UPDATE_MOVE_CHILD_ASSOCS, params);
+ int countSA = template.update(UPDATE_MOVE_SOURCE_ASSOCS, params);
+ int countTA = template.update(UPDATE_MOVE_TARGET_ASSOCS, params);
+ int countP = template.update(UPDATE_MOVE_PROPERTIES, params);
+ int countA = template.update(UPDATE_MOVE_ASPECTS, params);
+ if (isDebugEnabled)
+ {
+ logger.debug(
+ "Moved node data: \n" +
+ " From: " + fromNodeId + "\n" +
+ " To: " + toNodeId + "\n" +
+ " PA: " + countPA + "\n" +
+ " CA: " + countCA + "\n" +
+ " SA: " + countSA + "\n" +
+ " TA: " + countTA + "\n" +
+ " P: " + countP + "\n" +
+ " A: " + countA);
+ }
+ }
+
/**
* The default implementation relies on ON DELETE CASCADE and the
* subscriptions avoiding deleted nodes - NoOp.
diff --git a/source/java/org/alfresco/repo/domain/permissions/ADMAccessControlListDAO.java b/source/java/org/alfresco/repo/domain/permissions/ADMAccessControlListDAO.java
index e1d3aa8d4f..ce0258d875 100644
--- a/source/java/org/alfresco/repo/domain/permissions/ADMAccessControlListDAO.java
+++ b/source/java/org/alfresco/repo/domain/permissions/ADMAccessControlListDAO.java
@@ -377,18 +377,17 @@ public class ADMAccessControlListDAO implements AccessControlListDAO
}
}
- /* (non-Javadoc)
- * @see org.alfresco.repo.domain.AccessControlListDAO#updateInheritance(java.lang.Long, java.lang.Long, java.lang.Long)
+ /**
+ * {@inheritDoc}
*/
- public void updateInheritance(Long childNodeId, Long oldParentNodeId, Long newParentNodeId)
+ public void updateInheritance(Long childNodeId, Long oldParentAclId, Long newParentAclId)
{
- if(oldParentNodeId == null)
+ if (oldParentAclId == null)
{
// nothing to do
return;
}
List changes = new ArrayList();
- Long newParentAclId = nodeDAO.getNodeAclId(newParentNodeId);
Long childAclId = nodeDAO.getNodeAclId(childNodeId);
if(childAclId == null)
@@ -400,55 +399,47 @@ public class ADMAccessControlListDAO implements AccessControlListDAO
}
}
Acl acl = aclDaoComponent.getAcl(childAclId);
- if(acl != null)
+ if (acl != null && acl.getInherits())
{
- if(acl.getInherits())
+ Long oldParentSharedAclId = aclDaoComponent.getInheritedAccessControlList(oldParentAclId);
+ Long sharedAclchildInheritsFrom = acl.getInheritsFrom();
+ if(childAclId.equals(oldParentSharedAclId))
{
- // Does it inherit from the old parent - if not nothing changes
- Long oldParentAclId = nodeDAO.getNodeAclId(oldParentNodeId);
- if(oldParentAclId != null)
+ // child had old shared acl
+ if(newParentAclId != null)
{
- Long oldParentSharedAclId = aclDaoComponent.getInheritedAccessControlList(oldParentAclId);
- Long sharedAclchildInheritsFrom = acl.getInheritsFrom();
- if(childAclId.equals(oldParentSharedAclId))
- {
- // child had old shared acl
- if(newParentAclId != null)
- {
- Long newParentSharedAclId = aclDaoComponent.getInheritedAccessControlList(newParentAclId);
- setFixedAcls(childNodeId, newParentSharedAclId, null, childAclId, changes, true);
- }
- }
- else if(sharedAclchildInheritsFrom == null)
- {
- // child has defining acl of some form that does not inherit ?
- // Leave alone
- }
- else if(sharedAclchildInheritsFrom.equals(oldParentSharedAclId))
- {
- // child has defining acl and needs to be remerged
- if (acl.getAclType() == ACLType.LAYERED)
- {
- throw new UnsupportedOperationException();
- }
- else if (acl.getAclType() == ACLType.DEFINING)
- {
- Long newParentSharedAclId = aclDaoComponent.getInheritedAccessControlList(newParentAclId);
- @SuppressWarnings("unused")
- List newChanges = aclDaoComponent.mergeInheritedAccessControlList(newParentSharedAclId, childAclId);
- }
- else if (acl.getAclType() == ACLType.SHARED)
- {
- throw new IllegalStateException();
- }
- }
- else
- {
- // the acl does not inherit from a node and does not need to be fixed up
- // Leave alone
- }
+ Long newParentSharedAclId = aclDaoComponent.getInheritedAccessControlList(newParentAclId);
+ setFixedAcls(childNodeId, newParentSharedAclId, null, childAclId, changes, true);
}
}
+ else if(sharedAclchildInheritsFrom == null)
+ {
+ // child has defining acl of some form that does not inherit ?
+ // Leave alone
+ }
+ else if(sharedAclchildInheritsFrom.equals(oldParentSharedAclId))
+ {
+ // child has defining acl and needs to be remerged
+ if (acl.getAclType() == ACLType.LAYERED)
+ {
+ throw new UnsupportedOperationException();
+ }
+ else if (acl.getAclType() == ACLType.DEFINING)
+ {
+ Long newParentSharedAclId = aclDaoComponent.getInheritedAccessControlList(newParentAclId);
+ @SuppressWarnings("unused")
+ List newChanges = aclDaoComponent.mergeInheritedAccessControlList(newParentSharedAclId, childAclId);
+ }
+ else if (acl.getAclType() == ACLType.SHARED)
+ {
+ throw new IllegalStateException();
+ }
+ }
+ else
+ {
+ // the acl does not inherit from a node and does not need to be fixed up
+ // Leave alone
+ }
}
}
}
diff --git a/source/java/org/alfresco/repo/domain/permissions/AccessControlListDAO.java b/source/java/org/alfresco/repo/domain/permissions/AccessControlListDAO.java
index 68b238610e..21b90ad2cc 100644
--- a/source/java/org/alfresco/repo/domain/permissions/AccessControlListDAO.java
+++ b/source/java/org/alfresco/repo/domain/permissions/AccessControlListDAO.java
@@ -89,7 +89,7 @@ public interface AccessControlListDAO
public void setAccessControlList(StoreRef storeRef, Acl acl);
- public void updateInheritance(Long childNodeId, Long oldParentNodeId, Long newParentNodeId);
+ public void updateInheritance(Long childNodeId, Long oldParentAclId, Long newParentAclId);
public void setFixedAcls(Long nodeId, Long inheritFrom, Long mergeFrom, Long sharedAclToReplace, List changes, boolean set);
}
diff --git a/source/java/org/alfresco/repo/node/NodeServiceTest.java b/source/java/org/alfresco/repo/node/NodeServiceTest.java
index fff9a2b549..32fad89f90 100644
--- a/source/java/org/alfresco/repo/node/NodeServiceTest.java
+++ b/source/java/org/alfresco/repo/node/NodeServiceTest.java
@@ -31,6 +31,7 @@ import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransacti
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.repository.MLText;
import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.NodeRef.Status;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.namespace.NamespaceService;
@@ -188,4 +189,190 @@ public class NodeServiceTest extends TestCase
"Node locale not set in setProperties(). ",
Locale.GERMAN, nodeService.getProperty(nodeRef2, ContentModel.PROP_LOCALE));
}
+
+ /**
+ * Creates a string of parent-child nodes to fill the given array of nodes
+ *
+ * @param workspaceRootNodeRef the store to use
+ * @param liveNodeRefs the node array to fill
+ */
+ private void buildNodeHierarchy(final NodeRef workspaceRootNodeRef, final NodeRef[] liveNodeRefs)
+ {
+ RetryingTransactionCallback setupCallback = new RetryingTransactionCallback()
+ {
+ @Override
+ public Void execute() throws Throwable
+ {
+ liveNodeRefs[0] = nodeService.createNode(
+ workspaceRootNodeRef,
+ ContentModel.ASSOC_CHILDREN,
+ QName.createQName(NAMESPACE, "depth-" + 0),
+ ContentModel.TYPE_FOLDER).getChildRef();
+ for (int i = 1; i < liveNodeRefs.length; i++)
+ {
+ liveNodeRefs[i] = nodeService.createNode(
+ liveNodeRefs[i-1],
+ ContentModel.ASSOC_CONTAINS,
+ QName.createQName(NAMESPACE, "depth-" + i),
+ ContentModel.TYPE_FOLDER).getChildRef();
+ }
+ return null;
+ }
+ };
+ txnService.getRetryingTransactionHelper().doInTransaction(setupCallback);
+ }
+
+ /**
+ * Tests that two separate node trees can be deleted concurrently at the database level.
+ * This is not a concurren thread issue; instead we delete a hierarchy and hold the txn
+ * open while we delete another in a new txn, thereby testing that DB locks don't prevent
+ * concurrent deletes.
+ *
+ * See: ALF-5714
+ */
+ public void testConcurrentArchive() throws Exception
+ {
+ final NodeRef workspaceRootNodeRef = nodeService.getRootNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE);
+ final NodeRef[] nodesOne = new NodeRef[10];
+ buildNodeHierarchy(workspaceRootNodeRef, nodesOne);
+ final NodeRef[] nodesTwo = new NodeRef[10];
+ buildNodeHierarchy(workspaceRootNodeRef, nodesTwo);
+
+ RetryingTransactionCallback outerCallback = new RetryingTransactionCallback()
+ {
+ @Override
+ public Void execute() throws Throwable
+ {
+ // Delete the first hierarchy
+ nodeService.deleteNode(nodesOne[0]);
+ // Keep the txn hanging around to maintain DB locks
+ // and start a second transaction to delete another hierarchy
+ RetryingTransactionCallback innerCallback = new RetryingTransactionCallback()
+ {
+ @Override
+ public Void execute() throws Throwable
+ {
+ nodeService.deleteNode(nodesTwo[0]);
+ return null;
+ }
+ };
+ txnService.getRetryingTransactionHelper().doInTransaction(innerCallback, false, true);
+ return null;
+ }
+ };
+ txnService.getRetryingTransactionHelper().doInTransaction(outerCallback, false, true);
+ }
+
+ /**
+ * Tests archive and restore of simple hierarchy, checking that references and IDs are
+ * used correctly.
+ */
+ public void testArchiveAndRestore()
+ {
+ // First create a node structure (a very simple one) and record the references and IDs
+ final NodeRef[] liveNodeRefs = new NodeRef[10];
+ final NodeRef[] archivedNodeRefs = new NodeRef[10];
+
+ final NodeRef workspaceRootNodeRef = nodeService.getRootNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE);
+ final NodeRef archiveRootNodeRef = nodeService.getRootNode(StoreRef.STORE_REF_ARCHIVE_SPACESSTORE);
+
+ buildNodeHierarchy(workspaceRootNodeRef, liveNodeRefs);
+
+ // Get the node status details
+ Long txnIdCreate = null;
+ for (int i = 0; i < liveNodeRefs.length; i++)
+ {
+ StoreRef archivedStoreRef = archiveRootNodeRef.getStoreRef();
+ archivedNodeRefs[i] = new NodeRef(archivedStoreRef, liveNodeRefs[i].getId());
+
+ Status liveStatus = nodeService.getNodeStatus(liveNodeRefs[i]);
+ Status archivedStatus = nodeService.getNodeStatus(archivedNodeRefs[i]);
+
+ // Check that live node statuses are correct
+ assertNotNull("'Live' node " + i + " status does not exist.", liveStatus);
+ assertFalse("'Live' node " + i + " should be node be deleted", liveStatus.isDeleted());
+ assertNull("'Archived' node " + i + " should not (yet) exist.", archivedStatus);
+
+ // Nodes in the hierarchy must be in the same txn
+ if (txnIdCreate == null)
+ {
+ txnIdCreate = liveStatus.getDbTxnId();
+ }
+ else
+ {
+ // Make sure that the DB Txn ID is the same
+ assertEquals(
+ "DB TXN ID should have been the same for the hierarchy. ",
+ txnIdCreate, liveStatus.getDbTxnId());
+ }
+ }
+
+ // Archive the top-level node
+ nodeService.deleteNode(liveNodeRefs[0]);
+
+ // Recheck the nodes and make sure that all the 'live' nodes are deleted
+ Long txnIdDelete = null;
+ for (int i = 0; i < liveNodeRefs.length; i++)
+ {
+ Status liveStatus = nodeService.getNodeStatus(liveNodeRefs[i]);
+ Status archivedStatus = nodeService.getNodeStatus(archivedNodeRefs[i]);
+
+ // Check that the ghosted nodes are marked as deleted and the archived nodes are not
+ assertNotNull("'Live' node " + i + " status does not exist.", liveStatus);
+ assertTrue("'Live' node " + i + " should be deleted (ghost entries)", liveStatus.isDeleted());
+ assertNotNull("'Archived' node " + i + " does not exist.", archivedStatus);
+ assertFalse("'Archived' node " + i + " should be undeleted", archivedStatus.isDeleted());
+
+ // Check that both old (ghosted deletes) and new nodes are in the same txn
+ if (txnIdDelete == null)
+ {
+ txnIdDelete = liveStatus.getDbTxnId();
+ }
+ else
+ {
+ // Make sure that the DB Txn ID is the same
+ assertEquals(
+ "DB TXN ID should have been the same for the deleted (ghost) nodes. ",
+ txnIdDelete, liveStatus.getDbTxnId());
+ }
+ assertEquals(
+ "DB TXN ID should be the same for deletes across the hierarchy",
+ txnIdDelete, archivedStatus.getDbTxnId());
+ }
+
+ // Restore the top-level node
+ nodeService.restoreNode(archivedNodeRefs[0], workspaceRootNodeRef, null, null);
+
+ // Recheck the nodes and make sure that all the 'archived' nodes are deleted and the 'live' nodes are back
+ Long txnIdRestore = null;
+ for (int i = 0; i < liveNodeRefs.length; i++)
+ {
+ Status liveStatus = nodeService.getNodeStatus(liveNodeRefs[i]);
+ StoreRef archivedStoreRef = archiveRootNodeRef.getStoreRef();
+ archivedNodeRefs[i] = new NodeRef(archivedStoreRef, liveNodeRefs[i].getId());
+ Status archivedStatus = nodeService.getNodeStatus(archivedNodeRefs[i]);
+
+ // Check that the ghosted nodes are marked as deleted and the archived nodes are not
+ assertNotNull("'Live' node " + i + " status does not exist.", liveStatus);
+ assertFalse("'Live' node " + i + " should not be deleted", liveStatus.isDeleted());
+ assertNotNull("'Archived' node " + i + " does not exist.", archivedStatus);
+ assertTrue("'Archived' node " + i + " should be deleted (ghost entry)", archivedStatus.isDeleted());
+
+ // Check that both old (ghosted deletes) and new nodes are in the same txn
+ if (txnIdRestore == null)
+ {
+ txnIdRestore = liveStatus.getDbTxnId();
+ }
+ else
+ {
+ // Make sure that the DB Txn ID is the same
+ assertEquals(
+ "DB TXN ID should have been the same for the restored nodes. ",
+ txnIdRestore, liveStatus.getDbTxnId());
+ }
+ assertEquals(
+ "DB TXN ID should be the same for the ex-archived (now-ghost) nodes. ",
+ txnIdRestore, archivedStatus.getDbTxnId());
+ }
+ }
}
diff --git a/source/java/org/alfresco/repo/node/archive/ArchiveAndRestoreTest.java b/source/java/org/alfresco/repo/node/archive/ArchiveAndRestoreTest.java
index 37c09296bb..8741409edd 100644
--- a/source/java/org/alfresco/repo/node/archive/ArchiveAndRestoreTest.java
+++ b/source/java/org/alfresco/repo/node/archive/ArchiveAndRestoreTest.java
@@ -190,7 +190,8 @@ public class ArchiveAndRestoreTest extends TestCase
* |/ \|
* AA <-> BB
*
- * Explicit UUIDs are used for debugging purposes.
+ * Explicit UUIDs are used for debugging purposes. Live nodes are cm:countable with the
+ * cm:counter property.
*
* A, B, AA and BB are set up to archive automatically
* on deletion.
@@ -199,6 +200,8 @@ public class ArchiveAndRestoreTest extends TestCase
{
Map properties = new HashMap(5);
+ properties.put(ContentModel.PROP_COUNTER, 50);
+
properties.put(ContentModel.PROP_NODE_UUID, "a");
a = nodeService.createNode(
workStoreRootNodeRef,
@@ -263,7 +266,7 @@ public class ArchiveAndRestoreTest extends TestCase
private void verifyNodeExistence(NodeRef nodeRef, boolean exists)
{
- assertEquals("Node should " + (exists ? "" : "not") + "exist", exists, nodeService.exists(nodeRef));
+ assertEquals("Node should " + (exists ? "" : "not ") + "exist", exists, nodeService.exists(nodeRef));
}
private void verifyChildAssocExistence(ChildAssociationRef childAssocRef, boolean exists)
@@ -297,6 +300,20 @@ public class ArchiveAndRestoreTest extends TestCase
}
}
+ private void verifyPropertyExistence(NodeRef nodeRef, QName propertyQName, boolean exists)
+ {
+ assertEquals(
+ "Property is not present " + nodeRef + " - " + propertyQName,
+ exists, nodeService.getProperty(nodeRef, propertyQName) != null);
+ }
+
+ private void verifyAspectExistence(NodeRef nodeRef, QName aspectQName, boolean exists)
+ {
+ assertEquals(
+ "Aspect is not present " + nodeRef + " - " + aspectQName,
+ exists, nodeService.hasAspect(nodeRef, aspectQName));
+ }
+
public void verifyAll()
{
// work store references
@@ -310,6 +327,14 @@ public class ArchiveAndRestoreTest extends TestCase
verifyChildAssocExistence(childAssocBtoAA, true);
verifyTargetAssocExistence(assocAtoB, true);
verifyTargetAssocExistence(assocAAtoBB, true);
+ verifyPropertyExistence(a, ContentModel.PROP_COUNTER, true);
+ verifyAspectExistence(a, ContentModel.ASPECT_COUNTABLE, true);
+ verifyPropertyExistence(b, ContentModel.PROP_COUNTER, true);
+ verifyAspectExistence(b, ContentModel.ASPECT_COUNTABLE, true);
+ verifyPropertyExistence(aa, ContentModel.PROP_COUNTER, true);
+ verifyAspectExistence(aa, ContentModel.ASPECT_COUNTABLE, true);
+ verifyPropertyExistence(bb, ContentModel.PROP_COUNTER, true);
+ verifyAspectExistence(bb, ContentModel.ASPECT_COUNTABLE, true);
// archive store references
verifyNodeExistence(a_, false);
verifyNodeExistence(b_, false);
@@ -545,6 +570,19 @@ public class ArchiveAndRestoreTest extends TestCase
txn.begin();
}
+ public void testSimple_Create_Commit_Delete_Commit() throws Exception
+ {
+ commitAndBeginNewTransaction();
+ nodeService.deleteNode(a);
+ commitAndBeginNewTransaction();
+ }
+
+ public void testSimple_Create_Delete_Commit() throws Exception
+ {
+ nodeService.deleteNode(a);
+ commitAndBeginNewTransaction();
+ }
+
public void testRestoreToMissingParent() throws Exception
{
nodeService.deleteNode(a);
diff --git a/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java b/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java
index 185f7c1765..be9d850ac1 100644
--- a/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java
+++ b/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java
@@ -37,6 +37,7 @@ import org.alfresco.model.ContentModel;
import org.alfresco.repo.domain.node.ChildAssocEntity;
import org.alfresco.repo.domain.node.Node;
import org.alfresco.repo.domain.node.NodeDAO;
+import org.alfresco.repo.domain.node.NodeExistsException;
import org.alfresco.repo.domain.node.NodeDAO.ChildAssocRefQueryCallback;
import org.alfresco.repo.domain.qname.QNameDAO;
import org.alfresco.repo.node.AbstractNodeServiceImpl;
@@ -734,7 +735,7 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl
invokeBeforeUpdateNode(nodeRef);
// Set the type
- nodeDAO.updateNode(nodePair.getFirst(), null, null, typeQName, null);
+ nodeDAO.updateNode(nodePair.getFirst(), typeQName, null);
// Add the default aspects and properties required for the given type. Existing values will not be overridden.
addAspectsAndProperties(nodePair, typeQName, null, null, null, null, false);
@@ -946,7 +947,7 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl
StoreRef storeRef = nodeRef.getStoreRef();
StoreRef archiveStoreRef = storeArchiveMap.get(storeRef);
- /**
+ /*
* Work out whether we need to archive or delete the node.
*/
@@ -985,7 +986,7 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl
}
}
- /**
+ /*
* Now we have worked out whether to archive or delete, go ahead and do it
*/
if (requiresDelete == null || requiresDelete)
@@ -2091,6 +2092,30 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl
Map existingProperties = nodeDAO.getNodeProperties(nodeId);
Map newProperties = new HashMap(11);
+ // move the node
+ Pair archiveStoreRootNodePair = nodeDAO.getRootNode(archiveStoreRef);
+ Pair newNodePair = null;
+ try
+ {
+ ChildAssociationRef newPrimaryParentAssocPair = moveNode(
+ nodeRef,
+ archiveStoreRootNodePair.getSecond(),
+ ContentModel.ASSOC_CHILDREN,
+ NodeArchiveService.QNAME_ARCHIVED_ITEM);
+ newNodePair = getNodePairNotNull(newPrimaryParentAssocPair.getChildRef());
+ }
+ catch (NodeExistsException e)
+ {
+ // Clear out the offending node and try again
+ deleteNode(e.getNodePair().getSecond());
+ ChildAssociationRef newPrimaryParentAssocPair = moveNode(
+ nodeRef,
+ archiveStoreRootNodePair.getSecond(),
+ ContentModel.ASSOC_CHILDREN,
+ NodeArchiveService.QNAME_ARCHIVED_ITEM);
+ newNodePair = getNodePairNotNull(newPrimaryParentAssocPair.getChildRef());
+ }
+
// add the aspect
newAspects.add(ContentModel.ASPECT_ARCHIVED);
newProperties.put(ContentModel.PROP_ARCHIVED_BY, AuthenticationUtil.getFullyAuthenticatedUser());
@@ -2104,22 +2129,12 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl
ContentModel.PROP_ARCHIVED_ORIGINAL_OWNER,
originalOwner != null ? originalOwner : originalCreator);
}
-
// change the node ownership
newAspects.add(ContentModel.ASPECT_OWNABLE);
newProperties.put(ContentModel.PROP_OWNER, AuthenticationUtil.getFullyAuthenticatedUser());
// Set the aspects and properties
- nodeDAO.addNodeProperties(nodeId, newProperties);
- nodeDAO.addNodeAspects(nodeId, newAspects);
-
- // move the node
- Pair archiveStoreRootNodePair = nodeDAO.getRootNode(archiveStoreRef);
- moveNode(
- nodeRef,
- archiveStoreRootNodePair.getSecond(),
- ContentModel.ASSOC_CHILDREN,
- NodeArchiveService.QNAME_ARCHIVED_ITEM);
+ addAspectsAndProperties(newNodePair, null, null, null, newAspects, newProperties, false);
}
/**
@@ -2233,13 +2248,12 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl
Long nodeToMoveId = nodeToMovePair.getFirst();
QName nodeToMoveTypeQName = nodeDAO.getNodeType(nodeToMoveId);
+ Set nodeToMoveAspectQNames = nodeDAO.getNodeAspects(nodeToMoveId);
NodeRef oldNodeToMoveRef = nodeToMovePair.getSecond();
Long parentNodeId = parentNodePair.getFirst();
NodeRef parentNodeRef = parentNodePair.getSecond();
StoreRef oldStoreRef = oldNodeToMoveRef.getStoreRef();
StoreRef newStoreRef = parentNodeRef.getStoreRef();
- NodeRef newNodeToMoveRef = new NodeRef(newStoreRef, oldNodeToMoveRef.getId());
- Pair newNodeToMovePair = new Pair(nodeToMoveId, newNodeToMoveRef);
// Get the primary parent association
Pair oldParentAssocPair = nodeDAO.getPrimaryParentAssoc(nodeToMoveId);
@@ -2250,15 +2264,6 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl
}
ChildAssociationRef oldParentAssocRef = oldParentAssocPair.getSecond();
- // Shortcut this whole process if nothing has changed
- if (EqualsHelper.nullSafeEquals(oldParentAssocRef.getParentRef(), newParentRef) &&
- EqualsHelper.nullSafeEquals(oldParentAssocRef.getTypeQName(), assocTypeQName) &&
- EqualsHelper.nullSafeEquals(oldParentAssocRef.getQName(), assocQName))
- {
- // It's all just the same
- return oldParentAssocRef;
- }
-
boolean movingStore = !oldStoreRef.equals(newStoreRef);
// Invoke "Before"policy behaviour
@@ -2279,11 +2284,13 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl
}
// Move node under the new parent
- Pair newParentAssocPair = nodeDAO.moveNode(
+ Pair, Pair> moveNodeResult = nodeDAO.moveNode(
nodeToMoveId,
parentNodeId,
assocTypeQName,
assocQName);
+ Pair newParentAssocPair = moveNodeResult.getFirst();
+ Pair newNodeToMovePair = moveNodeResult.getSecond();
ChildAssociationRef newParentAssocRef = newParentAssocPair.getSecond();
// Handle indexing differently if it is a store move
@@ -2302,7 +2309,6 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl
// Call behaviours
if (movingStore)
{
- Set nodeToMoveAspectQNames = nodeDAO.getNodeAspects(nodeToMoveId);
// The Node changes NodeRefs, so this is really the deletion of the old node and creation
// of a node in a new store as far as the clients are concerned.
invokeOnDeleteNode(oldParentAssocRef, nodeToMoveTypeQName, nodeToMoveAspectQNames, true);
@@ -2371,7 +2377,6 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl
QName childNodeTypeQName = nodeDAO.getNodeType(childNodeId);
Set childNodeAspectQNames = nodeDAO.getNodeAspects(childNodeId);
Pair oldParentAssocPair = nodeDAO.getPrimaryParentAssoc(childNodeId);
- Pair newChildNodePair = oldChildNodePair;
Pair newParentAssocPair = oldParentAssocPair;
ChildAssociationRef newParentAssocRef = newParentAssocPair.getSecond();
@@ -2389,7 +2394,18 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl
newParentAssocRef.getQName(),
childNodeTypeQName);
// Move the node as this gives back the primary parent association
- newParentAssocPair = nodeDAO.moveNode(childNodeId, nodeId, null,null);
+ Pair, Pair> moveResult;
+ try
+ {
+ moveResult = nodeDAO.moveNode(childNodeId, nodeId, null,null);
+ }
+ catch (NodeExistsException e)
+ {
+ deleteNode(e.getNodePair().getSecond());
+ moveResult = nodeDAO.moveNode(childNodeId, nodeId, null,null);
+ }
+ newParentAssocPair = moveResult.getFirst();
+ Pair newChildNodePair = moveResult.getSecond();
// Index
nodeIndexer.indexCreateNode(newParentAssocPair.getSecond());
// Fire node policies. This ensures that each node in the hierarchy gets a notification fired.
diff --git a/source/java/org/alfresco/repo/transfer/RepoTransferReceiverImpl.java b/source/java/org/alfresco/repo/transfer/RepoTransferReceiverImpl.java
index 3212428278..35b3c491e2 100644
--- a/source/java/org/alfresco/repo/transfer/RepoTransferReceiverImpl.java
+++ b/source/java/org/alfresco/repo/transfer/RepoTransferReceiverImpl.java
@@ -1,4 +1,4 @@
-/*
+/*
* Copyright (C) 2009-2010 Alfresco Software Limited.
*
* This file is part of Alfresco
@@ -14,1671 +14,1657 @@
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
- * along with Alfresco. If not, see .
- */
-
-package org.alfresco.repo.transfer;
-
-import java.io.BufferedOutputStream;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.Serializable;
-import java.text.SimpleDateFormat;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-
-import javax.xml.parsers.SAXParser;
-import javax.xml.parsers.SAXParserFactory;
-
-import org.alfresco.model.ContentModel;
-import org.alfresco.repo.content.ContentServicePolicies;
-import org.alfresco.repo.copy.CopyBehaviourCallback;
-import org.alfresco.repo.copy.CopyDetails;
-import org.alfresco.repo.copy.CopyServicePolicies;
-import org.alfresco.repo.copy.DefaultCopyBehaviourCallback;
-import org.alfresco.repo.lock.JobLockService;
-import org.alfresco.repo.lock.LockAcquisitionException;
-import org.alfresco.repo.node.NodeServicePolicies;
-import org.alfresco.repo.policy.BehaviourFilter;
-import org.alfresco.repo.policy.ClassPolicyDelegate;
-import org.alfresco.repo.policy.JavaBehaviour;
-import org.alfresco.repo.policy.PolicyComponent;
-import org.alfresco.repo.policy.Behaviour.NotificationFrequency;
-import org.alfresco.repo.security.authentication.AuthenticationUtil;
-import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
-import org.alfresco.repo.tenant.TenantService;
-import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
-import org.alfresco.repo.transaction.RetryingTransactionHelper;
-import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
-import org.alfresco.repo.transfer.ChangeCapturingProgressMonitor.TransferChangesRecord;
-import org.alfresco.repo.transfer.manifest.TransferManifestProcessor;
-import org.alfresco.repo.transfer.manifest.XMLTransferManifestReader;
-import org.alfresco.repo.transfer.requisite.XMLTransferRequsiteWriter;
-import org.alfresco.service.cmr.action.Action;
-import org.alfresco.service.cmr.action.ActionService;
-import org.alfresco.service.cmr.repository.ChildAssociationRef;
-import org.alfresco.service.cmr.repository.ContentWriter;
-import org.alfresco.service.cmr.repository.NodeRef;
-import org.alfresco.service.cmr.repository.NodeService;
-import org.alfresco.service.cmr.repository.StoreRef;
-import org.alfresco.service.cmr.rule.RuleService;
-import org.alfresco.service.cmr.search.ResultSet;
-import org.alfresco.service.cmr.search.SearchService;
-import org.alfresco.service.cmr.transfer.TransferException;
-import org.alfresco.service.cmr.transfer.TransferProgress;
-import org.alfresco.service.cmr.transfer.TransferReceiver;
-import org.alfresco.service.cmr.transfer.TransferServicePolicies;
-import org.alfresco.service.cmr.transfer.TransferProgress.Status;
-import org.alfresco.service.cmr.transfer.TransferServicePolicies.BeforeStartInboundTransferPolicy;
-import org.alfresco.service.cmr.transfer.TransferServicePolicies.OnEndInboundTransferPolicy;
-import org.alfresco.service.cmr.transfer.TransferServicePolicies.OnStartInboundTransferPolicy;
-import org.alfresco.service.cmr.transfer.TransferVersion;
-import org.alfresco.service.descriptor.Descriptor;
-import org.alfresco.service.descriptor.DescriptorService;
-import org.alfresco.service.namespace.NamespaceService;
-import org.alfresco.service.namespace.QName;
-import org.alfresco.service.namespace.RegexQNamePattern;
-import org.alfresco.service.transaction.TransactionService;
-import org.alfresco.util.PropertyCheck;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-import org.springframework.util.FileCopyUtils;
-
-/**
- * The Repo Transfer Receiver is the "Back-End" for transfer subsystem.
- *
- * Provides the implementation of the transfer commands on the destination repository.
- *
- * Provides callback handlers for Aliens and Transferred Aspects.
- *
- * Calls transfer policies.
- *
- * Co-ordinates locking and logging as the transfer progresses.
- *
- * @author brian
- */
-public class RepoTransferReceiverImpl implements TransferReceiver,
- NodeServicePolicies.OnCreateChildAssociationPolicy,
- NodeServicePolicies.BeforeDeleteNodePolicy,
- NodeServicePolicies.OnRestoreNodePolicy,
- NodeServicePolicies.OnMoveNodePolicy,
- ContentServicePolicies.OnContentUpdatePolicy
-{
- /**
- * This embedded class is used to push requests for asynchronous commits onto a different thread
- *
- * @author Brian
- *
- */
- public class AsyncCommitCommand implements Runnable
- {
-
- private String transferId;
- private String runAsUser;
-
- public AsyncCommitCommand(String transferId)
- {
- this.transferId = transferId;
- this.runAsUser = AuthenticationUtil.getFullyAuthenticatedUser();
- }
-
- public void run()
- {
- RunAsWork