You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by ad...@apache.org on 2016/07/04 14:46:20 UTC
[5/5] james-project git commit: JAMES-1788 Store uploaded content
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 <ra...@linagora.com>
Authored: Fri Jul 1 17:33:31 2016 +0200
Committer: Raphael Ouazana <ra...@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 "username@domain.tld" upload a content
Then the user should receive a created response
+
+ Scenario: Uploading a content without content type should be denied
+ When "username@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 "username@domain.tld" upload a content
+ Then "username@domain.tld" should be able to retrieve the content
+
+ Scenario: Uploading a content, the server should respond specified JSON
+ When "username@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-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org