From e83e9f4bc1c3adc6d65ee6adff6b74e3d9397fa6 Mon Sep 17 00:00:00 2001 From: Steven Glover Date: Thu, 15 Sep 2016 13:06:35 +0000 Subject: [PATCH] Merged DEV/SG/MNT15135 to 5.2.N MNT-15135 "Alfresco Media Management: Rendition Concurrency Failure on Property update" git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/BRANCHES/DEV/5.2.N/root@130692 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- .../alfresco/rendition-services-context.xml | 9 + .../executer/AbstractRenderingEngine.java | 534 +++++++++----- ...AbstractTransformationRenderingEngine.java | 378 +++++----- .../repo/thumbnail/ThumbnailServiceImpl.java | 26 +- .../RenditionServiceIntegrationTest.java | 156 ++-- .../thumbnail/ThumbnailServiceImplTest.java | 685 ++++++++++++++---- 6 files changed, 1217 insertions(+), 571 deletions(-) diff --git a/config/alfresco/rendition-services-context.xml b/config/alfresco/rendition-services-context.xml index df4f880012..00687b0609 100644 --- a/config/alfresco/rendition-services-context.xml +++ b/config/alfresco/rendition-services-context.xml @@ -122,6 +122,15 @@ + + + + + + + + + {http://www.alfresco.org/model/content/1.0}content diff --git a/source/java/org/alfresco/repo/rendition/executer/AbstractRenderingEngine.java b/source/java/org/alfresco/repo/rendition/executer/AbstractRenderingEngine.java index 0bbbc2071c..c92d950ff3 100644 --- a/source/java/org/alfresco/repo/rendition/executer/AbstractRenderingEngine.java +++ b/source/java/org/alfresco/repo/rendition/executer/AbstractRenderingEngine.java @@ -25,60 +25,69 @@ */ package org.alfresco.repo.rendition.executer; -import static org.alfresco.service.cmr.rendition.RenditionService.PARAM_DESTINATION_PATH_TEMPLATE; -import static org.alfresco.service.cmr.rendition.RenditionService.PARAM_IS_COMPONENT_RENDITION; -import static org.alfresco.service.cmr.rendition.RenditionService.PARAM_ORPHAN_EXISTING_RENDITION; -import static org.alfresco.service.cmr.rendition.RenditionService.PARAM_RENDITION_NODETYPE; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.alfresco.error.AlfrescoRuntimeException; -import org.alfresco.model.ContentModel; -import org.alfresco.model.RenditionModel; -import org.alfresco.repo.action.ParameterDefinitionImpl; -import org.alfresco.repo.action.executer.ActionExecuterAbstractBase; -import org.alfresco.repo.content.MimetypeMap; -import org.alfresco.repo.content.transform.UnimportantTransformException; -import org.alfresco.repo.nodelocator.NodeLocator; -import org.alfresco.repo.nodelocator.SelfNodeLocator; -import org.alfresco.repo.policy.BehaviourFilter; -import org.alfresco.repo.rendition.RenderingEngineDefinitionImpl; -import org.alfresco.repo.rendition.RenditionDefinitionImpl; -import org.alfresco.repo.rendition.RenditionLocation; -import org.alfresco.repo.rendition.RenditionLocationResolver; -import org.alfresco.repo.rendition.RenditionNodeManager; -import org.alfresco.repo.security.authentication.AuthenticationUtil; -import org.alfresco.service.cmr.action.Action; -import org.alfresco.service.cmr.action.ActionDefinition; -import org.alfresco.service.cmr.action.ActionServiceException; -import org.alfresco.service.cmr.action.ActionTrackingService; -import org.alfresco.service.cmr.action.ExecutionSummary; -import org.alfresco.service.cmr.action.ParameterDefinition; -import org.alfresco.service.cmr.dictionary.DataTypeDefinition; -import org.alfresco.service.cmr.rendition.RenderCallback; -import org.alfresco.service.cmr.rendition.RenditionDefinition; -import org.alfresco.service.cmr.rendition.RenditionService; -import org.alfresco.service.cmr.rendition.RenditionServiceException; -import org.alfresco.service.cmr.repository.ChildAssociationRef; -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.NodeService; -import org.alfresco.service.cmr.repository.SerializedTransformationOptionsAccessor; -import org.alfresco.service.namespace.NamespaceException; -import org.alfresco.service.namespace.NamespaceService; -import org.alfresco.service.namespace.QName; -import org.alfresco.util.GUID; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.extensions.surf.util.I18NUtil; - +import static org.alfresco.service.cmr.rendition.RenditionService.PARAM_DESTINATION_PATH_TEMPLATE; +import static org.alfresco.service.cmr.rendition.RenditionService.PARAM_IS_COMPONENT_RENDITION; +import static org.alfresco.service.cmr.rendition.RenditionService.PARAM_ORPHAN_EXISTING_RENDITION; +import static org.alfresco.service.cmr.rendition.RenditionService.PARAM_RENDITION_NODETYPE; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +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.model.RenditionModel; +import org.alfresco.repo.action.ParameterDefinitionImpl; +import org.alfresco.repo.action.executer.ActionExecuterAbstractBase; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.content.transform.UnimportantTransformException; +import org.alfresco.repo.nodelocator.NodeLocator; +import org.alfresco.repo.nodelocator.SelfNodeLocator; +import org.alfresco.repo.policy.BehaviourFilter; +import org.alfresco.repo.rendition.RenderingEngineDefinitionImpl; +import org.alfresco.repo.rendition.RenditionDefinitionImpl; +import org.alfresco.repo.rendition.RenditionLocation; +import org.alfresco.repo.rendition.RenditionLocationResolver; +import org.alfresco.repo.rendition.RenditionNodeManager; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.repo.transaction.TransactionalResourceHelper; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionDefinition; +import org.alfresco.service.cmr.action.ActionServiceException; +import org.alfresco.service.cmr.action.ActionTrackingService; +import org.alfresco.service.cmr.action.ExecutionSummary; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.attributes.AttributeService; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.rendition.RenderCallback; +import org.alfresco.service.cmr.rendition.RenditionDefinition; +import org.alfresco.service.cmr.rendition.RenditionService; +import org.alfresco.service.cmr.rendition.RenditionServiceException; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +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.NodeService; +import org.alfresco.service.cmr.repository.SerializedTransformationOptionsAccessor; +import org.alfresco.service.cmr.version.Version; +import org.alfresco.service.cmr.version.VersionService; +import org.alfresco.service.namespace.NamespaceException; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.GUID; +import org.alfresco.util.transaction.TransactionListener; +import org.alfresco.util.transaction.TransactionSupportUtil; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.I18NUtil; + import com.sun.star.lang.NullPointerException; /** @@ -89,11 +98,13 @@ import com.sun.star.lang.NullPointerException; * @author Nick Smith * @since 3.3 */ -public abstract class AbstractRenderingEngine extends ActionExecuterAbstractBase +public abstract class AbstractRenderingEngine extends ActionExecuterAbstractBase implements TransactionListener { - /** Logger */ private static Log logger = LogFactory.getLog(AbstractRenderingEngine.class); + + private static final String RENDITIONED_CONTENT = "RENDITIONED_CONTENT"; + private static final String RENDERING_CONTEXTS = "RenderingEngine.Contexts"; protected static final String CONTENT_READER_NOT_FOUND_MESSAGE = "Cannot find Content Reader for document. Operation can't be performed"; protected static final String DEFAULT_RUN_AS_NAME = AuthenticationUtil.getSystemUserName(); @@ -144,7 +155,10 @@ public abstract class AbstractRenderingEngine extends ActionExecuterAbstractBase /* Injected Services */ protected ContentService contentService; protected MimetypeMap mimetypeMap; - protected ActionTrackingService actionTrackingService; + protected ActionTrackingService actionTrackingService; + protected AttributeService attributeService; + protected TransactionService transactionService; + protected VersionService versionService; /* Parameter names common to all Rendering Actions */ /** @@ -215,7 +229,22 @@ public abstract class AbstractRenderingEngine extends ActionExecuterAbstractBase private final NodeLocator temporaryParentNodeLocator; private final QName temporaryRenditionLinkType; - + + public void setVersionService(VersionService versionService) + { + this.versionService = versionService; + } + + public void setAttributeService(AttributeService attributeService) + { + this.attributeService = attributeService; + } + + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + /** * Injects the nodeService bean. * @@ -526,11 +555,13 @@ public abstract class AbstractRenderingEngine extends ActionExecuterAbstractBase RenderingContext context = new RenderingContext(sourceNode, renditionDefinition, - targetContentProp); - render(context); + targetContentProp); + + render(context); + // This is a workaround for the fact that actions don't have return // values. - action.getParameterValues().put(PARAM_RESULT, context.getChildAssociationRef()); + action.getParameterValues().put(PARAM_RESULT, context.getChildAssociationRef()); } /** @@ -759,111 +790,220 @@ public abstract class AbstractRenderingEngine extends ActionExecuterAbstractBase result = defaultValue; return result; } + + public static String getRenderedContentKey(NodeRef sourceNode, VersionService versionService) + { + StringBuilder sb = new StringBuilder(); + sb.append(sourceNode.toString()); + sb.append("."); + Version version = versionService.getCurrentVersion(sourceNode); + sb.append(version != null ? version.getVersionLabel() : "1.0"); + return sb.toString(); + } + + protected class RenderingContext implements SerializedTransformationOptionsAccessor + { + private final String guid = GUID.generate(); + + private final NodeRef sourceNode; + private final RenditionDefinition definition; + private final QName renditionContentProperty; + private String renderedContentKey; + private ChildAssociationRef caNodeRef; + + /** + * @param sourceNode NodeRef + * @param definition RenditionDefinition + * @param renditionContentProperty QName + */ + RenderingContext(NodeRef sourceNode, RenditionDefinition definition, QName renditionContentProperty) + { + this.sourceNode = sourceNode; + this.definition = definition; + this.renditionContentProperty = renditionContentProperty; + this.renderedContentKey = AbstractRenderingEngine.getRenderedContentKey(sourceNode, versionService); + } + + public String getRenderedContentKey() + { + return renderedContentKey; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + getOuterType().hashCode(); + result = prime * result + ((guid == null) ? 0 : guid.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + RenderingContext other = (RenderingContext) obj; + if (!getOuterType().equals(other.getOuterType())) + return false; + if (guid == null) { + if (other.guid != null) + return false; + } else if (!guid.equals(other.guid)) + return false; + return true; + } + + /** + * Save an existing transformed content url for this rendition and node + * + * @param existingTransformedContentUrl + */ + void setExistingTransformedContentUrl(final String existingTransformedContentUrl) + { + // add the rendering context to the txn listener so that we can clean up + // any rendering context data saved using the AttributeService + TransactionalResourceHelper.getSet(RENDERING_CONTEXTS).add(this); + TransactionSupportUtil.bindListener(AbstractRenderingEngine.this, 0); + + if(logger.isDebugEnabled()) + { + logger.debug("setExistingTransformedContentUrl for renderedContentKey " + renderedContentKey + + ", rendition " + getDefinition().getRenditionName()); + } + + // make sure this is saved in a new transaction + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + attributeService.setAttribute(existingTransformedContentUrl, RENDITIONED_CONTENT, renderedContentKey, + getDefinition().getRenditionName()); + return null; + } + }, false, true); + } + + /** + * Get an existing transformed content/rendition url for this rendition and node + * + * @return content url of transformed content/rendition + */ + String getExistingTransformedContentUrl() + { + // add the rendering context to the txn listener so that we can clean up + // any rendering context data saved using the AttributeService + TransactionalResourceHelper.getSet(RENDERING_CONTEXTS).add(this); + TransactionSupportUtil.bindListener(AbstractRenderingEngine.this, 0); + + String contentUrl = (String)attributeService.getAttribute(RENDITIONED_CONTENT, renderedContentKey, + getDefinition().getRenditionName()); + return contentUrl; + } + + /** + * @return the sourceNode + */ + public NodeRef getSourceNode() + { + return this.sourceNode; + } + + /** + * Lazily instantiation of the ChildAssociationRef + * @return ChildAssociationRef + */ + public synchronized ChildAssociationRef getChildAssociationRef() + { + if (this.caNodeRef == null) + { + this.caNodeRef = createRenditionNodeAssoc(sourceNode, definition); + } + + return this.caNodeRef; + } + + /** + * @return the destinationNode + */ + public NodeRef getDestinationNode() + { + return getChildAssociationRef().getChildRef(); + } + + /** + * @return the definition + */ + public RenditionDefinition getDefinition() + { + return this.definition; + } + + public T getCheckedParam(String paramName, Class clazz) + { + return AbstractRenderingEngine.getCheckedParam(paramName, clazz, definition); + } + + public T getParamWithDefault(String paramName, T defaultValue) + { + return AbstractRenderingEngine.getParamWithDefault(paramName, defaultValue, definition); + } + + public ContentReader makeContentReader() + { + QName srcContentProp = getParamWithDefault(PARAM_SOURCE_CONTENT_PROPERTY, DEFAULT_CONTENT_PROPERTY); + ContentReader contentReader = contentService.getReader(sourceNode, srcContentProp); + if (contentReader == null || !contentReader.exists()) + { + throw new UnimportantTransformException(CONTENT_READER_NOT_FOUND_MESSAGE); + } + return contentReader; + } + + public ContentWriter makeContentWriter() + { + ContentWriter contentWriter = contentService.getWriter(getDestinationNode(), renditionContentProperty, true); + String mimetype = getTargetMimeType(this); + contentWriter.setMimetype(mimetype); + String encoding = getTargetEncoding(this); + contentWriter.setEncoding(encoding); + return contentWriter; + } + + public int getIntegerParam(String key, int defaultValue) + { + Serializable serializable = definition.getParameterValue(key); + if (serializable == null) + return defaultValue; + else + { + Number number = (Number) serializable; + return number.intValue(); + } + } + + @Override + public String toString() + { + return "RenderingContext [sourceNode=" + sourceNode + ", renderedContentKey=" + renderedContentKey + + ", definition=" + definition + ", renditionContentProperty=" + renditionContentProperty + ", caNodeRef=" + caNodeRef + + ", actionDefinitionName=" + definition.getActionDefinitionName() + + ", renditionName=" + definition.getRenditionName() + + "]"; + } + + private AbstractRenderingEngine getOuterType() + { + return AbstractRenderingEngine.this; + } + } - protected class RenderingContext implements SerializedTransformationOptionsAccessor - { - private final NodeRef sourceNode; - private final RenditionDefinition definition; - private final QName renditionContentProperty; - - private ChildAssociationRef caNodeRef; - - /** - * @param sourceNode NodeRef - * @param definition RenditionDefinition - * @param renditionContentProperty QName - */ - public RenderingContext(NodeRef sourceNode,// - RenditionDefinition definition,// - QName renditionContentProperty) - { - this.sourceNode = sourceNode; - this.definition = definition; - this.renditionContentProperty = renditionContentProperty; - } - - /** - * @return the sourceNode - */ - public NodeRef getSourceNode() - { - return this.sourceNode; - } - - /** - * Lazily instantiation of the ChildAssociationRef - * @return ChildAssociationRef - */ - public synchronized ChildAssociationRef getChildAssociationRef() - { - if (this.caNodeRef == null) - { - this.caNodeRef = createRenditionNodeAssoc(sourceNode, definition); - } - return this.caNodeRef; - } - - /** - * @return the destinationNode - */ - public NodeRef getDestinationNode() - { - return getChildAssociationRef().getChildRef(); - } - - /** - * @return the definition - */ - public RenditionDefinition getDefinition() - { - return this.definition; - } - - public T getCheckedParam(String paramName, Class clazz) - { - return AbstractRenderingEngine.getCheckedParam(paramName, clazz, definition); - } - - public T getParamWithDefault(String paramName, T defaultValue) - { - return AbstractRenderingEngine.getParamWithDefault(paramName, defaultValue, definition); - } - - public ContentReader makeContentReader() - { - QName srcContentProp = getParamWithDefault(PARAM_SOURCE_CONTENT_PROPERTY, DEFAULT_CONTENT_PROPERTY); - ContentReader contentReader = contentService.getReader(sourceNode, srcContentProp); - if (contentReader == null || !contentReader.exists()) - { - throw new UnimportantTransformException(CONTENT_READER_NOT_FOUND_MESSAGE); - } - return contentReader; - } - - public ContentWriter makeContentWriter() - { - ContentWriter contentWriter = contentService.getWriter(getDestinationNode(), renditionContentProperty, true); - String mimetype = getTargetMimeType(this); - contentWriter.setMimetype(mimetype); - String encoding = getTargetEncoding(this); - contentWriter.setEncoding(encoding); - return contentWriter; - } - - public int getIntegerParam(String key, int defaultValue) - { - Serializable serializable = definition.getParameterValue(key); - if (serializable == null) - return defaultValue; - else - { - Number number = (Number) serializable; - return number.intValue(); - } - } - } - - protected void tagSourceNodeAsRenditioned(final RenditionDefinition renditionDef, final NodeRef actionedUponNodeRef) { // Adds the 'Renditioned' aspect to the source node if it @@ -1120,5 +1260,69 @@ public abstract class AbstractRenderingEngine extends ActionExecuterAbstractBase "multiple instances of the same action"); } return executionSummaries.iterator().next(); + } + + /** + * {@inheritDoc} + */ + @Override + public void beforeCommit(boolean readOnly) + { + } + + /** + * {@inheritDoc} + */ + @Override + public void beforeCompletion() + { + } + + /** + * {@inheritDoc} + */ + @Override + public void afterCommit() + { + // clear saved rendition data if we are successful + + final Set renderingContexts = TransactionalResourceHelper.getSet(RENDERING_CONTEXTS); + + if(logger.isDebugEnabled()) + { + logger.debug("Cleaning up " + renderingContexts.size() + " rendering contexts"); + } + + if(!renderingContexts.isEmpty()) + { + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + for(RenderingContext context : renderingContexts) + { + if(logger.isDebugEnabled()) + { + logger.debug("Cleaning up rendering context for source node " + context.getSourceNode() + + ", rendition " + context.getDefinition().getRenditionName()); + } + + attributeService.removeAttributes(RENDITIONED_CONTENT, context.getRenderedContentKey(), + context.getDefinition().getRenditionName()); + } + + return null; + } + }, false, true); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void afterRollback() + { } } diff --git a/source/java/org/alfresco/repo/rendition/executer/AbstractTransformationRenderingEngine.java b/source/java/org/alfresco/repo/rendition/executer/AbstractTransformationRenderingEngine.java index 63b8bf5b20..9cb74474d3 100644 --- a/source/java/org/alfresco/repo/rendition/executer/AbstractTransformationRenderingEngine.java +++ b/source/java/org/alfresco/repo/rendition/executer/AbstractTransformationRenderingEngine.java @@ -26,43 +26,50 @@ package org.alfresco.repo.rendition.executer; -import java.io.Serializable; -import java.util.Collection; -import java.util.Date; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.FutureTask; - -import org.alfresco.repo.action.ParameterDefinitionImpl; -import org.alfresco.repo.content.transform.ContentTransformer; -import org.alfresco.repo.content.transform.TransformerConfig; -import org.alfresco.repo.content.transform.TransformerDebug; +import java.io.Serializable; +import java.util.Collection; +import java.util.Date; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; + +import org.alfresco.repo.action.ParameterDefinitionImpl; +import org.alfresco.repo.content.ContentContext; +import org.alfresco.repo.content.ContentStore; +import org.alfresco.repo.content.filestore.FileContentStore; +import org.alfresco.repo.content.transform.ContentTransformer; +import org.alfresco.repo.content.transform.TransformerConfig; +import org.alfresco.repo.content.transform.TransformerDebug; import org.alfresco.repo.content.transform.UnsupportedTransformationException; -import org.alfresco.repo.security.authentication.AuthenticationUtil; -import org.alfresco.service.cmr.action.ActionServiceException; -import org.alfresco.service.cmr.action.ActionTrackingService; -import org.alfresco.service.cmr.action.ExecutionDetails; -import org.alfresco.service.cmr.action.ExecutionSummary; -import org.alfresco.service.cmr.action.ParameterDefinition; -import org.alfresco.service.cmr.dictionary.DataTypeDefinition; -import org.alfresco.service.cmr.rendition.RenditionCancelledException; -import org.alfresco.service.cmr.rendition.RenditionServiceException; -import org.alfresco.service.cmr.repository.ContentReader; -import org.alfresco.service.cmr.repository.ContentWriter; -import org.alfresco.service.cmr.repository.NoTransformerException; -import org.alfresco.service.cmr.repository.TransformationOptionLimits; -import org.alfresco.service.cmr.repository.TransformationOptions; -import org.alfresco.service.cmr.repository.TransformationSourceOptions; -import org.alfresco.service.cmr.repository.TransformationSourceOptions.TransformationSourceOptionsSerializer; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.action.ActionServiceException; +import org.alfresco.service.cmr.action.ActionTrackingService; +import org.alfresco.service.cmr.action.ExecutionDetails; +import org.alfresco.service.cmr.action.ExecutionSummary; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.rendition.RenditionCancelledException; +import org.alfresco.service.cmr.rendition.RenditionServiceException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NoTransformerException; +import org.alfresco.service.cmr.repository.TransformationOptionLimits; +import org.alfresco.service.cmr.repository.TransformationOptions; +import org.alfresco.service.cmr.repository.TransformationSourceOptions; +import org.alfresco.service.cmr.repository.TransformationSourceOptions.TransformationSourceOptionsSerializer; +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; /** * @author Nick Smith */ -public abstract class AbstractTransformationRenderingEngine extends AbstractRenderingEngine +public abstract class AbstractTransformationRenderingEngine extends AbstractRenderingEngine implements ApplicationContextAware { private static Log logger = LogFactory.getLog(AbstractTransformationRenderingEngine.class); @@ -112,6 +119,8 @@ public abstract class AbstractTransformationRenderingEngine extends AbstractRend * This optional {@link String} parameter specifies the type (or use) of the rendition. */ public static final String PARAM_USE = TransformerConfig.USE.replaceAll("\\.", ""); + + private ApplicationContext applicationContext; /* Error messages */ private static final String TRANSFORMER_NOT_EXISTS_MESSAGE_PATTERN = "Transformer for '%s' source mime type and '%s' target mime type was not found. Operation can't be performed"; @@ -119,11 +128,18 @@ public abstract class AbstractTransformationRenderingEngine extends AbstractRend private static final String TRANSFORMING_ERROR_MESSAGE = "Some error occurred during document transforming. Error message: "; private Collection sourceOptionsSerializers; - + + private ContentStore tempStore; + public Collection getSourceOptionsSerializers() { return sourceOptionsSerializers; } + + public void setApplicationContext(ApplicationContext applicationContext) + { + this.applicationContext = applicationContext; + } public void setSourceOptionsSerializers(Collection sourceOptionsSerializers) { @@ -157,166 +173,198 @@ public abstract class AbstractTransformationRenderingEngine extends AbstractRend if (executorService == null) { executorService = Executors.newCachedThreadPool(); - } + } + + this.tempStore = new FileContentStore(this.applicationContext, TempFileProvider.getTempDir().getAbsolutePath()); } - + + private void copyTransformedContent(ContentReader transformedContentReader, RenderingContext context) + { + // Copy content from temp writer to real writer + ContentWriter writer = context.makeContentWriter(); + writer.putContent(transformedContentReader.getContentInputStream()); + } + /* * (non-Javadoc) * @see org.alfresco.repo.rendition.executer.AbstractRenderingEngine#render(org.alfresco.repo.rendition.executer.AbstractRenderingEngine.RenderingContext) */ @Override protected void render(RenderingContext context) - { - ContentReader contentReader = context.makeContentReader(); - // There will have been an exception if there is no content data so contentReader is not null. - String sourceUrl = contentReader.getContentUrl(); - String sourceMimeType = contentReader.getMimetype(); - String targetMimeType = getTargetMimeType(context); - - // The child NodeRef gets created here - TransformationOptions options = getTransformOptions(context); - - // Log the following getTransform() as trace so we can see the wood for the trees - ContentTransformer transformer; - boolean orig = TransformerDebug.setDebugOutput(false); - try - { - transformer = this.contentService.getTransformer(sourceUrl, sourceMimeType, contentReader.getSize(), targetMimeType, options); - } - finally - { - TransformerDebug.setDebugOutput(orig); - } - - if (null == transformer) - { - // There's no transformer available for the requested rendition! - throw new RenditionServiceException(String.format(TRANSFORMER_NOT_EXISTS_MESSAGE_PATTERN, sourceMimeType, - targetMimeType)); - } - - if (!transformer.isTransformable(sourceMimeType, contentReader.getSize(), targetMimeType, options)) - { - throw new RenditionServiceException(String.format(NOT_TRANSFORMABLE_MESSAGE_PATTERN, sourceMimeType, targetMimeType)); - } - - long startTime = new Date().getTime(); - boolean actionCancelled = false; - boolean actionCompleted = false; - - // Cache the execution summary to get details later - ExecutionSummary executionSummary = null; - try - { - executionSummary = getExecutionSummary(context); - } - catch (ActionServiceException e) - { - if (logger.isInfoEnabled()) + { + // check for existing transformed content and reuse if present to avoid re-rendering + String existingTransformedContentUrl = context.getExistingTransformedContentUrl(); + if(existingTransformedContentUrl != null) + { + if(logger.isDebugEnabled()) { - logger.info("Cancelling of multiple concurrent action instances " + - "currently unsupported, this action can't be cancelled"); + logger.debug("Reusing existing rendered content " + existingTransformedContentUrl + + " for rendering context " + context); } - } - // Call the transform in a different thread so we can move on if cancelled - FutureTask transformTask = new FutureTask( - new TransformationCallable(contentReader, targetMimeType, options, context, - AuthenticationUtil.getFullyAuthenticatedUser())); - getExecutorService().execute(transformTask); - - // Start checking for cancellation or timeout - while (true) - { + ContentReader existingTransformedContent = tempStore.getReader(existingTransformedContentUrl); + copyTransformedContent(existingTransformedContent, context); + } + else + { + if(logger.isDebugEnabled()) + { + logger.debug("Rendering for rendering context " + context); + } + + ContentReader contentReader = context.makeContentReader(); + // There will have been an exception if there is no content data so contentReader is not null. + String sourceUrl = contentReader.getContentUrl(); + String sourceMimeType = contentReader.getMimetype(); + String targetMimeType = getTargetMimeType(context); + + // The child NodeRef gets created here + TransformationOptions options = getTransformOptions(context); + + // Log the following getTransform() as trace so we can see the wood for the trees + ContentTransformer transformer; + boolean orig = TransformerDebug.setDebugOutput(false); try { - Thread.sleep(CANCELLED_ACTION_POLLING_INTERVAL); - if (transformTask.isDone()) + transformer = this.contentService.getTransformer(sourceUrl, sourceMimeType, contentReader.getSize(), targetMimeType, options); + } + finally + { + TransformerDebug.setDebugOutput(orig); + } + + if (null == transformer) + { + // There's no transformer available for the requested rendition! + throw new RenditionServiceException(String.format(TRANSFORMER_NOT_EXISTS_MESSAGE_PATTERN, sourceMimeType, + targetMimeType)); + } + + if (!transformer.isTransformable(sourceMimeType, contentReader.getSize(), targetMimeType, options)) + { + throw new RenditionServiceException(String.format(NOT_TRANSFORMABLE_MESSAGE_PATTERN, sourceMimeType, targetMimeType)); + } + + long startTime = new Date().getTime(); + boolean actionCancelled = false; + boolean actionCompleted = false; + + // Cache the execution summary to get details later + ExecutionSummary executionSummary = null; + try + { + executionSummary = getExecutionSummary(context); + } + catch (ActionServiceException e) + { + if (logger.isInfoEnabled()) { - actionCompleted = true; - break; + logger.info("Cancelling of multiple concurrent action instances " + + "currently unsupported, this action can't be cancelled"); } - // Check timeout in case transformer doesn't obey it - if (options.getTimeoutMs() > 0 && - new Date().getTime() - startTime > (options.getTimeoutMs() + CANCELLED_ACTION_POLLING_INTERVAL)) + } + + // Call the transform in a different thread so we can move on if cancelled + FutureTask transformTask = new FutureTask( + new TransformationCallable(contentReader, targetMimeType, options, context, + AuthenticationUtil.getFullyAuthenticatedUser())); + getExecutorService().execute(transformTask); + + // Start checking for cancellation or timeout + while (true) + { + try { - // We hit a timeout, let the transform thread continue but results will be ignored - if (logger.isDebugEnabled()) + Thread.sleep(CANCELLED_ACTION_POLLING_INTERVAL); + if (transformTask.isDone()) { - logger.debug("Transformation did not obey timeout limit, " + - "rendition action is moving on"); + actionCompleted = true; + break; } - break; - } - if (executionSummary != null) - { - ExecutionDetails executionDetails = - actionTrackingService.getExecutionDetails(executionSummary); - if (executionDetails != null) + // Check timeout in case transformer doesn't obey it + if (options.getTimeoutMs() > 0 && + new Date().getTime() - startTime > (options.getTimeoutMs() + CANCELLED_ACTION_POLLING_INTERVAL)) { - actionCancelled = executionDetails.isCancelRequested(); - if (actionCancelled) + // We hit a timeout, let the transform thread continue but results will be ignored + if (logger.isDebugEnabled()) { - if (logger.isDebugEnabled()) + logger.debug("Transformation did not obey timeout limit, " + + "rendition action is moving on"); + } + break; + } + if (executionSummary != null) + { + ExecutionDetails executionDetails = + actionTrackingService.getExecutionDetails(executionSummary); + if (executionDetails != null) + { + actionCancelled = executionDetails.isCancelRequested(); + if (actionCancelled) { - logger.debug("Cancelling transformation"); + if (logger.isDebugEnabled()) + { + logger.debug("Cancelling transformation"); + } + transformTask.cancel(true); + break; } - transformTask.cancel(true); - break; } } } - } - catch (InterruptedException e) - { - // entire thread was asked to stop - actionCancelled = true; - transformTask.cancel(true); - break; - } - } - - if (actionCancelled) - { - throw new RenditionCancelledException("Rendition action cancelled"); - } - - if (!actionCompleted && !actionCancelled) - { - throw new RenditionServiceException("Transformation failed to obey timeout limit"); - } - - if (actionCompleted) - { - // Copy content from temp writer to real writer - ContentWriter writer = context.makeContentWriter(); - try - { - // We should not need another timeout here, things should be ready for us - ContentWriter tempTarget = transformTask.get(); - if (tempTarget == null) + catch (InterruptedException e) { - // We should never be in this state, but just in case - throw new RenditionServiceException("Target of transformation not present"); + // entire thread was asked to stop + actionCancelled = true; + transformTask.cancel(true); + break; } - writer.putContent(tempTarget.getReader().getContentInputStream()); } - catch (ExecutionException e) + + if (actionCancelled) { - // Unwrap our cause and throw that - Throwable transformException = e.getCause(); - if (transformException instanceof RuntimeException) - { - throw (RuntimeException) e.getCause(); - } - throw new RenditionServiceException(TRANSFORMING_ERROR_MESSAGE + e.getCause().getMessage(), e.getCause()); + throw new RenditionCancelledException("Rendition action cancelled"); } - catch (InterruptedException e) + + if (!actionCompleted && !actionCancelled) { - // We were asked to stop - transformTask.cancel(true); - } - } + throw new RenditionServiceException("Transformation failed to obey timeout limit"); + } + + if (actionCompleted) + { + try + { + // We should not need another timeout here, things should be ready for us + ContentWriter tempTarget = transformTask.get(); + if (tempTarget == null) + { + // We should never be in this state, but just in case + throw new RenditionServiceException("Target of transformation not present"); + } + + // save transform result in case we need to retry + context.setExistingTransformedContentUrl(tempTarget.getReader().getContentUrl()); + + copyTransformedContent(tempTarget.getReader(), context); + } + catch (ExecutionException e) + { + // Unwrap our cause and throw that + Throwable transformException = e.getCause(); + if (transformException instanceof RuntimeException) + { + throw (RuntimeException) e.getCause(); + } + throw new RenditionServiceException(TRANSFORMING_ERROR_MESSAGE + e.getCause().getMessage(), e.getCause()); + } + catch (InterruptedException e) + { + // We were asked to stop + transformTask.cancel(true); + } + } + } } protected abstract TransformationOptions getTransformOptions(RenderingContext context); @@ -404,7 +452,7 @@ public abstract class AbstractTransformationRenderingEngine extends AbstractRend return paramList; } - + /** * Implementation of Callable for doing the work of the transformation * which returns the temporary content writer if successful. @@ -440,7 +488,7 @@ public abstract class AbstractTransformationRenderingEngine extends AbstractRend public ContentWriter doWork() throws Exception { // ALF-15715: Use temporary write to avoid operating on the real node for fear of row locking while long transforms are in progress - ContentWriter tempContentWriter = contentService.getTempWriter(); + ContentWriter tempContentWriter = tempStore.getWriter(ContentContext.NULL_CONTEXT); tempContentWriter.setMimetype(targetMimeType); try { diff --git a/source/java/org/alfresco/repo/thumbnail/ThumbnailServiceImpl.java b/source/java/org/alfresco/repo/thumbnail/ThumbnailServiceImpl.java index 65c4abd289..9d34d4e315 100644 --- a/source/java/org/alfresco/repo/thumbnail/ThumbnailServiceImpl.java +++ b/source/java/org/alfresco/repo/thumbnail/ThumbnailServiceImpl.java @@ -265,8 +265,8 @@ public class ThumbnailServiceImpl implements ThumbnailService, if (logger.isDebugEnabled()) { - logger.debug("Thumbnail created " + childAssoc + " for sourceNodeRef " + sourceNodeRef + ", thumbnail " + thumbnailName - + ", thumbnailNodeRef " + thumbnailNodeRef); + logger.debug("Thumbnail created " + childAssoc + " for sourceNodeRef " + sourceNodeRef + ", thumbnail " + + thumbnailName + ", thumbnailNodeRef " + thumbnailNodeRef); } // MNT-15135: Cache the associations between parent nodes and updated thumbnails, @@ -382,6 +382,12 @@ public class ThumbnailServiceImpl implements ThumbnailService, //We can be in a read-only transaction, so force a new transaction requiresNew = true; } + + // Get the name of the thumbnail and add to properties map + QName thumbnailQName = getThumbnailQName(thumbnailName); + final RenditionDefinition definition = createRenditionDefinition(contentProperty, mimetype, + transformationOptions, thumbnailQName, assocDetails); + return txnHelper.doInTransaction(new RetryingTransactionCallback() { @@ -392,18 +398,12 @@ public class ThumbnailServiceImpl implements ThumbnailService, { public NodeRef doWork() throws Exception { - return createThumbnailNode( node, - contentProperty, - mimetype, - transformationOptions, - thumbnailName, - assocDetails); + return createThumbnailNode(node, definition, thumbnailName); } }, AuthenticationUtil.getSystemUserName()); } }, false, requiresNew); - } private QName getThumbnailQName(String localThumbnailName) @@ -717,14 +717,8 @@ public class ThumbnailServiceImpl implements ThumbnailService, return definition; } - private NodeRef createThumbnailNode(final NodeRef node, final QName contentProperty, - final String mimetype, final TransformationOptions transformationOptions, final String thumbnailName, - final ThumbnailParentAssociationDetails assocDetails) + private NodeRef createThumbnailNode(final NodeRef node, final RenditionDefinition definition, final String thumbnailName) { - // Get the name of the thumbnail and add to properties map - QName thumbnailQName = getThumbnailQName(thumbnailName); - RenditionDefinition definition = createRenditionDefinition(contentProperty, mimetype, - transformationOptions, thumbnailQName, assocDetails); try { ChildAssociationRef thumbnailAssoc = renditionService.render(node, definition); diff --git a/source/test-java/org/alfresco/repo/rendition/RenditionServiceIntegrationTest.java b/source/test-java/org/alfresco/repo/rendition/RenditionServiceIntegrationTest.java index 26793fd4c2..fc239c240e 100644 --- a/source/test-java/org/alfresco/repo/rendition/RenditionServiceIntegrationTest.java +++ b/source/test-java/org/alfresco/repo/rendition/RenditionServiceIntegrationTest.java @@ -26,73 +26,75 @@ package org.alfresco.repo.rendition; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.Serializable; -import java.net.URL; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import javax.imageio.ImageIO; - -import org.alfresco.error.AlfrescoRuntimeException; -import org.alfresco.model.ContentModel; -import org.alfresco.model.RenditionModel; -import org.alfresco.repo.action.RuntimeActionService; -import org.alfresco.repo.action.executer.ExporterActionExecuter; -import org.alfresco.repo.content.MimetypeMap; -import org.alfresco.repo.content.transform.AbstractContentTransformerTest; -import org.alfresco.repo.content.transform.ContentTransformer; -import org.alfresco.repo.content.transform.ContentTransformerRegistry; -import org.alfresco.repo.content.transform.UnimportantTransformException; -import org.alfresco.repo.content.transform.magick.ImageTransformationOptions; -import org.alfresco.repo.jscript.ClasspathScriptLocation; -import org.alfresco.repo.model.Repository; -import org.alfresco.repo.policy.BehaviourFilter; -import org.alfresco.repo.rendition.executer.AbstractRenderingEngine; -import org.alfresco.repo.rendition.executer.FreemarkerRenderingEngine; -import org.alfresco.repo.rendition.executer.ImageRenderingEngine; -import org.alfresco.repo.rendition.executer.ReformatRenderingEngine; -import org.alfresco.repo.security.authentication.AuthenticationUtil; -import org.alfresco.repo.transaction.RetryingTransactionHelper; -import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; -import org.alfresco.service.cmr.action.Action; -import org.alfresco.service.cmr.coci.CheckOutCheckInService; -import org.alfresco.service.cmr.lock.LockService; -import org.alfresco.service.cmr.lock.LockType; -import org.alfresco.service.cmr.rendition.CompositeRenditionDefinition; -import org.alfresco.service.cmr.rendition.RenderCallback; -import org.alfresco.service.cmr.rendition.RenderingEngineDefinition; -import org.alfresco.service.cmr.rendition.RenditionCancelledException; -import org.alfresco.service.cmr.rendition.RenditionDefinition; -import org.alfresco.service.cmr.rendition.RenditionService; -import org.alfresco.service.cmr.rendition.RenditionServiceException; -import org.alfresco.service.cmr.repository.ChildAssociationRef; -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.ContentWriter; -import org.alfresco.service.cmr.repository.CropSourceOptions.CropSourceOptionsSerializer; -import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.repository.NodeService; -import org.alfresco.service.cmr.repository.ScriptLocation; -import org.alfresco.service.cmr.repository.ScriptService; -import org.alfresco.service.cmr.repository.StoreRef; -import org.alfresco.service.cmr.repository.TransformationOptions; -import org.alfresco.service.namespace.NamespaceService; -import org.alfresco.service.namespace.QName; -import org.alfresco.service.namespace.RegexQNamePattern; -import org.alfresco.test_category.OwnJVMTestsCategory; -import org.alfresco.util.BaseAlfrescoSpringTest; -import org.alfresco.util.Pair; -import org.junit.experimental.categories.Category; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.imageio.ImageIO; + +import org.alfresco.model.ContentModel; +import org.alfresco.model.RenditionModel; +import org.alfresco.repo.action.RuntimeActionService; +import org.alfresco.repo.action.executer.ExporterActionExecuter; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.content.transform.AbstractContentTransformerTest; +import org.alfresco.repo.content.transform.ContentTransformer; +import org.alfresco.repo.content.transform.ContentTransformerRegistry; +import org.alfresco.repo.content.transform.UnimportantTransformException; +import org.alfresco.repo.content.transform.magick.ImageTransformationOptions; +import org.alfresco.repo.jscript.ClasspathScriptLocation; +import org.alfresco.repo.model.Repository; +import org.alfresco.repo.policy.BehaviourFilter; +import org.alfresco.repo.rendition.executer.AbstractRenderingEngine; +import org.alfresco.repo.rendition.executer.FreemarkerRenderingEngine; +import org.alfresco.repo.rendition.executer.ImageRenderingEngine; +import org.alfresco.repo.rendition.executer.ReformatRenderingEngine; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.attributes.AttributeService; +import org.alfresco.service.cmr.coci.CheckOutCheckInService; +import org.alfresco.service.cmr.lock.LockService; +import org.alfresco.service.cmr.lock.LockType; +import org.alfresco.service.cmr.rendition.CompositeRenditionDefinition; +import org.alfresco.service.cmr.rendition.RenderCallback; +import org.alfresco.service.cmr.rendition.RenderingEngineDefinition; +import org.alfresco.service.cmr.rendition.RenditionCancelledException; +import org.alfresco.service.cmr.rendition.RenditionDefinition; +import org.alfresco.service.cmr.rendition.RenditionService; +import org.alfresco.service.cmr.rendition.RenditionServiceException; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +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.ContentWriter; +import org.alfresco.service.cmr.repository.CropSourceOptions.CropSourceOptionsSerializer; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.ScriptLocation; +import org.alfresco.service.cmr.repository.ScriptService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.TransformationOptions; +import org.alfresco.service.cmr.version.VersionService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.test_category.OwnJVMTestsCategory; +import org.alfresco.util.BaseAlfrescoSpringTest; +import org.alfresco.util.Pair; +import org.junit.experimental.categories.Category; import org.springframework.context.ConfigurableApplicationContext; /** @@ -1292,7 +1294,7 @@ public class RenditionServiceIntegrationTest extends BaseAlfrescoSpringTest assertNull(results.getAssoc()); assertEquals("Expected a UnimportantTransformException", UnimportantTransformException.class, results.getThrowable().getClass()); - } + } /** * This method performs an asynchronous rendition and calls back the result to the @@ -2606,15 +2608,22 @@ public class RenditionServiceIntegrationTest extends BaseAlfrescoSpringTest assertTrue("upper left should be "+topLeft, Integer.toHexString(rgbAtTopLeft).endsWith(topLeft)); int rgbAtBottomRight = img.getRGB(img.getWidth() - 1, img.getHeight() - 1); assertTrue("lower right should be "+bottomRight, Integer.toHexString(rgbAtBottomRight).endsWith(bottomRight)); - } - + } + /** * A dummy rendering engine used in testing */ private static class DummyHelloWorldRenditionEngine extends AbstractRenderingEngine { private static final String ENGINE_NAME = "helloWorldRenderingEngine"; - + + public DummyHelloWorldRenditionEngine(ConfigurableApplicationContext ctx) + { + this.transactionService = (TransactionService)ctx.getBean("transactionService"); + this.attributeService = (AttributeService)ctx.getBean("attributeService"); + this.versionService = (VersionService)ctx.getBean("versionService"); + } + /** * Loads this executor into the ApplicationContext, if it * isn't already there @@ -2624,7 +2633,7 @@ public class RenditionServiceIntegrationTest extends BaseAlfrescoSpringTest if(!ctx.containsBean(ENGINE_NAME)) { // Create, and do dependencies - DummyHelloWorldRenditionEngine hw = new DummyHelloWorldRenditionEngine(); + DummyHelloWorldRenditionEngine hw = new DummyHelloWorldRenditionEngine(ctx); hw.setRuntimeActionService( (RuntimeActionService)ctx.getBean("actionService") ); @@ -2653,7 +2662,8 @@ public class RenditionServiceIntegrationTest extends BaseAlfrescoSpringTest } @Override - protected void render(RenderingContext context) { + protected void render(RenderingContext context) + { ContentWriter contentWriter = context.makeContentWriter(); contentWriter.setMimetype("text/plain"); contentWriter.putContent( "Hello, world!" ); diff --git a/source/test-java/org/alfresco/repo/thumbnail/ThumbnailServiceImplTest.java b/source/test-java/org/alfresco/repo/thumbnail/ThumbnailServiceImplTest.java index d4a592374a..29be17829c 100644 --- a/source/test-java/org/alfresco/repo/thumbnail/ThumbnailServiceImplTest.java +++ b/source/test-java/org/alfresco/repo/thumbnail/ThumbnailServiceImplTest.java @@ -27,8 +27,15 @@ package org.alfresco.repo.thumbnail; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.io.Serializable; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; import java.util.ArrayList; import java.util.Collections; import java.util.Date; @@ -47,12 +54,16 @@ import org.alfresco.repo.content.transform.ContentTransformer; import org.alfresco.repo.content.transform.magick.ImageResizeOptions; import org.alfresco.repo.content.transform.magick.ImageTransformationOptions; import org.alfresco.repo.jscript.ClasspathScriptLocation; +import org.alfresco.repo.rendition.executer.AbstractRenderingEngine; +import org.alfresco.repo.rendition.executer.ImageRenderingEngine; import org.alfresco.repo.thumbnail.script.ScriptThumbnailService; import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.action.Action; import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.attributes.AttributeService; +import org.alfresco.service.cmr.rendition.RenditionDefinition; import org.alfresco.service.cmr.rendition.RenditionService; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.ContentData; @@ -71,6 +82,7 @@ import org.alfresco.service.cmr.rule.RuleType; import org.alfresco.service.cmr.thumbnail.FailedThumbnailInfo; import org.alfresco.service.cmr.thumbnail.ThumbnailParentAssociationDetails; import org.alfresco.service.cmr.thumbnail.ThumbnailService; +import org.alfresco.service.cmr.version.VersionService; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.QNamePattern; @@ -80,6 +92,7 @@ import org.alfresco.test_category.OwnJVMTestsCategory; import org.alfresco.util.ApplicationContextHelper; import org.alfresco.util.BaseAlfrescoSpringTest; import org.alfresco.util.TempFileProvider; +import org.apache.commons.io.IOUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.experimental.categories.Category; @@ -102,6 +115,8 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest private ScriptService scriptService; private MimetypeMap mimetypeMap; private TransactionService transactionService; + private VersionService versionService; + private AttributeService attributeService; private ServiceRegistry services; private FailureHandlingOptions failureHandlingOptions; @@ -127,8 +142,10 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest this.scriptThumbnailService = (ScriptThumbnailService) this.applicationContext.getBean("thumbnailServiceScript"); this.mimetypeMap = (MimetypeMap) this.applicationContext.getBean("mimetypeService"); this.scriptService = (ScriptService) this.applicationContext.getBean("ScriptService"); + this.attributeService = (AttributeService) this.applicationContext.getBean("attributeService"); this.services = (ServiceRegistry) this.applicationContext.getBean("ServiceRegistry"); this.transactionService = (TransactionService) this.applicationContext.getBean("transactionService"); + this.versionService = (VersionService) this.applicationContext.getBean("versionService"); this.failureHandlingOptions = (FailureHandlingOptions) this.applicationContext.getBean("standardFailureOptions"); // Create a folder and some content @@ -169,7 +186,7 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest { QName qname = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "doclib"); - ThumbnailDefinition details = thumbnailService.getThumbnailRegistry().getThumbnailDefinition( + final ThumbnailDefinition details = thumbnailService.getThumbnailRegistry().getThumbnailDefinition( qname.getLocalName()); assertEquals("doclib", details.getName()); assertEquals("image/png", details.getMimetype()); @@ -177,21 +194,43 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest checkTransformer(); - NodeRef jpgOrig = createOriginalContent(this.folder, MimetypeMap.MIMETYPE_IMAGE_JPEG); + final NodeRef jpgOrig = createOriginalContent(this.folder, MimetypeMap.MIMETYPE_IMAGE_JPEG); + + setComplete(); + endTransaction(); + + final NodeRef thumbnail0 = transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public NodeRef execute() throws Throwable + { + NodeRef thumbnail = thumbnailService.createThumbnail(jpgOrig, ContentModel.PROP_CONTENT, + MimetypeMap.MIMETYPE_IMAGE_JPEG, details.getTransformationOptions(), "doclib"); + return thumbnail; + } + }, false, true); - NodeRef thumbnail0 = this.thumbnailService.createThumbnail(jpgOrig, ContentModel.PROP_CONTENT, - MimetypeMap.MIMETYPE_IMAGE_JPEG, details.getTransformationOptions(), "doclib"); assertNotNull(thumbnail0); - checkRenditioned(jpgOrig, Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "doclib", 1))); - checkRendition("doclib", thumbnail0); - outputThumbnailTempContentLocation(thumbnail0, "jpg", "doclib test"); + + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + checkRenditioned(jpgOrig, Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "doclib", 1))); + checkRendition(jpgOrig, "doclib", thumbnail0); + outputThumbnailTempContentLocation(thumbnail0, "jpg", "doclib test"); + + return null; + } + }, false, true); } public void testCreateRenditionThumbnailFromPdf() throws Exception { QName qname = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "doclib"); - ThumbnailDefinition details = thumbnailService.getThumbnailRegistry().getThumbnailDefinition( + final ThumbnailDefinition details = thumbnailService.getThumbnailRegistry().getThumbnailDefinition( qname.getLocalName()); assertEquals("doclib", details.getName()); assertEquals("image/png", details.getMimetype()); @@ -199,14 +238,36 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest checkTransformer(); - NodeRef pdfOrig = createOriginalContent(this.folder, MimetypeMap.MIMETYPE_PDF); + final NodeRef pdfOrig = createOriginalContent(this.folder, MimetypeMap.MIMETYPE_PDF); + + setComplete(); + endTransaction(); + + final NodeRef thumbnail0 = transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public NodeRef execute() throws Throwable + { + NodeRef thumbnail = thumbnailService.createThumbnail(pdfOrig, ContentModel.PROP_CONTENT, + MimetypeMap.MIMETYPE_IMAGE_JPEG, details.getTransformationOptions(), "doclib"); + return thumbnail; + } + }, false, true); - NodeRef thumbnail0 = this.thumbnailService.createThumbnail(pdfOrig, ContentModel.PROP_CONTENT, - MimetypeMap.MIMETYPE_IMAGE_JPEG, details.getTransformationOptions(), "doclib"); assertNotNull(thumbnail0); - checkRenditioned(pdfOrig, Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "doclib", 1))); - checkRendition("doclib", thumbnail0); - outputThumbnailTempContentLocation(thumbnail0, "jpg", "doclib test"); + + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + checkRenditioned(pdfOrig, Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "doclib", 1))); + checkRendition(pdfOrig, "doclib", thumbnail0); + outputThumbnailTempContentLocation(thumbnail0, "jpg", "doclib test"); + + return null; + } + }, false, true); } public void testCreateRenditionThumbnailFromPdfPage2() throws Exception @@ -222,119 +283,228 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest checkTransformer(); - NodeRef pdfOrig = createOriginalContent(this.folder, MimetypeMap.MIMETYPE_PDF); + final NodeRef pdfOrig = createOriginalContent(this.folder, MimetypeMap.MIMETYPE_PDF); - NodeRef thumbnail0 = this.thumbnailService.createThumbnail(pdfOrig, ContentModel.PROP_CONTENT, + final NodeRef thumbnail0 = this.thumbnailService.createThumbnail(pdfOrig, ContentModel.PROP_CONTENT, MimetypeMap.MIMETYPE_IMAGE_JPEG, thumbnailDefinition.getTransformationOptions(), "doclib_2"); + + setComplete(); + endTransaction(); + assertNotNull(thumbnail0); - checkRenditioned(pdfOrig, Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "doclib_2", 1))); - checkRendition("doclib_2", thumbnail0); - - // Check the length - File tempFile = TempFileProvider.createTempFile("thumbnailServiceImplTest", ".jpg"); - ContentReader reader = this.contentService.getReader(thumbnail0, ContentModel.PROP_CONTENT); - - long size = reader.getSize(); - System.out.println("size=" + size); - assertTrue("Page 2 should be blank and less than 4500 bytes", size < 4500); - - reader.getContent(tempFile); - System.out.println("doclib_2 test: " + tempFile.getPath()); + + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + checkRenditioned(pdfOrig, Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "doclib_2", 1))); + checkRendition(pdfOrig, "doclib_2", thumbnail0); + + // Check the length + File tempFile = TempFileProvider.createTempFile("thumbnailServiceImplTest", ".jpg"); + ContentReader reader = contentService.getReader(thumbnail0, ContentModel.PROP_CONTENT); + + long size = reader.getSize(); + System.out.println("size=" + size); + assertTrue("Page 2 should be blank and less than 4500 bytes", size < 4500); + + reader.getContent(tempFile); + + return null; + } + }, false, true); } public void testCreateThumbnailFromImage() throws Exception { checkTransformer(); - NodeRef jpgOrig = createOriginalContent(this.folder, MimetypeMap.MIMETYPE_IMAGE_JPEG); - NodeRef gifOrig = createOriginalContent(this.folder, MimetypeMap.MIMETYPE_IMAGE_GIF); + final NodeRef jpgOrig = createOriginalContent(folder, MimetypeMap.MIMETYPE_IMAGE_JPEG); + final NodeRef gifOrig = createOriginalContent(folder, MimetypeMap.MIMETYPE_IMAGE_GIF); + + setComplete(); + endTransaction(); // ===== small: 64x64, marked as thumbnail ==== - ImageResizeOptions imageResizeOptions = new ImageResizeOptions(); + final ImageResizeOptions imageResizeOptions = new ImageResizeOptions(); imageResizeOptions.setWidth(64); imageResizeOptions.setHeight(64); imageResizeOptions.setResizeToThumbnail(true); - ImageTransformationOptions imageTransformationOptions = new ImageTransformationOptions(); + final ImageTransformationOptions imageTransformationOptions = new ImageTransformationOptions(); imageTransformationOptions.setResizeOptions(imageResizeOptions); - // ThumbnailDetails createOptions = new ThumbnailDetails(); - NodeRef thumbnail1 = this.thumbnailService.createThumbnail(jpgOrig, ContentModel.PROP_CONTENT, - MimetypeMap.MIMETYPE_IMAGE_JPEG, imageTransformationOptions, "small"); + final NodeRef thumbnail1 = transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public NodeRef execute() throws Throwable + { + NodeRef thumbnail1 = thumbnailService.createThumbnail(jpgOrig, ContentModel.PROP_CONTENT, + MimetypeMap.MIMETYPE_IMAGE_JPEG, imageTransformationOptions, "small"); + return thumbnail1; + } + }, false, true); + assertNotNull(thumbnail1); - checkRenditioned(jpgOrig, Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "small", 1))); - checkRendition("small", thumbnail1); - outputThumbnailTempContentLocation(thumbnail1, "jpg", "small - 64x64, marked as thumbnail"); + + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + checkRenditioned(jpgOrig, Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "small", 1))); + checkRendition(jpgOrig, "small", thumbnail1); + outputThumbnailTempContentLocation(thumbnail1, "jpg", "small - 64x64, marked as thumbnail"); + return null; + } + }, false, true); // ===== small2: 64x64, aspect not maintained ==== - ImageResizeOptions imageResizeOptions2 = new ImageResizeOptions(); + final ImageResizeOptions imageResizeOptions2 = new ImageResizeOptions(); imageResizeOptions2.setWidth(64); imageResizeOptions2.setHeight(64); imageResizeOptions2.setMaintainAspectRatio(false); - ImageTransformationOptions imageTransformationOptions2 = new ImageTransformationOptions(); + final ImageTransformationOptions imageTransformationOptions2 = new ImageTransformationOptions(); imageTransformationOptions2.setResizeOptions(imageResizeOptions2); - // ThumbnailDetails createOptions2 = new ThumbnailDetails(); - NodeRef thumbnail2 = this.thumbnailService.createThumbnail(jpgOrig, ContentModel.PROP_CONTENT, - MimetypeMap.MIMETYPE_IMAGE_JPEG, imageTransformationOptions2, "small2"); - checkRenditioned(jpgOrig, Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "small2", 1))); - checkRendition("small2", thumbnail2); - outputThumbnailTempContentLocation(thumbnail2, "jpg", "small2 - 64x64, aspect not maintained"); + + final NodeRef thumbnail2 = transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public NodeRef execute() throws Throwable + { + NodeRef thumbnail2 = thumbnailService.createThumbnail(jpgOrig, ContentModel.PROP_CONTENT, + MimetypeMap.MIMETYPE_IMAGE_JPEG, imageTransformationOptions2, "small2"); + return thumbnail2; + } + }, false, true); + + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + checkRenditioned(jpgOrig, Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "small2", 1))); + checkRendition(jpgOrig, "small2", thumbnail2); + outputThumbnailTempContentLocation(thumbnail2, "jpg", "small2 - 64x64, aspect not maintained"); + + return null; + } + }, false, true); // ===== half: 50%x50 ===== - ImageResizeOptions imageResizeOptions3 = new ImageResizeOptions(); + final ImageResizeOptions imageResizeOptions3 = new ImageResizeOptions(); imageResizeOptions3.setWidth(50); imageResizeOptions3.setHeight(50); imageResizeOptions3.setPercentResize(true); - ImageTransformationOptions imageTransformationOptions3 = new ImageTransformationOptions(); + final ImageTransformationOptions imageTransformationOptions3 = new ImageTransformationOptions(); imageTransformationOptions3.setResizeOptions(imageResizeOptions3); - // ThumbnailDetails createOptions3 = new ThumbnailDetails(); - NodeRef thumbnail3 = this.thumbnailService.createThumbnail(jpgOrig, ContentModel.PROP_CONTENT, - MimetypeMap.MIMETYPE_IMAGE_JPEG, imageTransformationOptions3, "half"); - checkRenditioned(jpgOrig, - Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "half", 1))); - checkRendition("half", thumbnail3); - outputThumbnailTempContentLocation(thumbnail3, "jpg", "half - 50%x50%"); + + final NodeRef thumbnail3 = transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public NodeRef execute() throws Throwable + { + NodeRef thumbnail3 = thumbnailService.createThumbnail(jpgOrig, ContentModel.PROP_CONTENT, + MimetypeMap.MIMETYPE_IMAGE_JPEG, imageTransformationOptions3, "half"); + return thumbnail3; + } + }, false, true); + + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + checkRenditioned(jpgOrig, + Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "half", 1))); + checkRendition(jpgOrig, "half", thumbnail3); + outputThumbnailTempContentLocation(thumbnail3, "jpg", "half - 50%x50%"); + + return null; + } + }, false, true); // ===== half2: 50%x50 from gif ===== - ImageResizeOptions imageResizeOptions4 = new ImageResizeOptions(); + final ImageResizeOptions imageResizeOptions4 = new ImageResizeOptions(); imageResizeOptions4.setWidth(50); imageResizeOptions4.setHeight(50); imageResizeOptions4.setPercentResize(true); - ImageTransformationOptions imageTransformationOptions4 = new ImageTransformationOptions(); + final ImageTransformationOptions imageTransformationOptions4 = new ImageTransformationOptions(); imageTransformationOptions4.setResizeOptions(imageResizeOptions4); - // ThumbnailDetails createOptions4 = new ThumbnailDetails(); - NodeRef thumbnail4 = this.thumbnailService.createThumbnail(gifOrig, ContentModel.PROP_CONTENT, - MimetypeMap.MIMETYPE_IMAGE_JPEG, imageTransformationOptions4, "half2"); - checkRenditioned(gifOrig, Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "half2", 1))); - checkRendition("half2", thumbnail4); - outputThumbnailTempContentLocation(thumbnail4, "jpg", "half2 - 50%x50%, from gif"); + + final NodeRef thumbnail4 = transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public NodeRef execute() throws Throwable + { + NodeRef thumbnail4 = thumbnailService.createThumbnail(gifOrig, ContentModel.PROP_CONTENT, + MimetypeMap.MIMETYPE_IMAGE_JPEG, imageTransformationOptions4, "half2"); + return thumbnail4; + } + }, false, true); + + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + checkRenditioned(gifOrig, Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "half2", 1))); + checkRendition(gifOrig, "half2", thumbnail4); + outputThumbnailTempContentLocation(thumbnail4, "jpg", "half2 - 50%x50%, from gif"); + + return null; + } + }, false, true); } public void testDuplicationNames() throws Exception { checkTransformer(); - NodeRef jpgOrig = createOriginalContent(this.folder, MimetypeMap.MIMETYPE_IMAGE_JPEG); - ImageResizeOptions imageResizeOptions = new ImageResizeOptions(); + final NodeRef jpgOrig = createOriginalContent(this.folder, MimetypeMap.MIMETYPE_IMAGE_JPEG); + + setComplete(); + endTransaction(); + + final ImageResizeOptions imageResizeOptions = new ImageResizeOptions(); imageResizeOptions.setWidth(64); imageResizeOptions.setHeight(64); imageResizeOptions.setResizeToThumbnail(true); - ImageTransformationOptions imageTransformationOptions = new ImageTransformationOptions(); + final ImageTransformationOptions imageTransformationOptions = new ImageTransformationOptions(); imageTransformationOptions.setResizeOptions(imageResizeOptions); - // ThumbnailDetails createOptions = new ThumbnailDetails(); - NodeRef thumbnail1 = this.thumbnailService.createThumbnail(jpgOrig, ContentModel.PROP_CONTENT, - MimetypeMap.MIMETYPE_IMAGE_JPEG, imageTransformationOptions, "small"); + + final NodeRef thumbnail1 = transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public NodeRef execute() throws Throwable + { + NodeRef thumbnail1 = thumbnailService.createThumbnail(jpgOrig, ContentModel.PROP_CONTENT, + MimetypeMap.MIMETYPE_IMAGE_JPEG, imageTransformationOptions, "small"); + return thumbnail1; + } + }, false, true); + assertNotNull(thumbnail1); checkRenditioned(jpgOrig, Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "small", 1))); - checkRendition("small", thumbnail1); + checkRendition(jpgOrig, "small", thumbnail1); // the origional thumbnail is returned if we are attempting to create a duplicate - NodeRef duplicate = this.thumbnailService.createThumbnail(jpgOrig, ContentModel.PROP_CONTENT, MimetypeMap.MIMETYPE_IMAGE_JPEG, + final NodeRef duplicate = transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public NodeRef execute() throws Throwable + { + NodeRef thumbnail = thumbnailService.createThumbnail(jpgOrig, ContentModel.PROP_CONTENT, MimetypeMap.MIMETYPE_IMAGE_JPEG, imageTransformationOptions, "small"); + return thumbnail; + } + }, false, true); + assertNotNull(duplicate); assertEquals(duplicate, thumbnail1); } @@ -588,6 +758,96 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest assertEquals(ContentModel.TYPE_THUMBNAIL, secureNodeService.getType(thumbnail1)); } + private boolean isEqual(InputStream i1, InputStream i2) throws IOException + { + ReadableByteChannel ch1 = Channels.newChannel(i1); + ReadableByteChannel ch2 = Channels.newChannel(i2); + + ByteBuffer buf1 = ByteBuffer.allocateDirect(1024); + ByteBuffer buf2 = ByteBuffer.allocateDirect(1024); + + try { + while (true) { + + int n1 = ch1.read(buf1); + int n2 = ch2.read(buf2); + + if (n1 == -1 || n2 == -1) return n1 == n2; + + buf1.flip(); + buf2.flip(); + + for (int i = 0; i < Math.min(n1, n2); i++) + if (buf1.get() != buf2.get()) + return false; + + buf1.compact(); + buf2.compact(); + } + + } finally { + if (i1 != null) i1.close(); + if (i2 != null) i2.close(); + } + } + + /** + * Test that multiple updates to source content generate different thumbnails. + * + * @throws Exception + */ + public void testMultipleThumbnailUpdates() throws Exception + { + checkTransformer(); + + NodeRef content = createOriginalContent(this.folder, MimetypeMap.MIMETYPE_IMAGE_JPEG); + + assertEquals(MimetypeMap.MIMETYPE_IMAGE_JPEG, + contentService.getReader(content, ContentModel.PROP_CONTENT).getMimetype()); + + // Create a thumbnail + ImageResizeOptions imageResizeOptions = new ImageResizeOptions(); + imageResizeOptions.setWidth(64); + imageResizeOptions.setHeight(64); + imageResizeOptions.setResizeToThumbnail(true); + ImageTransformationOptions imageTransformationOptions = new ImageTransformationOptions(); + imageTransformationOptions.setResizeOptions(imageResizeOptions); + NodeRef thumbnail = this.thumbnailService.createThumbnail(content, ContentModel.PROP_CONTENT, + MimetypeMap.MIMETYPE_IMAGE_JPEG, imageTransformationOptions, "small"); + + // Thumbnails should always be of type cm:thumbnail. + assertEquals(ContentModel.TYPE_THUMBNAIL, secureNodeService.getType(thumbnail)); + + // make a copy of the thumbnail content + ContentReader thumbnailReader = contentService.getReader(thumbnail, ContentModel.PROP_CONTENT); + InputStream thumbnailStream = thumbnailReader.getContentInputStream(); + File file = TempFileProvider.createTempFile(getClass().getName(), "jpg"); + OutputStream out = new FileOutputStream(file); + IOUtils.copy(thumbnailStream, out); + InputStream oldThumbnailStream = new FileInputStream(file); + + // update the source node content + file = AbstractContentTransformerTest.loadNamedQuickTestFile("quickGEO.jpg"); + ContentWriter writer = this.contentService.getWriter(content, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_IMAGE_JPEG); + writer.setEncoding("UTF-8"); + writer.putContent(file); + assertEquals(MimetypeMap.MIMETYPE_IMAGE_JPEG, + contentService.getReader(content, ContentModel.PROP_CONTENT).getMimetype()); + + // update the thumbnail + this.thumbnailService.updateThumbnail(thumbnail, imageTransformationOptions); + + // Thumbnails should always be of type cm:thumbnail. + assertEquals(ContentModel.TYPE_THUMBNAIL, secureNodeService.getType(thumbnail)); + + thumbnailReader = contentService.getReader(thumbnail, ContentModel.PROP_CONTENT); + thumbnailStream = thumbnailReader.getContentInputStream(); + + // the thumbnail content should be different + assertFalse(isEqual(oldThumbnailStream, thumbnailStream)); + } + public void testGetThumbnailByName() throws Exception { checkTransformer(); @@ -608,10 +868,13 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest this.thumbnailService.createThumbnail(jpgOrig, ContentModel.PROP_CONTENT, MimetypeMap.MIMETYPE_IMAGE_JPEG, imageTransformationOptions, "small"); + setComplete(); + endTransaction(); + // Try and retrieve the thumbnail NodeRef result2 = this.thumbnailService.getThumbnailByName(jpgOrig, ContentModel.PROP_CONTENT, "small"); assertNotNull(result2); - checkRendition("small", result2); + checkRendition(jpgOrig, "small", result2); // Check for an other thumbnail that doesn't exist NodeRef result3 = this.thumbnailService.getThumbnailByName(jpgOrig, ContentModel.PROP_CONTENT, "anotherone"); @@ -621,14 +884,23 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest private static class ExpectedAssoc { private QNamePattern assocTypeQName; - private String assocName; + private QNamePattern assocName; private int count; public ExpectedAssoc(QNamePattern assocTypeQName, String assocName, int count) { super(); + this.assocName = assocName != null + ? QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, assocName) : null; this.assocTypeQName = assocTypeQName; + this.count = count; + } + + public ExpectedAssoc(QNamePattern assocTypeQName, QNamePattern assocName, int count) + { + super(); this.assocName = assocName; + this.assocTypeQName = assocTypeQName; this.count = count; } @@ -637,7 +909,7 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest return assocTypeQName; } - public String getAssocName() + public QNamePattern getAssocName() { return assocName; } @@ -692,21 +964,21 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest } } - private void checkRenditioned(NodeRef contentNodeRef, List expectedAssocs) { + private void checkRenditioned(NodeRef contentNodeRef, List expectedAssocs) + { assertTrue("Renditioned aspect should have been applied", this.secureNodeService.hasAspect(contentNodeRef, RenditionModel.ASPECT_RENDITIONED)); - for (ExpectedAssoc expectedAssoc : expectedAssocs) { - QNamePattern qNamePattern = expectedAssoc.getAssocName() != null - ? QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, expectedAssoc.getAssocName()) : null; + for (ExpectedAssoc expectedAssoc : expectedAssocs) + { List assocs = this.secureNodeService.getChildAssocs(contentNodeRef, - expectedAssoc.getAssocTypeQName(), qNamePattern); + expectedAssoc.getAssocTypeQName(), expectedAssoc.getAssocName()); assertNotNull(assocs); assertEquals(expectedAssoc + " association count mismatch", expectedAssoc.getCount(), assocs.size()); } } - private void checkRendition(String thumbnailName, NodeRef thumbnail) + private void checkRendition(final NodeRef sourceNode, final String thumbnailName, final NodeRef thumbnail) { // Check the thumbnail is of the correct type assertTrue("Thumbnail should have been a rendition", @@ -732,6 +1004,29 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest ContentData thumbnailData = (ContentData) secureNodeService.getProperty(thumbnail, ContentModel.PROP_CONTENT); assertNotNull("Thumbnail data was null", thumbnailData); assertTrue("Thumbnail data was empty", thumbnailData.getSize() > 0); + + if(sourceNode != null) + { + final String renderedContentKey = AbstractRenderingEngine.getRenderedContentKey(sourceNode, versionService); + QName renditionName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, thumbnailName); + final RenditionDefinition renditionDef = renditionService.createRenditionDefinition(renditionName, + ImageRenderingEngine.NAME); + + final QName thumbnailNameQName = thumbnailName != null ? + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, thumbnailName) : null; + logger.debug("checkedRendition: " + sourceNode + " " + thumbnailNameQName); + String contentUrl = transactionService.getRetryingTransactionHelper() + .doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public String execute() throws Throwable + { + String contentUrl = (String)attributeService.getAttribute("RENDITIONED_CONTENT", renderedContentKey, + renditionDef.getRenditionName()); + return contentUrl; + }; + }, false, true); + assertNull("Cached rendition contentUrl was not cleaned up", contentUrl); + } } private void outputThumbnailTempContentLocation(NodeRef thumbnail, String ext, String message) throws IOException @@ -769,7 +1064,18 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest return node; } - + + private void updateContent(NodeRef node, String mimetype) throws IOException + { + String ext = this.mimetypeMap.getExtension(mimetype); + File file = AbstractContentTransformerTest.loadNamedQuickTestFile("quickGEO.jpg"); + + ContentWriter writer = this.contentService.getWriter(node, ContentModel.PROP_CONTENT, true); + writer.setMimetype(mimetype); + writer.setEncoding("UTF-8"); + writer.putContent(file); + } + private NodeRef createCorruptedContent(NodeRef parentFolder) throws IOException { // The below pdf file has been truncated such that it is identifiable as a PDF but otherwise corrupt. @@ -870,33 +1176,74 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest .getChildRef(); checkTransformer(); - NodeRef jpgOrig = createOriginalContent(this.folder, MimetypeMap.MIMETYPE_IMAGE_JPEG); - - ImageResizeOptions imageResizeOptions = new ImageResizeOptions(); + final NodeRef jpgOrig = createOriginalContent(this.folder, MimetypeMap.MIMETYPE_IMAGE_JPEG); + + setComplete(); + endTransaction(); + + final ImageResizeOptions imageResizeOptions = new ImageResizeOptions(); imageResizeOptions.setWidth(64); imageResizeOptions.setHeight(64); imageResizeOptions.setResizeToThumbnail(true); - ImageTransformationOptions imageTransformationOptions = new ImageTransformationOptions(); + final ImageTransformationOptions imageTransformationOptions = new ImageTransformationOptions(); imageTransformationOptions.setResizeOptions(imageResizeOptions); - - // Create thumbnail - same MIME type - NodeRef thumbnail1 = this.thumbnailService.createThumbnail(jpgOrig, ContentModel.PROP_CONTENT, - MimetypeMap.MIMETYPE_IMAGE_JPEG, imageTransformationOptions, "smallJpeg"); + + final NodeRef thumbnail1 = transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public NodeRef execute() throws Throwable + { + // Create thumbnail - same MIME type + NodeRef thumbnail = thumbnailService.createThumbnail(jpgOrig, ContentModel.PROP_CONTENT, + MimetypeMap.MIMETYPE_IMAGE_JPEG, imageTransformationOptions, "smallJpeg"); + return thumbnail; + } + }, false, true); + assertNotNull(thumbnail1); - checkRenditioned(jpgOrig, - Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "smallJpeg", 1))); - checkRendition("smallJpeg", thumbnail1); - outputThumbnailTempContentLocation(thumbnail1, "jpg", "smallJpeg - 64x64, marked as thumbnail"); + + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + checkRenditioned(jpgOrig, + Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "smallJpeg", 1))); + checkRendition(jpgOrig, "smallJpeg", thumbnail1); + outputThumbnailTempContentLocation(thumbnail1, "jpg", "smallJpeg - 64x64, marked as thumbnail"); + return null; + } + }, false, true); // Create thumbnail - different MIME type - thumbnail1 = this.thumbnailService.createThumbnail(jpgOrig, ContentModel.PROP_CONTENT, - MimetypeMap.MIMETYPE_IMAGE_PNG, imageTransformationOptions, "smallPng"); - assertNotNull(thumbnail1); - checkRenditioned(jpgOrig, - Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "smallPng", 1))); - checkRendition("smallPng", thumbnail1); - outputThumbnailTempContentLocation(thumbnail1, "png", "smallPng - 64x64, marked as thumbnail"); - + final NodeRef thumbnail2 = transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public NodeRef execute() throws Throwable + { + // Create thumbnail - same MIME type + NodeRef thumbnail = thumbnailService.createThumbnail(jpgOrig, ContentModel.PROP_CONTENT, + MimetypeMap.MIMETYPE_IMAGE_PNG, imageTransformationOptions, "smallPng"); + return thumbnail; + } + }, false, true); + + assertNotNull(thumbnail2); + + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + checkRenditioned(jpgOrig, + Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "smallPng", 1))); + checkRendition(jpgOrig, "smallPng", thumbnail2); + outputThumbnailTempContentLocation(thumbnail2, "png", "smallPng - 64x64, marked as thumbnail"); + + return null; + } + }, false, true); + // Create thumbnail - different content property // TODO @@ -906,8 +1253,18 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest try { imageTransformationOptions.setCommandOptions("-noSuchOption"); - thumbnail1 = this.thumbnailService.createThumbnail(jpgOrig, ContentModel.PROP_CONTENT, - MimetypeMap.MIMETYPE_IMAGE_PNG, imageTransformationOptions, "smallCO"); + + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + // Create thumbnail - same MIME type + thumbnailService.createThumbnail(jpgOrig, ContentModel.PROP_CONTENT, + MimetypeMap.MIMETYPE_IMAGE_PNG, imageTransformationOptions, "smallCO"); + return null; + } + }, false, true); } catch (ContentIOException ciox) { x = ciox; @@ -919,28 +1276,46 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest // Create thumbnail - different target assoc details - ThumbnailParentAssociationDetails tpad + final ThumbnailParentAssociationDetails tpad = new ThumbnailParentAssociationDetails(otherFolder, QName.createQName(NamespaceService.RENDITION_MODEL_1_0_URI, "foo"), QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "bar")); - thumbnail1 = this.thumbnailService.createThumbnail(jpgOrig, ContentModel.PROP_CONTENT, + final NodeRef thumbnail4 = this.thumbnailService.createThumbnail(jpgOrig, ContentModel.PROP_CONTENT, MimetypeMap.MIMETYPE_IMAGE_PNG, imageTransformationOptions, "targetDetails", tpad); - assertNotNull(thumbnail1); - checkRenditioned(jpgOrig, - Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "targetDetails", 1))); - checkRendition("targetDetails", thumbnail1); - outputThumbnailTempContentLocation(thumbnail1, "png", "targetDetails - 64x64, marked as thumbnail"); + assertNotNull(thumbnail4); + + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + checkRenditioned(jpgOrig, + Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "targetDetails", 1))); + checkRendition(jpgOrig, "targetDetails", thumbnail4); + outputThumbnailTempContentLocation(thumbnail4, "png", "targetDetails - 64x64, marked as thumbnail"); + + return null; + } + }, false, true); - - // Create thumbnail - null thumbnail name - thumbnail1 = this.thumbnailService.createThumbnail(jpgOrig, ContentModel.PROP_CONTENT, + final NodeRef thumbnail5 = this.thumbnailService.createThumbnail(jpgOrig, ContentModel.PROP_CONTENT, MimetypeMap.MIMETYPE_IMAGE_PNG, imageTransformationOptions, null); - assertNotNull(thumbnail1); - checkRenditioned(jpgOrig, - Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, null, 5))); - checkRendition(null, thumbnail1); - outputThumbnailTempContentLocation(thumbnail1, "png", "'null' - 64x64, marked as thumbnail"); + assertNotNull(thumbnail5); + + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + // we expected 4 rendition associations + checkRenditioned(jpgOrig, Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, (QNamePattern)null, 4))); + checkRendition(null, null, thumbnail5); + outputThumbnailTempContentLocation(thumbnail5, "png", "'null' - 64x64, marked as thumbnail"); + + return null; + } + }, false, true); } public void testRegistry() @@ -1005,9 +1380,8 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest assertEquals(standardMediumIcon, mediumIcon); } - protected void performLongRunningThumbnailTest(final List expectedThumbnails, - final List expectedAssocs, final LongRunningConcurrentWork concurrentWork, - final Integer retryPeriod, final Integer quietPeriod) throws Exception + protected void performLongRunningThumbnailTest(final List expectedThumbnails, final List expectedAssocs, + final LongRunningConcurrentWork concurrentWork, final Integer retryPeriod, final Integer quietPeriod) throws Exception { long saveRetryPeriod = failureHandlingOptions.getRetryPeriod(); long saveQuietPeriod = failureHandlingOptions.getQuietPeriod(); @@ -1041,7 +1415,8 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest } // Create our thumbnail(s) - for (ExpectedThumbnail expectedThumbnail : expectedThumbnails) { + for (ExpectedThumbnail expectedThumbnail : expectedThumbnails) + { ThumbnailDefinition thumbnailDef = thumbnailService.getThumbnailRegistry() .getThumbnailDefinition(expectedThumbnail.getThumbnailName()); @@ -1056,13 +1431,15 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest // Thumbnailing process(es) are running in other threads, do the // concurrent work here - if (concurrentWork != null) { + if (concurrentWork != null) + { logger.debug("Starting concurrent work for " + source); concurrentWork.run(source); } // Verify our concurrent work ran successfully - if (concurrentWork != null) { + if (concurrentWork != null) + { logger.debug("Verifying concurrent work for " + source); concurrentWork.verify(source); } @@ -1071,17 +1448,21 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest // Wait for thumbnail(s) to finish long endTime = (new Date()).getTime(); - for (final ExpectedThumbnail expectedThumbnail : expectedThumbnails) { + for (final ExpectedThumbnail expectedThumbnail : expectedThumbnails) + { NodeRef thumbnail = null; - while ((endTime - startTime) < (TEST_LONG_RUNNING_TRANSFORM_TIME * numIterations)) { + while ((endTime - startTime) < (TEST_LONG_RUNNING_TRANSFORM_TIME * numIterations)) + { thumbnail = transactionService.getRetryingTransactionHelper() - .doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() { + .doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { public NodeRef execute() throws Throwable { return thumbnailService.getThumbnailByName(source, ContentModel.PROP_CONTENT, expectedThumbnail.getThumbnailName()); } }, false, true); - if (thumbnail == null) { + if (thumbnail == null) + { Thread.sleep(200); logger.debug("Elapsed " + (endTime - startTime) + " ms of " + TEST_LONG_RUNNING_TRANSFORM_TIME * numIterations + " ms waiting for " @@ -1096,27 +1477,33 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest } transactionService.getRetryingTransactionHelper() - .doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() { - public Void execute() throws Throwable { + .doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { // Verify that the thumbnail(s) was/were created - for (final ExpectedThumbnail expectedThumbnail : expectedThumbnails) { + for (final ExpectedThumbnail expectedThumbnail : expectedThumbnails) + { String thumbnailName = expectedThumbnail.getThumbnailName(); NodeRef thumbnailNodeRef = thumbnailService.getThumbnailByName(source, ContentModel.PROP_CONTENT, thumbnailName); - checkRendition(thumbnailName, thumbnailNodeRef); + checkRendition(source, thumbnailName, thumbnailNodeRef); } // verify associations checkRenditioned(source, expectedAssocs); return null; - }; - }); + }; + }); + + // we expect the transformer to run once for each thumbnail + assertEquals(expectedThumbnails.size(), transformer.getTransformCount()); } finally { - failureHandlingOptions.setRetryPeriod(saveRetryPeriod); - failureHandlingOptions.setQuietPeriod(saveQuietPeriod); + failureHandlingOptions.setRetryPeriod(saveRetryPeriod); + failureHandlingOptions.setQuietPeriod(saveQuietPeriod); } } @@ -1129,10 +1516,9 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest public void testLongRunningThumbnails() throws Exception { logger.debug("Starting testLongRunningThumbnails"); - performLongRunningThumbnailTest( - Collections.singletonList(ExpectedThumbnail.withName("imgpreview")), - Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "imgpreview", 1)), - new EmptyLongRunningConcurrentWork(), 60, null); + performLongRunningThumbnailTest(Collections.singletonList(ExpectedThumbnail.withName("imgpreview")), + Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "imgpreview", 1)), + new EmptyLongRunningConcurrentWork(), 60, null); } /** @@ -1166,22 +1552,18 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest } }; + // we expect an imgpreview thumbnail association but no failed thumbnail association + List expectedAssocs = new ArrayList<>(2); + expectedAssocs.add(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "imgpreview", 1)); + expectedAssocs.add(new ExpectedAssoc(ContentModel.ASSOC_FAILED_THUMBNAIL, RegexQNamePattern.MATCH_ALL, 0)); + performLongRunningThumbnailTest( - Collections.singletonList(ExpectedThumbnail.withName("imgpreview")), - Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "imgpreview", 1)), - updatePropertyWork, 1, null); + Collections.singletonList(ExpectedThumbnail.withName("imgpreview")), expectedAssocs, updatePropertyWork, 1, null); } /** * Verifies that multiple thumbnails can be successfully created. * - * Note: the current architecture of the thumbnail and rendition services can't guarantee that there - * won't be failed thumbnails for the scenario covered by this test. In particular, given long-running - * thumbnails/renditions, the concurrent creation of more than one thumbnail may fail with a primary key constraint - * exception because both transactions try to add the same aspect to the same parent content node. Whilst - * the retrying transaction handler correctly handles this scenario, the {@link org.alfresco.service.cmr.action.ActionService} - * incorrectly generates a compensating action (failed thumbnail) when in fact the thumbnail creation is recoverable. - * * @throws Exception */ public void testCreateMultipleLongRunningThumbnails() throws Exception @@ -1198,7 +1580,6 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest List expectedAssocs = new ArrayList<>(5); expectedAssocs.add(new ExpectedAssoc(RenditionModel.ASSOC_RENDITION, "imgpreview", 1)); expectedAssocs.add(new ExpectedAssoc(RenditionModel.ASSOC_RENDITION, "avatar", 1)); -// expectedAssocs.add(new ExpectedAssoc(ContentModel.ASSOC_FAILED_THUMBNAIL, null, 1)); performLongRunningThumbnailTest(expectedThumbnails, expectedAssocs, new EmptyLongRunningConcurrentWork(), 1, 1); }