From 453099a3e619cd1b027eefa37aad64b209e55094 Mon Sep 17 00:00:00 2001 From: Ostrzyciel Date: Tue, 15 Oct 2024 22:18:31 +0200 Subject: [PATCH 1/5] Move Fuseki integration to a Jena module Follow-up from #136 Fuseki modules are not supported in all Fuseki distributions, so it makes more sense to move the functionality to a Jena module. The only disadvantage is that the module loads regardless of whether we are in a Fuseki instance or not, but that can be solved with some exception catching... --- ...ache.jena.fuseki.main.sys.FusekiAutoModule | 1 - ...org.apache.jena.sys.JenaSubsystemLifecycle | 1 + ...odule.scala => JellyFusekiLifecycle.scala} | 31 ++++++++++--------- .../jena/fuseki/JellyFusekiModuleSpec.scala | 22 ++++++------- 4 files changed, 28 insertions(+), 27 deletions(-) delete mode 100644 jena/src/main/resources/META-INF/services/org.apache.jena.fuseki.main.sys.FusekiAutoModule rename jena/src/main/scala/eu/ostrzyciel/jelly/convert/jena/fuseki/{JellyFusekiModule.scala => JellyFusekiLifecycle.scala} (60%) diff --git a/jena/src/main/resources/META-INF/services/org.apache.jena.fuseki.main.sys.FusekiAutoModule b/jena/src/main/resources/META-INF/services/org.apache.jena.fuseki.main.sys.FusekiAutoModule deleted file mode 100644 index af06007d..00000000 --- a/jena/src/main/resources/META-INF/services/org.apache.jena.fuseki.main.sys.FusekiAutoModule +++ /dev/null @@ -1 +0,0 @@ -eu.ostrzyciel.jelly.convert.jena.fuseki.JellyFusekiModule \ No newline at end of file diff --git a/jena/src/main/resources/META-INF/services/org.apache.jena.sys.JenaSubsystemLifecycle b/jena/src/main/resources/META-INF/services/org.apache.jena.sys.JenaSubsystemLifecycle index de01b4e3..e79aa3d8 100644 --- a/jena/src/main/resources/META-INF/services/org.apache.jena.sys.JenaSubsystemLifecycle +++ b/jena/src/main/resources/META-INF/services/org.apache.jena.sys.JenaSubsystemLifecycle @@ -1 +1,2 @@ eu.ostrzyciel.jelly.convert.jena.riot.JellySubsystemLifecycle +eu.ostrzyciel.jelly.convert.jena.fuseki.JellyFusekiLifecycle \ No newline at end of file diff --git a/jena/src/main/scala/eu/ostrzyciel/jelly/convert/jena/fuseki/JellyFusekiModule.scala b/jena/src/main/scala/eu/ostrzyciel/jelly/convert/jena/fuseki/JellyFusekiLifecycle.scala similarity index 60% rename from jena/src/main/scala/eu/ostrzyciel/jelly/convert/jena/fuseki/JellyFusekiModule.scala rename to jena/src/main/scala/eu/ostrzyciel/jelly/convert/jena/fuseki/JellyFusekiLifecycle.scala index 94c320b9..65be6759 100644 --- a/jena/src/main/scala/eu/ostrzyciel/jelly/convert/jena/fuseki/JellyFusekiModule.scala +++ b/jena/src/main/scala/eu/ostrzyciel/jelly/convert/jena/fuseki/JellyFusekiLifecycle.scala @@ -3,28 +3,23 @@ package eu.ostrzyciel.jelly.convert.jena.fuseki import eu.ostrzyciel.jelly.core.Constants import org.apache.jena.atlas.web.{AcceptList, MediaRange} import org.apache.jena.fuseki.{DEF, Fuseki} -import org.apache.jena.fuseki.main.FusekiServer -import org.apache.jena.fuseki.main.sys.FusekiAutoModule -import org.apache.jena.rdf.model.Model -import org.apache.jena.riot.WebContent +import org.apache.jena.sys.JenaSubsystemLifecycle import java.util -object JellyFusekiModule: +object JellyFusekiLifecycle: val mediaRangeJelly: MediaRange = new MediaRange(Constants.jellyContentType) /** - * A Fuseki module that adds Jelly content type to the list of accepted content types. + * A Jena module that adds Jelly content type to the list of accepted content types in Fuseki. + * This isn't a Fuseki module, because Fuseki modules are not supported in all distributions of Fuseki, see: + * https://github.com/apache/jena/issues/2774 * * This allows users to use the Accept header set to application/x-jelly-rdf to request Jelly RDF responses. * It works for SPARQL CONSTRUCT queries and for the Graph Store Protocol. - * - * More info on Fuseki modules: https://jena.apache.org/documentation/fuseki2/fuseki-modules.html */ -final class JellyFusekiModule extends FusekiAutoModule: - import JellyFusekiModule.* - - override def name(): String = "Jelly" +final class JellyFusekiLifecycle extends JenaSubsystemLifecycle: + import JellyFusekiLifecycle.* override def start(): Unit = try { @@ -32,16 +27,22 @@ final class JellyFusekiModule extends FusekiAutoModule: maybeAddJellyToList(DEF.rdfOffer).foreach(offer => DEF.rdfOffer = offer) maybeAddJellyToList(DEF.quadsOffer).foreach(offer => { DEF.quadsOffer = offer - Fuseki.serverLog.info(s"Added ${Constants.jellyContentType} to the list of accepted content types") + Fuseki.serverLog.info(s"Jelly: Added ${Constants.jellyContentType} to the list of accepted content types") }) } catch { + case e: NoClassDefFoundError => // ignore, we are not running Fuseki case e: IllegalAccessError => Fuseki.serverLog.warn( - s"Cannot register the ${Constants.jellyContentType} content type, because you are running an Apache Jena " + - s"Fuseki version that doesn't support content type registration. " + + s"Jelly: Cannot register the ${Constants.jellyContentType} content type, because you are running an " + + s"Apache Jena Fuseki version that doesn't support content type registration. " + s"Update to Fuseki 5.2.0 or newer for this to work." ) } + override def stop(): Unit = () + + // Initialize after JellySubsystemLifecycle + override def level(): Int = 502 + /** * Adds the Jelly content type to the list of accepted content types if it is not already present. * @param list current list of accepted content types diff --git a/jena/src/test/scala/eu/ostrzyciel/jelly/convert/jena/fuseki/JellyFusekiModuleSpec.scala b/jena/src/test/scala/eu/ostrzyciel/jelly/convert/jena/fuseki/JellyFusekiModuleSpec.scala index 3f734109..fedca240 100644 --- a/jena/src/test/scala/eu/ostrzyciel/jelly/convert/jena/fuseki/JellyFusekiModuleSpec.scala +++ b/jena/src/test/scala/eu/ostrzyciel/jelly/convert/jena/fuseki/JellyFusekiModuleSpec.scala @@ -9,38 +9,38 @@ import scala.jdk.CollectionConverters.* class JellyFusekiModuleSpec extends AnyWordSpec, Matchers: "JellyFusekiModule" should { "have a name" in { - JellyFusekiModule().name() should be ("Jelly") + JellyFusekiLifecycle().name() should be ("Jelly") } "use the correct content type for Jelly" in { - JellyFusekiModule.mediaRangeJelly.getContentTypeStr should be ("application/x-jelly-rdf") + JellyFusekiLifecycle.mediaRangeJelly.getContentTypeStr should be ("application/x-jelly-rdf") } "register the Jelly content type in the lists of accepted content types" in { val oldLists = List(DEF.constructOffer, DEF.rdfOffer, DEF.quadsOffer) for list <- oldLists do - list.entries().asScala should not contain JellyFusekiModule.mediaRangeJelly - DEF.constructOffer.entries().asScala should not contain JellyFusekiModule.mediaRangeJelly - DEF.rdfOffer.entries().asScala should not contain JellyFusekiModule.mediaRangeJelly - DEF.quadsOffer.entries().asScala should not contain JellyFusekiModule.mediaRangeJelly + list.entries().asScala should not contain JellyFusekiLifecycle.mediaRangeJelly + DEF.constructOffer.entries().asScala should not contain JellyFusekiLifecycle.mediaRangeJelly + DEF.rdfOffer.entries().asScala should not contain JellyFusekiLifecycle.mediaRangeJelly + DEF.quadsOffer.entries().asScala should not contain JellyFusekiLifecycle.mediaRangeJelly - val module = JellyFusekiModule() + val module = JellyFusekiLifecycle() module.start() val lists = List(DEF.constructOffer, DEF.rdfOffer, DEF.quadsOffer) for (list, oldList) <- lists.zip(oldLists) do - list.entries().asScala should contain (JellyFusekiModule.mediaRangeJelly) + list.entries().asScala should contain (JellyFusekiLifecycle.mediaRangeJelly) list.entries().size() should be (oldList.entries().size() + 1) } "not register the Jelly content type if it's already registered" in { - val module = JellyFusekiModule() + val module = JellyFusekiLifecycle() module.start() - DEF.rdfOffer.entries().asScala should contain (JellyFusekiModule.mediaRangeJelly) + DEF.rdfOffer.entries().asScala should contain (JellyFusekiLifecycle.mediaRangeJelly) val size1 = DEF.rdfOffer.entries().size() module.start() - DEF.rdfOffer.entries().asScala should contain (JellyFusekiModule.mediaRangeJelly) + DEF.rdfOffer.entries().asScala should contain (JellyFusekiLifecycle.mediaRangeJelly) val size2 = DEF.rdfOffer.entries().size() size2 should be (size1) } From f6ea19e9af53859c52b6ea1e82696d9d57c9be24 Mon Sep 17 00:00:00 2001 From: Ostrzyciel Date: Tue, 15 Oct 2024 22:20:11 +0200 Subject: [PATCH 2/5] Docs --- docs/docs/getting-started-plugins.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/docs/getting-started-plugins.md b/docs/docs/getting-started-plugins.md index fb683b2f..9f95f3c6 100644 --- a/docs/docs/getting-started-plugins.md +++ b/docs/docs/getting-started-plugins.md @@ -20,13 +20,9 @@ You can simply add Jelly format support to [Apache Jena](https://jena.apache.org - For other applications, consult the manual of the application. - You can now use the Jelly format for parsing, serialization, and streaming serialization in your Jena application. -!!! bug "Content negotiation in Fuseki" +!!! warning "Content negotiation in Fuseki" - Content negotiation using the `application/x-jelly-rdf` media type in the `Accept` header works in Fuseki "Main" distribution since version 5.2.0. To get it working, you need to run Fuseki with the `--modules=true` command-line option. Content negotiation does not work in the "webapp" distribution with UI due to an [upstream bug](https://github.com/apache/jena/issues/2774). - - In Fuseki 5.1.0 and older, content negotiation with Jelly does not work at all. - - To work around these issues, you can specify the `output=application/x-jelly-rdf` parameter (either in the URL or in the URL-encoded form body) when querying the endpoint. + Content negotiation using the `application/x-jelly-rdf` media type in the `Accept` header works in Fuseki since Apache Jena version 5.2.0. Previous versions of Fuseki did not support media type registration. ### Eclipse RDF4J From a95c931a4c53ca924a725769fc27fbeac824e987 Mon Sep 17 00:00:00 2001 From: Ostrzyciel Date: Tue, 15 Oct 2024 22:23:59 +0200 Subject: [PATCH 3/5] Update test --- ...oduleSpec.scala => JellyFusekiLifecycleSpec.scala} | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) rename jena/src/test/scala/eu/ostrzyciel/jelly/convert/jena/fuseki/{JellyFusekiModuleSpec.scala => JellyFusekiLifecycleSpec.scala} (83%) diff --git a/jena/src/test/scala/eu/ostrzyciel/jelly/convert/jena/fuseki/JellyFusekiModuleSpec.scala b/jena/src/test/scala/eu/ostrzyciel/jelly/convert/jena/fuseki/JellyFusekiLifecycleSpec.scala similarity index 83% rename from jena/src/test/scala/eu/ostrzyciel/jelly/convert/jena/fuseki/JellyFusekiModuleSpec.scala rename to jena/src/test/scala/eu/ostrzyciel/jelly/convert/jena/fuseki/JellyFusekiLifecycleSpec.scala index fedca240..5617a9ab 100644 --- a/jena/src/test/scala/eu/ostrzyciel/jelly/convert/jena/fuseki/JellyFusekiModuleSpec.scala +++ b/jena/src/test/scala/eu/ostrzyciel/jelly/convert/jena/fuseki/JellyFusekiLifecycleSpec.scala @@ -1,15 +1,18 @@ package eu.ostrzyciel.jelly.convert.jena.fuseki +import eu.ostrzyciel.jelly.convert.jena.riot.JellySubsystemLifecycle import org.apache.jena.fuseki.DEF import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec import scala.jdk.CollectionConverters.* -class JellyFusekiModuleSpec extends AnyWordSpec, Matchers: - "JellyFusekiModule" should { - "have a name" in { - JellyFusekiLifecycle().name() should be ("Jelly") +class JellyFusekiLifecycleSpec extends AnyWordSpec, Matchers: + "JellyFusekiLifecycle" should { + "initialize after JenaSubsystemLifecycle" in { + val jenaModule = JellySubsystemLifecycle() + val module = JellyFusekiLifecycle() + module.level() should be > jenaModule.level() } "use the correct content type for Jelly" in { From a8acbdae0cbd94d4692303e7c01d9b000be0d009 Mon Sep 17 00:00:00 2001 From: Ostrzyciel Date: Tue, 15 Oct 2024 22:36:35 +0200 Subject: [PATCH 4/5] Fix test --- .../convert/jena/fuseki/JellyFusekiLifecycleSpec.scala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/jena/src/test/scala/eu/ostrzyciel/jelly/convert/jena/fuseki/JellyFusekiLifecycleSpec.scala b/jena/src/test/scala/eu/ostrzyciel/jelly/convert/jena/fuseki/JellyFusekiLifecycleSpec.scala index 5617a9ab..c9d92d1b 100644 --- a/jena/src/test/scala/eu/ostrzyciel/jelly/convert/jena/fuseki/JellyFusekiLifecycleSpec.scala +++ b/jena/src/test/scala/eu/ostrzyciel/jelly/convert/jena/fuseki/JellyFusekiLifecycleSpec.scala @@ -2,6 +2,7 @@ package eu.ostrzyciel.jelly.convert.jena.fuseki import eu.ostrzyciel.jelly.convert.jena.riot.JellySubsystemLifecycle import org.apache.jena.fuseki.DEF +import org.apache.jena.sys.JenaSystem import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -20,6 +21,11 @@ class JellyFusekiLifecycleSpec extends AnyWordSpec, Matchers: } "register the Jelly content type in the lists of accepted content types" in { + // This assumption is broken for `jenaPlugin` tests, because there the JenaSystem is initialized + // and reads the service definition. + // This test thus only can be executed in the `jena` module. + assume(DEF.constructOffer == DEF.constructOfferDefault()) + val oldLists = List(DEF.constructOffer, DEF.rdfOffer, DEF.quadsOffer) for list <- oldLists do list.entries().asScala should not contain JellyFusekiLifecycle.mediaRangeJelly From 11d03b41ed8f53643f5d641e86e874b1343f1be1 Mon Sep 17 00:00:00 2001 From: Ostrzyciel Date: Tue, 15 Oct 2024 22:37:07 +0200 Subject: [PATCH 5/5] Remove old code from test --- .../jelly/convert/jena/fuseki/JellyFusekiLifecycleSpec.scala | 3 --- 1 file changed, 3 deletions(-) diff --git a/jena/src/test/scala/eu/ostrzyciel/jelly/convert/jena/fuseki/JellyFusekiLifecycleSpec.scala b/jena/src/test/scala/eu/ostrzyciel/jelly/convert/jena/fuseki/JellyFusekiLifecycleSpec.scala index c9d92d1b..a0735687 100644 --- a/jena/src/test/scala/eu/ostrzyciel/jelly/convert/jena/fuseki/JellyFusekiLifecycleSpec.scala +++ b/jena/src/test/scala/eu/ostrzyciel/jelly/convert/jena/fuseki/JellyFusekiLifecycleSpec.scala @@ -29,9 +29,6 @@ class JellyFusekiLifecycleSpec extends AnyWordSpec, Matchers: val oldLists = List(DEF.constructOffer, DEF.rdfOffer, DEF.quadsOffer) for list <- oldLists do list.entries().asScala should not contain JellyFusekiLifecycle.mediaRangeJelly - DEF.constructOffer.entries().asScala should not contain JellyFusekiLifecycle.mediaRangeJelly - DEF.rdfOffer.entries().asScala should not contain JellyFusekiLifecycle.mediaRangeJelly - DEF.quadsOffer.entries().asScala should not contain JellyFusekiLifecycle.mediaRangeJelly val module = JellyFusekiLifecycle() module.start()