From c72a19b637b8d042c6e5edfc5bc941602a9ec458 Mon Sep 17 00:00:00 2001 From: Derek Hulley Date: Wed, 24 Aug 2011 23:09:39 +0000 Subject: [PATCH] Merged DEV to HEAD: 29876: Removed emotive language from build.xml 29877: Made getter final; should make members final. 29879: Extra check for erroneous, pre-existing transactions 29880: ALF-8966: RSOLR 042: Node deletes keep DB ID (ALF-8965) - There is no longer code to change the store ID and UUID for an entry in alf_node - During store moves, new node entries are created and node data is moved onto the new node - Primarily affects archive/restore use cases - Any NodeRef can be tracked using the DB ID associated with it (see NodeService.getNodeStatus) 29926: Minor toString modification 29927: Code cleanup: line endings, non-javadoc and isDebugEnabled 29928: Moved exception absorbtion out of the non-propagating txn (and cleaned up unused code) 29930: Test for ALF-5714: Write transactions serialized on MySQL - The fix is to add this to the MySQL configuration and restart the MySQL server innodb_locks_unsafe_for_binlog = true See: http://dev.mysql.com/doc/refman/5.1/en/innodb-parameters.html#sysvar_innodb_locks_unsafe_for_binlog - The test holds DB resources from one archive process while doing another. It fails without the setting given. - TODO: Documentation required 29969: ALF-8966: RSOLR 042: Node deletes keep DB ID (ALF-8965) - Fixed timing of circular hierarchy detection 29972: ALF-8966: RSOLR 042: Node deletes keep DB ID (ALF-8965) - Fixed ACL retrieval from old parent node during move - Fixes PermissionServiceTest failures 29979: Fix test's initialization 29987: Fixed txn demarcation during write of transfer report git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@30044 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- .../node-common-SqlMap.xml | 39 +- .../repo/domain/node/AbstractNodeDAOImpl.java | 326 +- .../repo/domain/node/ChildAssocEntity.java | 4 +- .../alfresco/repo/domain/node/NodeDAO.java | 18 +- .../repo/domain/node/NodeExistsException.java | 47 + .../repo/domain/node/NodeUpdateEntity.java | 24 +- .../repo/domain/node/ibatis/NodeDAOImpl.java | 37 + .../permissions/ADMAccessControlListDAO.java | 89 +- .../permissions/AccessControlListDAO.java | 2 +- .../alfresco/repo/node/NodeServiceTest.java | 187 + .../node/archive/ArchiveAndRestoreTest.java | 42 +- .../repo/node/db/DbNodeServiceImpl.java | 74 +- .../transfer/RepoTransferReceiverImpl.java | 3324 ++++++++--------- .../transfer/TransferServiceCallbackTest.java | 5 +- .../repo/transfer/TransferServiceImpl2.java | 185 +- .../transfer/TransferServiceImplTest.java | 9 + 16 files changed, 2316 insertions(+), 2096 deletions(-) create mode 100644 source/java/org/alfresco/repo/domain/node/NodeExistsException.java 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 actionRunAs = new RunAsWork() - { - public Object doWork() throws Exception - { - return transactionService.getRetryingTransactionHelper().doInTransaction( - new RetryingTransactionCallback() - { - public Object execute() - { - commit(transferId); - return null; - } - }, false, true); - } - }; - AuthenticationUtil.runAs(actionRunAs, runAsUser); - } - - } - - private final static Log log = LogFactory.getLog(RepoTransferReceiverImpl.class); - - private static final String MSG_FAILED_TO_CREATE_STAGING_FOLDER = "transfer_service.receiver.failed_to_create_staging_folder"; - private static final String MSG_ERROR_WHILE_STARTING = "transfer_service.receiver.error_start"; - private static final String MSG_TRANSFER_TEMP_FOLDER_NOT_FOUND = "transfer_service.receiver.temp_folder_not_found"; - private static final String MSG_TRANSFER_LOCK_UNAVAILABLE = "transfer_service.receiver.lock_unavailable"; - private static final String MSG_INBOUND_TRANSFER_FOLDER_NOT_FOUND = "transfer_service.receiver.record_folder_not_found"; - - private static final String MSG_ERROR_WHILE_ENDING_TRANSFER = "transfer_service.receiver.error_ending_transfer"; - private static final String MSG_ERROR_WHILE_STAGING_SNAPSHOT = "transfer_service.receiver.error_staging_snapshot"; - private static final String MSG_ERROR_WHILE_STAGING_CONTENT = "transfer_service.receiver.error_staging_content"; - private static final String MSG_NO_SNAPSHOT_RECEIVED = "transfer_service.receiver.no_snapshot_received"; - private static final String MSG_ERROR_WHILE_COMMITTING_TRANSFER = "transfer_service.receiver.error_committing_transfer"; - private static final String MSG_ERROR_WHILE_GENERATING_REQUISITE = "transfer_service.receiver.error_generating_requisite"; - private static final String MSG_LOCK_TIMED_OUT = "transfer_service.receiver.lock_timed_out"; - private static final String MSG_LOCK_NOT_FOUND = "transfer_service.receiver.lock_not_found"; - private static final String MSG_TRANSFER_TO_SELF = "transfer_service.receiver.error.transfer_to_self"; - private static final String MSG_INCOMPATIBLE_VERSIONS = "transfer_service.incompatible_versions"; - - private static final String SNAPSHOT_FILE_NAME = "snapshot.xml"; - - private NodeService nodeService; - private SearchService searchService; - private TransactionService transactionService; - private String transferLockFolderPath; - private String inboundTransferRecordsPath; - private String rootStagingDirectory; - private String transferTempFolderPath; - private ManifestProcessorFactory manifestProcessorFactory; - private BehaviourFilter behaviourFilter; - private ChangeCapturingProgressMonitor progressMonitor; - private ActionService actionService; - private TenantService tenantService; - private RuleService ruleService; - private PolicyComponent policyComponent; - private DescriptorService descriptorService; - private AlienProcessor alienProcessor; - private JobLockService jobLockService; - private TransferVersionChecker transferVersionChecker; - - /** - * Where the temporary files are stored. Tenant Domain Name, NodeRef - */ - private Map transferTempFolderMap = new ConcurrentHashMap(); - - /** - * Where the destination side transfer report is generated. Tenant Domain Name, NodeRef - */ - private Map inboundTransferRecordsFolderMap = new ConcurrentHashMap(); - - private ClassPolicyDelegate beforeStartInboundTransferDelegate; - private ClassPolicyDelegate onStartInboundTransferDelegate; - private ClassPolicyDelegate onEndInboundTransferDelegate; - - /** - * Locks for the transfers in progress - *

- * TransferId, Lock - */ - private Map locks = new ConcurrentHashMap(); - - /** - * How many mS before refreshing the lock? - */ - private long lockRefreshTime = 60000; - - /** - * How many times to retry to obtain the lock - */ - private int lockRetryCount = 2; - - /** - * How long to wait between retries - */ - private long lockRetryWait = 100; - - /** - * How long in mS to keep the lock before giving up and ending the transfer, - * possibly the client has terminated? - */ - private long lockTimeOut = 3600000; - - public void init() - { - PropertyCheck.mandatory(this, "nodeService", nodeService); - PropertyCheck.mandatory(this, "searchService", searchService); - PropertyCheck.mandatory(this, "ruleService", ruleService); - PropertyCheck.mandatory(this, "actionService", actionService); - PropertyCheck.mandatory(this, "behaviourFilter", behaviourFilter); - PropertyCheck.mandatory(this, "tennantService", tenantService); - PropertyCheck.mandatory(this, "transactionService", transactionService); - PropertyCheck.mandatory(this, "transferLockFolderPath", transferLockFolderPath); - PropertyCheck.mandatory(this, "inboundTransferRecordsPath", inboundTransferRecordsPath); - PropertyCheck.mandatory(this, "rootStagingDirectory", rootStagingDirectory); - PropertyCheck.mandatory(this, "policyComponent", policyComponent); - PropertyCheck.mandatory(this, "descriptorService", descriptorService); - PropertyCheck.mandatory(this, "alienProcessor", alienProcessor); - PropertyCheck.mandatory(this, "jobLockService", getJobLockService()); - PropertyCheck.mandatory(this, "transferVersionChecker", getTransferVersionChecker()); - - beforeStartInboundTransferDelegate = policyComponent.registerClassPolicy(TransferServicePolicies.BeforeStartInboundTransferPolicy.class); - onStartInboundTransferDelegate = policyComponent.registerClassPolicy(TransferServicePolicies.OnStartInboundTransferPolicy.class); - onEndInboundTransferDelegate = policyComponent.registerClassPolicy(TransferServicePolicies.OnEndInboundTransferPolicy.class); - - /** - * For every new child of a node with the trx:transferred aspect run this.onCreateChildAssociation - */ - this.getPolicyComponent().bindAssociationBehaviour( - NodeServicePolicies.OnCreateChildAssociationPolicy.QNAME, - TransferModel.ASPECT_TRANSFERRED, - new JavaBehaviour(this, "onCreateChildAssociation", NotificationFrequency.EVERY_EVENT)); - - /** - * For every update of a transferred node - */ - this.getPolicyComponent().bindClassBehaviour( - ContentServicePolicies.OnContentUpdatePolicy.QNAME, - TransferModel.ASPECT_TRANSFERRED, - new JavaBehaviour(this, "onContentUpdate", NotificationFrequency.EVERY_EVENT)); - - /** - * For every copy of a transferred node run onCopyTransferred - */ - this.getPolicyComponent().bindClassBehaviour( - CopyServicePolicies.OnCopyNodePolicy.QNAME, - TransferModel.ASPECT_TRANSFERRED, - new JavaBehaviour(this, "onCopyTransferred", NotificationFrequency.EVERY_EVENT)); - - /** - * For every new child of a node with the trx:alien aspect run this.onCreateChildAssociation - */ - this.getPolicyComponent().bindAssociationBehaviour( - NodeServicePolicies.OnCreateChildAssociationPolicy.QNAME, - TransferModel.ASPECT_ALIEN, - new JavaBehaviour(this, "onCreateChildAssociation", NotificationFrequency.EVERY_EVENT)); - - /** - * For every node with the trx:alien aspect run this.beforeDeleteNode - */ - this.getPolicyComponent().bindClassBehaviour( - NodeServicePolicies.BeforeDeleteNodePolicy.QNAME, - TransferModel.ASPECT_ALIEN, - new JavaBehaviour(this, "beforeDeleteNode", NotificationFrequency.EVERY_EVENT)); - - /** - * For every restore of a node with the trx:alien aspect - */ - this.getPolicyComponent().bindClassBehaviour( - NodeServicePolicies.OnRestoreNodePolicy.QNAME, - TransferModel.ASPECT_ALIEN, - new JavaBehaviour(this, "onRestoreNode", NotificationFrequency.EVERY_EVENT)); - - /** - * For every move of a node with the trx:alien aspect. - */ - this.getPolicyComponent().bindClassBehaviour( - NodeServicePolicies.OnMoveNodePolicy.QNAME, - TransferModel.ASPECT_ALIEN, - new JavaBehaviour(this, "onMoveNode", NotificationFrequency.EVERY_EVENT)); - - /** - * For every copy of an alien node remove the alien aspect - */ - this.getPolicyComponent().bindClassBehaviour( - CopyServicePolicies.OnCopyNodePolicy.QNAME, - TransferModel.ASPECT_ALIEN, - new JavaBehaviour(this, "onCopyAlien", NotificationFrequency.EVERY_EVENT)); - } - - /* - * (non-Javadoc) - * - * @see - * org.alfresco.repo.web.scripts.transfer.TransferReceiver#getStagingFolder(org.alfresco.service.cmr.repository. - * NodeRef) - */ - public File getStagingFolder(String transferId) - { - if (transferId == null) - { - throw new IllegalArgumentException("transferId = " + transferId); - } - NodeRef transferNodeRef = new NodeRef(transferId); - File tempFolder; - String tempFolderPath = rootStagingDirectory + "/" + transferNodeRef.getId(); - tempFolder = new File(tempFolderPath); - if (!tempFolder.exists()) - { - if (!tempFolder.mkdirs()) - { - tempFolder = null; - throw new TransferException(MSG_FAILED_TO_CREATE_STAGING_FOLDER, new Object[] { transferId }); - } - } - return tempFolder; - - } - - public NodeRef getTempFolder(String transferId) - { - String tenantDomain = tenantService.getUserDomain(AuthenticationUtil.getRunAsUser()); - NodeRef transferTempFolder = transferTempFolderMap.get(tenantDomain); - - // Have we already resolved the node that is the temp folder? - // If not then do so. - if (transferTempFolder == null) - { - ResultSet rs = searchService.query(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, SearchService.LANGUAGE_XPATH, - transferTempFolderPath); - if (rs.length() > 0) - { - transferTempFolder = rs.getNodeRef(0); - transferTempFolderMap.put(tenantDomain, transferTempFolder); - } - else - { - throw new TransferException(MSG_TRANSFER_TEMP_FOLDER_NOT_FOUND, new Object[] { transferId, - transferTempFolderPath }); - } - } - - NodeRef transferNodeRef = new NodeRef(transferId); - String tempTransferFolderName = transferNodeRef.getId(); - NodeRef tempFolderNode = null; - QName folderName = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, tempTransferFolderName); - - // Do we already have a temp folder for this transfer? - List tempChildren = nodeService.getChildAssocs(transferTempFolder, - RegexQNamePattern.MATCH_ALL, folderName); - if (tempChildren.isEmpty()) - { - // No, we don't have a temp folder for this transfer yet. Create it... - Map props = new HashMap(); - props.put(ContentModel.PROP_NAME, tempTransferFolderName); - tempFolderNode = nodeService.createNode(transferTempFolder, ContentModel.ASSOC_CONTAINS, folderName, - TransferModel.TYPE_TEMP_TRANSFER_STORE, props).getChildRef(); - } - else - { - // Yes, we do have a temp folder for this transfer already. Return it. - tempFolderNode = tempChildren.get(0).getChildRef(); - } - return tempFolderNode; - - } - - /* - * (non-Javadoc) - * - * @see org.alfresco.repo.web.scripts.transfer.TransferReceiver#start() - */ - public String start(String fromRepositoryId, boolean transferToSelf, TransferVersion fromVersion) - { - log.debug("Start transfer"); - - /** - * Check that transfer is allowed to this repository - */ - checkTransfer(fromRepositoryId, transferToSelf); - - /** - * Check that the versions are compatible - */ - TransferVersion toVersion = getVersion(); - - if(!getTransferVersionChecker().checkTransferVersions(fromVersion, toVersion)) - { - throw new TransferException(MSG_INCOMPATIBLE_VERSIONS, new Object[] {"None", fromVersion, toVersion}); - } - - /** - * First get the transfer lock for this domain - */ - String tenantDomain = tenantService.getUserDomain(AuthenticationUtil.getRunAsUser()); - String lockStr = tenantDomain.isEmpty() ? "transfer.server.default" : "transfer.server.tenant." + tenantDomain; - QName lockQName = QName.createQName(TransferModel.TRANSFER_MODEL_1_0_URI, lockStr); - Lock lock = new Lock(lockQName); - - try - { - TransferServicePolicies.BeforeStartInboundTransferPolicy beforeStartPolicy = - beforeStartInboundTransferDelegate.get(TransferModel.TYPE_TRANSFER_RECORD); - beforeStartPolicy.beforeStartInboundTransfer(); - - lock.makeLock(); - - /** - * Transfer Lock held if we get this far - */ - String transferId = null; - - try - { - /** - * Now create a transfer record and use its NodeRef as the transfer id - */ - RetryingTransactionHelper txHelper = transactionService.getRetryingTransactionHelper(); - - transferId = txHelper.doInTransaction( - new RetryingTransactionHelper.RetryingTransactionCallback() - { - public String execute() throws Throwable - { - final NodeRef relatedTransferRecord = createTransferRecord(); - String transferId = relatedTransferRecord.toString(); - getTempFolder(transferId); - getStagingFolder(transferId); - - TransferServicePolicies.OnStartInboundTransferPolicy onStartPolicy = - onStartInboundTransferDelegate.get(TransferModel.TYPE_TRANSFER_RECORD); - onStartPolicy.onStartInboundTransfer(transferId); - - return transferId; - } - }, false, true); - } - catch (Exception e) - { - log.debug("Exception while staring transfer", e); - log.debug("releasing lock - we never created the transfer id"); - lock.releaseLock(); - throw new TransferException(MSG_ERROR_WHILE_STARTING, e); - } - - /** - * Here if we have begun a transfer and have a valid transfer id - */ - lock.transferId = transferId; - locks.put(transferId, lock); - log.info("transfer started:" + transferId); - lock.enableLockTimeout(); - return transferId; - - } - catch (LockAcquisitionException lae) - { - log.debug("transfer lock is already taken", lae); - // lock is already taken. - throw new TransferException(MSG_TRANSFER_LOCK_UNAVAILABLE); - } - } - - /** - * @return - */ - private NodeRef createTransferRecord() - { - log.debug("Receiver createTransferRecord"); - String tenantDomain = tenantService.getUserDomain(AuthenticationUtil.getRunAsUser()); - NodeRef inboundTransferRecordsFolder = inboundTransferRecordsFolderMap.get(tenantDomain); - - if (inboundTransferRecordsFolder == null) - { - log.debug("Trying to find transfer records folder: " + inboundTransferRecordsPath); - ResultSet rs = searchService.query(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, SearchService.LANGUAGE_XPATH, - inboundTransferRecordsPath); - if (rs.length() > 0) - { - inboundTransferRecordsFolder = rs.getNodeRef(0); - inboundTransferRecordsFolderMap.put(tenantDomain, inboundTransferRecordsFolder); - log.debug("Found inbound transfer records folder: " + inboundTransferRecordsFolder); - } - else - { - throw new TransferException(MSG_INBOUND_TRANSFER_FOLDER_NOT_FOUND, - new Object[] { inboundTransferRecordsPath }); - } - } - - SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmssSSSZ"); - String timeNow = format.format(new Date()); - String name = timeNow + ".xml"; - - QName recordName = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, name); - - Map props = new HashMap(); - props.put(ContentModel.PROP_NAME, name); - props.put(TransferModel.PROP_PROGRESS_POSITION, 0); - props.put(TransferModel.PROP_PROGRESS_ENDPOINT, 1); - props.put(TransferModel.PROP_TRANSFER_STATUS, TransferProgress.Status.PRE_COMMIT.toString()); - - log.debug("Creating transfer record with name: " + name); - ChildAssociationRef assoc = nodeService.createNode(inboundTransferRecordsFolder, ContentModel.ASSOC_CONTAINS, - recordName, TransferModel.TYPE_TRANSFER_RECORD, props); - log.debug("<-createTransferRecord: " + assoc.getChildRef()); - - return assoc.getChildRef(); - } - - /** - * Timeout a transfer. Called after the lock has been released via a timeout. - * - * This is the last chance to clean up. - * - * @param transferId - */ - private void timeout(final String transferId) - { - log.info("Inbound Transfer has timed out transferId:" + transferId); - /* - * There is no transaction or authentication context in this method since it is called via a - * timer thread. - */ - final RetryingTransactionCallback timeoutCB = new RetryingTransactionCallback() { - - - public Void execute() throws Throwable - { - TransferProgress progress = getProgressMonitor().getProgress(transferId); - - if (progress.getStatus().equals(TransferProgress.Status.PRE_COMMIT)) - { - log.warn("Inbound Transfer Lock Timeout - transferId:" + transferId); - /** - * Did not get out of PRE_COMMIT. The client has probably "gone away" after calling - * "start", but before calling commit, cancel or error. - */ - locks.remove(transferId); - removeTempFolders(transferId); - Object[] msgParams = { transferId }; - getProgressMonitor().logException(transferId, "transfer timeout", new TransferException(MSG_LOCK_TIMED_OUT, msgParams)); - getProgressMonitor().updateStatus(transferId, TransferProgress.Status.ERROR); - } - else - { - // We got beyond PRE_COMMIT, therefore leave the clean up to either - // commit, cancel or error command, since there may still be "in-flight" - // transfer in another thread. Although why, in that case, are we here? - log.warn("Inbound Transfer Lock Timeout - already past PRE-COMMIT - do no cleanup transferId:" + transferId); - } - return null; - } - }; - - AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() - { - public String doWork() throws Exception - { - transactionService.getRetryingTransactionHelper().doInTransaction(timeoutCB, false, true); - return null; - } - }, AuthenticationUtil.getSystemUserName()); - } - - /* - * (non-Javadoc) - * - * @see org.alfresco.repo.web.scripts.transfer.TransferReceiver#end(org.alfresco.service.cmr.repository.NodeRef) - */ - public void end(final String transferId) - { - if (log.isDebugEnabled()) - { - log.debug("Request to end transfer " + transferId); - } - if (transferId == null) - { - throw new IllegalArgumentException("transferId = null"); - } - - try - { - Lock lock = locks.get(transferId); - if(lock != null) - { - log.debug("releasing lock:" + lock.lockToken); - lock.releaseLock(); - locks.remove(lock); - } - - removeTempFolders(transferId); - - - //Fire the OnEndInboundTransfer policy - Set createdNodes = Collections.emptySet(); - Set updatedNodes = Collections.emptySet(); - Set deletedNodes = Collections.emptySet(); - TransferChangesRecord changesRecord = progressMonitor.removeChangeRecord(transferId); - if (changesRecord != null) - { - createdNodes = new HashSet(changesRecord.getCreatedNodes()); - updatedNodes = new HashSet(changesRecord.getUpdatedNodes()); - deletedNodes = new HashSet(changesRecord.getDeletedNodes()); - } - TransferServicePolicies.OnEndInboundTransferPolicy onEndPolicy = - onEndInboundTransferDelegate.get(TransferModel.TYPE_TRANSFER_RECORD); - onEndPolicy.onEndInboundTransfer(transferId, createdNodes, updatedNodes, deletedNodes); - } - catch (TransferException ex) - { - throw ex; - } - catch (Exception ex) - { - throw new TransferException(MSG_ERROR_WHILE_ENDING_TRANSFER, new Object[] {transferId}, ex); - } - } - - private void removeTempFolders(final String transferId) - { - NodeRef tempStoreNode = null; - try - { - log.debug("Deleting temporary store node..."); - tempStoreNode = getTempFolder(transferId); - nodeService.deleteNode(tempStoreNode); - log.debug("Deleted temporary store node."); - } - catch (Exception ex) - { - log.warn("Failed to delete temp store node for transfer id " + transferId + - "\nTemp store noderef = " + tempStoreNode); - } - - File stagingFolder = null; - try - { - log.debug("delete staging folder " + transferId); - // Delete the staging folder. - stagingFolder = getStagingFolder(transferId); - deleteFile(stagingFolder); - log.debug("Staging folder deleted"); - } - catch(Exception ex) - { - log.warn("Failed to delete staging folder for transfer id " + transferId + - "\nStaging folder = " + stagingFolder.toString()); - } - } - - - public void cancel(String transferId) throws TransferException - { - // no need to check the lock - TransferProgress progress = getProgressMonitor().getProgress(transferId); - getProgressMonitor().updateStatus(transferId, TransferProgress.Status.CANCELLED); - if (progress.getStatus().equals(TransferProgress.Status.PRE_COMMIT)) - { - end(transferId); - } - } - - public void prepare(String transferId) throws TransferException - { - // Check that this transfer still owns the lock - Lock lock = checkLock(transferId); - try - { - - } - finally - { - lock.enableLockTimeout(); - } - - } - - /** - * @param stagingFolder - */ - private void deleteFile(File file) - { - if (file.isDirectory()) - { - File[] fileList = file.listFiles(); - if (fileList != null) - { - for (File currentFile : fileList) - { - deleteFile(currentFile); - } - } - } - file.delete(); - } - - /* - * (non-Javadoc) - * - * @see org.alfresco.service.cmr.transfer.TransferReceiver#nudgeLock(java.lang.String) - */ - public Lock checkLock(final String transferId) throws TransferException - { - if (transferId == null) - { - throw new IllegalArgumentException("nudgeLock: transferId = null"); - } - - Lock lock = locks.get(transferId); - if(lock != null) - { - if(lock.isActive()) - { - lock.suspendLockTimeout(); - return lock; - } - else - { - // lock is no longer active - log.debug("lock not active"); - throw new TransferException(MSG_LOCK_TIMED_OUT, new Object[]{transferId}); - - } - } - else - { - log.debug("lock not found"); - throw new TransferException(MSG_LOCK_NOT_FOUND, new Object[]{transferId}); - // lock not found - } - } - - /* - * (non-Javadoc) - * - * @see org.alfresco.service.cmr.transfer.TransferReceiver#saveSnapshot(java.io.InputStream) - */ - public void saveSnapshot(String transferId, InputStream openStream) throws TransferException - { - // Check that this transfer still owns the lock - Lock lock = checkLock(transferId); - try - { - if (log.isDebugEnabled()) - { - log.debug("Saving snapshot for transferId =" + transferId); - } - - File snapshotFile = new File(getStagingFolder(transferId), SNAPSHOT_FILE_NAME); - try - { - if (snapshotFile.createNewFile()) - { - FileCopyUtils.copy(openStream, new BufferedOutputStream(new FileOutputStream(snapshotFile))); - } - if (log.isDebugEnabled()) - { - log.debug("Saved snapshot for transferId =" + transferId); - } - } - catch (Exception ex) - { - throw new TransferException(MSG_ERROR_WHILE_STAGING_SNAPSHOT, new Object[]{transferId}, ex); - } - } - finally - { - lock.enableLockTimeout(); - } - } - - /* - * (non-Javadoc) - * - * @see org.alfresco.service.cmr.transfer.TransferReceiver#saveContent(java.lang.String, java.lang.String, - * java.io.InputStream) - */ - public void saveContent(String transferId, String contentFileId, InputStream contentStream) - throws TransferException - { - Lock lock = checkLock(transferId); - try - { - File stagedFile = new File(getStagingFolder(transferId), contentFileId); - if (stagedFile.createNewFile()) - { - FileCopyUtils.copy(contentStream, new BufferedOutputStream(new FileOutputStream(stagedFile))); - } - } - catch (Exception ex) - { - throw new TransferException(MSG_ERROR_WHILE_STAGING_CONTENT, new Object[]{transferId, contentFileId}, ex); - } - finally - { - lock.enableLockTimeout(); - } - } - - public void commitAsync(String transferId) - { - /** - * A side-effect of checking the lock here is that the lock timeout is suspended. - * - */ - Lock lock = checkLock(transferId); - try - { - progressMonitor.updateStatus(transferId, Status.COMMIT_REQUESTED); - Action commitAction = actionService.createAction(TransferCommitActionExecuter.NAME); - commitAction.setParameterValue(TransferCommitActionExecuter.PARAM_TRANSFER_ID, transferId); - commitAction.setExecuteAsynchronously(true); - actionService.executeAction(commitAction, new NodeRef(transferId)); - if (log.isDebugEnabled()) - { - log.debug("Registered transfer commit for asynchronous execution: " + transferId); - } - } - catch (Exception error) - { - /** - * Error somewhere in the action service? - */ - //TODO consider whether the methods in this class should be retried/retryable.. - - // need to re-enable the lock timeout otherwise we will hold the lock forever... - lock.enableLockTimeout(); - - throw new TransferException(MSG_ERROR_WHILE_COMMITTING_TRANSFER, new Object[]{transferId}, error); - } - - /** - * Lock intentionally not re-enabled here - */ - } - - public void commit(final String transferId) throws TransferException - { - if (log.isDebugEnabled()) - { - log.debug("Committing transferId=" + transferId); - } - - /** - * A side-effect of checking the lock here is that it ensures that the lock timeout is suspended. - */ - checkLock(transferId); - - /** - * Turn off rules while transfer is being committed. - */ - boolean rulesEnabled = ruleService.isEnabled(); - ruleService.disableRules(); - - try - { - /* lock is going to be released */ checkLock(transferId); - progressMonitor.updateStatus(transferId, Status.COMMITTING); - - RetryingTransactionHelper.RetryingTransactionCallback commitWork = new RetryingTransactionCallback() - { - public Object execute() throws Throwable - { - AlfrescoTransactionSupport.bindListener(new TransferCommitTransactionListener(transferId, - RepoTransferReceiverImpl.this)); - - List commitProcessors = manifestProcessorFactory.getCommitProcessors( - RepoTransferReceiverImpl.this, transferId); - - SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); - SAXParser parser = saxParserFactory.newSAXParser(); - File snapshotFile = getSnapshotFile(transferId); - - if (snapshotFile.exists()) - { - if (log.isDebugEnabled()) - { - log.debug("Processing manifest file:" + snapshotFile.getAbsolutePath()); - } - // We parse the file as many times as we have processors - for (TransferManifestProcessor processor : commitProcessors) - { - XMLTransferManifestReader reader = new XMLTransferManifestReader(processor); - - //behaviourFilter.disableBehaviour(ContentModel.ASPECT_AUDITABLE); - behaviourFilter.disableAllBehaviours(); - - try - { - parser.parse(snapshotFile, reader); - } - finally - { - // behaviourFilter.enableBehaviour(ContentModel.ASPECT_AUDITABLE); - behaviourFilter.enableAllBehaviours(); - } - parser.reset(); - } - } - else - { - progressMonitor.logException(transferId, "Unable to start commit. No snapshot file received", - new TransferException(MSG_NO_SNAPSHOT_RECEIVED, new Object[]{transferId})); - } - return null; - } - }; - - transactionService.getRetryingTransactionHelper().doInTransaction(commitWork, false, true); - - Throwable error = progressMonitor.getProgress(transferId).getError(); - if (error != null) - { - if (TransferException.class.isAssignableFrom(error.getClass())) - { - throw (TransferException) error; - } - else - { - throw new TransferException(MSG_ERROR_WHILE_COMMITTING_TRANSFER, new Object[]{transferId}, error); - } - } - - /** - * Successfully committed - */ - if (log.isDebugEnabled()) - { - log.debug("Commit success transferId=" + transferId); - } - } - catch (Exception ex) - { - if (TransferException.class.isAssignableFrom(ex.getClass())) - { - throw (TransferException) ex; - } - else - { - throw new TransferException(MSG_ERROR_WHILE_COMMITTING_TRANSFER, ex); - } - } - finally - { - if(rulesEnabled) - { - /** - * Turn rules back on if we turned them off earlier. - */ - ruleService.enableRules(); - } - - /** - * Clean up at the end of the transfer - */ - try - { - end(transferId); - } - catch (Exception ex) - { - log.error("Failed to clean up transfer. Lock may still be in place: " + transferId, ex); - } - } - } - - public TransferProgress getStatus(String transferId) throws TransferException - { - return getProgressMonitor().getProgress(transferId); - } - - private File getSnapshotFile(String transferId) - { - return new File(getStagingFolder(transferId), SNAPSHOT_FILE_NAME); - } - - /** - * @param searchService - * the searchService to set - */ - public void setSearchService(SearchService searchService) - { - this.searchService = searchService; - } - - /** - * @param transactionService - * the transactionService to set - */ - public void setTransactionService(TransactionService transactionService) - { - this.transactionService = transactionService; - } - - public void setTenantService(TenantService tenantService) - { - this.tenantService = tenantService; - } - - /** - * @param transferLockFolderPath - * the transferLockFolderPath to set - */ - public void setTransferLockFolderPath(String transferLockFolderPath) - { - this.transferLockFolderPath = transferLockFolderPath; - } - - /** - * @param transferTempFolderPath - * the transferTempFolderPath to set - */ - public void setTransferTempFolderPath(String transferTempFolderPath) - { - this.transferTempFolderPath = transferTempFolderPath; - } - - /** - * @param rootStagingDirectory - * the rootTransferFolder to set - */ - public void setRootStagingDirectory(String rootStagingDirectory) - { - this.rootStagingDirectory = rootStagingDirectory; - } - - /** - * @param inboundTransferRecordsPath - * the inboundTransferRecordsPath to set - */ - public void setInboundTransferRecordsPath(String inboundTransferRecordsPath) - { - this.inboundTransferRecordsPath = inboundTransferRecordsPath; - } - - /** - * @param nodeService - * the nodeService to set - */ - public void setNodeService(NodeService nodeService) - { - this.nodeService = nodeService; - } - - /** - * @param manifestProcessorFactory - * the manifestProcessorFactory to set - */ - public void setManifestProcessorFactory(ManifestProcessorFactory manifestProcessorFactory) - { - this.manifestProcessorFactory = manifestProcessorFactory; - } - - /** - * @param behaviourFilter - * the behaviourFilter to set - */ - public void setBehaviourFilter(BehaviourFilter behaviourFilter) - { - this.behaviourFilter = behaviourFilter; - } - - /** - * @return the progressMonitor - */ - public TransferProgressMonitor getProgressMonitor() - { - return progressMonitor; - } - - /** - * @param progressMonitor - * the progressMonitor to set - */ - public void setProgressMonitor(TransferProgressMonitor progressMonitor) - { - this.progressMonitor = new ChangeCapturingProgressMonitor(progressMonitor); - } - - public void setActionService(ActionService actionService) - { - this.actionService = actionService; - } - - /** - * Set the ruleService - * @param ruleService - * the ruleService to set - */ - public void setRuleService(RuleService ruleService) - { - this.ruleService = ruleService; - } - - /** - * Get the rule service - * @return the rule service - */ - public RuleService getRuleService() - { - return this.ruleService; - } - - /** - * Generate the requsite - */ - public void generateRequsite(String transferId, OutputStream out) throws TransferException - { - log.debug("Generate Requsite for transfer:" + transferId); - try - { - File snapshotFile = getSnapshotFile(transferId); - - if (snapshotFile.exists()) - { - log.debug("snapshot does exist"); - SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); - SAXParser parser = saxParserFactory.newSAXParser(); - OutputStreamWriter dest = new OutputStreamWriter(out, "UTF-8"); - - XMLTransferRequsiteWriter writer = new XMLTransferRequsiteWriter(dest); - TransferManifestProcessor processor = manifestProcessorFactory.getRequsiteProcessor( - RepoTransferReceiverImpl.this, - transferId, - writer); - - XMLTransferManifestReader reader = new XMLTransferManifestReader(processor); - - /** - * Now run the parser - */ - parser.parse(snapshotFile, reader); - - /** - * And flush the destination in case any content remains in the writer. - */ - dest.flush(); - - } - log.debug("Generate Requsite done transfer:" + transferId); - - } - catch (Exception ex) - { - if (TransferException.class.isAssignableFrom(ex.getClass())) - { - throw (TransferException) ex; - } - else - { - throw new TransferException(MSG_ERROR_WHILE_GENERATING_REQUISITE, ex); - } - } - } - - public InputStream getTransferReport(String transferId) - { - return progressMonitor.getLogInputStream(transferId); - } - - public void setPolicyComponent(PolicyComponent policyComponent) - { - this.policyComponent = policyComponent; - } - - public PolicyComponent getPolicyComponent() - { - return policyComponent; - } - - /** - * When a new node is created as a child of a Transferred or Alien node then - * the new node needs to be marked as an alien. - *

- * Then the tree needs to be walked upwards to mark all parent - * transferred nodes as alien. - */ - public void onCreateChildAssociation(ChildAssociationRef childAssocRef, - boolean isNewNode) - { - - log.debug("on create child association to transferred node"); - - final String localRepositoryId = descriptorService.getCurrentRepositoryDescriptor().getId(); - alienProcessor.onCreateChild(childAssocRef, localRepositoryId, isNewNode); - } - - /** - * When an alien node is deleted the it may be the last alien invader - *

- * Walk the tree checking the invasion status! - */ - public void beforeDeleteNode(NodeRef deletedNodeRef) - { - log.debug("on delete node - need to check for transferred node"); - alienProcessor.beforeDeleteAlien(deletedNodeRef, null); - } - - /** - * When a transferred node is restored it may be a new invader or it may no - * longer be an invader. - *

- * Walk the tree checking the invasion status! - */ - public void onRestoreNode(ChildAssociationRef childAssocRef) - { - log.debug("on restore node"); - log.debug("restoredAssocRef:" + childAssocRef); - alienProcessor.afterMoveAlien(childAssocRef); - } - - /** - * When an alien node is moved it may un-invade its old location and invade a new - * location. The node may also cease to be alien. - */ - public void onMoveNode(ChildAssociationRef oldChildAssocRef, - ChildAssociationRef newChildAssocRef) - { - - log.debug("onMoveNode"); - log.debug("oldChildAssocRef:" + oldChildAssocRef); - log.debug("newChildAssocRef:" + newChildAssocRef); - - NodeRef oldParentRef = oldChildAssocRef.getParentRef(); - NodeRef newParentRef = newChildAssocRef.getParentRef(); - - if(newParentRef.equals(oldParentRef)) - { - log.debug("old parent and new parent are the same - this is a rename, do nothing"); - } - else - { - if(log.isDebugEnabled()) - { - log.debug("moving node from oldParentRef:" + oldParentRef +" to:" + newParentRef); - } - alienProcessor.beforeDeleteAlien(newChildAssocRef.getChildRef(), oldChildAssocRef); - alienProcessor.afterMoveAlien(newChildAssocRef); - } - } - - /** - * When a transferred node is copied, don't copy the transferred aspect. - */ - public CopyBehaviourCallback onCopyTransferred(QName classRef, - CopyDetails copyDetails) - { - return TransferredAspectCopyBehaviourCallback.INSTANCE; - } - - /** - * When an alien node is copied, don't copy the alien aspect. - */ - public CopyBehaviourCallback onCopyAlien(QName classRef, - CopyDetails copyDetails) - { - return AlienAspectCopyBehaviourCallback.INSTANCE; - } - - /** - * Extends the default copy behaviour to prevent copying of transferred aspect and properties. - * - * @author Mark Rogers - * @since 3.4 - */ - private static class TransferredAspectCopyBehaviourCallback extends DefaultCopyBehaviourCallback - { - private static final CopyBehaviourCallback INSTANCE = new TransferredAspectCopyBehaviourCallback(); - - /** - * @return Returns an empty map - */ - @Override - public Map getCopyProperties( - QName classQName, CopyDetails copyDetails, Map properties) - { - return Collections.emptyMap(); - } - - /** - * Don't copy the transferred aspect. - * - * @return Returns true always - */ - @Override - public boolean getMustCopy(QName classQName, CopyDetails copyDetails) - { - if(classQName.equals(TransferModel.ASPECT_TRANSFERRED)) - { - return false; - } - else - { - return true; - } - } - } - - /** - * Extends the default copy behaviour to prevent copying of alien aspect and properties. - * - * @author Mark Rogers - * @since 3.4 - */ - private static class AlienAspectCopyBehaviourCallback extends DefaultCopyBehaviourCallback - { - private static final CopyBehaviourCallback INSTANCE = new AlienAspectCopyBehaviourCallback(); - - /** - * @return Returns an empty map - */ - @Override - public Map getCopyProperties( - QName classQName, CopyDetails copyDetails, Map properties) - { - return Collections.emptyMap(); - } - - /** - * Don't copy the transferred aspect. - * - * @return Returns true always - */ - @Override - public boolean getMustCopy(QName classQName, CopyDetails copyDetails) - { - if(classQName.equals(TransferModel.ASPECT_ALIEN)) - { - return false; - } - else - { - return true; - } - } - } - - - public void setDescriptorService(DescriptorService descriptorService) - { - this.descriptorService = descriptorService; - } - - public DescriptorService getDescriptorService() - { - return descriptorService; - } - - public void setAlienProcessor(AlienProcessor alienProcessor) - { - this.alienProcessor = alienProcessor; - } - - public AlienProcessor getAlienProcessor() - { - return alienProcessor; - } - - @Override - public void onContentUpdate(NodeRef nodeRef, boolean newContent) - { - /** - * On update of a transferred node remove the from content from property. - */ - log.debug("on content update called:" + nodeRef); - if(newContent) - { - log.debug("new content remove PROP_FROM_CONTENT from node:" + nodeRef); - nodeService.setProperty(nodeRef, TransferModel.PROP_FROM_CONTENT, null); - } - } - - public void setJobLockService(JobLockService jobLockService) - { - this.jobLockService = jobLockService; - } - - public JobLockService getJobLockService() - { - return jobLockService; - } - - public void setLockRetryCount(int lockRetryCount) - { - this.lockRetryCount = lockRetryCount; - } - - public int getLockRetryCount() - { - return lockRetryCount; - } - - public void setLockRetryWait(long lockRetryWait) - { - this.lockRetryWait = lockRetryWait; - } - - public long getLockRetryWait() - { - return lockRetryWait; - } - - public void setLockTimeOut(long lockTimeOut) - { - this.lockTimeOut = lockTimeOut; - } - - public long getLockTimeOut() - { - return lockTimeOut; - } - - public void setLockRefreshTime(long lockRefreshTime) - { - this.lockRefreshTime = lockRefreshTime; - } - - public long getLockRefreshTime() - { - return lockRefreshTime; - } - - /** - * A Transfer Lock - */ - private class Lock implements JobLockService.JobLockRefreshCallback - { - /** - * The name of the lock - unique for each domain - */ - QName lockQName; - - /** - * The unique token for this lock instance. - */ - String lockToken; - - /** - * The transfer that this lock belongs to. - */ - String transferId; - - /** - * Is the lock active ? - */ - private boolean active = false; - - /** - * Is the server processing ? - */ - private boolean processing = false; - - /** - * When did we last check whether the lock is active - */ - Date lastActive = new Date(); - - public Lock(QName lockQName) - { - this.lockQName = lockQName; - } - - - /** - * Make the lock - called on main thread - * - * @throws LockAquisitionException - */ - public void makeLock() - { - if(log.isDebugEnabled()) - { - log.debug("makeLock" + lockQName); - } - - lockToken = getJobLockService().getLock(lockQName, getLockRefreshTime(), getLockRetryWait(), getLockRetryCount()); - - synchronized(this) - { - active = true; - } - - if (log.isDebugEnabled()) - { - log.debug("lock taken: name" + lockQName + " token:" +lockToken); - } - log.debug("register lock callback, target lock refresh time :" + getLockRefreshTime()); - getJobLockService().refreshLock(lockToken, lockQName, getLockRefreshTime(), this); - log.debug("refreshLock callback registered"); - } - - /** - * Check that the lock is still active - * - * Called on main transfer thread as transfer proceeds. - * @throws TransferException (Lock timeout) - */ - public void suspendLockTimeout() - { - log.debug("suspend lock called"); - if(active) - { - processing = true; - } - else - { - // lock is no longer active - log.debug("lock not active, throw timed out exception"); - throw new TransferException(MSG_LOCK_TIMED_OUT); - } - } - - public void enableLockTimeout() - { - Date now = new Date(); - - // Update lastActive to 1S boundary - if(now.getTime() > lastActive.getTime() + 1000) - { - lastActive = new Date(); - log.debug("start waiting : lastActive:" + lastActive); - } - - processing = false; - } - - /** - * Release the lock - * - * Called on main thread - */ - public void releaseLock() - { - if(log.isDebugEnabled()) - { - log.debug("transfer service about to releaseLock : " + lockQName); - } - - synchronized(this) - { - if(active) - { - getJobLockService().releaseLock(lockToken, lockQName); - } - active = false; - } - } - - /** - * Called by Job Lock Service to determine whether the lock is still active - */ - @Override - public boolean isActive() - { - Date now = new Date(); - - synchronized(this) - { - if(active) - { - if(!processing) - { - if(now.getTime() > lastActive.getTime() + getLockTimeOut()) - { - return false; - } - } - } - - if(log.isDebugEnabled()) - { - log.debug("transfer service callback isActive: " + active); - } - - return active; - } - } - - /** - * Called by Job Lock Service on release of the lock after time-out - */ - @Override - public void lockReleased() - { - synchronized(this) - { - if(active) - { - log.info("transfer service: lock has timed out, timeout :" + lockQName); - timeout(transferId); - } - - active = false; - } - } - } - - /** - * Check Whether transfer is allowed from the specified repository. - * Called prior to "begin". - */ - - private void checkTransfer(String fromRepository, boolean transferToSelf) - { - if(log.isDebugEnabled()) - { - log.debug("checkTransfer fromRepository:" + fromRepository + ", transferToSelf:" + transferToSelf ); - } - final String localRepositoryId = descriptorService.getCurrentRepositoryDescriptor().getId(); - - if(!transferToSelf) - { - if(fromRepository != null) - { - if(fromRepository.equalsIgnoreCase(localRepositoryId)) - { - throw new TransferException(MSG_TRANSFER_TO_SELF); - } - } - else - { - throw new TransferException("from repository id is missing"); - } - } - } - - public void setTransferVersionChecker(TransferVersionChecker transferVersionChecker) - { - this.transferVersionChecker = transferVersionChecker; - } - - public TransferVersionChecker getTransferVersionChecker() - { - return transferVersionChecker; - } - - @Override - public TransferVersion getVersion() - { - Descriptor d = descriptorService.getServerDescriptor(); - // needs to be serverDescriptor to pick up versionEdition - return new TransferVersionImpl(d); - } - - public void setFileTransferRootNodeFileFileSystem(String rootFileSystem) - { - //just ignore, no relevant for transferring on file system - } -} + * 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.Behaviour.NotificationFrequency; +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.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.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.TransferProgress.Status; +import org.alfresco.service.cmr.transfer.TransferReceiver; +import org.alfresco.service.cmr.transfer.TransferServicePolicies; +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 actionRunAs = new RunAsWork() + { + public Object doWork() throws Exception + { + return transactionService.getRetryingTransactionHelper().doInTransaction( + new RetryingTransactionCallback() + { + public Object execute() + { + commit(transferId); + return null; + } + }, false, true); + } + }; + AuthenticationUtil.runAs(actionRunAs, runAsUser); + } + + } + + private final static Log log = LogFactory.getLog(RepoTransferReceiverImpl.class); + + private static final String MSG_FAILED_TO_CREATE_STAGING_FOLDER = "transfer_service.receiver.failed_to_create_staging_folder"; + private static final String MSG_ERROR_WHILE_STARTING = "transfer_service.receiver.error_start"; + private static final String MSG_TRANSFER_TEMP_FOLDER_NOT_FOUND = "transfer_service.receiver.temp_folder_not_found"; + private static final String MSG_TRANSFER_LOCK_UNAVAILABLE = "transfer_service.receiver.lock_unavailable"; + private static final String MSG_INBOUND_TRANSFER_FOLDER_NOT_FOUND = "transfer_service.receiver.record_folder_not_found"; + + private static final String MSG_ERROR_WHILE_ENDING_TRANSFER = "transfer_service.receiver.error_ending_transfer"; + private static final String MSG_ERROR_WHILE_STAGING_SNAPSHOT = "transfer_service.receiver.error_staging_snapshot"; + private static final String MSG_ERROR_WHILE_STAGING_CONTENT = "transfer_service.receiver.error_staging_content"; + private static final String MSG_NO_SNAPSHOT_RECEIVED = "transfer_service.receiver.no_snapshot_received"; + private static final String MSG_ERROR_WHILE_COMMITTING_TRANSFER = "transfer_service.receiver.error_committing_transfer"; + private static final String MSG_ERROR_WHILE_GENERATING_REQUISITE = "transfer_service.receiver.error_generating_requisite"; + private static final String MSG_LOCK_TIMED_OUT = "transfer_service.receiver.lock_timed_out"; + private static final String MSG_LOCK_NOT_FOUND = "transfer_service.receiver.lock_not_found"; + private static final String MSG_TRANSFER_TO_SELF = "transfer_service.receiver.error.transfer_to_self"; + private static final String MSG_INCOMPATIBLE_VERSIONS = "transfer_service.incompatible_versions"; + + private static final String SNAPSHOT_FILE_NAME = "snapshot.xml"; + + private NodeService nodeService; + private SearchService searchService; + private TransactionService transactionService; + private String transferLockFolderPath; + private String inboundTransferRecordsPath; + private String rootStagingDirectory; + private String transferTempFolderPath; + private ManifestProcessorFactory manifestProcessorFactory; + private BehaviourFilter behaviourFilter; + private ChangeCapturingProgressMonitor progressMonitor; + private ActionService actionService; + private TenantService tenantService; + private RuleService ruleService; + private PolicyComponent policyComponent; + private DescriptorService descriptorService; + private AlienProcessor alienProcessor; + private JobLockService jobLockService; + private TransferVersionChecker transferVersionChecker; + + /** + * Where the temporary files are stored. Tenant Domain Name, NodeRef + */ + private Map transferTempFolderMap = new ConcurrentHashMap(); + + /** + * Where the destination side transfer report is generated. Tenant Domain Name, NodeRef + */ + private Map inboundTransferRecordsFolderMap = new ConcurrentHashMap(); + + private ClassPolicyDelegate beforeStartInboundTransferDelegate; + private ClassPolicyDelegate onStartInboundTransferDelegate; + private ClassPolicyDelegate onEndInboundTransferDelegate; + + /** + * Locks for the transfers in progress + *

+ * TransferId, Lock + */ + private Map locks = new ConcurrentHashMap(); + + /** + * How many mS before refreshing the lock? + */ + private long lockRefreshTime = 60000; + + /** + * How many times to retry to obtain the lock + */ + private int lockRetryCount = 2; + + /** + * How long to wait between retries + */ + private long lockRetryWait = 100; + + /** + * How long in mS to keep the lock before giving up and ending the transfer, + * possibly the client has terminated? + */ + private long lockTimeOut = 3600000; + + public void init() + { + PropertyCheck.mandatory(this, "nodeService", nodeService); + PropertyCheck.mandatory(this, "searchService", searchService); + PropertyCheck.mandatory(this, "ruleService", ruleService); + PropertyCheck.mandatory(this, "actionService", actionService); + PropertyCheck.mandatory(this, "behaviourFilter", behaviourFilter); + PropertyCheck.mandatory(this, "tennantService", tenantService); + PropertyCheck.mandatory(this, "transactionService", transactionService); + PropertyCheck.mandatory(this, "transferLockFolderPath", transferLockFolderPath); + PropertyCheck.mandatory(this, "inboundTransferRecordsPath", inboundTransferRecordsPath); + PropertyCheck.mandatory(this, "rootStagingDirectory", rootStagingDirectory); + PropertyCheck.mandatory(this, "policyComponent", policyComponent); + PropertyCheck.mandatory(this, "descriptorService", descriptorService); + PropertyCheck.mandatory(this, "alienProcessor", alienProcessor); + PropertyCheck.mandatory(this, "jobLockService", getJobLockService()); + PropertyCheck.mandatory(this, "transferVersionChecker", getTransferVersionChecker()); + + beforeStartInboundTransferDelegate = policyComponent.registerClassPolicy(TransferServicePolicies.BeforeStartInboundTransferPolicy.class); + onStartInboundTransferDelegate = policyComponent.registerClassPolicy(TransferServicePolicies.OnStartInboundTransferPolicy.class); + onEndInboundTransferDelegate = policyComponent.registerClassPolicy(TransferServicePolicies.OnEndInboundTransferPolicy.class); + + /** + * For every new child of a node with the trx:transferred aspect run this.onCreateChildAssociation + */ + this.getPolicyComponent().bindAssociationBehaviour( + NodeServicePolicies.OnCreateChildAssociationPolicy.QNAME, + TransferModel.ASPECT_TRANSFERRED, + new JavaBehaviour(this, "onCreateChildAssociation", NotificationFrequency.EVERY_EVENT)); + + /** + * For every update of a transferred node + */ + this.getPolicyComponent().bindClassBehaviour( + ContentServicePolicies.OnContentUpdatePolicy.QNAME, + TransferModel.ASPECT_TRANSFERRED, + new JavaBehaviour(this, "onContentUpdate", NotificationFrequency.EVERY_EVENT)); + + /** + * For every copy of a transferred node run onCopyTransferred + */ + this.getPolicyComponent().bindClassBehaviour( + CopyServicePolicies.OnCopyNodePolicy.QNAME, + TransferModel.ASPECT_TRANSFERRED, + new JavaBehaviour(this, "onCopyTransferred", NotificationFrequency.EVERY_EVENT)); + + /** + * For every new child of a node with the trx:alien aspect run this.onCreateChildAssociation + */ + this.getPolicyComponent().bindAssociationBehaviour( + NodeServicePolicies.OnCreateChildAssociationPolicy.QNAME, + TransferModel.ASPECT_ALIEN, + new JavaBehaviour(this, "onCreateChildAssociation", NotificationFrequency.EVERY_EVENT)); + + /** + * For every node with the trx:alien aspect run this.beforeDeleteNode + */ + this.getPolicyComponent().bindClassBehaviour( + NodeServicePolicies.BeforeDeleteNodePolicy.QNAME, + TransferModel.ASPECT_ALIEN, + new JavaBehaviour(this, "beforeDeleteNode", NotificationFrequency.EVERY_EVENT)); + + /** + * For every restore of a node with the trx:alien aspect + */ + this.getPolicyComponent().bindClassBehaviour( + NodeServicePolicies.OnRestoreNodePolicy.QNAME, + TransferModel.ASPECT_ALIEN, + new JavaBehaviour(this, "onRestoreNode", NotificationFrequency.EVERY_EVENT)); + + /** + * For every move of a node with the trx:alien aspect. + */ + this.getPolicyComponent().bindClassBehaviour( + NodeServicePolicies.OnMoveNodePolicy.QNAME, + TransferModel.ASPECT_ALIEN, + new JavaBehaviour(this, "onMoveNode", NotificationFrequency.EVERY_EVENT)); + + /** + * For every copy of an alien node remove the alien aspect + */ + this.getPolicyComponent().bindClassBehaviour( + CopyServicePolicies.OnCopyNodePolicy.QNAME, + TransferModel.ASPECT_ALIEN, + new JavaBehaviour(this, "onCopyAlien", NotificationFrequency.EVERY_EVENT)); + } + + /* + * (non-Javadoc) + * + * @see + * org.alfresco.repo.web.scripts.transfer.TransferReceiver#getStagingFolder(org.alfresco.service.cmr.repository. + * NodeRef) + */ + public File getStagingFolder(String transferId) + { + if (transferId == null) + { + throw new IllegalArgumentException("transferId = " + transferId); + } + NodeRef transferNodeRef = new NodeRef(transferId); + File tempFolder; + String tempFolderPath = rootStagingDirectory + "/" + transferNodeRef.getId(); + tempFolder = new File(tempFolderPath); + if (!tempFolder.exists()) + { + if (!tempFolder.mkdirs()) + { + tempFolder = null; + throw new TransferException(MSG_FAILED_TO_CREATE_STAGING_FOLDER, new Object[] { transferId }); + } + } + return tempFolder; + + } + + public NodeRef getTempFolder(String transferId) + { + String tenantDomain = tenantService.getUserDomain(AuthenticationUtil.getRunAsUser()); + NodeRef transferTempFolder = transferTempFolderMap.get(tenantDomain); + + // Have we already resolved the node that is the temp folder? + // If not then do so. + if (transferTempFolder == null) + { + ResultSet rs = searchService.query(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, SearchService.LANGUAGE_XPATH, + transferTempFolderPath); + if (rs.length() > 0) + { + transferTempFolder = rs.getNodeRef(0); + transferTempFolderMap.put(tenantDomain, transferTempFolder); + } + else + { + throw new TransferException(MSG_TRANSFER_TEMP_FOLDER_NOT_FOUND, new Object[] { transferId, + transferTempFolderPath }); + } + } + + NodeRef transferNodeRef = new NodeRef(transferId); + String tempTransferFolderName = transferNodeRef.getId(); + NodeRef tempFolderNode = null; + QName folderName = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, tempTransferFolderName); + + // Do we already have a temp folder for this transfer? + List tempChildren = nodeService.getChildAssocs(transferTempFolder, + RegexQNamePattern.MATCH_ALL, folderName); + if (tempChildren.isEmpty()) + { + // No, we don't have a temp folder for this transfer yet. Create it... + Map props = new HashMap(); + props.put(ContentModel.PROP_NAME, tempTransferFolderName); + tempFolderNode = nodeService.createNode(transferTempFolder, ContentModel.ASSOC_CONTAINS, folderName, + TransferModel.TYPE_TEMP_TRANSFER_STORE, props).getChildRef(); + } + else + { + // Yes, we do have a temp folder for this transfer already. Return it. + tempFolderNode = tempChildren.get(0).getChildRef(); + } + return tempFolderNode; + + } + + /* + * (non-Javadoc) + * + * @see org.alfresco.repo.web.scripts.transfer.TransferReceiver#start() + */ + public String start(String fromRepositoryId, boolean transferToSelf, TransferVersion fromVersion) + { + log.debug("Start transfer"); + + /** + * Check that transfer is allowed to this repository + */ + checkTransfer(fromRepositoryId, transferToSelf); + + /** + * Check that the versions are compatible + */ + TransferVersion toVersion = getVersion(); + + if(!getTransferVersionChecker().checkTransferVersions(fromVersion, toVersion)) + { + throw new TransferException(MSG_INCOMPATIBLE_VERSIONS, new Object[] {"None", fromVersion, toVersion}); + } + + /** + * First get the transfer lock for this domain + */ + String tenantDomain = tenantService.getUserDomain(AuthenticationUtil.getRunAsUser()); + String lockStr = tenantDomain.isEmpty() ? "transfer.server.default" : "transfer.server.tenant." + tenantDomain; + QName lockQName = QName.createQName(TransferModel.TRANSFER_MODEL_1_0_URI, lockStr); + Lock lock = new Lock(lockQName); + + try + { + TransferServicePolicies.BeforeStartInboundTransferPolicy beforeStartPolicy = + beforeStartInboundTransferDelegate.get(TransferModel.TYPE_TRANSFER_RECORD); + beforeStartPolicy.beforeStartInboundTransfer(); + + lock.makeLock(); + + /** + * Transfer Lock held if we get this far + */ + String transferId = null; + + try + { + /** + * Now create a transfer record and use its NodeRef as the transfer id + */ + RetryingTransactionHelper txHelper = transactionService.getRetryingTransactionHelper(); + + transferId = txHelper.doInTransaction( + new RetryingTransactionHelper.RetryingTransactionCallback() + { + public String execute() throws Throwable + { + final NodeRef relatedTransferRecord = createTransferRecord(); + String transferId = relatedTransferRecord.toString(); + getTempFolder(transferId); + getStagingFolder(transferId); + + TransferServicePolicies.OnStartInboundTransferPolicy onStartPolicy = + onStartInboundTransferDelegate.get(TransferModel.TYPE_TRANSFER_RECORD); + onStartPolicy.onStartInboundTransfer(transferId); + + return transferId; + } + }, false, true); + } + catch (Exception e) + { + log.debug("Exception while staring transfer", e); + log.debug("releasing lock - we never created the transfer id"); + lock.releaseLock(); + throw new TransferException(MSG_ERROR_WHILE_STARTING, e); + } + + /** + * Here if we have begun a transfer and have a valid transfer id + */ + lock.transferId = transferId; + locks.put(transferId, lock); + log.info("transfer started:" + transferId); + lock.enableLockTimeout(); + return transferId; + + } + catch (LockAcquisitionException lae) + { + log.debug("transfer lock is already taken", lae); + // lock is already taken. + throw new TransferException(MSG_TRANSFER_LOCK_UNAVAILABLE); + } + } + + /** + * @return + */ + private NodeRef createTransferRecord() + { + log.debug("Receiver createTransferRecord"); + String tenantDomain = tenantService.getUserDomain(AuthenticationUtil.getRunAsUser()); + NodeRef inboundTransferRecordsFolder = inboundTransferRecordsFolderMap.get(tenantDomain); + + if (inboundTransferRecordsFolder == null) + { + log.debug("Trying to find transfer records folder: " + inboundTransferRecordsPath); + ResultSet rs = searchService.query(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, SearchService.LANGUAGE_XPATH, + inboundTransferRecordsPath); + if (rs.length() > 0) + { + inboundTransferRecordsFolder = rs.getNodeRef(0); + inboundTransferRecordsFolderMap.put(tenantDomain, inboundTransferRecordsFolder); + log.debug("Found inbound transfer records folder: " + inboundTransferRecordsFolder); + } + else + { + throw new TransferException(MSG_INBOUND_TRANSFER_FOLDER_NOT_FOUND, + new Object[] { inboundTransferRecordsPath }); + } + } + + SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmssSSSZ"); + String timeNow = format.format(new Date()); + String name = timeNow + ".xml"; + + QName recordName = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, name); + + Map props = new HashMap(); + props.put(ContentModel.PROP_NAME, name); + props.put(TransferModel.PROP_PROGRESS_POSITION, 0); + props.put(TransferModel.PROP_PROGRESS_ENDPOINT, 1); + props.put(TransferModel.PROP_TRANSFER_STATUS, TransferProgress.Status.PRE_COMMIT.toString()); + + log.debug("Creating transfer record with name: " + name); + ChildAssociationRef assoc = nodeService.createNode(inboundTransferRecordsFolder, ContentModel.ASSOC_CONTAINS, + recordName, TransferModel.TYPE_TRANSFER_RECORD, props); + log.debug("<-createTransferRecord: " + assoc.getChildRef()); + + return assoc.getChildRef(); + } + + /** + * Timeout a transfer. Called after the lock has been released via a timeout. + * + * This is the last chance to clean up. + * + * @param transferId + */ + private void timeout(final String transferId) + { + log.info("Inbound Transfer has timed out transferId:" + transferId); + /* + * There is no transaction or authentication context in this method since it is called via a + * timer thread. + */ + final RetryingTransactionCallback timeoutCB = new RetryingTransactionCallback() { + + + public Void execute() throws Throwable + { + TransferProgress progress = getProgressMonitor().getProgress(transferId); + + if (progress.getStatus().equals(TransferProgress.Status.PRE_COMMIT)) + { + log.warn("Inbound Transfer Lock Timeout - transferId:" + transferId); + /** + * Did not get out of PRE_COMMIT. The client has probably "gone away" after calling + * "start", but before calling commit, cancel or error. + */ + locks.remove(transferId); + removeTempFolders(transferId); + Object[] msgParams = { transferId }; + getProgressMonitor().logException(transferId, "transfer timeout", new TransferException(MSG_LOCK_TIMED_OUT, msgParams)); + getProgressMonitor().updateStatus(transferId, TransferProgress.Status.ERROR); + } + else + { + // We got beyond PRE_COMMIT, therefore leave the clean up to either + // commit, cancel or error command, since there may still be "in-flight" + // transfer in another thread. Although why, in that case, are we here? + log.warn("Inbound Transfer Lock Timeout - already past PRE-COMMIT - do no cleanup transferId:" + transferId); + } + return null; + } + }; + + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public String doWork() throws Exception + { + transactionService.getRetryingTransactionHelper().doInTransaction(timeoutCB, false, true); + return null; + } + }, AuthenticationUtil.getSystemUserName()); + } + + /* + * (non-Javadoc) + * + * @see org.alfresco.repo.web.scripts.transfer.TransferReceiver#end(org.alfresco.service.cmr.repository.NodeRef) + */ + public void end(final String transferId) + { + if (log.isDebugEnabled()) + { + log.debug("Request to end transfer " + transferId); + } + if (transferId == null) + { + throw new IllegalArgumentException("transferId = null"); + } + + try + { + Lock lock = locks.get(transferId); + if(lock != null) + { + log.debug("releasing lock:" + lock.lockToken); + lock.releaseLock(); + locks.remove(lock); + } + + removeTempFolders(transferId); + + + //Fire the OnEndInboundTransfer policy + Set createdNodes = Collections.emptySet(); + Set updatedNodes = Collections.emptySet(); + Set deletedNodes = Collections.emptySet(); + TransferChangesRecord changesRecord = progressMonitor.removeChangeRecord(transferId); + if (changesRecord != null) + { + createdNodes = new HashSet(changesRecord.getCreatedNodes()); + updatedNodes = new HashSet(changesRecord.getUpdatedNodes()); + deletedNodes = new HashSet(changesRecord.getDeletedNodes()); + } + TransferServicePolicies.OnEndInboundTransferPolicy onEndPolicy = + onEndInboundTransferDelegate.get(TransferModel.TYPE_TRANSFER_RECORD); + onEndPolicy.onEndInboundTransfer(transferId, createdNodes, updatedNodes, deletedNodes); + } + catch (TransferException ex) + { + throw ex; + } + catch (Exception ex) + { + throw new TransferException(MSG_ERROR_WHILE_ENDING_TRANSFER, new Object[] {transferId}, ex); + } + } + + private void removeTempFolders(final String transferId) + { + NodeRef tempStoreNode = null; + try + { + log.debug("Deleting temporary store node..."); + tempStoreNode = getTempFolder(transferId); + nodeService.deleteNode(tempStoreNode); + log.debug("Deleted temporary store node."); + } + catch (Exception ex) + { + log.warn("Failed to delete temp store node for transfer id " + transferId + + "\nTemp store noderef = " + tempStoreNode); + } + + File stagingFolder = null; + try + { + log.debug("delete staging folder " + transferId); + // Delete the staging folder. + stagingFolder = getStagingFolder(transferId); + deleteFile(stagingFolder); + log.debug("Staging folder deleted"); + } + catch(Exception ex) + { + log.warn("Failed to delete staging folder for transfer id " + transferId + + "\nStaging folder = " + stagingFolder.toString()); + } + } + + + public void cancel(String transferId) throws TransferException + { + // no need to check the lock + TransferProgress progress = getProgressMonitor().getProgress(transferId); + getProgressMonitor().updateStatus(transferId, TransferProgress.Status.CANCELLED); + if (progress.getStatus().equals(TransferProgress.Status.PRE_COMMIT)) + { + end(transferId); + } + } + + public void prepare(String transferId) throws TransferException + { + // Check that this transfer still owns the lock + Lock lock = checkLock(transferId); + try + { + + } + finally + { + lock.enableLockTimeout(); + } + + } + + /** + * @param stagingFolder + */ + private void deleteFile(File file) + { + if (file.isDirectory()) + { + File[] fileList = file.listFiles(); + if (fileList != null) + { + for (File currentFile : fileList) + { + deleteFile(currentFile); + } + } + } + file.delete(); + } + + public Lock checkLock(final String transferId) throws TransferException + { + if (transferId == null) + { + throw new IllegalArgumentException("nudgeLock: transferId = null"); + } + + Lock lock = locks.get(transferId); + if(lock != null) + { + if(lock.isActive()) + { + lock.suspendLockTimeout(); + return lock; + } + else + { + // lock is no longer active + log.debug("lock not active"); + throw new TransferException(MSG_LOCK_TIMED_OUT, new Object[]{transferId}); + + } + } + else + { + log.debug("lock not found"); + throw new TransferException(MSG_LOCK_NOT_FOUND, new Object[]{transferId}); + // lock not found + } + } + + public void saveSnapshot(String transferId, InputStream openStream) throws TransferException + { + // Check that this transfer still owns the lock + Lock lock = checkLock(transferId); + try + { + if (log.isDebugEnabled()) + { + log.debug("Saving snapshot for transferId =" + transferId); + } + + File snapshotFile = new File(getStagingFolder(transferId), SNAPSHOT_FILE_NAME); + try + { + if (snapshotFile.createNewFile()) + { + FileCopyUtils.copy(openStream, new BufferedOutputStream(new FileOutputStream(snapshotFile))); + } + if (log.isDebugEnabled()) + { + log.debug("Saved snapshot for transferId =" + transferId); + } + } + catch (Exception ex) + { + throw new TransferException(MSG_ERROR_WHILE_STAGING_SNAPSHOT, new Object[]{transferId}, ex); + } + } + finally + { + lock.enableLockTimeout(); + } + } + + public void saveContent(String transferId, String contentFileId, InputStream contentStream) + throws TransferException + { + Lock lock = checkLock(transferId); + try + { + File stagedFile = new File(getStagingFolder(transferId), contentFileId); + if (stagedFile.createNewFile()) + { + FileCopyUtils.copy(contentStream, new BufferedOutputStream(new FileOutputStream(stagedFile))); + } + } + catch (Exception ex) + { + throw new TransferException(MSG_ERROR_WHILE_STAGING_CONTENT, new Object[]{transferId, contentFileId}, ex); + } + finally + { + lock.enableLockTimeout(); + } + } + + public void commitAsync(String transferId) + { + /** + * A side-effect of checking the lock here is that the lock timeout is suspended. + * + */ + Lock lock = checkLock(transferId); + try + { + progressMonitor.updateStatus(transferId, Status.COMMIT_REQUESTED); + Action commitAction = actionService.createAction(TransferCommitActionExecuter.NAME); + commitAction.setParameterValue(TransferCommitActionExecuter.PARAM_TRANSFER_ID, transferId); + commitAction.setExecuteAsynchronously(true); + actionService.executeAction(commitAction, new NodeRef(transferId)); + if (log.isDebugEnabled()) + { + log.debug("Registered transfer commit for asynchronous execution: " + transferId); + } + } + catch (Exception error) + { + /** + * Error somewhere in the action service? + */ + //TODO consider whether the methods in this class should be retried/retryable.. + + // need to re-enable the lock timeout otherwise we will hold the lock forever... + lock.enableLockTimeout(); + + throw new TransferException(MSG_ERROR_WHILE_COMMITTING_TRANSFER, new Object[]{transferId}, error); + } + + /** + * Lock intentionally not re-enabled here + */ + } + + public void commit(final String transferId) throws TransferException + { + if (log.isDebugEnabled()) + { + log.debug("Committing transferId=" + transferId); + } + + /** + * A side-effect of checking the lock here is that it ensures that the lock timeout is suspended. + */ + checkLock(transferId); + + /** + * Turn off rules while transfer is being committed. + */ + boolean rulesEnabled = ruleService.isEnabled(); + ruleService.disableRules(); + + try + { + /* lock is going to be released */ checkLock(transferId); + progressMonitor.updateStatus(transferId, Status.COMMITTING); + + RetryingTransactionHelper.RetryingTransactionCallback commitWork = new RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + AlfrescoTransactionSupport.bindListener(new TransferCommitTransactionListener(transferId, + RepoTransferReceiverImpl.this)); + + List commitProcessors = manifestProcessorFactory.getCommitProcessors( + RepoTransferReceiverImpl.this, transferId); + + SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); + SAXParser parser = saxParserFactory.newSAXParser(); + File snapshotFile = getSnapshotFile(transferId); + + if (snapshotFile.exists()) + { + if (log.isDebugEnabled()) + { + log.debug("Processing manifest file:" + snapshotFile.getAbsolutePath()); + } + // We parse the file as many times as we have processors + for (TransferManifestProcessor processor : commitProcessors) + { + XMLTransferManifestReader reader = new XMLTransferManifestReader(processor); + + //behaviourFilter.disableBehaviour(ContentModel.ASPECT_AUDITABLE); + behaviourFilter.disableAllBehaviours(); + + try + { + parser.parse(snapshotFile, reader); + } + finally + { + // behaviourFilter.enableBehaviour(ContentModel.ASPECT_AUDITABLE); + behaviourFilter.enableAllBehaviours(); + } + parser.reset(); + } + } + else + { + progressMonitor.logException(transferId, "Unable to start commit. No snapshot file received", + new TransferException(MSG_NO_SNAPSHOT_RECEIVED, new Object[]{transferId})); + } + return null; + } + }; + + transactionService.getRetryingTransactionHelper().doInTransaction(commitWork, false, true); + + Throwable error = progressMonitor.getProgress(transferId).getError(); + if (error != null) + { + if (TransferException.class.isAssignableFrom(error.getClass())) + { + throw (TransferException) error; + } + else + { + throw new TransferException(MSG_ERROR_WHILE_COMMITTING_TRANSFER, new Object[]{transferId}, error); + } + } + + /** + * Successfully committed + */ + if (log.isDebugEnabled()) + { + log.debug("Commit success transferId=" + transferId); + } + } + catch (Exception ex) + { + if (TransferException.class.isAssignableFrom(ex.getClass())) + { + throw (TransferException) ex; + } + else + { + throw new TransferException(MSG_ERROR_WHILE_COMMITTING_TRANSFER, ex); + } + } + finally + { + if(rulesEnabled) + { + /** + * Turn rules back on if we turned them off earlier. + */ + ruleService.enableRules(); + } + + /** + * Clean up at the end of the transfer + */ + try + { + end(transferId); + } + catch (Exception ex) + { + log.error("Failed to clean up transfer. Lock may still be in place: " + transferId, ex); + } + } + } + + public TransferProgress getStatus(String transferId) throws TransferException + { + return getProgressMonitor().getProgress(transferId); + } + + private File getSnapshotFile(String transferId) + { + return new File(getStagingFolder(transferId), SNAPSHOT_FILE_NAME); + } + + /** + * @param searchService + * the searchService to set + */ + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + /** + * @param transactionService + * the transactionService to set + */ + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + public void setTenantService(TenantService tenantService) + { + this.tenantService = tenantService; + } + + /** + * @param transferLockFolderPath + * the transferLockFolderPath to set + */ + public void setTransferLockFolderPath(String transferLockFolderPath) + { + this.transferLockFolderPath = transferLockFolderPath; + } + + /** + * @param transferTempFolderPath + * the transferTempFolderPath to set + */ + public void setTransferTempFolderPath(String transferTempFolderPath) + { + this.transferTempFolderPath = transferTempFolderPath; + } + + /** + * @param rootStagingDirectory + * the rootTransferFolder to set + */ + public void setRootStagingDirectory(String rootStagingDirectory) + { + this.rootStagingDirectory = rootStagingDirectory; + } + + /** + * @param inboundTransferRecordsPath + * the inboundTransferRecordsPath to set + */ + public void setInboundTransferRecordsPath(String inboundTransferRecordsPath) + { + this.inboundTransferRecordsPath = inboundTransferRecordsPath; + } + + /** + * @param nodeService + * the nodeService to set + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * @param manifestProcessorFactory + * the manifestProcessorFactory to set + */ + public void setManifestProcessorFactory(ManifestProcessorFactory manifestProcessorFactory) + { + this.manifestProcessorFactory = manifestProcessorFactory; + } + + /** + * @param behaviourFilter + * the behaviourFilter to set + */ + public void setBehaviourFilter(BehaviourFilter behaviourFilter) + { + this.behaviourFilter = behaviourFilter; + } + + /** + * @return the progressMonitor + */ + public TransferProgressMonitor getProgressMonitor() + { + return progressMonitor; + } + + /** + * @param progressMonitor + * the progressMonitor to set + */ + public void setProgressMonitor(TransferProgressMonitor progressMonitor) + { + this.progressMonitor = new ChangeCapturingProgressMonitor(progressMonitor); + } + + public void setActionService(ActionService actionService) + { + this.actionService = actionService; + } + + /** + * Set the ruleService + * @param ruleService + * the ruleService to set + */ + public void setRuleService(RuleService ruleService) + { + this.ruleService = ruleService; + } + + /** + * Get the rule service + * @return the rule service + */ + public RuleService getRuleService() + { + return this.ruleService; + } + + /** + * Generate the requsite + */ + public void generateRequsite(String transferId, OutputStream out) throws TransferException + { + log.debug("Generate Requsite for transfer:" + transferId); + try + { + File snapshotFile = getSnapshotFile(transferId); + + if (snapshotFile.exists()) + { + log.debug("snapshot does exist"); + SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); + SAXParser parser = saxParserFactory.newSAXParser(); + OutputStreamWriter dest = new OutputStreamWriter(out, "UTF-8"); + + XMLTransferRequsiteWriter writer = new XMLTransferRequsiteWriter(dest); + TransferManifestProcessor processor = manifestProcessorFactory.getRequsiteProcessor( + RepoTransferReceiverImpl.this, + transferId, + writer); + + XMLTransferManifestReader reader = new XMLTransferManifestReader(processor); + + /** + * Now run the parser + */ + parser.parse(snapshotFile, reader); + + /** + * And flush the destination in case any content remains in the writer. + */ + dest.flush(); + + } + log.debug("Generate Requsite done transfer:" + transferId); + + } + catch (Exception ex) + { + if (TransferException.class.isAssignableFrom(ex.getClass())) + { + throw (TransferException) ex; + } + else + { + throw new TransferException(MSG_ERROR_WHILE_GENERATING_REQUISITE, ex); + } + } + } + + public InputStream getTransferReport(String transferId) + { + return progressMonitor.getLogInputStream(transferId); + } + + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + public PolicyComponent getPolicyComponent() + { + return policyComponent; + } + + /** + * When a new node is created as a child of a Transferred or Alien node then + * the new node needs to be marked as an alien. + *

+ * Then the tree needs to be walked upwards to mark all parent + * transferred nodes as alien. + */ + public void onCreateChildAssociation(ChildAssociationRef childAssocRef, + boolean isNewNode) + { + + log.debug("on create child association to transferred node"); + + final String localRepositoryId = descriptorService.getCurrentRepositoryDescriptor().getId(); + alienProcessor.onCreateChild(childAssocRef, localRepositoryId, isNewNode); + } + + /** + * When an alien node is deleted the it may be the last alien invader + *

+ * Walk the tree checking the invasion status! + */ + public void beforeDeleteNode(NodeRef deletedNodeRef) + { + log.debug("on delete node - need to check for transferred node"); + alienProcessor.beforeDeleteAlien(deletedNodeRef, null); + } + + /** + * When a transferred node is restored it may be a new invader or it may no + * longer be an invader. + *

+ * Walk the tree checking the invasion status! + */ + public void onRestoreNode(ChildAssociationRef childAssocRef) + { + log.debug("on restore node"); + log.debug("restoredAssocRef:" + childAssocRef); + alienProcessor.afterMoveAlien(childAssocRef); + } + + /** + * When an alien node is moved it may un-invade its old location and invade a new + * location. The node may also cease to be alien. + */ + public void onMoveNode(ChildAssociationRef oldChildAssocRef, + ChildAssociationRef newChildAssocRef) + { + + log.debug("onMoveNode"); + log.debug("oldChildAssocRef:" + oldChildAssocRef); + log.debug("newChildAssocRef:" + newChildAssocRef); + + NodeRef oldParentRef = oldChildAssocRef.getParentRef(); + NodeRef newParentRef = newChildAssocRef.getParentRef(); + + if(newParentRef.equals(oldParentRef)) + { + log.debug("old parent and new parent are the same - this is a rename, do nothing"); + } + else + { + if(log.isDebugEnabled()) + { + log.debug("moving node from oldParentRef:" + oldParentRef +" to:" + newParentRef); + } + alienProcessor.beforeDeleteAlien(newChildAssocRef.getChildRef(), oldChildAssocRef); + alienProcessor.afterMoveAlien(newChildAssocRef); + } + } + + /** + * When a transferred node is copied, don't copy the transferred aspect. + */ + public CopyBehaviourCallback onCopyTransferred(QName classRef, + CopyDetails copyDetails) + { + return TransferredAspectCopyBehaviourCallback.INSTANCE; + } + + /** + * When an alien node is copied, don't copy the alien aspect. + */ + public CopyBehaviourCallback onCopyAlien(QName classRef, + CopyDetails copyDetails) + { + return AlienAspectCopyBehaviourCallback.INSTANCE; + } + + /** + * Extends the default copy behaviour to prevent copying of transferred aspect and properties. + * + * @author Mark Rogers + * @since 3.4 + */ + private static class TransferredAspectCopyBehaviourCallback extends DefaultCopyBehaviourCallback + { + private static final CopyBehaviourCallback INSTANCE = new TransferredAspectCopyBehaviourCallback(); + + /** + * @return Returns an empty map + */ + @Override + public Map getCopyProperties( + QName classQName, CopyDetails copyDetails, Map properties) + { + return Collections.emptyMap(); + } + + /** + * Don't copy the transferred aspect. + * + * @return Returns true always + */ + @Override + public boolean getMustCopy(QName classQName, CopyDetails copyDetails) + { + if(classQName.equals(TransferModel.ASPECT_TRANSFERRED)) + { + return false; + } + else + { + return true; + } + } + } + + /** + * Extends the default copy behaviour to prevent copying of alien aspect and properties. + * + * @author Mark Rogers + * @since 3.4 + */ + private static class AlienAspectCopyBehaviourCallback extends DefaultCopyBehaviourCallback + { + private static final CopyBehaviourCallback INSTANCE = new AlienAspectCopyBehaviourCallback(); + + /** + * @return Returns an empty map + */ + @Override + public Map getCopyProperties( + QName classQName, CopyDetails copyDetails, Map properties) + { + return Collections.emptyMap(); + } + + /** + * Don't copy the transferred aspect. + * + * @return Returns true always + */ + @Override + public boolean getMustCopy(QName classQName, CopyDetails copyDetails) + { + if(classQName.equals(TransferModel.ASPECT_ALIEN)) + { + return false; + } + else + { + return true; + } + } + } + + + public void setDescriptorService(DescriptorService descriptorService) + { + this.descriptorService = descriptorService; + } + + public DescriptorService getDescriptorService() + { + return descriptorService; + } + + public void setAlienProcessor(AlienProcessor alienProcessor) + { + this.alienProcessor = alienProcessor; + } + + public AlienProcessor getAlienProcessor() + { + return alienProcessor; + } + + @Override + public void onContentUpdate(NodeRef nodeRef, boolean newContent) + { + /** + * On update of a transferred node remove the from content from property. + */ + log.debug("on content update called:" + nodeRef); + if(newContent) + { + log.debug("new content remove PROP_FROM_CONTENT from node:" + nodeRef); + nodeService.setProperty(nodeRef, TransferModel.PROP_FROM_CONTENT, null); + } + } + + public void setJobLockService(JobLockService jobLockService) + { + this.jobLockService = jobLockService; + } + + public JobLockService getJobLockService() + { + return jobLockService; + } + + public void setLockRetryCount(int lockRetryCount) + { + this.lockRetryCount = lockRetryCount; + } + + public int getLockRetryCount() + { + return lockRetryCount; + } + + public void setLockRetryWait(long lockRetryWait) + { + this.lockRetryWait = lockRetryWait; + } + + public long getLockRetryWait() + { + return lockRetryWait; + } + + public void setLockTimeOut(long lockTimeOut) + { + this.lockTimeOut = lockTimeOut; + } + + public long getLockTimeOut() + { + return lockTimeOut; + } + + public void setLockRefreshTime(long lockRefreshTime) + { + this.lockRefreshTime = lockRefreshTime; + } + + public long getLockRefreshTime() + { + return lockRefreshTime; + } + + /** + * A Transfer Lock + */ + private class Lock implements JobLockService.JobLockRefreshCallback + { + /** + * The name of the lock - unique for each domain + */ + QName lockQName; + + /** + * The unique token for this lock instance. + */ + String lockToken; + + /** + * The transfer that this lock belongs to. + */ + String transferId; + + /** + * Is the lock active ? + */ + private boolean active = false; + + /** + * Is the server processing ? + */ + private boolean processing = false; + + /** + * When did we last check whether the lock is active + */ + Date lastActive = new Date(); + + public Lock(QName lockQName) + { + this.lockQName = lockQName; + } + + + /** + * Make the lock - called on main thread + * + * @throws LockAquisitionException + */ + public void makeLock() + { + if(log.isDebugEnabled()) + { + log.debug("makeLock" + lockQName); + } + + lockToken = getJobLockService().getLock(lockQName, getLockRefreshTime(), getLockRetryWait(), getLockRetryCount()); + + synchronized(this) + { + active = true; + } + + if (log.isDebugEnabled()) + { + log.debug("lock taken: name" + lockQName + " token:" +lockToken); + log.debug("register lock callback, target lock refresh time :" + getLockRefreshTime()); + } + getJobLockService().refreshLock(lockToken, lockQName, getLockRefreshTime(), this); + if (log.isDebugEnabled()) + { + log.debug("refreshLock callback registered"); + } + } + + /** + * Check that the lock is still active + * + * Called on main transfer thread as transfer proceeds. + * @throws TransferException (Lock timeout) + */ + public void suspendLockTimeout() + { + log.debug("suspend lock called"); + if(active) + { + processing = true; + } + else + { + // lock is no longer active + log.debug("lock not active, throw timed out exception"); + throw new TransferException(MSG_LOCK_TIMED_OUT); + } + } + + public void enableLockTimeout() + { + Date now = new Date(); + + // Update lastActive to 1S boundary + if(now.getTime() > lastActive.getTime() + 1000) + { + lastActive = new Date(); + log.debug("start waiting : lastActive:" + lastActive); + } + + processing = false; + } + + /** + * Release the lock + * + * Called on main thread + */ + public void releaseLock() + { + if(log.isDebugEnabled()) + { + log.debug("transfer service about to releaseLock : " + lockQName); + } + + synchronized(this) + { + if(active) + { + getJobLockService().releaseLock(lockToken, lockQName); + } + active = false; + } + } + + /** + * Called by Job Lock Service to determine whether the lock is still active + */ + @Override + public boolean isActive() + { + Date now = new Date(); + + synchronized(this) + { + if(active) + { + if(!processing) + { + if(now.getTime() > lastActive.getTime() + getLockTimeOut()) + { + return false; + } + } + } + + if(log.isDebugEnabled()) + { + log.debug("transfer service callback isActive: " + active); + } + + return active; + } + } + + /** + * Called by Job Lock Service on release of the lock after time-out + */ + @Override + public void lockReleased() + { + synchronized(this) + { + if(active) + { + log.info("transfer service: lock has timed out, timeout :" + lockQName); + timeout(transferId); + } + + active = false; + } + } + } + + /** + * Check Whether transfer is allowed from the specified repository. + * Called prior to "begin". + */ + + private void checkTransfer(String fromRepository, boolean transferToSelf) + { + if(log.isDebugEnabled()) + { + log.debug("checkTransfer fromRepository:" + fromRepository + ", transferToSelf:" + transferToSelf ); + } + final String localRepositoryId = descriptorService.getCurrentRepositoryDescriptor().getId(); + + if(!transferToSelf) + { + if(fromRepository != null) + { + if(fromRepository.equalsIgnoreCase(localRepositoryId)) + { + throw new TransferException(MSG_TRANSFER_TO_SELF); + } + } + else + { + throw new TransferException("from repository id is missing"); + } + } + } + + public void setTransferVersionChecker(TransferVersionChecker transferVersionChecker) + { + this.transferVersionChecker = transferVersionChecker; + } + + public TransferVersionChecker getTransferVersionChecker() + { + return transferVersionChecker; + } + + @Override + public TransferVersion getVersion() + { + Descriptor d = descriptorService.getServerDescriptor(); + // needs to be serverDescriptor to pick up versionEdition + return new TransferVersionImpl(d); + } + + public void setFileTransferRootNodeFileFileSystem(String rootFileSystem) + { + //just ignore, no relevant for transferring on file system + } +} diff --git a/source/java/org/alfresco/repo/transfer/TransferServiceCallbackTest.java b/source/java/org/alfresco/repo/transfer/TransferServiceCallbackTest.java index 436f0303eb..bc95723769 100644 --- a/source/java/org/alfresco/repo/transfer/TransferServiceCallbackTest.java +++ b/source/java/org/alfresco/repo/transfer/TransferServiceCallbackTest.java @@ -115,6 +115,7 @@ public class TransferServiceCallbackTest extends TestCase applicationContext = ApplicationContextHelper.getApplicationContext(); // Get the required services + descriptorService = (DescriptorService) this.applicationContext.getBean("DescriptorService"); transferServiceImpl = (TransferServiceImpl2) this.applicationContext.getBean("transferService2"); transferService = transferServiceImpl; authenticationComponent = (AuthenticationComponent) applicationContext.getBean("authenticationComponent"); @@ -122,8 +123,8 @@ public class TransferServiceCallbackTest extends TestCase repository = (Repository) applicationContext.getBean("repositoryHelper"); fileFolderService = (FileFolderService) applicationContext.getBean("fileFolderService"); - localRepositoryId = transferServiceImpl.getDescriptorService().getCurrentRepositoryDescriptor().getId(); - version = new TransferVersionImpl(transferServiceImpl.getDescriptorService().getServerDescriptor()); + localRepositoryId = descriptorService.getCurrentRepositoryDescriptor().getId(); + version = new TransferVersionImpl(descriptorService.getServerDescriptor()); mockedTransferTransmitter = mock(TransferTransmitter.class); mockedCallback = mock(TransferCallback.class); diff --git a/source/java/org/alfresco/repo/transfer/TransferServiceImpl2.java b/source/java/org/alfresco/repo/transfer/TransferServiceImpl2.java index d87f653581..4be51bb9f2 100644 --- a/source/java/org/alfresco/repo/transfer/TransferServiceImpl2.java +++ b/source/java/org/alfresco/repo/transfer/TransferServiceImpl2.java @@ -22,7 +22,6 @@ import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; @@ -52,7 +51,7 @@ import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.model.ContentModel; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.tenant.TenantService; -import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.repo.transfer.manifest.TransferManifestDeletedNode; import org.alfresco.repo.transfer.manifest.TransferManifestHeader; import org.alfresco.repo.transfer.manifest.TransferManifestNode; @@ -73,7 +72,6 @@ import org.alfresco.service.cmr.repository.ContentData; 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.search.ResultSet; import org.alfresco.service.cmr.search.SearchService; import org.alfresco.service.cmr.transfer.TransferCallback; import org.alfresco.service.cmr.transfer.TransferCancelledException; @@ -144,7 +142,7 @@ public class TransferServiceImpl2 implements TransferService2 public void init() { PropertyCheck.mandatory(this, "nodeService", nodeService); - PropertyCheck.mandatory(this, "searchService", getSearchService()); + PropertyCheck.mandatory(this, "searchService", searchService); PropertyCheck.mandatory(this, "transferSpaceQuery", transferSpaceQuery); PropertyCheck.mandatory(this, "defaultTransferGroup", defaultTransferGroup); PropertyCheck.mandatory(this, "transmitter", transmitter); @@ -152,7 +150,7 @@ public class TransferServiceImpl2 implements TransferService2 PropertyCheck.mandatory(this, "actionService", actionService); PropertyCheck.mandatory(this, "transactionService", transactionService); PropertyCheck.mandatory(this, "descriptorService", descriptorService); - PropertyCheck.mandatory(this, "transferVersionChecker", getTransferVersionChecker()); + PropertyCheck.mandatory(this, "transferVersionChecker", transferVersionChecker); } private String transferSpaceQuery; @@ -465,7 +463,7 @@ public class TransferServiceImpl2 implements TransferService2 params.put("definition", definition); params.put("callbacks", (Serializable)callbacks); - Action transferAction = getActionService().createAction("transfer-async", params); + Action transferAction = actionService.createAction("transfer-async", params); /** * Execute transfer async in its own transaction. @@ -478,7 +476,7 @@ public class TransferServiceImpl2 implements TransferService2 { trx.begin(); logger.debug("calling action service to execute action"); - getActionService().executeAction(transferAction, null, false, true); + actionService.executeAction(transferAction, null, false, true); trx.commit(); logger.debug("committed successfully"); success = true; @@ -617,7 +615,7 @@ public class TransferServiceImpl2 implements TransferService2 { // check alfresco versions are compatible TransferVersion toVersion = transfer.getToVersion(); - if(!getTransferVersionChecker().checkTransferVersions(fromVersion, toVersion)) + if(!this.transferVersionChecker.checkTransferVersions(fromVersion, toVersion)) { throw new TransferException(MSG_INCOMPATIBLE_VERSIONS, new Object[] {transfer.getTransferId(), fromVersion, toVersion}); } @@ -1135,21 +1133,11 @@ public class TransferServiceImpl2 implements TransferService2 this.nodeService = nodeService; } - public NodeService getNodeService() - { - return nodeService; - } - public void setSearchService(SearchService searchService) { this.searchService = searchService; } - public SearchService getSearchService() - { - return searchService; - } - public void setTenantService(TenantService tenantService) { this.tenantService = tenantService; @@ -1160,21 +1148,11 @@ public class TransferServiceImpl2 implements TransferService2 this.transferSpaceQuery = transferSpaceQuery; } - public String getTransferSpaceQuery() - { - return transferSpaceQuery; - } - public void setDefaultTransferGroup(String defaultGroup) { this.defaultTransferGroup = defaultGroup; } - public String getDefaultTransferGroup() - { - return defaultTransferGroup; - } - public TransferTransmitter getTransmitter() { return transmitter; @@ -1331,29 +1309,27 @@ public class TransferServiceImpl2 implements TransferService2 private NodeRef persistTransferReport(final String transferName, final Transfer transfer, final TransferTarget target, final TransferDefinition definition, final List events, final File snapshotFile, final Exception exception) { - /** - * persist the transfer report in its own transaction so it cannot be rolled back - */ - NodeRef reportNode = transactionService.getRetryingTransactionHelper().doInTransaction( - new RetryingTransactionHelper.RetryingTransactionCallback() - { - public NodeRef execute() throws Throwable - { - logger.debug("transfer report starting"); - NodeRef reportNode = null; - if (exception != null) - { - reportNode = transferReporter.createTransferReport(transferName, exception, target, definition, events, snapshotFile); - - } - else - { - reportNode = transferReporter.createTransferReport(transferName, transfer, target, definition, events, snapshotFile); - } - logger.debug("transfer report done"); - return reportNode; - } - }, false, true); + // persist the transfer report in its own transaction so it cannot be rolled back + RetryingTransactionCallback writeReportCallback = new RetryingTransactionCallback() + { + @Override + public NodeRef execute() throws Throwable + { + logger.debug("transfer report starting"); + NodeRef reportNode = null; + if (exception != null) + { + reportNode = transferReporter.createTransferReport(transferName, exception, target, definition, events, snapshotFile); + } + else + { + reportNode = transferReporter.createTransferReport(transferName, transfer, target, definition, events, snapshotFile); + } + logger.debug("transfer report done"); + return reportNode; + } + }; + NodeRef reportNode = transactionService.getRetryingTransactionHelper().doInTransaction(writeReportCallback, false, true); return reportNode; } @@ -1365,49 +1341,41 @@ public class TransferServiceImpl2 implements TransferService2 final Transfer transfer, final TransferTarget target) { - /** - * in its own transaction so it cannot be rolled back - */ - NodeRef reportNode = transactionService.getRetryingTransactionHelper().doInTransaction( - new RetryingTransactionHelper.RetryingTransactionCallback() + // in its own transaction so it cannot be rolled back + RetryingTransactionCallback writeReportCallback = new RetryingTransactionCallback() + { + @Override + public NodeRef execute() throws Throwable { - public NodeRef execute() throws Throwable + File tempDir = TempFileProvider.getLongLifeTempDir(FILE_DIRECTORY); + File destReportFile = TempFileProvider.createTempFile("TRX-DREP", FILE_SUFFIX, tempDir); + FileOutputStream destReportOutput = new FileOutputStream(destReportFile); + transmitter.getTransferReport(transfer, destReportOutput); + logger.debug("transfer report (destination) starting"); + + NodeRef reportNode = transferReporter.writeDestinationReport(transferName, target, destReportFile); + logger.debug("transfer report (destination) done"); + + if(destReportFile != null) { - try - { - File tempDir = TempFileProvider.getLongLifeTempDir(FILE_DIRECTORY); - File destReportFile = TempFileProvider.createTempFile("TRX-DREP", FILE_SUFFIX, tempDir); - FileOutputStream destReportOutput = new FileOutputStream(destReportFile); - transmitter.getTransferReport(transfer, destReportOutput); - logger.debug("transfer report (destination) starting"); + destReportFile.delete(); + } + logger.debug("destination report temp file deleted"); - NodeRef reportNode = transferReporter.writeDestinationReport(transferName, target, destReportFile); - logger.debug("transfer report (destination) done"); - - if(destReportFile != null) - { - destReportFile.delete(); - } - logger.debug("destination report temp file deleted"); - - return reportNode; - } - catch(FileNotFoundException ie) - { - // there's nothing we can do here. - but we do not want the exception to propogate up. - logger.debug("unexpected error while obtaining destination transfer report", ie); - return null; - } - catch(TransferException ie) - { - // there's nothing we can do here. - but we do not want the exception to propogate up. - logger.debug("unexpected error while obtaining destination transfer report", ie); - return null; - } - } // end execute - }, false, true); - - return reportNode; + return reportNode; + } + }; + try + { + NodeRef reportNode = transactionService.getRetryingTransactionHelper().doInTransaction(writeReportCallback, false, true); + return reportNode; + } + catch (Throwable e) + { + // there's nothing we can do here. - but we do not want the exception to propogate up. + logger.debug("unexpected error while obtaining destination transfer report", e); + return null; + } } public void setTransferManifestNodeFactory(TransferManifestNodeFactory transferManifestNodeFactory) @@ -1415,71 +1383,36 @@ public class TransferServiceImpl2 implements TransferService2 this.transferManifestNodeFactory = transferManifestNodeFactory; } - public TransferManifestNodeFactory getTransferManifestNodeFactory() - { - return transferManifestNodeFactory; - } - public void setActionService(ActionService actionService) { this.actionService = actionService; } - public ActionService getActionService() - { - return actionService; - } - public void setTransactionService(TransactionService transactionService) { this.transactionService = transactionService; } - public TransactionService getTransactionService() - { - return transactionService; - } - public void setTransferReporter(TransferReporter transferReporter) { this.transferReporter = transferReporter; } - public TransferReporter getTransferReporter() - { - return transferReporter; - } - public void setCommitPollDelay(long commitPollDelay) { this.commitPollDelay = commitPollDelay; } - public long getCommitPollDelay() - { - return commitPollDelay; - } - public void setDescriptorService(DescriptorService descriptorService) { this.descriptorService = descriptorService; } - public DescriptorService getDescriptorService() - { - return descriptorService; - } - public void setTransferVersionChecker(TransferVersionChecker transferVersionChecker) { this.transferVersionChecker = transferVersionChecker; } - public TransferVersionChecker getTransferVersionChecker() - { - return transferVersionChecker; - } - public void setNamespaceService(NamespaceService namespaceService) { this.namespaceService = namespaceService; diff --git a/source/java/org/alfresco/repo/transfer/TransferServiceImplTest.java b/source/java/org/alfresco/repo/transfer/TransferServiceImplTest.java index 463d500982..8cee169925 100644 --- a/source/java/org/alfresco/repo/transfer/TransferServiceImplTest.java +++ b/source/java/org/alfresco/repo/transfer/TransferServiceImplTest.java @@ -50,7 +50,9 @@ import org.alfresco.model.ContentModel; import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.security.authentication.AuthenticationComponent; import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.repo.transfer.manifest.TransferManifestNodeFactory; import org.alfresco.service.cmr.action.ActionService; @@ -169,6 +171,13 @@ public class TransferServiceImplTest extends BaseAlfrescoSpringTest super.runBare(); } + public void testSetup() + { + assertEquals( + "Must run without transactions", + TxnReadState.TXN_NONE, AlfrescoTransactionSupport.getTransactionReadState()); + } + /** * Test create target. *