Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jena integration – add support for content negotiation #176

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ sonatypeRepository := "https://s01.oss.sonatype.org/service/local"

lazy val pekkoV = "1.1.2"
lazy val pekkoGrpcV = "1.1.0"
lazy val jenaV = "5.1.0"
lazy val jenaV = "5.2.0"
lazy val rdf4jV = "5.0.2"
// !! When updating ScalaPB also change the version of the plugin in plugins.sbt
lazy val scalapbV = "0.11.17"
Expand Down Expand Up @@ -104,6 +104,8 @@ lazy val jena = (project in file("jena"))
libraryDependencies ++= Seq(
"org.apache.jena" % "jena-core" % jenaV,
"org.apache.jena" % "jena-arq" % jenaV,
// Integration with Fuseki is optional, so include this dep as "provided"
"org.apache.jena" % "jena-fuseki-main" % jenaV % "provided,test",
),
commonSettings,
)
Expand All @@ -120,6 +122,7 @@ lazy val jenaPlugin = (project in file("jena-plugin"))
// Use the "provided" scope to not include the Jena dependencies in the plugin JAR
"org.apache.jena" % "jena-core" % jenaV % "provided,test",
"org.apache.jena" % "jena-arq" % jenaV % "provided,test",
"org.apache.jena" % "jena-fuseki-main" % jenaV % "provided,test",
),
// Do not publish this to Maven – we will separately do sbt assembly and publish to GitHub
publishArtifact := false,
Expand Down
6 changes: 4 additions & 2 deletions docs/docs/getting-started-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ You can simply add Jelly format support to [Apache Jena](https://jena.apache.org

!!! bug "Content negotiation in Fuseki"

Currently Apache Jena Fuseki will not properly handle content negotiation for the Jelly format, due to the supported content types being hardcoded in Fuseki (see [upstream issue](https://github.com/apache/jena/issues/2700)).
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.

Until that is fixed, you can use Jelly with Fuseki endpoints by specifying the `output=application/x-jelly-rdf` parameter (either in the URL or in the URL-encoded form body) when querying the endpoint.
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.


### Eclipse RDF4J
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
eu.ostrzyciel.jelly.convert.jena.fuseki.JellyFusekiModule
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ package eu.ostrzyciel.jelly.convert.jena

import eu.ostrzyciel.jelly.core.*
import eu.ostrzyciel.jelly.core.proto.v1.{GraphTerm, RdfStreamOptions, SpoTerm}
import org.apache.jena.JenaRuntime
import org.apache.jena.datatypes.xsd.XSDDatatype
import org.apache.jena.datatypes.xsd.impl.RDFLangString
import org.apache.jena.graph.*
import org.apache.jena.sparql.core.Quad

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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 java.util

object JellyFusekiModule:
val mediaRangeJelly: MediaRange = new MediaRange(Constants.jellyContentType)

/**
* A Fuseki module that adds Jelly content type to the list of accepted content types.
*
* 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"

override def start(): Unit =
try {
maybeAddJellyToList(DEF.constructOffer).foreach(offer => DEF.constructOffer = offer)
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")
})
} catch {
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"Update to Fuseki 5.2.0 or newer for this to work."
)
}

/**
* 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
* @return none or a new list with Jelly content type
*/
private def maybeAddJellyToList(list: AcceptList): Option[AcceptList] =
if list.entries().contains(mediaRangeJelly) then
None
else
val newList = new util.Vector[MediaRange](list.entries())
newList.add(mediaRangeJelly)
Some(new AcceptList(newList))
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package eu.ostrzyciel.jelly.convert.jena.fuseki

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 {
JellyFusekiModule().name() should be ("Jelly")
}

"use the correct content type for Jelly" in {
JellyFusekiModule.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

val module = JellyFusekiModule()
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().size() should be (oldList.entries().size() + 1)
}

"not register the Jelly content type if it's already registered" in {
val module = JellyFusekiModule()
module.start()
DEF.rdfOffer.entries().asScala should contain (JellyFusekiModule.mediaRangeJelly)
val size1 = DEF.rdfOffer.entries().size()

module.start()
DEF.rdfOffer.entries().asScala should contain (JellyFusekiModule.mediaRangeJelly)
val size2 = DEF.rdfOffer.entries().size()
size2 should be (size1)
}
}