blob: 13bf6c1035ccdd0fcac137f6a453be46f1613f22 [file] [log] [blame]
/****************************************************************
* 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.{CapabilityFactory, ErrorCode, Invocation, JmapRfc8621Configuration, 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[CapabilityIdentifier], configuration: JmapRfc8621Configuration) {
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[CapabilityFactory], configuration: JmapRfc8621Configuration) = {
this(javaMethods.asScala.toSet, supportedCapabilities.asScala.map(x => x.id()).toSet, configuration)
}
def process(requestObject: RequestObject,
mailboxSession: MailboxSession): SMono[ResponseObject] = {
val processingContext: ProcessingContext = ProcessingContext(Map.empty, Map.empty)
val unsupportedCapabilities = requestObject.using -- defaultCapabilities
val capabilities: Set[CapabilityIdentifier] = requestObject.using
if (unsupportedCapabilities.nonEmpty) {
SMono.error(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)
.fold(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) ++ configuration.disabledCapabilities.intersect(capabilities)
if (missingCapabilities.nonEmpty) {
Left(MissingCapabilityException(s"Missing capability(ies): ${missingCapabilities.mkString(", ")}"))
} else {
Right((): Unit)
}
}
}