Skip to content

Instantly share code, notes, and snippets.

@ebruchez
Created January 4, 2024 22:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ebruchez/b57887e624234d228c426ba0d893c189 to your computer and use it in GitHub Desktop.
Save ebruchez/b57887e624234d228c426ba0d893c189 to your computer and use it in GitHub Desktop.
Example of `SubmissionProvider` in Scala
package org.orbeon.fr.offline
import org.orbeon.dom.io.XMLWriter
import org.orbeon.facades.{TextDecoder, TextEncoder}
import org.orbeon.oxf.http.{Headers, HttpMethod, StatusCode}
import org.orbeon.oxf.util.ContentTypes
import org.orbeon.sjsdom
import org.orbeon.xforms.XFormsCrossPlatformSupport
import org.orbeon.xforms.embedding.{SubmissionProvider, SubmissionRequest, SubmissionResponse}
import org.scalajs.dom.experimental.{Headers => FetchHeaders}
import org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits._
import scala.concurrent.Promise
import scala.concurrent.duration.DurationInt
import scala.scalajs.js
import scala.scalajs.js.JSConverters._
import scala.scalajs.js.typedarray.Uint8Array
import scala.util.{Failure, Success}
object DemoSubmissionProvider extends SubmissionProvider {
import org.orbeon.oxf.util.Logging._
import org.orbeon.xforms.offline.OfflineSupport._
case class FormData(contentTypeOpt: Option[String], data: Uint8Array, workflowStageOpt: Option[String])
private var store = Map[String, FormData]()
def submit(req: SubmissionRequest): SubmissionResponse = {
info(
s"handling submission",
List(
"method" -> req.method,
"path" -> req.url.pathname,
"headers" -> headersAsString(req)
)
)
req.url.pathname match {
case "/fr/service/custom/orbeon/controls/countries" =>
HttpMethod.withNameInsensitive(req.method) match {
case HttpMethod.GET =>
countriesService(req)
case _ =>
emptyBodyResponse(StatusCode.MethodNotAllowed)
}
case "/fr/service/custom/orbeon/echo" =>
echoService(req)
case _ =>
HttpMethod.withNameInsensitive(req.method) match {
case HttpMethod.GET =>
getFormData(req)
case HttpMethod.PUT =>
putFormData(req)
// TODO: check pathname is persistence path
case _ =>
emptyBodyResponse(StatusCode.MethodNotAllowed)
}
}
}
def submitAsync(req: SubmissionRequest): js.Promise[SubmissionResponse] = {
info(
s"handling async submission",
List(
"method" -> req.method,
"path" -> req.url.pathname,
"headers" -> headersAsString(req)
)
)
req.url.pathname match {
case DelayPath(delay) =>
HttpMethod.withNameInsensitive(req.method) match {
case HttpMethod.GET => getDelay(req, delay.toInt)
case _ => js.Promise.resolve[SubmissionResponse](emptyBodyResponse(StatusCode.MethodNotAllowed))
}
case path if path.endsWith(".bin") => // TODO: better matching
HttpMethod.withNameInsensitive(req.method) match {
case HttpMethod.PUT => putAttachmentAsync(req)
case HttpMethod.GET => getAttachmentAsync(req)
case _ => js.Promise.resolve[SubmissionResponse](emptyBodyResponse(StatusCode.MethodNotAllowed))
}
case _ =>
req.body.toOption match {
case Some(_: Uint8Array) | None =>
js.Promise.resolve[SubmissionResponse](submit(req))
case Some(body) =>
readUint8Array(body.asInstanceOf[sjsdom.ReadableStream[Uint8Array]]).toFuture.map { uint8ArrayBody =>
submit(
new SubmissionRequest {
val method = req.method
val url = req.url
val headers = req.headers
val body = uint8ArrayBody
}
)
}.toJSPromise
}
}
}
private def countriesService(req: SubmissionRequest): SubmissionResponse = {
val headersList = List(Headers.ContentType -> ContentTypes.XmlContentType)
new SubmissionResponse {
val statusCode = StatusCode.Ok
val headers = new FetchHeaders(headersList.toJSArray.map{ case (k, v) => js.Array(k, v) })
val body = new TextEncoder().encode("""<countries><country><name>India</name><us-code>in</us-code></country><country><name>USA</name><us-code>us</us-code></country></countries>""")
}
}
private def echoService(req: SubmissionRequest): SubmissionResponse =
new SubmissionResponse {
val statusCode = StatusCode.Ok
val headers = new FetchHeaders
val body = req.body
}
private def getFormData(req: SubmissionRequest): SubmissionResponse =
store.get(req.url.pathname) match {
case Some(FormData(responseContentTypeOpt, responseBody, workflowStageOpt)) =>
val headersList =
responseContentTypeOpt.map(Headers.ContentType ->).toList :::
workflowStageOpt .map(Headers.OrbeonWorkflowStage ->).toList
new SubmissionResponse {
val statusCode = StatusCode.Ok
val headers = new FetchHeaders(headersList.toJSArray.map{ case (k, v) => js.Array(k, v) })
val body = responseBody
}
case None =>
emptyBodyResponse(StatusCode.NotFound)
}
private def prettyFyXml(s: String): String =
XFormsCrossPlatformSupport.readOrbeonDom(s)
.getRootElement.serializeToString(XMLWriter.PrettyFormat)
private def putFormData(req: SubmissionRequest): SubmissionResponse =
req.body.toOption match {
case Some(body: Uint8Array) =>
if (Option(req.headers.get(Headers.ContentType)).exists(ContentTypes.isXMLContentType))
info(s"PUT XML body", List("body" -> prettyFyXml(new TextDecoder().decode(body))))
val existing = store.contains(req.url.pathname)
store += req.url.pathname ->
FormData(
Option(req.headers.get(Headers.ContentType)),
body,
Option(req.headers.get(Headers.OrbeonWorkflowStage))
)
emptyBodyResponse(if (existing) StatusCode.Ok else StatusCode.Created)
case Some(_) =>
warn("body is not `Uint8Array`")
emptyBodyResponse(StatusCode.BadRequest)
case None =>
warn("missing request body for `PUT`")
emptyBodyResponse(StatusCode.BadRequest)
}
private def getDelay(req: SubmissionRequest, delay: Int): js.Promise[SubmissionResponse] = {
val headersList = List(Headers.ContentType -> ContentTypes.JsonContentType)
val q = Option(req.url.searchParams.get("q")).getOrElse("null")
val p = Promise[SubmissionResponse]()
js.timers.setTimeout(delay.seconds) {
info(s"delayed for $delay seconds, q = $q")
p.success(
new SubmissionResponse {
val statusCode = StatusCode.Ok
val headers = new FetchHeaders(headersList.toJSArray.map{ case (k, v) => js.Array(k, v) })
val body = new TextEncoder().encode(s"""{"q": "$q"}""")
}
)
}
p.future.toJSPromise
}
private def putAttachmentAsync(req: SubmissionRequest): js.Promise[SubmissionResponse] = {
info(s"`PUT` attachment for path `${req.url.pathname}`")
val p = Promise[SubmissionResponse]()
req.body.toOption match {
case Some(body: Uint8Array) =>
info(s"`PUT` attachment: `Uint8Array`, total bytes = ${body.length}")
p.success(emptyBodyResponse(StatusCode.Created))
case Some(body) =>
info(s"`PUT` attachment: `ReadableStream`")
val stream = body.asInstanceOf[sjsdom.ReadableStream[Uint8Array]]
val reader = stream.getReader()
var totalBytes = 0
def readOneChunk(): Unit = {
val r = reader.read()
r.toFuture.onComplete {
case Success(chunk) if chunk.done =>
info(s"done reading attachment stream, total bytes = $totalBytes")
p.success(emptyBodyResponse(StatusCode.Ok))
case Success(chunk) =>
info(s"reading chunk of ${chunk.value.length} bytes")
totalBytes += chunk.value.length
readOneChunk()
case Failure(t) =>
p.failure(t)
}
}
readOneChunk()
case None =>
warn("`PUT` attachment: missing request body")
p.success(emptyBodyResponse(StatusCode.BadRequest))
}
p.future.toJSPromise
}
private def readUint8Array(stream: sjsdom.ReadableStream[Uint8Array]): js.Promise[Uint8Array] = {
val p = Promise[Uint8Array]()
val reader = stream.getReader()
var current: Uint8Array = new Uint8Array(0)
def readOneChunk(): Unit = {
val r = reader.read()
r.toFuture.onComplete {
case Success(chunk) if chunk.done =>
info(s"done reading attachment stream, total bytes = ${current.length}")
p.success(current)
case Success(chunk) =>
info(s"reading chunk of ${chunk.value.length} bytes")
// Work with copies, which is not ideal, but `transfer()` is not supported by all browsers
val c = current
current = new Uint8Array(c.length + chunk.value.length)
current.set(c)
current.set(chunk.value, c.length)
readOneChunk()
case Failure(t) =>
p.failure(t)
}
}
readOneChunk()
p.future.toJSPromise
}
private def getAttachmentAsync(req: SubmissionRequest): js.Promise[SubmissionResponse] = {
info(s"`GET` attachment for path `${req.url.pathname}`")
val p = Promise[SubmissionResponse]()
???
p.future.toJSPromise
}
private val DelayPath = """/delay/(\d+)""".r
private def headersAsString(req: SubmissionRequest): String =
req.headers.iterator map { array =>
val name = array(0)
val value = array(1)
s"$name=$value"
} mkString "&"
private def emptyBodyResponse(responseStatusCode: Int): SubmissionResponse =
new SubmissionResponse {
val statusCode = responseStatusCode
val headers = new FetchHeaders
val body = new Uint8Array(0)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment