JAMES-3376 BackReferences should allow reading arrays element
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/ProcessingContext.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/ProcessingContext.scala
index 8402d48..8506f2f 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/ProcessingContext.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/ProcessingContext.scala
@@ -19,6 +19,9 @@
 
 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}
@@ -28,9 +31,11 @@
 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 class WildcardPart() extends JsonPathPart
+case object WildcardPart extends JsonPathPart
 
 case class PlainPart(name: String) extends JsonPathPart {
   def read(jsValue: JsValue): JsResult[JsValue] = jsValue match {
@@ -39,13 +44,63 @@
   }
 }
 
+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 "" => None
-      case "*" => Some(WildcardPart())
-      case part => Some(PlainPart(part))
+      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]) {
@@ -55,7 +110,8 @@
       val tailAsJsonPath = JsonPath(tail)
       head match {
         case part: PlainPart => part.read(jsValue).flatMap(subPart => tailAsJsonPath.evaluate(subPart))
-        case _: WildcardPart => tailAsJsonPath.readWildcard(jsValue)
+        case part: ArrayElementPart => part.read(jsValue).flatMap(subPart => tailAsJsonPath.evaluate(subPart))
+        case WildcardPart => tailAsJsonPath.readWildcard(jsValue)
       }
   }
 
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/BackReferenceTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/BackReferenceTest.scala
index 7906f7e..6d31491 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/BackReferenceTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/BackReferenceTest.scala
@@ -21,10 +21,10 @@
 
 import eu.timepit.refined.auto._
 import org.apache.james.jmap.model.Invocation.{MethodCallId, MethodName}
-import org.apache.james.jmap.routes.{BackReference, JsonPath}
+import org.apache.james.jmap.routes.{ArrayElementPart, BackReference, JsonPath, PlainPart}
 import org.scalatest.matchers.should.Matchers
 import org.scalatest.wordspec.AnyWordSpec
-import play.api.libs.json.{JsError, JsSuccess, Json}
+import play.api.libs.json.{JsError, JsString, JsSuccess, Json}
 
 
 class BackReferenceTest extends AnyWordSpec with Matchers {
@@ -45,6 +45,30 @@
     }
   }
 
+  "Array element parsing" should {
+    "succeed when poositive" in {
+      ArrayElementPart.parse("[1]") should equal(Some(ArrayElementPart(1)))
+    }
+    "succeed when zero" in {
+      ArrayElementPart.parse("[0]") should equal(Some(ArrayElementPart(0)))
+    }
+    "fail when negative" in {
+      ArrayElementPart.parse("[-1]") should equal(None)
+    }
+    "fail when not an int" in {
+      ArrayElementPart.parse("[invalid]") should equal(None)
+    }
+    "fail when not closed" in {
+      ArrayElementPart.parse("[0") should equal(None)
+    }
+    "fail when not open" in {
+      ArrayElementPart.parse("0]") should equal(None)
+    }
+    "fail when no bracket" in {
+      ArrayElementPart.parse("0") should equal(None)
+    }
+  }
+
   "JsonPath evaluation" should {
     "noop when empty" in {
       val jsonPath = JsonPath.parse("")
@@ -60,6 +84,41 @@
 
       jsonPath.evaluate(json) should equal(JsSuccess(expected))
     }
+    "succeed when array element is present and root" in {
+      val jsonPath = JsonPath.parse("[1]")
+      val json = Json.parse("""["1", "2", "3"]""")
+      val expected = JsString("2")
+
+      jsonPath.evaluate(json) should equal(JsSuccess(expected))
+    }
+    "succeed when first array element is present" in {
+      val jsonPath = JsonPath.parse("path[0]")
+      val json = Json.parse("""{"path" : ["1", "2", "3"]}""")
+      val expected = JsString("1")
+
+      jsonPath.evaluate(json) should equal(JsSuccess(expected))
+    }
+    "fail when overflow" in {
+      val jsonPath = JsonPath.parse("path[3]")
+      val json = Json.parse("""{"path" : ["1", "2", "3"]}""")
+
+      jsonPath.evaluate(json) shouldBe a[JsError]
+    }
+    "parse should default to plain part when negative" in {
+      JsonPath.parse("path[-1]") should equal(JsonPath(List(PlainPart("path[-1]"))))
+    }
+    "parse should default to plain part when not an int" in {
+      JsonPath.parse("path[invalid]") should equal(JsonPath(List(PlainPart("path[invalid]"))))
+    }
+    "parse should default to plain part when empty" in {
+      JsonPath.parse("path[]") should equal(JsonPath(List(PlainPart("path[]"))))
+    }
+    "parse should default to plain part when not closed" in {
+      JsonPath.parse("path[36") should equal(JsonPath(List(PlainPart("path[36"))))
+    }
+    "parse should default to plain part when not closed and root" in {
+      JsonPath.parse("[36") should equal(JsonPath(List(PlainPart("[36"))))
+    }
     "succeed when array part is present" in {
       val jsonPath = JsonPath.parse("path/*")
       val json = Json.parse("""{"path" : ["1", "2"]}""")