Created
January 4, 2024 22:12
-
-
Save ebruchez/b57887e624234d228c426ba0d893c189 to your computer and use it in GitHub Desktop.
Example of `SubmissionProvider` in Scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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