diff --git a/source/java/org/alfresco/rest/api/PublicApiHttpServletRequest.java b/source/java/org/alfresco/rest/api/PublicApiHttpServletRequest.java index c5ba3929d7..ff2fcd6628 100644 --- a/source/java/org/alfresco/rest/api/PublicApiHttpServletRequest.java +++ b/source/java/org/alfresco/rest/api/PublicApiHttpServletRequest.java @@ -1,3 +1,4 @@ + package org.alfresco.rest.api; import java.io.BufferedInputStream; @@ -10,50 +11,80 @@ import javax.servlet.http.HttpServletRequestWrapper; public class PublicApiHttpServletRequest extends HttpServletRequestWrapper { - public PublicApiHttpServletRequest(HttpServletRequest request) throws IOException - { - super(getWrappedHttpServletRequest(request)); - } + private static final String HEADER_CONTENT_TYPE = "Content-Type"; + public static final String MULTIPART_FORM_DATA = "multipart/form-data"; - public void resetInputStream() throws IOException - { - ServletInputStream stream = getInputStream(); - stream.reset(); - } - - private static HttpServletRequest getWrappedHttpServletRequest(HttpServletRequest request) throws IOException - { - final PublicApiServletInputStream sis = new PublicApiServletInputStream(request.getInputStream()); - HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request) - { - public ServletInputStream getInputStream() throws java.io.IOException - { - return sis; - } - }; - return wrapper; - } - - private static class PublicApiServletInputStream extends ServletInputStream - { - private BufferedInputStream in; + public PublicApiHttpServletRequest(HttpServletRequest request) throws IOException + { + super(getWrappedHttpServletRequest(request)); + } - PublicApiServletInputStream(InputStream in) - { - this.in = new BufferedInputStream(in); - this.in.mark(8096); - } + public void resetInputStream() throws IOException + { + ServletInputStream stream = getInputStream(); + if (stream.markSupported()) + { + stream.reset(); + } + } - @Override - public int read() throws IOException - { - return in.read(); - } + private static HttpServletRequest getWrappedHttpServletRequest(HttpServletRequest request) throws IOException + { + //TODO is it really necessary to wrap the request into a BufferedInputStream? + // If not, then we could remove the check for multipart upload. + // The check is needed as we get an IOException (Resetting to invalid mark) for files more than 8193 bytes. + boolean resetSupported = true; + String contentType = request.getHeader(HEADER_CONTENT_TYPE); + if (contentType != null && contentType.startsWith(MULTIPART_FORM_DATA)) + { + resetSupported = false; + } + final PublicApiServletInputStream sis = new PublicApiServletInputStream(request.getInputStream(), resetSupported); + HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request) + { + public ServletInputStream getInputStream() throws java.io.IOException + { + return sis; + } + }; + return wrapper; + } - @Override - public void reset() throws IOException - { - in.reset(); - } - } + private static class PublicApiServletInputStream extends ServletInputStream + { + private final InputStream in; + private final boolean resetSupported; + + PublicApiServletInputStream(InputStream in, boolean resetSupported) + { + this.resetSupported = resetSupported; + if (resetSupported) + { + this.in = new BufferedInputStream(in); + this.in.mark(8096); + } + else + { + this.in = in; + } + } + + @Override + public int read() throws IOException + { + return in.read(); + } + + @Override + public void reset() throws IOException + { + in.reset(); + } + + @Override + public boolean markSupported() + { + return resetSupported; + } + } } diff --git a/source/java/org/alfresco/rest/framework/core/ResourceInspector.java b/source/java/org/alfresco/rest/framework/core/ResourceInspector.java index 64c7ce0d41..ec35cc49df 100644 --- a/source/java/org/alfresco/rest/framework/core/ResourceInspector.java +++ b/source/java/org/alfresco/rest/framework/core/ResourceInspector.java @@ -27,6 +27,8 @@ import org.alfresco.rest.framework.resource.RelationshipResource; import org.alfresco.rest.framework.resource.UniqueId; import org.alfresco.rest.framework.resource.actions.interfaces.BinaryResourceAction; import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction; +import org.alfresco.rest.framework.resource.actions.interfaces.MultiPartResourceAction; +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.ResourceAction; import org.alfresco.util.Pair; @@ -58,13 +60,15 @@ public class ResourceInspector ALL_ENTITY_RESOURCE_INTERFACES.add(EntityResourceAction.Update.class); ALL_ENTITY_RESOURCE_INTERFACES.add(EntityResourceAction.Delete.class); ALL_ENTITY_RESOURCE_INTERFACES.add(BinaryResourceAction.Read.class); - + ALL_ENTITY_RESOURCE_INTERFACES.add(MultiPartResourceAction.Create.class); + ALL_RELATIONSHIP_RESOURCE_INTERFACES.add(RelationshipResourceAction.Create.class); ALL_RELATIONSHIP_RESOURCE_INTERFACES.add(RelationshipResourceAction.Read.class); ALL_RELATIONSHIP_RESOURCE_INTERFACES.add(RelationshipResourceAction.ReadById.class); ALL_RELATIONSHIP_RESOURCE_INTERFACES.add(RelationshipResourceAction.Update.class); ALL_RELATIONSHIP_RESOURCE_INTERFACES.add(RelationshipResourceAction.Delete.class); - + ALL_RELATIONSHIP_RESOURCE_INTERFACES.add(MultiPartRelationshipResourceAction.Create.class); + ALL_PROPERTY_RESOURCE_INTERFACES.add(BinaryResourceAction.Read.class); ALL_PROPERTY_RESOURCE_INTERFACES.add(BinaryResourceAction.Delete.class); ALL_PROPERTY_RESOURCE_INTERFACES.add(BinaryResourceAction.Update.class); @@ -90,6 +94,7 @@ public class ResourceInspector findOperation(EntityResourceAction.ReadById.class, HttpMethod.GET, helper); findOperation(EntityResourceAction.Update.class, HttpMethod.PUT, helper); findOperation(EntityResourceAction.Delete.class, HttpMethod.DELETE, helper); + findOperation(MultiPartResourceAction.Create.class, HttpMethod.POST, helper); if (resource.isAnnotationPresent(WebApiDeleted.class)) { @@ -189,7 +194,8 @@ public class ResourceInspector findOperation(RelationshipResourceAction.Read.class, HttpMethod.GET, helper); findOperation(RelationshipResourceAction.ReadById.class, HttpMethod.GET, helper); findOperation(RelationshipResourceAction.Update.class, HttpMethod.PUT, helper); - findOperation(RelationshipResourceAction.Delete.class, HttpMethod.DELETE, helper); + findOperation(RelationshipResourceAction.Delete.class, HttpMethod.DELETE, helper); + findOperation(MultiPartRelationshipResourceAction.Create.class, HttpMethod.POST, helper); if (resource.isAnnotationPresent(WebApiDeleted.class)) { diff --git a/source/java/org/alfresco/rest/framework/resource/actions/interfaces/MultiPartRelationshipResourceAction.java b/source/java/org/alfresco/rest/framework/resource/actions/interfaces/MultiPartRelationshipResourceAction.java new file mode 100644 index 0000000000..740e4c3ae5 --- /dev/null +++ b/source/java/org/alfresco/rest/framework/resource/actions/interfaces/MultiPartRelationshipResourceAction.java @@ -0,0 +1,38 @@ +/* + * 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 . + */ + +package org.alfresco.rest.framework.resource.actions.interfaces; + +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.springframework.extensions.webscripts.servlet.FormData; + +/** + * @author Jamal Kaabi-Mofrad + */ +public interface MultiPartRelationshipResourceAction +{ + + /** + * HTTP POST - Upload file content and meta-data into repository + */ + public static interface Create extends ResourceAction + { + public E create(String entityResourceId, FormData formData, Parameters parameters); + } +} diff --git a/source/java/org/alfresco/rest/framework/resource/actions/interfaces/MultiPartResourceAction.java b/source/java/org/alfresco/rest/framework/resource/actions/interfaces/MultiPartResourceAction.java new file mode 100644 index 0000000000..0e3b5e6318 --- /dev/null +++ b/source/java/org/alfresco/rest/framework/resource/actions/interfaces/MultiPartResourceAction.java @@ -0,0 +1,38 @@ +/* + * 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 . + */ + +package org.alfresco.rest.framework.resource.actions.interfaces; + +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.springframework.extensions.webscripts.servlet.FormData; + +/** + * @author Jamal Kaabi-Mofrad + */ +public interface MultiPartResourceAction +{ + + /** + * HTTP POST - Upload file content and meta-data into repository + */ + public static interface Create extends ResourceAction + { + public E create(FormData formData, Parameters parameters); + } +} diff --git a/source/java/org/alfresco/rest/framework/resource/parameters/Parameters.java b/source/java/org/alfresco/rest/framework/resource/parameters/Parameters.java index f83e547cb0..56b0bf66d4 100644 --- a/source/java/org/alfresco/rest/framework/resource/parameters/Parameters.java +++ b/source/java/org/alfresco/rest/framework/resource/parameters/Parameters.java @@ -7,6 +7,7 @@ import org.alfresco.rest.framework.jacksonextensions.BeanPropertiesFilter; import org.alfresco.rest.framework.resource.content.BasicContentInfo; import org.alfresco.rest.framework.resource.parameters.where.Query; import org.apache.poi.ss.formula.functions.T; +import org.springframework.extensions.webscripts.Status; /** @@ -90,5 +91,12 @@ public interface Parameters * Gets the basic information about content, typically taken from a HTTPServletRequest. * @return BasicContentInfo the content info */ - BasicContentInfo getContentInfo(); + BasicContentInfo getContentInfo(); + + /** + * Gets Web Script status + * + * @return {@link Status} + */ + public Status getStatus(); } diff --git a/source/java/org/alfresco/rest/framework/resource/parameters/Params.java b/source/java/org/alfresco/rest/framework/resource/parameters/Params.java index dc20e9fca2..1415b874bc 100644 --- a/source/java/org/alfresco/rest/framework/resource/parameters/Params.java +++ b/source/java/org/alfresco/rest/framework/resource/parameters/Params.java @@ -14,6 +14,7 @@ import org.alfresco.rest.framework.resource.parameters.where.Query; import org.alfresco.rest.framework.resource.parameters.where.QueryImpl; import org.apache.commons.beanutils.ConvertUtils; import org.apache.poi.ss.formula.functions.T; +import org.springframework.extensions.webscripts.Status; /** * Parameters passed in from a Rest client for use in calls to the rest api. @@ -29,7 +30,8 @@ public class Params implements Parameters private final RecognizedParams recognizedParams; private final String addressedProperty; private final BasicContentInfo contentInfo; - + private final Status status; + //Constants private static final RecognizedParams NULL_PARAMS = new RecognizedParams(null, null, null, null, null, null, null); private static final BasicContentInfo DEFAULT_CONTENT_INFO = new ContentInfoImpl(MimetypeMap.MIMETYPE_BINARY, "UTF-8", -1, null); @@ -44,6 +46,7 @@ public class Params implements Parameters this.recognizedParams = recognizedParams; this.addressedProperty = addressedProperty; this.contentInfo = contentInfo==null?DEFAULT_CONTENT_INFO:contentInfo; + this.status = new Status(); } public static Params valueOf(BeanPropertiesFilter paramFilter, String entityId) @@ -196,10 +199,17 @@ public class Params implements Parameters } @Override - public BasicContentInfo getContentInfo() { - return contentInfo; - } - + public BasicContentInfo getContentInfo() + { + return contentInfo; + } + + @Override + public Status getStatus() + { + return status; + } + /** * A formal set of params that any rest service could potentially have passed in as request params */ diff --git a/source/java/org/alfresco/rest/framework/webscripts/AbstractResourceWebScript.java b/source/java/org/alfresco/rest/framework/webscripts/AbstractResourceWebScript.java index c66d2dc9c7..271f30d2b3 100644 --- a/source/java/org/alfresco/rest/framework/webscripts/AbstractResourceWebScript.java +++ b/source/java/org/alfresco/rest/framework/webscripts/AbstractResourceWebScript.java @@ -72,7 +72,14 @@ public abstract class AbstractResourceWebScript extends ApiWebScript implements { respons.put("toSerialize", result); respons.put("contentInfo", contentInfo); - setSuccessResponseStatus(res); + if (params.getStatus().getRedirect()) + { + res.setStatus(params.getStatus().getCode()); + } + else + { + setSuccessResponseStatus(res); + } } }); diff --git a/source/java/org/alfresco/rest/framework/webscripts/ResourceWebScriptPost.java b/source/java/org/alfresco/rest/framework/webscripts/ResourceWebScriptPost.java index 28bfd190b6..fcd4364913 100644 --- a/source/java/org/alfresco/rest/framework/webscripts/ResourceWebScriptPost.java +++ b/source/java/org/alfresco/rest/framework/webscripts/ResourceWebScriptPost.java @@ -13,6 +13,8 @@ import org.alfresco.rest.framework.core.exceptions.DeletedResourceException; import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; import org.alfresco.rest.framework.core.exceptions.UnsupportedResourceOperationException; import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction; +import org.alfresco.rest.framework.resource.actions.interfaces.MultiPartResourceAction; +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.Params; @@ -20,7 +22,9 @@ import org.alfresco.rest.framework.resource.parameters.Params.RecognizedParams; import org.apache.commons.lang.StringUtils; import org.springframework.extensions.webscripts.Status; import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptRequestImpl; import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.extensions.webscripts.servlet.FormData; import org.springframework.http.HttpMethod; /** @@ -55,7 +59,7 @@ public class ResourceWebScriptPost extends AbstractResourceWebScript implements } else { - Object postedObj = extractObjFromJson(resourceMeta, req); + Object postedObj = processRequest(resourceMeta, req); return Params.valueOf(null, params, postedObj); } case RELATIONSHIP: @@ -63,18 +67,33 @@ public class ResourceWebScriptPost extends AbstractResourceWebScript implements String relationshipId = req.getServiceMatch().getTemplateVars().get(ResourceLocator.RELATIONSHIP_ID); if (StringUtils.isNotBlank(relationshipId)) { - throw new UnsupportedResourceOperationException("POST is executed against the collection URL"); + throw new UnsupportedResourceOperationException("POST is executed against the collection URL"); } else { - Object postedRel = extractObjFromJson(resourceMeta, req); - return Params.valueOf(entityId,params,postedRel); + Object postedRel = processRequest(resourceMeta, req); + return Params.valueOf(entityId, params, postedRel); } default: throw new UnsupportedResourceOperationException("POST not supported for Actions"); } } + /** + * If the request content-type is multipart/form-data then it + * returns the {@link FormData}, otherwise it tries to extract the required + * object from the JSON payload. + */ + private Object processRequest(ResourceMetadata resourceMeta, WebScriptRequest req) + { + if (WebScriptRequestImpl.MULTIPART_FORM_DATA.equals(req.getContentType())) + { + return (FormData) req.parseContent(); + } + + return extractObjFromJson(resourceMeta, req); + } + /** * If the @WebApiParam has been used and set allowMultiple to false then this will get a single entry. It * should error if an array is passed in. @@ -86,7 +105,7 @@ public class ResourceWebScriptPost extends AbstractResourceWebScript implements { List params = resourceMeta.getParameters(HttpMethod.POST); Class objType = resourceMeta.getObjectType(HttpMethod.POST); - + if (!params.isEmpty()) { for (ResourceParameter resourceParameter : params) @@ -125,39 +144,60 @@ public class ResourceWebScriptPost extends AbstractResourceWebScript implements @SuppressWarnings("unchecked") private Object executeInternal(ResourceWithMetadata resource, Params params) { + final Object resObj = resource.getResource(); switch (resource.getMetaData().getType()) { case ENTITY: if (resource.getMetaData().isDeleted(EntityResourceAction.Create.class)) { - throw new DeletedResourceException("(DELETE) "+resource.getMetaData().getUniqueId()); + throw new DeletedResourceException("(DELETE) " + resource.getMetaData().getUniqueId()); } - EntityResourceAction.Create creator = (EntityResourceAction.Create) resource.getResource(); - List created = creator.create((List) params.getPassedIn(), params); - if (created !=null && created.size() == 1) + + if (resObj instanceof MultiPartResourceAction.Create) { - //return just one object instead of an array - return created.get(0); + MultiPartResourceAction.Create creator = (MultiPartResourceAction.Create) resObj; + return creator.create((FormData) params.getPassedIn(), params); + } else { - return wrapWithCollectionWithPaging(created); + EntityResourceAction.Create creator = (EntityResourceAction.Create) resObj; + List created = creator.create((List) params.getPassedIn(), params); + if (created != null && created.size() == 1) + { + // return just one object instead of an array + return created.get(0); + } + else + { + return wrapWithCollectionWithPaging(created); + } } + case RELATIONSHIP: if (resource.getMetaData().isDeleted(RelationshipResourceAction.Create.class)) { - throw new DeletedResourceException("(DELETE) "+resource.getMetaData().getUniqueId()); + throw new DeletedResourceException("(DELETE) " + resource.getMetaData().getUniqueId()); } - RelationshipResourceAction.Create createRelation = (RelationshipResourceAction.Create) resource.getResource(); - List createdRel = createRelation.create(params.getEntityId(), (List) params.getPassedIn(), params); - if (createdRel !=null && createdRel.size() == 1) + + if (resObj instanceof MultiPartRelationshipResourceAction.Create) { - //return just one object instead of an array - return createdRel.get(0); + MultiPartRelationshipResourceAction.Create creator = (MultiPartRelationshipResourceAction.Create) resObj; + return creator.create(params.getEntityId(), (FormData) params.getPassedIn(), params); } else { - return wrapWithCollectionWithPaging(createdRel); + RelationshipResourceAction.Create createRelation = (RelationshipResourceAction.Create) resource.getResource(); + List createdRel = createRelation.create(params.getEntityId(), (List) params.getPassedIn(), params); + if (createdRel != null && createdRel.size() == 1) + { + // return just one object instead of an array + return createdRel.get(0); + } + else + { + return wrapWithCollectionWithPaging(createdRel); + } } default: throw new UnsupportedResourceOperationException("POST not supported for Actions"); @@ -168,7 +208,7 @@ public class ResourceWebScriptPost extends AbstractResourceWebScript implements { if (created !=null && created.size() > 1) { - return CollectionWithPagingInfo.asPagedCollection(created.toArray()); + return CollectionWithPagingInfo.asPagedCollection(created.toArray()); } else { @@ -176,7 +216,6 @@ public class ResourceWebScriptPost extends AbstractResourceWebScript implements } } - @Override public void execute(final ResourceWithMetadata resource, final Params params, final ExecutionCallback executionCallback) { @@ -194,6 +233,7 @@ public class ResourceWebScriptPost extends AbstractResourceWebScript implements } }, false, true); } + @Override protected void setSuccessResponseStatus(WebScriptResponse res) { diff --git a/source/test-java/org/alfresco/rest/api/tests/AbstractBaseApiTest.java b/source/test-java/org/alfresco/rest/api/tests/AbstractBaseApiTest.java index 36c93b74d4..3f431b8507 100644 --- a/source/test-java/org/alfresco/rest/api/tests/AbstractBaseApiTest.java +++ b/source/test-java/org/alfresco/rest/api/tests/AbstractBaseApiTest.java @@ -61,6 +61,19 @@ public abstract class AbstractBaseApiTest extends EnterpriseTestApi return response; } + protected HttpResponse post(String url, String runAsUser, String body, String queryString, String contentType, int expectedStatus) throws Exception + { + publicApiClient.setRequestContext(new RequestContext(runAsUser)); + if (queryString != null) + { + url += queryString; + } + HttpResponse response = publicApiClient.post(getScope(), url, null, null, null, body, contentType); + checkStatus(expectedStatus, response.getStatusCode()); + + return response; + } + protected HttpResponse getAll(String url, String runAsUser, PublicApiClient.Paging paging, int expectedStatus) throws Exception { publicApiClient.setRequestContext(new RequestContext(runAsUser)); diff --git a/source/test-java/org/alfresco/rest/api/tests/client/PublicApiClient.java b/source/test-java/org/alfresco/rest/api/tests/client/PublicApiClient.java index 3e43a70e83..1be48079ed 100644 --- a/source/test-java/org/alfresco/rest/api/tests/client/PublicApiClient.java +++ b/source/test-java/org/alfresco/rest/api/tests/client/PublicApiClient.java @@ -440,6 +440,17 @@ public class PublicApiClient return response; } + public HttpResponse post(String scope, String entityCollectionName, Object entityId, String relationCollectionName, Object relationshipEntityId, + String body, String contentType) throws IOException + { + HttpResponse response = client.post(getRequestContext(), scope, entityCollectionName, entityId, relationCollectionName, + relationshipEntityId != null ? relationshipEntityId.toString() : null, body, contentType); + + logger.debug(response.toString()); + + return response; + } + public HttpResponse post(String urlSuffix, String body) throws IOException { HttpResponse response = client.post(getRequestContext(), urlSuffix, body); diff --git a/source/test-java/org/alfresco/rest/api/tests/client/PublicApiHttpClient.java b/source/test-java/org/alfresco/rest/api/tests/client/PublicApiHttpClient.java index edaa7aff0a..3804fc1b3a 100644 --- a/source/test-java/org/alfresco/rest/api/tests/client/PublicApiHttpClient.java +++ b/source/test-java/org/alfresco/rest/api/tests/client/PublicApiHttpClient.java @@ -450,20 +450,33 @@ public class PublicApiHttpClient PatchMethod req = new PatchMethod(url.toString()); return submitRequest(req, rq); } - - public HttpResponse post(final RequestContext rq, final String scope, final String entityCollectionName, final Object entityId, final String relationCollectionName, final Object relationshipEntityId, final String body) throws IOException - { - RestApiEndpoint endpoint = new RestApiEndpoint(rq.getNetworkId(), scope, entityCollectionName, entityId, relationCollectionName, relationshipEntityId, null); - String url = endpoint.getUrl(); - - PostMethod req = new PostMethod(url.toString()); - if(body != null) - { - StringRequestEntity requestEntity = new StringRequestEntity(body, "application/json", "UTF-8"); - req.setRequestEntity(requestEntity); - } - return submitRequest(req, rq); - } + + public HttpResponse post(final RequestContext rq, final String scope, final String entityCollectionName, final Object entityId, + final String relationCollectionName, final Object relationshipEntityId, final String body) throws IOException + { + return post(rq, scope, entityCollectionName, entityId, relationCollectionName, relationshipEntityId, body, "application/json"); + } + + public HttpResponse post(final RequestContext rq, final String scope, final String entityCollectionName, final Object entityId, + final String relationCollectionName, final Object relationshipEntityId, final String body, String contentType) throws IOException + { + RestApiEndpoint endpoint = new RestApiEndpoint(rq.getNetworkId(), scope, entityCollectionName, entityId, relationCollectionName, + relationshipEntityId, null); + String url = endpoint.getUrl(); + + PostMethod req = new PostMethod(url.toString()); + if (body != null) + { + if (contentType == null || contentType.isEmpty()) + { + contentType = "application/json"; + } + StringRequestEntity requestEntity = new StringRequestEntity(body, contentType, "UTF-8"); + req.setRequestEntity(requestEntity); + } + return submitRequest(req, rq); + } + public HttpResponse delete(final Class c, final RequestContext rq, final Object entityId, final Object relationshipEntityId) throws IOException { 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 new file mode 100644 index 0000000000..80c1479dcc --- /dev/null +++ b/source/test-java/org/alfresco/rest/api/tests/util/MultiPartBuilder.java @@ -0,0 +1,259 @@ +/* + * 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 . + */ + +package org.alfresco.rest.api.tests.util; + +import static org.junit.Assert.assertNotNull; + +import org.apache.commons.httpclient.methods.multipart.FilePart; +import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity; +import org.apache.commons.httpclient.methods.multipart.Part; +import org.apache.commons.httpclient.methods.multipart.StringPart; +import org.apache.commons.httpclient.params.HttpMethodParams; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * multipart/form-data builder. + * + * @author Jamal Kaabi-Mofrad + */ +public class MultiPartBuilder +{ + private FileData fileData; + private String siteId; + private String containerId; + private String destination; + private String uploadDirectory; + private String updateNodeRef; + private String description; + private String contentTypeQNameStr; + private List aspects; + private boolean majorVersion; + private boolean overwrite = true; // If a fileName clashes for a versionable file + + private MultiPartBuilder() + { + } + + private MultiPartBuilder(MultiPartBuilder that) + { + this.fileData = that.fileData; + this.siteId = that.siteId; + this.containerId = that.containerId; + this.destination = that.destination; + this.uploadDirectory = that.uploadDirectory; + this.updateNodeRef = that.updateNodeRef; + this.description = that.description; + this.contentTypeQNameStr = that.contentTypeQNameStr; + this.aspects = that.aspects; + this.majorVersion = that.majorVersion; + this.overwrite = that.overwrite; + } + + public static MultiPartBuilder create() + { + return new MultiPartBuilder(); + } + + public static MultiPartBuilder copy(MultiPartBuilder copy) + { + return new MultiPartBuilder(copy); + } + + public MultiPartBuilder setFileData(FileData fileData) + { + this.fileData = fileData; + return this; + } + + public MultiPartBuilder setSiteId(String siteId) + { + this.siteId = siteId; + return this; + } + + public MultiPartBuilder setContainerId(String containerId) + { + this.containerId = containerId; + return this; + } + + public MultiPartBuilder setDestination(String destination) + { + this.destination = destination; + return this; + } + + public MultiPartBuilder setUploadDirectory(String uploadDirectory) + { + this.uploadDirectory = uploadDirectory; + return this; + } + + public MultiPartBuilder setUpdateNoderef(String updateNodeRef) + { + this.updateNodeRef = updateNodeRef; + return this; + } + + public MultiPartBuilder setDescription(String description) + { + this.description = description; + return this; + } + + public MultiPartBuilder setContentTypeQNameStr(String contentTypeQNameStr) + { + this.contentTypeQNameStr = contentTypeQNameStr; + return this; + } + + public MultiPartBuilder setAspects(List aspects) + { + this.aspects = aspects; + return this; + } + + public MultiPartBuilder setMajorVersion(boolean majorVersion) + { + this.majorVersion = majorVersion; + return this; + } + + public MultiPartBuilder setOverwrite(boolean overwrite) + { + this.overwrite = overwrite; + return this; + } + + private String getAspects(List aspects) + { + if (aspects != null) + { + StringBuilder sb = new StringBuilder(aspects.size() * 2); + for (String str : aspects) + { + sb.append(str).append(','); + } + + sb.deleteCharAt(sb.length() - 1); // remove leading separator + + return sb.toString(); + + } + return null; + } + + public static class FileData + { + private final String fileName; + private final File file; + private final String mimetype; + + public FileData(String fileName, File file, String mimetype) + { + this.fileName = fileName; + this.file = file; + this.mimetype = mimetype; + } + + public String getFileName() + { + return fileName; + } + + public File getFile() + { + return file; + } + + public String getMimetype() + { + return mimetype; + } + } + + public static class MultiPartRequest + { + private final byte[] body; + private final String contentType; + private final long contentLength; + + public MultiPartRequest(byte[] body, String contentType, long contentLength) + { + this.body = body; + this.contentType = contentType; + this.contentLength = contentLength; + } + + public byte[] getBody() + { + return body; + } + + public String getContentType() + { + return contentType; + } + + public long getContentLength() + { + return contentLength; + } + } + + 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()); + addPartIfNotNull(parts, "siteid", siteId); + addPartIfNotNull(parts, "containerid", containerId); + addPartIfNotNull(parts, "destination", destination); + addPartIfNotNull(parts, "uploaddirectory", uploadDirectory); + addPartIfNotNull(parts, "updatenoderef", updateNodeRef); + addPartIfNotNull(parts, "description", description); + addPartIfNotNull(parts, "contenttype", contentTypeQNameStr); + addPartIfNotNull(parts, "aspects", getAspects(aspects)); + addPartIfNotNull(parts, "majorversion", Boolean.toString(majorVersion)); + addPartIfNotNull(parts, "overwrite", Boolean.toString(overwrite)); + + MultipartRequestEntity req = new MultipartRequestEntity(parts.toArray(new Part[parts.size()]), new HttpMethodParams()); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + req.writeRequest(os); + + return new MultiPartRequest(os.toByteArray(), req.getContentType(), req.getContentLength()); + } + + private void addPartIfNotNull(List list, String partName, String partValue) + { + if (partValue != null) + { + list.add(new StringPart(partName, partValue)); + } + } +} diff --git a/source/test-java/org/alfresco/rest/framework/tests/api/mocks/MultiPartTestEntityResource.java b/source/test-java/org/alfresco/rest/framework/tests/api/mocks/MultiPartTestEntityResource.java new file mode 100644 index 0000000000..ac3464a533 --- /dev/null +++ b/source/test-java/org/alfresco/rest/framework/tests/api/mocks/MultiPartTestEntityResource.java @@ -0,0 +1,22 @@ + +package org.alfresco.rest.framework.tests.api.mocks; + +import org.alfresco.rest.framework.resource.EntityResource; +import org.alfresco.rest.framework.resource.actions.interfaces.MultiPartResourceAction; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.springframework.extensions.webscripts.servlet.FormData; + +/** + * @author Jamal Kaabi-Mofrad + */ +@EntityResource(name = "multiparttest", title = "multi-part upload test") +public class MultiPartTestEntityResource + implements MultiPartResourceAction.Create +{ + + @Override + public MultiPartTestResponse create(FormData formData, Parameters parameters) + { + return new MultiPartTestResponse(formData.getParameters().get("filename")[0]); + } +} diff --git a/source/test-java/org/alfresco/rest/framework/tests/api/mocks/MultiPartTestRelationshipResource.java b/source/test-java/org/alfresco/rest/framework/tests/api/mocks/MultiPartTestRelationshipResource.java new file mode 100644 index 0000000000..5cd8d0f7c8 --- /dev/null +++ b/source/test-java/org/alfresco/rest/framework/tests/api/mocks/MultiPartTestRelationshipResource.java @@ -0,0 +1,24 @@ + +package org.alfresco.rest.framework.tests.api.mocks; + +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.MultiPartRelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.springframework.extensions.webscripts.servlet.FormData; + +/** + * @author Jamal Kaabi-Mofrad + */ +@RelationshipResource(name = "sheepUpload", entityResource = SheepEntityResource.class, title = "Sheep mulitpart upload") +public class MultiPartTestRelationshipResource + implements MultiPartRelationshipResourceAction.Create +{ + + @Override + public MultiPartTestResponse create(String entityResourceId, FormData formData, + Parameters parameters) + { + return new MultiPartTestResponse(formData.getParameters().get("filename")[0]); + } + +} diff --git a/source/test-java/org/alfresco/rest/framework/tests/api/mocks/MultiPartTestResponse.java b/source/test-java/org/alfresco/rest/framework/tests/api/mocks/MultiPartTestResponse.java new file mode 100644 index 0000000000..892d1ddb2d --- /dev/null +++ b/source/test-java/org/alfresco/rest/framework/tests/api/mocks/MultiPartTestResponse.java @@ -0,0 +1,23 @@ + +package org.alfresco.rest.framework.tests.api.mocks; + +/** + * Simple mock pojo for MultiPart response. + * + * @author Jamal Kaabi-Mofrad + */ +public class MultiPartTestResponse +{ + + private String fileName; + + public MultiPartTestResponse(String fileName) + { + this.fileName = fileName; + } + + public String getFileName() + { + return this.fileName; + } +} diff --git a/source/test-java/org/alfresco/rest/framework/tests/core/InspectorTests.java b/source/test-java/org/alfresco/rest/framework/tests/core/InspectorTests.java index 84b9f14461..99c8928a35 100644 --- a/source/test-java/org/alfresco/rest/framework/tests/core/InspectorTests.java +++ b/source/test-java/org/alfresco/rest/framework/tests/core/InspectorTests.java @@ -26,6 +26,9 @@ import org.alfresco.rest.framework.tests.api.mocks.Farmer; import org.alfresco.rest.framework.tests.api.mocks.GoatEntityResource; import org.alfresco.rest.framework.tests.api.mocks.Grass; import org.alfresco.rest.framework.tests.api.mocks.GrassEntityResource; +import org.alfresco.rest.framework.tests.api.mocks.MultiPartTestEntityResource; +import org.alfresco.rest.framework.tests.api.mocks.MultiPartTestRelationshipResource; +import org.alfresco.rest.framework.tests.api.mocks.MultiPartTestResponse; import org.alfresco.rest.framework.tests.api.mocks.Sheep; import org.alfresco.rest.framework.tests.api.mocks.SheepBlackSheepResource; import org.alfresco.rest.framework.tests.api.mocks.SheepEntityResource; @@ -83,7 +86,17 @@ public class InspectorTests assertTrue("FlockEntityResource supports PUT", metaData.supports(HttpMethod.PUT)); assertTrue("FlockEntityResource supports DELETE", metaData.supports(HttpMethod.DELETE)); assertTrue("FlockEntityResource does not support POST", !metaData.supports(HttpMethod.POST)); - + + metainfo = ResourceInspector.inspect(MultiPartTestEntityResource.class); + assertTrue("Must be one ResourceMetadata",metainfo.size()==1); + metaData = metainfo.get(0); + assertNotNull(metaData); + assertTrue("MultiPartTestEntityResource support POST", metaData.supports(HttpMethod.POST)); + assertFalse("MultiPartTestEntityResource does not supports GET", metaData.supports(HttpMethod.GET)); + assertFalse("MultiPartTestEntityResource does not supports PUT", metaData.supports(HttpMethod.PUT)); + assertFalse("MultiPartTestEntityResource does not supports DELETE", metaData.supports(HttpMethod.DELETE)); + assertTrue("MultiPartTestEntityResource must support MultiPartTestResponse", MultiPartTestResponse.class.equals(metaData.getObjectType(HttpMethod.POST))); + } @Test @@ -114,9 +127,18 @@ public class InspectorTests assertTrue("SheepBlackSheepResource supports DELETE", metaData.supports(HttpMethod.DELETE)); params = metaData.getParameters(HttpMethod.DELETE); assertTrue("DELETE method on a relations should have 2 url params.", params.size() == 2); - + + metainfo = ResourceInspector.inspect(MultiPartTestRelationshipResource.class); + assertTrue("Must be one ResourceMetadata",metainfo.size()==1); + metaData = metainfo.get(0); + assertNotNull(metaData); + assertTrue("MultiPartTestRelationshipResource support POST", metaData.supports(HttpMethod.POST)); + assertFalse("MultiPartTestRelationshipResource does not supports GET", metaData.supports(HttpMethod.GET)); + assertFalse("MultiPartTestRelationshipResource does not supports PUT", metaData.supports(HttpMethod.PUT)); + assertFalse("MultiPartTestRelationshipResource does not supports DELETE", metaData.supports(HttpMethod.DELETE)); + assertTrue("MultiPartTestRelationshipResource must support MultiPartTestResponse", MultiPartTestResponse.class.equals(metaData.getObjectType(HttpMethod.POST))); } - + @Test public void testInspectApi() { diff --git a/source/test-java/org/alfresco/rest/framework/tests/core/ParamsExtractorTests.java b/source/test-java/org/alfresco/rest/framework/tests/core/ParamsExtractorTests.java index a9f81e7f89..63d27d44ad 100644 --- a/source/test-java/org/alfresco/rest/framework/tests/core/ParamsExtractorTests.java +++ b/source/test-java/org/alfresco/rest/framework/tests/core/ParamsExtractorTests.java @@ -9,13 +9,18 @@ import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.io.File; import java.io.IOException; +import java.io.PrintWriter; import java.io.StringReader; import java.util.HashMap; import java.util.List; import java.util.Map; import org.alfresco.repo.content.MimetypeMap; +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.framework.core.ResourceLocator; import org.alfresco.rest.framework.core.ResourceMetadata; import org.alfresco.rest.framework.core.exceptions.UnsupportedResourceOperationException; @@ -30,12 +35,16 @@ import org.alfresco.rest.framework.webscripts.ResourceWebScriptDelete; import org.alfresco.rest.framework.webscripts.ResourceWebScriptGet; import org.alfresco.rest.framework.webscripts.ResourceWebScriptPost; import org.alfresco.rest.framework.webscripts.ResourceWebScriptPut; +import org.alfresco.util.TempFileProvider; import org.junit.BeforeClass; import org.junit.Test; import org.springframework.extensions.surf.util.Content; import org.springframework.extensions.webscripts.Match; import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.servlet.FormData; import org.springframework.http.HttpMethod; +import org.springframework.mock.web.MockHttpServletRequest; + /** * Tests extracting of params from req @@ -119,7 +128,7 @@ public class ParamsExtractorTests assertNotNull(params.getFilter()); assertTrue("Default filter is BeanPropertiesFilter.AllProperties", BeanPropertiesFilter.AllProperties.class.equals(params.getFilter().getClass())); - + Object passed = params.getPassedIn(); assertNotNull(passed); @@ -178,6 +187,65 @@ public class ParamsExtractorTests } } + @Test + public void testMultiPartPostExtractor() throws Exception + { + ResourceWebScriptPost extractor = new ResourceWebScriptPost(); + extractor.setJsonHelper(jsonHelper); + Map templateVars = new HashMap(); + + WebScriptRequest request = mock(WebScriptRequest.class); + when(request.getServiceMatch()).thenReturn(new Match(null, templateVars, null)); + + File file = TempFileProvider.createTempFile("ParamsExtractorTests-", ".txt"); + PrintWriter writer = new PrintWriter(file); + writer.println("Multipart Mock test."); + writer.close(); + + MultiPartRequest reqBody = MultiPartBuilder.create() + .setFileData(new FileData(file.getName(), file, MimetypeMap.MIMETYPE_TEXT_PLAIN)) + .build(); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", ""); + mockRequest.setContent(reqBody.getBody()); + mockRequest.setContentType(reqBody.getContentType()); + + when(request.getContentType()).thenReturn("multipart/form-data"); + when(request.parseContent()).thenReturn(new FormData(mockRequest)); + + Params params = extractor.extractParams(mockEntity(), request); + assertNotNull(params); + Object passed = params.getPassedIn(); + assertNotNull(passed); + assertTrue(FormData.class.isAssignableFrom(passed.getClass())); + FormData formData = (FormData) passed; + assertTrue(formData.getIsMultiPart()); + + assertNotNull(params.getStatus()); + assertFalse(params.getStatus().getRedirect()); + + // No entity id for POST + templateVars.put(ResourceLocator.ENTITY_ID, "1234"); + try + { + params = extractor.extractParams(mockEntity(), request); + fail("Should not get here. No entity id for POST"); + } + catch (UnsupportedResourceOperationException uoe) + { + assertNotNull(uoe); + } + + params = extractor.extractParams(mockRelationship(), request); + assertNotNull(params); + assertEquals("1234", params.getEntityId()); + passed = params.getPassedIn(); + assertNotNull(passed); + assertTrue(FormData.class.isAssignableFrom(passed.getClass())); + formData = (FormData) passed; + assertTrue(formData.getIsMultiPart()); + } + @Test public void testPutExtractor() throws IOException { diff --git a/source/test-java/org/alfresco/rest/framework/tests/core/SerializeTests.java b/source/test-java/org/alfresco/rest/framework/tests/core/SerializeTests.java index aca02d302e..15a5b6fc7d 100644 --- a/source/test-java/org/alfresco/rest/framework/tests/core/SerializeTests.java +++ b/source/test-java/org/alfresco/rest/framework/tests/core/SerializeTests.java @@ -11,7 +11,9 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; +import java.io.PrintWriter; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; @@ -19,8 +21,12 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +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.framework.Api; import org.alfresco.rest.framework.core.ResourceDictionaryBuilder; import org.alfresco.rest.framework.core.ResourceLookupDictionary; @@ -34,6 +40,7 @@ import org.alfresco.rest.framework.resource.EntityResource; import org.alfresco.rest.framework.resource.RelationshipResource; import org.alfresco.rest.framework.resource.actions.ActionExecutor.ExecutionCallback; import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction; +import org.alfresco.rest.framework.resource.actions.interfaces.MultiPartResourceAction; import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction.Read; import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction.ReadById; import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; @@ -52,6 +59,7 @@ import org.alfresco.rest.framework.tests.api.mocks3.SlimGoat; import org.alfresco.rest.framework.webscripts.AbstractResourceWebScript; import org.alfresco.rest.framework.webscripts.ResourceWebScriptHelper; import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.TempFileProvider; import org.apache.commons.lang.StringUtils; import org.codehaus.jackson.JsonGenerationException; import org.codehaus.jackson.JsonGenerator; @@ -69,7 +77,9 @@ import org.mockito.stubbing.Answer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.extensions.webscripts.Format; +import org.springframework.extensions.webscripts.servlet.FormData; import org.springframework.http.HttpMethod; +import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -133,6 +143,31 @@ public class SerializeTests out = writeResponse(helper.postProcessResponse(api,null, Params.valueOf("notUsed", null), resources)); assertTrue("There must be json output as List", StringUtils.startsWith(out, "{\"list\":")); } + + @Test + public void testInvokeMultiPartEntity() throws IOException + { + ResourceWithMetadata entityResource = locator.locateEntityResource(api,"multiparttest", HttpMethod.POST); + assertNotNull(entityResource); + MultiPartResourceAction.Create resource = (MultiPartResourceAction.Create) entityResource.getResource(); + + File file = TempFileProvider.createTempFile("ParamsExtractorTests-", ".txt"); + PrintWriter writer = new PrintWriter(file); + writer.println("Multipart Mock test2."); + writer.close(); + + MultiPartRequest reqBody = MultiPartBuilder.create() + .setFileData(new FileData(file.getName(), file, MimetypeMap.MIMETYPE_TEXT_PLAIN)) + .build(); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", ""); + mockRequest.setContent(reqBody.getBody()); + mockRequest.setContentType(reqBody.getContentType()); + + String out = writeResponse(helper.postProcessResponse(api,null, NOT_USED, resource.create(new FormData(mockRequest), NOT_USED))); + assertTrue("There must be json output", StringUtils.startsWith(out, "{\"entry\":")); + } + @Test public void testSerializeResponse() throws IOException { diff --git a/source/test-java/org/alfresco/rest/framework/tests/metadata/WriterTests.java b/source/test-java/org/alfresco/rest/framework/tests/metadata/WriterTests.java index f39520516e..7cecf9f937 100644 --- a/source/test-java/org/alfresco/rest/framework/tests/metadata/WriterTests.java +++ b/source/test-java/org/alfresco/rest/framework/tests/metadata/WriterTests.java @@ -59,7 +59,7 @@ public class WriterTests ResourceDictionary resourceDic = locator.getDictionary(); Map apiResources = resourceDic.getAllResources().get(api); String writtenOut = testWriter(defaultMetaWriter, apiResources.get("/sheep"), apiResources); - assertTrue(writtenOut.startsWith("{\"list\":{\"pagination\":{\"count\":4")); + assertTrue(writtenOut.startsWith("{\"list\":{\"pagination\":{\"count\":5")); // ResourceMetaDataWriter wadlWriter = new WebScriptOptionsMetaData(); // writtenOut = testWriter(wadlWriter, apiResources.get("/sheep"), apiResources);