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