This is an automated email from the ASF dual-hosted git repository. rcordier pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit c01bf3a985407595fbfb2d50329718e47abf68c0 Author: Rene Cordier <[email protected]> AuthorDate: Fri May 8 17:45:35 2020 +0700 JAMES-3091 JSON serialization for Mailbox POJO --- server/protocols/jmap-rfc-8621/pom.xml | 5 + .../org/apache/james/jmap/json/Serializer.scala | 73 ++++++++++- .../scala/org/apache/james/jmap/mail/Mailbox.scala | 2 +- .../scala/org/apache/james/jmap/mail/Quotas.scala | 19 ++- .../james/jmap/json/MailboxSerializationTest.scala | 135 +++++++++++++++++++++ .../org/apache/james/jmap/mail/MailboxTest.scala | 12 +- 6 files changed, 233 insertions(+), 13 deletions(-) diff --git a/server/protocols/jmap-rfc-8621/pom.xml b/server/protocols/jmap-rfc-8621/pom.xml index bb6a83b..9f2d220 100644 --- a/server/protocols/jmap-rfc-8621/pom.xml +++ b/server/protocols/jmap-rfc-8621/pom.xml @@ -78,6 +78,11 @@ <scope>test</scope> </dependency> <dependency> + <groupId>net.javacrumbs.json-unit</groupId> + <artifactId>json-unit-assertj</artifactId> + <scope>test</scope> + </dependency> + <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <scope>test</scope> diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/Serializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/Serializer.scala index 173a82d..87937a1 100644 --- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/Serializer.scala +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/Serializer.scala @@ -22,11 +22,14 @@ package org.apache.james.jmap.json import java.io.InputStream import java.net.URL -import org.apache.james.core.Username +import org.apache.james.core.{Domain, Username} +import org.apache.james.jmap.mail.{DelegatedNamespace, IsSubscribed, Mailbox, MailboxNamespace, MailboxRights, MayAddItems, MayCreateChild, MayDelete, MayReadItems, MayRemoveItems, MayRename, MaySetKeywords, MaySetSeen, MaySubmit, PersonalNamespace, Quota, QuotaId, QuotaRoot, Quotas, Right, Rights, SortOrder, TotalEmails, TotalThreads, UnreadEmails, UnreadThreads, Value} import org.apache.james.jmap.model import org.apache.james.jmap.model.CapabilityIdentifier.CapabilityIdentifier import org.apache.james.jmap.model.Invocation.{Arguments, MethodCallId, MethodName} import org.apache.james.jmap.model.{Account, Invocation, Session, _} +import org.apache.james.mailbox.Role +import org.apache.james.mailbox.model.{MailboxACL, MailboxId} import play.api.libs.functional.syntax._ import play.api.libs.json._ @@ -126,6 +129,70 @@ class Serializer { private implicit val sessionWrites: Writes[Session] = Json.writes[Session] + private implicit val mailboxIdWrites: Writes[MailboxId] = mailboxId => JsString(mailboxId.serialize) + private implicit val roleWrites: Writes[Role] = role => JsString(role.serialize) + private implicit val sortOrderWrites: Writes[SortOrder] = Json.valueWrites[SortOrder] + private implicit val totalEmailsWrites: Writes[TotalEmails] = Json.valueWrites[TotalEmails] + private implicit val unreadEmailsWrites: Writes[UnreadEmails] = Json.valueWrites[UnreadEmails] + private implicit val totalThreadsWrites: Writes[TotalThreads] = Json.valueWrites[TotalThreads] + private implicit val unreadThreadsWrites: Writes[UnreadThreads] = Json.valueWrites[UnreadThreads] + private implicit val isSubscribedWrites: Writes[IsSubscribed] = Json.valueWrites[IsSubscribed] + + private implicit val mayReadItemsWrites: Writes[MayReadItems] = Json.valueWrites[MayReadItems] + private implicit val mayAddItemsWrites: Writes[MayAddItems] = Json.valueWrites[MayAddItems] + private implicit val mayRemoveItemsWrites: Writes[MayRemoveItems] = Json.valueWrites[MayRemoveItems] + private implicit val maySetSeenWrites: Writes[MaySetSeen] = Json.valueWrites[MaySetSeen] + private implicit val maySetKeywordsWrites: Writes[MaySetKeywords] = Json.valueWrites[MaySetKeywords] + private implicit val mayCreateChildWrites: Writes[MayCreateChild] = Json.valueWrites[MayCreateChild] + private implicit val mayRenameWrites: Writes[MayRename] = Json.valueWrites[MayRename] + private implicit val mayDeleteWrites: Writes[MayDelete] = Json.valueWrites[MayDelete] + private implicit val maySubmitWrites: Writes[MaySubmit] = Json.valueWrites[MaySubmit] + private implicit val mailboxRightsWrites: Writes[MailboxRights] = Json.writes[MailboxRights] + + private implicit val personalNamespaceWrites: Writes[PersonalNamespace] = namespace => JsString("Personal") + private implicit val delegatedNamespaceWrites: Writes[DelegatedNamespace] = namespace => JsString(s"Delegated[${namespace.owner.asString}]") + private implicit val mailboxNamespaceWrites: Writes[MailboxNamespace] = Json.writes[MailboxNamespace] + + private implicit val mailboxACLWrites: Writes[MailboxACL.Right] = right => JsString(right.asCharacter.toString) + + private implicit val rightWrites: Writes[Right] = Json.valueWrites[Right] + private implicit val rightsWrites: Writes[Rights] = Json.valueWrites[Rights] + + private implicit def rightsMapWrites(implicit rightWriter: Writes[Seq[Right]]): Writes[Map[Username, Seq[Right]]] = + (m: Map[Username, Seq[Right]]) => { + m.foldLeft(JsObject.empty)((jsObject, kv) => { + val (username: Username, rights: Seq[Right]) = kv + jsObject.+(username.asString, rightWriter.writes(rights)) + }) + } + + private implicit val domainWrites: Writes[Domain] = domain => JsString(domain.asString) + private implicit val quotaRootWrites: Writes[QuotaRoot] = Json.writes[QuotaRoot] + private implicit val quotaIdWrites: Writes[QuotaId] = Json.valueWrites[QuotaId] + + private implicit val quotaValueWrites: Writes[Value] = Json.writes[Value] + private implicit val quotaWrites: Writes[Quota] = Json.valueWrites[Quota] + + private implicit def quotaMapWrites(implicit valueWriter: Writes[Value]): Writes[Map[Quotas.Type, Value]] = + (m: Map[Quotas.Type, Value]) => { + m.foldLeft(JsObject.empty)((jsObject, kv) => { + val (quotaType: Quotas.Type, value: Value) = kv + jsObject.+(quotaType.toString, valueWriter.writes(value)) + }) + } + + private implicit val quotasWrites: Writes[Quotas] = Json.valueWrites[Quotas] + + private implicit def quotasMapWrites(implicit quotaWriter: Writes[Quota]): Writes[Map[QuotaId, Quota]] = + (m: Map[QuotaId, Quota]) => { + m.foldLeft(JsObject.empty)((jsObject, kv) => { + val (quotaId: QuotaId, quota: Quota) = kv + jsObject.+(quotaId.getName, quotaWriter.writes(quota)) + }) + } + + private implicit val mailboxWrites: Writes[Mailbox] = Json.writes[Mailbox] + def serialize(session: Session): JsValue = { Json.toJson(session) } @@ -138,6 +205,10 @@ class Serializer { Json.toJson(responseObject) } + def serialize(mailbox: Mailbox): JsValue = { + Json.toJson(mailbox) + } + def deserializeRequestObject(input: String): JsResult[RequestObject] = { Json.parse(input).validate[RequestObject] } diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Mailbox.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Mailbox.scala index 09f881f..aaff40e 100644 --- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Mailbox.scala +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Mailbox.scala @@ -56,7 +56,7 @@ object MailboxNamespace { sealed trait MailboxNamespace -case object PersonalNamespace extends MailboxNamespace +case class PersonalNamespace() extends MailboxNamespace case class DelegatedNamespace(owner: Username) extends MailboxNamespace diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Quotas.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Quotas.scala index be64e75..c9d4212 100644 --- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Quotas.scala +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Quotas.scala @@ -19,10 +19,19 @@ package org.apache.james.jmap.mail -import java.util.Optional - +import org.apache.james.core.Domain import org.apache.james.jmap.model.UnsignedInt.UnsignedInt -import org.apache.james.mailbox.model.QuotaRoot +import org.apache.james.mailbox.model.{QuotaRoot => JavaQuotaRoot} + +import scala.compat.java8.OptionConverters._ + +object QuotaRoot{ + def fromJava(quotaRoot: JavaQuotaRoot) = QuotaRoot(quotaRoot.getValue, quotaRoot.getDomain.asScala) +} + +case class QuotaRoot(value: String, domain: Option[Domain]) { + def asJava: JavaQuotaRoot = JavaQuotaRoot.quotaRoot(value, domain.asJava) +} object Quotas { sealed trait Type @@ -39,11 +48,11 @@ object QuotaId { } case class QuotaId(quotaRoot: QuotaRoot) extends AnyVal { - def getName: String = quotaRoot.getValue + def getName: String = quotaRoot.value } case class Quota(quota: Map[Quotas.Type, Value]) extends AnyVal -case class Value(used: UnsignedInt, max: Optional[UnsignedInt]) +case class Value(used: UnsignedInt, max: Option[UnsignedInt]) case class Quotas(quotas: Map[QuotaId, Quota]) extends AnyVal \ No newline at end of file diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/MailboxSerializationTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/MailboxSerializationTest.scala new file mode 100644 index 0000000..5ab9690 --- /dev/null +++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/MailboxSerializationTest.scala @@ -0,0 +1,135 @@ +/**************************************************************** + * 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.auto._ +import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson +import org.apache.james.core.{Domain, Username} +import org.apache.james.jmap.json.MailboxSerializationTest.MAILBOX +import org.apache.james.jmap.mail.Mailbox.MailboxName +import org.apache.james.jmap.mail.{IsSubscribed, Mailbox, MailboxNamespace, MailboxRights, MayAddItems, MayCreateChild, MayDelete, MayReadItems, MayRemoveItems, MayRename, MaySetKeywords, MaySetSeen, MaySubmit, PersonalNamespace, Quota, QuotaId, QuotaRoot, Quotas, Right, Rights, SortOrder, TotalEmails, TotalThreads, UnreadEmails, UnreadThreads, Value} +import org.apache.james.mailbox.Role +import org.apache.james.mailbox.model.{MailboxId, TestId} +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec +import play.api.libs.json.Json + +object MailboxSerializationTest { + private val MAILBOX_ID: MailboxId = TestId.of(2) + private val MAILBOX_NAME: MailboxName = "inbox" + private val PARENT_ID: Option[MailboxId] = Option.apply(TestId.of(1)) + private val ROLE: Option[Role] = Option.apply(Role.INBOX) + private val SORT_ORDER: SortOrder = SortOrder(10L) + private val TOTAL_EMAILS: TotalEmails = TotalEmails(1234L) + private val UNREAD_EMAILS: UnreadEmails = UnreadEmails(123L) + private val TOTAL_THREADS: TotalThreads = TotalThreads(58L) + private val UNREAD_THREADS: UnreadThreads = UnreadThreads(22L) + private val IS_SUBSCRIBED: IsSubscribed = IsSubscribed(true) + private val NAMESPACE: MailboxNamespace = PersonalNamespace() + + private val MY_RIGHTS: MailboxRights = MailboxRights( + mayAddItems = MayAddItems(true), + mayReadItems = MayReadItems(false), + mayRemoveItems = MayRemoveItems(false), + maySetSeen = MaySetSeen(true), + maySetKeywords = MaySetKeywords(false), + mayCreateChild = MayCreateChild(true), + mayRename = MayRename(true), + mayDelete = MayDelete(false), + maySubmit = MaySubmit(false) + ) + + private val RIGHTS: Rights = Rights.of(Username.of("bob"), Seq(Right.Expunge, Right.Lookup)) + .append(Username.of("alice"), Seq(Right.Read, Right.Write)) + + private val QUOTAS = Quotas(Map( + QuotaId(QuotaRoot("quotaRoot", None)) -> Quota(Map( + Quotas.Message -> Value(18L, Some(42L)), + Quotas.Storage -> Value(12L, None))), + QuotaId(QuotaRoot("quotaRoot2@localhost", Some(Domain.LOCALHOST))) -> Quota(Map( + Quotas.Message -> Value(14L, Some(43L)), + Quotas.Storage -> Value(19L, None))))) + + private val MAILBOX: Mailbox = Mailbox( + id = MAILBOX_ID, + name = MAILBOX_NAME, + parentId = PARENT_ID, + role = ROLE, + sortOrder = SORT_ORDER, + totalEmails = TOTAL_EMAILS, + unreadEmails = UNREAD_EMAILS, + totalThreads = TOTAL_THREADS, + unreadThreads = UNREAD_THREADS, + myRights = MY_RIGHTS, + isSubscribed = IS_SUBSCRIBED, + namespace = NAMESPACE, + rights = RIGHTS, + quotas = QUOTAS + ) +} + +class MailboxSerializationTest extends AnyWordSpec with Matchers { + "Serialize Mailbox" should { + "succeed " in { + + val expectedJson = Json.parse( + """{ + | "id":"2", + | "name":"inbox", + | "parentId":"1", + | "role":"inbox", + | "sortOrder":10, + | "totalEmails":1234, + | "unreadEmails":123, + | "totalThreads":58, + | "unreadThreads":22, + | "myRights":{ + | "mayReadItems":false, + | "mayAddItems":true, + | "mayRemoveItems":false, + | "maySetSeen":true, + | "maySetKeywords":false, + | "mayCreateChild":true, + | "mayRename":true, + | "mayDelete":false, + | "maySubmit":false + | }, + | "isSubscribed":true, + | "namespace":"Personal", + | "rights":{ + | "bob":["e","l"], + | "alice":["r","w"] + | }, + | "quotas":{ + | "quotaRoot":{ + | "Message":{"used":18,"max":42}, + | "Storage":{"used":12} + | }, + | "quotaRoot2@localhost":{ + | "Message":{"used":14,"max":43}, + | "Storage":{"used":19} + | } + | } + |}""".stripMargin) + + assertThatJson(new Serializer().serialize(MAILBOX)).isEqualTo(expectedJson) + } + } +} diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/mail/MailboxTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/mail/MailboxTest.scala index 018c2bb..47e8c95 100644 --- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/mail/MailboxTest.scala +++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/mail/MailboxTest.scala @@ -58,7 +58,7 @@ class MailboxTest extends AnyWordSpec with Matchers { MayDelete(true), MaySubmit(true)), IsSubscribed(true), - MailboxNamespace.personal, + MailboxNamespace.personal(), Rights.EMPTY, Quotas(Map())) .hasRole(Role.INBOX) must be(false) @@ -84,7 +84,7 @@ class MailboxTest extends AnyWordSpec with Matchers { MayDelete(true), MaySubmit(true)), IsSubscribed(true), - MailboxNamespace.personal, + MailboxNamespace.personal(), Rights.EMPTY, Quotas(Map())) .hasRole(Role.INBOX) must be(false) @@ -110,7 +110,7 @@ class MailboxTest extends AnyWordSpec with Matchers { MayDelete(true), MaySubmit(true)), IsSubscribed(true), - MailboxNamespace.personal, + MailboxNamespace.personal(), Rights.EMPTY, Quotas(Map())).hasRole(Role.INBOX) must be(true) } @@ -138,7 +138,7 @@ class MailboxTest extends AnyWordSpec with Matchers { MayDelete(true), MaySubmit(true)), IsSubscribed(true), - MailboxNamespace.personal, + MailboxNamespace.personal(), Rights.EMPTY, Quotas(Map())) .hasSystemRole must be(false) @@ -164,7 +164,7 @@ class MailboxTest extends AnyWordSpec with Matchers { MayDelete(true), MaySubmit(true)), IsSubscribed(true), - MailboxNamespace.personal, + MailboxNamespace.personal(), Rights.EMPTY, Quotas(Map())).hasSystemRole must be(false) } @@ -189,7 +189,7 @@ class MailboxTest extends AnyWordSpec with Matchers { MayDelete(true), MaySubmit(true)), IsSubscribed(true), - MailboxNamespace.personal, + MailboxNamespace.personal(), Rights.EMPTY, Quotas(Map())).hasSystemRole must be(true) } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
