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/04/10 02:02:17 UTC

[james-project] 09/15: JAMES-3092 Instauring the Y structure with jmap-draft

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 <rc...@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-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org