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