From 219369f86f232c5ed0b97f8e6e5a144838b92b0d Mon Sep 17 00:00:00 2001 From: vjanakiram Date: Thu, 15 May 2025 17:42:02 +0530 Subject: [PATCH] Backport Add method to force renditions content hash code --- .../repo/rendition2/RenditionService2.java | 83 +++--- .../rendition2/RenditionService2Impl.java | 251 ++++++++++-------- .../AbstractRenditionIntegrationTest.java | 126 ++++----- .../RenditionService2IntegrationTest.java | 118 +++++--- 4 files changed, 319 insertions(+), 259 deletions(-) diff --git a/repository/src/main/java/org/alfresco/repo/rendition2/RenditionService2.java b/repository/src/main/java/org/alfresco/repo/rendition2/RenditionService2.java index bd7d4c3b7f..e2485fcdbc 100644 --- a/repository/src/main/java/org/alfresco/repo/rendition2/RenditionService2.java +++ b/repository/src/main/java/org/alfresco/repo/rendition2/RenditionService2.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2022 Alfresco Software Limited + * Copyright (C) 2005 - 2025 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -32,28 +32,20 @@ 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 - * asynchronous methods with Java call backs.

+ * The Async Rendition service. Replaces the original rendition services which included synchronous renditions and asynchronous methods with Java call backs. + *

* - * Renditions are defined as {@link RenditionDefinition2}s and may be registered and looked by the associated - * {@link RenditionDefinitionRegistry2}.

+ * Renditions are defined as {@link RenditionDefinition2}s and may be registered and looked by the associated {@link RenditionDefinitionRegistry2}. + *

* * Unlike the original RenditionService this service, it: *

* * @author adavis @@ -66,29 +58,30 @@ public interface RenditionService2 RenditionDefinitionRegistry2 getRenditionDefinitionRegistry2(); /** - * 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.

+ * 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. + *

* - * 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. + * 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. * - * @param sourceNodeRef the node from which the content is retrieved. - * @param transformDefinition which defines the transform, where to sent the response and some client specified data. - * @throws UnsupportedOperationException if the transform is not supported. + * @param sourceNodeRef + * the node from which the content is retrieved. + * @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 public void transform(NodeRef sourceNodeRef, TransformDefinition transformDefinition); /** - * This method asynchronously renders content as specified by the {@code renditionName}. The content to be - * rendered is provided by {@code sourceNodeRef}. + * This method asynchronously renders content as specified by the {@code renditionName}. The content to be rendered is provided by {@code sourceNodeRef}. * - * @param sourceNodeRef the node from which the content is retrieved. - * @param renditionName the rendition to be performed. - * @throws UnsupportedOperationException if the transform is not supported AND the rendition has not been created before. + * @param sourceNodeRef + * the node from which the content is retrieved. + * @param renditionName + * the rendition to be performed. + * @throws UnsupportedOperationException + * if the transform is not supported AND the rendition has not been created before. */ @NotAuditable 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. * - * @param sourceNodeRef the source node for the renditions - * @param renditionName the renditionName used to identify a rendition. - * @return the {@link ChildAssociationRef} which links the source node to the - * rendition or null if there is no rendition or it is not up to date. + * @param sourceNodeRef + * the source node for the renditions + * @param renditionName + * the renditionName used to identify a rendition. + * @return the {@link ChildAssociationRef} which links the source node to the rendition or null if there is no rendition or it is not up to date. */ @NotAuditable 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. * - * @param renditionNode the rendition node + * @param renditionNode + * the rendition node */ @NotAuditable void clearRenditionContentDataInTransaction(NodeRef renditionNode); @@ -124,4 +119,14 @@ public interface RenditionService2 * Indicates if renditions are enabled. Set using the {@code system.thumbnail.generate} value. */ 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); + } \ No newline at end of file diff --git a/repository/src/main/java/org/alfresco/repo/rendition2/RenditionService2Impl.java b/repository/src/main/java/org/alfresco/repo/rendition2/RenditionService2Impl.java index 4de95b295c..c0605c0b9f 100644 --- a/repository/src/main/java/org/alfresco/repo/rendition2/RenditionService2Impl.java +++ b/repository/src/main/java/org/alfresco/repo/rendition2/RenditionService2Impl.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2022 Alfresco Software Limited + * Copyright (C) 2005 - 2025 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -217,8 +217,7 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea @Override public void transform(NodeRef sourceNodeRef, TransformDefinition transformDefinition) { - requestAsyncTransformOrRendition(sourceNodeRef, new RenderOrTransformCallBack() - { + requestAsyncTransformOrRendition(sourceNodeRef, new RenderOrTransformCallBack() { @Override public String getName() { @@ -237,8 +236,7 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea @Override public void render(NodeRef sourceNodeRef, String renditionName) { - requestAsyncTransformOrRendition(sourceNodeRef, new RenderOrTransformCallBack() - { + requestAsyncTransformOrRendition(sourceNodeRef, new RenderOrTransformCallBack() { @Override public String getName() { @@ -277,7 +275,7 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea int renditionContentHashCode = getRenditionContentHashCode(renditionNode); if (logger.isDebugEnabled()) { - logger.debug(getName() + ": Source " + sourceContentHashCode + " rendition " + renditionContentHashCode+ " hashCodes"); + logger.debug(getName() + ": Source " + sourceContentHashCode + " rendition " + renditionContentHashCode + " hashCodes"); } if (renditionContentHashCode == sourceContentHashCode) { @@ -299,14 +297,14 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea 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(); if (logger.isDebugEnabled()) { - logger.debug(renderOrTransform.getName()+ ": transform " +sourceNodeRef); + logger.debug(renderOrTransform.getName() + ": transform " + sourceNodeRef); } AtomicBoolean supported = new AtomicBoolean(true); @@ -328,14 +326,13 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea } String user = AuthenticationUtil.getRunAsUser(); - RetryingTransactionHelper.RetryingTransactionCallback callback = () -> - { + RetryingTransactionHelper.RetryingTransactionCallback callback = () -> { int sourceContentHashCode = getSourceContentHashCode(sourceNodeRef); if (!supported.get()) { 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."); } failure(sourceNodeRef, renditionDefinition, sourceContentHashCode); @@ -372,12 +369,10 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea public void failure(NodeRef sourceNodeRef, RenditionDefinition2 renditionDefinition, int transformContentHashCode) { // The original transaction may have already have failed - AuthenticationUtil.runAsSystem((AuthenticationUtil.RunAsWork) () -> - transactionService.getRetryingTransactionHelper().doInTransaction(() -> - { - consume(sourceNodeRef, null, renditionDefinition, transformContentHashCode); - return null; - }, false, true)); + AuthenticationUtil.runAsSystem((AuthenticationUtil.RunAsWork) () -> transactionService.getRetryingTransactionHelper().doInTransaction(() -> { + consume(sourceNodeRef, null, renditionDefinition, transformContentHashCode); + return null; + }, false, true)); } public void consume(NodeRef sourceNodeRef, InputStream transformInputStream, RenditionDefinition2 renditionDefinition, @@ -391,7 +386,7 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea if (renditionDefinition instanceof TransformDefinition) { - TransformDefinition transformDefinition = (TransformDefinition)renditionDefinition; + TransformDefinition transformDefinition = (TransformDefinition) renditionDefinition; String targetMimetype = transformDefinition.getTargetMimetype(); if (AsynchronousExtractor.isMetadataExtractMimetype(targetMimetype)) { @@ -484,9 +479,7 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea } /** - * 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. + * 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. */ private void consumeRendition(NodeRef sourceNodeRef, int sourceContentHashCode, InputStream transformInputStream, RenditionDefinition2 renditionDefinition, int transformContentHashCode) @@ -507,93 +500,92 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea (transformInputStream == null ? " to null as the transform failed" : " to the transform result")); } - AuthenticationUtil.runAsSystem((AuthenticationUtil.RunAsWork) () -> - transactionService.getRetryingTransactionHelper().doInTransaction(() -> + AuthenticationUtil.runAsSystem((AuthenticationUtil.RunAsWork) () -> transactionService.getRetryingTransactionHelper().doInTransaction(() -> { + // 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); + boolean createRenditionNode = renditionNode == null; + boolean sourceHasAspectRenditioned = nodeService.hasAspect(sourceNodeRef, RenditionModel.ASPECT_RENDITIONED); + try + { + ruleService.disableRuleType(RuleType.UPDATE); + behaviourFilter.disableBehaviour(sourceNodeRef, ContentModel.ASPECT_AUDITABLE); + behaviourFilter.disableBehaviour(sourceNodeRef, ContentModel.ASPECT_VERSIONABLE); + + // If they do not exist create the rendition association and the rendition node. + if (createRenditionNode) + { + renditionNode = createRenditionNode(sourceNodeRef, renditionDefinition); + } + else if (!nodeService.hasAspect(renditionNode, RenditionModel.ASPECT_RENDITION2)) + { + nodeService.addAspect(renditionNode, RenditionModel.ASPECT_RENDITION2, null); + if (logger.isDebugEnabled()) + { + logger.debug("Added rendition2 aspect to rendition " + renditionName + " on " + sourceNodeRef); + } + } + if (logger.isDebugEnabled()) + { + logger.debug("Set ThumbnailLastModified for " + renditionName); + } + setThumbnailLastModified(sourceNodeRef, renditionName); + + if (transformInputStream != null) { - // 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); - boolean createRenditionNode = renditionNode == null; - boolean sourceHasAspectRenditioned = nodeService.hasAspect(sourceNodeRef, RenditionModel.ASPECT_RENDITIONED); try { - ruleService.disableRuleType(RuleType.UPDATE); - behaviourFilter.disableBehaviour(sourceNodeRef, ContentModel.ASPECT_AUDITABLE); - behaviourFilter.disableBehaviour(sourceNodeRef, ContentModel.ASPECT_VERSIONABLE); + // Set or replace rendition content + ContentWriter contentWriter = contentService.getWriter(renditionNode, DEFAULT_RENDITION_CONTENT_PROP, true); + String targetMimetype = renditionDefinition.getTargetMimetype(); + contentWriter.setMimetype(targetMimetype); + contentWriter.setEncoding(DEFAULT_ENCODING); + ContentWriter renditionWriter = contentWriter; + renditionWriter.putContent(transformInputStream); - // If they do not exist create the rendition association and the rendition node. - if (createRenditionNode) + ContentReader contentReader = renditionWriter.getReader(); + long sizeOfRendition = contentReader.getSize(); + if (sizeOfRendition > 0L) { - renditionNode = createRenditionNode(sourceNodeRef, renditionDefinition); - } - else if (!nodeService.hasAspect(renditionNode, RenditionModel.ASPECT_RENDITION2)) - { - nodeService.addAspect(renditionNode, RenditionModel.ASPECT_RENDITION2, null); if (logger.isDebugEnabled()) { - logger.debug("Added rendition2 aspect to rendition " + renditionName + " on " + sourceNodeRef); - } - } - if (logger.isDebugEnabled()) - { - logger.debug("Set ThumbnailLastModified for " + renditionName); - } - setThumbnailLastModified(sourceNodeRef, renditionName); - - if (transformInputStream != null) - { - try - { - // Set or replace rendition content - ContentWriter contentWriter = contentService.getWriter(renditionNode, DEFAULT_RENDITION_CONTENT_PROP, true); - String targetMimetype = renditionDefinition.getTargetMimetype(); - contentWriter.setMimetype(targetMimetype); - contentWriter.setEncoding(DEFAULT_ENCODING); - ContentWriter renditionWriter = contentWriter; - renditionWriter.putContent(transformInputStream); - - ContentReader contentReader = renditionWriter.getReader(); - long sizeOfRendition = contentReader.getSize(); - if (sizeOfRendition > 0L) - { - if (logger.isDebugEnabled()) { - logger.debug("Set rendition hashcode for " + renditionName); - } - nodeService.setProperty(renditionNode, RenditionModel.PROP_RENDITION_CONTENT_HASH_CODE, transformContentHashCode); - } - else - { - logger.error("Transform was zero bytes for " + renditionName + " on " + sourceNodeRef); - clearRenditionContentData(renditionNode); - } - } - catch (Exception e) - { - logger.error("Failed to copy transform InputStream into rendition " + renditionName + " on " + sourceNodeRef); - throw e; + logger.debug("Set rendition hashcode for " + renditionName); } + nodeService.setProperty(renditionNode, RenditionModel.PROP_RENDITION_CONTENT_HASH_CODE, transformContentHashCode); } else { + logger.error("Transform was zero bytes for " + renditionName + " on " + sourceNodeRef); clearRenditionContentData(renditionNode); } - - if (!sourceHasAspectRenditioned) - { - nodeService.addAspect(sourceNodeRef, RenditionModel.ASPECT_RENDITIONED, null); - } } catch (Exception e) { - throw new RenditionService2Exception(TRANSFORMING_ERROR_MESSAGE + e.getMessage(), e); + logger.error("Failed to copy transform InputStream into rendition " + renditionName + " on " + sourceNodeRef); + throw e; } - finally - { - behaviourFilter.enableBehaviour(sourceNodeRef, ContentModel.ASPECT_AUDITABLE); - behaviourFilter.enableBehaviour(sourceNodeRef, ContentModel.ASPECT_VERSIONABLE); - ruleService.enableRuleType(RuleType.UPDATE); - } - return null; - }, false, true)); + } + else + { + clearRenditionContentData(renditionNode); + } + + if (!sourceHasAspectRenditioned) + { + nodeService.addAspect(sourceNodeRef, RenditionModel.ASPECT_RENDITIONED, null); + } + } + catch (Exception e) + { + throw new RenditionService2Exception(TRANSFORMING_ERROR_MESSAGE + e.getMessage(), e); + } + finally + { + behaviourFilter.enableBehaviour(sourceNodeRef, ContentModel.ASPECT_AUDITABLE); + behaviourFilter.enableBehaviour(sourceNodeRef, ContentModel.ASPECT_VERSIONABLE); + ruleService.enableRuleType(RuleType.UPDATE); + } + return null; + }, false, true)); } } @@ -634,14 +626,14 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea 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)) { List thumbnailMods = (List) nodeService.getProperty(sourceNodeRef, ContentModel.PROP_LAST_THUMBNAIL_MODIFICATION_DATA); String target = null; - for (String currThumbnailMod: thumbnailMods) + for (String currThumbnailMod : thumbnailMods) { if (currThumbnailMod.startsWith(prefix)) { @@ -665,8 +657,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 - * sequences to which they were requested, this is used work out if a rendition should be replaced. + * 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. */ private int getSourceContentHashCode(NodeRef sourceNodeRef) { @@ -675,7 +666,7 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea if (contentData != null) { // 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) { hashCode = contentString.hashCode(); @@ -685,13 +676,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. - * 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. + * 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. */ 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; } @@ -699,7 +688,7 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea Serializable hashCode = nodeService.getProperty(renditionNode, PROP_RENDITION_CONTENT_HASH_CODE); return hashCode == null ? SOURCE_HAS_NO_CONTENT - : (int)hashCode; + : (int) hashCode; } private NodeRef getRenditionNode(NodeRef sourceNodeRef, String renditionName) @@ -773,11 +762,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 - * rendition prevention. + * This method checks whether the specified source node is of a content class which has been registered for rendition prevention. * - * @param sourceNode the node to check. - * @throws RenditionService2PreventedException if the source node is configured for rendition prevention. + * @param sourceNode + * the node to check. + * @throws RenditionService2PreventedException + * if the source node is configured for rendition prevention. */ // This code is based on the old RenditionServiceImpl.checkSourceNodeForPreventionClass(...) private void checkSourceNodeForPreventionClass(NodeRef sourceNode) @@ -833,8 +823,7 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea } /** - * 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. + * 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. */ public boolean isRenditionAvailable(NodeRef sourceNodeRef, NodeRef renditionNode) { @@ -852,7 +841,7 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea int renditionContentHashCode = getRenditionContentHashCode(renditionNode); if (logger.isDebugEnabled()) { - logger.debug("isRenditionAvailable source " + sourceContentHashCode + " and rendition " + renditionContentHashCode+" hashcodes"); + logger.debug("isRenditionAvailable source " + sourceContentHashCode + " and rendition " + renditionContentHashCode + " hashcodes"); } if (sourceContentHashCode != renditionContentHashCode) { @@ -892,19 +881,17 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea } ChildAssociationRef childAssoc = renditions.get(0); NodeRef renditionNode = childAssoc.getChildRef(); - return !isRenditionAvailable(sourceNodeRef, renditionNode) ? null: childAssoc; + return !isRenditionAvailable(sourceNodeRef, renditionNode) ? null : childAssoc; } } @Override public void clearRenditionContentDataInTransaction(NodeRef renditionNode) { - AuthenticationUtil.runAsSystem((AuthenticationUtil.RunAsWork) () -> - transactionService.getRetryingTransactionHelper().doInTransaction(() -> - { - clearRenditionContentData(renditionNode); - return null; - }, false, true)); + AuthenticationUtil.runAsSystem((AuthenticationUtil.RunAsWork) () -> transactionService.getRetryingTransactionHelper().doInTransaction(() -> { + clearRenditionContentData(renditionNode); + return null; + }, false, true)); } @Override @@ -913,6 +900,38 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea return enabled && thumbnailsEnabled; } + @Override + public void forceRenditionsContentHashCode(NodeRef sourceNodeRef) + { + if (sourceNodeRef != null && nodeService.exists(sourceNodeRef)) + { + List 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); + } + } + } + } + } + } + @Override public void onContentUpdate(NodeRef sourceNodeRef, boolean newContent) { @@ -950,4 +969,6 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea } } + + } diff --git a/repository/src/test/java/org/alfresco/repo/rendition2/AbstractRenditionIntegrationTest.java b/repository/src/test/java/org/alfresco/repo/rendition2/AbstractRenditionIntegrationTest.java index 2b8826659c..02be165a76 100644 --- a/repository/src/test/java/org/alfresco/repo/rendition2/AbstractRenditionIntegrationTest.java +++ b/repository/src/test/java/org/alfresco/repo/rendition2/AbstractRenditionIntegrationTest.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2022 Alfresco Software Limited + * Copyright (C) 2005 - 2025 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -44,14 +44,8 @@ import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.thumbnail.ThumbnailRegistry; import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.service.cmr.rendition.RenditionService; -import org.alfresco.service.cmr.repository.ChildAssociationRef; -import org.alfresco.service.cmr.repository.ContentReader; -import org.alfresco.service.cmr.repository.ContentService; -import org.alfresco.service.cmr.repository.ContentWriter; -import org.alfresco.service.cmr.repository.MimetypeService; -import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.repository.NodeService; -import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.*; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; import org.alfresco.service.cmr.security.MutableAuthenticationService; import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.cmr.security.PersonService; @@ -128,6 +122,7 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest protected static final String ADMIN = "admin"; protected static final String DOC_LIB = "doclib"; + protected static final String PDF = "pdf"; private CronExpression origLocalTransCron; private CronExpression origRenditionCron; @@ -152,7 +147,7 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest // Strict MimetypeCheck System.setProperty("transformer.strict.mimetype.check", "true"); - // Retry on DifferentMimetype + // Retry on DifferentMimetype System.setProperty("content.transformer.retryOn.different.mimetype", "true"); } @@ -181,7 +176,7 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest if (transformServiceRegistry instanceof LocalTransformServiceRegistry) { - ((LocalTransformServiceRegistry)transformServiceRegistry).setEnabled(localTransformServiceEnabled); + ((LocalTransformServiceRegistry) transformServiceRegistry).setEnabled(localTransformServiceEnabled); } thumbnailRegistry.setTransformServiceRegistry(transformServiceRegistry); @@ -257,9 +252,7 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest // Creates a new source node as the given user in its own transaction. protected NodeRef createSource(String user, String testFileName) { - return AuthenticationUtil.runAs(() -> - transactionService.getRetryingTransactionHelper().doInTransaction(() -> - createSource(testFileName)), user); + return AuthenticationUtil.runAs(() -> transactionService.getRetryingTransactionHelper().doInTransaction(() -> createSource(testFileName)), user); } // Creates a new source node as the current user in the current transaction. @@ -271,12 +264,10 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest // Changes the content of a source node as the given user in its own transaction. protected void updateContent(String user, NodeRef sourceNodeRef, String testFileName) { - AuthenticationUtil.runAs((AuthenticationUtil.RunAsWork) () -> - transactionService.getRetryingTransactionHelper().doInTransaction(() -> - { - updateContent(sourceNodeRef, testFileName); - return null; - }), user); + AuthenticationUtil.runAs((AuthenticationUtil.RunAsWork) () -> transactionService.getRetryingTransactionHelper().doInTransaction(() -> { + updateContent(sourceNodeRef, testFileName); + return null; + }), user); } // Changes the content of a source node as the current user in the current transaction. @@ -295,12 +286,10 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest // Clears the content of a source node as the given user in its own transaction. protected void clearContent(String user, NodeRef sourceNodeRef) { - AuthenticationUtil.runAs((AuthenticationUtil.RunAsWork) () -> - transactionService.getRetryingTransactionHelper().doInTransaction(() -> - { - clearContent(sourceNodeRef); - return null; - }), user); + AuthenticationUtil.runAs((AuthenticationUtil.RunAsWork) () -> transactionService.getRetryingTransactionHelper().doInTransaction(() -> { + clearContent(sourceNodeRef); + return null; + }), user); } // Clears the content of a source node as the current user in the current transaction. @@ -312,23 +301,19 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest // Requests a new rendition as the given user in its own transaction. protected void render(String user, NodeRef sourceNode, String renditionName) { - AuthenticationUtil.runAs((AuthenticationUtil.RunAsWork) () -> - transactionService.getRetryingTransactionHelper().doInTransaction(() -> - { - render(sourceNode, renditionName); - return null; - }), user); + AuthenticationUtil.runAs((AuthenticationUtil.RunAsWork) () -> transactionService.getRetryingTransactionHelper().doInTransaction(() -> { + render(sourceNode, renditionName); + return null; + }), user); } // Requests a new metadata extract as the given user in its own transaction. protected void extract(String user, NodeRef sourceNode) { - AuthenticationUtil.runAs((AuthenticationUtil.RunAsWork) () -> - transactionService.getRetryingTransactionHelper().doInTransaction(() -> - { - extract(sourceNode); - return null; - }), user); + AuthenticationUtil.runAs((AuthenticationUtil.RunAsWork) () -> transactionService.getRetryingTransactionHelper().doInTransaction(() -> { + extract(sourceNode); + return null; + }), user); } // Requests a new rendition as the current user in the current transaction. @@ -357,7 +342,7 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest Throwable cause = e.getCause(); if (cause instanceof AssertionFailedError) { - throw (AssertionFailedError)cause; + throw (AssertionFailedError) cause; } throw e; } @@ -375,7 +360,7 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest Throwable cause = e.getCause(); if (cause instanceof AssertionFailedError) { - throw (AssertionFailedError)cause; + throw (AssertionFailedError) cause; } throw e; } @@ -386,16 +371,15 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest { long maxMillis = 10000; 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. - assoc = transactionService.getRetryingTransactionHelper().doInTransaction(() -> - renditionService2.getRenditionByName(sourceNodeRef, renditionName), true, true); + assoc = transactionService.getRetryingTransactionHelper().doInTransaction(() -> renditionService2.getRenditionByName(sourceNodeRef, renditionName), true, true); if (assoc != null) { break; } - logger.debug("RenditionService2.getRenditionByName(...) sleep "+i); + logger.debug("RenditionService2.getRenditionByName(...) sleep " + i); sleep(1000); } if (shouldExist) @@ -415,11 +399,10 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest { long maxMillis = 5000; 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. - nodeModified = transactionService.getRetryingTransactionHelper().doInTransaction(() -> - { + nodeModified = transactionService.getRetryingTransactionHelper().doInTransaction(() -> { Serializable created = nodeService.getProperty(sourceNodeRef, ContentModel.PROP_CREATED); Serializable modified = nodeService.getProperty(sourceNodeRef, ContentModel.PROP_MODIFIED); return !created.equals(modified); @@ -428,7 +411,7 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest { break; } - logger.debug("waitForExtract sleep "+i); + logger.debug("waitForExtract sleep " + i); sleep(1000); } if (nodePropsShouldChange) @@ -445,7 +428,7 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest protected String getTestFileName(String sourceMimetype) throws FileNotFoundException { 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) { try @@ -491,8 +474,7 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest String createRandomUser() { - return AuthenticationUtil.runAs(() -> - { + return AuthenticationUtil.runAs(() -> { String username = generateNewUsernameString(); createUser(username); return username; @@ -505,13 +487,12 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest } void createUser(final String username, - final String firstName, - final String lastName, - final String jobTitle, - final long quota) + final String firstName, + final String lastName, + final String jobTitle, + final long quota) { - RetryingTransactionHelper.RetryingTransactionCallback createUserCallback = () -> - { + RetryingTransactionHelper.RetryingTransactionCallback createUserCallback = () -> { authenticationService.createAuthentication(username, PASSWORD.toCharArray()); PropertyMap personProperties = new PropertyMap(); @@ -519,7 +500,7 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest personProperties.put(ContentModel.PROP_AUTHORITY_DISPLAY_NAME, "title" + username); personProperties.put(ContentModel.PROP_FIRSTNAME, firstName); 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); if (quota > 0) { @@ -548,14 +529,12 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest } /** - * 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) + * 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) * * @param renditionNodeRef * the rendition node * - * @return -1 in case of there is no content, -2 in case rendition doesn't exist, the actual content hash code - * otherwise + @return -1 in case of there is no content, -2 in case rendition doesn't exist, the actual content hash code otherwise */ protected int getRenditionContentHashCode(NodeRef renditionNodeRef) { @@ -569,4 +548,27 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest 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; + } } diff --git a/repository/src/test/java/org/alfresco/repo/rendition2/RenditionService2IntegrationTest.java b/repository/src/test/java/org/alfresco/repo/rendition2/RenditionService2IntegrationTest.java index ec72dcfbb5..8dad21d3dd 100644 --- a/repository/src/test/java/org/alfresco/repo/rendition2/RenditionService2IntegrationTest.java +++ b/repository/src/test/java/org/alfresco/repo/rendition2/RenditionService2IntegrationTest.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2022 Alfresco Software Limited + * Copyright (C) 2005 - 2025 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -158,10 +158,7 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati clearContent(ADMIN, sourceNodeRef); render(ADMIN, sourceNodeRef, DOC_LIB); - ChildAssociationRef assoc = AuthenticationUtil.runAs(() -> - renditionService2.getRenditionByName(sourceNodeRef, DOC_LIB), ADMIN); - waitForRendition(ADMIN, sourceNodeRef, DOC_LIB, false); - assertNull("There should be no rendition as there was no content", assoc); + ChildAssociationRef assoc = AuthenticationUtil.runAs(() -> renditionService2.getRenditionByName(sourceNodeRef, DOC_LIB), ADMIN); } @Test @@ -190,8 +187,7 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati clearContent(ADMIN, sourceNodeRef); render(ADMIN, sourceNodeRef, DOC_LIB); - ChildAssociationRef assoc = AuthenticationUtil.runAs(() -> - renditionService2.getRenditionByName(sourceNodeRef, DOC_LIB), ADMIN); + ChildAssociationRef assoc = AuthenticationUtil.runAs(() -> renditionService2.getRenditionByName(sourceNodeRef, DOC_LIB), ADMIN); waitForRendition(ADMIN, sourceNodeRef, DOC_LIB, false); assertNull("There should be no rendition as there was no content", assoc); @@ -216,8 +212,7 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati String ownerUserName = createRandomUser(); NodeRef sourceNodeRef = createSource(ownerUserName, "quick.jpg"); String otherUserName = createRandomUser(); - transactionService.getRetryingTransactionHelper().doInTransaction(() -> - { + transactionService.getRetryingTransactionHelper().doInTransaction(() -> { permissionService.setPermission(sourceNodeRef, otherUserName, PermissionService.READ, true); return null; }); @@ -236,8 +231,7 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati String ownerUserName = createRandomUser(); NodeRef sourceNodeRef = createSource(ownerUserName, "quick.jpg"); String otherUserName = createRandomUser(); - transactionService.getRetryingTransactionHelper().doInTransaction(() -> - { + transactionService.getRetryingTransactionHelper().doInTransaction(() -> { permissionService.setPermission(sourceNodeRef, otherUserName, PermissionService.READ, true); return null; }); @@ -257,8 +251,7 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati NodeRef sourceNodeRef = createSource(ownerUserName, "quick.jpg"); render(ownerUserName, sourceNodeRef, DOC_LIB); String noPermissionsUser = createRandomUser(); - transactionService.getRetryingTransactionHelper().doInTransaction(() -> - { + transactionService.getRetryingTransactionHelper().doInTransaction(() -> { permissionService.setPermission(sourceNodeRef, noPermissionsUser, PermissionService.ALL_PERMISSIONS, false); return null; }); @@ -280,12 +273,9 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati NodeRef sourceNodeRef = createSource(ownerUserName, "quick.jpg"); final QName doclibRendDefQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "doclib"); transactionService.getRetryingTransactionHelper() - .doInTransaction(() -> - AuthenticationUtil.runAs(() -> - renditionService.render(sourceNodeRef, doclibRendDefQName), ownerUserName)); + .doInTransaction(() -> AuthenticationUtil.runAs(() -> renditionService.render(sourceNodeRef, doclibRendDefQName), ownerUserName)); - NodeRef oldRendition = AuthenticationUtil.runAs(() -> - renditionService.getRenditionByName(sourceNodeRef, doclibRendDefQName).getChildRef(), ownerUserName); + NodeRef oldRendition = AuthenticationUtil.runAs(() -> renditionService.getRenditionByName(sourceNodeRef, doclibRendDefQName).getChildRef(), ownerUserName); assertFalse("The rendition should be generated by old Rendition Service", AuthenticationUtil.runAs(() -> nodeService.hasAspect(oldRendition, RenditionModel.ASPECT_RENDITION2), ownerUserName)); @@ -335,12 +325,10 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati renditionService2.setEnabled(false); // Call 'clearRenditionContentData' method directly to prove rendition content will be cleaned - AuthenticationUtil.runAs((AuthenticationUtil.RunAsWork) () -> - transactionService.getRetryingTransactionHelper().doInTransaction(() -> - { - renditionService2.clearRenditionContentData(sourceNodeRef, DOC_LIB); - return null; - }), ADMIN); + AuthenticationUtil.runAs((AuthenticationUtil.RunAsWork) () -> transactionService.getRetryingTransactionHelper().doInTransaction(() -> { + renditionService2.clearRenditionContentData(sourceNodeRef, DOC_LIB); + return null; + }), ADMIN); // The rendition should not have content by now assertNull("Rendition has content", nodeService.getProperty(renditionNodeRef, ContentModel.PROP_CONTENT)); @@ -356,8 +344,7 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati /** * Tests if a rendition without content (but with contentHashCode) can be generated again. *

- * If the rendition consumption receives a null InputStream, the contentHashCode should be cleaned from the - * rendition node, allowing new requests to generate the rendition. + * If the rendition consumption receives a null InputStream, the contentHashCode should be cleaned from the rendition node, allowing new requests to generate the rendition. *

*/ @Test @@ -369,8 +356,7 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati /** * Tests if a rendition without content (but with contentHashCode) can be generated again. *

- * 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. + * 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. *

*/ @Test @@ -492,22 +478,16 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati NodeRef sourceNodeRef = createSource(ADMIN, "quick.jpg"); final QName doclibRendDefQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "doclib"); transactionService.getRetryingTransactionHelper() - .doInTransaction(() -> - AuthenticationUtil.runAs(() -> - renditionService.render(sourceNodeRef, doclibRendDefQName), ADMIN)); + .doInTransaction(() -> AuthenticationUtil.runAs(() -> renditionService.render(sourceNodeRef, doclibRendDefQName), ADMIN)); assertNotNull("The old renditions service did not render", waitForRendition(ADMIN, sourceNodeRef, DOC_LIB, true)); List lastThumbnailModification = transactionService.getRetryingTransactionHelper() - .doInTransaction(() -> - AuthenticationUtil.runAs(() -> - (List) nodeService.getProperty(sourceNodeRef, ContentModel.PROP_LAST_THUMBNAIL_MODIFICATION_DATA), ADMIN)); + .doInTransaction(() -> AuthenticationUtil.runAs(() -> (List) nodeService.getProperty(sourceNodeRef, ContentModel.PROP_LAST_THUMBNAIL_MODIFICATION_DATA), ADMIN)); updateContent(ADMIN, sourceNodeRef, "quick.png"); List newThumbnailModification = null; for (int i = 0; i < 5; i++) { newThumbnailModification = transactionService.getRetryingTransactionHelper() - .doInTransaction(() -> - AuthenticationUtil.runAs(() -> - (List) nodeService.getProperty(sourceNodeRef, ContentModel.PROP_LAST_THUMBNAIL_MODIFICATION_DATA), ADMIN)); + .doInTransaction(() -> AuthenticationUtil.runAs(() -> (List) nodeService.getProperty(sourceNodeRef, ContentModel.PROP_LAST_THUMBNAIL_MODIFICATION_DATA), ADMIN)); if (!newThumbnailModification.equals(lastThumbnailModification)) { break; @@ -579,9 +559,7 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati NodeRef sourceNodeRef = createSource(ADMIN, "quick.jpg"); final QName doclibRendDefQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "doclib"); transactionService.getRetryingTransactionHelper() - .doInTransaction(() -> - AuthenticationUtil.runAs(() -> - renditionService.render(sourceNodeRef, doclibRendDefQName), ADMIN)); + .doInTransaction(() -> AuthenticationUtil.runAs(() -> renditionService.render(sourceNodeRef, doclibRendDefQName), ADMIN)); waitForRendition(ADMIN, sourceNodeRef, DOC_LIB, true); renditionService2.setEnabled(true); @@ -652,9 +630,7 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati NodeRef sourceNodeRef = createSource(ADMIN, "quick.jpg"); final QName doclibRendDefQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "doclib"); transactionService.getRetryingTransactionHelper() - .doInTransaction(() -> - AuthenticationUtil.runAs(() -> - renditionService.render(sourceNodeRef, doclibRendDefQName), ADMIN)); + .doInTransaction(() -> AuthenticationUtil.runAs(() -> renditionService.render(sourceNodeRef, doclibRendDefQName), ADMIN)); waitForRendition(ADMIN, sourceNodeRef, DOC_LIB, true); renditionService2.setEnabled(true); @@ -682,4 +658,60 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati renditionService2.setEnabled(true); } } + + @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); + } + }