blob: 8506f2f362b1a114de9c7fd58eadf415a1b64f8b [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 eu.timepit.refined.numeric.NonNegative
import eu.timepit.refined.refineV
import eu.timepit.refined.types.numeric.NonNegInt
import org.apache.james.jmap.json.BackReferenceDeserializer
import org.apache.james.jmap.mail.MailboxSetRequest.UnparsedMailboxId
import org.apache.james.jmap.mail.VacationResponse.{UnparsedVacationResponseId, VACATION_RESPONSE_ID}
import org.apache.james.jmap.model.Id.Id
import org.apache.james.jmap.model.Invocation.{Arguments, MethodCallId, MethodName}
import org.apache.james.jmap.model.{ClientId, Id, Invocation, ServerId}
import org.apache.james.mailbox.model.MailboxId
import play.api.libs.json.{JsArray, JsError, JsObject, JsResult, JsSuccess, JsValue, Reads}
import scala.util.Try
sealed trait JsonPathPart
case object WildcardPart extends JsonPathPart
case class PlainPart(name: String) extends JsonPathPart {
def read(jsValue: JsValue): JsResult[JsValue] = jsValue match {
case JsObject(underlying) => underlying.get(name).map(JsSuccess(_)).getOrElse(JsError(s"Expected path $name was missing"))
case _ => JsError("Expecting a JsObject but got a different structure")
}
}
object ArrayElementPart {
def parse(string: String): Option[ArrayElementPart] = {
if (string.startsWith("[") && string.endsWith("]")) {
val positionPart: String = string.substring(1, string.length - 1)
Try(positionPart.toInt)
.fold(_ => None, fromInt)
} else {
None
}
}
private def fromInt(position: Int): Option[ArrayElementPart] =
refineV[NonNegative](position)
.fold(_ => None,
ref => Some(ArrayElementPart(ref)))
}
case class ArrayElementPart(position: NonNegInt) extends JsonPathPart {
def read(jsValue: JsValue): JsResult[JsValue] = jsValue match {
case JsArray(values) => values.lift(position.value)
.map(JsSuccess(_))
.getOrElse(JsError(s"Supplied array have no $position element"))
case _ => JsError("Expecting a JsArray but got a different structure")
}
}
object JsonPath {
def parse(string: String): JsonPath = JsonPath(string.split('/').toList
.flatMap {
case "" => Nil
case "*" => List(WildcardPart)
case string if ArrayElementPart.parse(string).isDefined => ArrayElementPart.parse(string)
case part: String =>
val arrayElementPartPosition = part.indexOf('[')
if (arrayElementPartPosition < 0) {
asPlainPart(part)
} else if (arrayElementPartPosition == 0) {
asArrayElementPart(string)
} else {
asArrayElementInAnObject(string, part, arrayElementPartPosition)
}
})
private def asPlainPart(part: String): List[JsonPathPart] = {
List(PlainPart(part))
}
private def asArrayElementInAnObject(string: String, part: String, arrayElementPartPosition: Int): List[JsonPathPart] = {
ArrayElementPart.parse(string.substring(arrayElementPartPosition))
.map(List(PlainPart(part.substring(0, arrayElementPartPosition)), _))
.getOrElse(List(PlainPart(part)))
}
private def asArrayElementPart(string: String): List[JsonPathPart] = {
List(ArrayElementPart.parse(string)
.getOrElse(PlainPart(string)))
}
}
case class JsonPath(parts: List[JsonPathPart]) {
def evaluate(jsValue: JsValue): JsResult[JsValue] = parts match {
case Nil => JsSuccess(jsValue)
case head :: tail =>
val tailAsJsonPath = JsonPath(tail)
head match {
case part: PlainPart => part.read(jsValue).flatMap(subPart => tailAsJsonPath.evaluate(subPart))
case part: ArrayElementPart => part.read(jsValue).flatMap(subPart => tailAsJsonPath.evaluate(subPart))
case WildcardPart => tailAsJsonPath.readWildcard(jsValue)
}
}
private def readWildcard(jsValue: JsValue) = jsValue match {
case JsArray(arrayItems) =>
val evaluationResults: List[JsResult[JsValue]] = arrayItems.toList.map(evaluate)
evaluationResults.find(x => x.isInstanceOf[JsError])
.getOrElse(JsSuccess(expendArray(evaluationResults)))
case _ => JsError("Expecting an array")
}
private def expendArray(evaluationResults: List[JsResult[JsValue]]): JsArray = {
JsArray(evaluationResults
.map(_.get)
.flatMap({
case JsArray(nestedArray) => nestedArray
case other: JsValue => List(other)
}))
}
}
case class BackReference(name: MethodName, path: JsonPath, resultOf: MethodCallId) {
def resolve(invocation: Invocation): JsResult[JsValue] = if (!(invocation.methodName equals name)) {
JsError(s"$resultOf references a ${invocation.methodName} method")
} else {
path.evaluate(invocation.arguments.value)
}
}
case class InvalidResultReferenceException(message: String) extends IllegalArgumentException
case class ProcessingContext(private val creationIds: Map[ClientId, ServerId], private val invocations: Map[MethodCallId, Invocation]) {
def recordCreatedId(clientId: ClientId, serverId: ServerId): ProcessingContext = ProcessingContext(creationIds + (clientId -> serverId), invocations)
private def retrieveServerId(clientId: ClientId): Option[ServerId] = creationIds.get(clientId)
def recordInvocation(invocation: Invocation): ProcessingContext = ProcessingContext(creationIds, invocations + (invocation.methodCallId -> invocation))
def resolveBackReferences(invocation: Invocation): Either[InvalidResultReferenceException, Invocation] =
backReferenceResolver().reads(invocation.arguments.value) match {
case JsError(e) => Left(InvalidResultReferenceException(e.toString()))
case JsSuccess(JsObject(underlying), _) => Right(Invocation(methodName = invocation.methodName,
methodCallId = invocation.methodCallId,
arguments = Arguments(JsObject(underlying))))
case others: JsSuccess[JsValue] => Left(InvalidResultReferenceException(s"Unexpected value $others"))
}
private def backReferenceResolver(): Reads[JsValue] = {
case JsArray(value) => resolveBackReferences(value)
case JsObject(underlying) => resolveBackReference(underlying)
case others: JsValue => JsSuccess(others)
}
private def resolveBackReferences(array: collection.IndexedSeq[JsValue]): JsResult[JsValue] = {
val resolver: Reads[JsValue] = backReferenceResolver()
val results: Seq[JsResult[JsValue]] = array.map(resolver.reads).toSeq
results.find(_.isError)
.getOrElse(JsSuccess(JsArray(results.map(_.get))))
}
private def resolveBackReference(underlying: collection.Map[String, JsValue]): JsResult[JsObject] = {
val resolutions = underlying.map(resolveBackReference)
val firstError = resolutions.flatMap({
case Left(jsError) => Some(jsError)
case _ => None
}).headOption
val transformedMap = resolutions.flatMap({
case Right((entry, value)) => Some((entry, value))
case _ => None
}).toMap
firstError.getOrElse(JsSuccess(JsObject(transformedMap)))
}
private def resolveBackReference(entry: (String, JsValue)): Either[JsError, (String, JsValue)] = {
if (entry._1.startsWith("#")) {
val newEntry: String = entry._1.substring(1)
BackReferenceDeserializer.deserializeBackReference(entry._2) match {
case JsSuccess(backReference, _) => resolveBackReference(newEntry, backReference)
// If the JSON object is not a back-reference continue parsing (it could be a creationId)
case JsError(_) => propagateBackReferenceResolution(entry)
}
} else {
propagateBackReferenceResolution(entry)
}
}
private def resolveBackReference(newEntry: String, backReference: BackReference): Either[JsError, (String, JsValue)] = {
resolve(backReference) match {
case JsError(e) => Left(JsError(e))
case JsSuccess(resolvedBackReference, _) => Right((newEntry, resolvedBackReference))
}
}
private def propagateBackReferenceResolution(entry: (String, JsValue)): Either[JsError, (String, JsValue)] = {
val entryPayload: JsResult[JsValue] = backReferenceResolver().reads(entry._2)
entryPayload match {
case JsError(e) => Left(JsError(e))
case JsSuccess(newValue, _) => Right((entry._1, newValue))
}
}
private def retrieveInvocation(callId: MethodCallId): Option[Invocation] = invocations.get(callId)
private def resolve(backReference: BackReference): JsResult[JsValue] = retrieveInvocation(backReference.resultOf)
.map(backReference.resolve)
.getOrElse(JsError("Back reference could not be resolved"))
def resolveMailboxId(unparsedMailboxId: UnparsedMailboxId, mailboxIdFactory: MailboxId.Factory): Either[IllegalArgumentException, MailboxId] =
Id.validate(unparsedMailboxId.value)
.flatMap(id => resolveServerId(ClientId(id)))
.flatMap(serverId => parseMailboxId(mailboxIdFactory, serverId))
private def parseMailboxId(mailboxIdFactory: MailboxId.Factory, serverId: ServerId) =
try {
Right(mailboxIdFactory.fromString(serverId.value.value))
} catch {
case e: IllegalArgumentException => Left(e)
}
private def resolveServerId(id: ClientId): Either[IllegalArgumentException, ServerId] =
id.retrieveOriginalClientId
.map(maybePreviousClientId => maybePreviousClientId.flatMap(previousClientId => retrieveServerId(previousClientId)
.map(serverId => Right(serverId))
.getOrElse(Left[IllegalArgumentException, ServerId](new IllegalArgumentException(s"$id was not used in previously defined creationIds")))))
.getOrElse(Right(ServerId(id.value)))
def resolveVacationResponseId(unparsedVacationId: UnparsedVacationResponseId): Either[IllegalArgumentException, Id] =
if (unparsedVacationId.equals(VACATION_RESPONSE_ID)) {
Right(VACATION_RESPONSE_ID)
} else {
Left(new IllegalArgumentException(s"$unparsedVacationId is not a valid VacationResponse ID"))
}
}