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
This commit is contained in:
Derek Hulley
2007-07-04 01:03:16 +00:00
parent d2027916c7
commit 6c07ba53c7
12 changed files with 851 additions and 25 deletions

View File

@@ -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");

View File

@@ -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;
}
/**

View File

@@ -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<FileInfo> results = new ArrayList<FileInfo>(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(

View File

@@ -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
* <b>sys:deleted</b> 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<StoreRef> 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)
{
}
}

View File

@@ -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<String> INBOUND_FIRST_ARG = new HashSet<String>(17);
private static final Set<String> INBOUND_SECOND_ARG = new HashSet<String>(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<Boolean> deleting = new ThreadLocal<Boolean>();
/** 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 <b>sys:deleted</b> 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 <tt>true</tt> if the node has the <b>sys:deleted</b> 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 <tt>true</tt> 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<BackgroundDeleteRunner> getDeleteWorkers()
{
@SuppressWarnings("unchecked")
List<BackgroundDeleteRunner> deleteRunners =
(List<BackgroundDeleteRunner>) AlfrescoTransactionSupport.getResource(KEY_DELETE_WORKERS);
if (deleteRunners == null)
{
// It is not bound, yet
deleteRunners = new ArrayList<BackgroundDeleteRunner>(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<BackgroundDeleteRunner> 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<Object> deleteTxnCallback = new RetryingTransactionCallback<Object>()
{
public Object execute() throws Throwable
{
// Determine if the execution should proceed
RunAsWork<Boolean> getContinueAuthCallback = new RunAsWork<Boolean>()
{
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<String> getUserAuthCallback = new RunAsWork<String>()
{
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<Object> deleteAuthCallback = new RunAsWork<Object>()
{
/**
* 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<ChildAssociationRef> 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<Object> callback = new RetryingTransactionCallback<Object>()
{
public Object execute() throws Throwable
{
RunAsWork<Object> authCallback = new RunAsWork<Object>()
{
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);
}
}
}
}

View File

@@ -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<QName, Serializable> 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<QName, Serializable> propertiesBefore = getPropertiesImpl(node);
Map<QName, Serializable> 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<QName, Serializable> 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);
}
}

View File

@@ -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);

View File

@@ -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.
* <p>
* 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 <tt>true</tt> if the node was completely removed, otherwise
* <tt>false</tt> 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.