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