From f80767359fec043ca0d12a94c744deba440c80ee Mon Sep 17 00:00:00 2001 From: Alan Davis Date: Fri, 14 Jan 2022 10:28:50 +0000 Subject: [PATCH] MNT-22696 Replace Rendition REST API (#880) Code to reject zero byte renditions Addition of: DELETE /nodes/{nodeId}/renditions/{renditionId} DELETE /nodes/{nodeId}/versions/{versionId}/renditions/{renditionId} end points Co-authored-by: kmagdziarz --- .../org/alfresco/rest/api/Renditions.java | 21 ++++- .../rest/api/impl/RenditionsImpl.java | 27 +++++- .../api/nodes/NodeRenditionsRelation.java | 11 ++- .../nodes/NodeVersionRenditionsRelation.java | 21 +++-- .../framework/resource/parameters/Params.java | 4 +- .../webscripts/ResourceWebScriptDelete.java | 13 ++- .../rest/api/tests/AbstractBaseApiTest.java | 12 ++- .../alfresco/rest/api/tests/NodeApiTest.java | 89 ++++++++++++++++++- .../repo/rendition2/RenditionService2.java | 10 ++- .../rendition2/RenditionService2Impl.java | 30 ++++++- .../RenditionService2IntegrationTest.java | 26 +++++- 11 files changed, 243 insertions(+), 21 deletions(-) diff --git a/remote-api/src/main/java/org/alfresco/rest/api/Renditions.java b/remote-api/src/main/java/org/alfresco/rest/api/Renditions.java index aa1e352798..ebcece08ef 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/Renditions.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/Renditions.java @@ -2,7 +2,7 @@ * #%L * Alfresco Remote API * %% - * Copyright (C) 2005 - 2021 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -147,6 +147,25 @@ public interface Renditions void createRenditions(NodeRef nodeRef, String versionId, List renditions, Parameters parameters) throws NotFoundException, ConstraintViolatedException; + /** + * Delete the rendition node. + * + * @param nodeRef the source nodeRef, ie. live node + * @param renditionId the rendition id + * @param parameters the {@link Parameters} object to get the parameters passed into the request + */ + void deleteRendition(NodeRef nodeRef, String renditionId, Parameters parameters); + + /** + * Delete the rendition node. + * + * @param nodeRef the source nodeRef, ie. live node + * @param versionId the version id (aka version label) + * @param renditionId the rendition id + * @param parameters the {@link Parameters} object to get the parameters passed into the request + */ + void deleteRendition(NodeRef nodeRef, String versionId, String renditionId, Parameters parameters); + /** * Downloads rendition. * diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/RenditionsImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/RenditionsImpl.java index cd7b665c54..b70dcd8401 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/impl/RenditionsImpl.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/RenditionsImpl.java @@ -2,7 +2,7 @@ * #%L * Alfresco Remote API * %% - * Copyright (C) 2005 - 2021 Alfresco Software LimitedP + * Copyright (C) 2005 - 2022 Alfresco Software LimitedP * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -444,6 +444,31 @@ public class RenditionsImpl implements Renditions, ResourceLoaderAware } + @Override + public void deleteRendition(NodeRef nodeRef, String renditionId, Parameters parameters) + { + deleteRendition(nodeRef, null, renditionId, parameters); + } + + @Override + public void deleteRendition(NodeRef nodeRef, String versionId, String renditionId, Parameters parameters) + { + if (!renditionService2.isEnabled()) + { + throw new DisabledServiceException("Rendition generation has been disabled."); + } + + final NodeRef validatedNodeRef = validateNode(nodeRef.getStoreRef(), nodeRef.getId(), versionId, parameters); + NodeRef renditionNodeRef = getRenditionByName(validatedNodeRef, renditionId, parameters); + + if (renditionNodeRef == null) + { + throw new NotFoundException(renditionId + " is not registered."); + } + + renditionService2.clearRenditionContentDataInTransaction(renditionNodeRef); + } + private String getName(Rendition rendition) { String renditionName = rendition.getId(); diff --git a/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeRenditionsRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeRenditionsRelation.java index 54cd41747b..56d38291a7 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeRenditionsRelation.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeRenditionsRelation.java @@ -2,7 +2,7 @@ * #%L * Alfresco Remote API * %% - * Copyright (C) 2005 - 2021 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -65,6 +65,7 @@ public class NodeRenditionsRelation implements RelationshipResourceAction.Read, RelationshipResourceAction.Create, RelationshipResourceBinaryAction.Read, + RelationshipResourceAction.Delete, InitializingBean { @@ -110,6 +111,14 @@ public class NodeRenditionsRelation implements RelationshipResourceAction.Read, RelationshipResourceAction.ReadById, RelationshipResourceAction.Create, + RelationshipResourceAction.Delete, RelationshipResourceBinaryAction.Read, InitializingBean { @@ -115,4 +117,13 @@ public class NodeVersionRenditionsRelation implements RelationshipResourceAction return renditions.getContent(nodeRef, versionId, renditionId, parameters); } + @WebApiDescription(title = "Delete rendition") + @Override + public void delete(String nodeId, String versionId, Parameters parameters) + { + String renditionId = parameters.getRelationship2Id(); + + NodeRef nodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, nodeId); + renditions.deleteRendition(nodeRef, versionId, renditionId, parameters); + } } diff --git a/remote-api/src/main/java/org/alfresco/rest/framework/resource/parameters/Params.java b/remote-api/src/main/java/org/alfresco/rest/framework/resource/parameters/Params.java index a3f9fa9613..0026089fe4 100644 --- a/remote-api/src/main/java/org/alfresco/rest/framework/resource/parameters/Params.java +++ b/remote-api/src/main/java/org/alfresco/rest/framework/resource/parameters/Params.java @@ -2,7 +2,7 @@ * #%L * Alfresco Remote API * %% - * Copyright (C) 2005 - 2020 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -188,6 +188,8 @@ public class Params implements Parameters builder.append(this.entityId); builder.append(", relationshipId="); builder.append(this.relationshipId); + builder.append(", relationship2Id="); + builder.append(this.relationship2Id); builder.append(", passedIn="); builder.append(this.passedIn); builder.append(", paging="); diff --git a/remote-api/src/main/java/org/alfresco/rest/framework/webscripts/ResourceWebScriptDelete.java b/remote-api/src/main/java/org/alfresco/rest/framework/webscripts/ResourceWebScriptDelete.java index ea8e62b23a..1541a6e1c5 100644 --- a/remote-api/src/main/java/org/alfresco/rest/framework/webscripts/ResourceWebScriptDelete.java +++ b/remote-api/src/main/java/org/alfresco/rest/framework/webscripts/ResourceWebScriptDelete.java @@ -2,7 +2,7 @@ * #%L * Alfresco Remote API * %% - * Copyright (C) 2005 - 2020 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -68,6 +68,7 @@ public class ResourceWebScriptDelete extends AbstractResourceWebScript implement final Map resourceVars = locator.parseTemplateVars(req.getServiceMatch().getTemplateVars()); final String entityId = resourceVars.get(ResourceLocator.ENTITY_ID); final String relationshipId = resourceVars.get(ResourceLocator.RELATIONSHIP_ID); + final String relationship2Id = resourceVars.get(ResourceLocator.RELATIONSHIP2_ID); final Params.RecognizedParams params = getRecognizedParams(req); @@ -78,7 +79,15 @@ public class ResourceWebScriptDelete extends AbstractResourceWebScript implement return Params.valueOf(params, entityId, relationshipId, req); case RELATIONSHIP: // note: relationshipId can be null - when deleting a related set/collection - return Params.valueOf(params, entityId, relationshipId, req); + if (StringUtils.isNotBlank(relationship2Id)) + { + return Params.valueOf(false, entityId, relationshipId, relationship2Id, + null, null, null, params, null, req); + } + else + { + return Params.valueOf(params, entityId, relationshipId, req); + } case PROPERTY: final String resourceName = resourceVars.get(ResourceLocator.RELATIONSHIP_RESOURCE); final String propertyName = resourceVars.get(ResourceLocator.PROPERTY); diff --git a/remote-api/src/test/java/org/alfresco/rest/api/tests/AbstractBaseApiTest.java b/remote-api/src/test/java/org/alfresco/rest/api/tests/AbstractBaseApiTest.java index c6f0d16d77..a3037ca199 100644 --- a/remote-api/src/test/java/org/alfresco/rest/api/tests/AbstractBaseApiTest.java +++ b/remote-api/src/test/java/org/alfresco/rest/api/tests/AbstractBaseApiTest.java @@ -2,7 +2,7 @@ * #%L * Alfresco Remote API * %% - * Copyright (C) 2005 - 2021 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -1035,6 +1035,16 @@ public abstract class AbstractBaseApiTest extends EnterpriseTestApi return URL_NODES + "/" + nodeId + "/" + URL_RENDITIONS; } + protected String getNodeRenditionIdUrl(String nodeId, String renditionID) + { + return URL_NODES + "/" + nodeId + "/" + URL_RENDITIONS + "/" + renditionID; + } + + protected String getNodeVersionRenditionIdUrl(String nodeId, String versionId, String renditionID) + { + return URL_NODES + "/" + nodeId + "/" + URL_VERSIONS + "/" + versionId + "/" + URL_RENDITIONS + "/" + renditionID; + } + protected String getNodeVersionsUrl(String nodeId) { return URL_NODES + "/" + nodeId + "/" + URL_VERSIONS; diff --git a/remote-api/src/test/java/org/alfresco/rest/api/tests/NodeApiTest.java b/remote-api/src/test/java/org/alfresco/rest/api/tests/NodeApiTest.java index ca61d3e22d..9ceeeb91ac 100644 --- a/remote-api/src/test/java/org/alfresco/rest/api/tests/NodeApiTest.java +++ b/remote-api/src/test/java/org/alfresco/rest/api/tests/NodeApiTest.java @@ -2,7 +2,7 @@ * #%L * Alfresco Remote API * %% - * Copyright (C) 2005 - 2021 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -6366,5 +6366,92 @@ public class NodeApiTest extends AbstractSingleNetworkSiteTest HttpResponse dauResponse = post(getRequestContentDirectUrl(contentNodeId), null, null, null, null, 501); } + + @Test + public void testRequestDeleteRendition() throws Exception + { + setRequestContext(networkOne.getId(), user1, null); + + String myNodeId = getMyNodeId(); + + // Create multipart request - txt file + String renditionName = "pdf"; + String fileName = "quick-1.txt"; + File file = getResourceFile(fileName); + MultiPartRequest reqBody = MultiPartBuilder.create() + .setFileData(new FileData(fileName, file)) + .setRenditions(Collections.singletonList(renditionName)) + .build(); + + //Upload file to user home node + HttpResponse response = post(getNodeChildrenUrl(myNodeId), reqBody.getBody(), null, reqBody.getContentType(), 201); + Document document = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Document.class); + String contentNodeId = document.getId(); + + // wait and check that rendition is created ... + Rendition rendition = waitAndGetRendition(contentNodeId, null, renditionName); + assertNotNull(rendition); + assertEquals(Rendition.RenditionStatus.CREATED, rendition.getStatus()); + + //clean rendition + delete(getNodeRenditionIdUrl(contentNodeId, renditionName), null, null, null, null, 204); + //retry to double-check deletion + delete(getNodeRenditionIdUrl(contentNodeId, renditionName), null, null, null, null, 404); + + //check if rendition was cleaned + HttpResponse getResponse = getSingle(getNodeRenditionIdUrl(contentNodeId, renditionName), null, 200); + Rendition renditionDeleted = RestApiUtil.parseRestApiEntry(getResponse.getJsonResponse(), Rendition.class); + assertNotNull(renditionDeleted); + assertEquals(Rendition.RenditionStatus.NOT_CREATED, renditionDeleted.getStatus()); + } + + @Test + public void testRequestVersionDeleteRendition() throws Exception + { + setRequestContext(networkOne.getId(), user1, null); + + String myNodeId = getMyNodeId(); + + // Create multipart request - txt file + String renditionName = "pdf"; + String fileName = "quick-1.txt"; + File file = getResourceFile(fileName); + MultiPartRequest reqBody = MultiPartBuilder.create() + .setFileData(new FileData(fileName, file)) + .setRenditions(Collections.singletonList(renditionName)) + .build(); + + //Upload file to user home node + HttpResponse response = post(getNodeChildrenUrl(myNodeId), reqBody.getBody(), null, reqBody.getContentType(), 201); + Document document = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Document.class); + String contentNodeId = document.getId(); + + //Update file to newer version + String content = "The quick brown fox jumps over the lazy dog\n the lazy dog jumps over the quick brown fox"; + Map params = new HashMap<>(); + params.put("comment", "my version "); + + document = updateTextFile(contentNodeId, content, params); + assertTrue(document.getAspectNames().contains("cm:versionable")); + assertNotNull(document.getProperties()); + assertEquals("1.1", document.getProperties().get("cm:versionLabel")); + + // create rendition for old version and check that rendition is created ... + Rendition renditionUpdated = createAndGetRendition(contentNodeId, "1.0", renditionName); + assertNotNull(renditionUpdated); + assertEquals(Rendition.RenditionStatus.CREATED, renditionUpdated.getStatus()); + + //clean rendition + delete(getNodeVersionRenditionIdUrl(contentNodeId, "1.0", renditionName), null, null, null, null, 204); + //retry to double-check deletion + delete(getNodeVersionRenditionIdUrl(contentNodeId, "1.0", renditionName), null, null, null, null, 404); + + //check if rendition was cleaned + HttpResponse getResponse = getSingle(getNodeVersionRenditionIdUrl(contentNodeId, "1.0", renditionName), null, 200); + Rendition renditionDeleted = RestApiUtil.parseRestApiEntry(getResponse.getJsonResponse(), Rendition.class); + assertNotNull(renditionDeleted); + assertEquals(Rendition.RenditionStatus.NOT_CREATED, renditionDeleted.getStatus()); + } + } 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 6e5b219be2..bd7d4c3b7f 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 - 2018 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -112,6 +112,14 @@ public interface RenditionService2 @NotAuditable ChildAssociationRef getRenditionByName(NodeRef sourceNodeRef, String renditionName); + /** + * This method clears source nodeRef rendition content and content hash code using supplied rendition name. + * + * @param renditionNode the rendition node + */ + @NotAuditable + void clearRenditionContentDataInTransaction(NodeRef renditionNode); + /** * Indicates if renditions are enabled. Set using the {@code system.thumbnail.generate} value. */ 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 1f540b6af7..cbe5e95f72 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 - 2021 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -38,6 +38,7 @@ import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.repo.util.PostTxnCallbackScheduler; 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.ContentService; import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.NodeRef; @@ -553,11 +554,21 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea contentWriter.setEncoding(DEFAULT_ENCODING); ContentWriter renditionWriter = contentWriter; renditionWriter.putContent(transformInputStream); - if (logger.isDebugEnabled()) + + ContentReader contentReader = renditionWriter.getReader(); + long sizeOfRendition = contentReader.getSize(); + if (sizeOfRendition > 0L) { - logger.debug("Set rendition hashcode for " + renditionName); + 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); } - nodeService.setProperty(renditionNode, RenditionModel.PROP_RENDITION_CONTENT_HASH_CODE, transformContentHashCode); } catch (Exception e) { @@ -892,6 +903,17 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea } } + @Override + public void clearRenditionContentDataInTransaction(NodeRef renditionNode) + { + AuthenticationUtil.runAsSystem((AuthenticationUtil.RunAsWork) () -> + transactionService.getRetryingTransactionHelper().doInTransaction(() -> + { + clearRenditionContentData(renditionNode); + return null; + }, false, true)); + } + @Override public boolean isEnabled() { 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 4cb250e266..f32e425847 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 - 2021 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -28,6 +28,8 @@ package org.alfresco.repo.rendition2; import static org.alfresco.model.ContentModel.PROP_CONTENT; import static org.junit.Assert.assertNotEquals; +import java.io.ByteArrayInputStream; +import java.io.InputStream; import java.util.List; import org.alfresco.model.ContentModel; @@ -358,7 +360,25 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati *

*/ @Test - public void testRenditionWithoutContentButWithContentHashCode() + public void testRenditionWithNullContentButWithContentHashCode() + { + testRenditionWithoutContentButWithContentHashCode(null); + } + + /** + * 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. + *

+ */ + @Test + public void testRenditionWithZeroContentButWithContentHashCode() + { + testRenditionWithoutContentButWithContentHashCode(new ByteArrayInputStream(new byte[0])); + } + + private void testRenditionWithoutContentButWithContentHashCode(InputStream transformInputStream) { // Create a node with an actual rendition NodeRef sourceNodeRef = createSource(ADMIN, "quick.jpg"); @@ -379,7 +399,7 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati // This is explicitly called to prove that rendition hash code will be cleaned (as opposite to previous behavior) RenditionDefinition2 renditionDefinition = renditionDefinitionRegistry2.getRenditionDefinition(DOC_LIB); AuthenticationUtil.runAs(() -> { - renditionService2.consume(sourceNodeRef, null, renditionDefinition, contentHashCode); + renditionService2.consume(sourceNodeRef, transformInputStream, renditionDefinition, contentHashCode); return null; }, ADMIN); waitForRendition(ADMIN, sourceNodeRef, DOC_LIB, false);