From 8c92948879657ee6cff67c3499f96b6ec1ca89c3 Mon Sep 17 00:00:00 2001 From: Derek Hulley Date: Sat, 6 May 2006 02:55:27 +0000 Subject: [PATCH] Low-level archive and restore functionality - Full tests of archive and restore against the contentModel.xml - TODO: Test permissions of archive store - Currently on a single, simple restoreNode method on NodeService - TODO: NodeRestoreService implementation to provide helpers around mass restoration, purging, etc git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@2782 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- config/alfresco/model/systemModel.xml | 4 +- .../public-services-security-context.xml | 2 + .../java/org/alfresco/model/ContentModel.java | 2 +- .../repo/domain/hibernate/Node.hbm.xml | 2 +- .../node/archive/ArchiveAndRestoreTest.java | 464 +++++++++++++ .../repo/node/db/DbNodeServiceImpl.java | 614 +++++++++++------- .../repo/version/NodeServiceImpl.java | 28 +- .../service/cmr/repository/NodeService.java | 29 + 8 files changed, 902 insertions(+), 243 deletions(-) create mode 100644 source/java/org/alfresco/repo/node/archive/ArchiveAndRestoreTest.java diff --git a/config/alfresco/model/systemModel.xml b/config/alfresco/model/systemModel.xml index f65db447b1..b30ed8c1ec 100644 --- a/config/alfresco/model/systemModel.xml +++ b/config/alfresco/model/systemModel.xml @@ -127,8 +127,8 @@ Archived - - d:noderef + + d:childassocref true diff --git a/config/alfresco/public-services-security-context.xml b/config/alfresco/public-services-security-context.xml index 010af3dae9..bbd89a376b 100644 --- a/config/alfresco/public-services-security-context.xml +++ b/config/alfresco/public-services-security-context.xml @@ -348,6 +348,8 @@ org.alfresco.service.cmr.repository.NodeService.getSourceAssocs=ROLE_AUTHENTICATED org.alfresco.service.cmr.repository.NodeService.getPath=ACL_NODE.0.sys:base.ReadProperties org.alfresco.service.cmr.repository.NodeService.getPaths=ACL_NODE.0.sys:base.ReadProperties + org.alfresco.service.cmr.repository.NodeService.getStoreArchiveNode=ACL_NODE.0.sys:base.Read + org.alfresco.service.cmr.repository.NodeService.restoreNode=ACL_NODE.0.sys:base.DeleteNode,ACL_NODE.1.sys:base.CreateChildren diff --git a/source/java/org/alfresco/model/ContentModel.java b/source/java/org/alfresco/model/ContentModel.java index bdf9c44490..8dfdc291ec 100644 --- a/source/java/org/alfresco/model/ContentModel.java +++ b/source/java/org/alfresco/model/ContentModel.java @@ -41,7 +41,7 @@ public interface ContentModel // archived nodes aspect constants static final QName ASPECT_ARCHIVED = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "archived"); - static final QName PROP_ARCHIVED_ORIGINAL_PARENT = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "archivedOriginalParent"); + static final QName PROP_ARCHIVED_ORIGINAL_PARENT_ASSOC = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "archivedOriginalParentAssoc"); static final QName PROP_ARCHIVED_BY = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "archivedBy"); static final QName PROP_ARCHIVED_DATE = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "archivedDate"); static final QName ASPECT_ARCHIVED_ASSOCS = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "archived-assocs"); diff --git a/source/java/org/alfresco/repo/domain/hibernate/Node.hbm.xml b/source/java/org/alfresco/repo/domain/hibernate/Node.hbm.xml index 3b97a33b9e..0701924b99 100644 --- a/source/java/org/alfresco/repo/domain/hibernate/Node.hbm.xml +++ b/source/java/org/alfresco/repo/domain/hibernate/Node.hbm.xml @@ -145,7 +145,7 @@ name="node" class="org.alfresco.repo.domain.hibernate.NodeImpl" column="node_id" - unique="true" + unique="false" not-null="false" fetch="join" lazy="false" /> diff --git a/source/java/org/alfresco/repo/node/archive/ArchiveAndRestoreTest.java b/source/java/org/alfresco/repo/node/archive/ArchiveAndRestoreTest.java new file mode 100644 index 0000000000..7565c401b0 --- /dev/null +++ b/source/java/org/alfresco/repo/node/archive/ArchiveAndRestoreTest.java @@ -0,0 +1,464 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node.archive; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.transaction.Status; +import javax.transaction.UserTransaction; + +import junit.framework.TestCase; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.node.StoreArchiveMap; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.AssociationRef; +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.security.AuthenticationService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.TestWithUserUtils; +import org.springframework.context.ApplicationContext; + +/** + * Test the archive and restore functionality provided by the low-level + * node service. + * + * @author Derek Hulley + */ +public class ArchiveAndRestoreTest extends TestCase +{ + private static final String USER_A = "AAAAA"; + private static final String USER_B = "BBBBB"; + private static final QName ASSOC_ATTACHMENTS = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "attachments"); + private static final QName QNAME_A = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "a"); + private static final QName QNAME_B = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "b"); + private static final QName QNAME_AA = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "aa"); + private static final QName QNAME_BB = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "bb"); + + private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + + private NodeService nodeService; + private PermissionService permissionService; + private AuthenticationComponent authenticationComponent; + private AuthenticationService authenticationService; + private TransactionService transactionService; + + private UserTransaction txn; + private StoreRef workStoreRef; + private NodeRef workStoreRootNodeRef; + private StoreRef archiveStoreRef; + private NodeRef archiveStoreRootNodeRef; + + private NodeRef a; + private NodeRef b; + private NodeRef aa; + private NodeRef bb; + AssociationRef assocAtoB; + AssociationRef assocAAtoBB; + ChildAssociationRef childAssocAtoAA; + ChildAssociationRef childAssocBtoBB; + ChildAssociationRef childAssocBtoAA; + ChildAssociationRef childAssocAtoBB; + private NodeRef a_; + private NodeRef b_; + private NodeRef aa_; + private NodeRef bb_; + ChildAssociationRef childAssocAtoAA_; + ChildAssociationRef childAssocBtoBB_; + + @Override + public void setUp() throws Exception + { + ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean("ServiceRegistry"); + nodeService = serviceRegistry.getNodeService(); + permissionService = serviceRegistry.getPermissionService(); + authenticationService = serviceRegistry.getAuthenticationService(); + authenticationComponent = (AuthenticationComponent) ctx.getBean("authenticationComponent"); + transactionService = serviceRegistry.getTransactionService(); + + // Start a transaction + txn = transactionService.getUserTransaction(); + txn.begin(); + + try + { + authenticationComponent.setSystemUserAsCurrentUser(); + // Create the work store + workStoreRef = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, getName() + System.currentTimeMillis()); + workStoreRootNodeRef = nodeService.getRootNode(workStoreRef); + archiveStoreRef = nodeService.createStore("archive", getName() + System.currentTimeMillis()); + archiveStoreRootNodeRef = nodeService.getRootNode(archiveStoreRef); + + // Map the work store to the archive store. This will already be wired into the NodeService. + StoreArchiveMap archiveMap = (StoreArchiveMap) ctx.getBean("storeArchiveMap"); + archiveMap.getArchiveMap().put(workStoreRef, archiveStoreRef); + + // grant everyone rights to the work store + permissionService.setPermission( + workStoreRootNodeRef, + PermissionService.ALL_AUTHORITIES, + PermissionService.ALL_PERMISSIONS, + true); + + TestWithUserUtils.createUser(USER_A, USER_A, workStoreRootNodeRef, nodeService, authenticationService); + TestWithUserUtils.createUser(USER_B, USER_B, workStoreRootNodeRef, nodeService, authenticationService); + } + finally + { + authenticationComponent.clearCurrentSecurityContext(); + } + // authenticate as normal user + authenticationService.authenticate(USER_A, USER_A.toCharArray()); + createNodeStructure(); + } + + @Override + public void tearDown() throws Exception + { + try + { + if (txn.getStatus() == Status.STATUS_ACTIVE) + { + txn.rollback(); + } + } + catch (Throwable e) + { + e.printStackTrace(); + } + } + + /** + * Create the following: + *
+     *        root
+     *       /  |  \
+     *      /   |   \
+     *     /    |    \
+     *    /     |     \
+     *   A  <-> B      X
+     *   |\    /|
+     *   | \  / |
+     *   |  \/  |
+     *   |  /\  |
+     *   | /  \ |
+     *   |/    \|
+     *   AA <-> BB
+     * 
+ * Explicit UUIDs are used for debugging purposes. + *

+ * A, B, AA and BB are set up to archive automatically + * on deletion. + */ + private void createNodeStructure() throws Exception + { + Map properties = new HashMap(5); + + properties.put(ContentModel.PROP_NODE_UUID, "a"); + a = nodeService.createNode( + workStoreRootNodeRef, + ContentModel.ASSOC_CHILDREN, + QNAME_A, + ContentModel.TYPE_FOLDER, + properties).getChildRef(); + properties.put(ContentModel.PROP_NODE_UUID, "aa"); + childAssocAtoAA = nodeService.createNode( + a, + ContentModel.ASSOC_CONTAINS, + QNAME_AA, + ContentModel.TYPE_CONTENT, + properties); + aa = childAssocAtoAA.getChildRef(); + properties.put(ContentModel.PROP_NODE_UUID, "b"); + b = nodeService.createNode( + workStoreRootNodeRef, + ContentModel.ASSOC_CHILDREN, + QNAME_B, + ContentModel.TYPE_FOLDER, + properties).getChildRef(); + properties.put(ContentModel.PROP_NODE_UUID, "bb"); + childAssocBtoBB = nodeService.createNode( + b, + ContentModel.ASSOC_CONTAINS, + QNAME_BB, + ContentModel.TYPE_CONTENT, + properties); + bb = childAssocBtoBB.getChildRef(); + assocAtoB = nodeService.createAssociation(a, b, ASSOC_ATTACHMENTS); + assocAAtoBB = nodeService.createAssociation(aa, bb, ASSOC_ATTACHMENTS); + childAssocBtoAA = nodeService.addChild( + b, + aa, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "aa")); + childAssocAtoBB = nodeService.addChild( + a, + bb, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "bb")); + + // deduce the references + a_ = new NodeRef(archiveStoreRef, a.getId()); + b_ = new NodeRef(archiveStoreRef, b.getId()); + aa_ = new NodeRef(archiveStoreRef, aa.getId()); + bb_ = new NodeRef(archiveStoreRef, bb.getId()); + childAssocAtoAA_ = new ChildAssociationRef( + childAssocAtoAA.getTypeQName(), + a_, + childAssocAtoAA.getQName(), + aa_); + childAssocBtoBB_ = new ChildAssociationRef( + childAssocBtoBB.getTypeQName(), + b_, + childAssocBtoBB.getQName(), + bb_); + } + + private void verifyNodeExistence(NodeRef nodeRef, boolean exists) + { + assertEquals("Node should " + (exists ? "" : "not") + "exist", exists, nodeService.exists(nodeRef)); + } + + private void verifyChildAssocExistence(ChildAssociationRef childAssocRef, boolean exists) + { + List childAssocs = nodeService.getChildAssocs( + childAssocRef.getParentRef(), + childAssocRef.getTypeQName(), + childAssocRef.getQName()); + if (exists) + { + assertEquals("Expected exactly one match for child association: " + childAssocRef, 1, childAssocs.size()); + } + else + { + assertEquals("Expected zero matches for child association: " + childAssocRef, 0, childAssocs.size()); + } + } + + private void verifyTargetAssocExistence(AssociationRef assocRef, boolean exists) + { + List assocs = nodeService.getTargetAssocs( + assocRef.getSourceRef(), + assocRef.getTypeQName()); + if (exists) + { + assertEquals("Expected exactly one match for target association: " + assocRef, 1, assocs.size()); + } + else + { + assertEquals("Expected zero matches for target association: " + assocRef, 0, assocs.size()); + } + } + + public void verifyAll() + { + // work store references + verifyNodeExistence(a, true); + verifyNodeExistence(b, true); + verifyNodeExistence(aa, true); + verifyNodeExistence(bb, true); + verifyChildAssocExistence(childAssocAtoAA, true); + verifyChildAssocExistence(childAssocBtoBB, true); + verifyChildAssocExistence(childAssocAtoBB, true); + verifyChildAssocExistence(childAssocBtoAA, true); + verifyTargetAssocExistence(assocAtoB, true); + verifyTargetAssocExistence(assocAAtoBB, true); + // archive store references + verifyNodeExistence(a_, false); + verifyNodeExistence(b_, false); + verifyNodeExistence(aa_, false); + verifyNodeExistence(bb_, false); + } + + public void testSetUp() throws Exception + { + verifyAll(); + } + + public void testGetStoreArchiveNode() throws Exception + { + NodeRef archiveNodeRef = nodeService.getStoreArchiveNode(workStoreRef); + assertEquals("Mapping of archived store is not correct", archiveStoreRootNodeRef, archiveNodeRef); + } + + public void testArchiveAndRestoreNodeBB() throws Exception + { + // delete a child + nodeService.deleteNode(bb); + // check + verifyNodeExistence(b, true); + verifyNodeExistence(bb, false); + verifyChildAssocExistence(childAssocAtoBB, false); + verifyChildAssocExistence(childAssocBtoBB, false); + verifyNodeExistence(b_, false); + verifyNodeExistence(bb_, true); + + // restore the node + nodeService.restoreNode(bb_, null, null, null); + // check + verifyAll(); + } + + public void testArchiveAndRestoreNodeB() throws Exception + { + // delete a child + nodeService.deleteNode(b); + // check + verifyNodeExistence(b, false); + verifyNodeExistence(bb, false); + verifyChildAssocExistence(childAssocAtoBB, false); + verifyTargetAssocExistence(assocAtoB, false); + verifyTargetAssocExistence(assocAAtoBB, false); + verifyNodeExistence(b_, true); + verifyNodeExistence(bb_, true); + verifyChildAssocExistence(childAssocBtoBB_, true); + + // restore the node + nodeService.restoreNode(b_, null, null, null); + // check + verifyAll(); + } + + public void testArchiveAndRestoreAll_B_A() throws Exception + { + // delete both trees in order 'b', 'a' + nodeService.deleteNode(b); + nodeService.deleteNode(a); + // restore in reverse order + nodeService.restoreNode(a_, null, null, null); + nodeService.restoreNode(b_, null, null, null); + // check + verifyAll(); + } + + public void testArchiveAndRestoreAll_A_B() throws Exception + { + // delete both trees in order 'b', 'a' + nodeService.deleteNode(a); + nodeService.deleteNode(b); + // restore in reverse order + nodeService.restoreNode(b_, null, null, null); + nodeService.restoreNode(a_, null, null, null); + // check + verifyAll(); + } + + public void testArchiveAndRestoreWithMissingAssocTargets() throws Exception + { + // delete a then b + nodeService.deleteNode(a); + nodeService.deleteNode(b); + // in restoring 'a' first, there will be some associations that won't be recreated + nodeService.restoreNode(a_, null, null, null); + nodeService.restoreNode(b_, null, null, null); + + // check + verifyNodeExistence(a, true); + verifyNodeExistence(b, true); + verifyNodeExistence(aa, true); + verifyNodeExistence(bb, true); + verifyChildAssocExistence(childAssocAtoAA, true); + verifyChildAssocExistence(childAssocBtoBB, true); + verifyChildAssocExistence(childAssocAtoBB, false); + verifyChildAssocExistence(childAssocBtoAA, false); + verifyTargetAssocExistence(assocAtoB, false); + verifyTargetAssocExistence(assocAAtoBB, false); + verifyNodeExistence(a_, false); + verifyNodeExistence(b_, false); + verifyNodeExistence(aa_, false); + verifyNodeExistence(bb_, false); + } + + /** + * Ensures that the archival is performed based on the node type. + */ + public void testTypeDetection() + { + // change the type of 'a' + nodeService.setType(a, ContentModel.TYPE_CONTAINER); + // delete it + nodeService.deleteNode(a); + // it must be gone + verifyNodeExistence(a, false); + verifyNodeExistence(a_, false); + } + + /** + * Attempt to measure how much archiving affects the deletion performance. + */ + public void testArchiveVsDeletePerformance() throws Exception + { + // Start by deleting the node structure and then recreating it. + // Only measure the delete speed + int iterations = 100; + long cumulatedArchiveTimeNs = 0; + long cumulatedRestoreTimeNs = 0; + for (int i = 0; i < iterations; i++) + { + // timed delete + long start = System.nanoTime(); + nodeService.deleteNode(b); + long end = System.nanoTime(); + cumulatedArchiveTimeNs += (end - start); + // now restore + start = System.nanoTime(); + nodeService.restoreNode(b_, null, null, null); + end = System.nanoTime(); + cumulatedRestoreTimeNs += (end - start); + } + double averageArchiveTimeMs = (double)cumulatedArchiveTimeNs / 1E6 / (double)iterations; + double averageRestoreTimeMs = (double)cumulatedRestoreTimeNs / 1E6 / (double)iterations; + System.out.println("Average archive time: " + averageArchiveTimeMs + " ms"); + System.out.println("Average restore time: " + averageRestoreTimeMs + " ms"); + + // Now force full deletions and creations + StoreArchiveMap archiveMap = (StoreArchiveMap) ctx.getBean("storeArchiveMap"); + archiveMap.getArchiveMap().clear(); + long cumulatedDeleteTimeNs = 0; + long cumulatedCreateTimeNs = 0; + for (int i = 0; i < iterations; i++) + { + // timed delete + long start = System.nanoTime(); + nodeService.deleteNode(b); + long end = System.nanoTime(); + cumulatedDeleteTimeNs += (end - start); + // delete 'a' as well + nodeService.deleteNode(a); + // now rebuild + start = System.nanoTime(); + createNodeStructure(); + end = System.nanoTime(); + cumulatedCreateTimeNs += (end - start); + } + double averageDeleteTimeMs = (double)cumulatedDeleteTimeNs / 1E6 / (double)iterations; + double averageCreateTimeMs = (double)cumulatedCreateTimeNs / 1E6 / (double)iterations; + System.out.println("Average delete time: " + averageDeleteTimeMs + " ms"); + System.out.println("Average create time: " + averageCreateTimeMs + " ms"); + } +} diff --git a/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java b/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java index 269d6875f8..9ae8aca26d 100644 --- a/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java +++ b/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java @@ -28,6 +28,7 @@ import java.util.Map; import java.util.Set; import java.util.Stack; +import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.model.ContentModel; import org.alfresco.repo.domain.ChildAssoc; import org.alfresco.repo.domain.Node; @@ -81,6 +82,7 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl public DbNodeServiceImpl() { + storeArchiveMap = new StoreArchiveMap(); // in case it is not set } public void setDictionaryService(DictionaryService dictionaryService) @@ -115,22 +117,6 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl return unchecked; } - /** - * Performs a null-safe get of the store - * @param storeRef the store to retrieve - * @return Returns the store entity (never null) - * @throws InvalidStoreRefException if the referenced store could not be found - */ - private Store getStoreNotNull(StoreRef storeRef) throws InvalidStoreRefException - { - Store unchecked = nodeDaoService.getStore(storeRef.getProtocol(), storeRef.getIdentifier()); - if (unchecked == null) - { - throw new InvalidStoreRefException("Store does not exist: " + storeRef, storeRef); - } - return unchecked; - } - public boolean exists(StoreRef storeRef) { Store store = nodeDaoService.getStore(storeRef.getProtocol(), storeRef.getIdentifier()); @@ -429,6 +415,15 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl invokeBeforeUpdateNode(oldParentNode.getNodeRef()); // old parent will be updated invokeBeforeUpdateNode(newParentRef); // new parent ditto + // If the node is moving stores, then drag the node hierarchy with it + if (!nodeToMoveRef.getStoreRef().equals(newParentRef.getStoreRef())) + { + Store newStore = newParentNode.getStore(); + moveNodeToStore(nodeToMove, newStore); + // the node reference will have changed too + nodeToMoveRef = nodeToMove.getNodeRef(); + } + // remove the child assoc from the old parent // don't cascade as we will still need the node afterwards nodeDaoService.deleteChildAssoc(oldAssoc, false); @@ -670,209 +665,6 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl invokeOnDeleteNode(childAssocRef, nodeTypeQName, nodeAspectQNames); } - private void archiveNode(NodeRef nodeRef, StoreRef archiveStoreRef) - { - Node node = getNodeNotNull(nodeRef); - Store archiveStore = getStoreNotNull(archiveStoreRef); - ChildAssoc primaryParentAssoc = nodeDaoService.getPrimaryParentAssoc(node); - - // add the aspect - node.getAspects().add(ContentModel.ASPECT_ARCHIVED); - Map properties = node.getProperties(); - PropertyValue archivedByProperty = makePropertyValue( - dictionaryService.getProperty(ContentModel.PROP_ARCHIVED_BY), - AuthenticationUtil.getCurrentUserName()); - properties.put(ContentModel.PROP_ARCHIVED_BY, archivedByProperty); - PropertyValue archivedDateProperty = makePropertyValue( - dictionaryService.getProperty(ContentModel.PROP_ARCHIVED_DATE), - new Date()); - properties.put(ContentModel.PROP_ARCHIVED_DATE, archivedDateProperty); - PropertyValue archivedPrimaryParentNodeRefProperty = makePropertyValue( - dictionaryService.getProperty(ContentModel.PROP_ARCHIVED_ORIGINAL_PARENT), - primaryParentAssoc.getParent().getNodeRef()); - properties.put(ContentModel.PROP_ARCHIVED_ORIGINAL_PARENT, archivedPrimaryParentNodeRefProperty); - - // get the IDs of all the node's primary children, including its own - Map nodesById = new HashMap(29); - getPrimaryChildren(node, nodesById); - - // move each node into the archive store - for (Node nodeToMove : nodesById.values()) - { - NodeRef oldNodeRef = nodeToMove.getNodeRef(); - nodeToMove.setStore(archiveStore); - NodeRef newNodeRef = nodeToMove.getNodeRef(); - - // update change statuses - String txnId = AlfrescoTransactionSupport.getTransactionId(); - NodeStatus oldNodeStatus = nodeDaoService.getNodeStatus(oldNodeRef, true); - oldNodeStatus.setNode(null); - oldNodeStatus.setChangeTxnId(txnId); - NodeStatus newNodeStatus = nodeDaoService.getNodeStatus(newNodeRef, true); - newNodeStatus.setNode(nodeToMove); - newNodeStatus.setChangeTxnId(txnId); - } - - // archive all the associations between the archived nodes and non-archived nodes - for (Node nodeToArchive : nodesById.values()) - { - archiveAssocs(nodeToArchive, nodesById); - } - - // the node reference has changed due to the store move - nodeRef = node.getNodeRef(); - - // now associate the top-level node with the root of the new store - NodeRef archiveStoreRootNodeRef = getRootNode(archiveStoreRef); - Node archiveStoreRootNode = getNodeNotNull(archiveStoreRootNodeRef); - QName assocTypeQName = ContentModel.ASSOC_CHILDREN; - QName assocQName = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "archivedItem"); - - invokeBeforeCreateChildAssociation(archiveStoreRootNodeRef, nodeRef, assocTypeQName, assocQName); - invokeBeforeUpdateNode(archiveStoreRootNodeRef); - - // create a new assoc - ChildAssoc newAssoc = nodeDaoService.newChildAssoc(archiveStoreRootNode, node, true, assocTypeQName, assocQName); - - // invoke policy behaviour - invokeOnCreateChildAssociation(newAssoc.getChildAssocRef()); - invokeOnUpdateNode(archiveStoreRootNodeRef); - } - - /** - * Fill the map of all primary children below the given node. - * The given node will be added to the map and the method is recursive - * to all primary children. - */ - private void getPrimaryChildren(Node node, Map nodesById) - { - Long id = node.getId(); - if (nodesById.containsKey(id)) - { - // this ID was already added - circular reference - logger.warn("Circular hierarchy found including node " + id); - return; - } - // add the node to the map - nodesById.put(id, node); - // recurse into the primary children - Collection childAssocs = node.getChildAssocs(); - for (ChildAssoc childAssoc : childAssocs) - { - // cascade into primary associations - if (childAssoc.getIsPrimary()) - { - Node primaryChild = childAssoc.getChild(); - getPrimaryChildren(primaryChild, nodesById); - } - } - } - - /** - * Archive all associations to and from the given node, with the - * exception of associations to or from nodes in the given map. - * @param node the node whose associations must be archived - * @param nodesById a map of nodes partaking in the archival process - */ - private void archiveAssocs(Node node, Map nodesById) - { - List childAssocsToDelete = new ArrayList(5); - // child associations - ArrayList archivedChildAssocRefs = new ArrayList(5); - for (ChildAssoc assoc : node.getChildAssocs()) - { - Long relatedNodeId = assoc.getChild().getId(); - if (nodesById.containsKey(relatedNodeId)) - { - // a sibling in the archive process - continue; - } - childAssocsToDelete.add(assoc); - archivedChildAssocRefs.add(assoc.getChildAssocRef()); - } - // parent associations - ArrayList archivedParentAssocRefs = new ArrayList(5); - for (ChildAssoc assoc : node.getParentAssocs()) - { - Long relatedNodeId = assoc.getParent().getId(); - if (nodesById.containsKey(relatedNodeId)) - { - // a sibling in the archive process - continue; - } - childAssocsToDelete.add(assoc); - archivedParentAssocRefs.add(assoc.getChildAssocRef()); - } - - List nodeAssocsToDelete = new ArrayList(5); - // source associations - ArrayList archivedSourceAssocRefs = new ArrayList(5); - for (NodeAssoc assoc : node.getSourceNodeAssocs()) - { - Long relatedNodeId = assoc.getSource().getId(); - if (nodesById.containsKey(relatedNodeId)) - { - // a sibling in the archive process - continue; - } - nodeAssocsToDelete.add(assoc); - archivedSourceAssocRefs.add(assoc.getNodeAssocRef()); - } - // target associations - ArrayList archivedTargetAssocRefs = new ArrayList(5); - for (NodeAssoc assoc : node.getTargetNodeAssocs()) - { - Long relatedNodeId = assoc.getSource().getId(); - if (nodesById.containsKey(relatedNodeId)) - { - // a sibling in the archive process - continue; - } - nodeAssocsToDelete.add(assoc); - archivedTargetAssocRefs.add(assoc.getNodeAssocRef()); - } - // delete child assocs - for (ChildAssoc assoc : childAssocsToDelete) - { - nodeDaoService.deleteChildAssoc(assoc, false); - } - // delete node assocs - for (NodeAssoc assoc : nodeAssocsToDelete) - { - nodeDaoService.deleteNodeAssoc(assoc); - } - - // add archived aspect - node.getAspects().add(ContentModel.ASPECT_ARCHIVED_ASSOCS); - // set properties - Map properties = node.getProperties(); - - if (archivedParentAssocRefs.size() > 0) - { - PropertyDefinition propertyDef = dictionaryService.getProperty(ContentModel.PROP_ARCHIVED_PARENT_ASSOCS); - PropertyValue propertyValue = makePropertyValue(propertyDef, archivedParentAssocRefs); - properties.put(ContentModel.PROP_ARCHIVED_PARENT_ASSOCS, propertyValue); - } - if (archivedChildAssocRefs.size() > 0) - { - PropertyDefinition propertyDef = dictionaryService.getProperty(ContentModel.PROP_ARCHIVED_CHILD_ASSOCS); - PropertyValue propertyValue = makePropertyValue(propertyDef, archivedChildAssocRefs); - properties.put(ContentModel.PROP_ARCHIVED_CHILD_ASSOCS, propertyValue); - } - if (archivedSourceAssocRefs.size() > 0) - { - PropertyDefinition propertyDef = dictionaryService.getProperty(ContentModel.PROP_ARCHIVED_SOURCE_ASSOCS); - PropertyValue propertyValue = makePropertyValue(propertyDef, archivedSourceAssocRefs); - properties.put(ContentModel.PROP_ARCHIVED_SOURCE_ASSOCS, propertyValue); - } - if (archivedTargetAssocRefs.size() > 0) - { - PropertyDefinition propertyDef = dictionaryService.getProperty(ContentModel.PROP_ARCHIVED_TARGET_ASSOCS); - PropertyValue propertyValue = makePropertyValue(propertyDef, archivedTargetAssocRefs); - properties.put(ContentModel.PROP_ARCHIVED_TARGET_ASSOCS, propertyValue); - } - } - public ChildAssociationRef addChild(NodeRef parentRef, NodeRef childRef, QName assocTypeQName, QName assocQName) { // Invoke policy behaviours @@ -1482,4 +1274,388 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl // done return paths; } + + private void archiveNode(NodeRef nodeRef, StoreRef archiveStoreRef) + { + Node node = getNodeNotNull(nodeRef); + ChildAssoc primaryParentAssoc = nodeDaoService.getPrimaryParentAssoc(node); + + // add the aspect + node.getAspects().add(ContentModel.ASPECT_ARCHIVED); + Map properties = node.getProperties(); + PropertyValue archivedByProperty = makePropertyValue( + dictionaryService.getProperty(ContentModel.PROP_ARCHIVED_BY), + AuthenticationUtil.getCurrentUserName()); + properties.put(ContentModel.PROP_ARCHIVED_BY, archivedByProperty); + PropertyValue archivedDateProperty = makePropertyValue( + dictionaryService.getProperty(ContentModel.PROP_ARCHIVED_DATE), + new Date()); + properties.put(ContentModel.PROP_ARCHIVED_DATE, archivedDateProperty); + PropertyValue archivedPrimaryParentNodeRefProperty = makePropertyValue( + dictionaryService.getProperty(ContentModel.PROP_ARCHIVED_ORIGINAL_PARENT_ASSOC), + primaryParentAssoc.getChildAssocRef()); + properties.put(ContentModel.PROP_ARCHIVED_ORIGINAL_PARENT_ASSOC, archivedPrimaryParentNodeRefProperty); + + // move the node + NodeRef archiveStoreRootNodeRef = getRootNode(archiveStoreRef); + moveNode( + nodeRef, + archiveStoreRootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "archivedItem")); + + // get the IDs of all the node's primary children, including its own + Map nodesById = getNodeHierarchy(node, null); + + // Archive all the associations between the archived nodes and non-archived nodes + for (Node nodeToArchive : nodesById.values()) + { + archiveAssocs(nodeToArchive, nodesById); + } + + // the node reference has changed due to the store move + nodeRef = node.getNodeRef(); + } + + /** + * Performs all the necessary housekeeping involved in changing a node's store. + * This method cascades down through all the primary children of the node as + * well. + * + * @param node the node whose store is changing + * @param store the new store for the node + */ + private void moveNodeToStore(Node node, Store store) + { + // get the IDs of all the node's primary children, including its own + Map nodesById = getNodeHierarchy(node, null); + + // move each node into the archive store + for (Node nodeToMove : nodesById.values()) + { + NodeRef oldNodeRef = nodeToMove.getNodeRef(); + nodeToMove.setStore(store); + NodeRef newNodeRef = nodeToMove.getNodeRef(); + + // update change statuses + String txnId = AlfrescoTransactionSupport.getTransactionId(); + NodeStatus oldNodeStatus = nodeDaoService.getNodeStatus(oldNodeRef, true); + oldNodeStatus.setNode(null); + oldNodeStatus.setChangeTxnId(txnId); + NodeStatus newNodeStatus = nodeDaoService.getNodeStatus(newNodeRef, true); + newNodeStatus.setNode(nodeToMove); + newNodeStatus.setChangeTxnId(txnId); + } + } + + /** + * Fill the map of all primary children below the given node. + * The given node will be added to the map and the method is recursive + * to all primary children. + * + * @param node the start of the hierarchy + * @param nodesById a map of nodes that will be reused as the return value + * @return Returns a map of nodes in the hierarchy keyed by their IDs + */ + private Map getNodeHierarchy(Node node, Map nodesById) + { + if (nodesById == null) + { + nodesById = new HashMap(23); + } + + Long id = node.getId(); + if (nodesById.containsKey(id)) + { + // this ID was already added - circular reference + logger.warn("Circular hierarchy found including node " + id); + return nodesById; + } + // add the node to the map + nodesById.put(id, node); + // recurse into the primary children + Collection childAssocs = node.getChildAssocs(); + for (ChildAssoc childAssoc : childAssocs) + { + // cascade into primary associations + if (childAssoc.getIsPrimary()) + { + Node primaryChild = childAssoc.getChild(); + nodesById = getNodeHierarchy(primaryChild, nodesById); + } + } + return nodesById; + } + + /** + * Archive all associations to and from the given node, with the + * exception of associations to or from nodes in the given map. + *

+ * Primary parent associations are also ignored. + * + * @param node the node whose associations must be archived + * @param nodesById a map of nodes partaking in the archival process + */ + private void archiveAssocs(Node node, Map nodesById) + { + List childAssocsToDelete = new ArrayList(5); + // child associations + ArrayList archivedChildAssocRefs = new ArrayList(5); + for (ChildAssoc assoc : node.getChildAssocs()) + { + Long relatedNodeId = assoc.getChild().getId(); + if (nodesById.containsKey(relatedNodeId)) + { + // a sibling in the archive process + continue; + } + childAssocsToDelete.add(assoc); + archivedChildAssocRefs.add(assoc.getChildAssocRef()); + } + // parent associations + ArrayList archivedParentAssocRefs = new ArrayList(5); + for (ChildAssoc assoc : node.getParentAssocs()) + { + Long relatedNodeId = assoc.getParent().getId(); + if (nodesById.containsKey(relatedNodeId)) + { + // a sibling in the archive process + continue; + } + else if (assoc.getIsPrimary()) + { + // ignore the primary parent as this is handled more specifically + continue; + } + childAssocsToDelete.add(assoc); + archivedParentAssocRefs.add(assoc.getChildAssocRef()); + } + + List nodeAssocsToDelete = new ArrayList(5); + // source associations + ArrayList archivedSourceAssocRefs = new ArrayList(5); + for (NodeAssoc assoc : node.getSourceNodeAssocs()) + { + Long relatedNodeId = assoc.getSource().getId(); + if (nodesById.containsKey(relatedNodeId)) + { + // a sibling in the archive process + continue; + } + nodeAssocsToDelete.add(assoc); + archivedSourceAssocRefs.add(assoc.getNodeAssocRef()); + } + // target associations + ArrayList archivedTargetAssocRefs = new ArrayList(5); + for (NodeAssoc assoc : node.getTargetNodeAssocs()) + { + Long relatedNodeId = assoc.getTarget().getId(); + if (nodesById.containsKey(relatedNodeId)) + { + // a sibling in the archive process + continue; + } + nodeAssocsToDelete.add(assoc); + archivedTargetAssocRefs.add(assoc.getNodeAssocRef()); + } + // delete child assocs + for (ChildAssoc assoc : childAssocsToDelete) + { + nodeDaoService.deleteChildAssoc(assoc, false); + } + // delete node assocs + for (NodeAssoc assoc : nodeAssocsToDelete) + { + nodeDaoService.deleteNodeAssoc(assoc); + } + + // add archived aspect + node.getAspects().add(ContentModel.ASPECT_ARCHIVED_ASSOCS); + // set properties + Map properties = node.getProperties(); + + if (archivedParentAssocRefs.size() > 0) + { + PropertyDefinition propertyDef = dictionaryService.getProperty(ContentModel.PROP_ARCHIVED_PARENT_ASSOCS); + PropertyValue propertyValue = makePropertyValue(propertyDef, archivedParentAssocRefs); + properties.put(ContentModel.PROP_ARCHIVED_PARENT_ASSOCS, propertyValue); + } + if (archivedChildAssocRefs.size() > 0) + { + PropertyDefinition propertyDef = dictionaryService.getProperty(ContentModel.PROP_ARCHIVED_CHILD_ASSOCS); + PropertyValue propertyValue = makePropertyValue(propertyDef, archivedChildAssocRefs); + properties.put(ContentModel.PROP_ARCHIVED_CHILD_ASSOCS, propertyValue); + } + if (archivedSourceAssocRefs.size() > 0) + { + PropertyDefinition propertyDef = dictionaryService.getProperty(ContentModel.PROP_ARCHIVED_SOURCE_ASSOCS); + PropertyValue propertyValue = makePropertyValue(propertyDef, archivedSourceAssocRefs); + properties.put(ContentModel.PROP_ARCHIVED_SOURCE_ASSOCS, propertyValue); + } + if (archivedTargetAssocRefs.size() > 0) + { + PropertyDefinition propertyDef = dictionaryService.getProperty(ContentModel.PROP_ARCHIVED_TARGET_ASSOCS); + PropertyValue propertyValue = makePropertyValue(propertyDef, archivedTargetAssocRefs); + properties.put(ContentModel.PROP_ARCHIVED_TARGET_ASSOCS, propertyValue); + } + } + + public NodeRef getStoreArchiveNode(StoreRef storeRef) + { + StoreRef archiveStoreRef = storeArchiveMap.getArchiveMap().get(storeRef); + if (archiveStoreRef == null) + { + // no mapping for the given store + return null; + } + else + { + return getRootNode(archiveStoreRef); + } + } + + public NodeRef restoreNode(NodeRef archivedNodeRef, NodeRef targetParentNodeRef, QName assocTypeQName, QName assocQName) + { + Node archivedNode = getNodeNotNull(archivedNodeRef); + Set aspects = archivedNode.getAspects(); + Map properties = archivedNode.getProperties(); + // the node must be a top-level archive node + if (!aspects.contains(ContentModel.ASPECT_ARCHIVED)) + { + throw new AlfrescoRuntimeException("The node to archive is not an archive node"); + } + ChildAssociationRef originalPrimaryParentAssocRef = (ChildAssociationRef) makeSerializableValue( + dictionaryService.getProperty(ContentModel.PROP_ARCHIVED_ORIGINAL_PARENT_ASSOC), + properties.get(ContentModel.PROP_ARCHIVED_ORIGINAL_PARENT_ASSOC)); + // remove the aspect archived aspect + aspects.remove(ContentModel.ASPECT_ARCHIVED); + properties.remove(ContentModel.PROP_ARCHIVED_ORIGINAL_PARENT_ASSOC); + properties.remove(ContentModel.PROP_ARCHIVED_BY); + properties.remove(ContentModel.PROP_ARCHIVED_DATE); + + if (targetParentNodeRef == null) + { + // we must restore to the original location + targetParentNodeRef = originalPrimaryParentAssocRef.getParentRef(); + } + // check the associations + if (assocTypeQName == null) + { + assocTypeQName = originalPrimaryParentAssocRef.getTypeQName(); + } + if (assocQName == null) + { + assocQName = originalPrimaryParentAssocRef.getQName(); + } + + // move the node to the target parent, which may or may not be the original parent + moveNode( + archivedNodeRef, + targetParentNodeRef, + assocTypeQName, + assocQName); + + // get the IDs of all the node's primary children, including its own + Map restoredNodesById = getNodeHierarchy(archivedNode, null); + // Restore the archived associations, if required + for (Node restoredNode : restoredNodesById.values()) + { + restoreAssocs(restoredNode); + } + + // the node reference has changed due to the store move + NodeRef restoredNodeRef = archivedNode.getNodeRef(); + + // done + if (logger.isDebugEnabled()) + { + logger.debug("Restored node: \n" + + " original noderef: " + archivedNodeRef + "\n" + + " restored noderef: " + restoredNodeRef + "\n" + + " new parent: " + targetParentNodeRef); + } + return restoredNodeRef; + } + + private void restoreAssocs(Node node) + { + NodeRef nodeRef = node.getNodeRef(); + // set properties + Map properties = node.getProperties(); + + // restore parent associations + Collection parentAssocRefs = (Collection) getProperty( + nodeRef, + ContentModel.PROP_ARCHIVED_PARENT_ASSOCS); + if (parentAssocRefs != null) + { + for (ChildAssociationRef assocRef : parentAssocRefs) + { + NodeRef parentNodeRef = assocRef.getParentRef(); + if (!exists(parentNodeRef)) + { + continue; + } + Node parentNode = getNodeNotNull(parentNodeRef); + nodeDaoService.newChildAssoc(parentNode, node, assocRef.isPrimary(), assocRef.getTypeQName(), assocRef.getQName()); + } + properties.remove(ContentModel.PROP_ARCHIVED_PARENT_ASSOCS); + } + // restore child associations + Collection childAssocRefs = (Collection) getProperty( + nodeRef, + ContentModel.PROP_ARCHIVED_CHILD_ASSOCS); + if (childAssocRefs != null) + { + for (ChildAssociationRef assocRef : childAssocRefs) + { + NodeRef childNodeRef = assocRef.getChildRef(); + if (!exists(childNodeRef)) + { + continue; + } + Node childNode = getNodeNotNull(childNodeRef); + nodeDaoService.newChildAssoc(node, childNode, assocRef.isPrimary(), assocRef.getTypeQName(), assocRef.getQName()); + } + properties.remove(ContentModel.PROP_ARCHIVED_CHILD_ASSOCS); + } + // restore source associations + Collection sourceAssocRefs = (Collection) getProperty( + nodeRef, + ContentModel.PROP_ARCHIVED_SOURCE_ASSOCS); + if (sourceAssocRefs != null) + { + for (AssociationRef assocRef : sourceAssocRefs) + { + NodeRef sourceNodeRef = assocRef.getSourceRef(); + if (!exists(sourceNodeRef)) + { + continue; + } + Node sourceNode = getNodeNotNull(sourceNodeRef); + nodeDaoService.newNodeAssoc(sourceNode, node, assocRef.getTypeQName()); + } + properties.remove(ContentModel.PROP_ARCHIVED_SOURCE_ASSOCS); + } + // restore target associations + Collection targetAssocRefs = (Collection) getProperty( + nodeRef, + ContentModel.PROP_ARCHIVED_TARGET_ASSOCS); + if (targetAssocRefs != null) + { + for (AssociationRef assocRef : targetAssocRefs) + { + NodeRef targetNodeRef = assocRef.getTargetRef(); + if (!exists(targetNodeRef)) + { + continue; + } + Node targetNode = getNodeNotNull(targetNodeRef); + nodeDaoService.newNodeAssoc(node, targetNode, assocRef.getTypeQName()); + } + properties.remove(ContentModel.PROP_ARCHIVED_TARGET_ASSOCS); + } + // remove the aspect + node.getAspects().remove(ContentModel.ASPECT_ARCHIVED_ASSOCS); + } } diff --git a/source/java/org/alfresco/repo/version/NodeServiceImpl.java b/source/java/org/alfresco/repo/version/NodeServiceImpl.java index 8a0828aa9b..62b50a957a 100644 --- a/source/java/org/alfresco/repo/version/NodeServiceImpl.java +++ b/source/java/org/alfresco/repo/version/NodeServiceImpl.java @@ -39,9 +39,7 @@ import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.Path; import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.service.cmr.repository.NodeRef.Status; -import org.alfresco.service.cmr.search.QueryParameterDefinition; import org.alfresco.service.cmr.search.SearchService; -import org.alfresco.service.namespace.NamespacePrefixResolver; import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.QNamePattern; import org.alfresco.service.namespace.RegexQNamePattern; @@ -534,29 +532,19 @@ public class NodeServiceImpl implements NodeService, VersionModel return paths; } - public List selectNodes(NodeRef contextNode, String XPath, QueryParameterDefinition[] parameters, NamespacePrefixResolver namespacePrefixResolver, boolean followAllParentLinks) + /** + * @throws UnsupportedOperationException always + */ + public NodeRef getStoreArchiveNode(StoreRef storeRef) { - // TODO Auto-generated method stub throw new UnsupportedOperationException(MSG_UNSUPPORTED); } - public List selectProperties(NodeRef contextNode, String XPath, QueryParameterDefinition[] parameters, NamespacePrefixResolver namespacePrefixResolver, boolean followAllParentLinks) + /** + * @throws UnsupportedOperationException always + */ + public NodeRef restoreNode(NodeRef archivedNodeRef, NodeRef targetParentNodeRef, QName assocTypeQName, QName assocQName) { - // TODO Auto-generated method stub throw new UnsupportedOperationException(MSG_UNSUPPORTED); } - - public boolean contains(NodeRef nodeRef, QName property, String sqlLikePattern) - { - // TODO Auto-generated method stub - throw new UnsupportedOperationException(); - } - - public boolean like(NodeRef nodeRef, QName property, String sqlLikePattern, boolean includeFTS) - { - // TODO Auto-generated method stub - throw new UnsupportedOperationException(); - } - - } diff --git a/source/java/org/alfresco/service/cmr/repository/NodeService.java b/source/java/org/alfresco/service/cmr/repository/NodeService.java index 94a4c57325..e6c3d6e792 100644 --- a/source/java/org/alfresco/service/cmr/repository/NodeService.java +++ b/source/java/org/alfresco/service/cmr/repository/NodeService.java @@ -119,6 +119,10 @@ public interface NodeService *

* This involves changing the node's primary parent and possibly the name of the * association referencing it. + *

+ * If the new parent is in a different store from the original, then the entire + * node hierarchy is moved to the new store. Inter-store associations are not + * affected. * * @param nodeToMoveRef the node to move * @param newParentRef the new parent of the moved node @@ -475,4 +479,29 @@ public interface NodeService * @throws InvalidNodeRefException if the node could not be found */ public List getPaths(NodeRef nodeRef, boolean primaryOnly) throws InvalidNodeRefException; + + /** + * Get the node where archived items will have gone when deleted from the given store. + * + * @param storeRef the store that items were deleted from + * @return Returns the archive node parent + */ + public NodeRef getStoreArchiveNode(StoreRef storeRef); + + /** + * Restore an individual node (along with its sub-tree nodes) to the target location. + * The archived node must have the {@link org.alfresco.model.ContentModel#ASPECT_ARCHIVED archived aspect} + * set against it. + * + * @param archivedNodeRef the archived node + * @param targetParentNodeRef + * @param assocTypeQName + * @param assocQName + * @return Returns the reference to the newly created node + */ + public NodeRef restoreNode( + NodeRef archivedNodeRef, + NodeRef targetParentNodeRef, + QName assocTypeQName, + QName assocQName); }