diff --git a/source/java/org/alfresco/rest/api/Nodes.java b/source/java/org/alfresco/rest/api/Nodes.java index c45dd706cd..1ad65ebef6 100644 --- a/source/java/org/alfresco/rest/api/Nodes.java +++ b/source/java/org/alfresco/rest/api/Nodes.java @@ -216,4 +216,6 @@ public interface Nodes String PARAM_VERSION_MAJOR = "majorVersion"; // true if major, false if minor String PARAM_VERSION_COMMENT = "comment"; + + String PARAM_RENDITIONS = "renditions"; } diff --git a/source/java/org/alfresco/rest/api/impl/NodesImpl.java b/source/java/org/alfresco/rest/api/impl/NodesImpl.java index b46193eabe..b8a08563d1 100644 --- a/source/java/org/alfresco/rest/api/impl/NodesImpl.java +++ b/source/java/org/alfresco/rest/api/impl/NodesImpl.java @@ -51,8 +51,10 @@ import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; import org.alfresco.repo.security.permissions.AccessDeniedException; import org.alfresco.repo.site.SiteModel; -import org.alfresco.repo.site.SiteServiceInternal; import org.alfresco.repo.tenant.TenantUtil; +import org.alfresco.repo.thumbnail.ThumbnailDefinition; +import org.alfresco.repo.thumbnail.ThumbnailHelper; +import org.alfresco.repo.thumbnail.ThumbnailRegistry; import org.alfresco.repo.version.VersionModel; import org.alfresco.rest.antlr.WhereClauseParser; import org.alfresco.rest.api.Nodes; @@ -67,9 +69,11 @@ import org.alfresco.rest.api.model.QuickShareLink; import org.alfresco.rest.api.model.UserInfo; import org.alfresco.rest.framework.core.exceptions.ApiException; import org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException; +import org.alfresco.rest.framework.core.exceptions.DisabledServiceException; import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; import org.alfresco.rest.framework.core.exceptions.InsufficientStorageException; import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.NotFoundException; import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; import org.alfresco.rest.framework.core.exceptions.RequestEntityTooLargeException; import org.alfresco.rest.framework.core.exceptions.UnsupportedMediaTypeException; @@ -114,6 +118,7 @@ import org.alfresco.service.cmr.security.AuthorityService; import org.alfresco.service.cmr.security.OwnableService; import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.thumbnail.ThumbnailService; import org.alfresco.service.cmr.usage.ContentQuotaException; import org.alfresco.service.cmr.version.VersionService; import org.alfresco.service.cmr.version.VersionType; @@ -168,6 +173,7 @@ public class NodesImpl implements Nodes private PersonService personService; private OwnableService ownableService; private AuthorityService authorityService; + private ThumbnailService thumbnailService; private BehaviourFilter behaviourFilter; @@ -204,6 +210,7 @@ public class NodesImpl implements Nodes this.personService = sr.getPersonService(); this.ownableService = sr.getOwnableService(); this.authorityService = sr.getAuthorityService(); + this.thumbnailService = sr.getThumbnailService(); if (defaultIgnoreTypesAndAspects != null) { @@ -1372,6 +1379,18 @@ public class NodesImpl implements Nodes validateCmObject(nodeTypeQName); } + List thumbnailDefs = null; + String renditionsParam = parameters.getParameter(PARAM_RENDITIONS); + if (renditionsParam != null) + { + if (!isContent) + { + throw new InvalidArgumentException("Renditions ['"+renditionsParam+"'] only apply to content types: "+parentNodeRef.getId()+","+nodeName); + } + + thumbnailDefs = getThumbnailDefs(renditionsParam); + } + Map props = new HashMap<>(1); if (nodeInfo.getProperties() != null) @@ -1422,7 +1441,11 @@ public class NodesImpl implements Nodes writer.putContent(""); } - return getFolderOrDocument(nodeRef.getId(), parameters); + Node newNode = getFolderOrDocument(nodeRef.getId(), parameters); + + requestRenditions(thumbnailDefs, newNode); // note: noop for folder + + return newNode; } private NodeRef getOrCreatePath(NodeRef parentNodeRef, String relativePath) @@ -1926,6 +1949,8 @@ public class NodesImpl implements Nodes Boolean majorVersion = null; String versionComment = null; String relativePath = null; + String renditionNames = null; + Map qnameStrProps = new HashMap<>(); Map properties = null; @@ -1977,6 +2002,10 @@ public class NodesImpl implements Nodes relativePath = getStringOrNull(field.getValue()); break; + case "renditions": + renditionNames = getStringOrNull(field.getValue()); + break; + default: { final String propName = field.getName(); @@ -2011,6 +2040,8 @@ public class NodesImpl implements Nodes try { + List thumbnailDefs = getThumbnailDefs(renditionNames); + // Map the given properties, if any. if (qnameStrProps.size() > 0) { @@ -2045,7 +2076,11 @@ public class NodesImpl implements Nodes } // Create a new file. - return createNewFile(parentNodeRef, fileName, nodeTypeQName, content, properties, parameters); + Node fileNode = createNewFile(parentNodeRef, fileName, nodeTypeQName, content, properties, parameters); + + requestRenditions(thumbnailDefs, fileNode); + + return fileNode; // Do not clean formData temp files to allow for retries. // Temp files will be deleted later when GC call DiskFileItem#finalize() method or by temp file cleaner. @@ -2109,6 +2144,77 @@ public class NodesImpl implements Nodes return null; } + private List getThumbnailDefs(String renditionsParam) + { + List thumbnailDefs = null; + + if (renditionsParam != null) + { + // If thumbnail generation has been configured off, then don't bother. + if (!thumbnailService.getThumbnailsEnabled()) + { + throw new DisabledServiceException("Thumbnail generation has been disabled."); + } + + String[] renditionNames = renditionsParam.split(","); + + // Temporary - pending future improvements to thumbnail service to minimise chance of + // missing/failed thumbnails (when requested/generated 'concurrently') + if (renditionNames.length > 1) + { + throw new InvalidArgumentException("Please specify one rendition entity id only"); + } + + thumbnailDefs = new ArrayList<>(renditionNames.length); + ThumbnailRegistry registry = thumbnailService.getThumbnailRegistry(); + for (String renditionName : renditionNames) + { + renditionName = renditionName.trim(); + if (!renditionName.isEmpty()) + { + // Use the thumbnail registry to get the details of the thumbnail + ThumbnailDefinition thumbnailDef = registry.getThumbnailDefinition(renditionName); + if (thumbnailDef == null) + { + throw new NotFoundException(renditionName + " is not registered."); + } + + thumbnailDefs.add(thumbnailDef); + } + } + } + + return thumbnailDefs; + } + + private void requestRenditions(List thumbnailDefs, Node fileNode) + { + if (thumbnailDefs != null) + { + ThumbnailRegistry registry = thumbnailService.getThumbnailRegistry(); + for (ThumbnailDefinition thumbnailDef : thumbnailDefs) + { + NodeRef sourceNodeRef = fileNode.getNodeRef(); + String mimeType = fileNode.getContent().getMimeType(); + long size = fileNode.getContent().getSizeInBytes(); + + // Check if anything is currently available to generate thumbnails for the specified mimeType + if (! registry.isThumbnailDefinitionAvailable(null, mimeType, size, sourceNodeRef, thumbnailDef)) + { + throw new InvalidArgumentException("Unable to create thumbnail '" + thumbnailDef.getName() + "' for " + + mimeType + " as no transformer is currently available."); + } + + Action action = ThumbnailHelper.createCreateThumbnailAction(thumbnailDef, sr); + + // Queue async creation of thumbnail + actionService.executeAction(action, sourceNodeRef, true, true); + } + } + } + + + /** * Writes the content to the repository. * diff --git a/source/test-java/org/alfresco/rest/api/tests/AbstractBaseApiTest.java b/source/test-java/org/alfresco/rest/api/tests/AbstractBaseApiTest.java index dbcda1ff6a..8d169649d0 100644 --- a/source/test-java/org/alfresco/rest/api/tests/AbstractBaseApiTest.java +++ b/source/test-java/org/alfresco/rest/api/tests/AbstractBaseApiTest.java @@ -421,6 +421,31 @@ public abstract class AbstractBaseApiTest extends EnterpriseTestApi protected static final long PAUSE_TIME = 5000; //millisecond protected static final int MAX_RETRY = 10; + protected Rendition waitAndGetRendition(String userId, String sourceNodeId, String renditionId) throws Exception + { + int retryCount = 0; + while (retryCount < MAX_RETRY) + { + try + { + HttpResponse response = getSingle(getNodeRenditionsUrl(sourceNodeId), userId, renditionId, 200); + Rendition rendition = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Rendition.class); + assertNotNull(rendition); + assertEquals(Rendition.RenditionStatus.CREATED, rendition.getStatus()); + return rendition; + } + catch (AssertionError ex) + { + // If the asynchronous create rendition action is not finished yet, + // wait for 'PAUSE_TIME' and try again. + retryCount++; + Thread.sleep(PAUSE_TIME); + } + } + + return null; + } + protected Rendition createAndGetRendition(String userId, String sourceNodeId, String renditionId) throws Exception { Rendition renditionRequest = new Rendition(); @@ -444,27 +469,7 @@ public abstract class AbstractBaseApiTest extends EnterpriseTestApi } } - retryCount = 0; - while (retryCount < MAX_RETRY) - { - try - { - HttpResponse response = getSingle(getNodeRenditionsUrl(sourceNodeId), userId, renditionId, 200); - Rendition rendition = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Rendition.class); - assertNotNull(rendition); - assertEquals(Rendition.RenditionStatus.CREATED, rendition.getStatus()); - return rendition; - } - catch (AssertionError ex) - { - // If the asynchronous create rendition action is not finished yet, - // wait for 'PAUSE_TIME' and try again. - retryCount++; - Thread.sleep(PAUSE_TIME); - } - } - - return null; + return waitAndGetRendition(userId, sourceNodeId, renditionId); } protected String getNodeRenditionsUrl(String nodeId) diff --git a/source/test-java/org/alfresco/rest/api/tests/RenditionsTest.java b/source/test-java/org/alfresco/rest/api/tests/RenditionsTest.java index 2e583d63f2..5308fb0d3b 100644 --- a/source/test-java/org/alfresco/rest/api/tests/RenditionsTest.java +++ b/source/test-java/org/alfresco/rest/api/tests/RenditionsTest.java @@ -20,6 +20,7 @@ package org.alfresco.rest.api.tests; import static org.alfresco.rest.api.tests.util.RestApiUtil.toJsonAsString; +import static org.alfresco.rest.api.tests.util.RestApiUtil.toJsonAsStringNonNull; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; @@ -58,6 +59,7 @@ import java.io.ByteArrayInputStream; import java.io.File; import java.io.InputStream; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -448,6 +450,97 @@ public class RenditionsTest extends AbstractBaseApiTest } } + /** + * Tests create rendition when on upload/create of a file + * + *

POST:

+ * {@literal :/alfresco/api//public/alfresco/versions/1/nodes//children} + */ + @Test + public void testCreateRenditionOnUpload() throws Exception + { + String userId = userOneN1.getId(); + + // Create a folder within the site document's library + String folderName = "folder" + System.currentTimeMillis(); + String folder_Id = addToDocumentLibrary(userOneN1Site, folderName, ContentModel.TYPE_FOLDER, userId); + + // Create multipart request + String renditionName = "doclib"; + String fileName = "quick.pdf"; + File file = getResourceFile(fileName); + MultiPartBuilder multiPartBuilder = MultiPartBuilder.create() + .setFileData(new FileData(fileName, file, MimetypeMap.MIMETYPE_PDF)) + .setRenditions(Collections.singletonList(renditionName)); + MultiPartRequest reqBody = multiPartBuilder.build(); + + // Upload quick.pdf file into 'folder' - including request to create 'doclib' thumbnail + HttpResponse response = post(getNodeChildrenUrl(folder_Id), userId, 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(userId, contentNodeId, renditionName); + assertNotNull(rendition); + assertEquals(RenditionStatus.CREATED, rendition.getStatus()); + + // also accepted for JSON when creating empty file (albeit with no content) + Document d1 = new Document(); + d1.setName("d1.txt"); + d1.setNodeType("cm:content"); + ContentInfo ci = new ContentInfo(); + ci.setMimeType("text/plain"); + d1.setContent(ci); + + // create empty file including request to generate imgpreview thumbnail + renditionName = "imgpreview"; + response = post(getNodeChildrenUrl(folder_Id), userId, toJsonAsStringNonNull(d1), "?renditions="+renditionName, 201); + Document documentResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Document.class); + String d1Id = documentResp.getId(); + + // wait and check that rendition is created ... + rendition = waitAndGetRendition(userId, d1Id, renditionName); + assertNotNull(rendition); + assertEquals(RenditionStatus.CREATED, rendition.getStatus()); + + + // -ve - currently we do not support multiple rendition requests on create + reqBody = MultiPartBuilder.create() + .setFileData(new FileData(fileName, file, MimetypeMap.MIMETYPE_PDF)) + .setRenditions(Arrays.asList(new String[]{"doclib,imgpreview"})) + .build(); + + post(getNodeChildrenUrl(folder_Id), userId, reqBody.getBody(), null, reqBody.getContentType(), 400); + + // -ve + reqBody = MultiPartBuilder.create() + .setFileData(new FileData(fileName, file, MimetypeMap.MIMETYPE_PDF)) + .setRenditions(Arrays.asList(new String[]{"unknown"})) + .build(); + + post(getNodeChildrenUrl(folder_Id), userId, reqBody.getBody(), null, reqBody.getContentType(), 404); + + // -ve + ThumbnailService thumbnailService = applicationContext.getBean("thumbnailService", ThumbnailService.class); + thumbnailService.setThumbnailsEnabled(false); + try + { + // Create multipart request + String txtFileName = "quick-1.txt"; + File txtFile = getResourceFile(fileName); + reqBody = MultiPartBuilder.create() + .setFileData(new FileData(txtFileName, txtFile, MimetypeMap.MIMETYPE_TEXT_PLAIN)) + .setRenditions(Arrays.asList(new String[]{"doclib"})) + .build(); + + post(getNodeChildrenUrl(folder_Id), userId, reqBody.getBody(), null, reqBody.getContentType(), 501); + } + finally + { + thumbnailService.setThumbnailsEnabled(true); + } + } + /** * Tests download rendition. *

GET:

diff --git a/source/test-java/org/alfresco/rest/api/tests/util/MultiPartBuilder.java b/source/test-java/org/alfresco/rest/api/tests/util/MultiPartBuilder.java index 645b99c222..75bf45bc08 100644 --- a/source/test-java/org/alfresco/rest/api/tests/util/MultiPartBuilder.java +++ b/source/test-java/org/alfresco/rest/api/tests/util/MultiPartBuilder.java @@ -53,6 +53,7 @@ public class MultiPartBuilder private Boolean overwrite; private Boolean autoRename; private String nodeType; + private List renditionIds = Collections.emptyList(); // initially single rendition name/id (in the future we may support multiple) private Map properties = Collections.emptyMap(); private MultiPartBuilder() @@ -71,6 +72,7 @@ public class MultiPartBuilder this.overwrite = that.overwrite; this.autoRename = that.autoRename; this.nodeType = that.nodeType; + this.renditionIds = that.renditionIds; this.properties = new HashMap<>(that.properties); } @@ -150,12 +152,18 @@ public class MultiPartBuilder return this; } - private String getAspects(List aspects) + public MultiPartBuilder setRenditions(List renditionIds) { - if (!aspects.isEmpty()) + this.renditionIds = renditionIds; + return this; + } + + private String getCommaSeparated(List names) + { + if (! names.isEmpty()) { - StringBuilder sb = new StringBuilder(aspects.size() * 2); - for (String str : aspects) + StringBuilder sb = new StringBuilder(names.size() * 2); + for (String str : names) { sb.append(str).append(','); } @@ -255,11 +263,12 @@ public class MultiPartBuilder addPartIfNotNull(parts, "updatenoderef", updateNodeRef); addPartIfNotNull(parts, "description", description); addPartIfNotNull(parts, "contenttype", contentTypeQNameStr); - addPartIfNotNull(parts, "aspects", getAspects(aspects)); + addPartIfNotNull(parts, "aspects", getCommaSeparated(aspects)); addPartIfNotNull(parts, "majorversion", majorVersion); addPartIfNotNull(parts, "overwrite", overwrite); addPartIfNotNull(parts, "autorename", autoRename); addPartIfNotNull(parts, "nodetype", nodeType); + addPartIfNotNull(parts, "renditions", getCommaSeparated(renditionIds)); if (!properties.isEmpty()) {