Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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"Riot 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.

59 changes: 51 additions & 8 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
protected[cli] var out = System.out
protected[cli] var err = System.err
private var osOut: ByteArrayOutputStream = uninitialized
private var osErr: ByteArrayOutputStream = uninitialized

Expand All @@ -27,40 +36,74 @@ 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 Jelly command
*/
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 Jelly command 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 Jelly command. It should be overridden
* by command-specific implementation.
*/
def doRun(options: T, remainingArgs: RemainingArgs): Unit

/** Override to have custom error handling for Jelly commands
*/
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
*/
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) =
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 =
def getOutContent: String =
if isTest then
out.flush()
val s = osOut.toString
osOut.reset()
s
else throw new IllegalStateException("Not in test mode")

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

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

def getErr: String =
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]
@throws[ExitException]
override def exit(code: Int): Nothing =
if isTest then throw ExitError(code)
if isTest then throw ExitException(code)
else super.exit(code)

override def printLine(line: String, toStderr: Boolean): Unit =
Expand Down
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.{InputFileInaccessible, InputFileNotFound, OutputFileCannotBeCreated}

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