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/02/09 04:29:41 UTC

[james-project] 13/33: JAMES-3491 StateChangeEvent Serialization

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 1947f84be42c6efdb68ac2bd42632c319b766483
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Tue Feb 2 11:52:43 2021 +0700

    JAMES-3491 StateChangeEvent Serialization
---
 server/protocols/jmap-rfc-8621/pom.xml             | 10 +++
 .../apache/james/jmap/change/EventDTOModule.scala  | 38 +++++++++++
 .../james/jmap/change/JmapEventSerializer.scala    | 76 ++++++++++++++++++++++
 .../org/apache/james/jmap/change/StateChange.scala | 57 ++++++++++++++++
 .../scala/org/apache/james/jmap/core/Session.scala |  6 ++
 .../james/jmap/json/ResponseSerializer.scala       |  7 ++
 .../change/StateChangeEventSerializerTest.scala    | 70 ++++++++++++++++++++
 7 files changed, 264 insertions(+)

diff --git a/server/protocols/jmap-rfc-8621/pom.xml b/server/protocols/jmap-rfc-8621/pom.xml
index 70a0624..9a6058e 100644
--- a/server/protocols/jmap-rfc-8621/pom.xml
+++ b/server/protocols/jmap-rfc-8621/pom.xml
@@ -65,6 +65,16 @@
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
+            <artifactId>james-json</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-json</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
             <artifactId>james-server-core</artifactId>
         </dependency>
         <dependency>
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/EventDTOModule.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/EventDTOModule.scala
new file mode 100644
index 0000000..f5a47ad
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/EventDTOModule.scala
@@ -0,0 +1,38 @@
+/****************************************************************
+ * 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.change
+
+import org.apache.james.events.Event
+import org.apache.james.json.{DTO, DTOModule}
+
+
+trait EventDTO extends DTO
+
+object EventDTOModule {
+  def forEvent[EventTypeT <: Event](eventType: Class[EventTypeT]) = new DTOModule.Builder[EventTypeT](eventType)
+}
+
+case class EventDTOModule[T <: Event, U <: EventDTO](converter: DTOModule.DTOConverter[T, U],
+                                                     toDomainObjectConverter: DTOModule.DomainObjectConverter[T, U],
+                                                     domainObjectType: Class[T],
+                                                     dtoType: Class[U],
+                                                     typeName: String) extends DTOModule[T, U](converter, toDomainObjectConverter, domainObjectType, dtoType, typeName) {
+  override def toDTO(domainObject: T) : U = super.toDTO(domainObject)
+}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/JmapEventSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/JmapEventSerializer.scala
new file mode 100644
index 0000000..2c4e28c
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/JmapEventSerializer.scala
@@ -0,0 +1,76 @@
+/****************************************************************
+ * 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.change
+
+import java.util.Optional
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import javax.inject.Inject
+import org.apache.james.core.Username
+import org.apache.james.events.Event.EventId
+import org.apache.james.events.{Event, EventSerializer}
+import org.apache.james.jmap.core.State
+import org.apache.james.json.JsonGenericSerializer
+
+import scala.jdk.CollectionConverters._
+import scala.jdk.OptionConverters._
+
+object StateChangeEventDTO {
+  val dtoModule: EventDTOModule[StateChangeEvent, StateChangeEventDTO] = EventDTOModule.forEvent(classOf[StateChangeEvent])
+    .convertToDTO(classOf[StateChangeEventDTO])
+    .toDomainObjectConverter(dto => dto.toDomainObject)
+    .toDTOConverter((event, aType) => StateChangeEventDTO.toDTO(event))
+    .typeName(classOf[StateChangeEvent].getCanonicalName)
+    .withFactory(EventDTOModule.apply);
+
+  def toDTO(event: StateChangeEvent): StateChangeEventDTO = StateChangeEventDTO(
+    getType = classOf[StateChangeEvent].getCanonicalName,
+    getEventId = event.eventId.getId.toString,
+    getUsername = event.username.asString(),
+    getMailboxState = event.mailboxState.map(_.value).map(_.toString).toJava,
+    getEmailState = event.emailState.map(_.value).map(_.toString).toJava)
+}
+
+case class StateChangeEventDTO(@JsonProperty("type") getType: String,
+                               @JsonProperty("eventId") getEventId: String,
+                               @JsonProperty("username") getUsername: String,
+                               @JsonProperty("mailboxState") getMailboxState: Optional[String],
+                               @JsonProperty("emailState") getEmailState: Optional[String]) extends EventDTO {
+  def toDomainObject: StateChangeEvent = StateChangeEvent(
+    eventId = EventId.of(getEventId),
+    username = Username.of(getUsername),
+    mailboxState = getMailboxState.toScala.map(State.fromStringUnchecked),
+    emailState = getEmailState.toScala.map(State.fromStringUnchecked))
+}
+
+case class JmapEventSerializer(dtoModules: Set[EventDTOModule[Event, EventDTO]]) extends EventSerializer {
+  @Inject
+  def this(javaModules: java.util.Set[EventDTOModule[Event, EventDTO]]) {
+    this(javaModules.asScala.toSet)
+  }
+
+  private val genericSerializer: JsonGenericSerializer[Event, EventDTO] = JsonGenericSerializer
+    .forModules(dtoModules.asJava)
+    .withoutNestedType()
+
+  override def toJson(event: Event): String = genericSerializer.serialize(event)
+
+  override def asEvent(serialized: String): Event = genericSerializer.deserialize(serialized)
+}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/StateChange.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/StateChange.scala
new file mode 100644
index 0000000..d6c555c
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/StateChange.scala
@@ -0,0 +1,57 @@
+/****************************************************************
+ * 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.change
+
+import org.apache.james.core.Username
+import org.apache.james.events.Event
+import org.apache.james.events.Event.EventId
+import org.apache.james.jmap.core.{AccountId, State}
+
+sealed trait TypeName {
+  def asMap(maybeState: Option[State]): Map[TypeName, State] =
+    maybeState.map(state => Map[TypeName, State](this -> state))
+      .getOrElse(Map())
+}
+case object MailboxTypeName extends TypeName
+case object EmailTypeName extends TypeName
+
+case class StateChange(changes: Map[AccountId, TypeState])
+
+case class TypeState(changes: Map[TypeName, State])
+
+case class StateChangeEvent(eventId: EventId,
+                            username: Username,
+                            mailboxState: Option[State],
+                            emailState: Option[State]) extends Event {
+
+  def asStateChange: StateChange =
+    StateChange(Map(AccountId.from(username).fold(
+      failure => throw new IllegalArgumentException(failure),
+      success => success) ->
+      TypeState(
+        MailboxTypeName.asMap(mailboxState) ++
+          EmailTypeName.asMap(emailState))))
+
+  override def getUsername: Username = username
+
+  override def isNoop: Boolean = false
+
+  override def getEventId: EventId = eventId
+}
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Session.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Session.scala
index bca220a..9ab5a13 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Session.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Session.scala
@@ -80,6 +80,12 @@ object State {
 
   val INSTANCE: State = fromJava(JavaState.INITIAL)
 
+  def fromStringUnchecked(value: String): State = 
+    refineV[Uuid](value)
+      .fold(
+        failure => throw new IllegalArgumentException(failure),
+        success => State.fromString(success))
+
   def fromString(value: UUIDString): State = State(UUID.fromString(value.value))
 
   def fromMailboxChanges(mailboxChanges: MailboxChanges): State = fromJava(mailboxChanges.getNewState)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala
index 71e04f7..2304565 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala
@@ -185,10 +185,17 @@ object ResponseSerializer {
       }
     case _ => JsError("Expecting a JsObject to represent a webSocket inbound request")
   }
+  private implicit val typeNameReads: Reads[TypeName] = {
+    case JsString(value) if value.equals(MailboxTypeName.asString()) => JsSuccess(MailboxTypeName)
+    case JsString(value) if value.equals(EmailTypeName.asString()) => JsSuccess(EmailTypeName)
+    case _ => JsError("Expecting a JsString as typeName")
+  }
+  private implicit val webSocketPushEnableReads: Reads[WebSocketPushEnable] = Json.reads[WebSocketPushEnable]
   private implicit val webSocketInboundReads: Reads[WebSocketInboundMessage] = {
     case json: JsObject =>
       json.value.get("@type") match {
         case Some(JsString("Request")) => webSocketRequestReads.reads(json)
+        case Some(JsString("WebSocketPushEnable")) => webSocketPushEnableReads.reads(json)
         case Some(JsString(unknownType)) => JsError(s"Unknown @type field on a webSocket inbound message: $unknownType")
         case Some(invalidType) => JsError(s"Invalid @type field on a webSocket inbound message: expecting a JsString, got $invalidType")
         case None => JsError(s"Missing @type field on a webSocket inbound message")
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/StateChangeEventSerializerTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/StateChangeEventSerializerTest.scala
new file mode 100644
index 0000000..cab2643
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/StateChangeEventSerializerTest.scala
@@ -0,0 +1,70 @@
+ /***************************************************************
+ * 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.change
+
+import org.apache.james.JsonSerializationVerifier
+import org.apache.james.core.Username
+import org.apache.james.events.Event.EventId
+import org.apache.james.jmap.change.StateChangeEventSerializerTest.{EVENT, EVENT_JSON}
+import org.apache.james.jmap.core.State
+import org.apache.james.json.JsonGenericSerializer
+import org.apache.james.json.JsonGenericSerializer.UnknownTypeException
+import org.assertj.core.api.Assertions.assertThatThrownBy
+import org.junit.jupiter.api.Test
+
+
+object StateChangeEventSerializerTest {
+  val EVENT_ID: EventId = EventId.of("6e0dd59d-660e-4d9b-b22f-0354479f47b4")
+  val USERNAME: Username = Username.of("bob")
+  val EVENT: StateChangeEvent = StateChangeEvent(EVENT_ID, USERNAME, Some(State.INSTANCE), Some(State.fromStringUnchecked("2d9f1b12-b35a-43e6-9af2-0106fb53a943")))
+  val EVENT_JSON: String =
+    """{
+      |  "eventId":"6e0dd59d-660e-4d9b-b22f-0354479f47b4",
+      |  "username":"bob",
+      |  "mailboxState":"2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+      |  "emailState":"2d9f1b12-b35a-43e6-9af2-0106fb53a943",
+      |  "type":"org.apache.james.jmap.change.StateChangeEvent"
+      |}""".stripMargin
+}
+
+class StateChangeEventSerializerTest {
+  @Test
+  def shouldSerializeKnownEvent(): Unit =
+    JsonSerializationVerifier.serializer(JsonGenericSerializer
+      .forModules(StateChangeEventDTO.dtoModule)
+      .withoutNestedType())
+      .bean(EVENT)
+      .json(EVENT_JSON)
+      .verify()
+
+  @Test
+  def shouldThrowWhenDeserializeUnknownEvent(): Unit =
+    assertThatThrownBy(() =>
+      JsonGenericSerializer
+        .forModules(StateChangeEventDTO.dtoModule)
+        .withoutNestedType()
+        .deserialize("""{
+                       |  "eventId":"6e0dd59d-660e-4d9b-b22f-0354479f47b4",
+                       |  "username":"bob",
+                       |  "mailboxState":"2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+                       |  "emailState":"2d9f1b12-b35a-43e6-9af2-0106fb53a943",
+                       |  "type":"org.apache.james.jmap.change.Unknown"
+                       |}""".stripMargin))
+      .isInstanceOf(classOf[UnknownTypeException])
+}
\ No newline at end of file


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