Skip to content

Commit

Permalink
Added support for SHACL_TQ
Browse files Browse the repository at this point in the history
  • Loading branch information
labra committed Oct 30, 2020
1 parent 611e889 commit 6b9b569
Show file tree
Hide file tree
Showing 9 changed files with 327 additions and 36 deletions.
7 changes: 5 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ lazy val scalatagsVersion = "0.6.7"
lazy val scallopVersion = "3.3.2"
lazy val seleniumVersion = "2.35.0"
lazy val sextVersion = "0.2.6"
lazy val shaclTQVersion = "1.3.2"
lazy val typesafeConfigVersion = "1.3.4"
lazy val xercesVersion = "2.12.0"

Expand Down Expand Up @@ -72,7 +73,8 @@ lazy val jenaFuseki = "org.apache.jena" % "jena-fuseki-main"
lazy val jline = "org.jline" % "jline" % jlineVersion
lazy val jna = "net.java.dev.jna" % "jna" % jnaVersion
lazy val pprint = "com.lihaoyi" %% "pprint" % pprintVersion
lazy val rdf4j_runtime = "org.eclipse.rdf4j" % "rdf4j-runtime" % rdf4jVersion
lazy val rdf4j_runtime = "org.eclipse.rdf4j" % "rdf4j-runtime" % rdf4jVersion
lazy val shaclTQ = "org.topbraid" % "shacl" % shaclTQVersion
lazy val scalaLogging = "com.typesafe.scala-logging" %% "scala-logging" % loggingVersion
lazy val scallop = "org.rogach" %% "scallop" % scallopVersion
lazy val scalactic = "org.scalactic" %% "scalactic" % scalacticVersion
Expand Down Expand Up @@ -149,7 +151,8 @@ lazy val schema = project
shex,
shacl,
shapeMaps,
jenaShacl
jenaShacl,
shaclTQ
)
)
.dependsOn(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,61 @@ import cats.data._
import cats.implicits._
import es.weso.rdf.PrefixMap
import es.weso.rdf.nodes._
import es.weso.rdf.path.{InversePath, PredicatePath, SHACLPath}
import es.weso.rdf.PREFIXES._
import es.weso.rdf.path._
import es.weso.shex.implicits.showShEx._
import es.weso.shex.linter.ShExLinter
import es.weso.{shacl, _}
import es.weso.shacl.TargetClass
import es.weso.shacl.TargetNode
import es.weso.shacl.TargetObjectsOf
import es.weso.shacl.TargetSubjectsOf
import es.weso.{shapeMaps,_}
import es.weso.shapeMaps.BNodeLabel
import es.weso.shapeMaps.IRILabel

object Shacl2ShEx {

def shacl2ShEx(schema: shacl.Schema): Either[String, (shex.Schema, shapeMaps.QueryShapeMap)] = {
def shacl2ShEx(schema: shacl.Schema, nodesPrefixMap: Option[PrefixMap] = None): Either[String, (shex.Schema, shapeMaps.QueryShapeMap)] = {
val (state, eitherSchema) = cnvSchema(schema).value.run(initialState)
val e = for {
shexSchema <- eitherSchema
schema1 = shexSchema.addTripleExprMap(state.tripleExprMap)
queryMap <- cnvShapeMap(schema)
queryMap <- cnvShapeMap(schema, nodesPrefixMap)
lintedSchema <- ShExLinter.inlineInclusions(schema1)
} yield (lintedSchema,queryMap)
// println(s"Result of conversion: \n$e")
// priprontln(s"Result of conversion: \n$e")
e
}

def cnvShapeMap(schema: shacl.Schema): Either[String,shapeMaps.QueryShapeMap] = {
val associations: List[Association] = schema.shapesMap.values.map(shape2Associations)
Right(shapeMaps.QueryShapeMap(List(), PrefixMap.empty, PrefixMap.empty))
def cnvShapeMap(schema: shacl.Schema, nodesPrefixMap: Option[PrefixMap] = None): Either[String,shapeMaps.QueryShapeMap] = for {
associations <- schema.shapesMap.values.toList.map(shape2Associations).sequence
} yield {
val as: List[shapeMaps.Association] = associations.flatten
shapeMaps.QueryShapeMap(as, nodesPrefixMap.getOrElse(schema.pm), schema.pm)
}

private def shape2Associations(shape: shacl.Shape): List[Association] = shape.targets.map(target2Association)
private def shape2Associations(shape: shacl.Shape): Either[String, List[shapeMaps.Association]] =
shape.targets.toList.map(target2Association(shape)).sequence

private def target2Association(target: shacl.Target): Association = target match {
case TargetClass(node) => ???
case TargetNode(node) => ???
case TargetObjectsOf(pred) => ???
case TargetSubjectsOf(pred) => ???
private def rdfTypeShacl: SHACLPath =
SequencePath(List(PredicatePath(`rdf:type`),
ZeroOrMorePath(PredicatePath(`rdfs:subClassOf`)))
)

private def shape2ShapeMapLabel(shape: shacl.Shape): Either[String, shapeMaps.ShapeMapLabel] = shape.id match {
case bnode: BNode => Right(shapeMaps.BNodeLabel(bnode))
case iri: IRI => Right(shapeMaps.IRILabel(iri))
case _ => Left(s"Cannot convert shape identifier ${shape.id} to shape map label")
}

private def target2Association(shape: shacl.Shape)(target: shacl.Target): Either[String,shapeMaps.Association] = for {
lbl <- shape2ShapeMapLabel(shape)
nodeSelector <- target2NodeSelector(target)
} yield shapeMaps.Association(nodeSelector,lbl,shapeMaps.Info.undefined("Generated by Shacl2ShEx converter"))

private def target2NodeSelector(target: shacl.Target): Either[String,shapeMaps.NodeSelector] = target match {
case shacl.TargetClass(node) => Right(shapeMaps.TriplePattern(shapeMaps.Focus,rdfTypeShacl,shapeMaps.NodePattern(node)))
case shacl.TargetNode(node) => Right(shapeMaps.RDFNodeSelector(node))
case shacl.TargetObjectsOf(pred) => Right(shapeMaps.TriplePattern(shapeMaps.WildCard, PredicatePath(pred), shapeMaps.Focus))
case shacl.TargetSubjectsOf(pred) => Right(shapeMaps.TriplePattern(shapeMaps.Focus, PredicatePath(pred), shapeMaps.WildCard))
case _ => Left(s"target2NodeSelector: Unsupported conversion of ${target}")
}

case class State(tripleExprMap: TEMap) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package es.weso.shacl.converter

import cats.implicits._
import es.weso._
import es.weso.rdf.jena.RDFAsJenaModel
import es.weso.utils.IOUtils
import org.scalatest.matchers.should._
import org.scalatest.funspec._
import es.weso.shapeMaps.Association
import es.weso.shapeMaps.ShapeMapLabel
import es.weso.shapeMaps.NodeSelector
import es.weso.shapeMaps.ShapeMap

class shacl2ShapeMapTest extends AnyFunSpec with Matchers {

describe("shacl2ShapeMaps converter") {
{
shouldConvertSHACLShapeMap(
"""|prefix : <http://example.org/>
|prefix sh: <http://www.w3.org/ns/shacl#>
|:S a sh:NodeShape ;
| sh:targetNode :x ;
| sh:nodeKind sh:IRI .
""".stripMargin,
""":x@:S""".stripMargin)

shouldConvertSHACLShapeMap(
"""|prefix : <http://example.org/>
|prefix sh: <http://www.w3.org/ns/shacl#>
|prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
|prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|:S a sh:NodeShape ;
| sh:targetClass :C ;
| sh:nodeKind sh:IRI .
""".stripMargin,
"""{FOCUS rdf:type/rdfs:subClassOf* :C}@:S""".stripMargin)

shouldConvertSHACLShapeMap(
"""|prefix : <http://example.org/>
|prefix sh: <http://www.w3.org/ns/shacl#>
|prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
|prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|:S a sh:NodeShape ;
| sh:targetSubjectsOf :p ;
| sh:nodeKind sh:IRI .
""".stripMargin,
"""{FOCUS :p _}@:S""".stripMargin)

shouldConvertSHACLShapeMap(
"""|prefix : <http://example.org/>
|prefix sh: <http://www.w3.org/ns/shacl#>
|prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
|prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|:S a sh:NodeShape ;
| sh:targetObjectsOf :p ;
| sh:nodeKind sh:IRI .
""".stripMargin,
"""{_ :p FOCUS}@:S""".stripMargin)
}
}

private def getAssociationPair(a: Association): (NodeSelector,ShapeMapLabel) = (a.node,a.shape)

private def getPairs(shapeMap: ShapeMap): List[(NodeSelector,ShapeMapLabel)] = shapeMap.associations.map(getAssociationPair)

def shouldConvertSHACLShapeMap(strSHACL: String, expected: String): Unit = {
it(s"Should convert: $strSHACL to ShapeMap and obtain: $expected") {
val cmp = RDFAsJenaModel.fromString(strSHACL, "TURTLE", None).flatMap(_.use(shaclRDF => for {
shacl <- RDF2Shacl.getShacl(shaclRDF)
shapeMapConverted <- IOUtils.fromES(Shacl2ShEx.shacl2ShEx(shacl).leftMap(e => s"Error in Shacl2ShEx conversion: $e"))
expectedShapeMap <- IOUtils.fromES(shapeMaps.ShapeMap.fromString(expected, "Compact", None,shacl.pm,shacl.pm).leftMap(e => s"Error in Shape maps parsing: $e"))
} yield (shapeMapConverted, expectedShapeMap, shacl)))
cmp.attempt.unsafeRunSync().fold(
e => fail(s"Error: $e"),
values => {
val (converted, expected, shacl) = values
val (schema,shapeMap) = converted
getPairs(shapeMap) should contain theSameElementsAs getPairs(expected)
}
)
}
}

}
3 changes: 3 additions & 0 deletions modules/schema/src/main/scala/es/weso/schema/Schema.scala
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ abstract class Schema {
*/
def pm: PrefixMap

/**
* Convert this schema into another schema
*/
def convert(targetFormat: Option[String],
targetEngine: Option[String],
base: Option[IRI]
Expand Down
4 changes: 2 additions & 2 deletions modules/schema/src/main/scala/es/weso/schema/Schemas.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ object Schemas {
lazy val shEx: Schema = ShExSchema.empty
lazy val shaclex : Schema = ShaclexSchema.empty
lazy val jenaShacl : Schema = JenaShacl.empty
// lazy val shacl_tq = Shacl_TQ.empty
lazy val shaclTQ = ShaclTQ.empty

val availableSchemas: List[Schema] = List(shEx, shaclex, jenaShacl) // shEx,shaclex) //,shacl_tq)
val availableSchemas: List[Schema] = List(shEx, shaclex, jenaShacl, shaclTQ)
val defaultSchema: Schema = shEx
val defaultSchemaName: String = defaultSchema.name
val defaultSchemaFormat: String = defaultSchema.defaultFormat
Expand Down
182 changes: 182 additions & 0 deletions modules/schema/src/main/scala/es/weso/schema/ShaclTQ.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package es.weso.schema

import cats.implicits._
import es.weso.rdf._
import es.weso.rdf.nodes._
import es.weso.rdf.jena.RDFAsJenaModel
import cats.effect._
import cats.effect.concurrent._
import scala.util.control.NoStackTrace
import org.apache.jena.rdf.model.Model
import org.apache.jena.rdf.model.ModelFactory
import java.io.StringReader
import org.apache.jena.riot._
import org.apache.jena.riot.Lang
import org.apache.jena.rdf.model.ModelFactory
import org.apache.jena.rdf.model.Resource;
import org.apache.jena.graph.Graph
import org.apache.jena.riot.system.{PrefixMap => _, _}
import org.apache.jena.riot.RDFLanguages
import es.weso.shapeMaps.ResultShapeMap
import collection.JavaConverters._
import es.weso.shapeMaps.ShapeMap
import java.io._
import es.weso.utils.JenaUtils
import org.topbraid.shacl.validation.ValidationUtil
import org.topbraid.jenax.util.JenaDatatypes
import org.topbraid.shacl.vocabulary.SH
import org.apache.jena.vocabulary.RDF

case class ShaclTQException(msg: String) extends Exception(msg) with NoStackTrace


case class ShaclTQ(shapesGraph: Model) extends Schema {
override def name = "SHACL_TQ"

override def formats: Seq[String] = DataFormats.formatNames ++ Seq(Lang.SHACLC.getName().toUpperCase())

override def defaultTriggerMode: ValidationTrigger = TargetDeclarations

override def validate(rdf: RDFReader, trigger: ValidationTrigger, builder: RDFBuilder): IO[Result] = trigger match {
case TargetDeclarations => validateTargetDecls(rdf).map(_.addTrigger(trigger))
case _ => IO(Result.errStr(s"Not implemented trigger ${trigger.name} for ${name} yet"))
}

private def validateTargetDecls(rdf: RDFReader): IO[Result] = rdf match {
case rdfJena: RDFAsJenaModel => for {
rdfModel <- rdfJena.getModel
pm <- rdfJena.getPrefixMap
shapesPm = shapesGraph.getNsPrefixMap()
report <- IO {
val report: Resource = ValidationUtil.validateModel(rdfModel, shapesGraph, true);

// val report: ValidationReport = ShaclValidator.get().validate(shapesGraph.getGraph(), rdfModel.getGraph())
report
}
result <- report2Result(report, pm, prefixMapFromModel(shapesGraph))
} yield result
case _ => IO.raiseError(ShaclTQException(s"Not Implemented Jena SHACL validation for ${rdf.rdfReaderName} yet"))
}

private def prefixMapFromModel(model: Model): PrefixMap = PrefixMap(model.getNsPrefixMap().asScala.toMap.map {
case (alias, iri) => (Prefix(alias), IRI(iri))
})

private def report2Result(
report: Resource,
nodesPrefixMap: PrefixMap,
shapesPrefixMap: PrefixMap
): IO[Result] = for {
eitherRdf <- report2reader(report.getModel()).attempt
isValid <- conforms(report)
numViolations <- countViolations(report)
} yield {
val message = if (isValid) s"Validated"
else s"Number of violations: ${numViolations}"
val shapesMap = report2ShapesMap()
val errors: Seq[ErrorInfo] = report2errors()
// val esRdf = eitherRdf.leftMap(_.getMessage())
Result(isValid = isValid,
message = message,
shapeMaps = Seq(shapesMap),
validationReport = JenaShaclReport(report.getModel),
errors = errors,
trigger = Some(TargetDeclarations),
nodesPrefixMap = nodesPrefixMap,
shapesPrefixMap = shapesPrefixMap)
}

private def conforms(report: Resource): IO[Boolean] =
IO { report.hasProperty(SH.conforms,JenaDatatypes.TRUE) }

private def countViolations(report: Resource): IO[Int] =
IO { report.getModel.listResourcesWithProperty(RDF.`type`,SH.Violation).toList.size }


private def report2reader(model: Model): IO[RDFReader] = for {
refModel <- Ref.of[IO, Model](model)
} yield RDFAsJenaModel(refModel,None,None)


private def report2errors(): Seq[ErrorInfo] = Seq()

private def report2ShapesMap(): ResultShapeMap = {
ResultShapeMap.empty
}

override def fromString(cs: CharSequence,
format: String,
base: Option[String]
): IO[Schema] = for {
model <- IO {
val m : Model = ModelFactory.createDefaultModel()
val str_reader = new StringReader(cs.toString)
val g: Graph = m.getGraph
val dest: StreamRDF = StreamRDFLib.graph(g)
RDFParser.create.source(str_reader).lang(RDFLanguages.shortnameToLang(format)).parse(dest)
m
}
} yield ShaclTQ(model)

// private def err[A](msg:String): EitherT[IO,String, A] = EitherT.leftT[IO,A](msg)

override def fromRDF(rdf: RDFReader): IO[es.weso.schema.Schema] = rdf match {
case rdfJena: RDFAsJenaModel => for {
_ <- IO { println(s"SHACL_TQ: Parsing Shapes graph from RDF data")}
model <- rdfJena.getModel
str <- rdfJena.serialize("TURTLE")
_ <- IO { println(s"RDF to parse:\n${str}")}
} yield ShaclTQ(model)
case _ => IO.raiseError(ShaclTQException(s"Cannot obtain ${name} from RDFReader ${rdf.rdfReaderName} yet"))
}

override def serialize(format: String, base: Option[IRI]): IO[String] =
if (formats.contains(format.toUpperCase)) IO {
val out = new ByteArrayOutputStream()
val relativizedModel = JenaUtils.relativizeModel(shapesGraph, base.map(_.uri))
relativizedModel.write(out, format)
out.toString
}
else
IO.raiseError(ShaclTQException(s"Format $format not supported to serialize $name. Supported formats=$formats"))

override def empty: Schema = ShaclTQ.empty

override def shapes: List[String] = {
List()
}

override def pm: PrefixMap = prefixMapFromModel(shapesGraph)

override def convert(targetFormat: Option[String],
targetEngine: Option[String],
base: Option[IRI]
): IO[String] = {
targetEngine.map(_.toUpperCase) match {
case None => serialize(targetFormat.getOrElse(DataFormats.defaultFormatName))
case Some("SHACL") | Some("SHACLEX") =>
serialize(targetFormat.getOrElse(DataFormats.defaultFormatName))
case Some("SHEX") =>
IO.raiseError(ShaclTQException(s"Not implemented conversion between ${name} to ShEx yet"))
case Some(other) =>
IO.raiseError(ShaclTQException(s"Conversion $name -> $other not implemented yet"))
}
}

override def info: SchemaInfo = {
// TODO: Check if shacl schemas are well formed
SchemaInfo(name,"SHACLex", isWellFormed = true, List())
}

override def toClingo(rdf: RDFReader, shapeMap: ShapeMap): IO[String] =
IO.raiseError(ShaclTQException(s"Not implemented yet toClingo for $name"))

}

object ShaclTQ {
def empty: ShaclTQ = {
val m = ModelFactory.createDefaultModel()
ShaclTQ(m)
}

}

0 comments on commit 6b9b569

Please sign in to comment.