diff --git a/backend/js/src/main/scala/eu/joaocosta/minart/backend/JsResource.scala b/backend/js/src/main/scala/eu/joaocosta/minart/backend/JsResource.scala index 6c622471..6c1da5bd 100644 --- a/backend/js/src/main/scala/eu/joaocosta/minart/backend/JsResource.scala +++ b/backend/js/src/main/scala/eu/joaocosta/minart/backend/JsResource.scala @@ -1,12 +1,13 @@ package eu.joaocosta.minart.backend -import java.io.{ByteArrayInputStream, InputStream} +import java.io.{ByteArrayInputStream, ByteArrayOutputStream, InputStream, OutputStream} import scala.concurrent.{Future, Promise} import scala.io.Source import scala.scalajs.js -import scala.util.Try +import scala.util.{Try, Using} +import org.scalajs.dom import org.scalajs.dom.{ProgressEvent, XMLHttpRequest} import eu.joaocosta.minart.runtime.Resource @@ -16,22 +17,35 @@ import eu.joaocosta.minart.runtime.Resource final case class JsResource(resourcePath: String) extends Resource { def path = "./" + resourcePath + private def loadFromLocalStorage(): Option[String] = + Option(dom.window.localStorage.getItem(resourcePath)) + def withSource[A](f: Source => A): Try[A] = Try { - val xhr = new XMLHttpRequest() - xhr.open("GET", path, false) - xhr.send() - f(Source.fromString(xhr.responseText)) + val data = loadFromLocalStorage() match { + case Some(d) => d + case None => + val xhr = new XMLHttpRequest() + xhr.open("GET", path, false) + xhr.send() + xhr.responseText + } + f(Source.fromString(data)) } def withSourceAsync[A](f: Source => A): Future[A] = { val promise = Promise[A]() - val xhr = new XMLHttpRequest() - xhr.open("GET", path) - xhr.onloadend = (event: ProgressEvent) => { - if (xhr.status != 200) promise.failure(new Exception(xhr.statusText)) - else promise.complete(Try(f(Source.fromString(xhr.responseText)))) + loadFromLocalStorage() match { + case Some(data) => + promise.complete(Try(f(Source.fromString(data)))) + case None => + val xhr = new XMLHttpRequest() + xhr.open("GET", path) + xhr.onloadend = (event: ProgressEvent) => { + if (xhr.status != 200) promise.failure(new Exception(xhr.statusText)) + else promise.complete(Try(f(Source.fromString(xhr.responseText)))) + } + xhr.send() } - xhr.send() promise.future } @@ -41,22 +55,39 @@ final case class JsResource(resourcePath: String) extends Resource { def withInputStreamAsync[A](f: InputStream => A): Future[A] = { val promise = Promise[A]() - val xhr = new XMLHttpRequest() - xhr.open("GET", path) - xhr.overrideMimeType("text/plain; charset=x-user-defined") - xhr.onloadend = (event: ProgressEvent) => { - if (xhr.status != 200) promise.failure(new Exception(xhr.statusText)) - else promise.complete(Try(f(new ByteArrayInputStream(xhr.responseText.toCharArray.map(_.toByte))))) + loadFromLocalStorage() match { + case Some(data) => + val is = new ByteArrayInputStream(data.toCharArray.map(_.toByte)) + promise.complete(Try(f(is))) + case None => + val xhr = new XMLHttpRequest() + xhr.open("GET", path) + xhr.overrideMimeType("text/plain; charset=x-user-defined") + xhr.onloadend = (event: ProgressEvent) => { + if (xhr.status != 200) promise.failure(new Exception(xhr.statusText)) + else promise.complete(Try(f(new ByteArrayInputStream(xhr.responseText.toCharArray.map(_.toByte))))) + } + xhr.send() } - xhr.send() promise.future } def unsafeInputStream(): InputStream = { - val xhr = new XMLHttpRequest() - xhr.open("GET", path, false) - xhr.overrideMimeType("text/plain; charset=x-user-defined") - xhr.send() - new ByteArrayInputStream(xhr.responseText.toCharArray.map(_.toByte)) + val data = loadFromLocalStorage() match { + case Some(d) => d + case None => + val xhr = new XMLHttpRequest() + xhr.open("GET", path, false) + xhr.overrideMimeType("text/plain; charset=x-user-defined") + xhr.send() + xhr.responseText + } + new ByteArrayInputStream(data.toCharArray.map(_.toByte)) } + + def withOutputStream(f: OutputStream => Unit): Unit = + Using[ByteArrayOutputStream, Unit](new ByteArrayOutputStream()) { os => + f(os) + dom.window.localStorage.setItem(resourcePath, os.toByteArray().iterator.map(_.toChar).mkString("")) + } } diff --git a/backend/jvm/src/main/scala/eu/joaocosta/minart/backend/JavaResource.scala b/backend/jvm/src/main/scala/eu/joaocosta/minart/backend/JavaResource.scala index 93f4046d..2778e80c 100644 --- a/backend/jvm/src/main/scala/eu/joaocosta/minart/backend/JavaResource.scala +++ b/backend/jvm/src/main/scala/eu/joaocosta/minart/backend/JavaResource.scala @@ -1,6 +1,6 @@ package eu.joaocosta.minart.backend -import java.io.{FileInputStream, InputStream} +import java.io.{FileInputStream, FileOutputStream, InputStream, OutputStream} import scala.concurrent._ import scala.io.Source @@ -23,10 +23,7 @@ final case class JavaResource(resourcePath: String) extends Resource { def path = "./" + resourcePath def withSource[A](f: Source => A): Try[A] = { Using[Source, A]( - Source.fromInputStream( - Try(Option(this.getClass().getResourceAsStream("/" + resourcePath)).get) - .getOrElse(new FileInputStream(path)) - ) + Source.fromInputStream(unsafeInputStream()) )(f) } def withSourceAsync[A](f: Source => A): Future[A] = Future(blocking(withSource(f)).get) @@ -36,5 +33,8 @@ final case class JavaResource(resourcePath: String) extends Resource { // TODO use Try(Source.fromResource(resourcePath)).getOrElse(Source.fromFile(path)) on scala 2.12+ def unsafeInputStream(): InputStream = - Try(Option(this.getClass().getResourceAsStream("/" + resourcePath)).get).getOrElse(new FileInputStream(path)) + Try(new FileInputStream(path)).orElse(Try(Option(this.getClass().getResourceAsStream("/" + resourcePath)).get)).get + + def withOutputStream(f: OutputStream => Unit): Unit = + Using[OutputStream, Unit](new FileOutputStream(path))(f) } diff --git a/backend/native/src/main/scala/eu/joaocosta/minart/backend/NativeResource.scala b/backend/native/src/main/scala/eu/joaocosta/minart/backend/NativeResource.scala index 56f19382..ae1fe63f 100644 --- a/backend/native/src/main/scala/eu/joaocosta/minart/backend/NativeResource.scala +++ b/backend/native/src/main/scala/eu/joaocosta/minart/backend/NativeResource.scala @@ -1,6 +1,6 @@ package eu.joaocosta.minart.backend -import java.io.{FileInputStream, InputStream} +import java.io.{FileInputStream, FileOutputStream, InputStream, OutputStream} import scala.concurrent.Future import scala.io.Source @@ -24,10 +24,7 @@ final case class NativeResource(resourcePath: String) extends Resource { def path = "./" + resourcePath def withSource[A](f: Source => A): Try[A] = { Using[Source, A]( - Source.fromInputStream( - Try(Option(this.getClass().getResourceAsStream("/" + resourcePath)).get) - .getOrElse(new FileInputStream(path)) - ) + Source.fromInputStream(unsafeInputStream()) )(f) } def withSourceAsync[A](f: Source => A): Future[A] = @@ -35,7 +32,11 @@ final case class NativeResource(resourcePath: String) extends Resource { def withInputStream[A](f: InputStream => A): Try[A] = Using[InputStream, A](unsafeInputStream())(f) def withInputStreamAsync[A](f: InputStream => A): Future[A] = Future.fromTry(withInputStream(f)) + // TODO use Try(Source.fromResource(resourcePath)).getOrElse(Source.fromFile(path)) on scala 2.12+ def unsafeInputStream(): InputStream = - Try(Option(this.getClass().getResourceAsStream("/" + resourcePath)).get).getOrElse(new FileInputStream(path)) + Try(new FileInputStream(path)).orElse(Try(Option(this.getClass().getResourceAsStream("/" + resourcePath)).get)).get + + def withOutputStream(f: OutputStream => Unit): Unit = + Using[OutputStream, Unit](new FileOutputStream(path))(f) } diff --git a/core/shared/src/main/scala/eu/joaocosta/minart/runtime/Resource.scala b/core/shared/src/main/scala/eu/joaocosta/minart/runtime/Resource.scala index 7219dd09..2e5527c6 100644 --- a/core/shared/src/main/scala/eu/joaocosta/minart/runtime/Resource.scala +++ b/core/shared/src/main/scala/eu/joaocosta/minart/runtime/Resource.scala @@ -1,6 +1,6 @@ package eu.joaocosta.minart.runtime -import java.io.InputStream +import java.io.{InputStream, OutputStream} import scala.concurrent.Future import scala.io.Source @@ -44,6 +44,11 @@ trait Resource { * This method should only be used if for some reason the input stream must stay open (e.g. for data streaming) */ def unsafeInputStream(): InputStream + + /** Provides a [[java.io.OutputStream]] to write data to this resource location. + * The OutputStream is closed in the end, so it should not escape this call. + */ + def withOutputStream(f: OutputStream => Unit): Unit } object Resource {