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 rc...@apache.org on 2020/03/18 03:03:44 UTC
[james-project] 11/15: JAMES-3078 JMAPApiRoutes
This is an automated email from the ASF dual-hosted git repository.
rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 202120510e44c49e55ebe6719ae836a6cd2843bb
Author: Rene Cordier <rc...@linagora.com>
AuthorDate: Wed Mar 11 13:47:57 2020 +0700
JAMES-3078 JMAPApiRoutes
---
.../org/apache/james/jmap/http/JMAPApiRoutes.java | 145 +++++++++++++++++++
.../apache/james/jmap/http/JMAPApiRoutesTest.java | 158 +++++++++++++++++++++
2 files changed, 303 insertions(+)
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/JMAPApiRoutes.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/JMAPApiRoutes.java
new file mode 100644
index 0000000..5451a8f
--- /dev/null
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/JMAPApiRoutes.java
@@ -0,0 +1,145 @@
+/****************************************************************
+ * 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.http;
+
+import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
+import static io.netty.handler.codec.http.HttpResponseStatus.OK;
+import static org.apache.james.jmap.HttpConstants.JSON_CONTENT_TYPE;
+import static org.apache.james.jmap.http.JMAPUrls.JMAP;
+
+import java.io.IOException;
+
+import javax.inject.Inject;
+
+import org.apache.james.jmap.JMAPRoutes;
+import org.apache.james.jmap.draft.exceptions.BadRequestException;
+import org.apache.james.jmap.draft.exceptions.InternalErrorException;
+import org.apache.james.jmap.draft.exceptions.UnauthorizedException;
+import org.apache.james.jmap.draft.methods.RequestHandler;
+import org.apache.james.jmap.draft.model.AuthenticatedRequest;
+import org.apache.james.jmap.draft.model.InvocationRequest;
+import org.apache.james.jmap.draft.model.InvocationResponse;
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.metrics.api.MetricFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.core.JsonParser.Feature;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Schedulers;
+import reactor.netty.http.server.HttpServerRequest;
+import reactor.netty.http.server.HttpServerResponse;
+import reactor.netty.http.server.HttpServerRoutes;
+
+public class JMAPApiRoutes implements JMAPRoutes {
+ public static final Logger LOGGER = LoggerFactory.getLogger(JMAPApiRoutes.class);
+
+ private final ObjectMapper objectMapper;
+ private final RequestHandler requestHandler;
+ private final MetricFactory metricFactory;
+ private final AuthenticationReactiveFilter authenticationReactiveFilter;
+ private final UserProvisioner userProvisioner;
+ private final DefaultMailboxesReactiveProvisioner defaultMailboxesProvisioner;
+
+ @Inject
+ public JMAPApiRoutes(RequestHandler requestHandler, MetricFactory metricFactory, AuthenticationReactiveFilter authenticationReactiveFilter, UserProvisioner userProvisioner, DefaultMailboxesReactiveProvisioner defaultMailboxesProvisioner) {
+ this.requestHandler = requestHandler;
+ this.metricFactory = metricFactory;
+ this.authenticationReactiveFilter = authenticationReactiveFilter;
+ this.userProvisioner = userProvisioner;
+ this.defaultMailboxesProvisioner = defaultMailboxesProvisioner;
+ this.objectMapper = new ObjectMapper();
+ objectMapper.configure(Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);
+ }
+
+ @Override
+ public Logger logger() {
+ return LOGGER;
+ }
+
+ @Override
+ public HttpServerRoutes define(HttpServerRoutes builder) {
+ return builder.post(JMAP, this::post)
+ .options(JMAP, CORS_CONTROL);
+ }
+
+ private Mono<Void> post(HttpServerRequest request, HttpServerResponse response) {
+ return authenticationReactiveFilter.authenticate(request)
+ .flatMap(session -> Flux.merge(
+ userProvisioner.provisionUser(session),
+ defaultMailboxesProvisioner.createMailboxesIfNeeded(session))
+ .then()
+ .thenReturn(session))
+ .flatMap(session -> Mono.from(metricFactory.runPublishingTimerMetric("JMAP-request",
+ post(request, response, session))))
+ .onErrorResume(BadRequestException.class, e -> handleBadRequest(response, e))
+ .onErrorResume(UnauthorizedException.class, e -> handleAuthenticationFailure(response, e))
+ .onErrorResume(e -> handleInternalError(response, e))
+ .subscribeOn(Schedulers.elastic());
+ }
+
+ private Mono<Void> post(HttpServerRequest request, HttpServerResponse response, MailboxSession session) {
+ Flux<Object[]> responses =
+ requestAsJsonStream(request)
+ .map(InvocationRequest::deserialize)
+ .map(invocationRequest -> AuthenticatedRequest.decorate(invocationRequest, session))
+ .concatMap(this::handle)
+ .map(InvocationResponse::asProtocolSpecification);
+
+ return sendResponses(response, responses);
+ }
+
+ private Mono<Void> sendResponses(HttpServerResponse response, Flux<Object[]> responses) {
+ return responses.collectList()
+ .map(objects -> {
+ try {
+ return objectMapper.writeValueAsString(objects);
+ } catch (JsonProcessingException e) {
+ throw new InternalErrorException("error serialising JMAP API response json");
+ }
+ })
+ .flatMap(json -> response.status(OK)
+ .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
+ .sendString(Mono.just(json))
+ .then());
+ }
+
+ private Flux<? extends InvocationResponse> handle(AuthenticatedRequest request) {
+ return Mono.fromCallable(() -> requestHandler.handle(request))
+ .flatMapMany(Flux::fromStream)
+ .subscribeOn(Schedulers.elastic());
+ }
+
+ private Flux<JsonNode[]> requestAsJsonStream(HttpServerRequest req) {
+ return req.receive().aggregate().asInputStream()
+ .map(inputStream -> {
+ try {
+ return objectMapper.readValue(inputStream, JsonNode[][].class);
+ } catch (IOException e) {
+ throw new BadRequestException("Error deserializing JSON", e);
+ }
+ })
+ .flatMapMany(Flux::fromArray);
+ }
+}
diff --git a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/JMAPApiRoutesTest.java b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/JMAPApiRoutesTest.java
new file mode 100644
index 0000000..c0b97d2
--- /dev/null
+++ b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/JMAPApiRoutesTest.java
@@ -0,0 +1,158 @@
+/****************************************************************
+ * 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.http;
+
+import static io.restassured.RestAssured.given;
+import static io.restassured.config.EncoderConfig.encoderConfig;
+import static io.restassured.config.RestAssuredConfig.newConfig;
+import static org.apache.james.jmap.http.JMAPUrls.JMAP;
+import static org.hamcrest.Matchers.equalTo;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.nio.charset.StandardCharsets;
+import java.util.stream.Stream;
+
+import org.apache.james.jmap.draft.methods.ErrorResponse;
+import org.apache.james.jmap.draft.methods.Method;
+import org.apache.james.jmap.draft.methods.RequestHandler;
+import org.apache.james.jmap.draft.model.InvocationResponse;
+import org.apache.james.jmap.draft.model.MethodCallId;
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.metrics.tests.RecordingMetricFactory;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import io.restassured.RestAssured;
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.http.ContentType;
+import reactor.core.publisher.Mono;
+import reactor.netty.DisposableServer;
+import reactor.netty.http.server.HttpServer;
+
+public class JMAPApiRoutesTest {
+ private static final int RANDOM_PORT = 0;
+
+ private DisposableServer server;
+ private RequestHandler requestHandler;
+ private AuthenticationReactiveFilter mockedAuthFilter;
+ private UserProvisioner mockedUserProvisionner;
+ private DefaultMailboxesReactiveProvisioner mockedMailboxesProvisionner;
+
+ @Before
+ public void setup() throws Exception {
+ requestHandler = mock(RequestHandler.class);
+ mockedAuthFilter = mock(AuthenticationReactiveFilter.class);
+ mockedUserProvisionner = mock(UserProvisioner.class);
+ mockedMailboxesProvisionner = mock(DefaultMailboxesReactiveProvisioner.class);
+
+ JMAPApiRoutes jmapApiRoutes = new JMAPApiRoutes(requestHandler, new RecordingMetricFactory(),
+ mockedAuthFilter, mockedUserProvisionner, mockedMailboxesProvisionner);
+
+ server = HttpServer.create()
+ .port(RANDOM_PORT)
+ .route(jmapApiRoutes::define)
+ .bindNow();
+
+ RestAssured.requestSpecification = new RequestSpecBuilder()
+ .setContentType(ContentType.JSON)
+ .setAccept(ContentType.JSON)
+ .setConfig(newConfig().encoderConfig(encoderConfig().defaultContentCharset(StandardCharsets.UTF_8)))
+ .setPort(server.port())
+ .setBasePath(JMAP)
+ .build();
+
+ when(mockedAuthFilter.authenticate(any()))
+ .thenReturn(Mono.just(mock(MailboxSession.class)));
+ when(mockedUserProvisionner.provisionUser(any()))
+ .thenReturn(Mono.empty());
+ when(mockedMailboxesProvisionner.createMailboxesIfNeeded(any()))
+ .thenReturn(Mono.empty());
+ }
+
+ @After
+ public void teardown() {
+ server.disposeNow();
+ }
+
+ @Test
+ public void mustReturnBadRequestOnMalformedRequest() {
+ String missingAnOpeningBracket = "[\"getAccounts\", {\"state\":false}, \"#0\"]]";
+
+ given()
+ .body(missingAnOpeningBracket)
+ .when()
+ .post()
+ .then()
+ .statusCode(400);
+ }
+
+ @Test
+ public void mustReturnInvalidArgumentOnInvalidState() throws Exception {
+ ObjectNode json = new ObjectNode(new JsonNodeFactory(false));
+ json.put("type", "invalidArgument");
+
+ when(requestHandler.handle(any()))
+ .thenReturn(Stream.of(new InvocationResponse(ErrorResponse.ERROR_METHOD, json, MethodCallId.of("#0"))));
+
+ given()
+ .body("[[\"getAccounts\", {\"state\":false}, \"#0\"]]")
+ .when()
+ .post()
+ .then()
+ .statusCode(200)
+ .body(equalTo("[[\"error\",{\"type\":\"invalidArgument\"},\"#0\"]]"));
+ }
+
+ @Test
+ public void mustReturnAccountsOnValidRequest() throws Exception {
+ ObjectNode json = new ObjectNode(new JsonNodeFactory(false));
+ json.put("state", "f6a7e214");
+ ArrayNode arrayNode = json.putArray("list");
+ ObjectNode list = new ObjectNode(new JsonNodeFactory(false));
+ list.put("id", "6asf5");
+ list.put("name", "roger@barcamp");
+ arrayNode.add(list);
+
+ when(requestHandler.handle(any()))
+ .thenReturn(Stream.of(new InvocationResponse(Method.Response.name("accounts"), json, MethodCallId.of("#0"))));
+
+ given()
+ .body("[[\"getAccounts\", {}, \"#0\"]]")
+ .when()
+ .post()
+ .then()
+ .statusCode(200)
+ .body(equalTo("[[\"accounts\",{" +
+ "\"state\":\"f6a7e214\"," +
+ "\"list\":[" +
+ "{" +
+ "\"id\":\"6asf5\"," +
+ "\"name\":\"roger@barcamp\"" +
+ "}" +
+ "]" +
+ "},\"#0\"]]"));
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org