diff --git a/config/alfresco/messages/rest-framework-messages.properties b/config/alfresco/messages/rest-framework-messages.properties
index b0de361dd4..024740621f 100644
--- a/config/alfresco/messages/rest-framework-messages.properties
+++ b/config/alfresco/messages/rest-framework-messages.properties
@@ -11,4 +11,5 @@ framework.exception.PermissionDenied=Permission was denied
framework.exception.StaleEntity=Attempt to update a stale entity
framework.exception.UnsupportedResourceOperation=The operation is unsupported
framework.exception.DeletedResource=In this version of the API resource {0} has been deleted
+framework.exception.RequestEntityTooLarge=Request entity too large
diff --git a/config/alfresco/public-rest-context.xml b/config/alfresco/public-rest-context.xml
index 8f53dd0e32..a2d7488a76 100644
--- a/config/alfresco/public-rest-context.xml
+++ b/config/alfresco/public-rest-context.xml
@@ -137,7 +137,8 @@
-
+
+
diff --git a/source/java/org/alfresco/rest/api/Nodes.java b/source/java/org/alfresco/rest/api/Nodes.java
index 3627e0f3ff..7a25104c3b 100644
--- a/source/java/org/alfresco/rest/api/Nodes.java
+++ b/source/java/org/alfresco/rest/api/Nodes.java
@@ -38,6 +38,7 @@ import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.namespace.QName;
+import org.springframework.extensions.webscripts.servlet.FormData;
/**
* @author steveglover
@@ -45,11 +46,11 @@ import org.alfresco.service.namespace.QName;
*/
public interface Nodes
{
- NodeRef validateNode(StoreRef storeRef, String nodeId);
- NodeRef validateNode(String nodeId);
- NodeRef validateNode(NodeRef nodeRef);
- boolean nodeMatches(NodeRef nodeRef, Set expectedTypes, Set excludedTypes);
-
+ NodeRef validateNode(StoreRef storeRef, String nodeId);
+ NodeRef validateNode(String nodeId);
+ NodeRef validateNode(NodeRef nodeRef);
+ boolean nodeMatches(NodeRef nodeRef, Set expectedTypes, Set excludedTypes);
+
/**
* Get the node representation for the given node.
* @param nodeId String
@@ -122,4 +123,14 @@ public interface Nodes
// TODO update REST fwk - to optionally support return of json
void updateContent(String fileNodeId, BasicContentInfo contentInfo, InputStream stream, Parameters parameters);
+
+ /**
+ * Uploads file content and meta-data into the repository.
+ *
+ * @param parentFolderNodeId String id of parent folder node or well-known alias, eg. "-root-" or "-my-"
+ * @param formData the {@link FormData}
+ * @param parameters the {@link Parameters} object to get the parameters passed into the request
+ * @return {@code Node} if successful
+ */
+ Node upload(String parentFolderNodeId, FormData formData, Parameters parameters);
}
diff --git a/source/java/org/alfresco/rest/api/impl/NodesImpl.java b/source/java/org/alfresco/rest/api/impl/NodesImpl.java
index e484533b79..9b92d51d3e 100644
--- a/source/java/org/alfresco/rest/api/impl/NodesImpl.java
+++ b/source/java/org/alfresco/rest/api/impl/NodesImpl.java
@@ -28,8 +28,10 @@ package org.alfresco.rest.api.impl;
import org.alfresco.model.ContentModel;
import org.alfresco.query.PagingRequest;
import org.alfresco.query.PagingResults;
+import org.alfresco.repo.content.ContentLimitViolationException;
import org.alfresco.repo.model.Repository;
import org.alfresco.repo.node.getchildren.GetChildrenCannedQuery;
+import org.alfresco.repo.security.permissions.AccessDeniedException;
import org.alfresco.rest.antlr.WhereClauseParser;
import org.alfresco.rest.api.Nodes;
import org.alfresco.rest.api.model.Document;
@@ -38,8 +40,12 @@ import org.alfresco.rest.api.model.Node;
import org.alfresco.rest.api.model.PathInfo;
import org.alfresco.rest.api.model.PathInfo.ElementInfo;
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.EntityNotFoundException;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
+import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException;
+import org.alfresco.rest.framework.core.exceptions.RequestEntityTooLargeException;
import org.alfresco.rest.framework.resource.content.BasicContentInfo;
import org.alfresco.rest.framework.resource.content.BinaryResource;
import org.alfresco.rest.framework.resource.content.NodeBinaryResource;
@@ -51,14 +57,19 @@ import org.alfresco.rest.framework.resource.parameters.where.Query;
import org.alfresco.rest.framework.resource.parameters.where.QueryHelper;
import org.alfresco.rest.workflow.api.impl.MapBasedQueryWalker;
import org.alfresco.service.ServiceRegistry;
+import org.alfresco.service.cmr.action.Action;
+import org.alfresco.service.cmr.action.ActionDefinition;
+import org.alfresco.service.cmr.action.ActionService;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.model.FileFolderService;
import org.alfresco.service.cmr.model.FileInfo;
import org.alfresco.service.cmr.model.FileNotFoundException;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentData;
+import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
+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.Path;
@@ -66,13 +77,23 @@ import org.alfresco.service.cmr.repository.Path.Element;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.security.AccessStatus;
import org.alfresco.service.cmr.security.PermissionService;
+import org.alfresco.service.cmr.usage.ContentQuotaException;
+import org.alfresco.service.cmr.version.VersionService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.Pair;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.extensions.surf.util.Content;
+import org.springframework.extensions.webscripts.servlet.FormData;
+import java.io.BufferedInputStream;
+import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.math.BigInteger;
+import java.nio.charset.Charset;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Arrays;
@@ -95,6 +116,8 @@ import java.util.Set;
*/
public class NodesImpl implements Nodes
{
+ private static final Log logger = LogFactory.getLog(NodesImpl.class);
+
private static enum Type
{
// Note: ordered
@@ -114,6 +137,10 @@ public class NodesImpl implements Nodes
private FileFolderService fileFolderService;
private NamespaceService namespaceService;
private PermissionService permissionService;
+ private MimetypeService mimetypeService;
+ private ContentService contentService;
+ private ActionService actionService;
+ private VersionService versionService;
private Repository repositoryHelper;
private ServiceRegistry sr;
private Set defaultIgnoreTypes;
@@ -121,6 +148,16 @@ public class NodesImpl implements Nodes
public void init()
{
+ this.namespaceService = sr.getNamespaceService();
+ this.fileFolderService = sr.getFileFolderService();
+ this.nodeService = sr.getNodeService();
+ this.permissionService = sr.getPermissionService();
+ this.dictionaryService = sr.getDictionaryService();
+ this.mimetypeService = sr.getMimetypeService();
+ this.contentService = sr.getContentService();
+ this.actionService = sr.getActionService();
+ this.versionService = sr.getVersionService();
+
if (defaultIgnoreTypes != null)
{
ignoreTypeQNames = new HashSet<>(defaultIgnoreTypes.size());
@@ -133,12 +170,6 @@ public class NodesImpl implements Nodes
public void setServiceRegistry(ServiceRegistry sr) {
this.sr = sr;
-
- this.namespaceService = sr.getNamespaceService();
- this.fileFolderService = sr.getFileFolderService();
- this.nodeService = sr.getNodeService();
- this.permissionService = sr.getPermissionService();
- this.dictionaryService = sr.getDictionaryService();
}
public void setRepositoryHelper(Repository repositoryHelper)
@@ -792,15 +823,15 @@ public class NodesImpl implements Nodes
}
// TODO should we able to specify content properties (eg. mimeType ... or use extension for now, or encoding)
- public Node createNode(String parentFolderNodeId, Node nodeInfo, Parameters parameters)
- {
+ public Node createNode(String parentFolderNodeId, Node nodeInfo, Parameters parameters)
+ {
// check that requested parent node exists and it's type is a (sub-)type of folder
final NodeRef parentNodeRef = validateOrLookupNode(parentFolderNodeId, null);
- if (! nodeMatches(parentNodeRef, Collections.singleton(ContentModel.TYPE_FOLDER), null))
- {
- throw new InvalidArgumentException("NodeId of folder is expected: "+parentNodeRef);
- }
+ if (! nodeMatches(parentNodeRef, Collections.singleton(ContentModel.TYPE_FOLDER), null))
+ {
+ throw new InvalidArgumentException("NodeId of folder is expected: "+parentNodeRef);
+ }
String nodeName = nodeInfo.getName();
if ((nodeName == null) || nodeName.isEmpty())
@@ -849,7 +880,7 @@ public class NodesImpl implements Nodes
}
return getFolderOrDocument(nodeRef.getId(), parameters);
- }
+ }
public Node updateNode(String nodeId, Node nodeInfo, Parameters parameters)
{
@@ -933,6 +964,259 @@ public class NodesImpl implements Nodes
return;
}
+ @Override
+ public Node upload(String parentFolderNodeId, FormData formData, Parameters parameters)
+ {
+ if (formData == null || !formData.getIsMultiPart())
+ {
+ throw new InvalidArgumentException("The request content-type is not multipart");
+ }
+
+ final NodeRef parentNodeRef = validateOrLookupNode(parentFolderNodeId, null);
+ if (Type.DOCUMENT == getType(parentNodeRef))
+ {
+ throw new InvalidArgumentException(parentFolderNodeId + " is not a folder.");
+ }
+
+ String fileName = null;
+ Content content = null;
+ boolean overwrite = false; // If a fileName clashes for a versionable file
+
+ for (FormData.FormField field : formData.getFields())
+ {
+ switch (field.getName().toLowerCase())
+ {
+ case "filename":
+ fileName = getStringOrNull(field.getValue());
+ break;
+
+ case "filedata":
+ if (field.getIsFile())
+ {
+ fileName = fileName != null ? fileName : field.getFilename();
+ content = field.getContent();
+ }
+ break;
+
+ case "overwrite":
+ overwrite = Boolean.valueOf(field.getValue());
+ break;
+ }
+ }
+
+ try
+ {
+ // MNT-7213 When alf_data runs out of disk space, Share uploads
+ // result in a success message, but the files do not appear.
+ if (formData.getFields().length == 0)
+ {
+ throw new ConstraintViolatedException(" No disk space available");
+ }
+
+ // Ensure mandatory file attributes have been located. Need either
+ // destination, or site + container or updateNodeRef
+ if ((fileName == null || content == null))
+ {
+ throw new InvalidArgumentException("Required parameters are missing");
+ }
+ /*
+ * Existing file handling
+ */
+ NodeRef existingFile = nodeService.getChildByName(parentNodeRef, ContentModel.ASSOC_CONTAINS, fileName);
+ if (existingFile != null)
+ {
+ // File already exists, decide what to do
+ if (overwrite && nodeService.hasAspect(existingFile, ContentModel.ASPECT_VERSIONABLE))
+ {
+ // Upload component was configured to overwrite files if name clashes
+ write(existingFile, content, fileName, false, true);
+
+ // Extract the metadata (The overwrite policy controls
+ // which if any parts of the document's properties are updated from this)
+ extractMetadata(existingFile);
+
+ // 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.
+ return createUploadResponse(parentNodeRef, existingFile);
+ }
+ else
+ {
+ throw new ConstraintViolatedException(fileName + " already exists.");
+ }
+ }
+
+ // Create a new file.
+ return createNewFile(parentNodeRef, fileName, content);
+
+ // 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.
+ }
+ catch (ApiException apiEx)
+ {
+ // As this is an public API fwk exception, there is no need to convert it, so just throw it.
+ throw apiEx;
+ }
+ catch (AccessDeniedException ade)
+ {
+ throw new PermissionDeniedException();
+ }
+ catch (ContentQuotaException cqe)
+ {
+ throw new RequestEntityTooLargeException();
+ }
+ catch (ContentLimitViolationException clv)
+ {
+ throw new ConstraintViolatedException();
+ }
+ catch (Exception ex)
+ {
+ /*
+ * NOTE: Do not clean formData temp files to allow for retries. It's
+ * possible for a temp file to remain if max retry attempts are
+ * made, but this is rare, so leave to usual temp file cleanup.
+ */
+
+ throw new ApiException("Unexpected error occurred during upload of new content.", ex);
+ }
+ }
+
+ /**
+ * Helper to create a new node and writes its content to the repository.
+ */
+ private Node createNewFile(NodeRef parentNodeRef, String fileName, Content content)
+ {
+ FileInfo fileInfo = fileFolderService.create(parentNodeRef, fileName, ContentModel.TYPE_CONTENT);
+ NodeRef newFile = fileInfo.getNodeRef();
+
+ // Write content
+ write(newFile, content, fileName, false, true);
+
+ // Ensure the file is versionable (autoVersion = true, autoVersionProps = false)
+ ensureVersioningEnabled(newFile, true, false);
+
+ // Extract the metadata
+ extractMetadata(newFile);
+
+ // Create the response
+ return createUploadResponse(parentNodeRef, newFile);
+ }
+
+ private Node createUploadResponse(NodeRef parentNodeRef, NodeRef newFileNodeRef)
+ {
+ return getFolderOrDocument(newFileNodeRef, parentNodeRef, ContentModel.TYPE_CONTENT, Collections.emptyList(), true, null);
+ }
+
+ private String getStringOrNull(String value)
+ {
+ if (StringUtils.isNotEmpty(value))
+ {
+ return value.equalsIgnoreCase("null") ? null : value;
+ }
+ return null;
+ }
+
+ /**
+ * Writes the content to the repository.
+ *
+ * @param nodeRef the reference to the node having a content property
+ * @param content the content
+ * @param fileName the uploaded file name
+ * @param applyMimeType If true, apply the mimeType from the Content object,
+ * else leave the original mimeType
+ * @param guessEncoding If true, guess the encoding from the underlying
+ * input stream, else use encoding set in the Content object as supplied
+ */
+ protected void write(NodeRef nodeRef, Content content, String fileName, boolean applyMimeType, boolean guessEncoding)
+ {
+ ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true);
+ InputStream is;
+ String mimeType = content.getMimetype();
+ if (!applyMimeType)
+ {
+ ContentData existingContentData = (ContentData) nodeService.getProperty(nodeRef, ContentModel.PROP_CONTENT);
+ if (existingContentData != null)
+ {
+ mimeType = existingContentData.getMimetype();
+ }
+ else
+ {
+ mimeType = mimetypeService.guessMimetype(fileName);
+ }
+ }
+ if (guessEncoding)
+ {
+ is = new BufferedInputStream(content.getInputStream());
+ is.mark(1024);
+
+ writer.setEncoding(guessEncoding(is, mimeType));
+ try
+ {
+ is.reset();
+ }
+ catch (IOException e)
+ {
+ logger.error("Failed to reset input stream", e);
+ }
+ }
+ else
+ {
+ writer.setEncoding(content.getEncoding());
+ is = content.getInputStream();
+ }
+ writer.setMimetype(mimeType);
+ writer.putContent(is);
+ }
+
+ /**
+ * Ensures the given node has the {@code cm:versionable} aspect applied to it, and
+ * that it has the initial version in the version store.
+ *
+ * @param nodeRef the reference to the node to be checked
+ * @param autoVersion If the {@code cm:versionable} aspect is applied, should auto
+ * versioning be requested?
+ * @param autoVersionProps If the {@code cm:versionable} aspect is applied, should
+ * auto versioning of properties be requested?
+ */
+ protected void ensureVersioningEnabled(NodeRef nodeRef, boolean autoVersion, boolean autoVersionProps)
+ {
+ Map props = new HashMap<>(2);
+ props.put(ContentModel.PROP_AUTO_VERSION, autoVersion);
+ props.put(ContentModel.PROP_AUTO_VERSION_PROPS, autoVersionProps);
+
+ versionService.ensureVersioningEnabled(nodeRef, props);
+ }
+
+ /**
+ * Guesses the character encoding of the given inputStream.
+ */
+ protected String guessEncoding(InputStream in, String mimeType)
+ {
+ String encoding = "UTF-8";
+
+ if (in != null)
+ {
+ Charset charset = mimetypeService.getContentCharsetFinder().getCharset(in, mimeType);
+ encoding = charset.name();
+ }
+
+ return encoding;
+ }
+
+ /**
+ * Extracts the given node metadata asynchronously.
+ */
+ private void extractMetadata(NodeRef nodeRef)
+ {
+ final String actionName = "extract-metadata";
+ ActionDefinition actionDef = actionService.getActionDefinition(actionName);
+ if (actionDef != null)
+ {
+ Action action = actionService.createAction(actionName);
+ actionService.executeAction(action, nodeRef);
+ }
+ }
+
/**
* Helper to create a QName from either a fully qualified or short-name QName string
*
diff --git a/source/java/org/alfresco/rest/api/nodes/NodeChildrenRelation.java b/source/java/org/alfresco/rest/api/nodes/NodeChildrenRelation.java
index 723f7104e2..07e2a9834d 100644
--- a/source/java/org/alfresco/rest/api/nodes/NodeChildrenRelation.java
+++ b/source/java/org/alfresco/rest/api/nodes/NodeChildrenRelation.java
@@ -19,15 +19,17 @@
package org.alfresco.rest.api.nodes;
import org.alfresco.rest.api.Nodes;
-import org.alfresco.rest.api.model.Folder;
import org.alfresco.rest.api.model.Node;
import org.alfresco.rest.framework.WebApiDescription;
+import org.alfresco.rest.framework.WebApiParam;
import org.alfresco.rest.framework.resource.RelationshipResource;
+import org.alfresco.rest.framework.resource.actions.interfaces.MultiPartRelationshipResourceAction;
import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.util.ParameterCheck;
import org.springframework.beans.factory.InitializingBean;
+import org.springframework.extensions.webscripts.servlet.FormData;
import java.util.ArrayList;
import java.util.List;
@@ -36,18 +38,20 @@ import java.util.List;
* TODO ... work-in-progress
*
* @author janv
+ * @author Jamal Kaabi-Mofrad
*/
@RelationshipResource(name = "children", entityResource = NodesEntityResource.class, title = "Folder children")
-public class NodeChildrenRelation implements RelationshipResourceAction.Read, RelationshipResourceAction.Create, InitializingBean
+public class NodeChildrenRelation implements RelationshipResourceAction.Read, RelationshipResourceAction.Create,
+ MultiPartRelationshipResourceAction.Create, InitializingBean
{
- private Nodes nodes;
+ private Nodes nodes;
public void setNodes(Nodes nodes)
{
this.nodes = nodes;
}
- @Override
+ @Override
public void afterPropertiesSet()
{
ParameterCheck.mandatory("nodes", this.nodes);
@@ -104,4 +108,13 @@ public class NodeChildrenRelation implements RelationshipResourceAction.Read.
+ */
+
+package org.alfresco.rest.framework.core.exceptions;
+
+/**
+ * @author Jamal Kaabi-Mofrad
+ */
+public class RequestEntityTooLargeException extends ApiException
+{
+ private static final long serialVersionUID = 3196212354672333823L;
+
+ public static String DEFAULT_MESSAGE_ID = "framework.exception.RequestEntityTooLarge";
+
+ public RequestEntityTooLargeException()
+ {
+ super(DEFAULT_MESSAGE_ID);
+ }
+
+ public RequestEntityTooLargeException(String msgId)
+ {
+ super(msgId);
+ }
+}
diff --git a/source/test-java/org/alfresco/rest/api/tests/NodeApiTest.java b/source/test-java/org/alfresco/rest/api/tests/NodeApiTest.java
index 6d16885058..1c1efd7249 100644
--- a/source/test-java/org/alfresco/rest/api/tests/NodeApiTest.java
+++ b/source/test-java/org/alfresco/rest/api/tests/NodeApiTest.java
@@ -19,18 +19,22 @@
package org.alfresco.rest.api.tests;
+import static org.alfresco.rest.api.tests.util.RestApiUtil.parsePaging;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import org.alfresco.model.ContentModel;
import org.alfresco.model.ForumModel;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.model.Repository;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
+import org.alfresco.rest.api.model.ContentInfo;
import org.alfresco.rest.api.model.Document;
+import org.alfresco.rest.api.model.Folder;
import org.alfresco.rest.api.model.Node;
import org.alfresco.rest.api.model.PathInfo;
import org.alfresco.rest.api.model.PathInfo.ElementInfo;
@@ -40,10 +44,14 @@ import org.alfresco.rest.api.tests.RepoService.TestNetwork;
import org.alfresco.rest.api.tests.RepoService.TestPerson;
import org.alfresco.rest.api.tests.RepoService.TestSite;
import org.alfresco.rest.api.tests.client.HttpResponse;
+import org.alfresco.rest.api.tests.client.PublicApiClient;
import org.alfresco.rest.api.tests.client.PublicApiClient.ExpectedPaging;
import org.alfresco.rest.api.tests.client.PublicApiClient.Paging;
import org.alfresco.rest.api.tests.client.data.SiteRole;
import org.alfresco.rest.api.tests.util.JacksonUtil;
+import org.alfresco.rest.api.tests.util.MultiPartBuilder;
+import org.alfresco.rest.api.tests.util.MultiPartBuilder.FileData;
+import org.alfresco.rest.api.tests.util.MultiPartBuilder.MultiPartRequest;
import org.alfresco.rest.api.tests.util.RestApiUtil;
import org.alfresco.rest.framework.jacksonextensions.JacksonHelper;
import org.alfresco.service.cmr.repository.NodeRef;
@@ -54,8 +62,12 @@ import org.alfresco.service.cmr.site.SiteVisibility;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
+import org.springframework.util.ResourceUtils;
+import java.io.File;
+import java.io.FileNotFoundException;
import java.io.Serializable;
+import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
@@ -67,14 +79,16 @@ import java.util.Set;
/**
* API tests for:
*
- * - {@literal host:port/alfresco/api/{networkId}/public/alfresco/versions/1/nodes/{nodeId}}
- * - {@literal host:port/alfresco/api/{networkId}/public/alfresco/versions/1/nodes/{nodeId}/children}
+ * - {@literal :/alfresco/api//public/alfresco/versions/1/nodes/}
+ * - {@literal :/alfresco/api//public/alfresco/versions/1/nodes//children}
*
*
* @author Jamal Kaabi-Mofrad
*/
public class NodeApiTest extends AbstractBaseApiTest
{
+ private static final String RESOURCE_PREFIX = "publicapi/upload/";
+
/**
* User one from network one
*/
@@ -145,6 +159,11 @@ public class NodeApiTest extends AbstractBaseApiTest
AuthenticationUtil.clearCurrentSecurityContext();
}
+ /**
+ * Tests get document library children.
+ * GET:
+ * {@literal :/alfresco/api//public/alfresco/versions/1/nodes//children}
+ */
@Test
public void testListDocLibChildren() throws Exception
{
@@ -246,6 +265,11 @@ public class NodeApiTest extends AbstractBaseApiTest
getAll(getChildrenUrl(docLibNodeRef), userTwoN1.getId(), paging, 403);
}
+ /**
+ * Tests get user's home children.
+ * GET:
+ * {@literal :/alfresco/api/-default-/public/alfresco/versions/1/nodes//children}
+ */
@Test
public void testListMyFilesChildren() throws Exception
{
@@ -317,6 +341,11 @@ public class NodeApiTest extends AbstractBaseApiTest
assertEquals("doclib:1444660852296", ((List>) entry.getValue()).get(0));
}
+ /**
+ * Tests get node with path information.
+ * GET:
+ * {@literal :/alfresco/api//public/alfresco/versions/1/nodes/?select=path}
+ */
@Test
public void testGetPathElements_DocLib() throws Exception
{
@@ -385,6 +414,11 @@ public class NodeApiTest extends AbstractBaseApiTest
assertEquals(folderC, pathElements.get(0).getName());
}
+ /**
+ * Tests get node with path information.
+ * GET:
+ * {@literal :/alfresco/api/-default-/public/alfresco/versions/1/nodes/?select=path}
+ */
@Test
public void testGetPathElements_MyFiles() throws Exception
{
@@ -434,6 +468,14 @@ public class NodeApiTest extends AbstractBaseApiTest
assertNotNull(pathElements.get(4).getId());
}
+ /**
+ * Tests well-known aliases.
+ * GET:
+ *
+ * - {@literal :/alfresco/api/-default-/public/alfresco/versions/1/nodes/-root-}
+ * - {@literal :/alfresco/api/-default-/public/alfresco/versions/1/nodes/-my-}
+ *
+ */
@Test
public void testGetNodeWithKnownAlias() throws Exception
{
@@ -472,9 +514,170 @@ public class NodeApiTest extends AbstractBaseApiTest
getSingle(NodesEntityResource.class, user1, userNodeAlias, null, 404); // Not found
}
- private String getChildrenUrl(NodeRef nodeRef)
+ /**
+ * Tests Multipart upload to user's home (a.k.a My Files).
+ * POST:
+ * {@literal :/alfresco/api/-default-/public/alfresco/versions/1/nodes//children}
+ */
+ @Test
+ public void testUploadToMyFiles() throws Exception
{
- return "nodes/" + nodeRef.getId() + "/children";
+ final String userNodeAlias = "-my-";
+ final String fileName = "quick.pdf";
+ final File file = getResourceFile(fileName);
+
+ Paging paging = getPaging(0, Integer.MAX_VALUE);
+ HttpResponse response = getAll(getChildrenUrl(userNodeAlias), user1, paging, 200);
+ PublicApiClient.ExpectedPaging pagingResult = parsePaging(response.getJsonResponse());
+ assertNotNull(paging);
+ final int numOfNodes = pagingResult.getCount().intValue();
+
+ MultiPartBuilder multiPartBuilder = MultiPartBuilder.create()
+ .setFileData(new FileData(fileName, file, MimetypeMap.MIMETYPE_PDF));
+ MultiPartRequest reqBody = multiPartBuilder.build();
+
+ // Try to upload
+ response = post(getChildrenUrl(userNodeAlias), user1, new String(reqBody.getBody()), null, reqBody.getContentType(), 201);
+ Document document = jacksonUtil.parseEntry(response.getJsonResponse(), Document.class);
+ // Check the upload response
+ assertEquals(fileName, document.getName());
+ ContentInfo contentInfo = document.getContent();
+ assertNotNull(contentInfo);
+ assertEquals(MimetypeMap.MIMETYPE_PDF, contentInfo.getMimeType());
+
+ // Retrieve the uploaded file
+ response = getSingle(NodesEntityResource.class, user1, document.getNodeRef().getId(), null, 200);
+ document = jacksonUtil.parseEntry(response.getJsonResponse(), Document.class);
+ assertEquals(fileName, document.getName());
+ contentInfo = document.getContent();
+ assertNotNull(contentInfo);
+ assertEquals(MimetypeMap.MIMETYPE_PDF, contentInfo.getMimeType());
+
+ // Check 'get children' is confirming the upload
+ response = getAll(getChildrenUrl(userNodeAlias), user1, paging, 200);
+ pagingResult = parsePaging(response.getJsonResponse());
+ assertNotNull(paging);
+ assertEquals(numOfNodes + 1, pagingResult.getCount().intValue());
+
+ // Upload the same file again to check the name conflicts handling
+ post(getChildrenUrl(userNodeAlias), user1, new String(reqBody.getBody()), null, reqBody.getContentType(), 409);
+
+ response = getAll(getChildrenUrl(userNodeAlias), user1, paging, 200);
+ pagingResult = parsePaging(response.getJsonResponse());
+ assertNotNull(paging);
+ assertEquals("Duplicate file name. The file shouldn't have been uploaded.", numOfNodes + 1, pagingResult.getCount().intValue());
+
+ // User2 tries to upload a new file into the user1's home folder.
+ response = getSingle(NodesEntityResource.class, user1, userNodeAlias, null, 200);
+ Folder user1Home = jacksonUtil.parseEntry(response.getJsonResponse(), Folder.class);
+ final String fileName2 = "quick-2.txt";
+ final File file2 = getResourceFile(fileName2);
+ reqBody = MultiPartBuilder.create()
+ .setFileData(new FileData(fileName2, file2, MimetypeMap.MIMETYPE_TEXT_PLAIN))
+ .build();
+ post(getChildrenUrl(user1Home.getNodeRef()), user2, new String(reqBody.getBody()), null, reqBody.getContentType(), 403);
+
+ response = getAll(getChildrenUrl(userNodeAlias), user1, paging, 200);
+ pagingResult = parsePaging(response.getJsonResponse());
+ assertNotNull(paging);
+ assertEquals("Access Denied. The file shouldn't have been uploaded.", numOfNodes + 1, pagingResult.getCount().intValue());
+
+ // User1 tries to upload a file into a document rather than a folder!
+ post(getChildrenUrl(document.getNodeRef()), user1, new String(reqBody.getBody()), null, reqBody.getContentType(), 400);
+
+ // Try to upload a file without defining the required formData
+ reqBody = MultiPartBuilder.create().build();
+ post(getChildrenUrl(userNodeAlias), user1, new String(reqBody.getBody()), null, reqBody.getContentType(), 400);
+ }
+
+ /**
+ * Tests Multipart upload to a Site.
+ * POST:
+ * {@literal :/alfresco/api//public/alfresco/versions/1/nodes//children}
+ */
+ @Test
+ public void testUploadToSite() throws Exception
+ {
+ final String fileName = "quick-1.txt";
+ final File file = getResourceFile(fileName);
+
+ AuthenticationUtil.setFullyAuthenticatedUser(userOneN1.getId());
+ String folderA = "folder" + System.currentTimeMillis() + "_A";
+ NodeRef folderA_Ref = repoService.addToDocumentLibrary(userOneN1Site, folderA, ContentModel.TYPE_FOLDER);
+
+ Paging paging = getPaging(0, Integer.MAX_VALUE);
+ HttpResponse response = getAll(getChildrenUrl(folderA_Ref), userOneN1.getId(), paging, 200);
+ PublicApiClient.ExpectedPaging pagingResult = parsePaging(response.getJsonResponse());
+ assertNotNull(paging);
+ final int numOfNodes = pagingResult.getCount().intValue();
+
+ MultiPartBuilder multiPartBuilder = MultiPartBuilder.create()
+ .setFileData(new FileData(fileName, file, MimetypeMap.MIMETYPE_TEXT_PLAIN));
+ MultiPartRequest reqBody = multiPartBuilder.build();
+ // Try to upload
+ response = post(getChildrenUrl(folderA_Ref), userOneN1.getId(), new String(reqBody.getBody()), null, reqBody.getContentType(), 201);
+ Document document = jacksonUtil.parseEntry(response.getJsonResponse(), Document.class);
+ // Check the upload response
+ assertEquals(fileName, document.getName());
+ ContentInfo contentInfo = document.getContent();
+ assertNotNull(contentInfo);
+ assertEquals(MimetypeMap.MIMETYPE_TEXT_PLAIN, contentInfo.getMimeType());
+
+ // Retrieve the uploaded file
+ response = getSingle(NodesEntityResource.class, userOneN1.getId(), document.getNodeRef().getId(), null, 200);
+ document = jacksonUtil.parseEntry(response.getJsonResponse(), Document.class);
+ assertEquals(fileName, document.getName());
+ contentInfo = document.getContent();
+ assertNotNull(contentInfo);
+ assertEquals(MimetypeMap.MIMETYPE_TEXT_PLAIN, contentInfo.getMimeType());
+
+ // Check 'get children' is confirming the upload
+ response = getAll(getChildrenUrl(folderA_Ref), userOneN1.getId(), paging, 200);
+ pagingResult = parsePaging(response.getJsonResponse());
+ assertNotNull(paging);
+ assertEquals(numOfNodes + 1, pagingResult.getCount().intValue());
+
+ // Upload the same file again to check the name conflicts handling
+ post(getChildrenUrl(folderA_Ref), userOneN1.getId(), new String(reqBody.getBody()), null, reqBody.getContentType(), 409);
+
+ // Set overwrite=true and upload the same file again
+ reqBody = MultiPartBuilder.copy(multiPartBuilder)
+ .setOverwrite(true)
+ .build();
+ post(getChildrenUrl(folderA_Ref), userOneN1.getId(), new String(reqBody.getBody()), null, reqBody.getContentType(), 201);
+
+ response = getAll(getChildrenUrl(folderA_Ref), userOneN1.getId(), paging, 200);
+ pagingResult = parsePaging(response.getJsonResponse());
+ assertNotNull(paging);
+ assertEquals(numOfNodes + 1, pagingResult.getCount().intValue());
+
+ final String fileName2 = "quick-2.txt";
+ final File file2 = getResourceFile(fileName2);
+ reqBody = MultiPartBuilder.create()
+ .setFileData(new FileData(fileName2, file2, MimetypeMap.MIMETYPE_TEXT_PLAIN))
+ .build();
+ // userTwoN1 tries to upload a new file into the folderA of userOneN1
+ post(getChildrenUrl(folderA_Ref), userTwoN1.getId(), new String(reqBody.getBody()), null, reqBody.getContentType(), 403);
+ }
+
+ private String getChildrenUrl(NodeRef parentNodeRef)
+ {
+ return getChildrenUrl(parentNodeRef.getId());
+ }
+
+ private String getChildrenUrl(String parentId)
+ {
+ return "nodes/" + parentId + "/children";
+ }
+
+ private File getResourceFile(String fileName) throws FileNotFoundException
+ {
+ URL url = NodeApiTest.class.getClassLoader().getResource(RESOURCE_PREFIX + fileName);
+ if (url == null)
+ {
+ fail("Cannot get the resource: " + fileName);
+ }
+ return ResourceUtils.getFile(url);
}
@Override
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 118ca967aa..2e97181728 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
@@ -57,7 +57,7 @@ public class MultiPartBuilder
private String contentTypeQNameStr;
private List aspects;
private boolean majorVersion;
- private boolean overwrite = true; // If a fileName clashes for a versionable file
+ private boolean overwrite = false; // If a fileName clashes for a versionable file
private MultiPartBuilder()
{
@@ -232,11 +232,13 @@ public class MultiPartBuilder
public MultiPartRequest build() throws IOException
{
- assertNotNull(fileData);
List parts = new ArrayList<>();
- parts.add(new FilePart("filedata", fileData.getFileName(), fileData.getFile(), fileData.getMimetype(), null));
- addPartIfNotNull(parts, "filename", fileData.getFileName());
+ if (fileData != null)
+ {
+ parts.add(new FilePart("filedata", fileData.getFileName(), fileData.getFile(), fileData.getMimetype(), null));
+ addPartIfNotNull(parts, "filename", fileData.getFileName());
+ }
addPartIfNotNull(parts, "siteid", siteId);
addPartIfNotNull(parts, "containerid", containerId);
addPartIfNotNull(parts, "destination", destination);
diff --git a/source/test-resources/publicapi/upload/quick-1.txt b/source/test-resources/publicapi/upload/quick-1.txt
new file mode 100644
index 0000000000..ff3bb63948
--- /dev/null
+++ b/source/test-resources/publicapi/upload/quick-1.txt
@@ -0,0 +1 @@
+The quick brown fox jumps over the lazy dog
\ No newline at end of file
diff --git a/source/test-resources/publicapi/upload/quick-2.txt b/source/test-resources/publicapi/upload/quick-2.txt
new file mode 100644
index 0000000000..ca103eec21
--- /dev/null
+++ b/source/test-resources/publicapi/upload/quick-2.txt
@@ -0,0 +1,2 @@
+Gym class featuring a brown fox and lazy dog
+The quick brown fox jumps over the lazy dog
\ No newline at end of file
diff --git a/source/test-resources/publicapi/upload/quick.pdf b/source/test-resources/publicapi/upload/quick.pdf
new file mode 100644
index 0000000000..a1779afd8b
Binary files /dev/null and b/source/test-resources/publicapi/upload/quick.pdf differ