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