mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-07-24 17:32:48 +00:00
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:
@@ -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");
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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(
|
||||
|
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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.
|
||||
|
Reference in New Issue
Block a user