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 6431f55128e974f51826d58899e18a65adbc0f19 Author: Rene Cordier <rcord...@linagora.com> AuthorDate: Wed Mar 18 17:44:49 2020 +0700 JAMES-3092 Instauring the Y structure with jmap-draft --- .../james/jmap/http/AuthenticationRoutes.java | 19 ++- .../org/apache/james/jmap/http/DownloadRoutes.java | 22 +-- .../org/apache/james/jmap/http/JMAPApiRoutes.java | 14 +- .../org/apache/james/jmap/http/UploadRoutes.java | 14 +- .../apache/james/jmap/http/JMAPApiRoutesTest.java | 9 +- server/protocols/jmap/pom.xml | 4 + .../main/java/org/apache/james/jmap/Endpoint.java | 6 + .../james/jmap/{Endpoint.java => JMAPRoute.java} | 45 +++---- .../java/org/apache/james/jmap/JMAPRoutes.java | 11 +- .../java/org/apache/james/jmap/JMAPServer.java | 77 ++++++++++- .../src/main/java/org/apache/james/jmap/Verb.java | 19 ++- .../java/org/apache/james/jmap/JMAPServerTest.java | 150 +++++++++++++++++++++ 12 files changed, 334 insertions(+), 56 deletions(-) diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/AuthenticationRoutes.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/AuthenticationRoutes.java index 4c33bc9..ed06cbf 100644 --- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/AuthenticationRoutes.java +++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/AuthenticationRoutes.java @@ -37,12 +37,17 @@ import static org.apache.james.util.ReactorUtils.logOnError; import java.io.IOException; import java.util.Objects; +import java.util.stream.Stream; import javax.inject.Inject; import org.apache.james.core.Username; +import org.apache.james.jmap.Endpoint; +import org.apache.james.jmap.JMAPRoute; import org.apache.james.jmap.JMAPRoutes; import org.apache.james.jmap.JMAPUrls; +import org.apache.james.jmap.Verb; +import org.apache.james.jmap.Version; import org.apache.james.jmap.api.access.AccessToken; import org.apache.james.jmap.draft.api.AccessTokenManager; import org.apache.james.jmap.draft.api.SimpleTokenFactory; @@ -69,7 +74,6 @@ 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 AuthenticationRoutes implements JMAPRoutes { private static final Logger LOGGER = LoggerFactory.getLogger(AuthenticationRoutes.class); @@ -102,12 +106,13 @@ public class AuthenticationRoutes implements JMAPRoutes { } @Override - public HttpServerRoutes define(HttpServerRoutes builder) { - return builder - .post(AUTHENTICATION, JMAPRoutes.corsHeaders(this::post)) - .get(AUTHENTICATION, JMAPRoutes.corsHeaders(this::returnEndPointsResponse)) - .delete(AUTHENTICATION, JMAPRoutes.corsHeaders(this::delete)) - .options(AUTHENTICATION, CORS_CONTROL); + public Stream<JMAPRoute> routes() { + return Stream.of( + new JMAPRoute(new Endpoint(Verb.POST, AUTHENTICATION), Version.DRAFT, JMAPRoutes.corsHeaders(this::post)), + new JMAPRoute(new Endpoint(Verb.GET, AUTHENTICATION), Version.DRAFT, JMAPRoutes.corsHeaders(this::returnEndPointsResponse)), + new JMAPRoute(new Endpoint(Verb.DELETE, AUTHENTICATION), Version.DRAFT, JMAPRoutes.corsHeaders(this::delete)), + new JMAPRoute(new Endpoint(Verb.OPTIONS, AUTHENTICATION), Version.DRAFT, CORS_CONTROL) + ); } private Mono<Void> post(HttpServerRequest request, HttpServerResponse response) { diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/DownloadRoutes.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/DownloadRoutes.java index c32edc6..0f97b15 100644 --- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/DownloadRoutes.java +++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/DownloadRoutes.java @@ -33,10 +33,15 @@ import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.Optional; +import java.util.stream.Stream; import javax.inject.Inject; +import org.apache.james.jmap.Endpoint; +import org.apache.james.jmap.JMAPRoute; import org.apache.james.jmap.JMAPRoutes; +import org.apache.james.jmap.Verb; +import org.apache.james.jmap.Version; import org.apache.james.jmap.draft.api.SimpleTokenFactory; import org.apache.james.jmap.draft.exceptions.BadRequestException; import org.apache.james.jmap.draft.exceptions.InternalErrorException; @@ -65,7 +70,6 @@ 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 DownloadRoutes implements JMAPRoutes { private static final Logger LOGGER = LoggerFactory.getLogger(DownloadRoutes.class); @@ -95,13 +99,15 @@ public class DownloadRoutes implements JMAPRoutes { } @Override - public HttpServerRoutes define(HttpServerRoutes builder) { - return builder.post(DOWNLOAD_FROM_ID, JMAPRoutes.corsHeaders(this::postFromId)) - .get(DOWNLOAD_FROM_ID, JMAPRoutes.corsHeaders(this::getFromId)) - .post(DOWNLOAD_FROM_ID_AND_NAME, JMAPRoutes.corsHeaders(this::postFromIdAndName)) - .get(DOWNLOAD_FROM_ID_AND_NAME, JMAPRoutes.corsHeaders(this::getFromIdAndName)) - .options(DOWNLOAD_FROM_ID, CORS_CONTROL) - .options(DOWNLOAD_FROM_ID_AND_NAME, CORS_CONTROL); + public Stream<JMAPRoute> routes() { + return Stream.of( + new JMAPRoute(new Endpoint(Verb.POST, DOWNLOAD_FROM_ID), Version.DRAFT, JMAPRoutes.corsHeaders(this::postFromId)), + new JMAPRoute(new Endpoint(Verb.GET, DOWNLOAD_FROM_ID), Version.DRAFT, JMAPRoutes.corsHeaders(this::getFromId)), + new JMAPRoute(new Endpoint(Verb.POST, DOWNLOAD_FROM_ID_AND_NAME), Version.DRAFT, JMAPRoutes.corsHeaders(this::postFromIdAndName)), + new JMAPRoute(new Endpoint(Verb.GET, DOWNLOAD_FROM_ID_AND_NAME), Version.DRAFT, JMAPRoutes.corsHeaders(this::getFromIdAndName)), + new JMAPRoute(new Endpoint(Verb.OPTIONS, DOWNLOAD_FROM_ID), Version.DRAFT, CORS_CONTROL), + new JMAPRoute(new Endpoint(Verb.OPTIONS, DOWNLOAD_FROM_ID_AND_NAME), Version.DRAFT, CORS_CONTROL) + ); } private Mono<Void> postFromId(HttpServerRequest request, HttpServerResponse response) { 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 index f8dc7ca..82b8669 100644 --- 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 @@ -27,10 +27,15 @@ import static org.apache.james.jmap.http.LoggingHelper.jmapContext; import static org.apache.james.util.ReactorUtils.logOnError; import java.io.IOException; +import java.util.stream.Stream; import javax.inject.Inject; +import org.apache.james.jmap.Endpoint; +import org.apache.james.jmap.JMAPRoute; import org.apache.james.jmap.JMAPRoutes; +import org.apache.james.jmap.Verb; +import org.apache.james.jmap.Version; import org.apache.james.jmap.draft.exceptions.BadRequestException; import org.apache.james.jmap.draft.exceptions.InternalErrorException; import org.apache.james.jmap.draft.exceptions.UnauthorizedException; @@ -53,7 +58,6 @@ 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); @@ -82,9 +86,11 @@ public class JMAPApiRoutes implements JMAPRoutes { } @Override - public HttpServerRoutes define(HttpServerRoutes builder) { - return builder.post(JMAP, JMAPRoutes.corsHeaders(this::post)) - .options(JMAP, CORS_CONTROL); + public Stream<JMAPRoute> routes() { + return Stream.of( + new JMAPRoute(new Endpoint(Verb.POST, JMAP), Version.DRAFT, JMAPRoutes.corsHeaders(this::post)), + new JMAPRoute(new Endpoint(Verb.OPTIONS, JMAP), Version.DRAFT, CORS_CONTROL) + ); } private Mono<Void> post(HttpServerRequest request, HttpServerResponse response) { diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/UploadRoutes.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/UploadRoutes.java index 2465b0b..253a076 100644 --- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/UploadRoutes.java +++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/UploadRoutes.java @@ -31,10 +31,15 @@ import static org.apache.james.util.ReactorUtils.logOnError; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; +import java.util.stream.Stream; import javax.inject.Inject; +import org.apache.james.jmap.Endpoint; +import org.apache.james.jmap.JMAPRoute; import org.apache.james.jmap.JMAPRoutes; +import org.apache.james.jmap.Verb; +import org.apache.james.jmap.Version; import org.apache.james.jmap.draft.exceptions.BadRequestException; import org.apache.james.jmap.draft.exceptions.InternalErrorException; import org.apache.james.jmap.draft.exceptions.UnauthorizedException; @@ -56,7 +61,6 @@ 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 UploadRoutes implements JMAPRoutes { private static final Logger LOGGER = LoggerFactory.getLogger(UploadRoutes.class); @@ -84,9 +88,11 @@ public class UploadRoutes implements JMAPRoutes { } @Override - public HttpServerRoutes define(HttpServerRoutes builder) { - return builder.post(UPLOAD, JMAPRoutes.corsHeaders(this::post)) - .options(UPLOAD, CORS_CONTROL); + public Stream<JMAPRoute> routes() { + return Stream.of( + new JMAPRoute(new Endpoint(Verb.POST, UPLOAD), Version.DRAFT, JMAPRoutes.corsHeaders(this::post)), + new JMAPRoute(new Endpoint(Verb.OPTIONS, UPLOAD), Version.DRAFT, CORS_CONTROL) + ); } private Mono<Void> post(HttpServerRequest request, HttpServerResponse response) { 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 index 7973c98..f688099 100644 --- 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 @@ -31,6 +31,8 @@ import static org.mockito.Mockito.when; import java.nio.charset.StandardCharsets; import org.apache.james.core.Username; +import org.apache.james.jmap.JMAPRoute; +import org.apache.james.jmap.Verb; import org.apache.james.jmap.draft.methods.ErrorResponse; import org.apache.james.jmap.draft.methods.Method; import org.apache.james.jmap.draft.methods.RequestHandler; @@ -73,9 +75,14 @@ public class JMAPApiRoutesTest { JMAPApiRoutes jmapApiRoutes = new JMAPApiRoutes(requestHandler, new RecordingMetricFactory(), mockedAuthFilter, mockedUserProvisionner, mockedMailboxesProvisionner); + JMAPRoute postApiRoute = jmapApiRoutes.routes() + .filter(jmapRoute -> jmapRoute.getEndpoint().getVerb().equals(Verb.POST)) + .findFirst() + .get(); + server = HttpServer.create() .port(RANDOM_PORT) - .route(jmapApiRoutes::define) + .route(routes -> routes.post(postApiRoute.getEndpoint().getPath(), (req, res) -> postApiRoute.getAction().apply(req, res))) .bindNow(); RestAssured.requestSpecification = new RequestSpecBuilder() diff --git a/server/protocols/jmap/pom.xml b/server/protocols/jmap/pom.xml index 7b40e91..1da1e9e 100644 --- a/server/protocols/jmap/pom.xml +++ b/server/protocols/jmap/pom.xml @@ -61,6 +61,10 @@ <scope>test</scope> </dependency> <dependency> + <groupId>javax.annotation</groupId> + <artifactId>javax.annotation-api</artifactId> + </dependency> + <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <scope>test</scope> diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/Endpoint.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/Endpoint.java index 1c4680c..3c878df 100644 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/Endpoint.java +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/Endpoint.java @@ -21,6 +21,8 @@ package org.apache.james.jmap; import java.util.Objects; +import reactor.netty.http.server.HttpServerRoutes; + public class Endpoint { private final Verb verb; private final String path; @@ -38,6 +40,10 @@ public class Endpoint { return path; } + HttpServerRoutes registerRoute(HttpServerRoutes builder, JMAPRoute.Action action) { + return verb.registerRoute(builder, this.path, action); + } + @Override public final boolean equals(Object o) { if (o instanceof Endpoint) { diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/Endpoint.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPRoute.java similarity index 62% copy from server/protocols/jmap/src/main/java/org/apache/james/jmap/Endpoint.java copy to server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPRoute.java index 1c4680c..d4d8986 100644 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/Endpoint.java +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPRoute.java @@ -19,38 +19,37 @@ package org.apache.james.jmap; -import java.util.Objects; +import java.util.function.BiFunction; -public class Endpoint { - private final Verb verb; - private final String path; +import org.reactivestreams.Publisher; - public Endpoint(Verb verb, String path) { - this.verb = verb; - this.path = path; - } +import reactor.netty.http.server.HttpServerRequest; +import reactor.netty.http.server.HttpServerResponse; + +public class JMAPRoute { + public interface Action extends BiFunction<HttpServerRequest, HttpServerResponse, Publisher<Void>> { - public Verb getVerb() { - return verb; } - public String getPath() { - return path; + private final Endpoint endpoint; + private final Version version; + private final Action action; + + public JMAPRoute(Endpoint endpoint, Version version, Action action) { + this.endpoint = endpoint; + this.version = version; + this.action = action; } - @Override - public final boolean equals(Object o) { - if (o instanceof Endpoint) { - Endpoint endpoint = (Endpoint) o; + public Endpoint getEndpoint() { + return endpoint; + } - return Objects.equals(this.verb, endpoint.verb) - && Objects.equals(this.path, endpoint.path); - } - return false; + public Version getVersion() { + return version; } - @Override - public final int hashCode() { - return Objects.hash(verb, path); + public Action getAction() { + return action; } } diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPRoutes.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPRoutes.java index c42085d..779c8ca 100644 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPRoutes.java +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPRoutes.java @@ -23,22 +23,19 @@ import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR; import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED; -import java.util.function.BiFunction; +import java.util.stream.Stream; -import org.reactivestreams.Publisher; import org.slf4j.Logger; import reactor.core.publisher.Mono; -import reactor.netty.http.server.HttpServerRequest; import reactor.netty.http.server.HttpServerResponse; -import reactor.netty.http.server.HttpServerRoutes; public interface JMAPRoutes { - HttpServerRoutes define(HttpServerRoutes builder); + Stream<JMAPRoute> routes(); - BiFunction<HttpServerRequest, HttpServerResponse, Publisher<Void>> CORS_CONTROL = corsHeaders((req, res) -> res.send()); + JMAPRoute.Action CORS_CONTROL = corsHeaders((req, res) -> res.send()); - static BiFunction<HttpServerRequest, HttpServerResponse, Publisher<Void>> corsHeaders(BiFunction<HttpServerRequest, HttpServerResponse, Publisher<Void>> action) { + static JMAPRoute.Action corsHeaders(JMAPRoute.Action action) { return (req, res) -> action.apply(req, res .header("Access-Control-Allow-Origin", "*") .header("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT") diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPServer.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPServer.java index 3b8bbbd..12426ed 100644 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPServer.java +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPServer.java @@ -19,6 +19,13 @@ package org.apache.james.jmap; +import static io.netty.handler.codec.http.HttpHeaderNames.ACCEPT; +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; +import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; import java.util.Optional; import java.util.Set; @@ -29,11 +36,18 @@ import org.apache.james.lifecycle.api.Startable; import org.apache.james.util.Port; import org.slf4j.LoggerFactory; +import com.github.steveash.guavate.Guavate; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; + import reactor.netty.DisposableServer; import reactor.netty.http.server.HttpServer; +import reactor.netty.http.server.HttpServerRequest; +import reactor.netty.http.server.HttpServerRoutes; public class JMAPServer implements Startable { private static final int RANDOM_PORT = 0; + private static final String JMAP_VERSION_HEADER = "jmapVersion="; private final JMAPConfiguration configuration; private final Set<JMAPRoutes> jmapRoutes; @@ -53,12 +67,17 @@ public class JMAPServer implements Startable { } public void start() { + ImmutableListMultimap<Endpoint, JMAPRoute> collect = jmapRoutes.stream() + .flatMap(JMAPRoutes::routes) + .collect(Guavate.toImmutableListMultimap(JMAPRoute::getEndpoint)); + if (configuration.isEnabled()) { server = Optional.of(HttpServer.create() .port(configuration.getPort() .map(Port::getValue) .orElse(RANDOM_PORT)) - .route(routes -> jmapRoutes.forEach(jmapRoute -> jmapRoute.define(routes))) + .route(routes -> jmapRoutes.forEach(jmapRoute -> collect.asMap().forEach( + (endpoint, route) -> injectRoutes(routes, endpoint, route)))) .wiretap(wireTapEnabled()) .bindNow()); } @@ -68,6 +87,62 @@ public class JMAPServer implements Startable { return LoggerFactory.getLogger("org.apache.james.jmap.wire").isTraceEnabled(); } + private HttpServerRoutes injectRoutes(HttpServerRoutes builder, Endpoint endpoint, Collection<JMAPRoute> routesList) { + if (routesList.size() == 1) { + JMAPRoute next = routesList.iterator().next(); + + return endpoint.registerRoute(builder, (req, res) -> + getExistingRoute(extractRequestVersionHeader(req), next).apply(req, res)); + } else if (routesList.size() == 2) { + ImmutableList<JMAPRoute> sorted = routesList.stream() + .sorted(Comparator.comparing(JMAPRoute::getVersion)) + .collect(Guavate.toImmutableList()); + JMAPRoute draftRoute = sorted.get(0); + JMAPRoute rfc8621Route = sorted.get(1); + + return endpoint.registerRoute(builder, (req, res) -> + chooseVersionRoute(extractRequestVersionHeader(req), draftRoute, rfc8621Route).apply(req, res)); + } + return builder; + } + + private JMAPRoute.Action getExistingRoute(String version, JMAPRoute route) { + try { + if (Version.of(version).equals(route.getVersion())) { + return route.getAction(); + } + } catch (IllegalArgumentException e) { + return (req, res) -> res.status(BAD_REQUEST).send(); + } + return (req, res) -> res.status(NOT_FOUND).send(); + } + + private JMAPRoute.Action chooseVersionRoute(String version, JMAPRoute draftRoute, JMAPRoute rfc8621Route) { + try { + if (hasRfc8621AcceptHeader(version)) { + return rfc8621Route.getAction(); + } + } catch (IllegalArgumentException e) { + return (req, res) -> res.status(BAD_REQUEST).send(); + } + return draftRoute.getAction(); + } + + private boolean hasRfc8621AcceptHeader(String version) { + return Version.of(version).equals(Version.RFC8621); + } + + private String extractRequestVersionHeader(HttpServerRequest request) { + return Arrays.stream(request.requestHeaders() + .get(ACCEPT) + .split(";")) + .map(value -> value.trim().toLowerCase()) + .filter(value -> value.startsWith(JMAP_VERSION_HEADER.toLowerCase())) + .map(value -> value.substring(JMAP_VERSION_HEADER.length())) + .findFirst() + .orElse(Version.DRAFT.getVersion()); + } + @PreDestroy public void stop() { server.ifPresent(DisposableServer::disposeNow); diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/Verb.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/Verb.java index f37ea9a..7a94547 100644 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/Verb.java +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/Verb.java @@ -19,9 +19,26 @@ package org.apache.james.jmap; +import reactor.netty.http.server.HttpServerRoutes; + public enum Verb { GET, POST, DELETE, - OPTIONS + OPTIONS; + + HttpServerRoutes registerRoute(HttpServerRoutes builder, String path, JMAPRoute.Action action) { + switch (this) { + case GET: + return builder.get(path, action); + case POST: + return builder.post(path, action); + case DELETE: + return builder.delete(path, action); + case OPTIONS: + return builder.options(path, action); + default: + return builder; + } + } } diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/JMAPServerTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/JMAPServerTest.java index 1c36384..b03a836 100644 --- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/JMAPServerTest.java +++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/JMAPServerTest.java @@ -19,15 +19,41 @@ package org.apache.james.jmap; +import static io.netty.handler.codec.http.HttpHeaderNames.ACCEPT; +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; 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.HttpConstants.JSON_CONTENT_TYPE_UTF8; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.hamcrest.Matchers.is; +import java.nio.charset.StandardCharsets; +import java.util.Set; +import java.util.stream.Stream; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.google.common.collect.ImmutableSet; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.restassured.RestAssured; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.http.ContentType; +import reactor.core.publisher.Mono; +import reactor.netty.http.server.HttpServerResponse; + class JMAPServerTest { + private static final String ACCEPT_JMAP_VERSION_HEADER = "application/json; jmapVersion="; + private static final String ACCEPT_DRAFT_VERSION_HEADER = ACCEPT_JMAP_VERSION_HEADER + Version.DRAFT.asString(); + private static final String ACCEPT_RFC8621_VERSION_HEADER = ACCEPT_JMAP_VERSION_HEADER + Version.RFC8621.asString(); + private static final JMAPConfiguration DISABLED_CONFIGURATION = JMAPConfiguration.builder().disable().build(); private static final JMAPConfiguration TEST_CONFIGURATION = JMAPConfiguration.builder() .enable() @@ -35,6 +61,20 @@ class JMAPServerTest { .build(); private static final ImmutableSet<JMAPRoutes> NO_ROUTES = ImmutableSet.of(); + private static final ImmutableSet<Endpoint> AUTHENTICATION_ENDPOINTS = ImmutableSet.of( + new Endpoint(Verb.POST, JMAPUrls.AUTHENTICATION), + new Endpoint(Verb.GET, JMAPUrls.AUTHENTICATION) + ); + private static final ImmutableSet<Endpoint> JMAP_ENDPOINTS = ImmutableSet.of( + new Endpoint(Verb.POST, JMAPUrls.JMAP), + new Endpoint(Verb.DELETE, JMAPUrls.JMAP) + ); + private static final ImmutableSet<JMAPRoutes> FAKE_ROUTES = ImmutableSet.of( + new FakeJMAPRoutes(AUTHENTICATION_ENDPOINTS, Version.DRAFT), + new FakeJMAPRoutes(AUTHENTICATION_ENDPOINTS, Version.RFC8621), + new FakeJMAPRoutes(JMAP_ENDPOINTS, Version.DRAFT) + ); + @Test void serverShouldAnswerWhenStarted() { JMAPServer jmapServer = new JMAPServer(TEST_CONFIGURATION, NO_ROUTES); @@ -84,4 +124,114 @@ class JMAPServerTest { assertThatThrownBy(jmapServer::getPort) .isInstanceOf(IllegalStateException.class); } + + @Nested + class RouteVersioningTest { + JMAPServer server; + + @BeforeEach + void setUp() { + server = new JMAPServer(TEST_CONFIGURATION, FAKE_ROUTES); + server.start(); + + RestAssured.requestSpecification = new RequestSpecBuilder() + .setContentType(ContentType.JSON) + .setAccept(ContentType.JSON) + .setConfig(newConfig().encoderConfig(encoderConfig().defaultContentCharset(StandardCharsets.UTF_8))) + .setPort(server.getPort().getValue()) + .build(); + } + + @AfterEach + void tearDown() { + server.stop(); + } + + @Test + void serverShouldReturnDefaultVersionRouteWhenNoVersionHeader() { + given() + .basePath(JMAPUrls.AUTHENTICATION) + .when() + .get() + .then() + .statusCode(HttpResponseStatus.OK.code()) + .body("Version", is(Version.DRAFT.asString())); + } + + @Test + void serverShouldReturnCorrectRouteWhenTwoVersionRoutes() { + given() + .basePath(JMAPUrls.AUTHENTICATION) + .header(ACCEPT.toString(), ACCEPT_RFC8621_VERSION_HEADER) + .when() + .get() + .then() + .statusCode(HttpResponseStatus.OK.code()) + .body("Version", is(Version.RFC8621.asString())); + } + + @Test + void serverShouldReturnCorrectRouteWhenOneVersionRoute() { + given() + .basePath(JMAPUrls.JMAP) + .header(ACCEPT.toString(), ACCEPT_DRAFT_VERSION_HEADER) + .when() + .post() + .then() + .statusCode(HttpResponseStatus.OK.code()) + .body("Version", is(Version.DRAFT.asString())); + } + + @Test + void serverShouldReturnNotFoundWhenRouteVersionDoesNotExist() { + given() + .basePath(JMAPUrls.JMAP) + .header(ACCEPT.toString(), ACCEPT_RFC8621_VERSION_HEADER) + .when() + .post() + .then() + .statusCode(HttpResponseStatus.NOT_FOUND.code()); + } + + @Test + void serverShouldReturnBadRequestWhenVersionIsUnknown() { + given() + .basePath(JMAPUrls.AUTHENTICATION) + .header(ACCEPT.toString(), ACCEPT_JMAP_VERSION_HEADER + "unknown") + .when() + .get() + .then() + .statusCode(HttpResponseStatus.BAD_REQUEST.code()); + } + } + + private static class FakeJMAPRoutes implements JMAPRoutes { + private static final Logger LOGGER = LoggerFactory.getLogger(FakeJMAPRoutes.class); + + private final Set<Endpoint> endpoints; + private final Version version; + + private FakeJMAPRoutes(Set<Endpoint> endpoints, Version version) { + this.endpoints = endpoints; + this.version = version; + } + + @Override + public Stream<JMAPRoute> routes() { + return endpoints.stream() + .map(endpoint -> new JMAPRoute(endpoint, version, (request, response) -> sendVersionResponse(response))); + } + + @Override + public Logger logger() { + return LOGGER; + } + + private Mono<Void> sendVersionResponse(HttpServerResponse response) { + return response.status(HttpResponseStatus.OK) + .header(CONTENT_TYPE, JSON_CONTENT_TYPE_UTF8) + .sendString(Mono.just(String.format("{\"Version\":\"%s\"}", version.asString()))) + .then(); + } + } } --------------------------------------------------------------------- To unsubscribe, e-mail: server-dev-unsubscr...@james.apache.org For additional commands, e-mail: server-dev-h...@james.apache.org