Merged HEAD (5.2) to 5.2.N (5.2.1)

126357 jkaabimofrad: Merged FILE-FOLDER-API (5.2.0) to HEAD (5.2)
      119504 jkaabimofrad: RA-637, SFS-260: Added multipart upload REST API.


git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/BRANCHES/DEV/5.2.N/root@126703 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
Ancuta Morarasu
2016-05-11 10:47:32 +00:00
parent fe59b7dcad
commit 0b3aacf260
11 changed files with 589 additions and 31 deletions

View File

@@ -11,4 +11,5 @@ framework.exception.PermissionDenied=Permission was denied
framework.exception.StaleEntity=Attempt to update a stale entity framework.exception.StaleEntity=Attempt to update a stale entity
framework.exception.UnsupportedResourceOperation=The operation is unsupported framework.exception.UnsupportedResourceOperation=The operation is unsupported
framework.exception.DeletedResource=In this version of the API resource {0} has been deleted framework.exception.DeletedResource=In this version of the API resource {0} has been deleted
framework.exception.RequestEntityTooLarge=Request entity too large

View File

@@ -137,7 +137,8 @@
<entry key="org.alfresco.rest.framework.core.exceptions.PermissionDeniedException" value="#{T(org.springframework.extensions.webscripts.Status).STATUS_FORBIDDEN}" /> <entry key="org.alfresco.rest.framework.core.exceptions.PermissionDeniedException" value="#{T(org.springframework.extensions.webscripts.Status).STATUS_FORBIDDEN}" />
<entry key="org.alfresco.rest.framework.core.exceptions.UnsupportedResourceOperationException" value="#{T(org.springframework.extensions.webscripts.Status).STATUS_METHOD_NOT_ALLOWED}" /> <entry key="org.alfresco.rest.framework.core.exceptions.UnsupportedResourceOperationException" value="#{T(org.springframework.extensions.webscripts.Status).STATUS_METHOD_NOT_ALLOWED}" />
<entry key="org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException" value="#{T(org.springframework.extensions.webscripts.Status).STATUS_CONFLICT}" /> <entry key="org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException" value="#{T(org.springframework.extensions.webscripts.Status).STATUS_CONFLICT}" />
<entry key="org.alfresco.rest.framework.core.exceptions.StaleEntityException" value="#{T(org.springframework.extensions.webscripts.Status).STATUS_CONFLICT}" /> <entry key="org.alfresco.rest.framework.core.exceptions.StaleEntityException" value="#{T(org.springframework.extensions.webscripts.Status).STATUS_CONFLICT}" />
<entry key="org.alfresco.rest.framework.core.exceptions.RequestEntityTooLargeException" value="#{T(org.springframework.extensions.webscripts.Status).STATUS_REQUEST_ENTITY_TOO_LARGE}" />
</map> </map>
</property> </property>
</bean> </bean>

View File

@@ -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.NodeRef;
import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.QName;
import org.springframework.extensions.webscripts.servlet.FormData;
/** /**
* @author steveglover * @author steveglover
@@ -45,11 +46,11 @@ import org.alfresco.service.namespace.QName;
*/ */
public interface Nodes public interface Nodes
{ {
NodeRef validateNode(StoreRef storeRef, String nodeId); NodeRef validateNode(StoreRef storeRef, String nodeId);
NodeRef validateNode(String nodeId); NodeRef validateNode(String nodeId);
NodeRef validateNode(NodeRef nodeRef); NodeRef validateNode(NodeRef nodeRef);
boolean nodeMatches(NodeRef nodeRef, Set<QName> expectedTypes, Set<QName> excludedTypes); boolean nodeMatches(NodeRef nodeRef, Set<QName> expectedTypes, Set<QName> excludedTypes);
/** /**
* Get the node representation for the given node. * Get the node representation for the given node.
* @param nodeId String * @param nodeId String
@@ -122,4 +123,14 @@ public interface Nodes
// TODO update REST fwk - to optionally support return of json // TODO update REST fwk - to optionally support return of json
void updateContent(String fileNodeId, BasicContentInfo contentInfo, InputStream stream, Parameters parameters); 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);
} }

View File

@@ -28,8 +28,10 @@ package org.alfresco.rest.api.impl;
import org.alfresco.model.ContentModel; import org.alfresco.model.ContentModel;
import org.alfresco.query.PagingRequest; import org.alfresco.query.PagingRequest;
import org.alfresco.query.PagingResults; import org.alfresco.query.PagingResults;
import org.alfresco.repo.content.ContentLimitViolationException;
import org.alfresco.repo.model.Repository; import org.alfresco.repo.model.Repository;
import org.alfresco.repo.node.getchildren.GetChildrenCannedQuery; import org.alfresco.repo.node.getchildren.GetChildrenCannedQuery;
import org.alfresco.repo.security.permissions.AccessDeniedException;
import org.alfresco.rest.antlr.WhereClauseParser; import org.alfresco.rest.antlr.WhereClauseParser;
import org.alfresco.rest.api.Nodes; import org.alfresco.rest.api.Nodes;
import org.alfresco.rest.api.model.Document; 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;
import org.alfresco.rest.api.model.PathInfo.ElementInfo; import org.alfresco.rest.api.model.PathInfo.ElementInfo;
import org.alfresco.rest.api.model.UserInfo; 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.EntityNotFoundException;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; 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.BasicContentInfo;
import org.alfresco.rest.framework.resource.content.BinaryResource; import org.alfresco.rest.framework.resource.content.BinaryResource;
import org.alfresco.rest.framework.resource.content.NodeBinaryResource; 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.framework.resource.parameters.where.QueryHelper;
import org.alfresco.rest.workflow.api.impl.MapBasedQueryWalker; import org.alfresco.rest.workflow.api.impl.MapBasedQueryWalker;
import org.alfresco.service.ServiceRegistry; 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.dictionary.DictionaryService;
import org.alfresco.service.cmr.model.FileFolderService; import org.alfresco.service.cmr.model.FileFolderService;
import org.alfresco.service.cmr.model.FileInfo; import org.alfresco.service.cmr.model.FileInfo;
import org.alfresco.service.cmr.model.FileNotFoundException; import org.alfresco.service.cmr.model.FileNotFoundException;
import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentData; 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.ContentWriter;
import org.alfresco.service.cmr.repository.InvalidNodeRefException; 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.NodeRef;
import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.Path; 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.repository.StoreRef;
import org.alfresco.service.cmr.security.AccessStatus; import org.alfresco.service.cmr.security.AccessStatus;
import org.alfresco.service.cmr.security.PermissionService; 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.NamespaceService;
import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.QName;
import org.alfresco.util.Pair; 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.InputStream;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigInteger; import java.math.BigInteger;
import java.nio.charset.Charset;
import java.util.AbstractList; import java.util.AbstractList;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@@ -95,6 +116,8 @@ import java.util.Set;
*/ */
public class NodesImpl implements Nodes public class NodesImpl implements Nodes
{ {
private static final Log logger = LogFactory.getLog(NodesImpl.class);
private static enum Type private static enum Type
{ {
// Note: ordered // Note: ordered
@@ -114,6 +137,10 @@ public class NodesImpl implements Nodes
private FileFolderService fileFolderService; private FileFolderService fileFolderService;
private NamespaceService namespaceService; private NamespaceService namespaceService;
private PermissionService permissionService; private PermissionService permissionService;
private MimetypeService mimetypeService;
private ContentService contentService;
private ActionService actionService;
private VersionService versionService;
private Repository repositoryHelper; private Repository repositoryHelper;
private ServiceRegistry sr; private ServiceRegistry sr;
private Set<String> defaultIgnoreTypes; private Set<String> defaultIgnoreTypes;
@@ -121,6 +148,16 @@ public class NodesImpl implements Nodes
public void init() 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) if (defaultIgnoreTypes != null)
{ {
ignoreTypeQNames = new HashSet<>(defaultIgnoreTypes.size()); ignoreTypeQNames = new HashSet<>(defaultIgnoreTypes.size());
@@ -133,12 +170,6 @@ public class NodesImpl implements Nodes
public void setServiceRegistry(ServiceRegistry sr) { public void setServiceRegistry(ServiceRegistry sr) {
this.sr = 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) 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) // 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 // check that requested parent node exists and it's type is a (sub-)type of folder
final NodeRef parentNodeRef = validateOrLookupNode(parentFolderNodeId, null); final NodeRef parentNodeRef = validateOrLookupNode(parentFolderNodeId, null);
if (! nodeMatches(parentNodeRef, Collections.singleton(ContentModel.TYPE_FOLDER), null)) if (! nodeMatches(parentNodeRef, Collections.singleton(ContentModel.TYPE_FOLDER), null))
{ {
throw new InvalidArgumentException("NodeId of folder is expected: "+parentNodeRef); throw new InvalidArgumentException("NodeId of folder is expected: "+parentNodeRef);
} }
String nodeName = nodeInfo.getName(); String nodeName = nodeInfo.getName();
if ((nodeName == null) || nodeName.isEmpty()) if ((nodeName == null) || nodeName.isEmpty())
@@ -849,7 +880,7 @@ public class NodesImpl implements Nodes
} }
return getFolderOrDocument(nodeRef.getId(), parameters); return getFolderOrDocument(nodeRef.getId(), parameters);
} }
public Node updateNode(String nodeId, Node nodeInfo, Parameters parameters) public Node updateNode(String nodeId, Node nodeInfo, Parameters parameters)
{ {
@@ -933,6 +964,259 @@ public class NodesImpl implements Nodes
return; 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.<String>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<QName, Serializable> 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 * Helper to create a QName from either a fully qualified or short-name QName string
* *

View File

@@ -19,15 +19,17 @@
package org.alfresco.rest.api.nodes; package org.alfresco.rest.api.nodes;
import 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.api.model.Node;
import org.alfresco.rest.framework.WebApiDescription; 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.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.actions.interfaces.RelationshipResourceAction;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Parameters; import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.util.ParameterCheck; import org.alfresco.util.ParameterCheck;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
import org.springframework.extensions.webscripts.servlet.FormData;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -36,18 +38,20 @@ import java.util.List;
* TODO ... work-in-progress * TODO ... work-in-progress
* *
* @author janv * @author janv
* @author Jamal Kaabi-Mofrad
*/ */
@RelationshipResource(name = "children", entityResource = NodesEntityResource.class, title = "Folder children") @RelationshipResource(name = "children", entityResource = NodesEntityResource.class, title = "Folder children")
public class NodeChildrenRelation implements RelationshipResourceAction.Read<Node>, RelationshipResourceAction.Create<Node>, InitializingBean public class NodeChildrenRelation implements RelationshipResourceAction.Read<Node>, RelationshipResourceAction.Create<Node>,
MultiPartRelationshipResourceAction.Create<Node>, InitializingBean
{ {
private Nodes nodes; private Nodes nodes;
public void setNodes(Nodes nodes) public void setNodes(Nodes nodes)
{ {
this.nodes = nodes; this.nodes = nodes;
} }
@Override @Override
public void afterPropertiesSet() public void afterPropertiesSet()
{ {
ParameterCheck.mandatory("nodes", this.nodes); ParameterCheck.mandatory("nodes", this.nodes);
@@ -104,4 +108,13 @@ public class NodeChildrenRelation implements RelationshipResourceAction.Read<Nod
return result; return result;
} }
@Override
@WebApiDescription(title = "Upload file content and meta-data into the repository.")
@WebApiParam(name = "formData", title = "A single form data", description = "A single form data which holds FormFields.")
public Node create(String parentFolderNodeId, FormData formData, Parameters parameters)
{
return nodes.upload(parentFolderNodeId, formData, parameters);
}
} }

View File

@@ -0,0 +1,40 @@
/*
* Copyright (C) 2005-2015 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* 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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@@ -19,18 +19,22 @@
package org.alfresco.rest.api.tests; 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.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import org.alfresco.model.ContentModel; import org.alfresco.model.ContentModel;
import org.alfresco.model.ForumModel; import org.alfresco.model.ForumModel;
import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.model.Repository; import org.alfresco.repo.model.Repository;
import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; 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.Document;
import org.alfresco.rest.api.model.Folder;
import org.alfresco.rest.api.model.Node; import org.alfresco.rest.api.model.Node;
import org.alfresco.rest.api.model.PathInfo; import org.alfresco.rest.api.model.PathInfo;
import org.alfresco.rest.api.model.PathInfo.ElementInfo; 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.TestPerson;
import org.alfresco.rest.api.tests.RepoService.TestSite; import org.alfresco.rest.api.tests.RepoService.TestSite;
import org.alfresco.rest.api.tests.client.HttpResponse; 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.ExpectedPaging;
import org.alfresco.rest.api.tests.client.PublicApiClient.Paging; import org.alfresco.rest.api.tests.client.PublicApiClient.Paging;
import org.alfresco.rest.api.tests.client.data.SiteRole; import org.alfresco.rest.api.tests.client.data.SiteRole;
import org.alfresco.rest.api.tests.util.JacksonUtil; 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.api.tests.util.RestApiUtil;
import org.alfresco.rest.framework.jacksonextensions.JacksonHelper; import org.alfresco.rest.framework.jacksonextensions.JacksonHelper;
import org.alfresco.service.cmr.repository.NodeRef; 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.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.util.ResourceUtils;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.Serializable; import java.io.Serializable;
import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@@ -67,14 +79,16 @@ import java.util.Set;
/** /**
* API tests for: * API tests for:
* <ul> * <ul>
* <li> {@literal host:port/alfresco/api/{networkId}/public/alfresco/versions/1/nodes/{nodeId}} </li> * <li> {@literal <host>:<port>/alfresco/api/<networkId>/public/alfresco/versions/1/nodes/<nodeId>} </li>
* <li> {@literal host:port/alfresco/api/{networkId}/public/alfresco/versions/1/nodes/{nodeId}/children} </li> * <li> {@literal <host>:<port>/alfresco/api/<networkId>/public/alfresco/versions/1/nodes/<nodeId>/children} </li>
* </ul> * </ul>
* *
* @author Jamal Kaabi-Mofrad * @author Jamal Kaabi-Mofrad
*/ */
public class NodeApiTest extends AbstractBaseApiTest public class NodeApiTest extends AbstractBaseApiTest
{ {
private static final String RESOURCE_PREFIX = "publicapi/upload/";
/** /**
* User one from network one * User one from network one
*/ */
@@ -145,6 +159,11 @@ public class NodeApiTest extends AbstractBaseApiTest
AuthenticationUtil.clearCurrentSecurityContext(); AuthenticationUtil.clearCurrentSecurityContext();
} }
/**
* Tests get document library children.
* <p>GET:</p>
* {@literal <host>:<port>/alfresco/api/<networkId>/public/alfresco/versions/1/nodes/<nodeId>/children}
*/
@Test @Test
public void testListDocLibChildren() throws Exception public void testListDocLibChildren() throws Exception
{ {
@@ -246,6 +265,11 @@ public class NodeApiTest extends AbstractBaseApiTest
getAll(getChildrenUrl(docLibNodeRef), userTwoN1.getId(), paging, 403); getAll(getChildrenUrl(docLibNodeRef), userTwoN1.getId(), paging, 403);
} }
/**
* Tests get user's home children.
* <p>GET:</p>
* {@literal <host>:<port>/alfresco/api/-default-/public/alfresco/versions/1/nodes/<nodeId>/children}
*/
@Test @Test
public void testListMyFilesChildren() throws Exception public void testListMyFilesChildren() throws Exception
{ {
@@ -317,6 +341,11 @@ public class NodeApiTest extends AbstractBaseApiTest
assertEquals("doclib:1444660852296", ((List<?>) entry.getValue()).get(0)); assertEquals("doclib:1444660852296", ((List<?>) entry.getValue()).get(0));
} }
/**
* Tests get node with path information.
* <p>GET:</p>
* {@literal <host>:<port>/alfresco/api/<networkId>/public/alfresco/versions/1/nodes/<nodeId>?select=path}
*/
@Test @Test
public void testGetPathElements_DocLib() throws Exception public void testGetPathElements_DocLib() throws Exception
{ {
@@ -385,6 +414,11 @@ public class NodeApiTest extends AbstractBaseApiTest
assertEquals(folderC, pathElements.get(0).getName()); assertEquals(folderC, pathElements.get(0).getName());
} }
/**
* Tests get node with path information.
* <p>GET:</p>
* {@literal <host>:<port>/alfresco/api/-default-/public/alfresco/versions/1/nodes/<nodeId>?select=path}
*/
@Test @Test
public void testGetPathElements_MyFiles() throws Exception public void testGetPathElements_MyFiles() throws Exception
{ {
@@ -434,6 +468,14 @@ public class NodeApiTest extends AbstractBaseApiTest
assertNotNull(pathElements.get(4).getId()); assertNotNull(pathElements.get(4).getId());
} }
/**
* Tests well-known aliases.
* <p>GET:</p>
* <ul>
* <li> {@literal <host>:<port>/alfresco/api/-default-/public/alfresco/versions/1/nodes/-root-}</li>
* <li> {@literal <host>:<port>/alfresco/api/-default-/public/alfresco/versions/1/nodes/-my-} </li>
* </ul>
*/
@Test @Test
public void testGetNodeWithKnownAlias() throws Exception public void testGetNodeWithKnownAlias() throws Exception
{ {
@@ -472,9 +514,170 @@ public class NodeApiTest extends AbstractBaseApiTest
getSingle(NodesEntityResource.class, user1, userNodeAlias, null, 404); // Not found 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).
* <p>POST:</p>
* {@literal <host>:<port>/alfresco/api/-default-/public/alfresco/versions/1/nodes/<nodeId>/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.
* <p>POST:</p>
* {@literal <host>:<port>/alfresco/api/<networkId>/public/alfresco/versions/1/nodes/<nodeId>/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 @Override

View File

@@ -57,7 +57,7 @@ public class MultiPartBuilder
private String contentTypeQNameStr; private String contentTypeQNameStr;
private List<String> aspects; private List<String> aspects;
private boolean majorVersion; 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() private MultiPartBuilder()
{ {
@@ -232,11 +232,13 @@ public class MultiPartBuilder
public MultiPartRequest build() throws IOException public MultiPartRequest build() throws IOException
{ {
assertNotNull(fileData);
List<Part> parts = new ArrayList<>(); List<Part> parts = new ArrayList<>();
parts.add(new FilePart("filedata", fileData.getFileName(), fileData.getFile(), fileData.getMimetype(), null)); if (fileData != null)
addPartIfNotNull(parts, "filename", fileData.getFileName()); {
parts.add(new FilePart("filedata", fileData.getFileName(), fileData.getFile(), fileData.getMimetype(), null));
addPartIfNotNull(parts, "filename", fileData.getFileName());
}
addPartIfNotNull(parts, "siteid", siteId); addPartIfNotNull(parts, "siteid", siteId);
addPartIfNotNull(parts, "containerid", containerId); addPartIfNotNull(parts, "containerid", containerId);
addPartIfNotNull(parts, "destination", destination); addPartIfNotNull(parts, "destination", destination);

View File

@@ -0,0 +1 @@
The quick brown fox jumps over the lazy dog

View File

@@ -0,0 +1,2 @@
Gym class featuring a brown fox and lazy dog
The quick brown fox jumps over the lazy dog

Binary file not shown.