diff --git a/src/main/scala/org/dbpedia/databus/ApiImpl.scala b/src/main/scala/org/dbpedia/databus/ApiImpl.scala index c8546a8..48df7bd 100644 --- a/src/main/scala/org/dbpedia/databus/ApiImpl.scala +++ b/src/main/scala/org/dbpedia/databus/ApiImpl.scala @@ -37,7 +37,7 @@ class ApiImpl(config: Config) extends DatabusApi { contextUrl(body.getBytes, defaultLang) .map(jenaJsonLdContextWithFallbackForLocalhost(_, request.getRemoteHost).get) ) - .flatMap(m => Tractate.extract(m.getGraph, TractateV1.Version)) + .flatMap(m => Tractate.extract(m._1.getGraph, TractateV1.Version)) .map(_.stringForSigning) override def deleteFile(username: String, path: String, prefix: Option[String])(request: HttpServletRequest): Try[OperationSuccess] = { @@ -72,12 +72,21 @@ class ApiImpl(config: Config) extends DatabusApi { val ctx = ctxU.map(cu => jenaJsonLdContextWithFallbackForLocalhost(cu, request.getRemoteHost).get) readModel(body.getBytes, lang, ctx) .flatMap(model => { - saveToVirtuoso(model, graphId)({ - graphToBytes(model.getGraph, defaultLang, ctxU) + saveToVirtuoso(model._1, graphId)({ + graphToBytes(model._1.getGraph, defaultLang, ctxU) .flatMap(a => saveFiles(username, Map( pa -> a )).map(hash => OperationSuccess(graphId, hash))) - }) + }).transform(Success(_), e => + if (model._2.isEmpty) { + Failure(e) + } else { + val ee = new RuntimeException( + s"Error saving data, potentially caused by: ${model._2.map(_.message).fold("")((l, r) => l + '\n' + r)}", + e) + ee.setStackTrace(Array.empty) + Failure(ee) + }) }) } @@ -116,6 +125,10 @@ class ApiImpl(config: Config) extends DatabusApi { override def saveFileMapException400(e: Throwable)(request: HttpServletRequest): Option[OperationFailure] = e match { case _: JenaException => Some(OperationFailure(e.getMessage)) case _: VirtuosoException if e.getMessage.contains("SQ200") => Some(OperationFailure(s"Wrong value for type. ${e.getMessage}")) + case _: RuntimeException if e.getCause.isInstanceOf[VirtuosoException] && e.getMessage.contains("SQ200") => + Some(OperationFailure(s"Wrong value for type. ${e.getMessage}")) + case _: RuntimeException if e.getCause.isInstanceOf[VirtuosoException] && e.getMessage.contains("SQ074") => + Some(OperationFailure(s"Wrong input data. ${e.getMessage}")) case _ => None } @@ -142,7 +155,7 @@ class ApiImpl(config: Config) extends DatabusApi { .map(jenaJsonLdContextWithFallbackForLocalhost(_, request.getRemoteHost).get) ) .flatMap(m => - graphToBytes(m.getGraph, lang, ctxUri) + graphToBytes(m._1.getGraph, lang, ctxUri) ) }) .map(new String(_)) diff --git a/src/main/scala/org/dbpedia/databus/SparqlClient.scala b/src/main/scala/org/dbpedia/databus/SparqlClient.scala index 1619be9..bf47757 100644 --- a/src/main/scala/org/dbpedia/databus/SparqlClient.scala +++ b/src/main/scala/org/dbpedia/databus/SparqlClient.scala @@ -11,7 +11,7 @@ import org.apache.jena.atlas.json.JsonString import org.apache.jena.graph.{Graph, Node} import org.apache.jena.rdf.model.{Model, ModelFactory} import org.apache.jena.riot.lang.JsonLDReader -import org.apache.jena.riot.system.StreamRDFLib +import org.apache.jena.riot.system.{ErrorHandler, ErrorHandlerFactory, StreamRDFLib} import org.apache.jena.riot.writer.JsonLDWriter import org.apache.jena.riot.{Lang, RDFDataMgr, RDFFormat, RDFLanguages, RDFParserBuilder, RDFWriter, RIOT} import org.apache.jena.shacl.{ShaclValidator, Shapes, ValidationReport} @@ -159,7 +159,7 @@ object RdfConversions { val DefaultShaclLang = Lang.TTL - def readModel(data: Array[Byte], lang: Lang, context: Option[util.Context]): Try[Model] = Try { + def readModel(data: Array[Byte], lang: Lang, context: Option[util.Context]): Try[(Model, List[Warning])] = Try { val model = ModelFactory.createDefaultModel() val dataStream = new ByteArrayInputStream(data) val dest = StreamRDFLib.graph(model.getGraph) @@ -168,11 +168,14 @@ object RdfConversions { .base(null) .lang(lang) + val eh = newErrorHandlerWithWarnings + parser.errorHandler(eh) + context.foreach(cs => parser.context(cs)) parser.parse(dest) - model + (model, eh.warningsList) } def graphToBytes(model: Graph, outputLang: Lang, context: Option[URL]): Try[Array[Byte]] = Try { @@ -200,15 +203,15 @@ object RdfConversions { def validateWithShacl(file: Array[Byte], shaclData: Array[Byte], fileCtx: Option[util.Context], shaclCtx: Option[util.Context], modelLang: Lang): Try[ValidationReport] = for { - shaclGra <- readModel(shaclData, DefaultShaclLang, shaclCtx) - model <- readModel(file, modelLang, fileCtx) + (shaclGra, _) <- readModel(shaclData, DefaultShaclLang, shaclCtx) + (model, _) <- readModel(file, modelLang, fileCtx) re <- validateWithShacl(model, shaclGra.getGraph) } yield re def validateWithShacl(file: Array[Byte], fileCtx: Option[util.Context], shaclUri: String, modelLang: Lang): Try[ValidationReport] = for { shaclGra <- Try(RDFDataMgr.loadGraph(shaclUri)) - model <- readModel(file, modelLang, fileCtx) + (model, _) <- readModel(file, modelLang, fileCtx) re <- validateWithShacl(model, shaclGra) } yield re @@ -414,6 +417,31 @@ object RdfConversions { sb.toString() } + case class Warning(message: String) + private class ErrorHandlerWithWarnings extends ErrorHandler { + private val defaultEH = ErrorHandlerFactory.getDefaultErrorHandler + + private var warnings: List[Warning] = List.empty + + import org.apache.jena.riot.SysRIOT.fmtMessage + + override def warning(message: String, line: Long, col: Long): Unit = { + warnings = warnings :+ Warning(fmtMessage(message, line, col)) + defaultEH.warning(message, line, col) + } + + override def error(message: String, line: Long, col: Long): Unit = + defaultEH.error(message, line, col) + + override def fatal(message: String, line: Long, col: Long): Unit = + defaultEH.fatal(message, line, col) + + def warningsList: List[Warning] = warnings + } + + private def newErrorHandlerWithWarnings: ErrorHandlerWithWarnings = + new ErrorHandlerWithWarnings + } diff --git a/src/test/resources/report_syntax_err.jsonld b/src/test/resources/report_syntax_err.jsonld new file mode 100644 index 0000000..a0c2f78 --- /dev/null +++ b/src/test/resources/report_syntax_err.jsonld @@ -0,0 +1,39 @@ +{ + "@context": "https://raw.githubusercontent.com/dbpedia/databus/master/server/app/common/res/context.jsonld", + "@graph": [ + { + "@id": "https://databus.coypu.org/narndt/coypu", + "@type": "Group", + "title": "CoyPu" + }, + { + "@id": "https://databus.coypu.org/narndt/coypu/countries", + "@type": "Artifact", + "title": "Raise VirtuosoException", + "abstract": "Counties and regions", + "description": "Counties and regions" + }, + { + "@type": [ + "Version", + "Dataset" + ], + "@id": "https://databus.coypu.org/narndt/coypu/countries/2023-09-18T122214Z", + "hasVersion": "2023-09-18T122214Z", + "title": "Countries", + "abstract": "Countries\n2023-09-18T12:22:14Z", + "description": "Countries\n2023-09-18T12:22:14Z", + "license": "https://dalicc.net/licenselibrary/Cc010Universal", + "wasDerivedFrom": "https://metadata.coypu.org/dataset/wikidata-distribution\nWikidata Query Service\nhttps://query.wikidata.org/", + "distribution": [ + { + "@type": "Part", + "formatExtension": "ttl", + "compression": "none", + "downloadURL": "https://databus.coypu.org/dav/narndt/coypu/countries/2023-09-18T122214Z/countries_freqency=static.ttl", + "dcv:frequency": "static" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/scala/org/dbpedia/databus/DatabusScalatraTest.scala b/src/test/scala/org/dbpedia/databus/DatabusScalatraTest.scala index 6f596d1..3b123a0 100644 --- a/src/test/scala/org/dbpedia/databus/DatabusScalatraTest.scala +++ b/src/test/scala/org/dbpedia/databus/DatabusScalatraTest.scala @@ -70,6 +70,19 @@ class DatabusScalatraTest extends ScalatraFlatSpec with BeforeAndAfter { } + "File save" should "report problems in input" in { + + + val file = "report_syntax_err.jsonld" + val bytes = Files.readAllBytes(Paths.get(getClass.getClassLoader.getResource(file).getFile)) + + post("/databus/graph/save?repo=kuckuck&path=pa/syntax_err.jsonld", bytes) { + (status >= 400) should equal(true) + response.body.contains("Spaces are not legal in URIs/IRIs") should equal(true) + } + + } + "File read" should "return 404" in { get("/databus/graph/read?repo=kuckuck&path=pa/not_existing.jsonld") { diff --git a/src/test/scala/org/dbpedia/databus/ExternalApiEmul.scala b/src/test/scala/org/dbpedia/databus/ExternalApiEmul.scala index 584bdf0..db8b1b4 100644 --- a/src/test/scala/org/dbpedia/databus/ExternalApiEmul.scala +++ b/src/test/scala/org/dbpedia/databus/ExternalApiEmul.scala @@ -1,24 +1,24 @@ package org.dbpedia.databus -import org.scalatra.{Ok, ScalatraServlet} +import org.scalatra.{BadRequest, Ok, ScalatraServlet} class ExternalApiEmul extends ScalatraServlet { - post("/oauth/*"){ + post("/oauth/*") { Ok( """ |{"access_token": "token"} |""".stripMargin) } - get("/api/v4/*"){ + get("/api/v4/*") { Ok( """ |{"id": "commit id?"} |""".stripMargin) } - get("/api/v4/projects"){ + get("/api/v4/projects") { Ok( """ |[{ @@ -28,7 +28,7 @@ class ExternalApiEmul extends ScalatraServlet { |""".stripMargin) } - post("/api/v4/*"){ + post("/api/v4/*") { Ok( """ |{"id": "random id"} @@ -36,13 +36,16 @@ class ExternalApiEmul extends ScalatraServlet { } - - get("/virtu/*"){ + get("/virtu/*") { Ok("Virtuoso emul") } - post("/virtu/*"){ - Ok("Virtuoso emul") + post("/virtu/*") { + if (request.multiParameters.get("query").get.exists(_.contains("Raise VirtuosoException"))) { + BadRequest("virtuoso.jdbc4.VirtuosoException: SQ074: Line 38: SP030: SPARQL compiler, line 5: syntax error") + } else { + Ok("Virtuoso emul") + } } }