diff --git a/config/alfresco/node-services-context.xml b/config/alfresco/node-services-context.xml
index 96a8be786b..d1c98514ea 100644
--- a/config/alfresco/node-services-context.xml
+++ b/config/alfresco/node-services-context.xml
@@ -28,6 +28,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/source/java/org/alfresco/repo/node/archive/ArchiveAndRestoreTest.java b/source/java/org/alfresco/repo/node/archive/ArchiveAndRestoreTest.java
index 7565c401b0..806f36adc6 100644
--- a/source/java/org/alfresco/repo/node/archive/ArchiveAndRestoreTest.java
+++ b/source/java/org/alfresco/repo/node/archive/ArchiveAndRestoreTest.java
@@ -28,6 +28,7 @@ import junit.framework.TestCase;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.node.StoreArchiveMap;
+import org.alfresco.repo.node.archive.RestoreNodeReport.RestoreStatus;
import org.alfresco.repo.security.authentication.AuthenticationComponent;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.repository.AssociationRef;
@@ -62,6 +63,7 @@ public class ArchiveAndRestoreTest extends TestCase
private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext();
+ private NodeArchiveService nodeArchiveService;
private NodeService nodeService;
private PermissionService permissionService;
private AuthenticationComponent authenticationComponent;
@@ -95,6 +97,7 @@ public class ArchiveAndRestoreTest extends TestCase
public void setUp() throws Exception
{
ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean("ServiceRegistry");
+ nodeArchiveService = (NodeArchiveService) ctx.getBean("nodeArchiveService");
nodeService = serviceRegistry.getNodeService();
permissionService = serviceRegistry.getPermissionService();
authenticationService = serviceRegistry.getAuthenticationService();
@@ -142,10 +145,7 @@ public class ArchiveAndRestoreTest extends TestCase
{
try
{
- if (txn.getStatus() == Status.STATUS_ACTIVE)
- {
- txn.rollback();
- }
+ txn.rollback();
}
catch (Throwable e)
{
@@ -461,4 +461,84 @@ public class ArchiveAndRestoreTest extends TestCase
System.out.println("Average delete time: " + averageDeleteTimeMs + " ms");
System.out.println("Average create time: " + averageCreateTimeMs + " ms");
}
+
+ public void testInTransactionRestore() throws Exception
+ {
+ RestoreNodeReport report = nodeArchiveService.restoreArchivedNode(a);
+ // expect a failure due to missing archive node
+ assertEquals("Expected failure", RestoreStatus.FAILURE_INVALID_ARCHIVE_NODE, report.getStatus());
+ // check that our transaction was not affected
+ assertEquals("Transaction should still be valid", Status.STATUS_ACTIVE, txn.getStatus());
+ }
+
+ public void testInTransactionPurge() throws Exception
+ {
+ nodeArchiveService.purgeArchivedNode(a);
+ // the node should still be there (it was not available to the purge transaction)
+ assertTrue("Node should not have been touched", nodeService.exists(a));
+ // check that our transaction was not affected
+ assertEquals("Transaction should still be valid", Status.STATUS_ACTIVE, txn.getStatus());
+ }
+
+ private void commitAndBeginNewTransaction() throws Exception
+ {
+ txn.commit();
+ txn = transactionService.getUserTransaction();
+ txn.begin();
+ }
+
+ public void testMassRestore() throws Exception
+ {
+ nodeService.deleteNode(a);
+ nodeService.deleteNode(b);
+ commitAndBeginNewTransaction();
+
+ List reports = nodeArchiveService.restoreAllArchivedNodes(workStoreRef);
+ // check that both a and b were restored
+ assertEquals("Incorrect number of node reports", 2, reports.size());
+ commitAndBeginNewTransaction();
+ // all nodes must be restored, but some of the inter a-b assocs might not be
+ verifyNodeExistence(a, true);
+ verifyNodeExistence(b, true);
+ verifyNodeExistence(aa, true);
+ verifyNodeExistence(bb, true);
+ verifyNodeExistence(a_, false);
+ verifyNodeExistence(b_, false);
+ verifyNodeExistence(aa_, false);
+ verifyNodeExistence(bb_, false);
+ }
+
+ public void testMassPurge() throws Exception
+ {
+ nodeService.deleteNode(a);
+ nodeService.deleteNode(b);
+ commitAndBeginNewTransaction();
+
+ nodeArchiveService.purgeAllArchivedNodes(workStoreRef);
+
+ commitAndBeginNewTransaction();
+ // all nodes must be gone
+ verifyNodeExistence(a, false);
+ verifyNodeExistence(b, false);
+ verifyNodeExistence(aa, false);
+ verifyNodeExistence(bb, false);
+ verifyNodeExistence(a_, false);
+ verifyNodeExistence(b_, false);
+ verifyNodeExistence(aa_, false);
+ verifyNodeExistence(bb_, false);
+ }
+//
+// public void testPermissionsForRestore() throws Exception
+// {
+// // user A deletes 'a'
+// authenticationService.authenticate(USER_A, USER_A.toCharArray());
+// nodeService.deleteNode(a);
+// // user B deletes 'b'
+// authenticationService.authenticate(USER_B, USER_B.toCharArray());
+// nodeService.deleteNode(b);
+//
+// // user B can't see archived 'a'
+// List restoredByB = nodeArchiveService.restoreAllArchivedNodes(workStoreRef);
+// assertEquals("User B should not have seen A's delete", 1, restoredByB.size());
+// }
}
diff --git a/source/java/org/alfresco/repo/node/archive/NodeArchiveServiceImpl.java b/source/java/org/alfresco/repo/node/archive/NodeArchiveServiceImpl.java
index b35c9fd677..258fa1d04d 100644
--- a/source/java/org/alfresco/repo/node/archive/NodeArchiveServiceImpl.java
+++ b/source/java/org/alfresco/repo/node/archive/NodeArchiveServiceImpl.java
@@ -19,11 +19,23 @@ package org.alfresco.repo.node.archive;
import java.util.ArrayList;
import java.util.List;
+import org.alfresco.model.ContentModel;
+import org.alfresco.repo.node.archive.RestoreNodeReport.RestoreStatus;
+import org.alfresco.repo.transaction.TransactionUtil;
+import org.alfresco.repo.transaction.TransactionUtil.TransactionWork;
+import org.alfresco.service.cmr.repository.InvalidNodeRefException;
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.ResultSetRow;
+import org.alfresco.service.cmr.search.SearchParameters;
+import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
+import org.alfresco.util.EqualsHelper;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
/**
* Implementation of the node archive abstraction.
@@ -32,7 +44,10 @@ import org.alfresco.service.transaction.TransactionService;
*/
public class NodeArchiveServiceImpl implements NodeArchiveService
{
+ private static Log logger = LogFactory.getLog(NodeArchiveServiceImpl.class);
+
private NodeService nodeService;
+ private SearchService searchService;
private TransactionService transactionService;
public void setNodeService(NodeService nodeService)
@@ -45,11 +60,99 @@ public class NodeArchiveServiceImpl implements NodeArchiveService
this.transactionService = transactionService;
}
+ public void setSearchService(SearchService searchService)
+ {
+ this.searchService = searchService;
+ }
+
public NodeRef getStoreArchiveNode(StoreRef storeRef)
{
return nodeService.getStoreArchiveNode(storeRef);
}
+ /**
+ * Get all the nodes that were archived from the given store.
+ */
+ private ResultSet getArchivedNodes(StoreRef originalStoreRef)
+ {
+ // Get the archive location
+ NodeRef archiveParentNodeRef = nodeService.getStoreArchiveNode(originalStoreRef);
+ StoreRef archiveStoreRef = archiveParentNodeRef.getStoreRef();
+ // build the query
+ String query = String.format("PARENT:\"%s\" AND ASPECT:\"%s\"", archiveParentNodeRef, ContentModel.ASPECT_ARCHIVED);
+ // search parameters
+ SearchParameters params = new SearchParameters();
+ params.addStore(archiveStoreRef);
+ params.setLanguage(SearchService.LANGUAGE_LUCENE);
+ params.setQuery(query);
+// params.addSort(ContentModel.PROP_ARCHIVED_DATE.toString(), false);
+ // get all archived children using a search
+ ResultSet rs = searchService.query(params);
+ // done
+ return rs;
+ }
+
+ /**
+ * This is the primary restore method that all restore
methods fall back on.
+ * It executes the restore for the node in a separate transaction and attempts to catch
+ * the known conditions that can be reported back to the client.
+ */
+ public RestoreNodeReport restoreArchivedNode(
+ final NodeRef archivedNodeRef,
+ final NodeRef destinationNodeRef,
+ final QName assocTypeQName,
+ final QName assocQName)
+ {
+ RestoreNodeReport report = new RestoreNodeReport(archivedNodeRef);
+ report.setTargetParentNodeRef(destinationNodeRef);
+ try
+ {
+ // Transactional wrapper to attempt the restore
+ TransactionWork restoreWork = new TransactionWork()
+ {
+ public NodeRef doWork() throws Exception
+ {
+ return nodeService.restoreNode(archivedNodeRef, destinationNodeRef, assocTypeQName, assocQName);
+ }
+ };
+ NodeRef newNodeRef = TransactionUtil.executeInNonPropagatingUserTransaction(transactionService, restoreWork);
+ // success
+ report.setRestoredNodeRef(newNodeRef);
+ report.setStatus(RestoreStatus.SUCCESS);
+ }
+ catch (InvalidNodeRefException e)
+ {
+ report.setCause(e);
+ NodeRef invalidNodeRef = e.getNodeRef();
+ if (archivedNodeRef.equals(invalidNodeRef))
+ {
+ // not too serious, but the node to archive is missing
+ report.setStatus(RestoreStatus.FAILURE_INVALID_ARCHIVE_NODE);
+ }
+ else if (EqualsHelper.nullSafeEquals(destinationNodeRef, invalidNodeRef))
+ {
+ report.setStatus(RestoreStatus.FAILURE_INVALID_PARENT);
+ }
+ else
+ {
+ // some other invalid node was detected
+ report.setStatus(RestoreStatus.FAILURE_OTHER);
+ }
+ }
+ // TODO: Catch permission exceptions
+ catch (Throwable e)
+ {
+ report.setCause(e);
+ report.setStatus(RestoreStatus.FAILURE_OTHER);
+ }
+ // done
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("Attempted node restore: "+ report);
+ }
+ return report;
+ }
+
/**
* @see #restoreArchivedNode(NodeRef, NodeRef, QName, QName)
*/
@@ -58,27 +161,12 @@ public class NodeArchiveServiceImpl implements NodeArchiveService
return restoreArchivedNode(archivedNodeRef, null, null, null);
}
- public RestoreNodeReport restoreArchivedNode(
- NodeRef archivedNodeRef,
- NodeRef destinationNodeRef,
- QName assocTypeQName,
- QName assocQName)
- {
- throw new UnsupportedOperationException();
- }
-
/**
- * @see #restoreArchivedNode(NodeRef, NodeRef, QName, QName)
+ * @see #restoreArchivedNodes(List, NodeRef, QName, QName)
*/
public List restoreArchivedNodes(List archivedNodeRefs)
{
- List results = new ArrayList(archivedNodeRefs.size());
- for (NodeRef nodeRef : archivedNodeRefs)
- {
- RestoreNodeReport result = restoreArchivedNode(nodeRef, null, null, null);
- results.add(result);
- }
- return results;
+ return restoreArchivedNodes(archivedNodeRefs, null, null, null);
}
/**
@@ -100,35 +188,96 @@ public class NodeArchiveServiceImpl implements NodeArchiveService
}
/**
- * @see #restoreArchivedNode(NodeRef, NodeRef, QName, QName)
+ * @see #restoreAllArchivedNodes(StoreRef, NodeRef, QName, QName)
*/
public List restoreAllArchivedNodes(StoreRef originalStoreRef)
{
- throw new UnsupportedOperationException();
+ return restoreAllArchivedNodes(originalStoreRef, null, null, null);
}
+ /**
+ * Finds the archive location for nodes that were deleted from the given store
+ * and attempt to restore each node.
+ *
+ * @see NodeService#getStoreArchiveNode(StoreRef)
+ * @see #restoreArchivedNode(NodeRef, NodeRef, QName, QName)
+ */
public List restoreAllArchivedNodes(
StoreRef originalStoreRef,
NodeRef destinationNodeRef,
QName assocTypeQName,
QName assocQName)
{
- throw new UnsupportedOperationException();
+ // get all archived children using a search
+ ResultSet rs = getArchivedNodes(originalStoreRef);
+ // loop through the resultset and attempt to restore all the nodes
+ List results = new ArrayList(1000);
+ for (ResultSetRow row : rs)
+ {
+ NodeRef archivedNodeRef = row.getNodeRef();
+ RestoreNodeReport result = restoreArchivedNode(archivedNodeRef, destinationNodeRef, assocTypeQName, assocQName);
+ results.add(result);
+ }
+ // done
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("Restored " + results.size() + " nodes into store " + originalStoreRef);
+ }
+ return results;
}
- public void purgeArchivedNode(NodeRef archivedNodeRef)
+ /**
+ * This is the primary purge methd that all purge methods fall back on. It isolates the delete
+ * work in a new transaction.
+ */
+ public void purgeArchivedNode(final NodeRef archivedNodeRef)
{
- throw new UnsupportedOperationException();
+ TransactionWork