From 31541e5409f449a4da65efd7fb5f4a4ebfa2239f Mon Sep 17 00:00:00 2001 From: Derek Hulley Date: Mon, 27 Apr 2009 13:03:18 +0000 Subject: [PATCH] Finished (with hacks) MOB-30: Purge Deleted Content - Only for use with RM use-cases - Switch on with property: system.content.eagerOrphanCleanup=true git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@14088 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- config/alfresco/content-services-context.xml | 26 +- config/alfresco/repository.properties | 5 + .../repo/content/ContentServiceImpl.java | 664 ++++++++++++++++++ .../repo/content/ContentServicePolicies.java | 36 +- .../repo/content/RoutingContentService.java | 585 +-------------- .../content/cleanup/ContentStoreCleaner.java | 316 ++++++++- .../cleanup/ContentStoreCleanerListener.java | 18 +- .../ContentStoreCleanerScalabilityRunner.java | 8 +- .../cleanup/ContentStoreCleanerTest.java | 191 ++++- .../DeletedContentBackupCleanerListener.java | 17 +- 10 files changed, 1248 insertions(+), 618 deletions(-) create mode 100644 source/java/org/alfresco/repo/content/ContentServiceImpl.java diff --git a/config/alfresco/content-services-context.xml b/config/alfresco/content-services-context.xml index a1004c7b31..37e67b8875 100644 --- a/config/alfresco/content-services-context.xml +++ b/config/alfresco/content-services-context.xml @@ -22,12 +22,27 @@ + + + + + + + + + + + + + + + @@ -40,17 +55,18 @@ + + ${system.content.eagerOrphanCleanup} + 14 - - - + - + @@ -59,7 +75,7 @@ - + diff --git a/config/alfresco/repository.properties b/config/alfresco/repository.properties index 929aed7322..9968a40df3 100644 --- a/config/alfresco/repository.properties +++ b/config/alfresco/repository.properties @@ -129,6 +129,11 @@ system.cascadeDeleteInTransaction=true # 'propagateTimestamps' element in the dictionary definition. system.enableTimestampPropagation=false +# +# Decide if content should be removed from the system immediately after being orphaned. +# Do not change this unless you have examined the impact it has on your backup procedures. +system.content.eagerOrphanCleanup=false + # #################### # # Lucene configuration # # #################### # diff --git a/source/java/org/alfresco/repo/content/ContentServiceImpl.java b/source/java/org/alfresco/repo/content/ContentServiceImpl.java new file mode 100644 index 0000000000..9fb59350fd --- /dev/null +++ b/source/java/org/alfresco/repo/content/ContentServiceImpl.java @@ -0,0 +1,664 @@ +/* + * 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.content; + +import java.io.Serializable; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.i18n.I18NUtil; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.avm.AVMNodeConverter; +import org.alfresco.repo.content.ContentServicePolicies.OnContentPropertyUpdatePolicy; +import org.alfresco.repo.content.ContentServicePolicies.OnContentReadPolicy; +import org.alfresco.repo.content.ContentServicePolicies.OnContentUpdatePolicy; +import org.alfresco.repo.content.filestore.FileContentStore; +import org.alfresco.repo.content.transform.ContentTransformer; +import org.alfresco.repo.content.transform.ContentTransformerRegistry; +import org.alfresco.repo.policy.ClassPolicyDelegate; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.service.cmr.avm.AVMService; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.InvalidTypeException; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentStreamListener; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NoTransformerException; +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.repository.TransformationOptions; +import org.alfresco.service.cmr.usage.ContentQuotaException; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.EqualsHelper; +import org.alfresco.util.Pair; +import org.alfresco.util.TempFileProvider; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ConfigurableApplicationContext; + + +/** + * Service implementation acting as a level of indirection between the client + * and the underlying content store. + *

+ * Note: This class was formerly the {@link RoutingContentService} but the + * 'routing' functionality has been pushed into the {@link AbstractRoutingContentStore store} + * implementations. + * + * @author Derek Hulley + * @since 3.2 + */ +public class ContentServiceImpl implements ContentService, ApplicationContextAware +{ + private static Log logger = LogFactory.getLog(ContentServiceImpl.class); + + private DictionaryService dictionaryService; + private NodeService nodeService; + private AVMService avmService; + private RetryingTransactionHelper transactionHelper; + private ApplicationContext applicationContext; + + /** a registry of all available content transformers */ + private ContentTransformerRegistry transformerRegistry; + /** the store to use. Any multi-store support is provided by the store implementation. */ + private ContentStore store; + /** the store for all temporarily created content */ + private ContentStore tempStore; + private ContentTransformer imageMagickContentTransformer; + + /** + * The policy component + */ + private PolicyComponent policyComponent; + + /* + * Policies delegates + */ + ClassPolicyDelegate onContentUpdateDelegate; + ClassPolicyDelegate onContentPropertyUpdateDelegate; + ClassPolicyDelegate onContentReadDelegate; + + /** + * @deprecated Replaced by {@link #setRetryingTransactionHelper(RetryingTransactionHelper)} + */ + public void setTransactionService(TransactionService transactionService) + { + logger.warn("Property 'transactionService' has been replaced by 'retryingTransactionHelper'."); + } + + public void setRetryingTransactionHelper(RetryingTransactionHelper helper) + { + this.transactionHelper = helper; + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setTransformerRegistry(ContentTransformerRegistry transformerRegistry) + { + this.transformerRegistry = transformerRegistry; + } + + public void setStore(ContentStore store) + { + this.store = store; + } + + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + public void setAvmService(AVMService service) + { + this.avmService = service; + } + + public void setImageMagickContentTransformer(ContentTransformer imageMagickContentTransformer) + { + this.imageMagickContentTransformer = imageMagickContentTransformer; + } + + + /* (non-Javadoc) + * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext) + */ + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException + { + this.applicationContext = applicationContext; + } + + /** + * Service initialise + */ + public void init() + { + // Set up a temporary store + this.tempStore = new FileContentStore((ConfigurableApplicationContext) this.applicationContext, + TempFileProvider.getTempDir().getAbsolutePath()); + + // Bind on update properties behaviour + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onUpdateProperties"), + this, + new JavaBehaviour(this, "onUpdateProperties")); + + // Register on content update policy + this.onContentUpdateDelegate = this.policyComponent.registerClassPolicy(OnContentUpdatePolicy.class); + this.onContentPropertyUpdateDelegate = this.policyComponent.registerClassPolicy(OnContentPropertyUpdatePolicy.class); + this.onContentReadDelegate = this.policyComponent.registerClassPolicy(OnContentReadPolicy.class); + } + + /** + * Update properties policy behaviour + * + * @param nodeRef the node reference + * @param before the before values of the properties + * @param after the after values of the properties + */ + public void onUpdateProperties( + NodeRef nodeRef, + Map before, + Map after) + { + // Don't duplicate work when firing multiple policies + Set types = null; + OnContentPropertyUpdatePolicy propertyPolicy = null; // Doesn't change for the node instance + // Variables to control firing of node-level policies (any content change) + boolean fire = false; + boolean isNewContent = false; + // check if any of the content properties have changed + for (QName propertyQName : after.keySet()) + { + // is this a content property? + PropertyDefinition propertyDef = dictionaryService.getProperty(propertyQName); + if (propertyDef == null) + { + // the property is not recognised + continue; + } + else if (!propertyDef.getDataType().getName().equals(DataTypeDefinition.CONTENT)) + { + // not a content type + continue; + } + else if (propertyDef.isMultiValued()) + { + // We don't fire notifications for multi-valued content properties + continue; + } + + try + { + ContentData beforeValue = (ContentData) before.get(propertyQName); + ContentData afterValue = (ContentData) after.get(propertyQName); + boolean hasContentBefore = ContentData.hasContent(beforeValue); + boolean hasContentAfter = ContentData.hasContent(afterValue); + + // There are some shortcuts here + if (!hasContentBefore && !hasContentAfter) + { + // Really, nothing happened + continue; + } + else if (EqualsHelper.nullSafeEquals(beforeValue, afterValue)) + { + // Still, nothing happening + continue; + } + + // Check for new content + isNewContent = !hasContentBefore && hasContentAfter; + + // So debug ... + if (logger.isDebugEnabled()) + { + String name = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_NAME); + logger.debug( + "Content property updated: \n" + + " Node Name: " + name + "\n" + + " Property: " + propertyQName + "\n" + + " Is new: " + isNewContent + "\n" + + " Before: " + beforeValue + "\n" + + " After: " + afterValue); + } + + // Fire specific policy + types = getTypes(nodeRef, types); + if (propertyPolicy == null) + { + propertyPolicy = onContentPropertyUpdateDelegate.get(nodeRef, types); + } + propertyPolicy.onContentPropertyUpdate(nodeRef, propertyQName, beforeValue, afterValue); + + // We also fire an event if *any* content property is changed + fire = true; + } + catch (ClassCastException e) + { + // properties don't conform to model + continue; + } + } + // fire? + if (fire) + { + // Fire the content update policy + types = getTypes(nodeRef, types); + OnContentUpdatePolicy policy = onContentUpdateDelegate.get(nodeRef, types); + policy.onContentUpdate(nodeRef, isNewContent); + } + } + + /** + * Helper method to lazily populate the types associated with a node + * + * @param nodeRef the node + * @param types any existing types + * @return the types - either newly populated or just what was passed in + */ + private Set getTypes(NodeRef nodeRef, Set types) + { + if (types != null) + { + return types; + } + types = new HashSet(this.nodeService.getAspects(nodeRef)); + types.add(this.nodeService.getType(nodeRef)); + return types; + } + + /** {@inheritDoc} */ + public ContentReader getRawReader(String contentUrl) + { + ContentReader reader = null; + try + { + reader = store.getReader(contentUrl); + } + catch (UnsupportedContentUrlException e) + { + // The URL is not supported, so we spoof it + reader = new EmptyContentReader(contentUrl); + } + if (reader == null) + { + throw new AlfrescoRuntimeException("ContentStore implementations may not return null ContentReaders"); + } + // set extra data on the reader + reader.setMimetype(MimetypeMap.MIMETYPE_BINARY); + reader.setEncoding("UTF-8"); + reader.setLocale(I18NUtil.getLocale()); + + // Done + if (logger.isDebugEnabled()) + { + logger.debug( + "Direct request for reader: \n" + + " Content URL: " + contentUrl + "\n" + + " Reader: " + reader); + } + return reader; + } + + public ContentReader getReader(NodeRef nodeRef, QName propertyQName) + { + return getReader(nodeRef, propertyQName, true); + } + + @SuppressWarnings("unchecked") + private ContentReader getReader(NodeRef nodeRef, QName propertyQName, boolean fireContentReadPolicy) + { + ContentData contentData = null; + Serializable propValue = nodeService.getProperty(nodeRef, propertyQName); + if (propValue instanceof Collection) + { + Collection colPropValue = (Collection)propValue; + if (colPropValue.size() > 0) + { + propValue = colPropValue.iterator().next(); + } + } + + if (propValue instanceof ContentData) + { + contentData = (ContentData)propValue; + } + + if (contentData == null) + { + PropertyDefinition contentPropDef = dictionaryService.getProperty(propertyQName); + + // if no value or a value other content, and a property definition has been provided, ensure that it's CONTENT or ANY + if (contentPropDef != null && + (!(contentPropDef.getDataType().getName().equals(DataTypeDefinition.CONTENT) || + contentPropDef.getDataType().getName().equals(DataTypeDefinition.ANY)))) + { + throw new InvalidTypeException("The node property must be of type content: \n" + + " node: " + nodeRef + "\n" + + " property name: " + propertyQName + "\n" + + " property type: " + ((contentPropDef == null) ? "unknown" : contentPropDef.getDataType()), + propertyQName); + } + } + + // check that the URL is available + if (contentData == null || contentData.getContentUrl() == null) + { + // there is no URL - the interface specifies that this is not an error condition + return null; + } + String contentUrl = contentData.getContentUrl(); + + // The context of the read is entirely described by the URL + ContentReader reader = store.getReader(contentUrl); + if (reader == null) + { + throw new AlfrescoRuntimeException("ContentStore implementations may not return null ContentReaders"); + } + + // set extra data on the reader + reader.setMimetype(contentData.getMimetype()); + reader.setEncoding(contentData.getEncoding()); + reader.setLocale(contentData.getLocale()); + + // Fire the content read policy + if (reader != null && fireContentReadPolicy == true) + { + // Fire the content update policy + Set types = new HashSet(this.nodeService.getAspects(nodeRef)); + types.add(this.nodeService.getType(nodeRef)); + OnContentReadPolicy policy = this.onContentReadDelegate.get(nodeRef, types); + policy.onContentRead(nodeRef); + } + + // we don't listen for anything + // result may be null - but interface contract says we may return null + return reader; + } + + public ContentWriter getWriter(NodeRef nodeRef, QName propertyQName, boolean update) + { + if (nodeRef == null) + { + ContentContext ctx = new ContentContext(null, null); + // for this case, we just give back a valid URL into the content store + ContentWriter writer = store.getWriter(ctx); + // done + return writer; + } + + // check for an existing URL - the get of the reader will perform type checking + ContentReader existingContentReader = getReader(nodeRef, propertyQName, false); + + // get the content using the (potentially) existing content - the new content + // can be wherever the store decides. + ContentContext ctx = new NodeContentContext(existingContentReader, null, nodeRef, propertyQName); + ContentWriter writer = store.getWriter(ctx); + + // Special case for AVM repository. + Serializable contentValue = null; + if (nodeRef.getStoreRef().getProtocol().equals(StoreRef.PROTOCOL_AVM)) + { + Pair avmVersionPath = AVMNodeConverter.ToAVMVersionPath(nodeRef); + contentValue = avmService.getContentDataForWrite(avmVersionPath.getSecond()); + } + else + { + contentValue = nodeService.getProperty(nodeRef, propertyQName); + } + + // set extra data on the reader if the property is pre-existing + if (contentValue != null && contentValue instanceof ContentData) + { + ContentData contentData = (ContentData)contentValue; + writer.setMimetype(contentData.getMimetype()); + writer.setEncoding(contentData.getEncoding()); + writer.setLocale(contentData.getLocale()); + } + + // attach a listener if required + if (update) + { + // need a listener to update the node when the stream closes + WriteStreamListener listener = new WriteStreamListener(nodeService, nodeRef, propertyQName, writer); + writer.addListener(listener); + writer.setRetryingTransactionHelper(transactionHelper); + } + + // give back to the client + return writer; + } + + /** + * @return Returns a writer to an anonymous location + */ + public ContentWriter getTempWriter() + { + // there is no existing content and we don't specify the location of the new content + return tempStore.getWriter(ContentContext.NULL_CONTEXT); + } + + /** + * @see org.alfresco.repo.content.transform.ContentTransformerRegistry + * @see org.alfresco.repo.content.transform.ContentTransformer + * @see org.alfresco.service.cmr.repository.ContentService#transform(org.alfresco.service.cmr.repository.ContentReader, org.alfresco.service.cmr.repository.ContentWriter) + */ + public void transform(ContentReader reader, ContentWriter writer) + { + // Call transform with no options + TransformationOptions options = null; + this.transform(reader, writer, options); + } + + /** + * @see org.alfresco.repo.content.transform.ContentTransformerRegistry + * @see org.alfresco.repo.content.transform.ContentTransformer + * @deprecated + */ + public void transform(ContentReader reader, ContentWriter writer, Map options) + throws NoTransformerException, ContentIOException + { + transform(reader, writer, new TransformationOptions(options)); + } + + /** + * @see org.alfresco.repo.content.transform.ContentTransformerRegistry + * @see org.alfresco.repo.content.transform.ContentTransformer + */ + public void transform(ContentReader reader, ContentWriter writer, TransformationOptions options) + throws NoTransformerException, ContentIOException + { + // check that source and target mimetypes are available + String sourceMimetype = reader.getMimetype(); + if (sourceMimetype == null) + { + throw new AlfrescoRuntimeException("The content reader mimetype must be set: " + reader); + } + String targetMimetype = writer.getMimetype(); + if (targetMimetype == null) + { + throw new AlfrescoRuntimeException("The content writer mimetype must be set: " + writer); + } + // look for a transformer + ContentTransformer transformer = transformerRegistry.getTransformer(sourceMimetype, targetMimetype, options); + if (transformer == null) + { + throw new NoTransformerException(sourceMimetype, targetMimetype); + } + // we have a transformer, so do it + transformer.transform(reader, writer, options); + // done + } + + /** + * @see org.alfresco.repo.content.transform.ContentTransformerRegistry + * @see org.alfresco.repo.content.transform.ContentTransformer + */ + public ContentTransformer getTransformer(String sourceMimetype, String targetMimetype) + { + return getTransformer(sourceMimetype, targetMimetype, new TransformationOptions()); + } + + /** + * @see org.alfresco.service.cmr.repository.ContentService#getTransformer(java.lang.String, java.lang.String, org.alfresco.service.cmr.repository.TransformationOptions) + */ + public ContentTransformer getTransformer(String sourceMimetype, String targetMimetype, TransformationOptions options) + { + return transformerRegistry.getTransformer(sourceMimetype, targetMimetype, options); + } + + /** + * @see org.alfresco.service.cmr.repository.ContentService#getImageTransformer() + */ + public ContentTransformer getImageTransformer() + { + return imageMagickContentTransformer; + } + + /** + * @see org.alfresco.repo.content.transform.ContentTransformerRegistry + * @see org.alfresco.repo.content.transform.ContentTransformer + */ + public boolean isTransformable(ContentReader reader, ContentWriter writer) + { + return isTransformable(reader, writer, new TransformationOptions()); + } + + /** + * @see org.alfresco.service.cmr.repository.ContentService#isTransformable(org.alfresco.service.cmr.repository.ContentReader, org.alfresco.service.cmr.repository.ContentWriter, org.alfresco.service.cmr.repository.TransformationOptions) + */ + public boolean isTransformable(ContentReader reader, ContentWriter writer, TransformationOptions options) + { + // check that source and target mimetypes are available + String sourceMimetype = reader.getMimetype(); + if (sourceMimetype == null) + { + throw new AlfrescoRuntimeException("The content reader mimetype must be set: " + reader); + } + String targetMimetype = writer.getMimetype(); + if (targetMimetype == null) + { + throw new AlfrescoRuntimeException("The content writer mimetype must be set: " + writer); + } + + // look for a transformer + ContentTransformer transformer = transformerRegistry.getTransformer(sourceMimetype, targetMimetype, options); + return (transformer != null); + } + + /** + * Ensures that, upon closure of the output stream, the node is updated with + * the latest URL of the content to which it refers. + *

+ * The listener close operation does not need a transaction as the + * ContentWriter takes care of that. + * + * @author Derek Hulley + */ + private static class WriteStreamListener implements ContentStreamListener + { + private NodeService nodeService; + private NodeRef nodeRef; + private QName propertyQName; + private ContentWriter writer; + + public WriteStreamListener( + NodeService nodeService, + NodeRef nodeRef, + QName propertyQName, + ContentWriter writer) + { + this.nodeService = nodeService; + this.nodeRef = nodeRef; + this.propertyQName = propertyQName; + this.writer = writer; + } + + public void contentStreamClosed() throws ContentIOException + { + try + { + // set the full content property + ContentData contentData = writer.getContentData(); + // Bypass NodeService for avm stores. + if (nodeRef.getStoreRef().getProtocol().equals(StoreRef.PROTOCOL_AVM)) + { + nodeService.setProperty(nodeRef, ContentModel.PROP_CONTENT, contentData); + } + else + { + nodeService.setProperty( + nodeRef, + propertyQName, + contentData); + } + // done + if (logger.isDebugEnabled()) + { + logger.debug("Stream listener updated node: \n" + + " node: " + nodeRef + "\n" + + " property: " + propertyQName + "\n" + + " value: " + contentData); + } + } + catch (ContentQuotaException qe) + { + throw qe; + } + catch (Throwable e) + { + throw new ContentIOException("Failed to set content property on stream closure: \n" + + " node: " + nodeRef + "\n" + + " property: " + propertyQName + "\n" + + " writer: " + writer, + e); + } + } + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/content/ContentServicePolicies.java b/source/java/org/alfresco/repo/content/ContentServicePolicies.java index a635ef5b58..578ba79c68 100644 --- a/source/java/org/alfresco/repo/content/ContentServicePolicies.java +++ b/source/java/org/alfresco/repo/content/ContentServicePolicies.java @@ -25,6 +25,7 @@ package org.alfresco.repo.content; import org.alfresco.repo.policy.ClassPolicy; +import org.alfresco.service.cmr.repository.ContentData; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; @@ -33,15 +34,19 @@ import org.alfresco.service.namespace.QName; * Content service policies interface * * @author Roy Wetherall + * @author Derek Hulley */ public interface ContentServicePolicies { /** The QName's of the policies */ public static final QName ON_CONTENT_UPDATE = QName.createQName(NamespaceService.ALFRESCO_URI, "onContentUpdate"); + public static final QName ON_CONTENT_PROPERTY_UPDATE = QName.createQName(NamespaceService.ALFRESCO_URI, "onContentPropertyUpdate"); public static final QName ON_CONTENT_READ = QName.createQName(NamespaceService.ALFRESCO_URI, "onContentRead"); /** - * On content update policy interface + * Policy that is raised once per node when any of the content properties on the node are + * changed; the specific properties are irrelevant. This is primarily useful to determine + * when a new file is introduced into the system. */ public interface OnContentUpdatePolicy extends ClassPolicy { @@ -51,6 +56,35 @@ public interface ContentServicePolicies public void onContentUpdate(NodeRef nodeRef, boolean newContent); } + /** + * Policy that is raised for each content property change. Any policy implementations must be aware + * that the transaction in which this is called could still roll back; no filesystem changes should + * occur against the source content until after the transaction has successfully completed. + * + * @since 3.2 + */ + public interface OnContentPropertyUpdatePolicy extends ClassPolicy + { + /** + * @param nodeRef the node reference + * @param propertyQName the name of the property that changed + * @param beforeValue the value of the content data prior to the change. + * Note that this value may be null or any of it's member + * values may be null according to the contract of the + * {@link ContentData} class. + * @param afterValue the value of the content data after the change + * + * @see ContentData#hasContent(ContentData) + * @see RoutingContentService#onUpdateProperties(NodeRef, java.util.Map, java.util.Map) + * @since 3.2 + */ + public void onContentPropertyUpdate( + NodeRef nodeRef, + QName propertyQName, + ContentData beforeValue, + ContentData afterValue); + } + /** * On content read policy interface. * diff --git a/source/java/org/alfresco/repo/content/RoutingContentService.java b/source/java/org/alfresco/repo/content/RoutingContentService.java index 138bb6546b..f6b6b9dd0c 100644 --- a/source/java/org/alfresco/repo/content/RoutingContentService.java +++ b/source/java/org/alfresco/repo/content/RoutingContentService.java @@ -24,599 +24,22 @@ */ package org.alfresco.repo.content; -import java.io.Serializable; -import java.util.Collection; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import org.alfresco.error.AlfrescoRuntimeException; -import org.alfresco.i18n.I18NUtil; -import org.alfresco.model.ContentModel; -import org.alfresco.repo.avm.AVMNodeConverter; -import org.alfresco.repo.content.ContentServicePolicies.OnContentReadPolicy; -import org.alfresco.repo.content.ContentServicePolicies.OnContentUpdatePolicy; -import org.alfresco.repo.content.filestore.FileContentStore; -import org.alfresco.repo.content.transform.ContentTransformer; -import org.alfresco.repo.content.transform.ContentTransformerRegistry; -import org.alfresco.repo.policy.ClassPolicyDelegate; -import org.alfresco.repo.policy.JavaBehaviour; -import org.alfresco.repo.policy.PolicyComponent; -import org.alfresco.repo.transaction.RetryingTransactionHelper; -import org.alfresco.service.cmr.avm.AVMService; -import org.alfresco.service.cmr.dictionary.DataTypeDefinition; -import org.alfresco.service.cmr.dictionary.DictionaryService; -import org.alfresco.service.cmr.dictionary.InvalidTypeException; -import org.alfresco.service.cmr.dictionary.PropertyDefinition; -import org.alfresco.service.cmr.repository.ContentData; -import org.alfresco.service.cmr.repository.ContentIOException; -import org.alfresco.service.cmr.repository.ContentReader; -import org.alfresco.service.cmr.repository.ContentService; -import org.alfresco.service.cmr.repository.ContentStreamListener; -import org.alfresco.service.cmr.repository.ContentWriter; -import org.alfresco.service.cmr.repository.NoTransformerException; -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.repository.TransformationOptions; -import org.alfresco.service.cmr.usage.ContentQuotaException; -import org.alfresco.service.namespace.NamespaceService; -import org.alfresco.service.namespace.QName; -import org.alfresco.service.transaction.TransactionService; -import org.alfresco.util.EqualsHelper; -import org.alfresco.util.Pair; -import org.alfresco.util.TempFileProvider; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.beans.BeansException; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.context.ConfigurableApplicationContext; - /** * A content service that determines at runtime the store that the * content associated with a node should be routed to. * + * @deprecated Replaced by {@link ContentServiceImpl} * @author Derek Hulley */ -public class RoutingContentService implements ContentService, ApplicationContextAware +public class RoutingContentService extends ContentServiceImpl { private static Log logger = LogFactory.getLog(RoutingContentService.class); - private DictionaryService dictionaryService; - private NodeService nodeService; - private AVMService avmService; - private RetryingTransactionHelper transactionHelper; - private ApplicationContext applicationContext; - - /** a registry of all available content transformers */ - private ContentTransformerRegistry transformerRegistry; - /** TEMPORARY until we have a map to choose from at runtime */ - private ContentStore store; - /** the store for all temporarily created content */ - private ContentStore tempStore; - private ContentTransformer imageMagickContentTransformer; - - /** - * The policy component - */ - private PolicyComponent policyComponent; - - /** - * Policies delegate - */ - ClassPolicyDelegate onContentUpdateDelegate; - ClassPolicyDelegate onContentReadDelegate; - - /** - * @deprecated Replaced by {@link #setRetryingTransactionHelper(RetryingTransactionHelper)} - */ - public void setTransactionService(TransactionService transactionService) + public RoutingContentService() { - logger.warn("Property 'transactionService' has been replaced by 'retryingTransactionHelper'."); - } - - public void setRetryingTransactionHelper(RetryingTransactionHelper helper) - { - this.transactionHelper = helper; - } - - public void setDictionaryService(DictionaryService dictionaryService) - { - this.dictionaryService = dictionaryService; - } - - public void setNodeService(NodeService nodeService) - { - this.nodeService = nodeService; - } - - public void setTransformerRegistry(ContentTransformerRegistry transformerRegistry) - { - this.transformerRegistry = transformerRegistry; - } - - public void setStore(ContentStore store) - { - this.store = store; - } - - public void setPolicyComponent(PolicyComponent policyComponent) - { - this.policyComponent = policyComponent; - } - - public void setAvmService(AVMService service) - { - this.avmService = service; - } - - public void setImageMagickContentTransformer(ContentTransformer imageMagickContentTransformer) - { - this.imageMagickContentTransformer = imageMagickContentTransformer; - } - - - /* (non-Javadoc) - * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext) - */ - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException - { - this.applicationContext = applicationContext; - } - - /** - * Service initialise - */ - public void init() - { - // Set up a temporary store - this.tempStore = new FileContentStore((ConfigurableApplicationContext) this.applicationContext, - TempFileProvider.getTempDir().getAbsolutePath()); - - // Bind on update properties behaviour - this.policyComponent.bindClassBehaviour( - QName.createQName(NamespaceService.ALFRESCO_URI, "onUpdateProperties"), - this, - new JavaBehaviour(this, "onUpdateProperties")); - - // Register on content update policy - this.onContentUpdateDelegate = this.policyComponent.registerClassPolicy(OnContentUpdatePolicy.class); - this.onContentReadDelegate = this.policyComponent.registerClassPolicy(OnContentReadPolicy.class); - } - - /** - * Update properties policy behaviour - * - * @param nodeRef the node reference - * @param before the before values of the properties - * @param after the after values of the properties - */ - public void onUpdateProperties( - NodeRef nodeRef, - Map before, - Map after) - { - boolean fire = false; - boolean isNewContent = false; - // check if any of the content properties have changed - for (QName propertyQName : after.keySet()) - { - // is this a content property? - PropertyDefinition propertyDef = dictionaryService.getProperty(propertyQName); - if (propertyDef == null) - { - // the property is not recognised - continue; - } - if (!propertyDef.getDataType().getName().equals(DataTypeDefinition.CONTENT)) - { - // not a content type - continue; - } - - try - { - ContentData beforeValue = (ContentData) before.get(propertyQName); - ContentData afterValue = (ContentData) after.get(propertyQName); - boolean hasContentBefore = ContentData.hasContent(beforeValue); - boolean hasContentAfter = ContentData.hasContent(afterValue); - - // There are some shortcuts here - if (!hasContentBefore && !hasContentAfter) - { - // Really, nothing happened - continue; - } - else if (EqualsHelper.nullSafeEquals(beforeValue, afterValue)) - { - // Still, nothing happening - continue; - } - - // Check for new content - isNewContent = !hasContentBefore && hasContentAfter; - - // So debug ... - if (logger.isDebugEnabled() == true) - { - String name = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_NAME); - logger.debug( - "onContentUpdate will fire: \n" + - " Name: " + name + "\n" + - " Is new: " + isNewContent + "\n" + - " Before: " + beforeValue + "\n" + - " After: " + afterValue); - } - - // We are interested in any content change - fire = true; - break; - } - catch (ClassCastException e) - { - // properties don't conform to model - continue; - } - } - // fire? - if (fire) - { - // Fire the content update policy - Set types = new HashSet(this.nodeService.getAspects(nodeRef)); - types.add(this.nodeService.getType(nodeRef)); - OnContentUpdatePolicy policy = this.onContentUpdateDelegate.get(nodeRef, types); - policy.onContentUpdate(nodeRef, isNewContent); - } - } - - /** {@inheritDoc} */ - public ContentReader getRawReader(String contentUrl) - { - ContentReader reader = null; - try - { - reader = store.getReader(contentUrl); - } - catch (UnsupportedContentUrlException e) - { - // The URL is not supported, so we spoof it - reader = new EmptyContentReader(contentUrl); - } - if (reader == null) - { - throw new AlfrescoRuntimeException("ContentStore implementations may not return null ContentReaders"); - } - // set extra data on the reader - reader.setMimetype(MimetypeMap.MIMETYPE_BINARY); - reader.setEncoding("UTF-8"); - reader.setLocale(I18NUtil.getLocale()); - - // Done - if (logger.isDebugEnabled()) - { - logger.debug( - "Direct request for reader: \n" + - " Content URL: " + contentUrl + "\n" + - " Reader: " + reader); - } - return reader; - } - - public ContentReader getReader(NodeRef nodeRef, QName propertyQName) - { - return getReader(nodeRef, propertyQName, true); - } - - @SuppressWarnings("unchecked") - private ContentReader getReader(NodeRef nodeRef, QName propertyQName, boolean fireContentReadPolicy) - { - ContentData contentData = null; - Serializable propValue = nodeService.getProperty(nodeRef, propertyQName); - if (propValue instanceof Collection) - { - Collection colPropValue = (Collection)propValue; - if (colPropValue.size() > 0) - { - propValue = colPropValue.iterator().next(); - } - } - - if (propValue instanceof ContentData) - { - contentData = (ContentData)propValue; - } - - if (contentData == null) - { - PropertyDefinition contentPropDef = dictionaryService.getProperty(propertyQName); - - // if no value or a value other content, and a property definition has been provided, ensure that it's CONTENT or ANY - if (contentPropDef != null && - (!(contentPropDef.getDataType().getName().equals(DataTypeDefinition.CONTENT) || - contentPropDef.getDataType().getName().equals(DataTypeDefinition.ANY)))) - { - throw new InvalidTypeException("The node property must be of type content: \n" + - " node: " + nodeRef + "\n" + - " property name: " + propertyQName + "\n" + - " property type: " + ((contentPropDef == null) ? "unknown" : contentPropDef.getDataType()), - propertyQName); - } - } - - // check that the URL is available - if (contentData == null || contentData.getContentUrl() == null) - { - // there is no URL - the interface specifies that this is not an error condition - return null; - } - String contentUrl = contentData.getContentUrl(); - - // The context of the read is entirely described by the URL - ContentReader reader = store.getReader(contentUrl); - if (reader == null) - { - throw new AlfrescoRuntimeException("ContentStore implementations may not return null ContentReaders"); - } - - // set extra data on the reader - reader.setMimetype(contentData.getMimetype()); - reader.setEncoding(contentData.getEncoding()); - reader.setLocale(contentData.getLocale()); - - // Fire the content read policy - if (reader != null && fireContentReadPolicy == true) - { - // Fire the content update policy - Set types = new HashSet(this.nodeService.getAspects(nodeRef)); - types.add(this.nodeService.getType(nodeRef)); - OnContentReadPolicy policy = this.onContentReadDelegate.get(nodeRef, types); - policy.onContentRead(nodeRef); - } - - // we don't listen for anything - // result may be null - but interface contract says we may return null - return reader; - } - - public ContentWriter getWriter(NodeRef nodeRef, QName propertyQName, boolean update) - { - if (nodeRef == null) - { - ContentContext ctx = new ContentContext(null, null); - // for this case, we just give back a valid URL into the content store - ContentWriter writer = store.getWriter(ctx); - // done - return writer; - } - - // check for an existing URL - the get of the reader will perform type checking - ContentReader existingContentReader = getReader(nodeRef, propertyQName, false); - - // get the content using the (potentially) existing content - the new content - // can be wherever the store decides. - ContentContext ctx = new NodeContentContext(existingContentReader, null, nodeRef, propertyQName); - ContentWriter writer = store.getWriter(ctx); - - // Special case for AVM repository. - Serializable contentValue = null; - if (nodeRef.getStoreRef().getProtocol().equals(StoreRef.PROTOCOL_AVM)) - { - Pair avmVersionPath = AVMNodeConverter.ToAVMVersionPath(nodeRef); - contentValue = avmService.getContentDataForWrite(avmVersionPath.getSecond()); - } - else - { - contentValue = nodeService.getProperty(nodeRef, propertyQName); - } - - // set extra data on the reader if the property is pre-existing - if (contentValue != null && contentValue instanceof ContentData) - { - ContentData contentData = (ContentData)contentValue; - writer.setMimetype(contentData.getMimetype()); - writer.setEncoding(contentData.getEncoding()); - writer.setLocale(contentData.getLocale()); - } - - // attach a listener if required - if (update) - { - // need a listener to update the node when the stream closes - WriteStreamListener listener = new WriteStreamListener(nodeService, nodeRef, propertyQName, writer); - writer.addListener(listener); - writer.setRetryingTransactionHelper(transactionHelper); - } - - // give back to the client - return writer; - } - - /** - * @return Returns a writer to an anonymous location - */ - public ContentWriter getTempWriter() - { - // there is no existing content and we don't specify the location of the new content - return tempStore.getWriter(ContentContext.NULL_CONTEXT); - } - - /** - * @see org.alfresco.repo.content.transform.ContentTransformerRegistry - * @see org.alfresco.repo.content.transform.ContentTransformer - * @see org.alfresco.service.cmr.repository.ContentService#transform(org.alfresco.service.cmr.repository.ContentReader, org.alfresco.service.cmr.repository.ContentWriter) - */ - public void transform(ContentReader reader, ContentWriter writer) - { - // Call transform with no options - TransformationOptions options = null; - this.transform(reader, writer, options); - } - - /** - * @see org.alfresco.repo.content.transform.ContentTransformerRegistry - * @see org.alfresco.repo.content.transform.ContentTransformer - * @deprecated - */ - public void transform(ContentReader reader, ContentWriter writer, Map options) - throws NoTransformerException, ContentIOException - { - transform(reader, writer, new TransformationOptions(options)); - } - - /** - * @see org.alfresco.repo.content.transform.ContentTransformerRegistry - * @see org.alfresco.repo.content.transform.ContentTransformer - */ - public void transform(ContentReader reader, ContentWriter writer, TransformationOptions options) - throws NoTransformerException, ContentIOException - { - // check that source and target mimetypes are available - String sourceMimetype = reader.getMimetype(); - if (sourceMimetype == null) - { - throw new AlfrescoRuntimeException("The content reader mimetype must be set: " + reader); - } - String targetMimetype = writer.getMimetype(); - if (targetMimetype == null) - { - throw new AlfrescoRuntimeException("The content writer mimetype must be set: " + writer); - } - // look for a transformer - ContentTransformer transformer = transformerRegistry.getTransformer(sourceMimetype, targetMimetype, options); - if (transformer == null) - { - throw new NoTransformerException(sourceMimetype, targetMimetype); - } - // we have a transformer, so do it - transformer.transform(reader, writer, options); - // done - } - - /** - * @see org.alfresco.repo.content.transform.ContentTransformerRegistry - * @see org.alfresco.repo.content.transform.ContentTransformer - */ - public ContentTransformer getTransformer(String sourceMimetype, String targetMimetype) - { - return getTransformer(sourceMimetype, targetMimetype, new TransformationOptions()); - } - - /** - * @see org.alfresco.service.cmr.repository.ContentService#getTransformer(java.lang.String, java.lang.String, org.alfresco.service.cmr.repository.TransformationOptions) - */ - public ContentTransformer getTransformer(String sourceMimetype, String targetMimetype, TransformationOptions options) - { - return transformerRegistry.getTransformer(sourceMimetype, targetMimetype, options); - } - - /** - * @see org.alfresco.service.cmr.repository.ContentService#getImageTransformer() - */ - public ContentTransformer getImageTransformer() - { - return imageMagickContentTransformer; - } - - /** - * @see org.alfresco.repo.content.transform.ContentTransformerRegistry - * @see org.alfresco.repo.content.transform.ContentTransformer - */ - public boolean isTransformable(ContentReader reader, ContentWriter writer) - { - return isTransformable(reader, writer, new TransformationOptions()); - } - - /** - * @see org.alfresco.service.cmr.repository.ContentService#isTransformable(org.alfresco.service.cmr.repository.ContentReader, org.alfresco.service.cmr.repository.ContentWriter, org.alfresco.service.cmr.repository.TransformationOptions) - */ - public boolean isTransformable(ContentReader reader, ContentWriter writer, TransformationOptions options) - { - // check that source and target mimetypes are available - String sourceMimetype = reader.getMimetype(); - if (sourceMimetype == null) - { - throw new AlfrescoRuntimeException("The content reader mimetype must be set: " + reader); - } - String targetMimetype = writer.getMimetype(); - if (targetMimetype == null) - { - throw new AlfrescoRuntimeException("The content writer mimetype must be set: " + writer); - } - - // look for a transformer - ContentTransformer transformer = transformerRegistry.getTransformer(sourceMimetype, targetMimetype, options); - return (transformer != null); - } - - /** - * Ensures that, upon closure of the output stream, the node is updated with - * the latest URL of the content to which it refers. - *

- * The listener close operation does not need a transaction as the - * ContentWriter takes care of that. - * - * @author Derek Hulley - */ - private static class WriteStreamListener implements ContentStreamListener - { - private NodeService nodeService; - private NodeRef nodeRef; - private QName propertyQName; - private ContentWriter writer; - - public WriteStreamListener( - NodeService nodeService, - NodeRef nodeRef, - QName propertyQName, - ContentWriter writer) - { - this.nodeService = nodeService; - this.nodeRef = nodeRef; - this.propertyQName = propertyQName; - this.writer = writer; - } - - public void contentStreamClosed() throws ContentIOException - { - try - { - // set the full content property - ContentData contentData = writer.getContentData(); - // Bypass NodeService for avm stores. - if (nodeRef.getStoreRef().getProtocol().equals(StoreRef.PROTOCOL_AVM)) - { - nodeService.setProperty(nodeRef, ContentModel.PROP_CONTENT, contentData); - } - else - { - nodeService.setProperty( - nodeRef, - propertyQName, - contentData); - } - // done - if (logger.isDebugEnabled()) - { - logger.debug("Stream listener updated node: \n" + - " node: " + nodeRef + "\n" + - " property: " + propertyQName + "\n" + - " value: " + contentData); - } - } - catch (ContentQuotaException qe) - { - throw qe; - } - catch (Throwable e) - { - throw new ContentIOException("Failed to set content property on stream closure: \n" + - " node: " + nodeRef + "\n" + - " property: " + propertyQName + "\n" + - " writer: " + writer, - e); - } - } + logger.warn("Class 'RoutingContentService' has been deprecated and replaced by 'ContentServiceImpl'."); } } \ No newline at end of file diff --git a/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleaner.java b/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleaner.java index 6b64ebd99a..a5ac6a7c79 100644 --- a/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleaner.java +++ b/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleaner.java @@ -28,50 +28,110 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Map; +import java.util.Set; import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; import org.alfresco.repo.avm.AVMNodeDAO; import org.alfresco.repo.avm.AVMNodeDAO.ContentUrlHandler; +import org.alfresco.repo.content.ContentServicePolicies; import org.alfresco.repo.content.ContentStore; +import org.alfresco.repo.copy.CopyServicePolicies; import org.alfresco.repo.domain.ContentUrlDAO; +import org.alfresco.repo.node.NodeServicePolicies; import org.alfresco.repo.node.db.NodeDaoService; import org.alfresco.repo.node.db.NodeDaoService.NodePropertyHandler; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.TransactionListenerAdapter; +import org.alfresco.repo.transaction.TransactionalResourceHelper; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.cmr.dictionary.DataTypeDefinition; import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.repository.ContentData; -import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.Pair; import org.alfresco.util.PropertyCheck; import org.alfresco.util.VmShutdownListener; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** - * This component is responsible for finding orphaned content in a given - * content store or stores. Deletion handlers can be provided to ensure - * that the content is moved to another location prior to being removed - * from the store(s) being cleaned. + * This component is responsible cleaning up orphaned content. + *

+ * Clean-up happens at two levels.

+ * Eager cleanup: (since 3.2)

+ * If {@link #setEagerOrphanCleanup(boolean) eager cleanup} is activated, then this + * component listens to all content property change events and recorded for post-transaction + * processing. All orphaned content is deleted from the registered store(s). Note that + * any {@link #setListeners(List) listeners} are called as normal; backup or scrubbing + * procedures should be plugged in as listeners if this is required. + *

+ * Lazy cleanup:

+ * This is triggered by means of a {@link ContentStoreCleanupJob Quartz job}. This is + * a heavy-weight process that effectively compares the database metadata with the + * content URLs controlled by the various stores. Once again, the listeners are called + * appropriately. + *

+ * How backup policies are affected:

+ * When restoring the system from a backup, the type of restore required is dictated by + * the cleanup policy being enforced. If eager cleanup is active, the system must
+ * (a) have a listeners configured to backup the deleted content + * e.g. {@link DeletedContentBackupCleanerListener}, or
+ * (b) ensure consistent backups across the database and content stores: backup + * when the system is not running; use a DB-based content store. This is the + * recommended route when running with eager cleanup. + *

+ * Lazy cleanup protects the content for a given period (e.g. 7 days) giving plenty of + * time for a backup to be taken; this allows hot backup without needing metadata-content + * consistency to be enforced. * * @author Derek Hulley */ public class ContentStoreCleaner + extends TransactionListenerAdapter + implements CopyServicePolicies.OnCopyCompletePolicy, + NodeServicePolicies.BeforeDeleteNodePolicy, + ContentServicePolicies.OnContentPropertyUpdatePolicy + { + /** + * Content URLs to delete once the transaction commits. + * @see #onContentPropertyUpdate(NodeRef, QName, ContentData, ContentData) + * @see #afterCommit() + */ + private static final String KEY_POST_COMMIT_DELETION_URLS = "ContentStoreCleaner.PostCommitDeletionUrls"; + /** + * Content URLs to delete if the transaction rolls b ack. + * @see #onContentPropertyUpdate(NodeRef, QName, ContentData, ContentData) + * @see #afterRollback() + */ + private static final String KEY_POST_ROLLBACK_DELETION_URLS = "ContentStoreCleaner.PostRollbackDeletionUrls"; + private static Log logger = LogFactory.getLog(ContentStoreCleaner.class); /** kept to notify the thread that it should quit */ private static VmShutdownListener vmShutdownListener = new VmShutdownListener("ContentStoreCleaner"); private DictionaryService dictionaryService; + private PolicyComponent policyComponent; + private ContentService contentService; private NodeDaoService nodeDaoService; private AVMNodeDAO avmNodeDAO; private ContentUrlDAO contentUrlDAO; private TransactionService transactionService; private List stores; + private boolean eagerOrphanCleanup; private List listeners; private int protectDays; @@ -90,6 +150,22 @@ public class ContentStoreCleaner this.dictionaryService = dictionaryService; } + /** + * @param policyComponent used to register to listen for content updates + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * @param contentService service to copy content binaries + */ + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + /** * @param nodeDaoService used to get the property values */ @@ -130,6 +206,15 @@ public class ContentStoreCleaner this.stores = stores; } + /** + * + * @param eagerOrphanCleanup true to clean up content + */ + public void setEagerOrphanCleanup(boolean eagerOrphanCleanup) + { + this.eagerOrphanCleanup = eagerOrphanCleanup; + } + /** * @param listeners the listeners that can react to deletions */ @@ -149,12 +234,42 @@ public class ContentStoreCleaner this.protectDays = protectDays; } + /** + * Initializes the cleaner based on the {@link #setEagerOrphanCleanup(boolean) eagerCleanup} flag. + */ + public void init() + { + checkProperties(); + if (!eagerOrphanCleanup) + { + // Don't register for anything because eager cleanup is disabled + return; + } + // Register for the updates + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onContentPropertyUpdate"), + this, + new JavaBehaviour(this, "onContentPropertyUpdate")); + // TODO: This is a RM-specific hack. Once content properties are separated out, the + // following should be accomplished with a trigger to clean up orphaned content. + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "beforeDeleteNode"), + ContentModel.TYPE_CONTENT, + new JavaBehaviour(this, "beforeDeleteNode")); + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onCopyComplete"), + ContentModel.TYPE_CONTENT, + new JavaBehaviour(this, "onCopyComplete")); + } + /** * Perform basic checks to ensure that the necessary dependencies were injected. */ private void checkProperties() { PropertyCheck.mandatory(this, "dictionaryService", dictionaryService); + PropertyCheck.mandatory(this, "policyComponent", policyComponent); + PropertyCheck.mandatory(this, "contentService", contentService); PropertyCheck.mandatory(this, "nodeDaoService", nodeDaoService); PropertyCheck.mandatory(this, "avmNodeDAO", avmNodeDAO); PropertyCheck.mandatory(this, "contentUrlDAO", contentUrlDAO); @@ -173,7 +288,185 @@ public class ContentStoreCleaner "It is possible that in-transaction content will be deleted."); } } - + + /** + * Makes sure that copied files get a new, unshared binary. + */ + public void onCopyComplete( + QName classRef, + NodeRef sourceNodeRef, + NodeRef targetNodeRef, + boolean copyToNewNode, + Map copyMap) + { + // Get the cm:content of the source + ContentReader sourceReader = contentService.getReader(sourceNodeRef, ContentModel.PROP_CONTENT); + if (sourceReader == null || !sourceReader.exists()) + { + // No point attempting to duplicate missing content. We're only interested in cleanin up. + return; + } + // Get the cm:content of the target + ContentReader targetReader = contentService.getReader(targetNodeRef, ContentModel.PROP_CONTENT); + if (targetReader == null || !targetReader.exists()) + { + // The target's content is not present, so we don't copy anything over + return; + } + else if (!targetReader.getContentUrl().equals(sourceReader.getContentUrl())) + { + // The target already has a different binary + return; + } + // Create new content for the target node. This will behave just like an update to the node + // but occurs in the same txn as the copy process. Clearly this is a hack and is only + // able to work when properties are copied with all the proper copy-related policies being + // triggered. + ContentWriter targetWriter = contentService.getWriter(targetNodeRef, ContentModel.PROP_CONTENT, true); + targetWriter.putContent(sourceReader); + // This will have triggered deletion of the source node's content because the target node + // is being updated. Force the source node's content to be protected. + Set urlsToDelete = TransactionalResourceHelper.getSet(KEY_POST_COMMIT_DELETION_URLS); + urlsToDelete.remove(sourceReader.getContentUrl()); + } + + /** + * Records the content URLs of cm:content for post-commit cleanup. + */ + public void beforeDeleteNode(NodeRef nodeRef) + { + // Get the cm:content property + Pair nodePair = nodeDaoService.getNodePair(nodeRef); + if (nodePair == null) + { + return; + } + ContentData contentData = (ContentData) nodeDaoService.getNodeProperty( + nodePair.getFirst(), ContentModel.PROP_CONTENT); + if (contentData == null || !ContentData.hasContent(contentData)) + { + return; + } + String contentUrl = contentData.getContentUrl(); + // Bind it to the list + Set urlsToDelete = TransactionalResourceHelper.getSet(KEY_POST_COMMIT_DELETION_URLS); + urlsToDelete.add(contentUrl); + AlfrescoTransactionSupport.bindListener(this); + } + + /** + * Checks for {@link #setEagerOrphanCleanup(boolean) eager cleanup} and pushes the old content URL into + * a list for post-transaction deletion. + */ + public void onContentPropertyUpdate( + NodeRef nodeRef, + QName propertyQName, + ContentData beforeValue, + ContentData afterValue) + { + boolean registerListener = false; + // Bind in URLs to delete when txn commits + if (beforeValue != null && ContentData.hasContent(beforeValue)) + { + // Register the URL to delete + String contentUrl = beforeValue.getContentUrl(); + Set urlsToDelete = TransactionalResourceHelper.getSet(KEY_POST_COMMIT_DELETION_URLS); + urlsToDelete.add(contentUrl); + // Register to listen for transaction commit + registerListener = true; + } + // Bind in URLs to delete when txn rolls back + if (afterValue != null && ContentData.hasContent(afterValue)) + { + // Register the URL to delete + String contentUrl = afterValue.getContentUrl(); + Set urlsToDelete = TransactionalResourceHelper.getSet(KEY_POST_ROLLBACK_DELETION_URLS); + urlsToDelete.add(contentUrl); + // Register to listen for transaction rollback + registerListener = true; + } + // Register listener + if (registerListener) + { + AlfrescoTransactionSupport.bindListener(this); + } + } + + /** + * Cleans up all newly-orphaned content + */ + @Override + public void afterCommit() + { + Set urlsToDelete = TransactionalResourceHelper.getSet(KEY_POST_COMMIT_DELETION_URLS); + // Debug + if (logger.isDebugEnabled()) + { + logger.debug("Post-commit deletion of old content URLs: "); + int count = 0; + for (String contentUrl : urlsToDelete) + { + if (count == 10) + { + logger.debug(" " + (urlsToDelete.size() - 10) + " more ..."); + } + else + { + logger.debug(" Deleting content URL: " + contentUrl); + } + count++; + } + } + // Delete + for (String contentUrl : urlsToDelete) + { + for (ContentStore store : stores) + { + for (ContentStoreCleanerListener listener : listeners) + { + listener.beforeDelete(store, contentUrl); + } + store.delete(contentUrl); + } + } + } + + @Override + public void afterRollback() + { + Set urlsToDelete = TransactionalResourceHelper.getSet(KEY_POST_ROLLBACK_DELETION_URLS); + // Debug + if (logger.isDebugEnabled()) + { + logger.debug("Post-rollback deletion of new content URLs: "); + int count = 0; + for (String contentUrl : urlsToDelete) + { + if (count == 10) + { + logger.debug(" " + (urlsToDelete.size() - 10) + " more ..."); + } + else + { + logger.debug(" Deleting content URL: " + contentUrl); + } + count++; + } + } + // Delete + for (String contentUrl : urlsToDelete) + { + for (ContentStore store : stores) + { + for (ContentStoreCleanerListener listener : listeners) + { + listener.beforeDelete(store, contentUrl); + } + store.delete(contentUrl); + } + } + } + private void removeContentUrlsPresentInMetadata() { RetryingTransactionHelper txnHelper = transactionService.getRetryingTransactionHelper(); @@ -273,7 +566,6 @@ public class ContentStoreCleaner { public void handle(String contentUrl) { - boolean listenersCalled = false; for (ContentStore store : stores) { if (vmShutdownListener.isVmShuttingDown()) @@ -287,15 +579,9 @@ public class ContentStoreCleaner logger.debug(" Deleting content URL: " + contentUrl); } } - // Only transfer the URL to the listeners once - if (!listenersCalled && listeners.size() > 0) + for (ContentStoreCleanerListener listener : listeners) { - listenersCalled = true; - ContentReader reader = store.getReader(contentUrl); - for (ContentStoreCleanerListener listener : listeners) - { - listener.beforeDelete(reader); - } + listener.beforeDelete(store, contentUrl); } // Delete store.delete(contentUrl); diff --git a/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleanerListener.java b/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleanerListener.java index d98d9f3777..b34049349e 100644 --- a/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleanerListener.java +++ b/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleanerListener.java @@ -24,17 +24,29 @@ */ package org.alfresco.repo.content.cleanup; +import org.alfresco.repo.content.ContentStore; import org.alfresco.service.cmr.repository.ContentIOException; -import org.alfresco.service.cmr.repository.ContentReader; /** * A listener that can be plugged into a * {@link org.alfresco.repo.content.cleanup.ContentStoreCleaner cleaner} to - * move soon-to-be-deleted content to a new location. + * move pre-process any content that is about to be deleted from a store. + *

+ * Implementations may backup the content or even perform scrubbing or obfuscation + * tasks on the content. In either case, this interface is called when the content + * really will disappear i.e. there is no potential rollback of this operation. * * @author Derek Hulley */ public interface ContentStoreCleanerListener { - public void beforeDelete(ContentReader reader) throws ContentIOException; + /** + * Handle the notification that a store is about to be deleted + * + * @param sourceStore the store from which the content will be deleted + * @param contentUrl the URL of the content to be deleted + * + * @since 3.2 + */ + public void beforeDelete(ContentStore sourceStore, String contentUrl) throws ContentIOException; } diff --git a/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleanerScalabilityRunner.java b/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleanerScalabilityRunner.java index 28596d0ef7..1fac9fcc24 100644 --- a/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleanerScalabilityRunner.java +++ b/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleanerScalabilityRunner.java @@ -30,7 +30,6 @@ import java.lang.reflect.Method; import java.util.Collections; import java.util.Date; -import org.alfresco.model.ContentModel; import org.alfresco.repo.content.AbstractContentStore; import org.alfresco.repo.content.ContentStore; import org.alfresco.repo.content.EmptyContentReader; @@ -38,9 +37,6 @@ import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.content.filestore.FileContentReader; import org.alfresco.repo.content.filestore.FileContentStore; import org.alfresco.repo.content.filestore.FileContentWriter; -import org.alfresco.repo.domain.Node; -import org.alfresco.repo.domain.PropertyValue; -import org.alfresco.repo.domain.hibernate.NodeImpl; import org.alfresco.repo.node.db.NodeDaoService; import org.alfresco.repo.node.db.NodeDaoService.NodePropertyHandler; import org.alfresco.repo.transaction.SingleEntryTransactionResourceInterceptor; @@ -52,13 +48,11 @@ import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.service.transaction.TransactionService; import org.alfresco.tools.Repository; import org.alfresco.tools.ToolException; -import org.alfresco.util.GUID; import org.alfresco.util.TempFileProvider; import org.alfresco.util.VmShutdownListener; import org.apache.commons.lang.mutable.MutableInt; @@ -217,7 +211,7 @@ public class ContentStoreCleanerScalabilityRunner extends Repository ContentStoreCleanerListener listener = new ContentStoreCleanerListener() { private int count = 0; - public void beforeDelete(ContentReader reader) throws ContentIOException + public void beforeDelete(ContentStore store, String contentUrl) throws ContentIOException { count++; if (count % 1000 == 0) diff --git a/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleanerTest.java b/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleanerTest.java index bee446676a..5981cf3f0b 100644 --- a/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleanerTest.java +++ b/source/java/org/alfresco/repo/content/cleanup/ContentStoreCleanerTest.java @@ -24,25 +24,41 @@ */ package org.alfresco.repo.content.cleanup; +import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.concurrent.CountDownLatch; import junit.framework.TestCase; +import org.alfresco.model.ContentModel; import org.alfresco.repo.avm.AVMNodeDAO; import org.alfresco.repo.content.ContentStore; +import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.content.filestore.FileContentStore; import org.alfresco.repo.domain.ContentUrlDAO; import org.alfresco.repo.node.db.NodeDaoService; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.CopyService; +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.ApplicationContextHelper; +import org.alfresco.util.EqualsHelper; +import org.alfresco.util.GUID; +import org.alfresco.util.Pair; import org.alfresco.util.TempFileProvider; import org.springframework.context.ConfigurableApplicationContext; @@ -55,6 +71,10 @@ public class ContentStoreCleanerTest extends TestCase { private static ConfigurableApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + private ContentService contentService; + private NodeService nodeService; + private CopyService copyService; + private TransactionService transactionService; private ContentStoreCleaner cleaner; private ContentStore store; private ContentStoreCleanerListener listener; @@ -63,9 +83,16 @@ public class ContentStoreCleanerTest extends TestCase @Override public void setUp() throws Exception { + AuthenticationUtil.setRunAsUserSystem(); + ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean("ServiceRegistry"); + contentService = serviceRegistry.getContentService(); + nodeService = serviceRegistry.getNodeService(); + copyService = serviceRegistry.getCopyService(); + transactionService = serviceRegistry.getTransactionService(); TransactionService transactionService = serviceRegistry.getTransactionService(); DictionaryService dictionaryService = serviceRegistry.getDictionaryService(); + PolicyComponent policyComponent = (PolicyComponent) ctx.getBean("policyComponent"); NodeDaoService nodeDaoService = (NodeDaoService) ctx.getBean("nodeDaoService"); AVMNodeDAO avmNodeDAO = (AVMNodeDAO) ctx.getBean("avmNodeDAO"); ContentUrlDAO contentUrlDAO = (ContentUrlDAO) ctx.getBean("contentUrlDAO"); @@ -81,11 +108,171 @@ public class ContentStoreCleanerTest extends TestCase cleaner = new ContentStoreCleaner(); cleaner.setTransactionService(transactionService); cleaner.setDictionaryService(dictionaryService); + cleaner.setPolicyComponent(policyComponent); cleaner.setNodeDaoService(nodeDaoService); cleaner.setAvmNodeDAO(avmNodeDAO); cleaner.setContentUrlDAO(contentUrlDAO); cleaner.setStores(Collections.singletonList(store)); cleaner.setListeners(Collections.singletonList(listener)); + cleaner.setEagerOrphanCleanup(false); + } + + public void tearDown() throws Exception + { + AuthenticationUtil.clearCurrentSecurityContext(); + } + + public void testEagerCleanupOnCommit() throws Exception + { + // Get the context-defined cleaner + ContentStoreCleaner cleaner = (ContentStoreCleaner) ctx.getBean("contentStoreCleaner"); + // Force eager cleanup + cleaner.setEagerOrphanCleanup(true); + cleaner.init(); + // Create a new file + RetryingTransactionCallback makeContentCallback = new RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + // Create some content + StoreRef storeRef = nodeService.createStore("test", "testEagerCleanupOnCommit-" + GUID.generate()); + NodeRef rootNodeRef = nodeService.getRootNode(storeRef); + Map properties = Collections.singletonMap(ContentModel.PROP_NAME, (Serializable)"test.txt"); + NodeRef contentNodeRef = nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_CONTENT, + properties).getChildRef(); + ContentWriter writer = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.putContent("INITIAL CONTENT"); + // Done + return contentNodeRef; + } + }; + final NodeRef contentNodeRef = transactionService.getRetryingTransactionHelper().doInTransaction(makeContentCallback); + ContentReader contentReader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT); + assertTrue("Expect content to exist", contentReader.exists()); + + // Now update the node, but force a failure i.e. txn rollback + final List newContentUrls = new ArrayList(); + RetryingTransactionCallback failUpdateCallback = new RetryingTransactionCallback() + { + public String execute() throws Throwable + { + ContentWriter writer = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.putContent("CONTENT FOR FAIL"); + // This will have updated the metadata, so we can fail now + newContentUrls.add(writer.getContentUrl()); + // Done + throw new RuntimeException("FAIL"); + } + }; + try + { + transactionService.getRetryingTransactionHelper().doInTransaction(failUpdateCallback); + fail("Transaction was meant to fail"); + } + catch (RuntimeException e) + { + // Expected + } + // Make sure that the new content is not there + // The original content must still be there + assertEquals("Expected one content URL to play with", 1, newContentUrls.size()); + ContentReader readerMissing = contentService.getRawReader(newContentUrls.get(0)); + assertFalse("Newly created content should have been removed.", readerMissing.exists()); + assertTrue("Original content should still be there.", contentReader.exists()); + + // Now update the node successfully + RetryingTransactionCallback successUpdateCallback = new RetryingTransactionCallback() + { + public String execute() throws Throwable + { + ContentWriter writer = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.putContent("CONTENT FOR SUCCESS"); + // Done + return writer.getContentUrl(); + } + }; + String newContentUrl = transactionService.getRetryingTransactionHelper().doInTransaction(successUpdateCallback); + // Make sure that the new content is there + // The original content was disposed of + ContentReader contentReaderNew = contentService.getRawReader(newContentUrl); + assertTrue("Newly created content should be present.", contentReaderNew.exists()); + assertFalse("Original content should have been removed.", contentReader.exists()); + + // Now delete the node + RetryingTransactionCallback deleteNodeCallback = new RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + nodeService.deleteNode(contentNodeRef); + // Done + return null; + } + }; + transactionService.getRetryingTransactionHelper().doInTransaction(deleteNodeCallback); + // The new content must have disappeared + assertFalse("Newly created content should be removed.", contentReaderNew.exists()); + } + + /** + * TODO: This test must be replaced with one that checks that the raw content binary lives + * as long as there is a reference to it. Once the RM-hacks are removed, content + * will once again be shared and must therefore be cleaned up based on unlinking of + * references. + */ + public void testEagerCleanupAfterCopy() throws Exception + { + // Get the context-defined cleaner + ContentStoreCleaner cleaner = (ContentStoreCleaner) ctx.getBean("contentStoreCleaner"); + // Force eager cleanup + cleaner.setEagerOrphanCleanup(true); + cleaner.init(); + // Create a new file, copy it + RetryingTransactionCallback> copyFileCallback = new RetryingTransactionCallback>() + { + public Pair execute() throws Throwable + { + // Create some content + StoreRef storeRef = nodeService.createStore("test", "testEagerCleanupAfterCopy-" + GUID.generate()); + NodeRef rootNodeRef = nodeService.getRootNode(storeRef); + Map properties = Collections.singletonMap(ContentModel.PROP_NAME, (Serializable)"test.txt"); + NodeRef contentNodeRef = nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN, + ContentModel.TYPE_CONTENT, + properties).getChildRef(); + ContentWriter writer = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.putContent("INITIAL CONTENT"); + // Now copy it + NodeRef copiedNodeRef = copyService.copy( + contentNodeRef, + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + ContentModel.ASSOC_CHILDREN); + // Done + return new Pair(contentNodeRef, copiedNodeRef); + } + }; + Pair nodeRefPair = transactionService.getRetryingTransactionHelper().doInTransaction(copyFileCallback); + // Check that the readers of the content have different URLs + ContentReader contentReaderSource = contentService.getReader(nodeRefPair.getFirst(), ContentModel.PROP_CONTENT); + assertNotNull("Expected reader for source cm:content", contentReaderSource); + assertTrue("Expected content for source cm:content", contentReaderSource.exists()); + ContentReader contentReaderCopy = contentService.getReader(nodeRefPair.getSecond(), ContentModel.PROP_CONTENT); + assertNotNull("Expected reader for copy cm:content", contentReaderCopy); + assertTrue("Expected content for copy cm:content", contentReaderCopy.exists()); + String contentUrlSource = contentReaderSource.getContentUrl(); + String contentUrlCopy = contentReaderCopy.getContentUrl(); + assertFalse("Source and copy must have different URLs", + EqualsHelper.nullSafeEquals(contentUrlSource, contentUrlCopy)); } public void testImmediateRemoval() throws Exception @@ -145,9 +332,9 @@ public class ContentStoreCleanerTest extends TestCase private class DummyCleanerListener implements ContentStoreCleanerListener { - public void beforeDelete(ContentReader reader) throws ContentIOException + public void beforeDelete(ContentStore store, String contentUrl) throws ContentIOException { - deletedUrls.add(reader.getContentUrl()); + deletedUrls.add(contentUrl); } } } diff --git a/source/java/org/alfresco/repo/content/cleanup/DeletedContentBackupCleanerListener.java b/source/java/org/alfresco/repo/content/cleanup/DeletedContentBackupCleanerListener.java index cbe942f813..0cf0f9dea0 100644 --- a/source/java/org/alfresco/repo/content/cleanup/DeletedContentBackupCleanerListener.java +++ b/source/java/org/alfresco/repo/content/cleanup/DeletedContentBackupCleanerListener.java @@ -24,6 +24,7 @@ */ package org.alfresco.repo.content.cleanup; +import org.alfresco.repo.content.ContentContext; import org.alfresco.repo.content.ContentStore; import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.repository.ContentReader; @@ -57,18 +58,26 @@ public class DeletedContentBackupCleanerListener implements ContentStoreCleanerL this.store = store; } - public void beforeDelete(ContentReader reader) throws ContentIOException + public void beforeDelete(ContentStore sourceStore, String contentUrl) throws ContentIOException { + ContentContext context = new ContentContext(null, contentUrl); + ContentReader reader = sourceStore.getReader(contentUrl); + if (!reader.exists()) + { + // Nothing to copy over + return; + } // write the content into the target store - ContentWriter writer = store.getWriter(null, reader.getContentUrl()); + ContentWriter writer = store.getWriter(context); // copy across writer.putContent(reader); // done if (logger.isDebugEnabled()) { logger.debug("Moved content before deletion: \n" + - " URL: " + reader.getContentUrl() + "\n" + - " Store: " + store); + " URL: " + contentUrl + "\n" + + " Source: " + sourceStore + "\n" + + " Target: " + store); } } }