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
This commit is contained in:
Steven Glover
2016-09-15 13:06:35 +00:00
parent f5a2ec747b
commit e83e9f4bc1
6 changed files with 1217 additions and 571 deletions

View File

@@ -122,6 +122,15 @@
<property name="contentService">
<ref bean="ContentService" />
</property>
<property name="versionService">
<ref bean="versionService" />
</property>
<property name="attributeService">
<ref bean="AttributeService" />
</property>
<property name="transactionService">
<ref bean="transactionService" />
</property>
<property name="applicableTypes">
<list>
<value>{http://www.alfresco.org/model/content/1.0}content</value>

View File

@@ -36,6 +36,7 @@ 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;
@@ -53,12 +54,15 @@ 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;
@@ -71,10 +75,15 @@ 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;
@@ -89,12 +98,14 @@ 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();
@@ -145,6 +156,9 @@ public abstract class AbstractRenderingEngine extends ActionExecuterAbstractBase
protected ContentService contentService;
protected MimetypeMap mimetypeMap;
protected ActionTrackingService actionTrackingService;
protected AttributeService attributeService;
protected TransactionService transactionService;
protected VersionService versionService;
/* Parameter names common to all Rendering Actions */
/**
@@ -216,6 +230,21 @@ 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.
*
@@ -527,7 +556,9 @@ public abstract class AbstractRenderingEngine extends ActionExecuterAbstractBase
RenderingContext context = new RenderingContext(sourceNode,
renditionDefinition,
targetContentProp);
render(context);
// This is a workaround for the fact that actions don't have return
// values.
action.getParameterValues().put(PARAM_RESULT, context.getChildAssociationRef());
@@ -760,12 +791,24 @@ public abstract class AbstractRenderingEngine extends ActionExecuterAbstractBase
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;
/**
@@ -773,13 +816,95 @@ public abstract class AbstractRenderingEngine extends ActionExecuterAbstractBase
* @param definition RenditionDefinition
* @param renditionContentProperty QName
*/
public RenderingContext(NodeRef sourceNode,//
RenditionDefinition definition,//
QName renditionContentProperty)
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<Void>()
{
@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;
}
/**
@@ -800,6 +925,7 @@ public abstract class AbstractRenderingEngine extends ActionExecuterAbstractBase
{
this.caNodeRef = createRenditionNodeAssoc(sourceNode, definition);
}
return this.caNodeRef;
}
@@ -861,8 +987,22 @@ public abstract class AbstractRenderingEngine extends ActionExecuterAbstractBase
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 void tagSourceNodeAsRenditioned(final RenditionDefinition renditionDef, final NodeRef actionedUponNodeRef)
{
@@ -1121,4 +1261,68 @@ public abstract class AbstractRenderingEngine extends ActionExecuterAbstractBase
}
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<RenderingContext> renderingContexts = TransactionalResourceHelper.getSet(RENDERING_CONTEXTS);
if(logger.isDebugEnabled())
{
logger.debug("Cleaning up " + renderingContexts.size() + " rendering contexts");
}
if(!renderingContexts.isEmpty())
{
transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>()
{
@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()
{
}
}

View File

@@ -36,6 +36,9 @@ 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;
@@ -56,13 +59,17 @@ 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);
@@ -113,6 +120,8 @@ public abstract class AbstractTransformationRenderingEngine extends AbstractRend
*/
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";
private static final String NOT_TRANSFORMABLE_MESSAGE_PATTERN = "Content not transformable for '%s' source mime type and '%s' target mime type. Operation can't be performed";
@@ -120,11 +129,18 @@ public abstract class AbstractTransformationRenderingEngine extends AbstractRend
private Collection<TransformationSourceOptionsSerializer> sourceOptionsSerializers;
private ContentStore tempStore;
public Collection<TransformationSourceOptionsSerializer> getSourceOptionsSerializers()
{
return sourceOptionsSerializers;
}
public void setApplicationContext(ApplicationContext applicationContext)
{
this.applicationContext = applicationContext;
}
public void setSourceOptionsSerializers(Collection<TransformationSourceOptionsSerializer> sourceOptionsSerializers)
{
this.sourceOptionsSerializers = sourceOptionsSerializers;
@@ -158,6 +174,15 @@ public abstract class AbstractTransformationRenderingEngine extends AbstractRend
{
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());
}
/*
@@ -167,6 +192,26 @@ public abstract class AbstractTransformationRenderingEngine extends AbstractRend
@Override
protected void render(RenderingContext context)
{
// check for existing transformed content and reuse if present to avoid re-rendering
String existingTransformedContentUrl = context.getExistingTransformedContentUrl();
if(existingTransformedContentUrl != null)
{
if(logger.isDebugEnabled())
{
logger.debug("Reusing existing rendered content " + existingTransformedContentUrl
+ " for rendering context " + context);
}
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();
@@ -288,8 +333,6 @@ public abstract class AbstractTransformationRenderingEngine extends AbstractRend
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
@@ -299,7 +342,11 @@ public abstract class AbstractTransformationRenderingEngine extends AbstractRend
// We should never be in this state, but just in case
throw new RenditionServiceException("Target of transformation not present");
}
writer.putContent(tempTarget.getReader().getContentInputStream());
// save transform result in case we need to retry
context.setExistingTransformedContentUrl(tempTarget.getReader().getContentUrl());
copyTransformedContent(tempTarget.getReader(), context);
}
catch (ExecutionException e)
{
@@ -318,6 +365,7 @@ public abstract class AbstractTransformationRenderingEngine extends AbstractRend
}
}
}
}
protected abstract TransformationOptions getTransformOptions(RenderingContext context);
@@ -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
{

View File

@@ -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<NodeRef>()
{
@@ -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);

View File

@@ -41,7 +41,6 @@ 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;
@@ -63,6 +62,7 @@ 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;
@@ -86,9 +86,11 @@ 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;
@@ -2615,6 +2617,13 @@ public class RenditionServiceIntegrationTest extends BaseAlfrescoSpringTest
{
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!" );

View File

@@ -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);
NodeRef thumbnail0 = this.thumbnailService.createThumbnail(jpgOrig, ContentModel.PROP_CONTENT,
setComplete();
endTransaction();
final NodeRef thumbnail0 = transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<NodeRef>()
{
@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);
assertNotNull(thumbnail0);
transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>()
{
@Override
public Void execute() throws Throwable
{
checkRenditioned(jpgOrig, Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "doclib", 1)));
checkRendition("doclib", thumbnail0);
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);
NodeRef thumbnail0 = this.thumbnailService.createThumbnail(pdfOrig, ContentModel.PROP_CONTENT,
setComplete();
endTransaction();
final NodeRef thumbnail0 = transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<NodeRef>()
{
@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);
assertNotNull(thumbnail0);
transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>()
{
@Override
public Void execute() throws Throwable
{
checkRenditioned(pdfOrig, Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "doclib", 1)));
checkRendition("doclib", thumbnail0);
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);
transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>()
{
@Override
public Void execute() throws Throwable
{
checkRenditioned(pdfOrig, Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "doclib_2", 1)));
checkRendition("doclib_2", thumbnail0);
checkRendition(pdfOrig, "doclib_2", thumbnail0);
// Check the length
File tempFile = TempFileProvider.createTempFile("thumbnailServiceImplTest", ".jpg");
ContentReader reader = this.contentService.getReader(thumbnail0, ContentModel.PROP_CONTENT);
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);
System.out.println("doclib_2 test: " + tempFile.getPath());
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,
final NodeRef thumbnail1 = transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<NodeRef>()
{
@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);
transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>()
{
@Override
public Void execute() throws Throwable
{
checkRenditioned(jpgOrig, Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "small", 1)));
checkRendition("small", thumbnail1);
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,
final NodeRef thumbnail2 = transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<NodeRef>()
{
@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<Void>()
{
@Override
public Void execute() throws Throwable
{
checkRenditioned(jpgOrig, Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "small2", 1)));
checkRendition("small2", thumbnail2);
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,
final NodeRef thumbnail3 = transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<NodeRef>()
{
@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<Void>()
{
@Override
public Void execute() throws Throwable
{
checkRenditioned(jpgOrig,
Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "half", 1)));
checkRendition("half", thumbnail3);
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,
final NodeRef thumbnail4 = transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<NodeRef>()
{
@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<Void>()
{
@Override
public Void execute() throws Throwable
{
checkRenditioned(gifOrig, Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "half2", 1)));
checkRendition("half2", thumbnail4);
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,
final NodeRef thumbnail1 = transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<NodeRef>()
{
@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<NodeRef>()
{
@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<ExpectedAssoc> expectedAssocs) {
private void checkRenditioned(NodeRef contentNodeRef, List<ExpectedAssoc> 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<ChildAssociationRef> 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<String>()
{
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
@@ -770,6 +1065,17 @@ 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,32 +1176,73 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest
.getChildRef();
checkTransformer();
NodeRef jpgOrig = createOriginalContent(this.folder, MimetypeMap.MIMETYPE_IMAGE_JPEG);
final NodeRef jpgOrig = createOriginalContent(this.folder, MimetypeMap.MIMETYPE_IMAGE_JPEG);
ImageResizeOptions imageResizeOptions = new ImageResizeOptions();
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);
final NodeRef thumbnail1 = transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<NodeRef>()
{
@Override
public NodeRef execute() throws Throwable
{
// Create thumbnail - same MIME type
NodeRef thumbnail1 = this.thumbnailService.createThumbnail(jpgOrig, ContentModel.PROP_CONTENT,
NodeRef thumbnail = thumbnailService.createThumbnail(jpgOrig, ContentModel.PROP_CONTENT,
MimetypeMap.MIMETYPE_IMAGE_JPEG, imageTransformationOptions, "smallJpeg");
return thumbnail;
}
}, false, true);
assertNotNull(thumbnail1);
transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>()
{
@Override
public Void execute() throws Throwable
{
checkRenditioned(jpgOrig,
Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "smallJpeg", 1)));
checkRendition("smallJpeg", thumbnail1);
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,
final NodeRef thumbnail2 = transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<NodeRef>()
{
@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");
assertNotNull(thumbnail1);
return thumbnail;
}
}, false, true);
assertNotNull(thumbnail2);
transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>()
{
@Override
public Void execute() throws Throwable
{
checkRenditioned(jpgOrig,
Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "smallPng", 1)));
checkRendition("smallPng", thumbnail1);
outputThumbnailTempContentLocation(thumbnail1, "png", "smallPng - 64x64, marked as thumbnail");
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,
transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>()
{
@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);
assertNotNull(thumbnail4);
transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>()
{
@Override
public Void execute() throws Throwable
{
checkRenditioned(jpgOrig,
Collections.singletonList(new ExpectedAssoc(RegexQNamePattern.MATCH_ALL, "targetDetails", 1)));
checkRendition("targetDetails", thumbnail1);
outputThumbnailTempContentLocation(thumbnail1, "png", "targetDetails - 64x64, marked as thumbnail");
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<Void>()
{
@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<ExpectedThumbnail> expectedThumbnails,
final List<ExpectedAssoc> expectedAssocs, final LongRunningConcurrentWork concurrentWork,
final Integer retryPeriod, final Integer quietPeriod) throws Exception
protected void performLongRunningThumbnailTest(final List<ExpectedThumbnail> expectedThumbnails, final List<ExpectedAssoc> 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<NodeRef>() {
.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<NodeRef>()
{
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,14 +1477,17 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest
}
transactionService.getRetryingTransactionHelper()
.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
public Void execute() throws Throwable {
.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>()
{
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
@@ -1112,6 +1496,9 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest
return null;
};
});
// we expect the transformer to run once for each thumbnail
assertEquals(expectedThumbnails.size(), transformer.getTransformCount());
}
finally
{
@@ -1129,8 +1516,7 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest
public void testLongRunningThumbnails() throws Exception
{
logger.debug("Starting testLongRunningThumbnails");
performLongRunningThumbnailTest(
Collections.singletonList(ExpectedThumbnail.withName("imgpreview")),
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<ExpectedAssoc> 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<ExpectedAssoc> 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);
}