From 870a9ee4fdbc8ddc23e39baf245bc5c17290be0b Mon Sep 17 00:00:00 2001 From: Denis Ungureanu Date: Wed, 30 Jun 2021 14:24:35 +0300 Subject: [PATCH] ACS-1631 : Storage classes - REST api - POST & GET /nodes/{nodeId}/children (#540) --- .../alfresco/repo/content/ContentContext.java | 15 +++- .../repo/content/StorageClassSet.java | 5 ++ .../org/alfresco/rest/api/impl/NodesImpl.java | 74 ++++++++++++++--- .../alfresco/rest/api/model/ContentInfo.java | 19 +++-- .../resource/content/ContentInfo.java | 4 +- .../resource/content/ContentInfoImpl.java | 13 +-- .../alfresco/rest/api/tests/NodeApiTest.java | 81 ++++++++++++++++++- .../api/tests/client/data/ContentInfo.java | 16 ++-- .../repo/content/ContentServiceImpl.java | 44 +++++++++- .../repo/content/NodeContentContext.java | 75 +++++++++++------ .../cmr/repository/ContentService.java | 34 ++++++++ 11 files changed, 314 insertions(+), 66 deletions(-) diff --git a/data-model/src/main/java/org/alfresco/repo/content/ContentContext.java b/data-model/src/main/java/org/alfresco/repo/content/ContentContext.java index 95ca0a8f96..b59b2d5697 100644 --- a/data-model/src/main/java/org/alfresco/repo/content/ContentContext.java +++ b/data-model/src/main/java/org/alfresco/repo/content/ContentContext.java @@ -63,7 +63,20 @@ public class ContentContext implements Serializable this.existingContentReader = existingContentReader; this.contentUrl = contentUrl; } - + + /** + * Construct the instance with the content URL. + * + * @param existingContentReader content with which to seed the new writer - may be null + * @param contentUrl the content URL - may be null + * @param storageClasses the storage classes specific to the provided content URL - may be null + */ + public ContentContext(ContentReader existingContentReader, String contentUrl, Set storageClasses) + { + this(existingContentReader, contentUrl); + this.storageClasses = storageClasses; + } + @Override public String toString() { diff --git a/data-model/src/main/java/org/alfresco/repo/content/StorageClassSet.java b/data-model/src/main/java/org/alfresco/repo/content/StorageClassSet.java index 224d069304..f4e864b235 100644 --- a/data-model/src/main/java/org/alfresco/repo/content/StorageClassSet.java +++ b/data-model/src/main/java/org/alfresco/repo/content/StorageClassSet.java @@ -37,6 +37,11 @@ import java.util.HashSet; */ public class StorageClassSet extends HashSet { + public StorageClassSet() + { + super(); + } + public StorageClassSet(String... storageClasses) { super(); diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/NodesImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/NodesImpl.java index 5929ea27dc..bce7ee530c 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/impl/NodesImpl.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/NodesImpl.java @@ -56,6 +56,8 @@ import org.alfresco.repo.action.executer.ContentMetadataExtracter; import org.alfresco.repo.activities.ActivityType; import org.alfresco.repo.content.ContentLimitViolationException; import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.content.StorageClassSet; +import org.alfresco.repo.content.UnsupportedStorageClassException; import org.alfresco.repo.domain.node.AuditablePropertiesEntity; import org.alfresco.repo.lock.mem.Lifetime; import org.alfresco.repo.model.Repository; @@ -105,6 +107,7 @@ import org.alfresco.rest.framework.core.exceptions.RequestEntityTooLargeExceptio import org.alfresco.rest.framework.core.exceptions.UnsupportedMediaTypeException; import org.alfresco.rest.framework.resource.content.BasicContentInfo; import org.alfresco.rest.framework.resource.content.BinaryResource; +import org.alfresco.rest.framework.resource.content.ContentInfo; import org.alfresco.rest.framework.resource.content.ContentInfoImpl; import org.alfresco.rest.framework.resource.content.NodeBinaryResource; import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; @@ -1048,8 +1051,8 @@ public class NodesImpl implements Nodes node.setNodeType(nodeTypeQName.toPrefixString(namespaceService)); node.setPath(pathInfo); - - if (includeParam.contains(PARAM_INCLUDE_STORAGECLASSES)) + if (includeParam.contains(PARAM_INCLUDE_STORAGECLASSES) && node.getIsFile() + && node.getContent().getSizeInBytes() > 0) { node.getContent().setStorageClasses(contentService.findStorageClasses(nodeRef)); } @@ -1883,7 +1886,8 @@ public class NodesImpl implements Nodes if (isContent) { // create empty file node - note: currently will be set to default encoding only (UTF-8) - nodeRef = createNewFile(parentNodeRef, nodeName, nodeTypeQName, null, props, assocTypeQName, parameters, versionMajor, versionComment); + nodeRef = createNewFile(parentNodeRef, nodeName, nodeTypeQName, null, null, props, + assocTypeQName, parameters, versionMajor, versionComment); } else { @@ -2778,7 +2782,13 @@ public class NodesImpl implements Nodes behaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_VERSIONABLE); try { - writeContent(nodeRef, fileName, stream, true); + writeContent(nodeRef, + fileName, + stream, + true, + contentInfo instanceof ContentInfo ? + ((ContentInfo) contentInfo).getStorageClasses() : + null); if ((isVersioned) || (versionMajor != null) || (versionComment != null) ) { @@ -2817,10 +2827,17 @@ public class NodesImpl implements Nodes } private void writeContent(NodeRef nodeRef, String fileName, InputStream stream, boolean guessEncoding) + { + writeContent(nodeRef, fileName, stream, guessEncoding, null); + } + + private void writeContent(NodeRef nodeRef, String fileName, InputStream stream, + boolean guessEncoding, StorageClassSet storageClassSet) { try { - ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true); + ContentWriter writer = contentService + .getWriter(nodeRef, ContentModel.PROP_CONTENT, true, storageClassSet); String mimeType = mimetypeService.guessMimetype(fileName); if ((mimeType != null) && (!mimeType.equals(MimetypeMap.MIMETYPE_BINARY))) @@ -2947,6 +2964,7 @@ public class NodesImpl implements Nodes String relativePath = null; String renditionNames = null; boolean versioningEnabled = true; + String storageClassesParam = null; Map qnameStrProps = new HashMap<>(); Map properties = null; @@ -2984,6 +3002,10 @@ public class NodesImpl implements Nodes } break; + case "storageclasses": + storageClassesParam = getStringOrNull(field.getValue()); + break; + case "overwrite": overwrite = Boolean.valueOf(field.getValue()); break; @@ -3052,8 +3074,9 @@ public class NodesImpl implements Nodes parentNodeRef = getOrCreatePath(parentNodeRef, relativePath); final QName assocTypeQName = ContentModel.ASSOC_CONTAINS; final Set renditions = getRequestedRenditions(renditionNames); + final StorageClassSet storageClasses = getRequestedStorageClasses(storageClassesParam); - validateProperties(qnameStrProps, EXCLUDED_NS, Arrays.asList()); + validateProperties(qnameStrProps, EXCLUDED_NS, Collections.emptyList()); try { // Map the given properties, if any. @@ -3079,8 +3102,13 @@ public class NodesImpl implements Nodes else if (overwrite && nodeService.hasAspect(existingFile, ContentModel.ASPECT_VERSIONABLE)) { // overwrite existing (versionable) file - BasicContentInfo contentInfo = new ContentInfoImpl(content.getMimetype(), content.getEncoding(), -1, null); - return updateExistingFile(parentNodeRef, existingFile, fileName, contentInfo, content.getInputStream(), parameters, versionMajor, versionComment); + + BasicContentInfo contentInfo = new ContentInfoImpl(content.getMimetype(), + content.getEncoding(), -1, + null, storageClasses); + return updateExistingFile(parentNodeRef, existingFile, fileName, contentInfo, + content.getInputStream(), parameters, versionMajor, + versionComment); } else { @@ -3098,7 +3126,9 @@ public class NodesImpl implements Nodes versionMajor = versioningEnabled ? versionMajor : null; // Create a new file. - NodeRef nodeRef = createNewFile(parentNodeRef, fileName, nodeTypeQName, content, properties, assocTypeQName, parameters, versionMajor, versionComment); + NodeRef nodeRef = createNewFile(parentNodeRef, fileName, nodeTypeQName, content, + storageClasses, properties, assocTypeQName, parameters, + versionMajor, versionComment); // Create the response final Node fileNode = getFolderOrDocumentFullInfo(nodeRef, parentNodeRef, nodeTypeQName, parameters); @@ -3115,6 +3145,10 @@ public class NodesImpl implements Nodes { throw new PermissionDeniedException(ade.getMessage()); } + catch (UnsupportedStorageClassException usce) + { + throw new InvalidArgumentException(usce.getMessage()); + } /* * NOTE: Do not clean formData temp files to allow for retries. It's @@ -3123,8 +3157,9 @@ public class NodesImpl implements Nodes */ } - private NodeRef createNewFile(NodeRef parentNodeRef, String fileName, QName nodeType, Content content, Map props, QName assocTypeQName, Parameters params, - Boolean versionMajor, String versionComment) + private NodeRef createNewFile(NodeRef parentNodeRef, String fileName, QName nodeType, + Content content, StorageClassSet storageClassSet, Map props, + QName assocTypeQName, Parameters params, Boolean versionMajor, String versionComment) { NodeRef nodeRef = createNodeImpl(parentNodeRef, fileName, nodeType, props, assocTypeQName); @@ -3136,7 +3171,7 @@ public class NodesImpl implements Nodes else { // Write content - writeContent(nodeRef, fileName, content.getInputStream(), true); + writeContent(nodeRef, fileName, content.getInputStream(), true, storageClassSet); } if ((versionMajor != null) || (versionComment != null)) @@ -3214,6 +3249,21 @@ public class NodesImpl implements Nodes return renditions; } + static StorageClassSet getRequestedStorageClasses(String storageClassesParam) + { + if (storageClassesParam == null) + { + return null; + } + + String[] storageClasses = Arrays.stream(storageClassesParam.split(",")) + .map(String::trim) + .filter(sc -> !sc.isEmpty()) + .toArray(String[]::new); + + return new StorageClassSet(storageClasses); + } + private void requestRenditions(Set renditionNames, Node fileNode) { if (renditionNames != null) diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/ContentInfo.java b/remote-api/src/main/java/org/alfresco/rest/api/model/ContentInfo.java index 7591ef4459..ade080d08d 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/model/ContentInfo.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/ContentInfo.java @@ -27,6 +27,8 @@ package org.alfresco.rest.api.model; import java.util.Set; +import org.alfresco.repo.content.StorageClassSet; + /** * Representation of content info * @@ -39,7 +41,7 @@ public class ContentInfo private String mimeTypeName; private Long sizeInBytes; private String encoding; - private Set storageClasses; + private StorageClassSet storageClassSet; public ContentInfo() { @@ -53,13 +55,13 @@ public class ContentInfo this.encoding = encoding; } - public ContentInfo(String mimeType, String mimeTypeName, Long sizeInBytes, String encoding, Set storageClasses) + public ContentInfo(String mimeType, String mimeTypeName, Long sizeInBytes, String encoding, StorageClassSet storageClassSet) { this.mimeType = mimeType; this.mimeTypeName = mimeTypeName; this.sizeInBytes = sizeInBytes; this.encoding = encoding; - this.storageClasses = storageClasses; + this.storageClassSet = storageClassSet; } public String getMimeType() { @@ -82,20 +84,21 @@ public class ContentInfo return encoding; } - public Set getStorageClasses() + public StorageClassSet getStorageClasses() { - return storageClasses; + return storageClassSet; } - public void setStorageClasses(Set storageClasses) + public void setStorageClasses(StorageClassSet storageClassSet) { - this.storageClasses = storageClasses; + this.storageClassSet = storageClassSet; } @Override public String toString() { return "ContentInfo [mimeType=" + mimeType + ", mimeTypeName=" + mimeTypeName - + ", encoding=" + encoding + ", sizeInBytes=" + sizeInBytes + ", storageClasses=" + storageClasses + "]"; + + ", encoding=" + encoding + ", sizeInBytes=" + sizeInBytes + ", storageClasses=" + storageClassSet + + "]"; } } diff --git a/remote-api/src/main/java/org/alfresco/rest/framework/resource/content/ContentInfo.java b/remote-api/src/main/java/org/alfresco/rest/framework/resource/content/ContentInfo.java index bc28a89f1e..25e921fa15 100755 --- a/remote-api/src/main/java/org/alfresco/rest/framework/resource/content/ContentInfo.java +++ b/remote-api/src/main/java/org/alfresco/rest/framework/resource/content/ContentInfo.java @@ -28,11 +28,13 @@ package org.alfresco.rest.framework.resource.content; import java.util.Locale; import java.util.Set; +import org.alfresco.repo.content.StorageClassSet; + /** * Basic information about content. Typically used with HTTPServletResponse */ public interface ContentInfo extends BasicContentInfo{ public long getLength(); public Locale getLocale(); - public Set getStorageClasses(); + public StorageClassSet getStorageClasses(); } diff --git a/remote-api/src/main/java/org/alfresco/rest/framework/resource/content/ContentInfoImpl.java b/remote-api/src/main/java/org/alfresco/rest/framework/resource/content/ContentInfoImpl.java index 6df1064197..ec6c4cc604 100755 --- a/remote-api/src/main/java/org/alfresco/rest/framework/resource/content/ContentInfoImpl.java +++ b/remote-api/src/main/java/org/alfresco/rest/framework/resource/content/ContentInfoImpl.java @@ -26,7 +26,8 @@ package org.alfresco.rest.framework.resource.content; import java.util.Locale; -import java.util.Set; + +import org.alfresco.repo.content.StorageClassSet; /** * Basic implementation of information about the returned content. @@ -37,21 +38,21 @@ public class ContentInfoImpl implements ContentInfo private final String encoding; private final long length; private final Locale locale; - private final Set storageClasses; + private final StorageClassSet storageClassSet; public ContentInfoImpl(String mimeType, String encoding, long length, Locale locale) { this(mimeType, encoding, length, locale, null); } - public ContentInfoImpl(String mimeType, String encoding, long length, Locale locale, Set storageClasses) + public ContentInfoImpl(String mimeType, String encoding, long length, Locale locale, StorageClassSet storageClassSet) { super(); this.mimeType = mimeType; this.encoding = encoding; this.length = length; this.locale = locale; - this.storageClasses = storageClasses; + this.storageClassSet = storageClassSet; } @Override @@ -76,8 +77,8 @@ public class ContentInfoImpl implements ContentInfo } @Override - public Set getStorageClasses() + public StorageClassSet getStorageClasses() { - return this.storageClasses; + return this.storageClassSet; } } 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 0a15799d18..94762872c0 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 @@ -2800,6 +2800,80 @@ public class NodeApiTest extends AbstractSingleNetworkSiteTest post(postUrl, toJsonAsStringNonNull(d1), "?"+Nodes.PARAM_AUTO_RENAME+"=false", 409); } + @Test + public void testUploadFileWithStorageClasses() throws Exception + { + setRequestContext(networkOne.getId(), user1, null); + + String title = "test title"; + Map docProps = new HashMap<>(); + docProps.put("cm:title", title); + docProps.put("cm:owner", user1); + docProps.put("storageClasses", "unsupported-storage-classes"); + docProps.put("include", "storageClasses"); + String contentName = "content " + RUNID + ".txt"; + + // Upload text with unsupported storage classes + createTextFile(Nodes.PATH_MY, contentName, "The quick brown fox jumps over the lazy dog.", "UTF-8", docProps, 400); + + // Upload text content with "default" storage classes + docProps.put("storageClasses", "default"); + Document document = createTextFile(Nodes.PATH_MY, contentName, "The quick brown fox jumps over the lazy dog.", "UTF-8", docProps); + + assertTrue(Set.of("default").containsAll(document.getContent().getStorageClasses())); + + // Upload new version with "default" storage classes + docProps.put("overwrite", "true"); + createTextFile(Nodes.PATH_MY, contentName, "New content - The quick brown fox jumps over the lazy dog.", "UTF-8", docProps); + + HttpResponse response = getAll(getNodeChildrenUrl(Nodes.PATH_MY), getPaging(0, 100), Map.of("include", "storageClasses"), 200); + List children = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Node.class); + + assertEquals(1, children.size()); + assertTrue(Set.of("default").containsAll(children.get(0).getContent().getStorageClasses())); + } + + @Test + public void testGetChildrenWithNoStorageClasses() throws Exception + { + setRequestContext(networkOne.getId(), user1, null); + + // Create folder + createFolder(Nodes.PATH_MY, "testFolder"); + + Map params = new HashMap<>(); + params.put("storageClasses", "default"); + params.put("include", "storageClasses"); + + // Create empty file + Document emptyTextFile = createEmptyTextFile(Nodes.PATH_MY, "empty-file.txt", params, 201); + + assertNotNull(emptyTextFile.getContent()); + assertNull( + emptyTextFile.getContent().getStorageClasses()); // no storage classes for empty files + + // Create file with content - default storage classes + Document fileWithContent = createTextFile(Nodes.PATH_MY, "file-with-content.txt", + "The quick brown fox jumps over the lazy dog.", + "UTF-8", params); + + assertNotNull(fileWithContent.getContent()); + assertTrue(Set.of("default").containsAll(fileWithContent.getContent().getStorageClasses())); + + HttpResponse response = getAll(getNodeChildrenUrl(Nodes.PATH_MY), getPaging(0, 100), + Map.of("include", "storageClasses"), 200); + List children = RestApiUtil + .parseRestApiEntries(response.getJsonResponse(), Node.class); + + assertEquals(3, children.size()); + long childrenWithStorageClasses = children + .stream() + .filter(child -> child.getContent() != null && + child.getContent().getStorageClasses() != null) + .count(); + assertEquals(1, childrenWithStorageClasses); + } + @Test public void testUpdateNodeConcurrentlyUsingInMemoryBacked() throws Exception { @@ -4784,16 +4858,15 @@ public class NodeApiTest extends AbstractSingleNetworkSiteTest { setRequestContext(user1); - // Create folder with an empty document - String postUrl = createFolder(); - String docId = createDocument(postUrl); + Document document = createTextFile(Nodes.PATH_MY, "file.txt", + "The quick brown fox jumps over the lazy dog."); Map params = new HashMap<>(); params.put("include", "storageClasses"); // Update node Document dUpdate = new Document(); - HttpResponse response = put(URL_NODES, docId, toJsonAsStringNonNull(dUpdate), null, 200); + HttpResponse response = put(URL_NODES, document.getId(), toJsonAsStringNonNull(dUpdate), null, 200); Document documentResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Document.class); // Check if storageClasses are retrieved if 'include=storageClasses' is not sent in the request diff --git a/remote-api/src/test/java/org/alfresco/rest/api/tests/client/data/ContentInfo.java b/remote-api/src/test/java/org/alfresco/rest/api/tests/client/data/ContentInfo.java index cfc7d090c4..9b52102385 100644 --- a/remote-api/src/test/java/org/alfresco/rest/api/tests/client/data/ContentInfo.java +++ b/remote-api/src/test/java/org/alfresco/rest/api/tests/client/data/ContentInfo.java @@ -30,6 +30,8 @@ import static org.junit.Assert.assertTrue; import java.util.Set; +import org.alfresco.repo.content.StorageClassSet; + /** * Representation of content info (initially for client tests for File Folder API) * @@ -42,7 +44,7 @@ public class ContentInfo private String mimeTypeName; private Long sizeInBytes; private String encoding; - private Set storageClasses; + private StorageClassSet storageClassSet; public ContentInfo() { @@ -80,14 +82,14 @@ public class ContentInfo this.encoding = encoding; } - public Set getStorageClasses() + public StorageClassSet getStorageClasses() { - return storageClasses; + return storageClassSet; } - public void setStorageClasses(Set storageClasses) + public void setStorageClasses(StorageClassSet storageClassSet) { - this.storageClasses = storageClasses; + this.storageClassSet = storageClassSet; } public void expected(Object o) @@ -100,7 +102,7 @@ public class ContentInfo AssertUtil.assertEquals("mimeTypeName", mimeTypeName, other.getMimeTypeName()); AssertUtil.assertEquals("sizeInBytes", sizeInBytes, other.getSizeInBytes()); AssertUtil.assertEquals("encoding", encoding, other.getEncoding()); - AssertUtil.assertEquals("storageClasses", storageClasses, other.storageClasses); + AssertUtil.assertEquals("storageClasses", storageClassSet, other.storageClassSet); } @Override @@ -111,7 +113,7 @@ public class ContentInfo .append(", mimeTypeName=").append(mimeTypeName) .append(", sizeInBytes=").append(sizeInBytes) .append(", encoding=").append(encoding) - .append(", storageClasses=").append(storageClasses) + .append(", storageClasses=").append(storageClassSet) .append(']'); return sb.toString(); } diff --git a/repository/src/main/java/org/alfresco/repo/content/ContentServiceImpl.java b/repository/src/main/java/org/alfresco/repo/content/ContentServiceImpl.java index fea984f609..2ce6f1e241 100644 --- a/repository/src/main/java/org/alfresco/repo/content/ContentServiceImpl.java +++ b/repository/src/main/java/org/alfresco/repo/content/ContentServiceImpl.java @@ -449,9 +449,21 @@ public class ContentServiceImpl implements ContentService, ApplicationContextAwa public ContentWriter getWriter(NodeRef nodeRef, QName propertyQName, boolean update) { + return getWriter(nodeRef,propertyQName, update, null); + } + + public ContentWriter getWriter(NodeRef nodeRef, QName propertyQName, boolean update, + StorageClassSet storageClassSet) + { + if (!isStorageClassesSupported(storageClassSet)) + { + throw new UnsupportedStorageClassException(store, storageClassSet, + "The supplied storage classes are not supported"); + } + if (nodeRef == null) { - ContentContext ctx = new ContentContext(null, null); + ContentContext ctx = new ContentContext(null, null, storageClassSet); // for this case, we just give back a valid URL into the content store ContentWriter writer = store.getWriter(ctx); // Register the new URL for rollback cleanup @@ -462,10 +474,38 @@ public class ContentServiceImpl implements ContentService, ApplicationContextAwa // check for an existing URL - the get of the reader will perform type checking ContentReader existingContentReader = getReader(nodeRef, propertyQName, false); + + if (storageClassSet != null) + { + if (existingContentReader != null && + existingContentReader.getContentData() != null && + existingContentReader.getContentData().getContentUrl() != null) + { + Set currentStorageClasses = findStorageClasses(nodeRef); + if (currentStorageClasses != null && + !currentStorageClasses.equals(storageClassSet)) + { + Set possibleTransitions = findStorageClassesTransitions(nodeRef) + .get(currentStorageClasses); + + if (possibleTransitions == null || + !possibleTransitions.contains(storageClassSet)) + { + throw new UnsupportedStorageClassException(store, storageClassSet, + "Transition from " + + currentStorageClasses + + " storage classes to " + + storageClassSet + + " is not supported"); + } + } + } + } // get the content using the (potentially) existing content - the new content // can be wherever the store decides. - ContentContext ctx = new NodeContentContext(existingContentReader, null, nodeRef, propertyQName); + ContentContext ctx = new NodeContentContext(existingContentReader, null, nodeRef, + propertyQName, storageClassSet); ContentWriter writer = store.getWriter(ctx); // Register the new URL for rollback cleanup eagerContentStoreCleaner.registerNewContentUrl(writer.getContentUrl()); diff --git a/repository/src/main/java/org/alfresco/repo/content/NodeContentContext.java b/repository/src/main/java/org/alfresco/repo/content/NodeContentContext.java index 307ffa1736..9b03d0492c 100644 --- a/repository/src/main/java/org/alfresco/repo/content/NodeContentContext.java +++ b/repository/src/main/java/org/alfresco/repo/content/NodeContentContext.java @@ -1,30 +1,32 @@ -/* - * #%L - * Alfresco Repository - * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - * #L% - */ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ package org.alfresco.repo.content; +import java.util.Set; + import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.namespace.QName; @@ -63,6 +65,29 @@ public class NodeContentContext extends ContentContext this.propertyQName = propertyQName; } + /** + * Construct the instance with the content URL. + * + * @param existingContentReader content with which to seed the new writer - may be null + * @param contentUrl the content URL - may be null + * @param nodeRef the node holding the content metadata - may not be null + * @param propertyQName the property holding the content metadata - may not be null + * @param storageClasses the storage classes specific to the provided content URL - may be null + */ + public NodeContentContext( + ContentReader existingContentReader, + String contentUrl, + NodeRef nodeRef, + QName propertyQName, + Set storageClasses) + { + super(existingContentReader, contentUrl, storageClasses); + ParameterCheck.mandatory("nodeRef", nodeRef); + ParameterCheck.mandatory("propertyQName", propertyQName); + this.nodeRef = nodeRef; + this.propertyQName = propertyQName; + } + @Override public String toString() { diff --git a/repository/src/main/java/org/alfresco/service/cmr/repository/ContentService.java b/repository/src/main/java/org/alfresco/service/cmr/repository/ContentService.java index 8caf9cab9a..a2edb0249f 100644 --- a/repository/src/main/java/org/alfresco/service/cmr/repository/ContentService.java +++ b/repository/src/main/java/org/alfresco/service/cmr/repository/ContentService.java @@ -151,6 +151,40 @@ public interface ContentService public ContentWriter getWriter(NodeRef nodeRef, QName propertyQName, boolean update) throws InvalidNodeRefException, InvalidTypeException; + /** + * Get a content writer for the given node property, choosing to optionally have + * the node property updated automatically when the content stream closes. + *

+ * If the update flag is off, then the state of the node property will remain unchanged + * regardless of the state of the written binary data. If the flag is on, then the node + * property will be updated on the same thread as the code that closed the write + * channel. + *

+ * If no node is supplied, then the writer will provide a stream into the backing content + * store, but will not be associated with any new or previous content. + *

+ * NOTE: The content URL provided will be registered for automatic cleanup in the event + * that the transaction, in which this method was called, rolls back. If the transaction + * is successful, the writer may still be open and available for use but the underlying binary + * will not be cleaned up subsequently. The recommended pattern is to group calls to retrieve + * the writer in the same transaction as the calls to subsequently update and close the + * write stream - including setting of the related content properties. + * + * @param nodeRef a reference to a node having a content property, or null + * to just get a valid writer into a backing content store. + * @param propertyQName the name of the property, which must be of type content + * @param update true if the property must be updated atomically when the content write + * stream is closed (attaches a listener to the stream); false if the client code + * will perform the updates itself. + * @param storageClassSet storage classes for the content associated with the node property + * @return Returns a writer for the content associated with the node property + * @throws InvalidNodeRefException if the node doesn't exist + * @throws InvalidTypeException if the node property is not of type content + */ + @Auditable(parameters = {"nodeRef", "propertyQName", "update", "storageClasses"}) + public ContentWriter getWriter(NodeRef nodeRef, QName propertyQName, boolean update, + StorageClassSet storageClassSet) throws InvalidNodeRefException, InvalidTypeException; + /** * Gets a writer to a temporary location. The longevity of the stored * temporary content is determined by the system.