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 src/main/scala/eu/neverblink/jelly/cli/App.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import org.apache.jena.sys.JenaSystem
/** Main entrypoint.
*/
object App extends CommandsEntryPoint:

// Initialize Jena now to avoid race conditions later
JenaSystem.init()

Expand All @@ -18,7 +19,6 @@ object App extends CommandsEntryPoint:
override def progName: String = "jelly-cli"

override def commands: Seq[Command[?]] = Seq(
FoolAround,
Version,
RdfFromJelly,
)
26 changes: 26 additions & 0 deletions src/main/scala/eu/neverblink/jelly/cli/ErrorHandler.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package eu.neverblink.jelly.cli

/** Handle exceptions. Common critical exceptions will be given custom output messages.
*/

object ErrorHandler:

def handle(command: JellyCommand[?], t: Throwable): Unit =
t match
case e: CriticalException =>
command.printLine(f"${e.getMessage}", toStderr = true)
case e: Throwable =>
command.printLine("Unknown error", toStderr = true)
printStackTrace(command, t)
command.exit(1)

/** Print out stack trace or debugging information
* @param command
* @param t
*/
private def printStackTrace(
command: JellyCommand[?],
t: Throwable,
): Unit =
if command.isDebugMode then t.printStackTrace(command.err)
else command.printLine("Run with --debug to see the complete stack trace.", toStderr = true)
29 changes: 29 additions & 0 deletions src/main/scala/eu/neverblink/jelly/cli/Exceptions.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package eu.neverblink.jelly.cli

import com.google.protobuf.InvalidProtocolBufferException
import org.apache.jena.riot.RiotException

/** Contains a set of common jelly-cli exceptions with custom output messages.
*/

case class InputFileNotFound(file: String)
extends CriticalException(s"Input file $file does not exist.")
case class InputFileInaccessible(file: String)
extends CriticalException(s"Not enough permissions to read input file $file.")
case class OutputFileCannotBeCreated(file: String)
extends CriticalException(
s"Not enough permissions to create output file $file in this directory.",
)
case class JellyDeserializationError(message: String)
extends CriticalException(s"Jelly deserialization error: $message")
case class JellySerializationError(message: String)
extends CriticalException(s"Jelly serialization error: $message")
case class JellyTranscodingError(message: String)
extends CriticalException(s"Jelly transcoding error: $message")
case class JenaRiotException(e: RiotException)
extends CriticalException(s"Jena RDF I/O exception: ${e.getMessage}")
case class InvalidJellyFile(e: InvalidProtocolBufferException)
extends CriticalException(s"Invalid Jelly file: ${e.getMessage}")
case class ExitException(code: Int) extends CriticalException(s"Exiting with code $code.")

class CriticalException(message: String) extends Exception(message)
3 changes: 0 additions & 3 deletions src/main/scala/eu/neverblink/jelly/cli/ExitError.scala

This file was deleted.

64 changes: 54 additions & 10 deletions src/main/scala/eu/neverblink/jelly/cli/JellyCommand.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,19 @@ import caseapp.*
import java.io.{ByteArrayOutputStream, OutputStream, PrintStream}
import scala.compiletime.uninitialized

abstract class JellyCommand[T: {Parser, Help}] extends Command[T]:
case class JellyOptions(
@HelpMessage("Add to run command in debug mode") debug: Boolean = false,
)

trait HasJellyOptions:
@Recurse
val common: JellyOptions

abstract class JellyCommand[T <: HasJellyOptions: {Parser, Help}] extends Command[T]:
private var isTest = false
private var out = System.out
private var err = System.err
private var isDebug = false
final protected[cli] var out = System.out
final protected[cli] var err = System.err
private var osOut: ByteArrayOutputStream = uninitialized
private var osErr: ByteArrayOutputStream = uninitialized

Expand All @@ -27,42 +36,77 @@ abstract class JellyCommand[T: {Parser, Help}] extends Command[T]:
out = System.out
err = System.err

/** 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

/** 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)
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

/** Override to have custom error handling for Jelly commands
*/
final override def main(progName: String, args: Array[String]): Unit =
try super.main(progName, args)
catch
case e: Throwable =>
ErrorHandler.handle(this, e)

/** 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

/** Runs the command in test mode from the outside app parsing level
* @param args
* the command line arguments
*/
def runCommand(args: List[String]): (String, String) =
final def runTestCommand(args: List[String]): (String, String) =
if !isTest then testMode(true)
osOut.reset()
osErr.reset()
App.main(args.toArray)
(osOut.toString, osErr.toString)

def getOut: String =
final def getOutContent: String =
if isTest then
out.flush()
val s = osOut.toString
osOut.reset()
s
else throw new IllegalStateException("Not in test mode")

final def getOutStream: OutputStream =
if isTest then osOut
else System.out

protected def getStdOut: OutputStream =
if isTest then osOut
else System.out

def getErr: String =
final def getErrContent: String =
if isTest then
err.flush()
val s = osErr.toString
osErr.reset()
s
else throw new IllegalStateException("Not in test mode")

@throws[ExitError]
override def exit(code: Int): Nothing =
if isTest then throw ExitError(code)
@throws[ExitException]
final override def exit(code: Int): Nothing =
if isTest then throw ExitException(code)
else super.exit(code)

override def printLine(line: String, toStderr: Boolean): Unit =
final override def printLine(line: String, toStderr: Boolean): Unit =
if toStderr then err.println(line)
else out.println(line)
20 changes: 0 additions & 20 deletions src/main/scala/eu/neverblink/jelly/cli/command/FoolAround.scala

This file was deleted.

7 changes: 5 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 @@ -3,15 +3,18 @@ package eu.neverblink.jelly.cli.command
import caseapp.*
import eu.neverblink.jelly.cli.*

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

object Version extends JellyCommand[VersionOptions]:
override def names: List[List[String]] = List(
List("version"),
List("v"),
)

override def run(options: VersionOptions, remainingArgs: RemainingArgs): Unit =
override def doRun(options: VersionOptions, remainingArgs: RemainingArgs): Unit =
val jenaV = BuildInfo.libraryDependencies
.find(_.startsWith("org.apache.jena:jena-core:")).get.split(":")(2)
val jellyV = BuildInfo.libraryDependencies
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package eu.neverblink.jelly.cli.command.rdf
import caseapp.*
import eu.neverblink.jelly.cli.JellyCommand
import eu.ostrzyciel.jelly.convert.jena.riot.{JellyLanguage, JellySubsystemLifecycle}
import com.google.protobuf.InvalidProtocolBufferException
import eu.neverblink.jelly.cli.*
import eu.neverblink.jelly.cli.util.IoUtil
import eu.ostrzyciel.jelly.convert.jena.riot.JellyLanguage
import eu.ostrzyciel.jelly.core.RdfProtoDeserializationError
import org.apache.jena.riot.system.StreamRDFWriter
import org.apache.jena.riot.{RDFLanguages, RDFParser}
import org.apache.jena.riot.{RDFLanguages, RDFParser, RiotException}

import java.io.{File, FileInputStream, FileOutputStream, InputStream, OutputStream}
import java.io.{InputStream, OutputStream}

case class RdfFromJellyOptions(
@Recurse
common: JellyOptions = JellyOptions(),
@ExtraName("to") outputFile: Option[String] = None,
)
) extends HasJellyOptions

object RdfFromJelly extends JellyCommand[RdfFromJellyOptions]:
override def group = "rdf"
Expand All @@ -18,26 +23,35 @@ object RdfFromJelly extends JellyCommand[RdfFromJellyOptions]:
List("rdf", "from-jelly"),
)

override def run(options: RdfFromJellyOptions, remainingArgs: RemainingArgs): Unit =
override def doRun(options: RdfFromJellyOptions, remainingArgs: RemainingArgs): Unit =
val inputStream = remainingArgs.remaining.headOption match {
case Some(fileName: String) =>
FileInputStream(File(fileName))
IoUtil.inputStream(fileName)
case _ => System.in
}
val outputStream = options.outputFile match {
case Some(fileName: String) =>
FileOutputStream(fileName)
IoUtil.outputStream(fileName)
case None => getStdOut
}
doConversion(inputStream, outputStream)

/*
This method reads the Jelly file, rewrite it to NQuads and writes it to some output stream
* @param inputStream InputStream
* @param outputStream OutputStream
*/
/** This method reads the Jelly file, rewrites it to NQuads and writes it to some output stream
* @param inputStream
* InputStream
* @param outputStream
* OutputStream
* @throws JellyDeserializationError
* @throws ParsingError
*/
private def doConversion(inputStream: InputStream, outputStream: OutputStream): Unit =
val mod = JellySubsystemLifecycle()
mod.start()
val nQuadWriter = StreamRDFWriter.getWriterStream(outputStream, RDFLanguages.NQUADS)
RDFParser.source(inputStream).lang(JellyLanguage.JELLY).parse(nQuadWriter)
try {
val nQuadWriter = StreamRDFWriter.getWriterStream(outputStream, RDFLanguages.NQUADS)
RDFParser.source(inputStream).lang(JellyLanguage.JELLY).parse(nQuadWriter)
} catch
case e: RdfProtoDeserializationError =>
throw JellyDeserializationError(e.getMessage)
case e: RiotException =>
throw JenaRiotException(e)
case e: InvalidProtocolBufferException =>
throw InvalidJellyFile(e)
35 changes: 35 additions & 0 deletions src/main/scala/eu/neverblink/jelly/cli/util/IoUtil.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package eu.neverblink.jelly.cli.util

import eu.neverblink.jelly.cli.*

import java.io.{File, FileInputStream, FileOutputStream}

/** Object for small, repeating I/O operations
*/
object IoUtil:

/** Read input file and return FileInputStream
* @param fileName
* @throws InputFileNotFound
* @throws InputFileInaccessible
* @return
* FileInputStream
*/
def inputStream(fileName: String): FileInputStream =
val file = File(fileName)
if !file.exists then throw InputFileNotFound(fileName)
if !file.canRead then throw InputFileInaccessible(fileName)
FileInputStream(file)

/** Create output stream with extra error handling. If the file exists, it will append to it.
* @param fileName
* @throws OutputFileExists
* @return
* FileOutputStream
*/
def outputStream(fileName: String): FileOutputStream =
val file = File(fileName)
val suppFile = file.getParentFile
val parentFile = if (suppFile != null) suppFile else File(".")
if !parentFile.canWrite then throw OutputFileCannotBeCreated(fileName)
FileOutputStream(file, true)
Loading