From 6c07ba53c710e0ff2ce330b85636bcb227abcf40 Mon Sep 17 00:00:00 2001 From: Derek Hulley Date: Wed, 4 Jul 2007 01:03:16 +0000 Subject: [PATCH] Fixed AR-822: Space deletion via FTP or Web Client is improved. The node is renamed and hidden from FileFolderService clients. A background task then performs the archival. A bootstrap component ensures that failed archivals are picked up again. Found a bug with the status of nodes archived as part of a hierarchy not being updated correctly. git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@6148 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- config/alfresco/bootstrap-context.xml | 10 + config/alfresco/model/systemModel.xml | 15 + config/alfresco/node-services-context.xml | 22 + config/alfresco/repository.properties | 6 + .../java/org/alfresco/model/ContentModel.java | 5 + .../org/alfresco/repo/avm/AVMNodeService.java | 3 +- .../filefolder/FileFolderServiceImpl.java | 18 +- .../node/archive/DeletedTagBootstrap.java | 105 +++ .../node/archive/NodeArchiveInterceptor.java | 631 ++++++++++++++++++ .../repo/node/db/DbNodeServiceImpl.java | 50 +- .../repo/version/NodeServiceImpl.java | 2 +- .../service/cmr/repository/NodeService.java | 9 +- 12 files changed, 851 insertions(+), 25 deletions(-) create mode 100644 source/java/org/alfresco/repo/node/archive/DeletedTagBootstrap.java create mode 100644 source/java/org/alfresco/repo/node/archive/NodeArchiveInterceptor.java diff --git a/config/alfresco/bootstrap-context.xml b/config/alfresco/bootstrap-context.xml index 2ecc926035..5d996c9b51 100644 --- a/config/alfresco/bootstrap-context.xml +++ b/config/alfresco/bootstrap-context.xml @@ -344,6 +344,16 @@ + + + + + + + + + + diff --git a/config/alfresco/model/systemModel.xml b/config/alfresco/model/systemModel.xml index fbd6f98adc..5d1065b39e 100644 --- a/config/alfresco/model/systemModel.xml +++ b/config/alfresco/model/systemModel.xml @@ -141,6 +141,21 @@ Temporary + + + Deleted Node + + + d:text + true + + + d:text + true + + + + Archived diff --git a/config/alfresco/node-services-context.xml b/config/alfresco/node-services-context.xml index 250c540929..0262c30704 100644 --- a/config/alfresco/node-services-context.xml +++ b/config/alfresco/node-services-context.xml @@ -4,6 +4,27 @@ + + + + + + + + + + + + + + + + + + ${system.deleted-items.mode} + + + @@ -25,6 +46,7 @@ mlPropertyInterceptor + nodeArchiveInterceptor diff --git a/config/alfresco/repository.properties b/config/alfresco/repository.properties index 2a45770328..9b4df8779c 100644 --- a/config/alfresco/repository.properties +++ b/config/alfresco/repository.properties @@ -27,6 +27,12 @@ system.acl.maxPermissionCheckTimeMillis=10000 # The maximum number of results to perform permission checks against system.acl.maxPermissionChecks=1000 +# +# Control the transfer of nodes to 'Deleted Items' +# EAGER - Transfer nodes to 'Deleted Items' in the same transaction. (Slower) +# LAZY - Move nodes away but transfer to 'Deleted Items' separately. (Faster) +system.deleted-items.mode=LAZY + # #################### # # Lucene configuration # # #################### # diff --git a/source/java/org/alfresco/model/ContentModel.java b/source/java/org/alfresco/model/ContentModel.java index 92b1d9f254..c3d9b771c3 100644 --- a/source/java/org/alfresco/model/ContentModel.java +++ b/source/java/org/alfresco/model/ContentModel.java @@ -55,6 +55,11 @@ public interface ContentModel static final QName ASPECT_LOCALIZED = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "localized"); static final QName PROP_LOCALE = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "locale"); + // Deleted nodes constants + static final QName ASPECT_DELETED_NODE = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "deletedNode"); + static final QName PROP_DELETED_NODE_ORIGINAL_NAME = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "deletedNodeOriginalName"); + static final QName PROP_DELETED_NODE_USER = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "deletedNodeUser"); + // 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_ASSOC = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "archivedOriginalParentAssoc"); diff --git a/source/java/org/alfresco/repo/avm/AVMNodeService.java b/source/java/org/alfresco/repo/avm/AVMNodeService.java index 5b50ef7e88..15f7b08ded 100644 --- a/source/java/org/alfresco/repo/avm/AVMNodeService.java +++ b/source/java/org/alfresco/repo/avm/AVMNodeService.java @@ -764,7 +764,7 @@ public class AVMNodeService extends AbstractNodeServiceImpl implements NodeServi * @param nodeRef reference to a node within a store * @throws InvalidNodeRefException if the reference given is invalid */ - public void deleteNode(NodeRef nodeRef) throws InvalidNodeRefException + public boolean deleteNode(NodeRef nodeRef) throws InvalidNodeRefException { // Invoke policy behaviors. // invokeBeforeDeleteNode(nodeRef); @@ -795,6 +795,7 @@ public class AVMNodeService extends AbstractNodeServiceImpl implements NodeServi { throw new InvalidNodeRefException(avmVersionPath.getSecond() +" not found.", nodeRef); } + return true; } /** diff --git a/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java b/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java index b65f35c7c3..7c51417c59 100644 --- a/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java +++ b/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java @@ -76,6 +76,7 @@ public class FileFolderServiceImpl implements FileFolderService "./*" + "[like(@cm:name, $cm:name, false)" + " and not (subtypeOf('" + ContentModel.TYPE_SYSTEM_FOLDER + "'))" + + " and not (hasAspect('" + ContentModel.ASPECT_DELETED_NODE + "'))" + " and (subtypeOf('" + ContentModel.TYPE_FOLDER + "') or subtypeOf('" + ContentModel.TYPE_CONTENT + "')" + " or subtypeOf('" + ContentModel.TYPE_LINK + "'))]"; @@ -83,6 +84,7 @@ public class FileFolderServiceImpl implements FileFolderService private static final String LUCENE_QUERY_SHALLOW_ALL = "+PARENT:\"${cm:parent}\"" + "-TYPE:\"" + ContentModel.TYPE_SYSTEM_FOLDER + "\" " + + "-ASPECT:\"" + ContentModel.ASPECT_DELETED_NODE + "\" " + "+(" + "TYPE:\"" + ContentModel.TYPE_CONTENT + "\" " + "TYPE:\"" + ContentModel.TYPE_FOLDER + "\" " + @@ -93,12 +95,14 @@ public class FileFolderServiceImpl implements FileFolderService private static final String LUCENE_QUERY_SHALLOW_FOLDERS = "+PARENT:\"${cm:parent}\"" + "-TYPE:\"" + ContentModel.TYPE_SYSTEM_FOLDER + "\" " + + "-ASPECT:\"" + ContentModel.ASPECT_DELETED_NODE + "\" " + "+TYPE:\"" + ContentModel.TYPE_FOLDER + "\" "; /** Shallow search for all files and folders */ private static final String LUCENE_QUERY_SHALLOW_FILES = "+PARENT:\"${cm:parent}\"" + "-TYPE:\"" + ContentModel.TYPE_SYSTEM_FOLDER + "\" " + + "-ASPECT:\"" + ContentModel.ASPECT_DELETED_NODE + "\" " + "+TYPE:\"" + ContentModel.TYPE_CONTENT + "\" "; /** Deep search for files and folders with a name pattern */ @@ -106,6 +110,7 @@ public class FileFolderServiceImpl implements FileFolderService ".//*" + "[like(@cm:name, $cm:name, false)" + " and not (subtypeOf('" + ContentModel.TYPE_SYSTEM_FOLDER + "'))" + + " and not (hasAspect('" + ContentModel.ASPECT_DELETED_NODE + "'))" + " and (subtypeOf('" + ContentModel.TYPE_FOLDER + "') or subtypeOf('" + ContentModel.TYPE_CONTENT + "')" + " or subtypeOf('" + ContentModel.TYPE_LINK + "'))]"; @@ -198,11 +203,14 @@ public class FileFolderServiceImpl implements FileFolderService List results = new ArrayList(nodeRefs.size()); for (NodeRef nodeRef : nodeRefs) { - if (nodeService.exists(nodeRef)) + // Ignore missing nodes + if (!nodeService.exists(nodeRef)) { - FileInfo fileInfo = toFileInfo(nodeRef, true); - results.add(fileInfo); + continue; } + // It's good + FileInfo fileInfo = toFileInfo(nodeRef, true); + results.add(fileInfo); } return results; } @@ -324,6 +332,10 @@ public class FileFolderServiceImpl implements FileFolderService public NodeRef searchSimple(NodeRef contextNodeRef, String name) { NodeRef childNodeRef = nodeService.getChildByName(contextNodeRef, ContentModel.ASSOC_CONTAINS, name); + if (childNodeRef != null && nodeService.hasAspect(childNodeRef, ContentModel.ASPECT_DELETED_NODE)) + { + childNodeRef = null; + } if (logger.isDebugEnabled()) { logger.debug( diff --git a/source/java/org/alfresco/repo/node/archive/DeletedTagBootstrap.java b/source/java/org/alfresco/repo/node/archive/DeletedTagBootstrap.java new file mode 100644 index 0000000000..26a2a70dda --- /dev/null +++ b/source/java/org/alfresco/repo/node/archive/DeletedTagBootstrap.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2005-2007 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.node.archive; + +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +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.util.AbstractLifecycleBean; +import org.springframework.context.ApplicationEvent; + +/** + * Bootstrap component that component that ensures that any nodes tagged with the + * sys:deleted aspects are removed as the archival process was probably interrupted. + * + * @since 2.1 + * @author Derek Hulley + */ +public class DeletedTagBootstrap extends AbstractLifecycleBean +{ + private static final String LUCENE_QUERY = + "+ASPECT:\"" + ContentModel.ASPECT_DELETED_NODE + "\""; + + private NodeService nodeService; + private SearchService searchService; + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + @Override + protected void onBootstrap(ApplicationEvent event) + { + AuthenticationUtil.setSystemUserAsCurrentUser(); + removeAspects(); + } + + private void removeAspects() + { + // Get all stores + List storeRefs = nodeService.getStores(); + for (StoreRef storeRef : storeRefs) + { + SearchParameters params = new SearchParameters(); + params.setLanguage(SearchService.LANGUAGE_LUCENE); + params.addStore(storeRef); + params.setQuery(LUCENE_QUERY); + // Search + ResultSet rs = searchService.query(params); + try + { + for (ResultSetRow row : rs) + { + NodeRef nodeRef = row.getNodeRef(); + // Delete it + nodeService.deleteNode(nodeRef); + } + } + finally + { + rs.close(); + } + } + } + + @Override + protected void onShutdown(ApplicationEvent event) + { + } +} diff --git a/source/java/org/alfresco/repo/node/archive/NodeArchiveInterceptor.java b/source/java/org/alfresco/repo/node/archive/NodeArchiveInterceptor.java new file mode 100644 index 0000000000..5ce62a4782 --- /dev/null +++ b/source/java/org/alfresco/repo/node/archive/NodeArchiveInterceptor.java @@ -0,0 +1,631 @@ +/* + * Copyright (C) 2005-2007 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.node.archive; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ThreadPoolExecutor; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.node.StoreArchiveMap; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.repo.transaction.TransactionListenerAdapter; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +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.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.GUID; +import org.alfresco.util.PropertyMap; +import org.alfresco.util.VmShutdownListener; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * An interceptor to handle handle the deletion of nodes. This allows + * deletion and archival process to be pushed into the background. + * + * @since 2.1 + * @author Derek Hulley + */ +public class NodeArchiveInterceptor extends TransactionListenerAdapter implements MethodInterceptor +{ + private static VmShutdownListener shutdownListener = new VmShutdownListener("NodeArchiveInterceptor"); + + private static final Set INBOUND_FIRST_ARG = new HashSet(17); + private static final Set INBOUND_SECOND_ARG = new HashSet(17); + static + { + // First arguments + INBOUND_FIRST_ARG.add("getNodeStatus"); + INBOUND_FIRST_ARG.add("createNode"); + INBOUND_FIRST_ARG.add("moveNode"); + INBOUND_FIRST_ARG.add("getType"); + INBOUND_FIRST_ARG.add("setType"); + INBOUND_FIRST_ARG.add("addAspect"); + INBOUND_FIRST_ARG.add("removeAspect"); + INBOUND_FIRST_ARG.add("hasAspect"); + INBOUND_FIRST_ARG.add("getAspects"); + INBOUND_FIRST_ARG.add("addChild"); + INBOUND_FIRST_ARG.add("removeChild"); + INBOUND_FIRST_ARG.add("getProperties"); + INBOUND_FIRST_ARG.add("getProperty"); + INBOUND_FIRST_ARG.add("setProperties"); + INBOUND_FIRST_ARG.add("setProperty"); + INBOUND_FIRST_ARG.add("removeProperty"); + INBOUND_FIRST_ARG.add("getParentAssocs"); + INBOUND_FIRST_ARG.add("getChildAssocs"); + INBOUND_FIRST_ARG.add("getChildByName"); + INBOUND_FIRST_ARG.add("getPrimaryParent"); + INBOUND_FIRST_ARG.add("createAssociation"); + INBOUND_FIRST_ARG.add("removeAssociation"); + INBOUND_FIRST_ARG.add("getTargetAssocs"); + INBOUND_FIRST_ARG.add("getSourceAssocs"); + INBOUND_FIRST_ARG.add("getPath"); + INBOUND_FIRST_ARG.add("getPaths"); + INBOUND_FIRST_ARG.add("restoreNode"); + // Second arguments + INBOUND_SECOND_ARG.add("moveNode"); + INBOUND_SECOND_ARG.add("addChild"); + INBOUND_SECOND_ARG.add("removeChild"); + INBOUND_SECOND_ARG.add("createAssociation"); + INBOUND_SECOND_ARG.add("removeAssociation"); + INBOUND_SECOND_ARG.add("restoreNode"); + } + + /** A key for storing in-transaction values */ + private static final String KEY_DELETE_WORKERS = "NodeArchiveInterceptor.DeleteNodeWorkers"; + + private static Log logger = LogFactory.getLog(NodeArchiveInterceptor.class); + private static boolean isDebugEnabled = logger.isDebugEnabled(); + + /** + * An archival strategy to follow. + * @since 2.1 + * @author Derek Hulley + */ + public static enum ArchiveMode + { + /** + * Node archival will be done immediately within the current transaction. + */ + EAGER, + /** + * Node archival, where archival is going to occur, will be pushed onto a background + * process. + */ + LAZY + } + + /** Used to ensure that the interceptor isn't in a configuration endless loop */ + private ThreadLocal deleting = new ThreadLocal(); + + /** Used for running background deletes */ + private TransactionService transactionService; + /** Direct access to the NodeService */ + private NodeService nodeService; + /** Used to access property definitions */ + private DictionaryService dictionaryService; + /** A map of stores to send archived nodes to */ + private StoreArchiveMap storeArchiveMap; + /** Helper to perform background deletes */ + private ThreadPoolExecutor threadPoolExecutor; + /** The archival timing */ + private ArchiveMode archiveMode; + + /** + * Default constructor + */ + public NodeArchiveInterceptor() + { + } + + @Override + public boolean equals(Object obj) + { + return super.equals(obj); // Just to be explicit + } + + @Override + public int hashCode() + { + return super.hashCode(); // Just to be explicit + } + + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + /** + * @param nodeService the NodeService that doesn't include this interceptor + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + public void setStoreArchiveMap(StoreArchiveMap storeArchiveMap) + { + this.storeArchiveMap = storeArchiveMap; + } + + public void setThreadPoolExecutor(ThreadPoolExecutor threadPoolExecutor) + { + this.threadPoolExecutor = threadPoolExecutor; + } + + public void setArchiveMode(ArchiveMode archiveMode) + { + this.archiveMode = archiveMode; + } + + @SuppressWarnings("unchecked") + public Object invoke(MethodInvocation invocation) throws Throwable + { + Object ret = null; + + String methodName = invocation.getMethod().getName(); + Object[] args = invocation.getArguments(); + + if (methodName.equals("deleteNode")) + { + NodeRef nodeRef = (NodeRef) args[0]; + // Handle the deletion + boolean deleted = handleDeleteNode(nodeRef); + ret = Boolean.valueOf(deleted); + } +// else if (methodName.equals("exists")) +// { +// if (args[0] instanceof NodeRef) +// { +// NodeRef nodeRef = (NodeRef) args[0]; +// if (nodeService.exists(nodeRef)) +// { +// if (nodeService.hasAspect(nodeRef, ContentModel.ASPECT_DELETED_NODE)) +// { +// // It really exists, but shouldn't be visible +// ret = Boolean.FALSE; +// } +// else +// { +// ret = Boolean.TRUE; +// } +// } +// else +// { +// ret = Boolean.FALSE; +// } +// } +// else +// { +// ret = invocation.proceed(); +// } +// } + else + { + // All other methods will be checked for 'real' deletion. We post-process + // the successful methods as required so that we don't unnecessarily check + // for missing nodes when it would be picked up anyway. + ret = invocation.proceed(); + + } +// // Check first argument +// if (INBOUND_FIRST_ARG.contains(methodName)) +// { +// checkNodeForDeleteMarker((NodeRef)args[0]); +// } +// // Check seconds argument +// if (INBOUND_SECOND_ARG.contains(methodName)) +// { +// checkNodeForDeleteMarker((NodeRef)args[1]); +// } + + // done + return ret; + } + +// /** +// * Check if the node should be treated as invalid due to a deletion +// * +// * @param nodeRef the node to check +// * @throws InvalidNodeRefException +// * if the node has the sys:deleted aspect +// */ +// private void checkNodeForDeleteMarker(NodeRef nodeRef) +// { +// if (nodeService.hasAspect(nodeRef, ContentModel.ASPECT_DELETED_NODE)) +// { +// throw new InvalidNodeRefException("Node has been deleted: " + nodeRef, nodeRef); +// } +// } +// +// /** +// * +// * @param nodeRef the node to check +// * @return Returns true if the node has the sys:deleted aspect +// */ +// private boolean isDeleted(NodeRef nodeRef) +// { +// return nodeService.hasAspect(nodeRef, ContentModel.ASPECT_DELETED_NODE); +// } +// + /** + * Determines whether the node can be archived. + * + * @param nodeRef the node to check + * @return Returns true if the node can be archived + */ + private boolean isArchivable(NodeRef nodeRef) + { + // Temporary nodes can't be archived + boolean isTemporary = nodeService.hasAspect(nodeRef, ContentModel.ASPECT_TEMPORARY); + if (isTemporary) + { + return false; + } + // Check that the store has an associated archive store + StoreRef storeRef = nodeRef.getStoreRef(); + if (!storeArchiveMap.getArchiveMap().containsKey(storeRef)) + { + // There is no mapping for the store + return false; + } + // Check the type + QName nodeTypeQName = nodeService.getType(nodeRef); + TypeDefinition typeDef = dictionaryService.getType(nodeTypeQName); + if (typeDef == null || !typeDef.isArchive()) + { + // It is not an archivable type + return false; + } + // Otherwise it can be archived + return true; + } + + /** + * Performs a real delete, whilst ensuring that the interceptor doesn't get into an + * infinite loop in the case of a configuration error. + * + * @param nodeRef the node to delete + */ + private boolean deleteNodeDirectly(NodeRef nodeRef) + { + // Catch the infinite loop + if (deleting.get() == Boolean.TRUE) // Handles null and TRUE + { + throw new AlfrescoRuntimeException( + "The NodeArchiveInterceptor must be given a " + + "NodeService that is not similarly intercepted."); + } + try + { + deleting.set(Boolean.TRUE); + // It can really be deleted + return nodeService.deleteNode(nodeRef); + } + finally + { + deleting.set(Boolean.FALSE); + } + } + + /** + * Get the worker runnables that need to be executed after the current transaction has committed. + * + * @return Returns a list of delete node workers + */ + private List getDeleteWorkers() + { + @SuppressWarnings("unchecked") + List deleteRunners = + (List) AlfrescoTransactionSupport.getResource(KEY_DELETE_WORKERS); + if (deleteRunners == null) + { + // It is not bound, yet + deleteRunners = new ArrayList(20); + AlfrescoTransactionSupport.bindResource(KEY_DELETE_WORKERS, deleteRunners); + } + return deleteRunners; + } + + private boolean handleDeleteNode(NodeRef nodeRef) throws Throwable + { + boolean deleteDirect = false; + // If the node is not archivable, then we delete it inline + boolean isArchivable = isArchivable(nodeRef); + if (!isArchivable) + { + deleteDirect = true; + if (isDebugEnabled) + { + logger.debug("\n" + + "Deleted node directly as it is not archivable: \n" + + " Node: " + nodeRef + "\n" + + " Type: " + nodeService.getType(nodeRef)); + } + } + // Check the archive mode + if (archiveMode == ArchiveMode.EAGER) + { + deleteDirect = true; + if (isDebugEnabled) + { + logger.debug("\n" + + "Deleted node directly due to archive mode: \n" + + " Node: " + nodeRef + "\n" + + " Mode: " + archiveMode); + } + } + // When must we the nodes? + if (deleteDirect) + { + return deleteNodeDirectly(nodeRef); + } + else + { + try + { + if (!nodeService.hasAspect(nodeRef, ContentModel.ASPECT_DELETED_NODE)) + { + // We need to keep the node's original name for later use + Serializable name = nodeService.getProperty(nodeRef, ContentModel.PROP_NAME); + String currentUser = AuthenticationUtil.getCurrentUserName(); + // Add the sys:deletedNode aspect + PropertyMap properties = new PropertyMap(); + properties.put(ContentModel.PROP_DELETED_NODE_ORIGINAL_NAME, name); + properties.put(ContentModel.PROP_DELETED_NODE_USER, currentUser); + nodeService.addAspect(nodeRef, ContentModel.ASPECT_DELETED_NODE, properties); + // Now rename the node to a random name + String guid = GUID.generate(); + nodeService.setProperty(nodeRef, ContentModel.PROP_NAME, guid); + } + // Store it for later deletion + BackgroundDeleteRunner backgroundDeleteRunner = new BackgroundDeleteRunner(nodeRef); + getDeleteWorkers().add(backgroundDeleteRunner); + + // Register this instance as a listener on the transaction + AlfrescoTransactionSupport.bindListener(this); + } + catch(Throwable e) + { + e.printStackTrace(); + throw e; + } + + if (isDebugEnabled) + { + logger.debug("\n" + + "Queued node deletion for post-transaction processing: \n" + + " Node: " + nodeRef); + } + + return false; + } + } + + /** + * Checks if there are any nodes that were earmarked for deletion. These are then + * pushed onto an execution queue to be handled in the background. What we are sure + * of is that any nodes that were created have been committed by the transaction + * that has just ended. + */ + public void afterCommit() + { + // Get the list of nodes + List deleteWorkers = getDeleteWorkers(); + for (BackgroundDeleteRunner deleteWorker : deleteWorkers) + { + // Push the node onto the execution queue + threadPoolExecutor.submit(deleteWorker); + } + } + + /** + * A worker class that is able to delete a node, on behalf of a particular user, as a background + * task. + * + * @since 2.1 + * @author Derek Hulley + */ + private class BackgroundDeleteRunner implements Runnable + { + private NodeRef nodeRef; + + /** + * @param nodeRef the node to delete + */ + public BackgroundDeleteRunner(NodeRef nodeRef) + { + this.nodeRef = nodeRef; + } + public void run() + { + // Transaction wrapper + RetryingTransactionCallback deleteTxnCallback = new RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // Determine if the execution should proceed + RunAsWork getContinueAuthCallback = new RunAsWork() + { + public Boolean doWork() throws Exception + { + if (nodeService.exists(nodeRef) && nodeService.hasAspect(nodeRef, ContentModel.ASPECT_DELETED_NODE)) + { + return Boolean.TRUE; + } + else + { + return Boolean.FALSE; + } + } + }; + Boolean mustContinue = AuthenticationUtil.runAs(getContinueAuthCallback, AuthenticationUtil.SYSTEM_USER_NAME); + if (mustContinue == Boolean.FALSE) + { + if (isDebugEnabled) + { + logger.debug("\n" + + "Queued deletion stopped. The node is no longer marked for deletion or no longer exists. \n" + + " Node: " + nodeRef); + } + return null; + } + // Get the user that initiated the delete + RunAsWork getUserAuthCallback = new RunAsWork() + { + public String doWork() throws Exception + { + return (String) nodeService.getProperty(nodeRef, ContentModel.PROP_DELETED_NODE_USER); + } + }; + String runAs = AuthenticationUtil.runAs(getUserAuthCallback, AuthenticationUtil.SYSTEM_USER_NAME); + // Authentication wrapper + RunAsWork deleteAuthCallback = new RunAsWork() + { + /** + * Recursive method that removes the aspect from all children of the given node + */ + private void removeAspectFromHierarchy(NodeRef nodeRef) + { + if (nodeService.hasAspect(nodeRef, ContentModel.ASPECT_DELETED_NODE)) + { + // Restore the original name + Serializable originalName = nodeService.getProperty(nodeRef, ContentModel.PROP_DELETED_NODE_ORIGINAL_NAME); + nodeService.setProperty(nodeRef, ContentModel.PROP_NAME, originalName); + // Remove the aspect to stake our claim + nodeService.removeAspect(nodeRef, ContentModel.ASPECT_DELETED_NODE); + } + // Make sure that nothing in the hierarchy has the aspect, either + List childAssocRefs = nodeService.getChildAssocs(nodeRef); + for (ChildAssociationRef assocRef : childAssocRefs) + { + // Ignore non-primary assocs + if (!assocRef.isPrimary()) + { + continue; + } + removeAspectFromHierarchy(assocRef.getChildRef()); + } + } + public Object doWork() throws Exception + { + deleteNodeDirectly(nodeRef); + // If the node went into an archive, then follow it and remove the sys:deleted aspect + // and revert the cm:name property + NodeRef archivedRootNodeRef = nodeService.getStoreArchiveNode(nodeRef.getStoreRef()); + if (archivedRootNodeRef != null) + { + StoreRef archiveStoreRef = archivedRootNodeRef.getStoreRef(); + NodeRef archivedNodeRef = new NodeRef(archiveStoreRef, nodeRef.getId()); + if (nodeService.exists(archivedNodeRef)) + { + removeAspectFromHierarchy(archivedNodeRef); + } + } + // Success + if (isDebugEnabled) + { + logger.debug("\n" + + "Successfully deleted node.\n" + + " Node: " + nodeRef); + } + // Done + return null; + } + }; + return AuthenticationUtil.runAs(deleteAuthCallback, runAs); + } + }; + try + { + transactionService.getRetryingTransactionHelper().doInTransaction(deleteTxnCallback); + // Done + } + catch (Throwable e) + { + // We can ignore all errors if the VM is shutting down + if (NodeArchiveInterceptor.shutdownListener.isVmShuttingDown()) + { + return; + } + + // It failed, so just ensure that the sys:deleted aspect has been removed + RetryingTransactionCallback callback = new RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + RunAsWork authCallback = new RunAsWork() + { + public Object doWork() throws Exception + { + nodeService.removeAspect(nodeRef, ContentModel.ASPECT_DELETED_NODE); + return null; + } + }; + return AuthenticationUtil.runAs(authCallback, AuthenticationUtil.SYSTEM_USER_NAME); + } + }; + try + { + transactionService.getRetryingTransactionHelper().doInTransaction(callback); + } + catch (Throwable ee) + { + // This is bad, but the original exception is the one that really needs to get out. + // We dump this error. + logger.info("\n" + + "Failed to remove sys:deletedNode aspect from node: \n" + + " Node: " + nodeRef + "\n" + + " After Error: " + e.getMessage(), + e); + } + // Rethrow the original error + throw new AlfrescoRuntimeException("\n" + + "Failed to delete node: \n" + + " Node: " + nodeRef, + e); + } + } + } +} diff --git a/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java b/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java index d33dbfdf6b..25a3498032 100644 --- a/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java +++ b/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java @@ -285,6 +285,9 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl Assert.notNull(assocTypeQName); Assert.notNull(assocQName); + // Get the parent node + Node parentNode = getNodeNotNull(parentRef); + // null property map is allowed if (properties == null) { @@ -324,8 +327,6 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl // We now have enough to declare the child association creation invokeBeforeCreateChildAssociation(parentRef, childNodeRef, assocTypeQName, assocQName, true); - // Get the parent node - Node parentNode = getNodeNotNull(parentRef); // Create the association ChildAssoc childAssoc = nodeDaoService.newChildAssoc( parentNode, @@ -520,12 +521,12 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl { throw new InvalidTypeException(typeQName); } + Node node = getNodeNotNull(nodeRef); // Invoke policies invokeBeforeUpdateNode(nodeRef); // Get the node and set the new type - Node node = getNodeNotNull(nodeRef); node.setTypeQName(typeQName); // Add the default aspects to the node (update the properties with any new default values) @@ -553,12 +554,12 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl throw new InvalidAspectException("The aspect is invalid: " + aspectTypeQName, aspectTypeQName); } + Node node = getNodeNotNull(nodeRef); + // Invoke policy behaviours invokeBeforeUpdateNode(nodeRef); invokeBeforeAddAspect(nodeRef, aspectTypeQName); - Node node = getNodeNotNull(nodeRef); - // attach the properties to the current node properties Map nodeProperties = getPropertiesImpl(node); @@ -687,11 +688,14 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl return ret; } - public void deleteNode(NodeRef nodeRef) + /** + * {@inheritDoc} + */ + public boolean deleteNode(NodeRef nodeRef) { // First get the node to ensure that it exists Node node = getNodeNotNull(nodeRef); - + boolean requiresDelete = false; // Invoke policy behaviours @@ -736,17 +740,20 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl archiveNode(nodeRef, archiveStoreRef); // The archive performs a move, which will fire the appropriate OnDeleteNode } + // Done + return true; } public ChildAssociationRef addChild(NodeRef parentRef, NodeRef childRef, QName assocTypeQName, QName assocQName) { - // Invoke policy behaviours - invokeBeforeCreateChildAssociation(parentRef, childRef, assocTypeQName, assocQName, false); - // get the parent node and ensure that it is a container node Node parentNode = getNodeNotNull(parentRef); // get the child node Node childNode = getNodeNotNull(childRef); + + // Invoke policy behaviours + invokeBeforeCreateChildAssociation(parentRef, childRef, assocTypeQName, assocQName, false); + // make the association ChildAssoc assoc = nodeDaoService.newChildAssoc( parentNode, @@ -924,6 +931,9 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl public Serializable getProperty(NodeRef nodeRef, QName qname) throws InvalidNodeRefException { + // get the property from the node + Node node = getNodeNotNull(nodeRef); + // spoof referencable properties if (qname.equals(ContentModel.PROP_STORE_PROTOCOL)) { @@ -938,9 +948,6 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl return nodeRef.getId(); } - // get the property from the node - Node node = getNodeNotNull(nodeRef); - if (qname.equals(ContentModel.PROP_NODE_DBID)) { return node.getId(); @@ -1041,12 +1048,12 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl { Assert.notNull(qname); - // Invoke policy behaviours - invokeBeforeUpdateNode(nodeRef); - // get the node Node node = getNodeNotNull(nodeRef); + // Invoke policy behaviours + invokeBeforeUpdateNode(nodeRef); + // Do the set operation Map propertiesBefore = getPropertiesImpl(node); Map propertiesAfter = setPropertyImpl(node, qname, value); @@ -1094,12 +1101,12 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl throw new UnsupportedOperationException("The property " + qname + " may not be removed individually"); } - // Invoke policy behaviours - invokeBeforeUpdateNode(nodeRef); - // Get the node Node node = getNodeNotNull(nodeRef); + // Invoke policy behaviours + invokeBeforeUpdateNode(nodeRef); + // Get the values before Map propertiesBefore = getPropertiesImpl(node); // Remove the property @@ -1610,6 +1617,7 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl for (NodeStatus oldNodeStatus : nodeStatusesById.values()) { Node nodeToMove = oldNodeStatus.getNode(); + NodeRef oldNodeRef = nodeToMove.getNodeRef(); nodeToMove.setStore(store); NodeRef newNodeRef = nodeToMove.getNodeRef(); @@ -1619,6 +1627,10 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl NodeStatus newNodeStatus = nodeDaoService.getNodeStatus(newNodeRef, true); newNodeStatus.setNode(nodeToMove); + // Record change IDs + nodeDaoService.recordChangeId(oldNodeRef); + nodeDaoService.recordChangeId(newNodeRef); + invokeOnUpdateNode(newNodeRef); } } diff --git a/source/java/org/alfresco/repo/version/NodeServiceImpl.java b/source/java/org/alfresco/repo/version/NodeServiceImpl.java index 9795d388e0..739fd5ddc6 100644 --- a/source/java/org/alfresco/repo/version/NodeServiceImpl.java +++ b/source/java/org/alfresco/repo/version/NodeServiceImpl.java @@ -199,7 +199,7 @@ public class NodeServiceImpl implements NodeService, VersionModel /** * @throws UnsupportedOperationException always */ - public void deleteNode(NodeRef nodeRef) throws InvalidNodeRefException + public boolean deleteNode(NodeRef nodeRef) throws InvalidNodeRefException { // This operation is not supported for a version store throw new UnsupportedOperationException(MSG_UNSUPPORTED); diff --git a/source/java/org/alfresco/service/cmr/repository/NodeService.java b/source/java/org/alfresco/service/cmr/repository/NodeService.java index fa59c93f48..35cc4d6104 100644 --- a/source/java/org/alfresco/service/cmr/repository/NodeService.java +++ b/source/java/org/alfresco/service/cmr/repository/NodeService.java @@ -287,12 +287,19 @@ public interface NodeService * All associations (both children and regular node associations) * will be deleted, and where the given node is the primary parent, * the children will also be cascade deleted. + *

+ * Depending on the node's type, the presence of certain aspects, the + * node's store or the any other factors determined by the implementation, + * the node may not actually disappear immediately. It may be lined up for + * archival or later deletion. * * @param nodeRef reference to a node within a store + * @return Returns true if the node was completely removed, otherwise + * false if the node will still exist after the call. * @throws InvalidNodeRefException if the reference given is invalid */ @Auditable(key = Auditable.Key.ARG_0 ,parameters = {"nodeRef"}) - public void deleteNode(NodeRef nodeRef) throws InvalidNodeRefException; + public boolean deleteNode(NodeRef nodeRef) throws InvalidNodeRefException; /** * Makes a parent-child association between the given nodes. Both nodes must belong to the same store.