[MNT-24992] Add method to force renditions content hash code (#3288) (#3291)

This commit is contained in:
Tiago Salvado
2025-03-31 15:00:23 +01:00
committed by GitHub
parent bb534040a1
commit 8276344ce6
5 changed files with 381 additions and 313 deletions

View File

@@ -1539,7 +1539,7 @@
"filename": "repository/src/test/java/org/alfresco/repo/rendition2/AbstractRenditionIntegrationTest.java", "filename": "repository/src/test/java/org/alfresco/repo/rendition2/AbstractRenditionIntegrationTest.java",
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", "hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
"is_verified": false, "is_verified": false,
"line_number": 127, "line_number": 130,
"is_secret": false "is_secret": false
} }
], ],
@@ -1888,5 +1888,5 @@
} }
] ]
}, },
"generated_at": "2025-01-10T19:40:38Z" "generated_at": "2025-03-31T12:42:09Z"
} }

View File

@@ -2,7 +2,7 @@
* #%L * #%L
* Alfresco Repository * Alfresco Repository
* %% * %%
* Copyright (C) 2005 - 2022 Alfresco Software Limited * Copyright (C) 2005 - 2025 Alfresco Software Limited
* %% * %%
* This file is part of the Alfresco software. * This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of * If the software was purchased under a paid Alfresco license, the terms of
@@ -25,35 +25,27 @@
*/ */
package org.alfresco.repo.rendition2; package org.alfresco.repo.rendition2;
import java.util.List;
import org.alfresco.service.NotAuditable; import org.alfresco.service.NotAuditable;
import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeRef;
import java.util.List;
/** /**
* The Async Rendition service. Replaces the original rendition services which included synchronous renditions and * The Async Rendition service. Replaces the original rendition services which included synchronous renditions and asynchronous methods with Java call backs.
* asynchronous methods with Java call backs.<p/> * <p/>
* *
* Renditions are defined as {@link RenditionDefinition2}s and may be registered and looked by the associated * Renditions are defined as {@link RenditionDefinition2}s and may be registered and looked by the associated {@link RenditionDefinitionRegistry2}.
* {@link RenditionDefinitionRegistry2}.<p/> * <p/>
* *
* Unlike the original RenditionService this service, it: * Unlike the original RenditionService this service, it:
* <ul> * <ul>
* <li>Performs async renditions without a Java callback, as another node in the cluster may complete the rendition. * <li>Performs async renditions without a Java callback, as another node in the cluster may complete the rendition. The current node requests a transform, but another node might take the resulting transform and turn it into a rendition if the external Transform Service is used.</li>
* The current node requests a transform, but another node might take the resulting transform and turn it into a * <li>Reduces the configurable options to do with with the associations of rendition nodes, their type. They are identical to 'hidden' (not normally seen as nodes in their own right in a UI) renditions produced by the original service. So, they are always directly under the source node connected by a {@code}rn:rendition{@code} association with the name of the rendition.</li>
* rendition if the external Transform Service is used.</li> * <li>The rendition nodes additionally have a {@code}rn:rendition2{@code} aspect and a {@code}contentUrlHashCode{@code} property. This property contains a value that allows the service to work out if it holds a rendition of the source node's current content.</li>
* <li>Reduces the configurable options to do with with the associations of rendition nodes, their type. They
* are identical to 'hidden' (not normally seen as nodes in their own right in a
* UI) renditions produced by the original service. So, they are always directly under the source node connected by a
* {@code}rn:rendition{@code} association with the name of the rendition.</li>
* <li>The rendition nodes additionally have a {@code}rn:rendition2{@code} aspect and a {@code}contentUrlHashCode{@code}
* property. This property contains a value that allows the service to work out if it holds a rendition of the
* source node's current content.</li>
* <li>Failures are handled by setting the rendition node's content to null.</li> * <li>Failures are handled by setting the rendition node's content to null.</li>
* <li>When a rendition is requested via the REST API, only the newer service is used.</li> * <li>When a rendition is requested via the REST API, only the newer service is used.</li>
* <li>Where possible old service renditions migrate automatically over to the new service when content on a * <li>Where possible old service renditions migrate automatically over to the new service when content on a source node is updated.</li>
* source node is updated.</li>
* </ul> * </ul>
* *
* @author adavis * @author adavis
@@ -66,29 +58,30 @@ public interface RenditionService2
RenditionDefinitionRegistry2 getRenditionDefinitionRegistry2(); RenditionDefinitionRegistry2 getRenditionDefinitionRegistry2();
/** /**
* This method asynchronously transforms content to a target mimetype with transform options supplied in the * This method asynchronously transforms content to a target mimetype with transform options supplied in the {@code transformDefinition}. A response is set on a message queue once the transform is complete or fails, together with some client supplied data. The response queue and client data are also included in the transformDefinition.
* {@code transformDefinition}. A response is set on a message queue once the transform is complete or fails, * <p>
* together with some client supplied data. The response queue and client data are also included in the
* transformDefinition.<p>
* *
* This method does not create a rendition node, but uses the same code as renditions to perform the transform. The * This method does not create a rendition node, but uses the same code as renditions to perform the transform. The {@code transformDefinition} extends {@link RenditionDefinition2}, but is not stored in a {@link RenditionDefinitionRegistry2}, as it is transient in nature.
* {@code transformDefinition} extends {@link RenditionDefinition2}, but is not stored in a
* {@link RenditionDefinitionRegistry2}, as it is transient in nature.
* *
* @param sourceNodeRef the node from which the content is retrieved. * @param sourceNodeRef
* @param transformDefinition which defines the transform, where to sent the response and some client specified data. * the node from which the content is retrieved.
* @throws UnsupportedOperationException if the transform is not supported. * @param transformDefinition
* which defines the transform, where to sent the response and some client specified data.
* @throws UnsupportedOperationException
* if the transform is not supported.
*/ */
@NotAuditable @NotAuditable
public void transform(NodeRef sourceNodeRef, TransformDefinition transformDefinition); public void transform(NodeRef sourceNodeRef, TransformDefinition transformDefinition);
/** /**
* This method asynchronously renders content as specified by the {@code renditionName}. The content to be * This method asynchronously renders content as specified by the {@code renditionName}. The content to be rendered is provided by {@code sourceNodeRef}.
* rendered is provided by {@code sourceNodeRef}.
* *
* @param sourceNodeRef the node from which the content is retrieved. * @param sourceNodeRef
* @param renditionName the rendition to be performed. * the node from which the content is retrieved.
* @throws UnsupportedOperationException if the transform is not supported AND the rendition has not been created before. * @param renditionName
* the rendition to be performed.
* @throws UnsupportedOperationException
* if the transform is not supported AND the rendition has not been created before.
*/ */
@NotAuditable @NotAuditable
public void render(NodeRef sourceNodeRef, String renditionName); public void render(NodeRef sourceNodeRef, String renditionName);
@@ -104,10 +97,11 @@ public interface RenditionService2
/** /**
* This method gets the rendition of the {@code sourceNodeRef} identified by its name. * This method gets the rendition of the {@code sourceNodeRef} identified by its name.
* *
* @param sourceNodeRef the source node for the renditions * @param sourceNodeRef
* @param renditionName the renditionName used to identify a rendition. * the source node for the renditions
* @return the {@link ChildAssociationRef} which links the source node to the * @param renditionName
* rendition or <code>null</code> if there is no rendition or it is not up to date. * the renditionName used to identify a rendition.
* @return the {@link ChildAssociationRef} which links the source node to the rendition or <code>null</code> if there is no rendition or it is not up to date.
*/ */
@NotAuditable @NotAuditable
ChildAssociationRef getRenditionByName(NodeRef sourceNodeRef, String renditionName); ChildAssociationRef getRenditionByName(NodeRef sourceNodeRef, String renditionName);
@@ -115,7 +109,8 @@ public interface RenditionService2
/** /**
* This method clears source nodeRef rendition content and content hash code using supplied rendition name. * This method clears source nodeRef rendition content and content hash code using supplied rendition name.
* *
* @param renditionNode the rendition node * @param renditionNode
* the rendition node
*/ */
@NotAuditable @NotAuditable
void clearRenditionContentDataInTransaction(NodeRef renditionNode); void clearRenditionContentDataInTransaction(NodeRef renditionNode);
@@ -124,4 +119,13 @@ public interface RenditionService2
* Indicates if renditions are enabled. Set using the {@code system.thumbnail.generate} value. * Indicates if renditions are enabled. Set using the {@code system.thumbnail.generate} value.
*/ */
boolean isEnabled(); boolean isEnabled();
/**
* This method forces the content hash code for every {@code sourceNodeRef} renditions.
*
* @param sourceNodeRef
* the source node to update renditions hash code
*/
@NotAuditable
void forceRenditionsContentHashCode(NodeRef sourceNodeRef);
} }

View File

@@ -2,7 +2,7 @@
* #%L * #%L
* Alfresco Repository * Alfresco Repository
* %% * %%
* Copyright (C) 2005 - 2022 Alfresco Software Limited * Copyright (C) 2005 - 2025 Alfresco Software Limited
* %% * %%
* This file is part of the Alfresco software. * This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of * If the software was purchased under a paid Alfresco license, the terms of
@@ -25,6 +25,24 @@
*/ */
package org.alfresco.repo.rendition2; package org.alfresco.repo.rendition2;
import static org.alfresco.model.ContentModel.PROP_CONTENT;
import static org.alfresco.model.RenditionModel.PROP_RENDITION_CONTENT_HASH_CODE;
import static org.alfresco.service.namespace.QName.createQName;
import java.io.InputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.alfresco.model.ContentModel; import org.alfresco.model.ContentModel;
import org.alfresco.model.RenditionModel; import org.alfresco.model.RenditionModel;
import org.alfresco.repo.content.ContentServicePolicies; import org.alfresco.repo.content.ContentServicePolicies;
@@ -51,23 +69,6 @@ import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.RegexQNamePattern; import org.alfresco.service.namespace.RegexQNamePattern;
import org.alfresco.service.transaction.TransactionService; import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.PropertyCheck; import org.alfresco.util.PropertyCheck;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import java.io.InputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.alfresco.model.ContentModel.PROP_CONTENT;
import static org.alfresco.model.RenditionModel.PROP_RENDITION_CONTENT_HASH_CODE;
import static org.alfresco.service.namespace.QName.createQName;
/** /**
* The Async Rendition service. Replaces the original deprecated RenditionService. * The Async Rendition service. Replaces the original deprecated RenditionService.
@@ -95,12 +96,10 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea
abstract RenditionDefinition2 getRenditionDefinition(); abstract RenditionDefinition2 getRenditionDefinition();
void handleUnsupported(UnsupportedOperationException e) void handleUnsupported(UnsupportedOperationException e)
{ {}
}
void throwIllegalStateExceptionIfAlreadyDone(int sourceContentHashCode) void throwIllegalStateExceptionIfAlreadyDone(int sourceContentHashCode)
{ {}
}
} }
private TransactionService transactionService; private TransactionService transactionService;
@@ -217,8 +216,7 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea
@Override @Override
public void transform(NodeRef sourceNodeRef, TransformDefinition transformDefinition) public void transform(NodeRef sourceNodeRef, TransformDefinition transformDefinition)
{ {
requestAsyncTransformOrRendition(sourceNodeRef, new RenderOrTransformCallBack() requestAsyncTransformOrRendition(sourceNodeRef, new RenderOrTransformCallBack() {
{
@Override @Override
public String getName() public String getName()
{ {
@@ -237,8 +235,7 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea
@Override @Override
public void render(NodeRef sourceNodeRef, String renditionName) public void render(NodeRef sourceNodeRef, String renditionName)
{ {
requestAsyncTransformOrRendition(sourceNodeRef, new RenderOrTransformCallBack() requestAsyncTransformOrRendition(sourceNodeRef, new RenderOrTransformCallBack() {
{
@Override @Override
public String getName() public String getName()
{ {
@@ -277,7 +274,7 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea
int renditionContentHashCode = getRenditionContentHashCode(renditionNode); int renditionContentHashCode = getRenditionContentHashCode(renditionNode);
if (logger.isDebugEnabled()) if (logger.isDebugEnabled())
{ {
logger.debug(getName() + ": Source " + sourceContentHashCode + " rendition " + renditionContentHashCode+ " hashCodes"); logger.debug(getName() + ": Source " + sourceContentHashCode + " rendition " + renditionContentHashCode + " hashCodes");
} }
if (renditionContentHashCode == sourceContentHashCode) if (renditionContentHashCode == sourceContentHashCode)
{ {
@@ -299,14 +296,14 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea
if (!nodeService.exists(sourceNodeRef)) if (!nodeService.exists(sourceNodeRef))
{ {
throw new IllegalArgumentException(renderOrTransform.getName()+ ": The supplied sourceNodeRef "+sourceNodeRef+" does not exist."); throw new IllegalArgumentException(renderOrTransform.getName() + ": The supplied sourceNodeRef " + sourceNodeRef + " does not exist.");
} }
RenditionDefinition2 renditionDefinition = renderOrTransform.getRenditionDefinition(); RenditionDefinition2 renditionDefinition = renderOrTransform.getRenditionDefinition();
if (logger.isDebugEnabled()) if (logger.isDebugEnabled())
{ {
logger.debug(renderOrTransform.getName()+ ": transform " +sourceNodeRef); logger.debug(renderOrTransform.getName() + ": transform " + sourceNodeRef);
} }
AtomicBoolean supported = new AtomicBoolean(true); AtomicBoolean supported = new AtomicBoolean(true);
@@ -328,14 +325,13 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea
} }
String user = AuthenticationUtil.getRunAsUser(); String user = AuthenticationUtil.getRunAsUser();
RetryingTransactionHelper.RetryingTransactionCallback callback = () -> RetryingTransactionHelper.RetryingTransactionCallback callback = () -> {
{
int sourceContentHashCode = getSourceContentHashCode(sourceNodeRef); int sourceContentHashCode = getSourceContentHashCode(sourceNodeRef);
if (!supported.get()) if (!supported.get())
{ {
if (logger.isDebugEnabled()) if (logger.isDebugEnabled())
{ {
logger.debug(renderOrTransform.getName() +" is not supported. " + logger.debug(renderOrTransform.getName() + " is not supported. " +
"The content might be too big or the source mimetype cannot be converted."); "The content might be too big or the source mimetype cannot be converted.");
} }
failure(sourceNodeRef, renditionDefinition, sourceContentHashCode); failure(sourceNodeRef, renditionDefinition, sourceContentHashCode);
@@ -372,9 +368,7 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea
public void failure(NodeRef sourceNodeRef, RenditionDefinition2 renditionDefinition, int transformContentHashCode) public void failure(NodeRef sourceNodeRef, RenditionDefinition2 renditionDefinition, int transformContentHashCode)
{ {
// The original transaction may have already have failed // The original transaction may have already have failed
AuthenticationUtil.runAsSystem((AuthenticationUtil.RunAsWork<Void>) () -> AuthenticationUtil.runAsSystem((AuthenticationUtil.RunAsWork<Void>) () -> transactionService.getRetryingTransactionHelper().doInTransaction(() -> {
transactionService.getRetryingTransactionHelper().doInTransaction(() ->
{
consume(sourceNodeRef, null, renditionDefinition, transformContentHashCode); consume(sourceNodeRef, null, renditionDefinition, transformContentHashCode);
return null; return null;
}, false, true)); }, false, true));
@@ -386,12 +380,12 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea
int sourceContentHashCode = getSourceContentHashCode(sourceNodeRef); int sourceContentHashCode = getSourceContentHashCode(sourceNodeRef);
if (logger.isDebugEnabled()) if (logger.isDebugEnabled())
{ {
logger.debug("Consume: Source " + sourceContentHashCode + " and transform's source " + transformContentHashCode+" hashcodes"); logger.debug("Consume: Source " + sourceContentHashCode + " and transform's source " + transformContentHashCode + " hashcodes");
} }
if (renditionDefinition instanceof TransformDefinition) if (renditionDefinition instanceof TransformDefinition)
{ {
TransformDefinition transformDefinition = (TransformDefinition)renditionDefinition; TransformDefinition transformDefinition = (TransformDefinition) renditionDefinition;
String targetMimetype = transformDefinition.getTargetMimetype(); String targetMimetype = transformDefinition.getTargetMimetype();
if (AsynchronousExtractor.isMetadataExtractMimetype(targetMimetype)) if (AsynchronousExtractor.isMetadataExtractMimetype(targetMimetype))
{ {
@@ -484,9 +478,7 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea
} }
/** /**
* Takes a transformation (InputStream) and attaches it as a rendition to the source node. * Takes a transformation (InputStream) and attaches it as a rendition to the source node. Does nothing if there is already a newer rendition. If the transformInputStream is null, this is taken to be a transform failure.
* Does nothing if there is already a newer rendition.
* If the transformInputStream is null, this is taken to be a transform failure.
*/ */
private void consumeRendition(NodeRef sourceNodeRef, int sourceContentHashCode, InputStream transformInputStream, private void consumeRendition(NodeRef sourceNodeRef, int sourceContentHashCode, InputStream transformInputStream,
RenditionDefinition2 renditionDefinition, int transformContentHashCode) RenditionDefinition2 renditionDefinition, int transformContentHashCode)
@@ -507,9 +499,7 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea
(transformInputStream == null ? " to null as the transform failed" : " to the transform result")); (transformInputStream == null ? " to null as the transform failed" : " to the transform result"));
} }
AuthenticationUtil.runAsSystem((AuthenticationUtil.RunAsWork<Void>) () -> AuthenticationUtil.runAsSystem((AuthenticationUtil.RunAsWork<Void>) () -> transactionService.getRetryingTransactionHelper().doInTransaction(() -> {
transactionService.getRetryingTransactionHelper().doInTransaction(() ->
{
// Ensure that the creation of a rendition does not cause updates to the modified, modifier properties on the source node // Ensure that the creation of a rendition does not cause updates to the modified, modifier properties on the source node
NodeRef renditionNode = getRenditionNode(sourceNodeRef, renditionName); NodeRef renditionNode = getRenditionNode(sourceNodeRef, renditionName);
boolean createRenditionNode = renditionNode == null; boolean createRenditionNode = renditionNode == null;
@@ -555,7 +545,8 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea
long sizeOfRendition = contentReader.getSize(); long sizeOfRendition = contentReader.getSize();
if (sizeOfRendition > 0L) if (sizeOfRendition > 0L)
{ {
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled())
{
logger.debug("Set rendition hashcode for " + renditionName); logger.debug("Set rendition hashcode for " + renditionName);
} }
nodeService.setProperty(renditionNode, RenditionModel.PROP_RENDITION_CONTENT_HASH_CODE, transformContentHashCode); nodeService.setProperty(renditionNode, RenditionModel.PROP_RENDITION_CONTENT_HASH_CODE, transformContentHashCode);
@@ -634,14 +625,14 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea
if (logger.isTraceEnabled()) if (logger.isTraceEnabled())
{ {
logger.trace("Setting thumbnail last modified date to " + lastModifiedValue +" on source node: " + sourceNodeRef); logger.trace("Setting thumbnail last modified date to " + lastModifiedValue + " on source node: " + sourceNodeRef);
} }
if (nodeService.hasAspect(sourceNodeRef, ContentModel.ASPECT_THUMBNAIL_MODIFICATION)) if (nodeService.hasAspect(sourceNodeRef, ContentModel.ASPECT_THUMBNAIL_MODIFICATION))
{ {
List<String> thumbnailMods = (List<String>) nodeService.getProperty(sourceNodeRef, ContentModel.PROP_LAST_THUMBNAIL_MODIFICATION_DATA); List<String> thumbnailMods = (List<String>) nodeService.getProperty(sourceNodeRef, ContentModel.PROP_LAST_THUMBNAIL_MODIFICATION_DATA);
String target = null; String target = null;
for (String currThumbnailMod: thumbnailMods) for (String currThumbnailMod : thumbnailMods)
{ {
if (currThumbnailMod.startsWith(prefix)) if (currThumbnailMod.startsWith(prefix))
{ {
@@ -665,8 +656,7 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea
} }
/** /**
* Returns the hash code of the source node's content url. As transformations may be returned in a different * Returns the hash code of the source node's content url. As transformations may be returned in a different sequences to which they were requested, this is used work out if a rendition should be replaced.
* sequences to which they were requested, this is used work out if a rendition should be replaced.
*/ */
private int getSourceContentHashCode(NodeRef sourceNodeRef) private int getSourceContentHashCode(NodeRef sourceNodeRef)
{ {
@@ -675,7 +665,7 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea
if (contentData != null) if (contentData != null)
{ {
// Originally we used the contentData URL, but that is not enough if the mimetype changes. // Originally we used the contentData URL, but that is not enough if the mimetype changes.
String contentString = contentData.getContentUrl()+contentData.getMimetype(); String contentString = contentData.getContentUrl() + contentData.getMimetype();
if (contentString != null) if (contentString != null)
{ {
hashCode = contentString.hashCode(); hashCode = contentString.hashCode();
@@ -685,13 +675,11 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea
} }
/** /**
* Returns the hash code of source node's content url on the rendition node (node may be null) if it does not exist. * Returns the hash code of source node's content url on the rendition node (node may be null) if it does not exist. Used work out if a rendition should be replaced. {@code -2} is returned if the rendition does not exist or was not created by RenditionService2. {@code -1} is returned if there was no source content or the rendition failed.
* Used work out if a rendition should be replaced. {@code -2} is returned if the rendition does not exist or was
* not created by RenditionService2. {@code -1} is returned if there was no source content or the rendition failed.
*/ */
private int getRenditionContentHashCode(NodeRef renditionNode) private int getRenditionContentHashCode(NodeRef renditionNode)
{ {
if ( renditionNode == null || !nodeService.hasAspect(renditionNode, RenditionModel.ASPECT_RENDITION2)) if (renditionNode == null || !nodeService.hasAspect(renditionNode, RenditionModel.ASPECT_RENDITION2))
{ {
return RENDITION2_DOES_NOT_EXIST; return RENDITION2_DOES_NOT_EXIST;
} }
@@ -699,7 +687,7 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea
Serializable hashCode = nodeService.getProperty(renditionNode, PROP_RENDITION_CONTENT_HASH_CODE); Serializable hashCode = nodeService.getProperty(renditionNode, PROP_RENDITION_CONTENT_HASH_CODE);
return hashCode == null return hashCode == null
? SOURCE_HAS_NO_CONTENT ? SOURCE_HAS_NO_CONTENT
: (int)hashCode; : (int) hashCode;
} }
private NodeRef getRenditionNode(NodeRef sourceNodeRef, String renditionName) private NodeRef getRenditionNode(NodeRef sourceNodeRef, String renditionName)
@@ -773,11 +761,12 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea
} }
/** /**
* This method checks whether the specified source node is of a content class which has been registered for * This method checks whether the specified source node is of a content class which has been registered for rendition prevention.
* rendition prevention.
* *
* @param sourceNode the node to check. * @param sourceNode
* @throws RenditionService2PreventedException if the source node is configured for rendition prevention. * the node to check.
* @throws RenditionService2PreventedException
* if the source node is configured for rendition prevention.
*/ */
// This code is based on the old RenditionServiceImpl.checkSourceNodeForPreventionClass(...) // This code is based on the old RenditionServiceImpl.checkSourceNodeForPreventionClass(...)
private void checkSourceNodeForPreventionClass(NodeRef sourceNode) private void checkSourceNodeForPreventionClass(NodeRef sourceNode)
@@ -833,8 +822,7 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea
} }
/** /**
* Indicates if the rendition is available. Failed renditions (there was an error) don't have a contentUrl * Indicates if the rendition is available. Failed renditions (there was an error) don't have a contentUrl and out of date renditions or those still being created don't have a matching contentHashCode.
* and out of date renditions or those still being created don't have a matching contentHashCode.
*/ */
public boolean isRenditionAvailable(NodeRef sourceNodeRef, NodeRef renditionNode) public boolean isRenditionAvailable(NodeRef sourceNodeRef, NodeRef renditionNode)
{ {
@@ -852,7 +840,7 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea
int renditionContentHashCode = getRenditionContentHashCode(renditionNode); int renditionContentHashCode = getRenditionContentHashCode(renditionNode);
if (logger.isDebugEnabled()) if (logger.isDebugEnabled())
{ {
logger.debug("isRenditionAvailable source " + sourceContentHashCode + " and rendition " + renditionContentHashCode+" hashcodes"); logger.debug("isRenditionAvailable source " + sourceContentHashCode + " and rendition " + renditionContentHashCode + " hashcodes");
} }
if (sourceContentHashCode != renditionContentHashCode) if (sourceContentHashCode != renditionContentHashCode)
{ {
@@ -892,16 +880,14 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea
} }
ChildAssociationRef childAssoc = renditions.get(0); ChildAssociationRef childAssoc = renditions.get(0);
NodeRef renditionNode = childAssoc.getChildRef(); NodeRef renditionNode = childAssoc.getChildRef();
return !isRenditionAvailable(sourceNodeRef, renditionNode) ? null: childAssoc; return !isRenditionAvailable(sourceNodeRef, renditionNode) ? null : childAssoc;
} }
} }
@Override @Override
public void clearRenditionContentDataInTransaction(NodeRef renditionNode) public void clearRenditionContentDataInTransaction(NodeRef renditionNode)
{ {
AuthenticationUtil.runAsSystem((AuthenticationUtil.RunAsWork<Void>) () -> AuthenticationUtil.runAsSystem((AuthenticationUtil.RunAsWork<Void>) () -> transactionService.getRetryingTransactionHelper().doInTransaction(() -> {
transactionService.getRetryingTransactionHelper().doInTransaction(() ->
{
clearRenditionContentData(renditionNode); clearRenditionContentData(renditionNode);
return null; return null;
}, false, true)); }, false, true));
@@ -950,4 +936,35 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea
} }
} }
@Override
public void forceRenditionsContentHashCode(NodeRef sourceNodeRef)
{
if (sourceNodeRef != null && nodeService.exists(sourceNodeRef))
{
List<ChildAssociationRef> renditions = getRenditionChildAssociations(sourceNodeRef);
if (renditions != null)
{
int sourceContentHashCode = getSourceContentHashCode(sourceNodeRef);
for (ChildAssociationRef rendition : renditions)
{
NodeRef renditionNode = rendition.getChildRef();
if (nodeService.hasAspect(renditionNode, RenditionModel.ASPECT_RENDITION2))
{
int renditionContentHashCode = getRenditionContentHashCode(renditionNode);
String renditionName = rendition.getQName().getLocalName();
if (sourceContentHashCode != renditionContentHashCode)
{
if (logger.isDebugEnabled())
{
logger.debug("Update content hash code for rendition " + renditionName + " of node "
+ sourceNodeRef);
}
nodeService.setProperty(renditionNode, PROP_RENDITION_CONTENT_HASH_CODE,
sourceContentHashCode);
}
}
}
}
}
}
} }

View File

@@ -2,7 +2,7 @@
* #%L * #%L
* Alfresco Repository * Alfresco Repository
* %% * %%
* Copyright (C) 2005 - 2022 Alfresco Software Limited * Copyright (C) 2005 - 2025 Alfresco Software Limited
* %% * %%
* This file is part of the Alfresco software. * This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of * If the software was purchased under a paid Alfresco license, the terms of
@@ -26,6 +26,7 @@
package org.alfresco.repo.rendition2; package org.alfresco.repo.rendition2;
import static java.lang.Thread.sleep; import static java.lang.Thread.sleep;
import static org.alfresco.model.ContentModel.PROP_CONTENT; import static org.alfresco.model.ContentModel.PROP_CONTENT;
import static org.alfresco.model.RenditionModel.PROP_RENDITION_CONTENT_HASH_CODE; import static org.alfresco.model.RenditionModel.PROP_RENDITION_CONTENT_HASH_CODE;
import static org.alfresco.repo.content.MimetypeMap.EXTENSION_BINARY; import static org.alfresco.repo.content.MimetypeMap.EXTENSION_BINARY;
@@ -35,6 +36,15 @@ import java.io.FileNotFoundException;
import java.io.Serializable; import java.io.Serializable;
import java.util.Collections; import java.util.Collections;
import junit.framework.AssertionFailedError;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.quartz.CronExpression;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.ResourceUtils;
import org.alfresco.model.ContentModel; import org.alfresco.model.ContentModel;
import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.content.metadata.AsynchronousExtractor; import org.alfresco.repo.content.metadata.AsynchronousExtractor;
@@ -45,6 +55,7 @@ import org.alfresco.repo.thumbnail.ThumbnailRegistry;
import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.service.cmr.rendition.RenditionService; import org.alfresco.service.cmr.rendition.RenditionService;
import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentService; import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.ContentWriter;
@@ -52,6 +63,7 @@ import org.alfresco.service.cmr.repository.MimetypeService;
import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
import org.alfresco.service.cmr.security.MutableAuthenticationService; import org.alfresco.service.cmr.security.MutableAuthenticationService;
import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.cmr.security.PersonService; import org.alfresco.service.cmr.security.PersonService;
@@ -61,15 +73,6 @@ import org.alfresco.transform.registry.TransformServiceRegistry;
import org.alfresco.util.BaseSpringTest; import org.alfresco.util.BaseSpringTest;
import org.alfresco.util.GUID; import org.alfresco.util.GUID;
import org.alfresco.util.PropertyMap; import org.alfresco.util.PropertyMap;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.quartz.CronExpression;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.ResourceUtils;
import junit.framework.AssertionFailedError;
/** /**
* Class unites common utility methods for {@link org.alfresco.repo.rendition2} package tests. * Class unites common utility methods for {@link org.alfresco.repo.rendition2} package tests.
@@ -128,6 +131,7 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest
protected static final String ADMIN = "admin"; protected static final String ADMIN = "admin";
protected static final String DOC_LIB = "doclib"; protected static final String DOC_LIB = "doclib";
protected static final String PDF = "pdf";
private CronExpression origLocalTransCron; private CronExpression origLocalTransCron;
private CronExpression origRenditionCron; private CronExpression origRenditionCron;
@@ -181,7 +185,7 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest
if (transformServiceRegistry instanceof LocalTransformServiceRegistry) if (transformServiceRegistry instanceof LocalTransformServiceRegistry)
{ {
((LocalTransformServiceRegistry)transformServiceRegistry).setEnabled(localTransformServiceEnabled); ((LocalTransformServiceRegistry) transformServiceRegistry).setEnabled(localTransformServiceEnabled);
} }
thumbnailRegistry.setTransformServiceRegistry(transformServiceRegistry); thumbnailRegistry.setTransformServiceRegistry(transformServiceRegistry);
@@ -257,9 +261,7 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest
// Creates a new source node as the given user in its own transaction. // Creates a new source node as the given user in its own transaction.
protected NodeRef createSource(String user, String testFileName) protected NodeRef createSource(String user, String testFileName)
{ {
return AuthenticationUtil.runAs(() -> return AuthenticationUtil.runAs(() -> transactionService.getRetryingTransactionHelper().doInTransaction(() -> createSource(testFileName)), user);
transactionService.getRetryingTransactionHelper().doInTransaction(() ->
createSource(testFileName)), user);
} }
// Creates a new source node as the current user in the current transaction. // Creates a new source node as the current user in the current transaction.
@@ -271,9 +273,7 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest
// Changes the content of a source node as the given user in its own transaction. // Changes the content of a source node as the given user in its own transaction.
protected void updateContent(String user, NodeRef sourceNodeRef, String testFileName) protected void updateContent(String user, NodeRef sourceNodeRef, String testFileName)
{ {
AuthenticationUtil.runAs((AuthenticationUtil.RunAsWork<Void>) () -> AuthenticationUtil.runAs((AuthenticationUtil.RunAsWork<Void>) () -> transactionService.getRetryingTransactionHelper().doInTransaction(() -> {
transactionService.getRetryingTransactionHelper().doInTransaction(() ->
{
updateContent(sourceNodeRef, testFileName); updateContent(sourceNodeRef, testFileName);
return null; return null;
}), user); }), user);
@@ -295,9 +295,7 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest
// Clears the content of a source node as the given user in its own transaction. // Clears the content of a source node as the given user in its own transaction.
protected void clearContent(String user, NodeRef sourceNodeRef) protected void clearContent(String user, NodeRef sourceNodeRef)
{ {
AuthenticationUtil.runAs((AuthenticationUtil.RunAsWork<Void>) () -> AuthenticationUtil.runAs((AuthenticationUtil.RunAsWork<Void>) () -> transactionService.getRetryingTransactionHelper().doInTransaction(() -> {
transactionService.getRetryingTransactionHelper().doInTransaction(() ->
{
clearContent(sourceNodeRef); clearContent(sourceNodeRef);
return null; return null;
}), user); }), user);
@@ -312,9 +310,7 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest
// Requests a new rendition as the given user in its own transaction. // Requests a new rendition as the given user in its own transaction.
protected void render(String user, NodeRef sourceNode, String renditionName) protected void render(String user, NodeRef sourceNode, String renditionName)
{ {
AuthenticationUtil.runAs((AuthenticationUtil.RunAsWork<Void>) () -> AuthenticationUtil.runAs((AuthenticationUtil.RunAsWork<Void>) () -> transactionService.getRetryingTransactionHelper().doInTransaction(() -> {
transactionService.getRetryingTransactionHelper().doInTransaction(() ->
{
render(sourceNode, renditionName); render(sourceNode, renditionName);
return null; return null;
}), user); }), user);
@@ -323,9 +319,7 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest
// Requests a new metadata extract as the given user in its own transaction. // Requests a new metadata extract as the given user in its own transaction.
protected void extract(String user, NodeRef sourceNode) protected void extract(String user, NodeRef sourceNode)
{ {
AuthenticationUtil.runAs((AuthenticationUtil.RunAsWork<Void>) () -> AuthenticationUtil.runAs((AuthenticationUtil.RunAsWork<Void>) () -> transactionService.getRetryingTransactionHelper().doInTransaction(() -> {
transactionService.getRetryingTransactionHelper().doInTransaction(() ->
{
extract(sourceNode); extract(sourceNode);
return null; return null;
}), user); }), user);
@@ -357,7 +351,7 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest
Throwable cause = e.getCause(); Throwable cause = e.getCause();
if (cause instanceof AssertionFailedError) if (cause instanceof AssertionFailedError)
{ {
throw (AssertionFailedError)cause; throw (AssertionFailedError) cause;
} }
throw e; throw e;
} }
@@ -375,7 +369,7 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest
Throwable cause = e.getCause(); Throwable cause = e.getCause();
if (cause instanceof AssertionFailedError) if (cause instanceof AssertionFailedError)
{ {
throw (AssertionFailedError)cause; throw (AssertionFailedError) cause;
} }
throw e; throw e;
} }
@@ -386,16 +380,15 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest
{ {
long maxMillis = 10000; long maxMillis = 10000;
ChildAssociationRef assoc = null; ChildAssociationRef assoc = null;
for (int i = (int)(maxMillis / 1000); i >= 0; i--) for (int i = (int) (maxMillis / 1000); i >= 0; i--)
{ {
// Must create a new transaction in order to see changes that take place after this method started. // Must create a new transaction in order to see changes that take place after this method started.
assoc = transactionService.getRetryingTransactionHelper().doInTransaction(() -> assoc = transactionService.getRetryingTransactionHelper().doInTransaction(() -> renditionService2.getRenditionByName(sourceNodeRef, renditionName), true, true);
renditionService2.getRenditionByName(sourceNodeRef, renditionName), true, true);
if (assoc != null) if (assoc != null)
{ {
break; break;
} }
logger.debug("RenditionService2.getRenditionByName(...) sleep "+i); logger.debug("RenditionService2.getRenditionByName(...) sleep " + i);
sleep(1000); sleep(1000);
} }
if (shouldExist) if (shouldExist)
@@ -415,11 +408,10 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest
{ {
long maxMillis = 5000; long maxMillis = 5000;
boolean nodeModified = true; boolean nodeModified = true;
for (int i = (int)(maxMillis / 1000); i >= 0; i--) for (int i = (int) (maxMillis / 1000); i >= 0; i--)
{ {
// Must create a new transaction in order to see changes that take place after this method started. // Must create a new transaction in order to see changes that take place after this method started.
nodeModified = transactionService.getRetryingTransactionHelper().doInTransaction(() -> nodeModified = transactionService.getRetryingTransactionHelper().doInTransaction(() -> {
{
Serializable created = nodeService.getProperty(sourceNodeRef, ContentModel.PROP_CREATED); Serializable created = nodeService.getProperty(sourceNodeRef, ContentModel.PROP_CREATED);
Serializable modified = nodeService.getProperty(sourceNodeRef, ContentModel.PROP_MODIFIED); Serializable modified = nodeService.getProperty(sourceNodeRef, ContentModel.PROP_MODIFIED);
return !created.equals(modified); return !created.equals(modified);
@@ -428,7 +420,7 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest
{ {
break; break;
} }
logger.debug("waitForExtract sleep "+i); logger.debug("waitForExtract sleep " + i);
sleep(1000); sleep(1000);
} }
if (nodePropsShouldChange) if (nodePropsShouldChange)
@@ -445,7 +437,7 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest
protected String getTestFileName(String sourceMimetype) throws FileNotFoundException protected String getTestFileName(String sourceMimetype) throws FileNotFoundException
{ {
String extension = mimetypeMap.getExtension(sourceMimetype); String extension = mimetypeMap.getExtension(sourceMimetype);
String testFileName = extension.equals(EXTENSION_BINARY) ? null : "quick."+extension; String testFileName = extension.equals(EXTENSION_BINARY) ? null : "quick." + extension;
if (testFileName != null) if (testFileName != null)
{ {
try try
@@ -491,8 +483,7 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest
String createRandomUser() String createRandomUser()
{ {
return AuthenticationUtil.runAs(() -> return AuthenticationUtil.runAs(() -> {
{
String username = generateNewUsernameString(); String username = generateNewUsernameString();
createUser(username); createUser(username);
return username; return username;
@@ -510,8 +501,7 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest
final String jobTitle, final String jobTitle,
final long quota) final long quota)
{ {
RetryingTransactionHelper.RetryingTransactionCallback<Void> createUserCallback = () -> RetryingTransactionHelper.RetryingTransactionCallback<Void> createUserCallback = () -> {
{
authenticationService.createAuthentication(username, PASSWORD.toCharArray()); authenticationService.createAuthentication(username, PASSWORD.toCharArray());
PropertyMap personProperties = new PropertyMap(); PropertyMap personProperties = new PropertyMap();
@@ -519,7 +509,7 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest
personProperties.put(ContentModel.PROP_AUTHORITY_DISPLAY_NAME, "title" + username); personProperties.put(ContentModel.PROP_AUTHORITY_DISPLAY_NAME, "title" + username);
personProperties.put(ContentModel.PROP_FIRSTNAME, firstName); personProperties.put(ContentModel.PROP_FIRSTNAME, firstName);
personProperties.put(ContentModel.PROP_LASTNAME, lastName); personProperties.put(ContentModel.PROP_LASTNAME, lastName);
personProperties.put(ContentModel.PROP_EMAIL, username+"@example.com"); personProperties.put(ContentModel.PROP_EMAIL, username + "@example.com");
personProperties.put(ContentModel.PROP_JOBTITLE, jobTitle); personProperties.put(ContentModel.PROP_JOBTITLE, jobTitle);
if (quota > 0) if (quota > 0)
{ {
@@ -548,14 +538,12 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest
} }
/** /**
* Helper method which gets the content hash code from the supplied rendition node without specific validations (the * Helper method which gets the content hash code from the supplied rendition node without specific validations (the equivalent method from {@link RenditionService2Impl} is not exposed)
* equivalent method from {@link RenditionService2Impl} is not exposed)
* *
* @param renditionNodeRef * @param renditionNodeRef
* the rendition node * the rendition node
* *
* @return -1 in case of there is no content, -2 in case rendition doesn't exist, the actual content hash code * @return -1 in case of there is no content, -2 in case rendition doesn't exist, the actual content hash code otherwise
* otherwise
*/ */
protected int getRenditionContentHashCode(NodeRef renditionNodeRef) protected int getRenditionContentHashCode(NodeRef renditionNodeRef)
{ {
@@ -569,4 +557,28 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest
return renditionContentHashCode; return renditionContentHashCode;
} }
/**
* Helper method which gets the content hash code from the supplied source node (the equivalent method from {@link RenditionService2Impl} is not public)
*
* @param sourceNodeRef
* the source node
*
* @return -1 in case of there is no content, otherwise, the actual content hash code otherwise
*/
protected int getSourceContentHashCode(NodeRef sourceNodeRef)
{
int hashCode = -1;
ContentData contentData = DefaultTypeConverter.INSTANCE.convert(ContentData.class, nodeService.getProperty(sourceNodeRef, PROP_CONTENT));
if (contentData != null)
{
// Originally we used the contentData URL, but that is not enough if the mimetype changes.
String contentString = contentData.getContentUrl() + contentData.getMimetype();
if (contentString != null)
{
hashCode = contentString.hashCode();
}
}
return hashCode;
}
} }

View File

@@ -2,7 +2,7 @@
* #%L * #%L
* Alfresco Repository * Alfresco Repository
* %% * %%
* Copyright (C) 2005 - 2022 Alfresco Software Limited * Copyright (C) 2005 - 2025 Alfresco Software Limited
* %% * %%
* This file is part of the Alfresco software. * This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of * If the software was purchased under a paid Alfresco license, the terms of
@@ -25,14 +25,18 @@
*/ */
package org.alfresco.repo.rendition2; package org.alfresco.repo.rendition2;
import static org.alfresco.model.ContentModel.PROP_CONTENT;
import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotEquals;
import static org.alfresco.model.ContentModel.PROP_CONTENT;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.junit.BeforeClass;
import org.junit.Test;
import org.alfresco.model.ContentModel; import org.alfresco.model.ContentModel;
import org.alfresco.model.RenditionModel; import org.alfresco.model.RenditionModel;
import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.authentication.AuthenticationUtil;
@@ -44,8 +48,6 @@ import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.QName;
import org.junit.BeforeClass;
import org.junit.Test;
/** /**
* Integration tests for {@link RenditionService2} * Integration tests for {@link RenditionService2}
@@ -158,8 +160,7 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati
clearContent(ADMIN, sourceNodeRef); clearContent(ADMIN, sourceNodeRef);
render(ADMIN, sourceNodeRef, DOC_LIB); render(ADMIN, sourceNodeRef, DOC_LIB);
ChildAssociationRef assoc = AuthenticationUtil.runAs(() -> ChildAssociationRef assoc = AuthenticationUtil.runAs(() -> renditionService2.getRenditionByName(sourceNodeRef, DOC_LIB), ADMIN);
renditionService2.getRenditionByName(sourceNodeRef, DOC_LIB), ADMIN);
waitForRendition(ADMIN, sourceNodeRef, DOC_LIB, false); waitForRendition(ADMIN, sourceNodeRef, DOC_LIB, false);
assertNull("There should be no rendition as there was no content", assoc); assertNull("There should be no rendition as there was no content", assoc);
} }
@@ -190,8 +191,7 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati
clearContent(ADMIN, sourceNodeRef); clearContent(ADMIN, sourceNodeRef);
render(ADMIN, sourceNodeRef, DOC_LIB); render(ADMIN, sourceNodeRef, DOC_LIB);
ChildAssociationRef assoc = AuthenticationUtil.runAs(() -> ChildAssociationRef assoc = AuthenticationUtil.runAs(() -> renditionService2.getRenditionByName(sourceNodeRef, DOC_LIB), ADMIN);
renditionService2.getRenditionByName(sourceNodeRef, DOC_LIB), ADMIN);
waitForRendition(ADMIN, sourceNodeRef, DOC_LIB, false); waitForRendition(ADMIN, sourceNodeRef, DOC_LIB, false);
assertNull("There should be no rendition as there was no content", assoc); assertNull("There should be no rendition as there was no content", assoc);
@@ -216,8 +216,7 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati
String ownerUserName = createRandomUser(); String ownerUserName = createRandomUser();
NodeRef sourceNodeRef = createSource(ownerUserName, "quick.jpg"); NodeRef sourceNodeRef = createSource(ownerUserName, "quick.jpg");
String otherUserName = createRandomUser(); String otherUserName = createRandomUser();
transactionService.getRetryingTransactionHelper().doInTransaction(() -> transactionService.getRetryingTransactionHelper().doInTransaction(() -> {
{
permissionService.setPermission(sourceNodeRef, otherUserName, PermissionService.READ, true); permissionService.setPermission(sourceNodeRef, otherUserName, PermissionService.READ, true);
return null; return null;
}); });
@@ -236,8 +235,7 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati
String ownerUserName = createRandomUser(); String ownerUserName = createRandomUser();
NodeRef sourceNodeRef = createSource(ownerUserName, "quick.jpg"); NodeRef sourceNodeRef = createSource(ownerUserName, "quick.jpg");
String otherUserName = createRandomUser(); String otherUserName = createRandomUser();
transactionService.getRetryingTransactionHelper().doInTransaction(() -> transactionService.getRetryingTransactionHelper().doInTransaction(() -> {
{
permissionService.setPermission(sourceNodeRef, otherUserName, PermissionService.READ, true); permissionService.setPermission(sourceNodeRef, otherUserName, PermissionService.READ, true);
return null; return null;
}); });
@@ -257,8 +255,7 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati
NodeRef sourceNodeRef = createSource(ownerUserName, "quick.jpg"); NodeRef sourceNodeRef = createSource(ownerUserName, "quick.jpg");
render(ownerUserName, sourceNodeRef, DOC_LIB); render(ownerUserName, sourceNodeRef, DOC_LIB);
String noPermissionsUser = createRandomUser(); String noPermissionsUser = createRandomUser();
transactionService.getRetryingTransactionHelper().doInTransaction(() -> transactionService.getRetryingTransactionHelper().doInTransaction(() -> {
{
permissionService.setPermission(sourceNodeRef, noPermissionsUser, PermissionService.ALL_PERMISSIONS, false); permissionService.setPermission(sourceNodeRef, noPermissionsUser, PermissionService.ALL_PERMISSIONS, false);
return null; return null;
}); });
@@ -280,12 +277,9 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati
NodeRef sourceNodeRef = createSource(ownerUserName, "quick.jpg"); NodeRef sourceNodeRef = createSource(ownerUserName, "quick.jpg");
final QName doclibRendDefQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "doclib"); final QName doclibRendDefQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "doclib");
transactionService.getRetryingTransactionHelper() transactionService.getRetryingTransactionHelper()
.doInTransaction(() -> .doInTransaction(() -> AuthenticationUtil.runAs(() -> renditionService.render(sourceNodeRef, doclibRendDefQName), ownerUserName));
AuthenticationUtil.runAs(() ->
renditionService.render(sourceNodeRef, doclibRendDefQName), ownerUserName));
NodeRef oldRendition = AuthenticationUtil.runAs(() -> NodeRef oldRendition = AuthenticationUtil.runAs(() -> renditionService.getRenditionByName(sourceNodeRef, doclibRendDefQName).getChildRef(), ownerUserName);
renditionService.getRenditionByName(sourceNodeRef, doclibRendDefQName).getChildRef(), ownerUserName);
assertFalse("The rendition should be generated by old Rendition Service", assertFalse("The rendition should be generated by old Rendition Service",
AuthenticationUtil.runAs(() -> nodeService.hasAspect(oldRendition, RenditionModel.ASPECT_RENDITION2), ownerUserName)); AuthenticationUtil.runAs(() -> nodeService.hasAspect(oldRendition, RenditionModel.ASPECT_RENDITION2), ownerUserName));
@@ -335,9 +329,7 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati
renditionService2.setEnabled(false); renditionService2.setEnabled(false);
// Call 'clearRenditionContentData' method directly to prove rendition content will be cleaned // Call 'clearRenditionContentData' method directly to prove rendition content will be cleaned
AuthenticationUtil.runAs((AuthenticationUtil.RunAsWork<Void>) () -> AuthenticationUtil.runAs((AuthenticationUtil.RunAsWork<Void>) () -> transactionService.getRetryingTransactionHelper().doInTransaction(() -> {
transactionService.getRetryingTransactionHelper().doInTransaction(() ->
{
renditionService2.clearRenditionContentData(sourceNodeRef, DOC_LIB); renditionService2.clearRenditionContentData(sourceNodeRef, DOC_LIB);
return null; return null;
}), ADMIN); }), ADMIN);
@@ -356,8 +348,7 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati
/** /**
* Tests if a rendition without content (but with contentHashCode) can be generated again. * Tests if a rendition without content (but with contentHashCode) can be generated again.
* <p> * <p>
* If the rendition consumption receives a null InputStream, the contentHashCode should be cleaned from the * If the rendition consumption receives a null InputStream, the contentHashCode should be cleaned from the rendition node, allowing new requests to generate the rendition.
* rendition node, allowing new requests to generate the rendition.
* </p> * </p>
*/ */
@Test @Test
@@ -369,8 +360,7 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati
/** /**
* Tests if a rendition without content (but with contentHashCode) can be generated again. * Tests if a rendition without content (but with contentHashCode) can be generated again.
* <p> * <p>
* If the rendition consumption receives a zero length InputStream, the contentHashCode should be cleaned from the * If the rendition consumption receives a zero length InputStream, the contentHashCode should be cleaned from the rendition node, allowing new requests to generate the rendition.
* rendition node, allowing new requests to generate the rendition.
* </p> * </p>
*/ */
@Test @Test
@@ -455,6 +445,61 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati
assertEquals(TOTAL_NODES, countModifier(nodes, user1)); assertEquals(TOTAL_NODES, countModifier(nodes, user1));
} }
@Test
public void testForceRenditionsContentHashCode()
{
// Create a node
NodeRef sourceNodeRef = createSource(ADMIN, "quick.docx");
assertNotNull("Node not generated", sourceNodeRef);
// Get content hash code for the source node
int sourceNodeContentHashCode = getSourceContentHashCode(sourceNodeRef);
// Trigger the pdf rendition
render(ADMIN, sourceNodeRef, PDF);
NodeRef pdfRenditionNodeRef = waitForRendition(ADMIN, sourceNodeRef, PDF, true);
assertNotNull("pdf rendition was not generated", pdfRenditionNodeRef);
assertNotNull("pdf rendition was not generated", nodeService.getProperty(pdfRenditionNodeRef, PROP_CONTENT));
// Check the pdf rendition content hash code is valid
int pdfRenditionContentHashCode = getRenditionContentHashCode(pdfRenditionNodeRef);
assertEquals("pdf rendition content hash code is different from source node content hash code", sourceNodeContentHashCode, pdfRenditionContentHashCode);
// Trigger the doc lib rendition
render(ADMIN, sourceNodeRef, DOC_LIB);
NodeRef docLibRenditionNodeRef = waitForRendition(ADMIN, sourceNodeRef, DOC_LIB, true);
assertNotNull("doc lib rendition was not generated", docLibRenditionNodeRef);
assertNotNull("doc lib rendition was not generated", nodeService.getProperty(docLibRenditionNodeRef, PROP_CONTENT));
// Check the doc lib rendition content hash code is valid
int docLibenditionContentHashCode = getRenditionContentHashCode(docLibRenditionNodeRef);
assertEquals("doc lib rendition content hash code is different from source node content hash code", sourceNodeContentHashCode, docLibenditionContentHashCode);
// Update the source node content
updateContent(ADMIN, sourceNodeRef, "quick.docx");
// Get source node content hash code after update
int sourceNodeContentHashCode2 = getSourceContentHashCode(sourceNodeRef);
// Check content hash code are different after content update
assertNotEquals("Source node content hash code is the same after content update", sourceNodeContentHashCode, sourceNodeContentHashCode2);
assertNotEquals("pdf rendition content hash code is the same after content update", sourceNodeContentHashCode2, pdfRenditionContentHashCode);
assertNotEquals("doc lib rendition content hash code is the same after content update", sourceNodeContentHashCode2, docLibenditionContentHashCode);
// Forces the content hash code for every source node renditions
AuthenticationUtil.runAs(() -> {
renditionService2.forceRenditionsContentHashCode(sourceNodeRef);
return null;
}, ADMIN);
// Check the renditions content hash code are now the same as the latest source node content hash code
int pdfRenditionContentHashCode2 = getRenditionContentHashCode(pdfRenditionNodeRef);
int docLibenditionContentHashCode2 = getRenditionContentHashCode(docLibRenditionNodeRef);
assertEquals("pdf rendition content hash code is different from latest source node content hash code", sourceNodeContentHashCode2, pdfRenditionContentHashCode2);
assertEquals("doc lib rendition content hash code is different from latest source node content hash code", sourceNodeContentHashCode2, docLibenditionContentHashCode2);
}
private int countModifier(List<NodeRef> nodes, String user) private int countModifier(List<NodeRef> nodes, String user)
{ {
int count = 0; int count = 0;
@@ -492,22 +537,16 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati
NodeRef sourceNodeRef = createSource(ADMIN, "quick.jpg"); NodeRef sourceNodeRef = createSource(ADMIN, "quick.jpg");
final QName doclibRendDefQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "doclib"); final QName doclibRendDefQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "doclib");
transactionService.getRetryingTransactionHelper() transactionService.getRetryingTransactionHelper()
.doInTransaction(() -> .doInTransaction(() -> AuthenticationUtil.runAs(() -> renditionService.render(sourceNodeRef, doclibRendDefQName), ADMIN));
AuthenticationUtil.runAs(() ->
renditionService.render(sourceNodeRef, doclibRendDefQName), ADMIN));
assertNotNull("The old renditions service did not render", waitForRendition(ADMIN, sourceNodeRef, DOC_LIB, true)); assertNotNull("The old renditions service did not render", waitForRendition(ADMIN, sourceNodeRef, DOC_LIB, true));
List<String> lastThumbnailModification = transactionService.getRetryingTransactionHelper() List<String> lastThumbnailModification = transactionService.getRetryingTransactionHelper()
.doInTransaction(() -> .doInTransaction(() -> AuthenticationUtil.runAs(() -> (List<String>) nodeService.getProperty(sourceNodeRef, ContentModel.PROP_LAST_THUMBNAIL_MODIFICATION_DATA), ADMIN));
AuthenticationUtil.runAs(() ->
(List<String>) nodeService.getProperty(sourceNodeRef, ContentModel.PROP_LAST_THUMBNAIL_MODIFICATION_DATA), ADMIN));
updateContent(ADMIN, sourceNodeRef, "quick.png"); updateContent(ADMIN, sourceNodeRef, "quick.png");
List<String> newThumbnailModification = null; List<String> newThumbnailModification = null;
for (int i = 0; i < 5; i++) for (int i = 0; i < 5; i++)
{ {
newThumbnailModification = transactionService.getRetryingTransactionHelper() newThumbnailModification = transactionService.getRetryingTransactionHelper()
.doInTransaction(() -> .doInTransaction(() -> AuthenticationUtil.runAs(() -> (List<String>) nodeService.getProperty(sourceNodeRef, ContentModel.PROP_LAST_THUMBNAIL_MODIFICATION_DATA), ADMIN));
AuthenticationUtil.runAs(() ->
(List<String>) nodeService.getProperty(sourceNodeRef, ContentModel.PROP_LAST_THUMBNAIL_MODIFICATION_DATA), ADMIN));
if (!newThumbnailModification.equals(lastThumbnailModification)) if (!newThumbnailModification.equals(lastThumbnailModification))
{ {
break; break;
@@ -579,9 +618,7 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati
NodeRef sourceNodeRef = createSource(ADMIN, "quick.jpg"); NodeRef sourceNodeRef = createSource(ADMIN, "quick.jpg");
final QName doclibRendDefQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "doclib"); final QName doclibRendDefQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "doclib");
transactionService.getRetryingTransactionHelper() transactionService.getRetryingTransactionHelper()
.doInTransaction(() -> .doInTransaction(() -> AuthenticationUtil.runAs(() -> renditionService.render(sourceNodeRef, doclibRendDefQName), ADMIN));
AuthenticationUtil.runAs(() ->
renditionService.render(sourceNodeRef, doclibRendDefQName), ADMIN));
waitForRendition(ADMIN, sourceNodeRef, DOC_LIB, true); waitForRendition(ADMIN, sourceNodeRef, DOC_LIB, true);
renditionService2.setEnabled(true); renditionService2.setEnabled(true);
@@ -652,9 +689,7 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati
NodeRef sourceNodeRef = createSource(ADMIN, "quick.jpg"); NodeRef sourceNodeRef = createSource(ADMIN, "quick.jpg");
final QName doclibRendDefQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "doclib"); final QName doclibRendDefQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "doclib");
transactionService.getRetryingTransactionHelper() transactionService.getRetryingTransactionHelper()
.doInTransaction(() -> .doInTransaction(() -> AuthenticationUtil.runAs(() -> renditionService.render(sourceNodeRef, doclibRendDefQName), ADMIN));
AuthenticationUtil.runAs(() ->
renditionService.render(sourceNodeRef, doclibRendDefQName), ADMIN));
waitForRendition(ADMIN, sourceNodeRef, DOC_LIB, true); waitForRendition(ADMIN, sourceNodeRef, DOC_LIB, true);
renditionService2.setEnabled(true); renditionService2.setEnabled(true);