From 4dcef089679976983bbeaaf25df4081d48438587 Mon Sep 17 00:00:00 2001 From: Daniel Bell Date: Tue, 21 Nov 2023 15:33:39 +0100 Subject: [PATCH] Use FS2 in test classpath resource loading --- build.sbt | 3 + .../utils/ClasspathResourceLoader.scala | 60 ++++++++++++------- ...cala => ClasspathResourceLoaderSpec.scala} | 2 +- .../delta/rdf/shacl/ShaclShapesGraph.scala | 17 ++++-- 4 files changed, 55 insertions(+), 27 deletions(-) rename delta/kernel/src/test/scala/ch/epfl/bluebrain/nexus/delta/kernel/utils/{ClasspathResourceUtilsSpec.scala => ClasspathResourceLoaderSpec.scala} (95%) diff --git a/build.sbt b/build.sbt index 33200416cf..7d951581b4 100755 --- a/build.sbt +++ b/build.sbt @@ -205,6 +205,9 @@ lazy val kernel = project caffeine, catsCore, catsRetry, + catsEffect, + fs2, + fs2io, circeCore, circeParser, handleBars, diff --git a/delta/kernel/src/main/scala/ch/epfl/bluebrain/nexus/delta/kernel/utils/ClasspathResourceLoader.scala b/delta/kernel/src/main/scala/ch/epfl/bluebrain/nexus/delta/kernel/utils/ClasspathResourceLoader.scala index 4f7bfbd3fb..0288a8741e 100644 --- a/delta/kernel/src/main/scala/ch/epfl/bluebrain/nexus/delta/kernel/utils/ClasspathResourceLoader.scala +++ b/delta/kernel/src/main/scala/ch/epfl/bluebrain/nexus/delta/kernel/utils/ClasspathResourceLoader.scala @@ -1,23 +1,28 @@ package ch.epfl.bluebrain.nexus.delta.kernel.utils -import cats.effect.IO +import cats.effect.{IO, Resource} +import cats.implicits.catsSyntaxMonadError import ch.epfl.bluebrain.nexus.delta.kernel.utils.ClasspathResourceError.{InvalidJson, InvalidJsonObject, ResourcePathNotFound} import ch.epfl.bluebrain.nexus.delta.kernel.utils.ClasspathResourceLoader.handleBars import com.github.jknack.handlebars.{EscapingStrategy, Handlebars} +import fs2.text import io.circe.parser.parse import io.circe.{Json, JsonObject} -import java.io.InputStream +import java.io.{IOException, InputStream} import java.util.Properties -import scala.io.{Codec, Source} import scala.jdk.CollectionConverters._ class ClasspathResourceLoader private (classLoader: ClassLoader) { - final def absolutePath(resourcePath: String): IO[String] = - IO.fromOption(Option(getClass.getResource(resourcePath)) orElse Option(classLoader.getResource(resourcePath)))( - ResourcePathNotFound(resourcePath) - ).map(_.getPath) + final def absolutePath(resourcePath: String): IO[String] = { + IO.blocking( + Option(getClass.getResource(resourcePath)) + .orElse(Option(classLoader.getResource(resourcePath))) + .toRight(ResourcePathNotFound(resourcePath)) + ).rethrow + .map(_.getPath) + } /** * Loads the content of the argument classpath resource as an [[InputStream]]. @@ -28,12 +33,15 @@ class ClasspathResourceLoader private (classLoader: ClassLoader) { * the content of the referenced resource as an [[InputStream]] or a [[ClasspathResourceError]] when the resource * is not found */ - def streamOf(resourcePath: String): IO[InputStream] = - IO.defer { - lazy val fromClass = Option(getClass.getResourceAsStream(resourcePath)) - val fromClassLoader = Option(classLoader.getResourceAsStream(resourcePath)) - IO.fromOption(fromClass orElse fromClassLoader)(ResourcePathNotFound(resourcePath)) - } + def streamOf(resourcePath: String): Resource[IO, InputStream] = { + Resource.make[IO, InputStream] { + IO.blocking { + Option(getClass.getResourceAsStream(resourcePath)) + .orElse(Option(classLoader.getResourceAsStream(resourcePath))) + .toRight(ResourcePathNotFound(resourcePath)) + }.rethrow + } { is => IO.blocking(is.close()) } + } /** * Loads the content of the argument classpath resource as a string and replaces all the key matches of the @@ -48,11 +56,12 @@ class ClasspathResourceLoader private (classLoader: ClassLoader) { final def contentOf( resourcePath: String, attributes: (String, Any)* - ): IO[String] = + ): IO[String] = { resourceAsTextFrom(resourcePath).map { case text if attributes.isEmpty => text case text => handleBars.compileInline(text).apply(attributes.toMap.asJava) } + } /** * Loads the content of the argument classpath resource as a java Properties and transforms it into a Map of key @@ -65,10 +74,12 @@ class ClasspathResourceLoader private (classLoader: ClassLoader) { * is not found */ final def propertiesOf(resourcePath: String): IO[Map[String, String]] = - streamOf(resourcePath).map { is => - val props = new Properties() - props.load(is) - props.asScala.toMap + streamOf(resourcePath).use { is => + IO.blocking { + val props = new Properties() + props.load(is) + props.asScala.toMap + } } /** @@ -106,8 +117,17 @@ class ClasspathResourceLoader private (classLoader: ClassLoader) { jsonObj <- IO.fromOption(json.asObject)(InvalidJsonObject(resourcePath)) } yield jsonObj - private def resourceAsTextFrom(resourcePath: String): IO[String] = - streamOf(resourcePath).map(is => Source.fromInputStream(is)(Codec.UTF8).mkString) + private def resourceAsTextFrom(resourcePath: String): IO[String] = { + fs2.io + .readClassLoaderResource[IO](resourcePath, classLoader = classLoader) + .through(text.utf8.decode) + .compile + .string + .adaptError { + case e: IOException if Option(e.getMessage).exists(_.endsWith("not found")) => + ResourcePathNotFound(resourcePath) + } + } } object ClasspathResourceLoader { diff --git a/delta/kernel/src/test/scala/ch/epfl/bluebrain/nexus/delta/kernel/utils/ClasspathResourceUtilsSpec.scala b/delta/kernel/src/test/scala/ch/epfl/bluebrain/nexus/delta/kernel/utils/ClasspathResourceLoaderSpec.scala similarity index 95% rename from delta/kernel/src/test/scala/ch/epfl/bluebrain/nexus/delta/kernel/utils/ClasspathResourceUtilsSpec.scala rename to delta/kernel/src/test/scala/ch/epfl/bluebrain/nexus/delta/kernel/utils/ClasspathResourceLoaderSpec.scala index 45a5670bb3..ad51d4d77f 100644 --- a/delta/kernel/src/test/scala/ch/epfl/bluebrain/nexus/delta/kernel/utils/ClasspathResourceUtilsSpec.scala +++ b/delta/kernel/src/test/scala/ch/epfl/bluebrain/nexus/delta/kernel/utils/ClasspathResourceLoaderSpec.scala @@ -9,7 +9,7 @@ import org.scalatest.concurrent.ScalaFutures import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike -class ClasspathResourceUtilsSpec extends AnyWordSpecLike with Matchers with ScalaFutures { +class ClasspathResourceLoaderSpec extends AnyWordSpecLike with Matchers with ScalaFutures { private val loader: ClasspathResourceLoader = ClasspathResourceLoader() private def accept[A](io: IO[A]): A = diff --git a/delta/rdf/src/main/scala/ch/epfl/bluebrain/nexus/delta/rdf/shacl/ShaclShapesGraph.scala b/delta/rdf/src/main/scala/ch/epfl/bluebrain/nexus/delta/rdf/shacl/ShaclShapesGraph.scala index 35b6538dd5..106d96ec0c 100644 --- a/delta/rdf/src/main/scala/ch/epfl/bluebrain/nexus/delta/rdf/shacl/ShaclShapesGraph.scala +++ b/delta/rdf/src/main/scala/ch/epfl/bluebrain/nexus/delta/rdf/shacl/ShaclShapesGraph.scala @@ -12,6 +12,7 @@ import org.topbraid.shacl.engine.ShapesGraph import org.topbraid.shacl.util.SHACLUtil import org.topbraid.shacl.validation.ValidationUtil +import java.io.InputStream import java.net.URI /** @@ -31,12 +32,16 @@ object ShaclShapesGraph { def shaclShaclShapes: IO[ShaclShapesGraph] = loader .streamOf("shacl-shacl.ttl") - .map { is => - val model = ModelFactory - .createModelForGraph(createDefaultGraph()) - .read(is, "http://www.w3.org/ns/shacl-shacl#", FileUtils.langTurtle) - validateAndRegister(model) - } + .use(readModel) + .map(model => validateAndRegister(model)) + + private def readModel(is: InputStream) = { + IO.blocking { + ModelFactory + .createModelForGraph(createDefaultGraph()) + .read(is, "http://www.w3.org/ns/shacl-shacl#", FileUtils.langTurtle) + } + } /** * Creates a [[ShaclShapesGraph]] initializing and registering the required validation components from the passed