Skip to content

Commit

Permalink
Merge branch 'issue/KRZ-212_webjar' into 'main'
Browse files Browse the repository at this point in the history
KRZ-212 Native Webjar Support

See merge request reactivecore/kreuzberg!73
  • Loading branch information
nob13 committed Jan 8, 2025
2 parents e61183b + 60d41e3 commit a93ab7a
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 10 deletions.
5 changes: 4 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,10 @@ lazy val extras = (crossProject(JSPlatform, JVMPlatform, NativePlatform) in file
lazy val miniserverCommon = (project in file("miniserver-common"))
.settings(
name := "kreuzberg-miniserver-common",
publishSettings
publishSettings,
libraryDependencies ++= Seq(
"org.webjars" % "jquery" % "3.7.1" % Test // For testing Webjar Loader
)
)
.dependsOn(lib.jvm, scalatags.jvm, rpc.jvm, testCore.jvm % Test)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package kreuzberg.miniserver

import java.io.{File, FileInputStream, InputStream}
import java.io.{File, FileInputStream, IOException, InputStream}
import java.nio.file.{Files, Paths}
import java.security.{DigestInputStream, MessageDigest}
import java.util.Properties
import scala.language.implicitConversions
import scala.util.Using
import scala.util.control.NonFatal

sealed trait Location {
def load(): InputStream
Expand Down Expand Up @@ -39,7 +41,11 @@ object Location {

case class ResourcePath(path: String) extends Location {
override def load(): InputStream = {
getClass.getClassLoader.getResourceAsStream(path)
val stream = getClass.getClassLoader.getResourceAsStream(path)
if (stream == null) {
throw new IOException(s"Could not load resource ${path}")
}
stream
}
}
}
Expand Down Expand Up @@ -87,6 +93,13 @@ object AssetCandidatePath {
}
}

/**
* Look in a directory
* @param dir
* directory base path
* @param prefix
* prefix which will be removed from any search file
*/
case class Directory(dir: String, prefix: String = "") extends AssetCandidatePath {
private val path = Paths.get(dir)
private val exists = Files.isDirectory(path)
Expand All @@ -112,4 +125,63 @@ object AssetCandidatePath {
}
}
}

/**
* Look into Webjars using [webjarname]/webjar-resource
*
* @param prefix
* prefix which will be removed from any search file
*/
case class Webjar(prefix: String = "") extends AssetCandidatePath {
private var versionCache: Map[String, String] = Map.empty
private val classLoader = getClass.getClassLoader

override def locate(path: String): Option[Location] = {
if (!path.startsWith(prefix)) {
return None
}
val normalized = Paths.get(path.stripPrefix(prefix)).normalize()
if (normalized.isAbsolute) {
// We need a path like [component]/[artefact/path/path]
return None
}
if (normalized.getNameCount <= 1) {
// No component/sub path given
return None
}
val component = normalized.getName(0).toString
val rest = normalized.subpath(1, normalized.getNameCount).toString
for {
version <- fetchMaybeCachedVersion(component)
fullPath = s"META-INF/resources/webjars/${component}/${version}/$rest"
_ <- Option(classLoader.getResource(fullPath))
} yield {
Location.ResourcePath(fullPath)
}
}

private def fetchMaybeCachedVersion(componentName: String): Option[String] = {
versionCache.get(componentName).orElse {
val got = fetchVersion(componentName)
got.foreach { version => versionCache = versionCache + (componentName -> version) }
got
}
}

private def fetchVersion(componentName: String): Option[String] = {
try {
val pomFile = s"META-INF/maven/org.webjars/${componentName}/pom.properties"
Option(classLoader.getResourceAsStream(pomFile)).flatMap { stream =>
Using.resource(stream) { _ =>
val props = new Properties()
props.load(stream)
Option(props.getProperty("version"))
}
}
} catch {
case NonFatal(_) =>
None
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ class Exporter(config: DeploymentConfig) {
case AssetCandidatePath.Directory(dir, prefix) =>
val sourceDir = Paths.get(dir)
copyPrefixedDirectory(sourceDir, assetDir, prefix)
case AssetCandidatePath.Webjar(prefix) =>
throw new IllegalStateException(s"Webjars not yet supported")
}

private def copyPrefixedJar(jarFile: Path, pathInJar: String, to: Path, prefix: String): Unit = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello Testcase!
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Should not be served
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package kreuzberg.miniserver

import kreuzberg.testcore.TestBase

import java.nio.charset.StandardCharsets
import java.nio.file.Files

class AssetCandidatePathTest extends TestBase {

"Resource" should "load resources" in {
val candidatePath = AssetCandidatePath.Resource("test_resource/sub1/", "foo/")

val bytes = candidatePath.locate("foo/Test.txt").value.load().readAllBytes()
new String(bytes, StandardCharsets.UTF_8).trim shouldBe "Hello Testcase!"

candidatePath.locate("foo/not_found") shouldBe empty
candidatePath.locate("foo/../sub2/Hidden.txt") shouldBe empty

val candidatePath2 = AssetCandidatePath.Resource("test_resource", "")
val bytes2 = candidatePath2.locate("sub1/Test.txt")
new String(bytes, StandardCharsets.UTF_8).trim shouldBe "Hello Testcase!"

candidatePath2.locate("../sub1/Test.txt") shouldBe empty
}

"Directory" should "load files" in {
val dir = Files.createTempDirectory("kreuzberg_test")
Files.createDirectory(dir.resolve("sub1"))
Files.createDirectory(dir.resolve("sub2"))
Files.writeString(dir.resolve("sub1/test.txt"), "Hello World")
Files.writeString(dir.resolve("sub2/test.txt"), "Hidden")

val candidatePath = AssetCandidatePath.Directory(dir.resolve("sub1").toString, "foo/")
new String(
candidatePath.locate("foo/test.txt").value.load().readAllBytes(),
StandardCharsets.UTF_8
) shouldBe "Hello World"
candidatePath.locate("foo/unknown") shouldBe empty
candidatePath.locate("test.txt") shouldBe empty

candidatePath.locate("foo/../sub2/test.txt") shouldBe empty
}

"Webjar" should "load webjars" in {
val candidatePath = AssetCandidatePath.Webjar("foo/")
val found = candidatePath.locate("foo/jquery/jquery.js").value
found.load().readAllBytes().size shouldBe >=(10)

candidatePath.locate("foo/jquery/missing.js") shouldBe empty
candidatePath.locate("jquery/jquery.js") shouldBe empty
candidatePath.locate("foo/other/jquery.js") shouldBe empty
candidatePath.locate("jquery") shouldBe empty
candidatePath.locate("foo/jquery") shouldBe empty
}
}
5 changes: 0 additions & 5 deletions project/metals.sbt

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package kreuzberg.testcore

import org.scalatest.{EitherValues, TryValues}
import org.scalatest.{EitherValues, OptionValues, TryValues}
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers

import scala.concurrent.duration.*
import scala.concurrent.{Await, Future}
import scala.reflect.ClassTag

abstract class TestBase extends AnyFlatSpec with Matchers with TryValues with EitherValues {
abstract class TestBase extends AnyFlatSpec with Matchers with TryValues with EitherValues with OptionValues {

def await[T](f: Future[T]): T = {
Await.result(f, 1.minute)
Expand Down

0 comments on commit a93ab7a

Please sign in to comment.