From 30f1449365f51d3e138a3fcfd46aca2a4a4c55b9 Mon Sep 17 00:00:00 2001 From: Federico Igne Date: Tue, 1 Dec 2020 22:53:43 +0000 Subject: Add alternative conversion of axioms using switch-cases This is part of an effort to move away from the Java-style visitor pattern pushed by the OWLAPI and RDFox. Using a Scala approach will allow us to be more flexible in the long run. --- .../ox/cs/rsacomb/converter/RDFoxConverter.scala | 480 +++++++++++++++++++++ .../uk/ac/ox/cs/rsacomb/RDFoxConverterSpec.scala | 104 +++++ 2 files changed, 584 insertions(+) create mode 100644 src/main/scala/uk/ac/ox/cs/rsacomb/converter/RDFoxConverter.scala create mode 100644 src/test/scala/uk/ac/ox/cs/rsacomb/RDFoxConverterSpec.scala diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/converter/RDFoxConverter.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/converter/RDFoxConverter.scala new file mode 100644 index 0000000..9b2071e --- /dev/null +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/converter/RDFoxConverter.scala @@ -0,0 +1,480 @@ +package uk.ac.ox.cs.rsacomb.converter + +import java.util.stream.Collectors +import org.semanticweb.owlapi.model.{ + OWLAnnotationProperty, + OWLLogicalAxiom, + OWLClass, + OWLClassAssertionAxiom, + OWLClassExpression, + OWLDataProperty, + OWLDataPropertyDomainAxiom, + OWLDataPropertyExpression, + OWLDataSomeValuesFrom, + OWLEquivalentClassesAxiom, + OWLEquivalentObjectPropertiesAxiom, + OWLInverseObjectPropertiesAxiom, + OWLNamedIndividual, + OWLObjectIntersectionOf, + OWLObjectInverseOf, + OWLObjectMaxCardinality, + OWLObjectOneOf, + OWLObjectProperty, + OWLObjectPropertyAssertionAxiom, + OWLObjectPropertyDomainAxiom, + OWLObjectPropertyExpression, + OWLObjectPropertyRangeAxiom, + OWLObjectSomeValuesFrom, + OWLPropertyExpression, + OWLSubClassOfAxiom, + OWLSubObjectPropertyOfAxiom +} +import scala.collection.JavaConverters._ +import tech.oxfordsemantic.jrdfox.logic.datalog.{ + BindAtom, + BodyFormula, + Rule, + TupleTableAtom +} +import tech.oxfordsemantic.jrdfox.logic.expression.{Term, IRI, FunctionCall} +import uk.ac.ox.cs.rsacomb.RSAOntology +import uk.ac.ox.cs.rsacomb.suffix.{RSASuffix, Inverse} +import uk.ac.ox.cs.rsacomb.util.RSA + +/** Horn-ALCHOIQ to RDFox axiom converter. + * + * Provides the tools to translate Horn-ALCHOIQ axioms into logic rules + * using RDFox syntax. + * + * @note the input axioms are assumed to be normalized. Trying to + * convert non normalized axioms might result in undefined behavious. + * We use the normalization defined in the main paper. + * + * @see [[https://github.com/KRR-Oxford/RSA-combined-approach GitHub repository]] + * for more information on the theoretical aspects of the system. + * + * @todo this is not ideal and it would be more sensible to prepend a + * normalization procedure that will prevent errors or unexpected + * results. + */ +object RDFoxConverter { + + /** Simplify conversion between Java and Scala collections */ + import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._ + + /** Simplify conversion between similar concepts in OWLAPI and RDFox + * abstract syntax. + */ + import uk.ac.ox.cs.rsacomb.implicits.RDFox._ + + /** Represents the result of the conversion of a + * [[org.semanticweb.owlapi.model.OWLClassExpression OWLClassExpression]]. + * + * In general a class expression is translated into a list of + * [[tech.oxfordsemantic.jrdfox.logic.datalog.TupleTableAtom TupleTableAtoms]]. + * In some cases a class appearing on the right of a GCI might + * generate additional atoms that will appear in the body of the + * resulting formula. + * + * @example + * In `A ⊑ ≤1R.B`, translated as + * ``` + * y = z <- A(x), R(x,y), B(y), R(x,z), B(z) + * ``` + * the atom `≤1R.B` produces `y = z` to appear as head of the rule, + * along with a set of atoms for the body of the rule (namely + * `R(x,y), B(y), R(x,z), B(z)`). + */ + private type Shards = (List[TupleTableAtom], List[BodyFormula]) + + /** Represent the result of the conversion of + * [[org.semanticweb.owlapi.model.OWLLogicalAxiom OWLLogicalAxiom]]. + * + * In general we have assertion returning (a collection of) atoms, + * while other axioms that generate rules. + */ + private type Result = Either[List[TupleTableAtom], List[Rule]] + + /** Converts a + * [[org.semanticweb.owlapi.model.OWLLogicalAxiom OWLLogicalAxiom]] + * into a collection of + * [[tech.oxfordsemantic.jrdfox.logic.datalog.TupleTableAtom TupleTableAtoms]] + * and + * [[tech.oxfordsemantic.jrdfox.logic.datalog.Rule Rules]]. + * + * @note not all possible axioms are handled correctly, and in + * general they are assumed to be normalised. Following is a list of + * all unhandled class expressions: + * - [[org.semanticweb.owlapi.model.OWLAsymmetricObjectPropertyAxiom OWLAsymmetricObjectPropertyAxiom]] + * - [[org.semanticweb.owlapi.model.OWLDataPropertyAssertionAxiom OWLDataPropertyAssertionAxiom]] + * - [[org.semanticweb.owlapi.model.OWLDataPropertyRangeAxiom OWLDataPropertyRangeAxiom]] + * - [[org.semanticweb.owlapi.model.OWLDatatypeDefinitionAxiom OWLDatatypeDefinitionAxiom]] + * - [[org.semanticweb.owlapi.model.OWLDifferentIndividualsAxiom OWLDifferentIndividualsAxiom]] + * - [[org.semanticweb.owlapi.model.OWLDisjointClassesAxiom OWLDisjointClassesAxiom]] + * - [[org.semanticweb.owlapi.model.OWLDisjointDataPropertiesAxiom OWLDisjointDataPropertiesAxiom]] + * - [[org.semanticweb.owlapi.model.OWLDisjointObjectPropertiesAxiom OWLDisjointObjectPropertiesAxiom]] + * - [[org.semanticweb.owlapi.model.OWLDisjointUnionAxiom OWLDisjointUnionAxiom]] + * - [[org.semanticweb.owlapi.model.OWLEquivalentDataPropertiesAxiom OWLEquivalentDataPropertiesAxiom]] + * - [[org.semanticweb.owlapi.model.OWLFunctionalDataPropertyAxiom OWLFunctionalDataPropertyAxiom]] + * - [[org.semanticweb.owlapi.model.OWLFunctionalObjectPropertyAxiom OWLFunctionalObjectPropertyAxiom]] + * - [[org.semanticweb.owlapi.model.OWLHasKeyAxiom OWLHasKeyAxiom]] + * - [[org.semanticweb.owlapi.model.OWLInverseFunctionalObjectPropertyAxiom OWLInverseFunctionalObjectPropertyAxiom]] + * - [[org.semanticweb.owlapi.model.OWLIrreflexiveObjectPropertyAxiom OWLIrreflexiveObjectPropertyAxiom]] + * - [[org.semanticweb.owlapi.model.OWLNegativeDataPropertyAssertionAxiom OWLNegativeDataPropertyAssertionAxiom]] + * - [[org.semanticweb.owlapi.model.OWLNegativeObjectPropertyAssertionAxiom OWLNegativeObjectPropertyAssertionAxiom]] + * - [[org.semanticweb.owlapi.model.OWLReflexiveObjectPropertyAxiom OWLReflexiveObjectPropertyAxiom]] + * - [[org.semanticweb.owlapi.model.OWLSameIndividualAxiom OWLSameIndividualAxiom]] + * - [[org.semanticweb.owlapi.model.OWLSubDataPropertyOfAxiom OWLSubDataPropertyOfAxiom]] + * - [[org.semanticweb.owlapi.model.OWLSubPropertyChainOfAxiom OWLSubPropertyChainOfAxiom]] + * - [[org.semanticweb.owlapi.model.OWLSymmetricObjectPropertyAxiom OWLSymmetricObjectPropertyAxiom]] + * - [[org.semanticweb.owlapi.model.OWLTransitiveObjectPropertyAxiom OWLTransitiveObjectPropertyAxiom]] + * - [[org.semanticweb.owlapi.model.SWRLRule SWRLRule]] + */ + def convert( + axiom: OWLLogicalAxiom, + term: Term, + unsafe: List[OWLObjectPropertyExpression], + skolem: SkolemStrategy, + suffix: RSASuffix + ): Result = + axiom match { + + case a: OWLSubClassOfAxiom => { + val (sub, _) = + convert(a.getSubClass, term, unsafe, SkolemStrategy.None, suffix) + val (sup, ext) = + convert(a.getSuperClass, term, unsafe, skolem, suffix) + val rule = Rule.create(sup, ext ::: sub) + Right(List(rule)) + } + + // cannot be left + // http://www.w3.org/TR/owl2-syntax/#Equivalent_Classes + case a: OWLEquivalentClassesAxiom => + Right( + a.asPairwiseAxioms + .flatMap(_.asOWLSubClassOfAxioms) + .map(convert(_, term, unsafe, skolem, suffix)) + .collect { case Right(rs) => rs } + .flatten + ) + + case a: OWLEquivalentObjectPropertiesAxiom => { + Right( + a.asPairwiseAxioms + .flatMap(_.asSubObjectPropertyOfAxioms) + .map(convert(_, term, unsafe, skolem, suffix)) + .collect { case Right(rs) => rs } + .flatten + ) + } + + case a: OWLSubObjectPropertyOfAxiom => { + val term1 = RSAOntology.genFreshVariable() + val body = convert(a.getSubProperty, term, term1, suffix) + val head = convert(a.getSuperProperty, term, term1, suffix) + Right(List(Rule.create(head, body))) + } + + case a: OWLObjectPropertyDomainAxiom => + convert(a.asOWLSubClassOfAxiom, term, unsafe, skolem, suffix) + + case a: OWLObjectPropertyRangeAxiom => { + val term1 = RSAOntology.genFreshVariable() + val (res, ext) = convert(a.getRange, term, unsafe, skolem, suffix) + val prop = convert(a.getProperty, term1, term, suffix) + Right(List(Rule.create(res, prop :: ext))) + } + + case a: OWLDataPropertyDomainAxiom => + convert(a.asOWLSubClassOfAxiom, term, unsafe, skolem, suffix) + + case a: OWLInverseObjectPropertiesAxiom => + Right( + a.asSubObjectPropertyOfAxioms + .map(convert(_, term, unsafe, skolem, suffix)) + .collect { case Right(rs) => rs } + .flatten + ) + + case a: OWLClassAssertionAxiom => { + val ind = a.getIndividual + ind match { + case i: OWLNamedIndividual => { + val cls = a.getClassExpression + val (res, _) = + convert(cls, i.getIRI, unsafe, SkolemStrategy.None, suffix) + Left(res) + } + case _ => Left(List()) + } + } + + case a: OWLObjectPropertyAssertionAxiom => + if (!a.getSubject.isNamed || !a.getObject.isNamed) + Left(List()) + else { + val subj = a.getSubject.asOWLNamedIndividual.getIRI + val obj = a.getObject.asOWLNamedIndividual.getIRI + val prop = convert(a.getProperty, subj, obj, suffix) + Left(List(prop)) + } + + /** Catch-all case for all unhandled axiom types. */ + case a => + throw new RuntimeException( + s"Axiom '$a' is not supported (yet?)" + ) + + } + + /** Converts a class expression into a collection of atoms. + * + * @note not all possible class expressions are handled correctly. + * Following is a list of all unhandled class expressions: + * - [[org.semanticweb.owlapi.model.OWLDataAllValuesFrom OWLDataAllValuesFrom]] + * - [[org.semanticweb.owlapi.model.OWLDataExactCardinality OWLDataExactCardinality]] + * - [[org.semanticweb.owlapi.model.OWLDataMaxCardinality OWLDataMaxCardinality]] + * - [[org.semanticweb.owlapi.model.OWLDataMinCardinality OWLDataMinCardinality]] + * - [[org.semanticweb.owlapi.model.OWLDataHasValue OWLDataHasValue]] + * - [[org.semanticweb.owlapi.model.OWLObjectAllValuesFrom OWLObjectAllValuesFrom]] + * - [[org.semanticweb.owlapi.model.OWLObjectComplementOf OWLObjectComplementOf]] + * - [[org.semanticweb.owlapi.model.OWLObjectExactCardinality OWLObjectExactCardinality]] + * - [[org.semanticweb.owlapi.model.OWLObjectHasSelf OWLObjectHasSelf]] + * - [[org.semanticweb.owlapi.model.OWLObjectHasValue OWLObjectHasValue]] + * - [[org.semanticweb.owlapi.model.OWLObjectMinCardinality OWLObjectMinCardinality]] + * - [[org.semanticweb.owlapi.model.OWLObjectUnionOf OWLObjectUnionOf]] + * + * Moreover: + * - [[org.semanticweb.owlapi.model.OWLObjectMaxCardinality OWLObjectMaxCardinality]] + * is accepted only when cardinality is set to 1; + * - [[org.semanticweb.owlapi.model.OWLObjectOneOf OWLObjectOneOf]] + * is accepted only when its arity is 1. + */ + def convert( + expr: OWLClassExpression, + term: Term, + unsafe: List[OWLObjectPropertyExpression], + skolem: SkolemStrategy, + suffix: RSASuffix + ): Shards = + expr match { + + /** Simple class name. + * + * @see [[http://www.w3.org/TR/owl2-syntax/#Classes]] + */ + case e: OWLClass => { + val iri: IRI = if (e.isTopEntity()) IRI.THING else e.getIRI + val atom = TupleTableAtom.rdf(term, IRI.RDF_TYPE, iri) + (List(atom), List()) + } + + /** Conjunction of class expressions. + * + * @see [[http://www.w3.org/TR/owl2-syntax/#Intersection_of_Class_Expressions]] + */ + case e: OWLObjectIntersectionOf => { + val (res, ext) = e.asConjunctSet + .map(convert(_, term, unsafe, skolem, suffix)) + .unzip + (res.flatten, ext.flatten) + } + + /** Enumeration of individuals. + * + * @note we only admit enumerations of arity 1. + * + * @throws `RuntimeException` when dealing with an enumeration + * with arity != 1. + * + * @see [[http://www.w3.org/TR/owl2-syntax/#Enumeration_of_Individuals]] + */ + case e: OWLObjectOneOf => { + val named = e.individuals + .collect(Collectors.toList()) + .collect { case x: OWLNamedIndividual => x } + if (named.length != 1) + throw new RuntimeException(s"Class expression '$e' has arity != 1.") + val atom = TupleTableAtom.rdf(term, IRI.SAME_AS, named.head.getIRI) + (List(atom), List()) + } + + /** Existential class expression (for data properties). + * + * Parameter `skolem` is used to determine the skolemization + * technique (if any) to use for the translation. + * + * @see [[http://www.w3.org/TR/owl2-syntax/#Existential_Quantification]] + */ + case e: OWLObjectSomeValuesFrom => { + val cls = e.getFiller() + val role = e.getProperty() + // TODO: simplify this: + // Computes the result of rule skolemization. Depending on the used + // technique it might involve the introduction of additional atoms, + // and/or fresh constants and variables. + val (head, body, term1) = skolem match { + case SkolemStrategy.None => + (List(), List(), RSAOntology.genFreshVariable) + case SkolemStrategy.Constant(c) => (List(), List(), c) + case SkolemStrategy.ConstantRSA(c) => { + if (unsafe.contains(role)) + (List(RSA.PE(term, c), RSA.U(c)), List(), c) + else + (List(), List(), c) + } + case SkolemStrategy.Standard(f) => { + val x = RSAOntology.genFreshVariable + ( + List(), + List(BindAtom.create(FunctionCall.create("SKOLEM", f, term), x)), + x + ) + } + } + val (res, ext) = convert(cls, term1, unsafe, skolem, suffix) + val prop = convert(role, term, term1, suffix) + (prop :: head ::: res, body ::: ext) + } + + /** Existential class expression (for data properties). + * + * Parameter `skolem` is used to determine the skolemization + * technique (if any) to use for the translation. + * + * @todo the "filler" of this OWL expression is currently ignored. + * This, in general might not be how we want to handle + * [[org.semanticweb.owlapi.model.OWLDataRange OWLDataRanges]]. + * + * @see [[http://www.w3.org/TR/owl2-syntax/#Existential_Quantification_2]] + */ + case e: OWLDataSomeValuesFrom => { + val role = e.getProperty() + // TODO: simplify this: + // Computes the result of rule skolemization. Depending on the used + // technique it might involve the introduction of additional atoms, + // and/or fresh constants and variables. + val (head, body, term1) = skolem match { + case SkolemStrategy.None => + (List(), List(), RSAOntology.genFreshVariable) + case SkolemStrategy.Constant(c) => (List(), List(), c) + case SkolemStrategy.ConstantRSA(c) => { + if (unsafe.contains(role)) + (List(RSA.PE(term, c), RSA.U(c)), List(), c) + else + (List(), List(), c) + } + case SkolemStrategy.Standard(f) => { + val y = RSAOntology.genFreshVariable() + ( + List(), + List(BindAtom.create(FunctionCall.create("SKOLEM", f, term), y)), + y + ) + } + } + val prop = convert(role, term, term1, suffix) + (prop :: head, body) + } + + /** Maximum cardinality restriction class + * + * @note we only admit classes with cardinality set to 1. + * + * @throws `RuntimeException` when dealing with a restriction + * with cardinality != 1. + * + * @see [[http://www.w3.org/TR/owl2-syntax/#Maximum_Cardinality_2]] + */ + case e: OWLObjectMaxCardinality => { + if (e.getCardinality != 1) + throw new RuntimeException( + s"Class expression '$e' has cardinality restriction != 1." + ) + val vars @ (y :: z :: _) = + Seq(RSAOntology.genFreshVariable(), RSAOntology.genFreshVariable()) + val cls = e.getFiller + val role = e.getProperty + val (res, ext) = vars.map(convert(cls, _, unsafe, skolem, suffix)).unzip + val props = vars.map(convert(role, term, _, suffix)) + val eq = TupleTableAtom.rdf(y, IRI.SAME_AS, z) + (List(eq), res.flatten ++ props) + } + + /** Catch-all case for all unhandled class expressions. */ + case e => + throw new RuntimeException( + s"Class expression '$e' is not supported (yet?)" + ) + } + + /** Converts an object property expression into an atom. */ + def convert( + expr: OWLObjectPropertyExpression, + term1: Term, + term2: Term, + suffix: RSASuffix + ): TupleTableAtom = + expr match { + + /** Simple named role/object property. + * + * @see [[http://www.w3.org/TR/owl2-syntax/#Object_Properties Object Properties]] + */ + case e: OWLObjectProperty => { + val role = IRI.create(e.getIRI.getIRIString :: suffix) + TupleTableAtom.rdf(term1, role, term2) + } + + /** Inverse of a named role/property + * + * OWLAPI does not admit nesting of negation, and double + * negations are always simplified. + * + * @see [[https://www.w3.org/TR/owl2-syntax/#Inverse_Object_Properties Inverse Object Properties]] + */ + case e: OWLObjectInverseOf => + convert(e.getInverse, term1, term2, suffix + Inverse) + + /** The infamous impossible case. + * + * @note all relevant cases are taken care of, and this branch + * throws a runtime exception to notify of the problem. + */ + case e => + throw new RuntimeException( + s"Unable to convert '$e' into a logic expression. This should be happening (TM)." + ) + } + + /** Converts a data property expression into an atom. */ + def convert( + expr: OWLDataPropertyExpression, + term1: Term, + term2: Term, + suffix: RSASuffix + ): TupleTableAtom = + expr match { + + /** Simple named role/data property + * + * @see [[https://www.w3.org/TR/owl2-syntax/#Datatypes Data Properties]] + */ + case e: OWLDataProperty => { + val role = IRI.create(e.getIRI.getIRIString :: suffix) + TupleTableAtom.rdf(term1, role, term2) + } + + /** The infamous impossible case. + * + * @note all relevant cases are taken care of, and this branch + * throws a runtime exception to notify of the problem. + */ + case e => + throw new RuntimeException( + s"Unable to convert '$e' into a logic expression. This should be happening (TM)." + ) + } + +} diff --git a/src/test/scala/uk/ac/ox/cs/rsacomb/RDFoxConverterSpec.scala b/src/test/scala/uk/ac/ox/cs/rsacomb/RDFoxConverterSpec.scala new file mode 100644 index 0000000..35af464 --- /dev/null +++ b/src/test/scala/uk/ac/ox/cs/rsacomb/RDFoxConverterSpec.scala @@ -0,0 +1,104 @@ +package rsacomb + +import org.scalatest.LoneElement +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import org.semanticweb.owlapi.apibinding.OWLManager +import org.semanticweb.owlapi.model.OWLOntologyManager + +import tech.oxfordsemantic.jrdfox.logic.datalog.TupleTableAtom +import tech.oxfordsemantic.jrdfox.logic.expression.{Variable, IRI} +import uk.ac.ox.cs.rsacomb.converter.RDFoxConverter +import uk.ac.ox.cs.rsacomb.suffix.{Empty, Forward, Backward, Inverse} +import uk.ac.ox.cs.rsacomb.converter.SkolemStrategy + +object RDFoxConverterSpec { + + val manager = OWLManager.createOWLOntologyManager() + val factory = manager.getOWLDataFactory + + val term0 = Variable.create("X") + val term1 = Variable.create("Y") + val iriString0 = "http://example.com/rsacomb/iri0" + val iriString1 = "http://example.com/rsacomb/iri1" + val iriString2 = "http://example.com/rsacomb/iri2" + val suffixes = Seq( + Empty, + Forward, + Backward, + Inverse, + Forward + Inverse, + Backward + Inverse + ) +} + +class RDFoxConverterSpec extends AnyFlatSpec with Matchers with LoneElement { + + import RDFoxConverterSpec._ + + "A class name" should "be converted into a single atom" in { + val cls = factory.getOWLClass(iriString0) + val atom = TupleTableAtom.rdf(term0, IRI.RDF_TYPE, IRI.create(iriString0)) + val (res, ext) = + RDFoxConverter.convert(cls, term0, List(), SkolemStrategy.None, Empty) + res.loneElement shouldEqual atom + ext shouldBe empty + } + + "A intersection of classes" should "be converted into the union of the conversion of the classes" in { + val cls0 = factory.getOWLClass(iriString0) + val cls1 = factory.getOWLClass(iriString1) + val cls2 = factory.getOWLClass(iriString2) + val conj = factory.getOWLObjectIntersectionOf(cls0, cls1, cls2) + val (res0, ext0) = + RDFoxConverter.convert(cls0, term0, List(), SkolemStrategy.None, Empty) + val (res1, ext1) = + RDFoxConverter.convert(cls1, term0, List(), SkolemStrategy.None, Empty) + val (res2, ext2) = + RDFoxConverter.convert(cls2, term0, List(), SkolemStrategy.None, Empty) + val (res, ext) = + RDFoxConverter.convert(conj, term0, List(), SkolemStrategy.None, Empty) + res should contain theSameElementsAs (res0 ::: res1 ::: res2) + ext should contain theSameElementsAs (ext0 ::: ext1 ::: ext2) + } + + "A singleton intersection" should "correspond to the conversion of the internal class" in { + val cls0 = factory.getOWLClass(iriString0) + val conj = factory.getOWLObjectIntersectionOf(cls0) + val (res0, ext0) = + RDFoxConverter.convert(cls0, term0, List(), SkolemStrategy.None, Empty) + val (res, ext) = + RDFoxConverter.convert(conj, term0, List(), SkolemStrategy.None, Empty) + res should contain theSameElementsAs res0 + ext should contain theSameElementsAs ext0 + } + + "An object property" should "be converted into an atom with matching predicate" in { + val prop = factory.getOWLObjectProperty(iriString0) + for (sx <- suffixes) { + val atom = + TupleTableAtom.rdf(term0, IRI.create(iriString0 :: sx), term1) + RDFoxConverter.convert(prop, term0, term1, sx) shouldEqual atom + } + } + + "The inverse of an object property" should "be converted into an atom with matching negated predicate" in { + val prop = factory.getOWLObjectProperty(iriString0) + val inv = factory.getOWLObjectInverseOf(prop) + for (sx <- Seq(Empty, Forward, Backward)) { + val atom = + TupleTableAtom.rdf(term0, IRI.create(iriString0 :: sx + Inverse), term1) + RDFoxConverter.convert(inv, term0, term1, sx) shouldEqual atom + } + } + + "A data property" should "be converted into an atom with matching predicate" in { + val prop = factory.getOWLDataProperty(iriString0) + for (suffix <- suffixes) { + val atom = + TupleTableAtom.rdf(term0, IRI.create(iriString0 :: suffix), term1) + RDFoxConverter.convert(prop, term0, term1, suffix) shouldEqual atom + } + } + +} -- cgit v1.2.3