Skip to content
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
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ resolvers +=
"Sonatype OSS Snapshots" at "https://s01.oss.sonatype.org/content/repositories/snapshots"

lazy val jenaV = "5.3.0"
lazy val jellyV = "2.8.0"
lazy val jellyV = "2.9.1"

addCommandAlias("fixAll", "scalafixAll; scalafmtAll")

Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/eu/neverblink/jelly/cli/ErrorHandler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ object ErrorHandler:
case e: Throwable =>
command.printLine("Unknown error", toStderr = true)
printStackTrace(command, t)
command.exit(1)
command.exit(1, t)

/** Print out stack trace or debugging information
* @param command
Expand Down
11 changes: 10 additions & 1 deletion src/main/scala/eu/neverblink/jelly/cli/Exceptions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ case class InvalidFormatSpecified(format: String, validFormats: String)
extends CriticalException(
s"Invalid format option: \"$format\", needs to be one of ${validFormats}.",
)
case class ExitException(code: Int) extends CriticalException(s"Exiting with code $code.")
case class InvalidArgument(argument: String, argumentValue: String, message: Option[String] = None)
extends CriticalException(
s"Invalid value for argument $argument: \"$argumentValue\". " + message.getOrElse(""),
)
case class ExitException(
code: Int,
cause: Option[Throwable] = None,
) extends CriticalException(
s"Exiting with code $code." + cause.map(e => s" Cause: ${e.getMessage}").getOrElse(""),
)

class CriticalException(message: String) extends Exception(message)
33 changes: 23 additions & 10 deletions src/main/scala/eu/neverblink/jelly/cli/JellyCommand.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ import eu.neverblink.jelly.cli.util.IoUtil
import java.io.*
import scala.compiletime.uninitialized

case class JellyOptions(
case class JellyCommandOptions(
@HelpMessage("Add to run command in debug mode") debug: Boolean = false,
)

trait HasJellyOptions:
trait HasJellyCommandOptions:
@Recurse
val common: JellyOptions
val common: JellyCommandOptions

abstract class JellyCommand[T <: HasJellyOptions: {Parser, Help}] extends Command[T]:
abstract class JellyCommand[T <: HasJellyCommandOptions: {Parser, Help}] extends Command[T]:

private var isTest = false
private var isDebug = false
private var options: Option[T] = None
final protected[cli] var out = System.out
final protected[cli] var err = System.err
final protected[cli] var in = System.in
Expand All @@ -44,21 +44,29 @@ abstract class JellyCommand[T <: HasJellyOptions: {Parser, Help}] extends Comman

/** Check and set the values of all the general options repeating for every JellyCommand
*/
private def setUpGeneralArgs(options: T, remainingArgs: RemainingArgs): Unit =
if options.common.debug then this.isDebug = true
private def setUpGeneralArgs(options: T): Unit =
this.options = Some(options)

/** Returns the options set up for this command
*/
protected final def getOptions: T = options match {
case Some(value) => value
case None =>
throw new CriticalException("Command tried to access options before they were set up")
}

/** Makes sure that the repetitive options needed for every JellyCommand are set up before calling
* the doRun method, which contains Command-specific logic
*/
final override def run(options: T, remainingArgs: RemainingArgs): Unit =
setUpGeneralArgs(options, remainingArgs)
setUpGeneralArgs(options)
doRun(options, remainingArgs)

/** This abstract method is the main entry point for every JellyCommand. It should be overridden
* by Command-specific implementation, including logic needed for this specific object extendind
* JellyCommand.
*/
def doRun(options: T, remainingArgs: RemainingArgs): Unit
protected def doRun(options: T, remainingArgs: RemainingArgs): Unit

/** Override to have custom error handling for Jelly commands
*/
Expand All @@ -71,7 +79,7 @@ abstract class JellyCommand[T <: HasJellyOptions: {Parser, Help}] extends Comman
/** Returns information about whether the command is in debug mode (which returns stack traces of
* every error) or not
*/
final def isDebugMode: Boolean = this.isDebug
final def isDebugMode: Boolean = this.getOptions.common.debug

/** Runs the command in test mode from the outside app parsing level
* @param args
Expand Down Expand Up @@ -154,6 +162,11 @@ abstract class JellyCommand[T <: HasJellyOptions: {Parser, Help}] extends Comman
}
(inputStream, outputStream)

@throws[ExitException]
final def exit(code: Int, cause: Throwable): Nothing =
if isTest then throw ExitException(code, Some(cause))
else exit(code)

@throws[ExitException]
final override def exit(code: Int): Nothing =
if isTest then throw ExitException(code)
Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/eu/neverblink/jelly/cli/command/Version.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import eu.neverblink.jelly.cli.*

case class VersionOptions(
@Recurse
common: JellyOptions = JellyOptions(),
) extends HasJellyOptions
common: JellyCommandOptions = JellyCommandOptions(),
) extends HasJellyCommandOptions

object Version extends JellyCommand[VersionOptions]:
override def names: List[List[String]] = List(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import java.io.{InputStream, OutputStream}

/** This abstract class is responsible for the common logic in both RDF parsing commands
*/
abstract class RdfCommand[T <: HasJellyOptions: {Parser, Help}, F <: RdfFormat](using
abstract class RdfCommand[T <: HasJellyCommandOptions: {Parser, Help}, F <: RdfFormat](using
tt: TypeTest[RdfFormat, F],
) extends JellyCommand[T]:

Expand All @@ -25,7 +25,7 @@ abstract class RdfCommand[T <: HasJellyOptions: {Parser, Help}, F <: RdfFormat](
lazy val printUtil: RdfCommandPrintUtil[F]

/** The method responsible for matching the format to a given action */
def matchToAction(option: F): Option[(InputStream, OutputStream) => Unit]
def matchFormatToAction(option: F): Option[(InputStream, OutputStream) => Unit]

/** This method takes care of proper error handling and takes care of the parameter priorities in
* matching the input to a given format conversion
Expand Down Expand Up @@ -54,13 +54,13 @@ abstract class RdfCommand[T <: HasJellyOptions: {Parser, Help}, F <: RdfFormat](
if (fileName.isDefined) RdfFormat.inferFormat(fileName.get) else None
(explicitFormat, implicitFormat) match {
case (Some(f: F), _) =>
matchToAction(f).get(inputStream, outputStream)
// If format explicitely defined but does not match any available actions or formats, we throw an error
matchFormatToAction(f).get(inputStream, outputStream)
// If format explicitly defined but does not match any available actions or formats, we throw an error
case (_, _) if format.isDefined =>
throw InvalidFormatSpecified(format.get, printUtil.validFormatsString)
case (_, Some(f: F)) =>
matchToAction(f).get(inputStream, outputStream)
// If format not explicitely defined but implicitely not understandable we default to this
matchFormatToAction(f).get(inputStream, outputStream)
// If format not explicitly defined but implicitly not understandable we default to this
case (_, _) => defaultAction(inputStream, outputStream)
}
} catch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ object RdfFromJellyPrint extends RdfCommandPrintUtil[RdfFormat.Writeable]:

case class RdfFromJellyOptions(
@Recurse
common: JellyOptions = JellyOptions(),
common: JellyCommandOptions = JellyCommandOptions(),
@ExtraName("to") outputFile: Option[String] = None,
@ValueDescription("Output format.")
@HelpMessage(
RdfFromJellyPrint.helpMsg,
)
@ExtraName("out-format") outputFormat: Option[String] = None,
) extends HasJellyOptions
) extends HasJellyCommandOptions

object RdfFromJelly extends RdfCommand[RdfFromJellyOptions, RdfFormat.Writeable]:

Expand All @@ -41,7 +41,7 @@ object RdfFromJelly extends RdfCommand[RdfFromJellyOptions, RdfFormat.Writeable]
this.getIoStreamsFromOptions(remainingArgs.remaining.headOption, options.outputFile)
parseFormatArgs(inputStream, outputStream, options.outputFormat, options.outputFile)

override def matchToAction(
override def matchFormatToAction(
option: RdfFormat.Writeable,
): Option[(InputStream, OutputStream) => Unit] =
option match
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package eu.neverblink.jelly.cli.command.rdf

import caseapp.*
import eu.neverblink.jelly.cli.InvalidArgument
import eu.ostrzyciel.jelly.core.{JellyOptions, LogicalStreamTypeFactory}
import eu.ostrzyciel.jelly.core.proto.v1.{LogicalStreamType, RdfStreamOptions}

/** Options for serializing in Jelly-RDF */
case class RdfJellySerializationOptions(
@HelpMessage("Name of the output stream (in metadata). Default: (empty)")
`opt.streamName`: String = "",
@HelpMessage(
"Whether the stream may contain generalized triples, quads, or datasets. Default: true",
)
`opt.generalizedStatements`: Boolean = true,
@HelpMessage("Whether the stream may contain RDF-star statements. Default: true")
`opt.rdfStar`: Boolean = true,
@HelpMessage(
"Maximum size of the name lookup table. Default: " + JellyOptions.bigStrict.maxNameTableSize,
)
`opt.maxNameTableSize`: Int = JellyOptions.bigStrict.maxNameTableSize,
@HelpMessage(
"Maximum size of the prefix lookup table. Default: " + JellyOptions.bigStrict.maxPrefixTableSize,
)
`opt.maxPrefixTableSize`: Int = JellyOptions.bigStrict.maxPrefixTableSize,
@HelpMessage(
"Maximum size of the datatype lookup table. Default: " + JellyOptions.bigStrict.maxDatatypeTableSize,
)
`opt.maxDatatypeTableSize`: Int = JellyOptions.bigStrict.maxDatatypeTableSize,
@HelpMessage(
"Logical (RDF-STaX-based) stream type. This can be either a name like " +
"`FLAT_QUADS` or a full IRI like `https://w3id.org/stax/ontology#flatQuadStream`. " +
"Default: (unspecified)",
)
`opt.logicalType`: Option[String] = None,
):
lazy val asRdfStreamOptions: RdfStreamOptions =
val logicalIri = `opt.logicalType`
.map(_.trim).filter(_.nonEmpty)
.map {
case x if x.startsWith("http") => x
case x if x.toUpperCase.endsWith("S") =>
val words = x.substring(0, x.length - 1).split("_").map(_.toLowerCase)
val wordSeq = words.head +: words.tail.map(_.capitalize)
"https://w3id.org/stax/ontology#" + wordSeq.mkString + "Stream"
case _ => "" // invalid IRI, we'll catch it in the next step
}
val logicalType = logicalIri.flatMap(LogicalStreamTypeFactory.fromOntologyIri)
if logicalIri.isDefined && logicalType.isEmpty then
throw InvalidArgument(
"--opt.logical-type",
`opt.logicalType`.get,
Some("Logical type must be either a full RDF-STaX IRI or a name like `FLAT_QUADS`"),
)
RdfStreamOptions(
streamName = `opt.streamName`,
generalizedStatements = `opt.generalizedStatements`,
rdfStar = `opt.rdfStar`,
maxNameTableSize = `opt.maxNameTableSize`,
maxPrefixTableSize = `opt.maxPrefixTableSize`,
maxDatatypeTableSize = `opt.maxDatatypeTableSize`,
logicalType = logicalType.getOrElse(LogicalStreamType.UNSPECIFIED),
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import eu.neverblink.jelly.cli.*
import eu.neverblink.jelly.cli.command.rdf.RdfFormat.*
import eu.ostrzyciel.jelly.convert.jena.riot.JellyLanguage
import org.apache.jena.riot.system.StreamRDFWriter
import org.apache.jena.riot.{Lang, RDFParser}
import org.apache.jena.riot.{Lang, RDFParser, RIOT}

import java.io.{InputStream, OutputStream}

Expand All @@ -13,14 +13,25 @@ object RdfToJellyPrint extends RdfCommandPrintUtil[RdfFormat.Jena.Readable]:

case class RdfToJellyOptions(
@Recurse
common: JellyOptions = JellyOptions(),
common: JellyCommandOptions = JellyCommandOptions(),
@ExtraName("to") outputFile: Option[String] = None,
@ValueDescription("Input format.")
@HelpMessage(
RdfToJellyPrint.helpMsg,
)
@ExtraName("in-format") inputFormat: Option[String] = None,
) extends HasJellyOptions
@Recurse
jellySerializationOptions: RdfJellySerializationOptions = RdfJellySerializationOptions(),
@HelpMessage(
"Target number of rows per frame – the writer may slightly exceed that. Default: 256",
)
rowsPerFrame: Int = 256,
@HelpMessage(
"Whether to preserve explicit namespace declarations in the output (PREFIX: in Turtle). " +
"Default: false",
)
enableNamespaceDeclarations: Boolean = false,
) extends HasJellyCommandOptions

object RdfToJelly extends RdfCommand[RdfToJellyOptions, RdfFormat.Jena.Readable]:

Expand All @@ -34,6 +45,8 @@ object RdfToJelly extends RdfCommand[RdfToJellyOptions, RdfFormat.Jena.Readable]
langToJelly(RdfFormat.NQuads.jenaLang, _, _)

override def doRun(options: RdfToJellyOptions, remainingArgs: RemainingArgs): Unit =
// Touch the options to make sure they are valid
options.jellySerializationOptions.asRdfStreamOptions
val (inputStream, outputStream) =
getIoStreamsFromOptions(remainingArgs.remaining.headOption, options.outputFile)
parseFormatArgs(
Expand All @@ -43,10 +56,10 @@ object RdfToJelly extends RdfCommand[RdfToJellyOptions, RdfFormat.Jena.Readable]
remainingArgs.remaining.headOption,
)

override def matchToAction(
option: RdfFormat.Jena.Readable,
override def matchFormatToAction(
format: RdfFormat.Jena.Readable,
): Option[(InputStream, OutputStream) => Unit] =
Some(langToJelly(option.jenaLang, _, _))
Some(langToJelly(format.jenaLang, _, _))

/** This method reads the file, rewrites it to Jelly and writes it to some output stream
* @param jenaLang
Expand All @@ -61,5 +74,20 @@ object RdfToJelly extends RdfCommand[RdfToJellyOptions, RdfFormat.Jena.Readable]
inputStream: InputStream,
outputStream: OutputStream,
): Unit =
val jellyWriter = StreamRDFWriter.getWriterStream(outputStream, JellyLanguage.JELLY)
// Configure the writer
val writerContext = RIOT.getContext.copy()
.set(
JellyLanguage.SYMBOL_STREAM_OPTIONS,
getOptions.jellySerializationOptions.asRdfStreamOptions,
)
.set(JellyLanguage.SYMBOL_FRAME_SIZE, getOptions.rowsPerFrame)
.set(
JellyLanguage.SYMBOL_ENABLE_NAMESPACE_DECLARATIONS,
getOptions.enableNamespaceDeclarations,
)
val jellyWriter = StreamRDFWriter.getWriterStream(
outputStream,
JellyLanguage.JELLY,
writerContext,
)
RDFParser.source(inputStream).lang(jenaLang).parse(jellyWriter)
Loading
Loading