Skip to content

Commit ce2ab8c

Browse files
authored
Add SPARQL proto text format, reduce dependencies (#339)
1 parent 494b293 commit ce2ab8c

14 files changed

Lines changed: 257 additions & 78 deletions

File tree

‎README.md‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,6 @@ jelly-cli sparql from-jelly results.jellys --out-format=csv > results.csv
120120

121121
Both commands handle SELECT results (bindings) and ASK results (a boolean). All standard result formats (JSON, XML, CSV and TSV) are supported, plus a plain text table (`text`) for output only.
122122

123-
Jelly-SPARQL is an experimental draft and the format may still change.
124-
125123
### General tips
126124

127125
Use the `--help` option to learn more about all the available settings:

‎build.sbt‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ resolvers +=
1111
"Sonatype OSS Snapshots" at "https://central.sonatype.com/repository/maven-snapshots"
1212

1313
lazy val jenaV = "6.2.0"
14-
lazy val jellyV = "3.7.3+63-e0758361-SNAPSHOT"
14+
lazy val jellyV = "3.7.3+64-36186011-SNAPSHOT"
1515
lazy val graalvmV = "25.4.4.1.1"
1616

1717
addCommandAlias("fixAll", "scalafixAll; scalafmtAll")
@@ -34,8 +34,7 @@ lazy val graalOptions = Seq(
3434
// https://github.com/scala/scala3/pull/24109#issuecomment-3786629196
3535
"-J--sun-misc-unsafe-memory-access=allow",
3636
// Custom Graal features
37-
"--features=eu.neverblink.jelly.cli.graal.ProtobufFeature," +
38-
"eu.neverblink.jelly.cli.graal.JenaInternalsFeature," +
37+
"--features=eu.neverblink.jelly.cli.graal.JenaInternalsFeature," +
3938
"eu.neverblink.jelly.cli.graal.LargeXmlFeature",
4039
"-H:ReflectionConfigurationFiles=" + file("graal.json").getAbsolutePath,
4140
// Needed to skip initializing all charsets.
@@ -69,13 +68,11 @@ lazy val root = (project in file("."))
6968
("eu.neverblink.jelly" % "jelly-jena" % jellyV).excludeAll(ExclusionRule("org.apache.jena")),
7069
("eu.neverblink.jelly" % "jelly-jena-sparql" % jellyV)
7170
.excludeAll(ExclusionRule("org.apache.jena")),
72-
"eu.neverblink.jelly" % "jelly-core-protos-google" % jellyV,
7371
"com.github.alexarchambault" %% "case-app" % "2.1.0",
7472
"org.scalatest" %% "scalatest" % "3.2.20" % "test,test-serial",
7573
"org.yaml" % "snakeyaml" % "2.7" % Test,
7674
// For native-image reflection compatibility
7775
"org.graalvm.sdk" % "graal-sdk" % graalvmV % "provided",
78-
"org.reflections" % "reflections" % "0.10.2",
7976
),
8077
scalacOptions ++= Seq(
8178
"-Wunused:imports",

‎src/main/scala/eu/neverblink/jelly/cli/command/rdf/RdfFromJelly.scala‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import eu.neverblink.jelly.cli.*
55
import eu.neverblink.jelly.cli.command.rdf.util.*
66
import eu.neverblink.jelly.cli.command.rdf.util.RdfFormat.*
77
import eu.neverblink.jelly.cli.util.args.IndexRange
8+
import eu.neverblink.jelly.cli.util.io.ProtoText
89
import eu.neverblink.jelly.cli.util.jena.{
910
JenaSystemOptions,
1011
StreamRdfBatchWriter,
@@ -14,13 +15,13 @@ import eu.neverblink.jelly.convert.jena.JenaConverterFactory
1415
import eu.neverblink.jelly.core.JellyOptions
1516
import eu.neverblink.jelly.core.RdfHandler.AnyStatementHandler
1617
import eu.neverblink.jelly.core.proto.v1.RdfStreamFrame
17-
import eu.neverblink.jelly.core.proto.google.v1 as google
1818
import org.apache.jena.graph.{Node, Triple}
1919
import org.apache.jena.riot.system.StreamRDF
2020
import org.apache.jena.riot.system.StreamRDFWriter
2121
import org.apache.jena.sparql.core.Quad
2222

2323
import java.io.{InputStream, OutputStream}
24+
import java.nio.charset.StandardCharsets.UTF_8
2425
import scala.jdk.CollectionConverters.*
2526

2627
object RdfFromJellyPrint extends RdfCommandPrintUtil[RdfFormat.Writeable]:
@@ -187,10 +188,9 @@ object RdfFromJelly extends RdfSerDesCommand[RdfFromJellyOptions, RdfFormat.Writ
187188
inline def writeFrameToOutput(f: RdfStreamFrame, frameIndex: Int): Unit =
188189
// we want to write a comment to the file before each frame
189190
val comment = f"# Frame $frameIndex\n"
190-
outputStream.write(comment.getBytes)
191-
val frame = google.RdfStreamFrame.parseFrom(f.toByteArray).toString
192-
// the protoString is basically the jelly-txt format already
193-
outputStream.write(frame.getBytes)
191+
outputStream.write(comment.getBytes(UTF_8))
192+
val frame = ProtoText.print(RdfStreamFrame.getDescriptor, f.toByteArray)
193+
outputStream.write(frame.getBytes(UTF_8))
194194

195195
try {
196196
val it = JellyUtil.iterateRdfStream(inputStream)

‎src/main/scala/eu/neverblink/jelly/cli/command/rdf/RdfToJelly.scala‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
package eu.neverblink.jelly.cli.command.rdf
22

33
import caseapp.*
4-
import com.google.protobuf.TextFormat
54
import eu.neverblink.jelly.cli.*
65
import eu.neverblink.jelly.cli.command.rdf.util.*
76
import eu.neverblink.jelly.cli.command.rdf.util.RdfFormat.*
7+
import eu.neverblink.jelly.cli.util.io.ProtoText
88
import eu.neverblink.jelly.cli.util.jena.JenaSystemOptions
99
import eu.neverblink.jelly.cli.util.jena.riot.{JellyStreamWriterGraphs, RiotParserUtil}
1010
import eu.neverblink.jelly.convert.jena.JenaConverterFactory
1111
import eu.neverblink.jelly.convert.jena.riot.{JellyFormatVariant, JellyLanguage, JellyStreamWriter}
1212
import eu.neverblink.jelly.core.{JellyOptions, RdfProtoDeserializationError}
13-
import eu.neverblink.jelly.core.proto.google.v1 as google
1413
import eu.neverblink.jelly.core.proto.v1.*
1514
import eu.neverblink.jelly.core.utils.IoUtils
1615
import org.apache.jena.riot.system.StreamRDFWriter
1716
import org.apache.jena.riot.RIOT
1817

1918
import java.io.{BufferedReader, FileInputStream, InputStream, InputStreamReader, OutputStream}
19+
import java.nio.charset.StandardCharsets.UTF_8
2020
import scala.util.Using
2121

2222
object RdfToJellyPrint extends RdfCommandPrintUtil[RdfFormat.Readable]:
@@ -216,10 +216,10 @@ object RdfToJelly extends RdfSerDesCommand[RdfToJellyOptions, RdfFormat.Readable
216216
"NEVER use it in production.\nUse --quiet to silence this warning.",
217217
true,
218218
)
219-
Using.resource(InputStreamReader(inputStream)) { r1 =>
219+
Using.resource(InputStreamReader(inputStream, UTF_8)) { r1 =>
220220
Using.resource(BufferedReader(r1)) { reader =>
221221
jellyTextStreamAsFrames(reader)
222-
.map(txt => TextFormat.parse(txt, classOf[google.RdfStreamFrame]))
222+
.map(txt => ProtoText.parse(RdfStreamFrame.getDescriptor, txt))
223223
.foreach(frame => {
224224
if getOptions.delimited then frame.writeDelimitedTo(outputStream)
225225
else frame.writeTo(outputStream)
Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
package eu.neverblink.jelly.cli.command.rdf.util
22

3-
import eu.neverblink.jelly.core.proto.google.v1 as google
3+
import eu.neverblink.jelly.cli.util.io.ProtoText
44
import eu.neverblink.jelly.core.proto.v1.RdfStreamOptions
55

66
object StreamOptionsUtil:
77

8-
def prettyPrint(streamOptions: RdfStreamOptions): String = {
9-
val streamOptionsBytes = streamOptions.toByteArray
10-
val googleStreamOptions = google.RdfStreamOptions.parseFrom(streamOptionsBytes)
11-
return googleStreamOptions.toString
12-
}
8+
def prettyPrint(streamOptions: RdfStreamOptions): String =
9+
ProtoText.print(RdfStreamOptions.getDescriptor, streamOptions.toByteArray)

‎src/main/scala/eu/neverblink/jelly/cli/command/sparql/SparqlFromJelly.scala‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ object SparqlFromJellyPrint:
1414
"If no input file is specified, the input is read from stdin.\n" +
1515
"If no output file is specified, the output is written to stdout.\n" +
1616
"Both SELECT results (bindings) and ASK results (a boolean) are supported.\n" +
17+
"The jelly-sparql-text format is a human-readable version of Jelly-SPARQL, for debugging.\n" +
1718
"If an error is detected, the program will exit with a non-zero code.\n" +
1819
"Otherwise, the program will exit with code 0.",
1920
)

‎src/main/scala/eu/neverblink/jelly/cli/command/sparql/SparqlSerDesCommand.scala‎

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
package eu.neverblink.jelly.cli.command.sparql
22

33
import caseapp.*
4-
import com.google.protobuf.InvalidProtocolBufferException
4+
import com.google.protobuf.{InvalidProtocolBufferException, TextFormat}
55
import eu.neverblink.jelly.cli.*
66
import eu.neverblink.jelly.cli.command.sparql.util.SparqlFormat
7+
import eu.neverblink.jelly.cli.util.io.ProtoText
78
import eu.neverblink.jelly.core.{RdfProtoDeserializationError, RdfProtoSerializationError}
9+
import eu.neverblink.jelly.core.proto.v1.sparql.SparqlResultsFrame
10+
import eu.neverblink.jelly.core.sparql.JellySparqlIoUtils
811
import org.apache.jena.riot.{RIOT, RiotException}
912
import org.apache.jena.riot.resultset.{ResultSetReaderRegistry, ResultSetWriterRegistry}
1013

11-
import java.io.{InputStream, OutputStream}
14+
import java.io.{BufferedReader, InputStream, InputStreamReader, OutputStream}
15+
import java.nio.charset.StandardCharsets.UTF_8
16+
import scala.util.Using
1217

1318
/** Common logic for the two SPARQL result set conversion commands.
1419
*/
@@ -51,13 +56,15 @@ abstract class SparqlSerDesCommand[T <: HasJellyCommandOptions: {Parser, Help}]
5156
outputStream: OutputStream,
5257
): Unit =
5358
try {
54-
val context = RIOT.getContext.copy()
55-
val reader = ResultSetReaderRegistry.getFactory(from.jenaLang).create(from.jenaLang)
56-
val writer = ResultSetWriterRegistry.getFactory(to.jenaLang).create(to.jenaLang)
57-
val result = reader.readAny(inputStream, context)
58-
if result.isBoolean then
59-
writer.write(outputStream, result.getBooleanResult.booleanValue, context)
60-
else writer.write(outputStream, result.getResultSet, context)
59+
(from, to) match
60+
case (SparqlFormat.JellySparql, SparqlFormat.JellySparqlText) =>
61+
jellyBinaryToText(inputStream, outputStream)
62+
case (SparqlFormat.JellySparqlText, SparqlFormat.JellySparql) =>
63+
jellyTextToBinary(inputStream, outputStream)
64+
case (f: SparqlFormat.Jena, t: SparqlFormat.Jena) =>
65+
jenaConvert(f, t, inputStream, outputStream)
66+
case _ =>
67+
throw CriticalException(f"Conversion from $from to $to is not supported.")
6168
outputStream.flush()
6269
} catch
6370
// The Jelly RowSet reader wraps I/O errors (including protobuf ones) in a RiotException,
@@ -72,3 +79,61 @@ abstract class SparqlSerDesCommand[T <: HasJellyCommandOptions: {Parser, Help}]
7279
throw JellyDeserializationError(e.getMessage)
7380
case e: RdfProtoSerializationError =>
7481
throw JellySerializationError(e.getMessage)
82+
case e: TextFormat.ParseException =>
83+
throw InvalidJellyFile(e)
84+
85+
private def jenaConvert(
86+
from: SparqlFormat.Jena,
87+
to: SparqlFormat.Jena,
88+
inputStream: InputStream,
89+
outputStream: OutputStream,
90+
): Unit =
91+
val context = RIOT.getContext.copy()
92+
val reader = ResultSetReaderRegistry.getFactory(from.jenaLang).create(from.jenaLang)
93+
val writer = ResultSetWriterRegistry.getFactory(to.jenaLang).create(to.jenaLang)
94+
val result = reader.readAny(inputStream, context)
95+
if result.isBoolean then
96+
writer.write(outputStream, result.getBooleanResult.booleanValue, context)
97+
else writer.write(outputStream, result.getResultSet, context)
98+
99+
private val frameCommentPrefix = "# Frame"
100+
101+
private def jellyBinaryToText(inputStream: InputStream, outputStream: OutputStream): Unit =
102+
val response = JellySparqlIoUtils.autodetectDelimiting(inputStream)
103+
val input = response.newInput()
104+
val frames =
105+
if response.isDelimited then
106+
Iterator.continually(SparqlResultsFrame.parseDelimitedFrom(input)).takeWhile(_ != null)
107+
else Iterator(SparqlResultsFrame.parseFrom(input))
108+
for (frame, i) <- frames.zipWithIndex do
109+
outputStream.write(f"$frameCommentPrefix $i\n".getBytes(UTF_8))
110+
val text = ProtoText.print(SparqlResultsFrame.getDescriptor, frame.toByteArray)
111+
outputStream.write(text.getBytes(UTF_8))
112+
113+
/** Frames are split on the "# Frame" comments. Without them, the whole input is one frame. */
114+
private def jellyTextToBinary(inputStream: InputStream, outputStream: OutputStream): Unit =
115+
if !isQuietMode then
116+
printLine(
117+
"WARNING: The Jelly-SPARQL text format is not stable and may change in incompatible " +
118+
"ways in the future.\nIt's only intended for testing and development.\n" +
119+
"NEVER use it in production.\nUse --quiet to silence this warning.",
120+
true,
121+
)
122+
def writeFrame(text: String): Unit =
123+
ProtoText.parse(SparqlResultsFrame.getDescriptor, text).writeDelimitedTo(outputStream)
124+
125+
Using.resource(BufferedReader(InputStreamReader(inputStream, UTF_8))) { reader =>
126+
val buffer = StringBuilder()
127+
var hasContent = false
128+
Iterator.continually(reader.readLine()).takeWhile(_ != null).foreach { line =>
129+
if line.startsWith(frameCommentPrefix) then
130+
if hasContent then writeFrame(buffer.toString)
131+
buffer.clear()
132+
hasContent = false
133+
else
134+
buffer.append(line).append('\n')
135+
val trimmed = line.trim
136+
if trimmed.nonEmpty && !trimmed.startsWith("#") then hasContent = true
137+
}
138+
if hasContent then writeFrame(buffer.toString)
139+
}

‎src/main/scala/eu/neverblink/jelly/cli/command/sparql/SparqlToJelly.scala‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ object SparqlToJellyPrint:
1414
"If no input file is specified, the input is read from stdin.\n" +
1515
"If no output file is specified, the output is written to stdout.\n" +
1616
"Both SELECT results (bindings) and ASK results (a boolean) are supported.\n" +
17+
"The input can also be in the jelly-sparql-text format, which is written by sparql from-jelly.\n" +
1718
"If an error is detected, the program will exit with a non-zero code.\n" +
1819
"Otherwise, the program will exit with code 0.",
1920
)

‎src/main/scala/eu/neverblink/jelly/cli/command/sparql/util/SparqlFormat.scala‎

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import org.apache.jena.riot.resultset.ResultSetLang
1212
sealed trait SparqlFormat:
1313
val fullName: String
1414
val cliOptions: List[String]
15-
val jenaLang: Lang
1615
override final def toString: String = fullName
1716

1817
object SparqlFormat:
@@ -23,41 +22,51 @@ object SparqlFormat:
2322
/** Formats we can write a result set to. */
2423
sealed trait Writeable extends SparqlFormat
2524

26-
case object Json extends SparqlFormat.Readable, SparqlFormat.Writeable:
25+
/** Formats handled by Jena's result set readers and writers. */
26+
sealed trait Jena extends SparqlFormat:
27+
val jenaLang: Lang
28+
29+
case object Json extends SparqlFormat.Jena, SparqlFormat.Readable, SparqlFormat.Writeable:
2730
override val fullName: String = "SPARQL results JSON"
2831
override val cliOptions: List[String] = List("json", "srj")
2932
override val jenaLang: Lang = ResultSetLang.RS_JSON
3033

31-
case object Xml extends SparqlFormat.Readable, SparqlFormat.Writeable:
34+
case object Xml extends SparqlFormat.Jena, SparqlFormat.Readable, SparqlFormat.Writeable:
3235
override val fullName: String = "SPARQL results XML"
3336
override val cliOptions: List[String] = List("xml", "srx")
3437
override val jenaLang: Lang = ResultSetLang.RS_XML
3538

36-
case object Csv extends SparqlFormat.Readable, SparqlFormat.Writeable:
39+
case object Csv extends SparqlFormat.Jena, SparqlFormat.Readable, SparqlFormat.Writeable:
3740
override val fullName: String = "CSV"
3841
override val cliOptions: List[String] = List("csv")
3942
override val jenaLang: Lang = ResultSetLang.RS_CSV
4043

41-
case object Tsv extends SparqlFormat.Readable, SparqlFormat.Writeable:
44+
case object Tsv extends SparqlFormat.Jena, SparqlFormat.Readable, SparqlFormat.Writeable:
4245
override val fullName: String = "TSV"
4346
override val cliOptions: List[String] = List("tsv")
4447
override val jenaLang: Lang = ResultSetLang.RS_TSV
4548

4649
/** Jena's pretty-printed table. Jena registers no reader for it, so it's output-only. */
47-
case object Text extends SparqlFormat.Writeable:
50+
case object Text extends SparqlFormat.Jena, SparqlFormat.Writeable:
4851
override val fullName: String = "Text table"
4952
override val cliOptions: List[String] = List("text")
5053
override val jenaLang: Lang = ResultSetLang.RS_Text
5154

5255
/** We never convert Jelly to Jelly, so this is neither Readable nor Writeable – it is only here
5356
* so that the other side of the conversion has a name.
5457
*/
55-
case object JellySparql extends SparqlFormat:
58+
case object JellySparql extends SparqlFormat.Jena:
5659
override val fullName: String = "Jelly-SPARQL"
5760
override val cliOptions: List[String] = List("jelly-sparql")
5861
override val jenaLang: Lang = JellySparqlLanguage.JELLY_SPARQL
5962

60-
private val sparqlFormats: List[SparqlFormat] = List(Json, Xml, Csv, Tsv, Text, JellySparql)
63+
case object JellySparqlText extends SparqlFormat.Readable, SparqlFormat.Writeable:
64+
override val fullName: String = "Jelly-SPARQL text"
65+
override val cliOptions: List[String] = List("jelly-sparql-text")
66+
val extension = ".jellys.txt"
67+
68+
private val sparqlFormats: List[SparqlFormat] =
69+
List(Json, Xml, Csv, Tsv, Text, JellySparql, JellySparqlText)
6170

6271
def all: List[SparqlFormat] = sparqlFormats
6372

@@ -86,5 +95,9 @@ object SparqlFormat:
8695
/** Infers the format based on the file name.
8796
*/
8897
def inferFormat(fileName: String): Option[SparqlFormat] =
89-
val guessType = RDFLanguages.guessContentType(fileName)
90-
sparqlFormats.collectFirst { case f if f.jenaLang.getContentType == guessType => f }
98+
if fileName.endsWith(JellySparqlText.extension) then Some(JellySparqlText)
99+
else
100+
val guessType = RDFLanguages.guessContentType(fileName)
101+
sparqlFormats.collectFirst {
102+
case f: SparqlFormat.Jena if f.jenaLang.getContentType == guessType => f
103+
}

‎src/main/scala/eu/neverblink/jelly/cli/graal/ProtobufFeature.scala‎

Lines changed: 0 additions & 27 deletions
This file was deleted.

0 commit comments

Comments
 (0)