You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2021/01/28 03:11:01 UTC

[james-project] 02/02: JAMES-3491 Transport independant JMAP API

This is an automated email from the ASF dual-hosted git repository.

btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit d21c2975f27ff5ac79a2829c3e56701cd085b4ab
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Tue Jan 26 14:30:01 2021 +0700

    JAMES-3491 Transport independant JMAP API
    
    Allows reuse over HTTP or WebSocket transport
---
 .../apache/james/jmap/routes/JMAPApiRoutes.scala   |  96 +++-----------------
 .../org/apache/james/jmap/routes/JmapApi.scala     | 101 +++++++++++++++++++++
 .../james/jmap/routes/JMAPApiRoutesTest.scala      |   4 +-
 3 files changed, 117 insertions(+), 84 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
index faf7742..2bc48bb 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
@@ -31,43 +31,29 @@ import javax.inject.{Inject, Named}
 import org.apache.james.jmap.HttpConstants.JSON_CONTENT_TYPE
 import org.apache.james.jmap.JMAPUrls.JMAP
 import org.apache.james.jmap.core.CapabilityIdentifier.CapabilityIdentifier
-import org.apache.james.jmap.core.Invocation.MethodName
 import org.apache.james.jmap.core.ProblemDetails.{notJSONProblem, notRequestProblem, unknownCapabilityProblem}
-import org.apache.james.jmap.core.{Capability, ErrorCode, Invocation, MissingCapabilityException, ProblemDetails, RequestObject, ResponseObject}
+import org.apache.james.jmap.core.{ProblemDetails, RequestObject}
 import org.apache.james.jmap.exceptions.UnauthorizedException
 import org.apache.james.jmap.http.rfc8621.InjectionKeys
 import org.apache.james.jmap.http.{Authenticator, UserProvisioning}
 import org.apache.james.jmap.json.ResponseSerializer
-import org.apache.james.jmap.method.{InvocationWithContext, Method}
 import org.apache.james.jmap.routes.DownloadRoutes.LOGGER
 import org.apache.james.jmap.{Endpoint, JMAPRoute, JMAPRoutes}
 import org.apache.james.mailbox.MailboxSession
 import org.slf4j.{Logger, LoggerFactory}
 import play.api.libs.json.{JsError, JsSuccess}
-import reactor.core.publisher.{Flux, Mono}
-import reactor.core.scala.publisher.{SFlux, SMono}
+import reactor.core.publisher.Mono
+import reactor.core.scala.publisher.SMono
 import reactor.core.scheduler.Schedulers
 import reactor.netty.http.server.{HttpServerRequest, HttpServerResponse}
 
-import scala.jdk.CollectionConverters._
-
 object JMAPApiRoutes {
   val LOGGER: Logger = LoggerFactory.getLogger(classOf[JMAPApiRoutes])
 }
 
-class JMAPApiRoutes (val authenticator: Authenticator,
+class JMAPApiRoutes @Inject() (@Named(InjectionKeys.RFC_8621) val authenticator: Authenticator,
                      userProvisioner: UserProvisioning,
-                     methods: Set[Method],
-                     defaultCapabilities: Set[Capability]) extends JMAPRoutes {
-
-  private val methodsByName: Map[MethodName, Method] = methods.map(method => method.methodName -> method).toMap
-
-  @Inject
-  def this(@Named(InjectionKeys.RFC_8621) authenticator: Authenticator,
-           userProvisioner: UserProvisioning,
-           javaMethods: java.util.Set[Method], supportedCapabilities: java.util.Set[Capability]) {
-    this(authenticator, userProvisioner, javaMethods.asScala.toSet, supportedCapabilities.asScala.toSet)
-  }
+                     jmapApi: JMAPApi) extends JMAPRoutes {
 
   override def routes(): stream.Stream[JMAPRoute] = Stream.of(
     JMAPRoute.builder
@@ -108,69 +94,15 @@ class JMAPApiRoutes (val authenticator: Authenticator,
 
   private def process(requestObject: RequestObject,
                       httpServerResponse: HttpServerResponse,
-                      mailboxSession: MailboxSession): SMono[Void] = {
-    val processingContext: ProcessingContext = ProcessingContext(Map.empty, Map.empty)
-    val unsupportedCapabilities = requestObject.using.toSet -- defaultCapabilities.map(_.identifier())
-    val capabilities: Set[CapabilityIdentifier] = requestObject.using.toSet
-
-    if (unsupportedCapabilities.nonEmpty) {
-      SMono.raiseError(UnsupportedCapabilitiesException(unsupportedCapabilities))
-    } else {
-      processSequentiallyAndUpdateContext(requestObject, mailboxSession, processingContext, capabilities)
-        .flatMap((invocations : Seq[InvocationWithContext]) =>
-          SMono.fromPublisher(httpServerResponse.status(OK)
-            .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
-            .sendString(
-              SMono.fromCallable(() =>
-                ResponseSerializer.serialize(ResponseObject(ResponseObject.SESSION_STATE, invocations.map(_.invocation))).toString),
-              StandardCharsets.UTF_8)
-            .`then`())
-        )
-    }
-  }
-
-  private def processSequentiallyAndUpdateContext(requestObject: RequestObject, mailboxSession: MailboxSession, processingContext: ProcessingContext, capabilities: Set[CapabilityIdentifier]): SMono[Seq[(InvocationWithContext)]] = {
-    SFlux.fromIterable(requestObject.methodCalls)
-      .foldLeft(List[SFlux[InvocationWithContext]]())((acc, elem) => {
-        val lastProcessingContext: SMono[ProcessingContext] = acc.headOption
-          .map(last => SMono.fromPublisher(Flux.from(last.map(_.processingContext)).last()))
-          .getOrElse(SMono.just(processingContext))
-        val invocation: SFlux[InvocationWithContext] = lastProcessingContext.flatMapMany(context => process(capabilities, mailboxSession, InvocationWithContext(elem, context)))
-        invocation.cache() :: acc
-      })
-      .map(_.reverse)
-      .flatMap(list => SFlux.fromIterable(list)
-        .concatMap(e => e)
-        .collectSeq())
-  }
-
-  private def process(capabilities: Set[CapabilityIdentifier], mailboxSession: MailboxSession, invocation: InvocationWithContext) : SFlux[InvocationWithContext] =
-    SFlux.fromPublisher(
-      invocation.processingContext.resolveBackReferences(invocation.invocation) match {
-        case Left(e) => SFlux.just[InvocationWithContext](InvocationWithContext(Invocation.error(
-          errorCode = ErrorCode.InvalidResultReference,
-          description = s"Failed resolving back-reference: ${e.message}",
-          methodCallId = invocation.invocation.methodCallId), invocation.processingContext))
-        case Right(resolvedInvocation) => processMethodWithMatchName(capabilities, InvocationWithContext(resolvedInvocation, invocation.processingContext), mailboxSession)
-          .map(_.recordInvocation)
-      })
-
-  private def processMethodWithMatchName(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession): SFlux[InvocationWithContext] =
-    methodsByName.get(invocation.invocation.methodName)
-      .map(method => validateCapabilities(capabilities, method.requiredCapabilities)
-        .fold(e => SFlux.just(InvocationWithContext(Invocation.error(ErrorCode.UnknownMethod, e.description, invocation.invocation.methodCallId), invocation.processingContext)),
-          _ => SFlux.fromPublisher(method.process(capabilities, invocation, mailboxSession))))
-      .getOrElse(SFlux.just(InvocationWithContext(Invocation.error(ErrorCode.UnknownMethod, invocation.invocation.methodCallId), invocation.processingContext)))
-      .onErrorResume(throwable => SMono.just(InvocationWithContext(Invocation.error(ErrorCode.ServerFail, throwable.getMessage, invocation.invocation.methodCallId), invocation.processingContext)))
-
-  private def validateCapabilities(capabilities: Set[CapabilityIdentifier], requiredCapabilities: Set[CapabilityIdentifier]): Either[MissingCapabilityException, Unit] = {
-    val missingCapabilities = requiredCapabilities -- capabilities
-    if (missingCapabilities.nonEmpty) {
-      Left(MissingCapabilityException(s"Missing capability(ies): ${missingCapabilities.mkString(", ")}"))
-    } else {
-      Right()
-    }
-  }
+                      mailboxSession: MailboxSession): SMono[Void] =
+    jmapApi.process(requestObject, mailboxSession)
+      .flatMap(responseObject => SMono.fromPublisher(httpServerResponse.status(OK)
+        .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
+        .sendString(
+          SMono.fromCallable(() =>
+            ResponseSerializer.serialize(responseObject).toString),
+          StandardCharsets.UTF_8)
+        .`then`()))
 
   private def handleError(throwable: Throwable, response: HttpServerResponse): SMono[Void] = throwable match {
     case exception: IllegalArgumentException => respondDetails(response,
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JmapApi.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JmapApi.scala
new file mode 100644
index 0000000..a8ca359
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JmapApi.scala
@@ -0,0 +1,101 @@
+/****************************************************************
+ * 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.routes
+
+import javax.inject.Inject
+import org.apache.james.jmap.core.CapabilityIdentifier.CapabilityIdentifier
+import org.apache.james.jmap.core.Invocation.MethodName
+import org.apache.james.jmap.core.{Capability, DefaultCapabilities, ErrorCode, Invocation, MissingCapabilityException, RequestObject, ResponseObject}
+import org.apache.james.jmap.method.{InvocationWithContext, Method}
+import org.apache.james.mailbox.MailboxSession
+import org.slf4j.{Logger, LoggerFactory}
+import reactor.core.publisher.Flux
+import reactor.core.scala.publisher.{SFlux, SMono}
+
+import scala.jdk.CollectionConverters._
+
+object JMAPApi {
+  val LOGGER: Logger = LoggerFactory.getLogger(classOf[JMAPApi])
+}
+
+class JMAPApi (methods: Set[Method], defaultCapabilities: Set[Capability]) {
+
+  private val methodsByName: Map[MethodName, Method] = methods.map(method => method.methodName -> method).toMap
+
+  @Inject
+  def this(javaMethods: java.util.Set[Method], supportedCapabilities: java.util.Set[Capability]) {
+    this(javaMethods.asScala.toSet, supportedCapabilities.asScala.toSet)
+  }
+
+  def process(requestObject: RequestObject,
+              mailboxSession: MailboxSession): SMono[ResponseObject] = {
+    val processingContext: ProcessingContext = ProcessingContext(Map.empty, Map.empty)
+    val unsupportedCapabilities = requestObject.using.toSet -- defaultCapabilities.map(_.identifier())
+    val capabilities: Set[CapabilityIdentifier] = requestObject.using.toSet
+
+    if (unsupportedCapabilities.nonEmpty) {
+      SMono.raiseError(UnsupportedCapabilitiesException(unsupportedCapabilities))
+    } else {
+      processSequentiallyAndUpdateContext(requestObject, mailboxSession, processingContext, capabilities)
+        .map(invocations => ResponseObject(ResponseObject.SESSION_STATE, invocations.map(_.invocation)))
+    }
+  }
+
+  private def processSequentiallyAndUpdateContext(requestObject: RequestObject, mailboxSession: MailboxSession, processingContext: ProcessingContext, capabilities: Set[CapabilityIdentifier]): SMono[Seq[(InvocationWithContext)]] =
+    SFlux.fromIterable(requestObject.methodCalls)
+      .foldLeft(List[SFlux[InvocationWithContext]]())((acc, elem) => {
+        val lastProcessingContext: SMono[ProcessingContext] = acc.headOption
+          .map(last => SMono.fromPublisher(Flux.from(last.map(_.processingContext)).last()))
+          .getOrElse(SMono.just(processingContext))
+        val invocation: SFlux[InvocationWithContext] = lastProcessingContext.flatMapMany(context => process(capabilities, mailboxSession, InvocationWithContext(elem, context)))
+        invocation.cache() :: acc
+      })
+      .map(_.reverse)
+      .flatMap(list => SFlux.fromIterable(list)
+        .concatMap(e => e)
+        .collectSeq())
+
+  private def process(capabilities: Set[CapabilityIdentifier], mailboxSession: MailboxSession, invocation: InvocationWithContext) : SFlux[InvocationWithContext] =
+    SFlux.fromPublisher(
+      invocation.processingContext.resolveBackReferences(invocation.invocation) match {
+        case Left(e) => SFlux.just[InvocationWithContext](InvocationWithContext(Invocation.error(
+          errorCode = ErrorCode.InvalidResultReference,
+          description = s"Failed resolving back-reference: ${e.message}",
+          methodCallId = invocation.invocation.methodCallId), invocation.processingContext))
+        case Right(resolvedInvocation) => processMethodWithMatchName(capabilities, InvocationWithContext(resolvedInvocation, invocation.processingContext), mailboxSession)
+          .map(_.recordInvocation)
+      })
+
+  private def processMethodWithMatchName(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession): SFlux[InvocationWithContext] =
+    methodsByName.get(invocation.invocation.methodName)
+      .map(method => validateCapabilities(capabilities, method.requiredCapabilities)
+        .fold(e => SFlux.just(InvocationWithContext(Invocation.error(ErrorCode.UnknownMethod, e.description, invocation.invocation.methodCallId), invocation.processingContext)),
+          _ => SFlux.fromPublisher(method.process(capabilities, invocation, mailboxSession))))
+      .getOrElse(SFlux.just(InvocationWithContext(Invocation.error(ErrorCode.UnknownMethod, invocation.invocation.methodCallId), invocation.processingContext)))
+      .onErrorResume(throwable => SMono.just(InvocationWithContext(Invocation.error(ErrorCode.ServerFail, throwable.getMessage, invocation.invocation.methodCallId), invocation.processingContext)))
+
+  private def validateCapabilities(capabilities: Set[CapabilityIdentifier], requiredCapabilities: Set[CapabilityIdentifier]): Either[MissingCapabilityException, Unit] = {
+    val missingCapabilities = requiredCapabilities -- capabilities
+    if (missingCapabilities.nonEmpty) {
+      Left(MissingCapabilityException(s"Missing capability(ies): ${missingCapabilities.mkString(", ")}"))
+    } else {
+      Right()
+    }
+  }
+}
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala
index fece846..61faf1b 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala
@@ -78,7 +78,7 @@ object JMAPApiRoutesTest {
   private val userProvisionner: UserProvisioning = new UserProvisioning(usersRepository, new RecordingMetricFactory)
   private val JMAP_METHODS: Set[Method] = Set(new CoreEchoMethod)
 
-  private val JMAP_API_ROUTE: JMAPApiRoutes = new JMAPApiRoutes(AUTHENTICATOR, userProvisionner, JMAP_METHODS, DefaultCapabilities.supported(JmapRfc8621Configuration.UPLOAD_LIMIT_30_MB).capabilities.toSet)
+  private val JMAP_API_ROUTE: JMAPApiRoutes = new JMAPApiRoutes(AUTHENTICATOR, userProvisionner, new JMAPApi(JMAP_METHODS, DefaultCapabilities.supported(JmapRfc8621Configuration.UPLOAD_LIMIT_30_MB).capabilities.toSet))
   private val ROUTES_HANDLER: ImmutableSet[JMAPRoutesHandler] = ImmutableSet.of(new JMAPRoutesHandler(Version.RFC8621, JMAP_API_ROUTE))
 
   private val userBase64String: String = Base64.getEncoder.encodeToString("user1:password".getBytes(StandardCharsets.UTF_8))
@@ -442,7 +442,7 @@ class JMAPApiRoutesTest extends AnyFlatSpec with BeforeAndAfter with Matchers {
     when(mockCoreEchoMethod.requiredCapabilities).thenReturn(Set(JMAP_CORE))
 
     val methods: Set[Method] = Set(mockCoreEchoMethod)
-    val apiRoute: JMAPApiRoutes = new JMAPApiRoutes(AUTHENTICATOR, userProvisionner, methods, DefaultCapabilities.supported(JmapRfc8621Configuration.UPLOAD_LIMIT_30_MB).capabilities.toSet)
+    val apiRoute: JMAPApiRoutes = new JMAPApiRoutes(AUTHENTICATOR, userProvisionner, new JMAPApi(methods, DefaultCapabilities.supported(JmapRfc8621Configuration.UPLOAD_LIMIT_30_MB).capabilities.toSet))
     val routesHandler: ImmutableSet[JMAPRoutesHandler] = ImmutableSet.of(new JMAPRoutesHandler(Version.RFC8621, apiRoute))
 
     val versionParser: VersionParser = new VersionParser(SUPPORTED_VERSIONS, JMAPConfiguration.DEFAULT)


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org