You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by rc...@apache.org on 2021/10/28 10:32:46 UTC
[james-project] 01/01: WIP JAMES-3539 PushSubscription/set create
This is an automated email from the ASF dual-hosted git repository.
rcordier pushed a commit to branch pushsub-set-create
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 1c0a657d213e515aa44a9c23ef9cf0de036ffd73
Author: Rene Cordier <rc...@linagora.com>
AuthorDate: Thu Oct 28 17:32:25 2021 +0700
WIP JAMES-3539 PushSubscription/set create
---
.../james/jmap/core/PushSubscriptionSet.scala | 65 +++++++++++++++++
.../jmap/json/PushSubscriptionSerializer.scala | 59 +++++++++++++++
.../PushSubscriptionSetCreatePerformer.scala | 81 +++++++++++++++++++++
.../jmap/method/PushSubscriptionSetMethod.scala | 84 ++++++++++++++++++++++
4 files changed, 289 insertions(+)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/PushSubscriptionSet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/PushSubscriptionSet.scala
new file mode 100644
index 0000000..386bfe4
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/PushSubscriptionSet.scala
@@ -0,0 +1,65 @@
+/****************************************************************
+ * 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.core
+
+import eu.timepit.refined.collection.NonEmpty
+import eu.timepit.refined.refineV
+import eu.timepit.refined.types.string.NonEmptyString
+import org.apache.james.jmap.api.model.{PushSubscriptionExpiredTime, PushSubscriptionId}
+import org.apache.james.jmap.core.Id.Id
+import org.apache.james.jmap.core.SetError.SetErrorDescription
+import play.api.libs.json.JsObject
+
+case class PushSubscriptionSetRequest(create: Option[Map[PushSubscriptionCreationId, JsObject]])
+
+case class PushSubscriptionCreationId(id: Id)
+
+object PushSubscriptionCreation {
+ private val serverSetProperty = Set("id", "verificationCode")
+ private val assignableProperties = Set("deviceClientId", "url", "keys", "expires", "types")
+ private val knownProperties = assignableProperties ++ serverSetProperty
+
+ def validateProperties(jsObject: JsObject): Either[PushSubscriptionCreationParseException, JsObject] =
+ (jsObject.keys.intersect(serverSetProperty), jsObject.keys.diff(knownProperties)) match {
+ case (_, unknownProperties) if unknownProperties.nonEmpty =>
+ Left(PushSubscriptionCreationParseException(SetError.invalidArguments(
+ SetErrorDescription("Some unknown properties were specified"),
+ Some(toProperties(unknownProperties.toSet)))))
+ case (specifiedServerSetProperties, _) if specifiedServerSetProperties.nonEmpty =>
+ Left(PushSubscriptionCreationParseException(SetError.invalidArguments(
+ SetErrorDescription("Some server-set properties were specified"),
+ Some(toProperties(specifiedServerSetProperties.toSet)))))
+ case _ => scala.Right(jsObject)
+ }
+
+ private def toProperties(strings: Set[String]): Properties = Properties(strings
+ .flatMap(string => {
+ val refinedValue: Either[String, NonEmptyString] = refineV[NonEmpty](string)
+ refinedValue.fold(_ => None, Some(_))
+ }))
+}
+
+case class PushSubscriptionCreationParseException(setError: SetError) extends Exception
+
+case class PushSubscriptionCreationResponse(id: PushSubscriptionId,
+ expires: Option[PushSubscriptionExpiredTime])
+
+case class PushSubscriptionSetResponse(created: Option[Map[PushSubscriptionCreationId, PushSubscriptionCreationResponse]],
+ notCreated: Option[Map[PushSubscriptionCreationId, SetError]])
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/PushSubscriptionSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/PushSubscriptionSerializer.scala
new file mode 100644
index 0000000..632fcf7
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/PushSubscriptionSerializer.scala
@@ -0,0 +1,59 @@
+/****************************************************************
+ * 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.json
+
+import eu.timepit.refined.refineV
+import org.apache.james.jmap.api.model.{DeviceClientId, PushSubscriptionCreationRequest, PushSubscriptionExpiredTime, PushSubscriptionId, PushSubscriptionKeys, PushSubscriptionServerURL, TypeName}
+import org.apache.james.jmap.core.Id.IdConstraint
+import org.apache.james.jmap.core.{PushSubscriptionCreationId, PushSubscriptionCreationResponse, PushSubscriptionSetRequest, PushSubscriptionSetResponse, SetError}
+import play.api.libs.json.{Format, JsError, JsObject, JsResult, JsSuccess, JsValue, Json, OWrites, Reads, Writes}
+
+class PushSubscriptionSerializer {
+ private implicit val pushSubscriptionIdWrites: Writes[PushSubscriptionId] = Json.valueWrites[PushSubscriptionId]
+
+ private implicit val pushSubscriptionExpiredTimeFormat: Format[PushSubscriptionExpiredTime] = Json.valueFormat[PushSubscriptionExpiredTime]
+ private implicit val deviceClientIdReads: Reads[DeviceClientId] = Json.valueReads[DeviceClientId]
+ private implicit val pushSubscriptionServerURLReads: Reads[PushSubscriptionServerURL] = Json.valueReads[PushSubscriptionServerURL]
+ private implicit val pushSubscriptionKeysReads: Reads[PushSubscriptionKeys] = Json.valueReads[PushSubscriptionKeys]
+ private implicit val typeNameReads: Reads[TypeName] = Json.valueReads[TypeName]
+
+ implicit val pushSubscriptionCreationRequest: Reads[PushSubscriptionCreationRequest] = Json.reads[PushSubscriptionCreationRequest]
+
+ private implicit val mapCreationRequestByPushSubscriptionCreationId: Reads[Map[PushSubscriptionCreationId, JsObject]] =
+ Reads.mapReads[PushSubscriptionCreationId, JsObject] {string => refineV[IdConstraint](string)
+ .fold(e => JsError(s"mailbox creationId needs to match id contraints: $e"),
+ id => JsSuccess(PushSubscriptionCreationId(id))) }
+
+ private implicit val pushSubscriptionSetRequestReads: Reads[PushSubscriptionSetRequest] = Json.reads[PushSubscriptionSetRequest]
+
+ private implicit val pushSubscriptionCreationResponseWrites: Writes[PushSubscriptionCreationResponse] = Json.writes[PushSubscriptionCreationResponse]
+
+ private implicit val pushSubscriptionMapSetErrorForCreationWrites: Writes[Map[PushSubscriptionCreationId, SetError]] =
+ mapWrites[PushSubscriptionCreationId, SetError](_.id.value, setErrorWrites)
+
+ private implicit val pushSubscriptionMapCreationResponseWrites: Writes[Map[PushSubscriptionCreationId, PushSubscriptionCreationResponse]] =
+ mapWrites[PushSubscriptionCreationId, PushSubscriptionCreationResponse](_.id.value, pushSubscriptionCreationResponseWrites)
+
+ private implicit val emailResponseSetWrites: OWrites[PushSubscriptionSetResponse] = Json.writes[PushSubscriptionSetResponse]
+
+ def deserializePushSubscriptionSetRequest(input: JsValue): JsResult[PushSubscriptionSetRequest] = Json.fromJson[PushSubscriptionSetRequest](input)
+
+ def serialize(response: PushSubscriptionSetResponse): JsObject = Json.toJsObject(response)
+}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/PushSubscriptionSetCreatePerformer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/PushSubscriptionSetCreatePerformer.scala
new file mode 100644
index 0000000..5a21dbc
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/PushSubscriptionSetCreatePerformer.scala
@@ -0,0 +1,81 @@
+package org.apache.james.jmap.method
+
+import org.apache.james.jmap.api.model.{PushSubscriptionCreationRequest, PushSubscriptionExpiredTime}
+import org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepository
+import org.apache.james.jmap.core.SetError.SetErrorDescription
+import org.apache.james.jmap.core.{PushSubscriptionCreation, PushSubscriptionCreationId, PushSubscriptionCreationParseException, PushSubscriptionCreationResponse, PushSubscriptionSetRequest, SetError}
+import org.apache.james.jmap.json.PushSubscriptionSerializer
+import org.apache.james.jmap.method.PushSubscriptionSetCreatePerformer.{CreationFailure, CreationResult, CreationResults, CreationSuccess}
+import org.apache.james.mailbox.MailboxSession
+import play.api.libs.json.{JsError, JsObject, JsPath, JsSuccess, Json, JsonValidationError}
+import reactor.core.scala.publisher.{SFlux, SMono}
+import reactor.core.scheduler.Schedulers
+
+import javax.inject.Inject
+
+object PushSubscriptionSetCreatePerformer {
+ trait CreationResult
+ case class CreationSuccess(clientId: PushSubscriptionCreationId, response: PushSubscriptionCreationResponse) extends CreationResult
+ case class CreationFailure(clientId: PushSubscriptionCreationId, e: Throwable) extends CreationResult {
+ def asMessageSetError: SetError = e match {
+ case e: IllegalArgumentException => SetError.invalidArguments(SetErrorDescription(e.getMessage))
+ case _ => SetError.serverFail(SetErrorDescription(e.getMessage))
+ }
+ }
+
+ case class CreationResults(results: Seq[CreationResult]) {
+ def created: Option[Map[PushSubscriptionCreationId, PushSubscriptionCreationResponse]] =
+ Option(results.flatMap{
+ case result: CreationSuccess => Some((result.clientId, result.response))
+ case _ => None
+ }.toMap)
+ .filter(_.nonEmpty)
+
+ def notCreated: Option[Map[PushSubscriptionCreationId, SetError]] = {
+ Option(results.flatMap{
+ case failure: CreationFailure => Some((failure.clientId, failure.asMessageSetError))
+ case _ => None
+ }
+ .toMap)
+ .filter(_.nonEmpty)
+ }
+ }
+}
+
+class PushSubscriptionSetCreatePerformer @Inject()(serializer: PushSubscriptionSerializer,
+ pushSubscriptionRepository: PushSubscriptionRepository) {
+ def create(request: PushSubscriptionSetRequest, mailboxSession: MailboxSession): SMono[CreationResults] =
+ SFlux.fromIterable(request.create.getOrElse(Map()))
+ .concatMap {
+ case (clientId, json) => parseCreate(json)
+ .fold(e => SMono.just[CreationResult](CreationFailure(clientId, new IllegalArgumentException(e.toString))),
+ creationRequest => create(clientId, creationRequest, mailboxSession))
+ }.collectSeq()
+ .map(CreationResults)
+
+ private def parseCreate(jsObject: JsObject): Either[PushSubscriptionCreationParseException, PushSubscriptionCreationRequest] =
+ PushSubscriptionCreation.validateProperties(jsObject)
+ .flatMap(validJsObject => Json.fromJson(validJsObject)(serializer.pushSubscriptionCreationRequest) match {
+ case JsSuccess(creationRequest, _) => Right(creationRequest)
+ case JsError(errors) => Left(PushSubscriptionCreationParseException(pushSubscriptionSetError(errors)))
+ })
+
+ private def create(clientId: PushSubscriptionCreationId, request: PushSubscriptionCreationRequest, mailboxSession: MailboxSession): SMono[CreationResult] =
+ SMono.fromPublisher(pushSubscriptionRepository.save(mailboxSession.getUser, request))
+ .map(subscription => CreationSuccess(clientId, PushSubscriptionCreationResponse(subscription.id, showExpires(subscription.expires, request))))
+ .onErrorResume(e => SMono.just[CreationResult](CreationFailure(clientId, e)))
+ .subscribeOn(Schedulers.elastic)
+
+ private def showExpires(expires: PushSubscriptionExpiredTime, request: PushSubscriptionCreationRequest): Option[PushSubscriptionExpiredTime] = request.expires match {
+ case Some(requestExpires) if expires.eq(requestExpires) => None
+ case _ => Some(expires)
+ }
+
+ private def pushSubscriptionSetError(errors: collection.Seq[(JsPath, collection.Seq[JsonValidationError])]): SetError =
+ errors.head match {
+ case (path, Seq()) => SetError.invalidArguments(SetErrorDescription(s"'$path' property in PushSubscription object is not valid"))
+ case (path, Seq(JsonValidationError(Seq("error.path.missing")))) => SetError.invalidArguments(SetErrorDescription(s"Missing '$path' property in PushSubscription object"))
+ case (path, Seq(JsonValidationError(Seq(message)))) => SetError.invalidArguments(SetErrorDescription(s"'$path' property in PushSubscription object is not valid: $message"))
+ case (path, _) => SetError.invalidArguments(SetErrorDescription(s"Unknown error on property '$path'"))
+ }
+}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/PushSubscriptionSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/PushSubscriptionSetMethod.scala
new file mode 100644
index 0000000..693225b
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/PushSubscriptionSetMethod.scala
@@ -0,0 +1,84 @@
+/****************************************************************
+ * 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.method
+
+import eu.timepit.refined.auto._
+import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, JMAP_CORE}
+import org.apache.james.jmap.core.Invocation.{Arguments, MethodName}
+import org.apache.james.jmap.core.{ClientId, ErrorCode, Id, Invocation, PushSubscriptionSetRequest, PushSubscriptionSetResponse, ServerId}
+import org.apache.james.jmap.json.{PushSubscriptionSerializer, ResponseSerializer}
+import org.apache.james.jmap.mail.{RequestTooLargeException, UnsupportedNestingException, UnsupportedRequestParameterException}
+import org.apache.james.mailbox.MailboxSession
+import org.apache.james.metrics.api.MetricFactory
+import play.api.libs.json.{JsError, JsSuccess}
+import reactor.core.scala.publisher.{SFlux, SMono}
+
+import javax.inject.Inject
+
+class PushSubscriptionSetMethod @Inject()(serializer: PushSubscriptionSerializer,
+ createPerformer: PushSubscriptionSetCreatePerformer,
+ val metricFactory: MetricFactory) extends Method {
+ override val methodName: Invocation.MethodName = MethodName("PushSubscription/set")
+ override val requiredCapabilities: Set[CapabilityIdentifier] = Set(JMAP_CORE)
+
+ override def process(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession): SMono[InvocationWithContext] = {
+ val either: Either[Exception, SMono[InvocationWithContext]] = for {
+ request <- getRequest(invocation.invocation)
+ } yield {
+ doProcess(invocation, mailboxSession, request)
+ }
+
+ val result: SFlux[InvocationWithContext] = SFlux.fromPublisher(either.fold(e => SFlux.error[InvocationWithContext](e), r => r))
+ .onErrorResume[InvocationWithContext] {
+ case e: UnsupportedRequestParameterException => SFlux.just[InvocationWithContext] (InvocationWithContext(Invocation.error(
+ ErrorCode.InvalidArguments,
+ s"The following parameter ${e.unsupportedParam} is syntactically valid, but is not supported by the server.",
+ invocation.invocation.methodCallId), invocation.processingContext))
+ case e: UnsupportedNestingException => SFlux.just[InvocationWithContext] (InvocationWithContext(Invocation.error(
+ ErrorCode.UnsupportedFilter,
+ description = e.message,
+ invocation.invocation.methodCallId), invocation.processingContext))
+ case e: IllegalArgumentException => SFlux.just[InvocationWithContext] (InvocationWithContext(Invocation.error(ErrorCode.InvalidArguments, e.getMessage, invocation.invocation.methodCallId), invocation.processingContext))
+ case e: RequestTooLargeException => SFlux.just[InvocationWithContext] (InvocationWithContext(Invocation.error(ErrorCode.RequestTooLarge, e.description, invocation.invocation.methodCallId), invocation.processingContext))
+ case e: Throwable => SFlux.error[InvocationWithContext] (e)
+ }
+
+ SMono.fromPublisher(metricFactory.decoratePublisherWithTimerMetric(JMAP_RFC8621_PREFIX + methodName.value, result))
+ }
+
+ private def getRequest(invocation: Invocation): Either[IllegalArgumentException, PushSubscriptionSetRequest] =
+ serializer.deserializePushSubscriptionSetRequest(invocation.arguments.value) match {
+ case JsSuccess(emailSetRequest, _) => Right(emailSetRequest)
+ case errors: JsError => Left(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
+ }
+
+ def doProcess(invocation: InvocationWithContext, mailboxSession: MailboxSession, request: PushSubscriptionSetRequest): SMono[InvocationWithContext] =
+ for {
+ created <- createPerformer.create(request, mailboxSession)
+ } yield InvocationWithContext(
+ invocation = Invocation(
+ methodName = methodName,
+ arguments = Arguments(serializer.serialize(PushSubscriptionSetResponse(
+ created = created.created,
+ notCreated = created.notCreated))),
+ methodCallId = invocation.invocation.methodCallId),
+ processingContext = invocation.processingContext
+ )
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org