diff --git a/.github/workflows/aot-test.yml b/.github/workflows/aot-test.yml index 259ece4..6cec2b9 100644 --- a/.github/workflows/aot-test.yml +++ b/.github/workflows/aot-test.yml @@ -37,9 +37,12 @@ jobs: # See if it runs at all target/graalvm-native-image/jelly-cli version - # Make sure reflection works + # Make sure reflection is supported target/graalvm-native-image/jelly-cli version | grep "JVM reflection: supported" + # Make sure large RDF/XML file parsing is supported + target/graalvm-native-image/jelly-cli version | grep "Large RDF/XML file parsing: supported" + # Test RDF conversions echo '_:b _:b .' > in.nt target/graalvm-native-image/jelly-cli \ diff --git a/.github/workflows/scala.yml b/.github/workflows/scala.yml index 6b04eb0..3c3301b 100644 --- a/.github/workflows/scala.yml +++ b/.github/workflows/scala.yml @@ -20,6 +20,8 @@ jobs: java: 17 - os: ubuntu-latest java: 21 + - os: ubuntu-latest + java: 24 runs-on: ${{ matrix.os }} steps: diff --git a/build.sbt b/build.sbt index 1354bb4..41bb740 100644 --- a/build.sbt +++ b/build.sbt @@ -27,7 +27,8 @@ lazy val graalOptions = Seq( if (isDevBuild) Seq("-Ob") else Seq("-O3", "--emit build-report"), ).flatten ++ Seq( "--features=eu.neverblink.jelly.cli.graal.ProtobufFeature," + - "eu.neverblink.jelly.cli.graal.JenaInternalsFeature", + "eu.neverblink.jelly.cli.graal.JenaInternalsFeature," + + "eu.neverblink.jelly.cli.graal.LargeXmlFeature", "-H:ReflectionConfigurationFiles=" + file("graal.json").getAbsolutePath, // Needed to skip initializing all charsets. // See: https://github.com/Jelly-RDF/cli/issues/154 diff --git a/src/main/scala/eu/neverblink/jelly/cli/command/Version.scala b/src/main/scala/eu/neverblink/jelly/cli/command/Version.scala index 4c4f5d4..66c2e14 100644 --- a/src/main/scala/eu/neverblink/jelly/cli/command/Version.scala +++ b/src/main/scala/eu/neverblink/jelly/cli/command/Version.scala @@ -29,25 +29,18 @@ object Version extends JellyCommand[VersionOptions]: .find(_.startsWith("org.apache.jena:jena-core:")).get.split(":")(2) val jellyV = BuildInfo.libraryDependencies .find(_.startsWith("eu.neverblink.jelly:jelly-jena:")).get.split(":")(2) - val reflectionSupported = JenaSystemOptions.disableTermValidation() + printLine(f""" |jelly-cli ${BuildInfo.version} - |---------------------------------------------- + |------------------------------------------------------------- |Jelly-JVM $jellyV |Apache Jena $jenaV |JVM ${System.getProperty("java.vm.name")} ${System.getProperty("java.vm.version")} - |---------------------------------------------- + |------------------------------------------------------------- |""".stripMargin.trim) // Print feature support info - reflectionSupported match { - case Failure(ex) => - printLine("[ ] JVM reflection: not supported. Parsing will be slower.") - if getOptions.common.debug then - printLine(" The exception was:") - ex.printStackTrace(out) - else printLine(" Run with --debug for details.") - case Success(_) => printLine("[X] JVM reflection: supported. Parsing optimizations enabled.") - } + printReflectionSupport() + printLargeXmlParsingSupport() // Print copyright info val buildYear = new SimpleDateFormat("yyyy").format(Date(BuildInfo.buildTime)) printLine(f""" @@ -57,3 +50,42 @@ object Version extends JellyCommand[VersionOptions]: |This software comes with no warranties and is provided 'as-is'. |Documentation and author list: https://github.com/Jelly-RDF/cli """.stripMargin) + + private def printReflectionSupport(): Unit = + val reflectionSupported = JenaSystemOptions.disableTermValidation() + reflectionSupported match { + case Failure(ex) => + printLine("[ ] JVM reflection: not supported. Parsing will be slower.") + if getOptions.common.debug then + printLine(" The exception was:") + ex.printStackTrace(out) + else printLine(" Run with --debug for details.") + case Success(_) => printLine("[X] JVM reflection: supported. Parsing optimizations enabled.") + } + + private def printLargeXmlParsingSupport(): Unit = + // See: https://github.com/Jelly-RDF/cli/issues/220 + val maxGeneralEntitySizeLimit = System.getProperty("jdk.xml.maxGeneralEntitySizeLimit") + val totalEntitySizeLimit = System.getProperty("jdk.xml.totalEntitySizeLimit") + val ok = + if Runtime.version().feature() <= 23 then + // JDK 23 and earlier did not have the new limits, so large XML files are always supported. + true + // 50M was the default totalEntitySizeLimit in JDK 23 and earlier. + // maxGeneralEntitySizeLimit was not defined (0) in JDK 23 and earlier. + else if maxGeneralEntitySizeLimit != null && maxGeneralEntitySizeLimit.toLong <= 0 && + totalEntitySizeLimit != null && (totalEntitySizeLimit.toLong <= 0 || totalEntitySizeLimit.toLong >= 50_000_000) + then true + else false + + if ok + then printLine("[X] Large RDF/XML file parsing: supported.") + else + printLine("[ ] Large RDF/XML file parsing: not supported.") + if getOptions.common.debug then + printLine( + f" jdk.xml.maxGeneralEntitySizeLimit = $maxGeneralEntitySizeLimit", + ) + printLine(f" jdk.xml.totalEntitySizeLimit = $totalEntitySizeLimit") + printLine(" To enable large XML parsing, set both properties to 0 (no limit).") + else printLine(" Run with --debug for details.") diff --git a/src/main/scala/eu/neverblink/jelly/cli/graal/LargeXmlFeature.scala b/src/main/scala/eu/neverblink/jelly/cli/graal/LargeXmlFeature.scala new file mode 100644 index 0000000..f4bff12 --- /dev/null +++ b/src/main/scala/eu/neverblink/jelly/cli/graal/LargeXmlFeature.scala @@ -0,0 +1,16 @@ +package eu.neverblink.jelly.cli.graal + +import org.graalvm.nativeimage.hosted.{Feature, RuntimeSystemProperties} + +class LargeXmlFeature extends Feature: + import Feature.* + + override def getDescription: String = + "Increases XML parsing limits to support large RDF/XML files." + + override def beforeAnalysis(access: BeforeAnalysisAccess): Unit = + // Support arbitrarily large RDF/XML files – needed since JDK 24. + // 0 indicates no limit. + // Issue: https://github.com/Jelly-RDF/cli/issues/220 + RuntimeSystemProperties.register("jdk.xml.maxGeneralEntitySizeLimit", "0") + RuntimeSystemProperties.register("jdk.xml.totalEntitySizeLimit", "0") diff --git a/src/test/scala/eu/neverblink/jelly/cli/command/VersionSpec.scala b/src/test/scala/eu/neverblink/jelly/cli/command/VersionSpec.scala index 7446fc2..eb33c37 100644 --- a/src/test/scala/eu/neverblink/jelly/cli/command/VersionSpec.scala +++ b/src/test/scala/eu/neverblink/jelly/cli/command/VersionSpec.scala @@ -18,6 +18,18 @@ class VersionSpec extends AnyWordSpec, Matchers: out should include("[X] JVM reflection: supported.") } + "report that large XML parsing is supported by default if running under JVM <= 23" in { + assume(Runtime.version().feature() <= 23, "Test only valid for JVM <= 23") + val (out, err) = Version.runTestCommand(List(alias)) + out should include("[X] Large RDF/XML file parsing: supported.") + } + + "report that large XML parsing is not supported by default if running under JVM >= 24" in { + assume(Runtime.version().feature() >= 24, "Test only valid for JVM >= 24") + val (out, err) = Version.runTestCommand(List(alias)) + out should include("[ ] Large RDF/XML file parsing: not supported.") + } + "include the copyright year" in { val (out, err) = Version.runTestCommand(List(alias)) val currentYear = java.time.Year.now.getValue.toString