JAMES-1788 Store uploaded content
Project: http://git-wip-us.apache.org/repos/asf/james-project/repo Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/57edcaa4 Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/57edcaa4 Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/57edcaa4 Branch: refs/heads/master Commit: 57edcaa4fa2021199c102c3c28ffd5fba179d3de Parents: 5fc0e78 Author: Raphael Ouazana <raphael.ouaz...@linagora.com> Authored: Fri Jul 1 17:33:31 2016 +0200 Committer: Raphael Ouazana <raphael.ouaz...@linagora.com> Committed: Mon Jul 4 14:57:38 2016 +0200 ---------------------------------------------------------------------- .../java/org/apache/james/jmap/JMAPModule.java | 1 + .../integration/cucumber/UploadStepdefs.java | 53 ++++++- .../resources/cucumber/UploadEndpoint.feature | 13 ++ .../org/apache/james/jmap/UploadHandler.java | 75 ++++++++++ .../org/apache/james/jmap/UploadServlet.java | 32 ++++- .../apache/james/jmap/model/UploadResponse.java | 144 +++++++++++++++++++ 6 files changed, 315 insertions(+), 3 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/james-project/blob/57edcaa4/server/container/guice/guice-common/src/main/java/org/apache/james/jmap/JMAPModule.java ---------------------------------------------------------------------- diff --git a/server/container/guice/guice-common/src/main/java/org/apache/james/jmap/JMAPModule.java b/server/container/guice/guice-common/src/main/java/org/apache/james/jmap/JMAPModule.java index bc27408..6fd9c1f 100644 --- a/server/container/guice/guice-common/src/main/java/org/apache/james/jmap/JMAPModule.java +++ b/server/container/guice/guice-common/src/main/java/org/apache/james/jmap/JMAPModule.java @@ -59,6 +59,7 @@ public class JMAPModule extends AbstractModule { install(new MethodsModule()); bind(JMAPServer.class).in(Scopes.SINGLETON); bind(RequestHandler.class).in(Scopes.SINGLETON); + bind(UploadHandler.class).in(Scopes.SINGLETON); bind(MailboxBasedHtmlTextExtractor.class).in(Scopes.SINGLETON); bind(HtmlTextExtractor.class).to(MailboxBasedHtmlTextExtractor.class); http://git-wip-us.apache.org/repos/asf/james-project/blob/57edcaa4/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/cucumber/UploadStepdefs.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/cucumber/UploadStepdefs.java b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/cucumber/UploadStepdefs.java index 10023ac..4d97af4 100644 --- a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/cucumber/UploadStepdefs.java +++ b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/cucumber/UploadStepdefs.java @@ -20,6 +20,8 @@ package org.apache.james.jmap.methods.integration.cucumber; import static com.jayway.restassured.RestAssured.with; +import static com.jayway.restassured.config.RestAssuredConfig.newConfig; +import static org.hamcrest.Matchers.equalTo; import java.io.BufferedInputStream; import java.io.InputStream; @@ -28,6 +30,9 @@ import javax.inject.Inject; import org.apache.james.jmap.api.access.AccessToken; +import com.google.common.base.Charsets; +import com.jayway.restassured.config.EncoderConfig; +import com.jayway.restassured.config.RestAssuredConfig; import com.jayway.restassured.http.ContentType; import com.jayway.restassured.response.Response; import com.jayway.restassured.specification.RequestSpecification; @@ -38,6 +43,8 @@ import cucumber.runtime.java.guice.ScenarioScoped; @ScenarioScoped public class UploadStepdefs { + private static final RestAssuredConfig NO_CHARSET = newConfig().encoderConfig(EncoderConfig.encoderConfig().appendDefaultContentCharsetToContentTypeIfUndefined(false)); + private static final String _1M_ZEROED_FILE_BLOB_ID = "3b71f43ff30f4b15b5cd85dd9e95ebc7e84eb5a3"; private static final int _1M = 1024 * 1024; private static final int _10M = 10 * _1M; @@ -57,6 +64,23 @@ public class UploadStepdefs { with.header("Authorization", accessToken.serialize()); } response = with + .config(NO_CHARSET) + .contentType(ContentType.BINARY) + .content(new BufferedInputStream(new ZeroedInputStream(_1M), _1M)) + .post("/upload"); + } + + @When("^\"([^\"]*)\" upload a content without content type$") + public void userUploadContentWithoutContentType(String username) throws Throwable { + AccessToken accessToken = userStepdefs.tokenByUser.get(username); + RequestSpecification with = with(); + if (accessToken != null) { + with.header("Authorization", accessToken.serialize()); + } + response = with + .config(NO_CHARSET) + .contentType("") + .content("some text".getBytes(Charsets.UTF_8)) .post("/upload"); } @@ -96,6 +120,12 @@ public class UploadStepdefs { .statusCode(201); } + @Then("^the user should receive bad request response$") + public void httpBadRequestStatus() throws Throwable { + response.then() + .statusCode(400); + } + @Then("^the user should receive a not authorized response$") public void httpUnauthorizedStatus() throws Exception { response.then() @@ -108,6 +138,28 @@ public class UploadStepdefs { .statusCode(413); } + @Then("^the user should receive a specified JSON content$") + public void jsonResponse() throws Exception { + response.then() + .contentType(ContentType.JSON) + .body("blobId", equalTo(_1M_ZEROED_FILE_BLOB_ID)) + .body("type", equalTo("application/octet-stream")) + .body("size", equalTo(_1M)); + } + + @Then("^\"([^\"]*)\" should be able to retrieve the content$") + public void contentShouldBeRetrievable(String username) throws Exception { + AccessToken accessToken = userStepdefs.tokenByUser.get(username); + RequestSpecification with = with(); + if (accessToken != null) { + with.header("Authorization", accessToken.serialize()); + } + with + .get("/download/" + _1M_ZEROED_FILE_BLOB_ID) + .then() + .statusCode(200); + } + public static class ZeroedInputStream extends InputStream { public static final int RETURNED_VALUE = 0; @@ -128,5 +180,4 @@ public class UploadStepdefs { return -1; } } - } http://git-wip-us.apache.org/repos/asf/james-project/blob/57edcaa4/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/resources/cucumber/UploadEndpoint.feature ---------------------------------------------------------------------- diff --git a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/resources/cucumber/UploadEndpoint.feature b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/resources/cucumber/UploadEndpoint.feature index 1379ddf..4edb949 100644 --- a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/resources/cucumber/UploadEndpoint.feature +++ b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/resources/cucumber/UploadEndpoint.feature @@ -26,3 +26,16 @@ Feature: An upload endpoint should be available to upload contents Scenario: Uploading a content being authenticated When "usern...@domain.tld" upload a content Then the user should receive a created response + + Scenario: Uploading a content without content type should be denied + When "usern...@domain.tld" upload a content without content type + Then the user should receive bad request response + + Scenario: Uploading a content, the content should be retrievable + When "usern...@domain.tld" upload a content + Then "usern...@domain.tld" should be able to retrieve the content + + Scenario: Uploading a content, the server should respond specified JSON + When "usern...@domain.tld" upload a content + Then the user should receive a specified JSON content + \ No newline at end of file http://git-wip-us.apache.org/repos/asf/james-project/blob/57edcaa4/server/protocols/jmap/src/main/java/org/apache/james/jmap/UploadHandler.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/UploadHandler.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/UploadHandler.java new file mode 100644 index 0000000..1bd5af3 --- /dev/null +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/UploadHandler.java @@ -0,0 +1,75 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ +package org.apache.james.jmap; + +import static javax.servlet.http.HttpServletResponse.SC_CREATED; + +import java.io.IOException; +import java.io.InputStream; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletResponse; + +import org.apache.james.jmap.json.ObjectMapperFactory; +import org.apache.james.jmap.model.UploadResponse; +import org.apache.james.mailbox.MailboxSession; +import org.apache.james.mailbox.exception.MailboxException; +import org.apache.james.mailbox.store.MailboxSessionMapperFactory; +import org.apache.james.mailbox.store.mail.AttachmentMapper; +import org.apache.james.mailbox.store.mail.model.Attachment; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.io.ByteStreams; + +public class UploadHandler { + private final MailboxSessionMapperFactory mailboxSessionMapperFactory; + private final ObjectMapper objectMapper; + + @Inject + private UploadHandler(MailboxSessionMapperFactory mailboxSessionMapperFactory, ObjectMapperFactory objectMapperFactory) { + this.mailboxSessionMapperFactory = mailboxSessionMapperFactory; + this.objectMapper = objectMapperFactory.forWriting(); + } + + public void handle(String contentType, InputStream content, HttpServletResponse response) throws IOException, MailboxException { + UploadResponse storedContent = uploadContent(contentType, content); + buildResponse(response, storedContent); + } + + private UploadResponse uploadContent(String contentType, InputStream inputStream) throws IOException, MailboxException { + MailboxSession session = null; + AttachmentMapper attachmentMapper = mailboxSessionMapperFactory.createAttachmentMapper(session); + Attachment attachment = Attachment.builder() + .bytes(ByteStreams.toByteArray(inputStream)) + .type(contentType) + .build(); + attachmentMapper.storeAttachment(attachment); + return UploadResponse.builder() + .blobId(attachment.getAttachmentId().getId()) + .type(attachment.getType()) + .size(attachment.getSize()) + .build(); + } + + private void buildResponse(HttpServletResponse resp, UploadResponse storedContent) throws IOException { + resp.setContentType(JMAPServlet.JSON_CONTENT_TYPE_UTF8); + resp.setStatus(SC_CREATED); + objectMapper.writeValue(resp.getOutputStream(), storedContent); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/57edcaa4/server/protocols/jmap/src/main/java/org/apache/james/jmap/UploadServlet.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/UploadServlet.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/UploadServlet.java index c7e9ed0..a0635a3 100644 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/UploadServlet.java +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/UploadServlet.java @@ -18,17 +18,45 @@ ****************************************************************/ package org.apache.james.jmap; -import static javax.servlet.http.HttpServletResponse.SC_CREATED; +import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; +import java.io.IOException; + +import javax.inject.Inject; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.james.mailbox.exception.MailboxException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Strings; + public class UploadServlet extends HttpServlet { + private static final Logger LOGGER = LoggerFactory.getLogger(UploadServlet.class); + + private final UploadHandler uploadHandler; + + @Inject + private UploadServlet(UploadHandler uploadHandler) { + this.uploadHandler = uploadHandler; + } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException { - resp.setStatus(SC_CREATED); + String contentType = req.getContentType(); + if (Strings.isNullOrEmpty(contentType)) { + resp.setStatus(SC_BAD_REQUEST); + } else { + try { + uploadHandler.handle(contentType, req.getInputStream(), resp); + } catch (IOException | MailboxException e) { + LOGGER.error("Error while uploading content", e); + resp.setStatus(SC_INTERNAL_SERVER_ERROR); + } + } } } http://git-wip-us.apache.org/repos/asf/james-project/blob/57edcaa4/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/UploadResponse.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/UploadResponse.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/UploadResponse.java new file mode 100644 index 0000000..1868122 --- /dev/null +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/UploadResponse.java @@ -0,0 +1,144 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.jmap.model; + +import java.time.ZonedDateTime; +import java.util.Optional; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; + +@JsonDeserialize(builder = UploadResponse.Builder.class) +public class UploadResponse { + + public static Builder builder() { + return new Builder(); + } + + @JsonPOJOBuilder(withPrefix = "") + public static class Builder { + private String accountId; + private String blobId; + private String type; + private Long size; + private ZonedDateTime expires; + + public Builder accountId(String accountId) { + this.accountId = accountId; + return this; + } + + public Builder blobId(String blobId) { + this.blobId = blobId; + return this; + } + + public Builder type(String type) { + this.type = type; + return this; + } + + public Builder size(long size) { + this.size = size; + return this; + } + + public Builder expires(ZonedDateTime expires) { + this.expires = expires; + return this; + } + + public UploadResponse build() { + Preconditions.checkState(!Strings.isNullOrEmpty(blobId), "'blobId' is mandatory"); + Preconditions.checkState(!Strings.isNullOrEmpty(type), "'type' is mandatory"); + Preconditions.checkState(size != null, "'size' is mandatory"); + return new UploadResponse(Optional.ofNullable(accountId), blobId, type, size, Optional.ofNullable(expires)); + } + } + + private final Optional<String> accountId; + private final String blobId; + private final String type; + private final Long size; + private final Optional<ZonedDateTime> expires; + + @VisibleForTesting UploadResponse(Optional<String> accountId, String blobId, String type, long size, Optional<ZonedDateTime> expires) { + this.accountId = accountId; + this.blobId = blobId; + this.type = type; + this.size = size; + this.expires = expires; + } + + public Optional<String> getAccountId() { + return accountId; + } + + public String getBlobId() { + return blobId; + } + + public String getType() { + return type; + } + + public long getSize() { + return size; + } + + public Optional<ZonedDateTime> getExpires() { + return expires; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof UploadResponse) { + UploadResponse other = (UploadResponse) obj; + return Objects.equal(accountId, accountId) + && Objects.equal(blobId, other.blobId) + && Objects.equal(type, other.type) + && Objects.equal(size, other.size) + && Objects.equal(expires, other.expires); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hashCode(accountId, blobId, type, size, expires); + } + + @Override + public String toString() { + return MoreObjects + .toStringHelper(this) + .add("accountId", accountId) + .add("blobId", blobId) + .add("type", type) + .add("size", size) + .add("expires", expires) + .toString(); + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: server-dev-unsubscr...@james.apache.org For additional commands, e-mail: server-dev-h...@james.apache.org