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