diff options
| author | Federico Igne <federico.igne@cs.ox.ac.uk> | 2020-12-01 22:53:43 +0000 |
|---|---|---|
| committer | Federico Igne <federico.igne@cs.ox.ac.uk> | 2020-12-01 22:53:43 +0000 |
| commit | 30f1449365f51d3e138a3fcfd46aca2a4a4c55b9 (patch) | |
| tree | 3aa0b1abb667356602cfeac3086b2818810aa419 /src | |
| parent | 6462d8566cc10b47473803e3e9e9547cec6524be (diff) | |
| download | RSAComb-30f1449365f51d3e138a3fcfd46aca2a4a4c55b9.tar.gz RSAComb-30f1449365f51d3e138a3fcfd46aca2a4a4c55b9.zip | |
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.
Diffstat (limited to 'src')
| -rw-r--r-- | src/main/scala/uk/ac/ox/cs/rsacomb/converter/RDFoxConverter.scala | 480 | ||||
| -rw-r--r-- | src/test/scala/uk/ac/ox/cs/rsacomb/RDFoxConverterSpec.scala | 104 |
2 files changed, 584 insertions, 0 deletions
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 @@ | |||
| 1 | package uk.ac.ox.cs.rsacomb.converter | ||
| 2 | |||
| 3 | import java.util.stream.Collectors | ||
| 4 | import org.semanticweb.owlapi.model.{ | ||
| 5 | OWLAnnotationProperty, | ||
| 6 | OWLLogicalAxiom, | ||
| 7 | OWLClass, | ||
| 8 | OWLClassAssertionAxiom, | ||
| 9 | OWLClassExpression, | ||
| 10 | OWLDataProperty, | ||
| 11 | OWLDataPropertyDomainAxiom, | ||
| 12 | OWLDataPropertyExpression, | ||
| 13 | OWLDataSomeValuesFrom, | ||
| 14 | OWLEquivalentClassesAxiom, | ||
| 15 | OWLEquivalentObjectPropertiesAxiom, | ||
| 16 | OWLInverseObjectPropertiesAxiom, | ||
| 17 | OWLNamedIndividual, | ||
| 18 | OWLObjectIntersectionOf, | ||
| 19 | OWLObjectInverseOf, | ||
| 20 | OWLObjectMaxCardinality, | ||
| 21 | OWLObjectOneOf, | ||
| 22 | OWLObjectProperty, | ||
| 23 | OWLObjectPropertyAssertionAxiom, | ||
| 24 | OWLObjectPropertyDomainAxiom, | ||
| 25 | OWLObjectPropertyExpression, | ||
| 26 | OWLObjectPropertyRangeAxiom, | ||
| 27 | OWLObjectSomeValuesFrom, | ||
| 28 | OWLPropertyExpression, | ||
| 29 | OWLSubClassOfAxiom, | ||
| 30 | OWLSubObjectPropertyOfAxiom | ||
| 31 | } | ||
| 32 | import scala.collection.JavaConverters._ | ||
| 33 | import tech.oxfordsemantic.jrdfox.logic.datalog.{ | ||
| 34 | BindAtom, | ||
| 35 | BodyFormula, | ||
| 36 | Rule, | ||
| 37 | TupleTableAtom | ||
| 38 | } | ||
| 39 | import tech.oxfordsemantic.jrdfox.logic.expression.{Term, IRI, FunctionCall} | ||
| 40 | import uk.ac.ox.cs.rsacomb.RSAOntology | ||
| 41 | import uk.ac.ox.cs.rsacomb.suffix.{RSASuffix, Inverse} | ||
| 42 | import uk.ac.ox.cs.rsacomb.util.RSA | ||
| 43 | |||
| 44 | /** Horn-ALCHOIQ to RDFox axiom converter. | ||
| 45 | * | ||
| 46 | * Provides the tools to translate Horn-ALCHOIQ axioms into logic rules | ||
| 47 | * using RDFox syntax. | ||
| 48 | * | ||
| 49 | * @note the input axioms are assumed to be normalized. Trying to | ||
| 50 | * convert non normalized axioms might result in undefined behavious. | ||
| 51 | * We use the normalization defined in the main paper. | ||
| 52 | * | ||
| 53 | * @see [[https://github.com/KRR-Oxford/RSA-combined-approach GitHub repository]] | ||
| 54 | * for more information on the theoretical aspects of the system. | ||
| 55 | * | ||
| 56 | * @todo this is not ideal and it would be more sensible to prepend a | ||
| 57 | * normalization procedure that will prevent errors or unexpected | ||
| 58 | * results. | ||
| 59 | */ | ||
| 60 | object RDFoxConverter { | ||
| 61 | |||
| 62 | /** Simplify conversion between Java and Scala collections */ | ||
| 63 | import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._ | ||
| 64 | |||
| 65 | /** Simplify conversion between similar concepts in OWLAPI and RDFox | ||
| 66 | * abstract syntax. | ||
| 67 | */ | ||
| 68 | import uk.ac.ox.cs.rsacomb.implicits.RDFox._ | ||
| 69 | |||
| 70 | /** Represents the result of the conversion of a | ||
| 71 | * [[org.semanticweb.owlapi.model.OWLClassExpression OWLClassExpression]]. | ||
| 72 | * | ||
| 73 | * In general a class expression is translated into a list of | ||
| 74 | * [[tech.oxfordsemantic.jrdfox.logic.datalog.TupleTableAtom TupleTableAtoms]]. | ||
| 75 | * In some cases a class appearing on the right of a GCI might | ||
| 76 | * generate additional atoms that will appear in the body of the | ||
| 77 | * resulting formula. | ||
| 78 | * | ||
| 79 | * @example | ||
| 80 | * In `A ⊑ ≤1R.B`, translated as | ||
| 81 | * ``` | ||
| 82 | * y = z <- A(x), R(x,y), B(y), R(x,z), B(z) | ||
| 83 | * ``` | ||
| 84 | * the atom `≤1R.B` produces `y = z` to appear as head of the rule, | ||
| 85 | * along with a set of atoms for the body of the rule (namely | ||
| 86 | * `R(x,y), B(y), R(x,z), B(z)`). | ||
| 87 | */ | ||
| 88 | private type Shards = (List[TupleTableAtom], List[BodyFormula]) | ||
| 89 | |||
| 90 | /** Represent the result of the conversion of | ||
| 91 | * [[org.semanticweb.owlapi.model.OWLLogicalAxiom OWLLogicalAxiom]]. | ||
| 92 | * | ||
| 93 | * In general we have assertion returning (a collection of) atoms, | ||
| 94 | * while other axioms that generate rules. | ||
| 95 | */ | ||
| 96 | private type Result = Either[List[TupleTableAtom], List[Rule]] | ||
| 97 | |||
| 98 | /** Converts a | ||
| 99 | * [[org.semanticweb.owlapi.model.OWLLogicalAxiom OWLLogicalAxiom]] | ||
| 100 | * into a collection of | ||
| 101 | * [[tech.oxfordsemantic.jrdfox.logic.datalog.TupleTableAtom TupleTableAtoms]] | ||
| 102 | * and | ||
| 103 | * [[tech.oxfordsemantic.jrdfox.logic.datalog.Rule Rules]]. | ||
| 104 | * | ||
| 105 | * @note not all possible axioms are handled correctly, and in | ||
| 106 | * general they are assumed to be normalised. Following is a list of | ||
| 107 | * all unhandled class expressions: | ||
| 108 | * - [[org.semanticweb.owlapi.model.OWLAsymmetricObjectPropertyAxiom OWLAsymmetricObjectPropertyAxiom]] | ||
| 109 | * - [[org.semanticweb.owlapi.model.OWLDataPropertyAssertionAxiom OWLDataPropertyAssertionAxiom]] | ||
| 110 | * - [[org.semanticweb.owlapi.model.OWLDataPropertyRangeAxiom OWLDataPropertyRangeAxiom]] | ||
| 111 | * - [[org.semanticweb.owlapi.model.OWLDatatypeDefinitionAxiom OWLDatatypeDefinitionAxiom]] | ||
| 112 | * - [[org.semanticweb.owlapi.model.OWLDifferentIndividualsAxiom OWLDifferentIndividualsAxiom]] | ||
| 113 | * - [[org.semanticweb.owlapi.model.OWLDisjointClassesAxiom OWLDisjointClassesAxiom]] | ||
| 114 | * - [[org.semanticweb.owlapi.model.OWLDisjointDataPropertiesAxiom OWLDisjointDataPropertiesAxiom]] | ||
| 115 | * - [[org.semanticweb.owlapi.model.OWLDisjointObjectPropertiesAxiom OWLDisjointObjectPropertiesAxiom]] | ||
| 116 | * - [[org.semanticweb.owlapi.model.OWLDisjointUnionAxiom OWLDisjointUnionAxiom]] | ||
| 117 | * - [[org.semanticweb.owlapi.model.OWLEquivalentDataPropertiesAxiom OWLEquivalentDataPropertiesAxiom]] | ||
| 118 | * - [[org.semanticweb.owlapi.model.OWLFunctionalDataPropertyAxiom OWLFunctionalDataPropertyAxiom]] | ||
| 119 | * - [[org.semanticweb.owlapi.model.OWLFunctionalObjectPropertyAxiom OWLFunctionalObjectPropertyAxiom]] | ||
| 120 | * - [[org.semanticweb.owlapi.model.OWLHasKeyAxiom OWLHasKeyAxiom]] | ||
| 121 | * - [[org.semanticweb.owlapi.model.OWLInverseFunctionalObjectPropertyAxiom OWLInverseFunctionalObjectPropertyAxiom]] | ||
| 122 | * - [[org.semanticweb.owlapi.model.OWLIrreflexiveObjectPropertyAxiom OWLIrreflexiveObjectPropertyAxiom]] | ||
| 123 | * - [[org.semanticweb.owlapi.model.OWLNegativeDataPropertyAssertionAxiom OWLNegativeDataPropertyAssertionAxiom]] | ||
| 124 | * - [[org.semanticweb.owlapi.model.OWLNegativeObjectPropertyAssertionAxiom OWLNegativeObjectPropertyAssertionAxiom]] | ||
| 125 | * - [[org.semanticweb.owlapi.model.OWLReflexiveObjectPropertyAxiom OWLReflexiveObjectPropertyAxiom]] | ||
| 126 | * - [[org.semanticweb.owlapi.model.OWLSameIndividualAxiom OWLSameIndividualAxiom]] | ||
| 127 | * - [[org.semanticweb.owlapi.model.OWLSubDataPropertyOfAxiom OWLSubDataPropertyOfAxiom]] | ||
| 128 | * - [[org.semanticweb.owlapi.model.OWLSubPropertyChainOfAxiom OWLSubPropertyChainOfAxiom]] | ||
| 129 | * - [[org.semanticweb.owlapi.model.OWLSymmetricObjectPropertyAxiom OWLSymmetricObjectPropertyAxiom]] | ||
| 130 | * - [[org.semanticweb.owlapi.model.OWLTransitiveObjectPropertyAxiom OWLTransitiveObjectPropertyAxiom]] | ||
| 131 | * - [[org.semanticweb.owlapi.model.SWRLRule SWRLRule]] | ||
| 132 | */ | ||
| 133 | def convert( | ||
| 134 | axiom: OWLLogicalAxiom, | ||
| 135 | term: Term, | ||
| 136 | unsafe: List[OWLObjectPropertyExpression], | ||
| 137 | skolem: SkolemStrategy, | ||
| 138 | suffix: RSASuffix | ||
| 139 | ): Result = | ||
| 140 | axiom match { | ||
| 141 | |||
| 142 | case a: OWLSubClassOfAxiom => { | ||
| 143 | val (sub, _) = | ||
| 144 | convert(a.getSubClass, term, unsafe, SkolemStrategy.None, suffix) | ||
| 145 | val (sup, ext) = | ||
| 146 | convert(a.getSuperClass, term, unsafe, skolem, suffix) | ||
| 147 | val rule = Rule.create(sup, ext ::: sub) | ||
| 148 | Right(List(rule)) | ||
| 149 | } | ||
| 150 | |||
| 151 | // cannot be left | ||
| 152 | // http://www.w3.org/TR/owl2-syntax/#Equivalent_Classes | ||
| 153 | case a: OWLEquivalentClassesAxiom => | ||
| 154 | Right( | ||
| 155 | a.asPairwiseAxioms | ||
| 156 | .flatMap(_.asOWLSubClassOfAxioms) | ||
| 157 | .map(convert(_, term, unsafe, skolem, suffix)) | ||
| 158 | .collect { case Right(rs) => rs } | ||
| 159 | .flatten | ||
| 160 | ) | ||
| 161 | |||
| 162 | case a: OWLEquivalentObjectPropertiesAxiom => { | ||
| 163 | Right( | ||
| 164 | a.asPairwiseAxioms | ||
| 165 | .flatMap(_.asSubObjectPropertyOfAxioms) | ||
| 166 | .map(convert(_, term, unsafe, skolem, suffix)) | ||
| 167 | .collect { case Right(rs) => rs } | ||
| 168 | .flatten | ||
| 169 | ) | ||
| 170 | } | ||
| 171 | |||
| 172 | case a: OWLSubObjectPropertyOfAxiom => { | ||
| 173 | val term1 = RSAOntology.genFreshVariable() | ||
| 174 | val body = convert(a.getSubProperty, term, term1, suffix) | ||
| 175 | val head = convert(a.getSuperProperty, term, term1, suffix) | ||
| 176 | Right(List(Rule.create(head, body))) | ||
| 177 | } | ||
| 178 | |||
| 179 | case a: OWLObjectPropertyDomainAxiom => | ||
| 180 | convert(a.asOWLSubClassOfAxiom, term, unsafe, skolem, suffix) | ||
| 181 | |||
| 182 | case a: OWLObjectPropertyRangeAxiom => { | ||
| 183 | val term1 = RSAOntology.genFreshVariable() | ||
| 184 | val (res, ext) = convert(a.getRange, term, unsafe, skolem, suffix) | ||
| 185 | val prop = convert(a.getProperty, term1, term, suffix) | ||
| 186 | Right(List(Rule.create(res, prop :: ext))) | ||
| 187 | } | ||
| 188 | |||
| 189 | case a: OWLDataPropertyDomainAxiom => | ||
| 190 | convert(a.asOWLSubClassOfAxiom, term, unsafe, skolem, suffix) | ||
| 191 | |||
| 192 | case a: OWLInverseObjectPropertiesAxiom => | ||
| 193 | Right( | ||
| 194 | a.asSubObjectPropertyOfAxioms | ||
| 195 | .map(convert(_, term, unsafe, skolem, suffix)) | ||
| 196 | .collect { case Right(rs) => rs } | ||
| 197 | .flatten | ||
| 198 | ) | ||
| 199 | |||
| 200 | case a: OWLClassAssertionAxiom => { | ||
| 201 | val ind = a.getIndividual | ||
| 202 | ind match { | ||
| 203 | case i: OWLNamedIndividual => { | ||
| 204 | val cls = a.getClassExpression | ||
| 205 | val (res, _) = | ||
| 206 | convert(cls, i.getIRI, unsafe, SkolemStrategy.None, suffix) | ||
| 207 | Left(res) | ||
| 208 | } | ||
| 209 | case _ => Left(List()) | ||
| 210 | } | ||
| 211 | } | ||
| 212 | |||
| 213 | case a: OWLObjectPropertyAssertionAxiom => | ||
| 214 | if (!a.getSubject.isNamed || !a.getObject.isNamed) | ||
| 215 | Left(List()) | ||
| 216 | else { | ||
| 217 | val subj = a.getSubject.asOWLNamedIndividual.getIRI | ||
| 218 | val obj = a.getObject.asOWLNamedIndividual.getIRI | ||
| 219 | val prop = convert(a.getProperty, subj, obj, suffix) | ||
| 220 | Left(List(prop)) | ||
| 221 | } | ||
| 222 | |||
| 223 | /** Catch-all case for all unhandled axiom types. */ | ||
| 224 | case a => | ||
| 225 | throw new RuntimeException( | ||
| 226 | s"Axiom '$a' is not supported (yet?)" | ||
| 227 | ) | ||
| 228 | |||
| 229 | } | ||
| 230 | |||
| 231 | /** Converts a class expression into a collection of atoms. | ||
| 232 | * | ||
| 233 | * @note not all possible class expressions are handled correctly. | ||
| 234 | * Following is a list of all unhandled class expressions: | ||
| 235 | * - [[org.semanticweb.owlapi.model.OWLDataAllValuesFrom OWLDataAllValuesFrom]] | ||
| 236 | * - [[org.semanticweb.owlapi.model.OWLDataExactCardinality OWLDataExactCardinality]] | ||
| 237 | * - [[org.semanticweb.owlapi.model.OWLDataMaxCardinality OWLDataMaxCardinality]] | ||
| 238 | * - [[org.semanticweb.owlapi.model.OWLDataMinCardinality OWLDataMinCardinality]] | ||
| 239 | * - [[org.semanticweb.owlapi.model.OWLDataHasValue OWLDataHasValue]] | ||
| 240 | * - [[org.semanticweb.owlapi.model.OWLObjectAllValuesFrom OWLObjectAllValuesFrom]] | ||
| 241 | * - [[org.semanticweb.owlapi.model.OWLObjectComplementOf OWLObjectComplementOf]] | ||
| 242 | * - [[org.semanticweb.owlapi.model.OWLObjectExactCardinality OWLObjectExactCardinality]] | ||
| 243 | * - [[org.semanticweb.owlapi.model.OWLObjectHasSelf OWLObjectHasSelf]] | ||
| 244 | * - [[org.semanticweb.owlapi.model.OWLObjectHasValue OWLObjectHasValue]] | ||
| 245 | * - [[org.semanticweb.owlapi.model.OWLObjectMinCardinality OWLObjectMinCardinality]] | ||
| 246 | * - [[org.semanticweb.owlapi.model.OWLObjectUnionOf OWLObjectUnionOf]] | ||
| 247 | * | ||
| 248 | * Moreover: | ||
| 249 | * - [[org.semanticweb.owlapi.model.OWLObjectMaxCardinality OWLObjectMaxCardinality]] | ||
| 250 | * is accepted only when cardinality is set to 1; | ||
| 251 | * - [[org.semanticweb.owlapi.model.OWLObjectOneOf OWLObjectOneOf]] | ||
| 252 | * is accepted only when its arity is 1. | ||
| 253 | */ | ||
| 254 | def convert( | ||
| 255 | expr: OWLClassExpression, | ||
| 256 | term: Term, | ||
| 257 | unsafe: List[OWLObjectPropertyExpression], | ||
| 258 | skolem: SkolemStrategy, | ||
| 259 | suffix: RSASuffix | ||
| 260 | ): Shards = | ||
| 261 | expr match { | ||
| 262 | |||
| 263 | /** Simple class name. | ||
| 264 | * | ||
| 265 | * @see [[http://www.w3.org/TR/owl2-syntax/#Classes]] | ||
| 266 | */ | ||
| 267 | case e: OWLClass => { | ||
| 268 | val iri: IRI = if (e.isTopEntity()) IRI.THING else e.getIRI | ||
| 269 | val atom = TupleTableAtom.rdf(term, IRI.RDF_TYPE, iri) | ||
| 270 | (List(atom), List()) | ||
| 271 | } | ||
| 272 | |||
| 273 | /** Conjunction of class expressions. | ||
| 274 | * | ||
| 275 | * @see [[http://www.w3.org/TR/owl2-syntax/#Intersection_of_Class_Expressions]] | ||
| 276 | */ | ||
| 277 | case e: OWLObjectIntersectionOf => { | ||
| 278 | val (res, ext) = e.asConjunctSet | ||
| 279 | .map(convert(_, term, unsafe, skolem, suffix)) | ||
| 280 | .unzip | ||
| 281 | (res.flatten, ext.flatten) | ||
| 282 | } | ||
| 283 | |||
| 284 | /** Enumeration of individuals. | ||
| 285 | * | ||
| 286 | * @note we only admit enumerations of arity 1. | ||
| 287 | * | ||
| 288 | * @throws `RuntimeException` when dealing with an enumeration | ||
| 289 | * with arity != 1. | ||
| 290 | * | ||
| 291 | * @see [[http://www.w3.org/TR/owl2-syntax/#Enumeration_of_Individuals]] | ||
| 292 | */ | ||
| 293 | case e: OWLObjectOneOf => { | ||
| 294 | val named = e.individuals | ||
| 295 | .collect(Collectors.toList()) | ||
| 296 | .collect { case x: OWLNamedIndividual => x } | ||
| 297 | if (named.length != 1) | ||
| 298 | throw new RuntimeException(s"Class expression '$e' has arity != 1.") | ||
| 299 | val atom = TupleTableAtom.rdf(term, IRI.SAME_AS, named.head.getIRI) | ||
| 300 | (List(atom), List()) | ||
| 301 | } | ||
| 302 | |||
| 303 | /** Existential class expression (for data properties). | ||
| 304 | * | ||
| 305 | * Parameter `skolem` is used to determine the skolemization | ||
| 306 | * technique (if any) to use for the translation. | ||
| 307 | * | ||
| 308 | * @see [[http://www.w3.org/TR/owl2-syntax/#Existential_Quantification]] | ||
| 309 | */ | ||
| 310 | case e: OWLObjectSomeValuesFrom => { | ||
| 311 | val cls = e.getFiller() | ||
| 312 | val role = e.getProperty() | ||
| 313 | // TODO: simplify this: | ||
| 314 | // Computes the result of rule skolemization. Depending on the used | ||
| 315 | // technique it might involve the introduction of additional atoms, | ||
| 316 | // and/or fresh constants and variables. | ||
| 317 | val (head, body, term1) = skolem match { | ||
| 318 | case SkolemStrategy.None => | ||
| 319 | (List(), List(), RSAOntology.genFreshVariable) | ||
| 320 | case SkolemStrategy.Constant(c) => (List(), List(), c) | ||
| 321 | case SkolemStrategy.ConstantRSA(c) => { | ||
| 322 | if (unsafe.contains(role)) | ||
| 323 | (List(RSA.PE(term, c), RSA.U(c)), List(), c) | ||
| 324 | else | ||
| 325 | (List(), List(), c) | ||
| 326 | } | ||
| 327 | case SkolemStrategy.Standard(f) => { | ||
| 328 | val x = RSAOntology.genFreshVariable | ||
| 329 | ( | ||
| 330 | List(), | ||
| 331 | List(BindAtom.create(FunctionCall.create("SKOLEM", f, term), x)), | ||
| 332 | x | ||
| 333 | ) | ||
| 334 | } | ||
| 335 | } | ||
| 336 | val (res, ext) = convert(cls, term1, unsafe, skolem, suffix) | ||
| 337 | val prop = convert(role, term, term1, suffix) | ||
| 338 | (prop :: head ::: res, body ::: ext) | ||
| 339 | } | ||
| 340 | |||
| 341 | /** Existential class expression (for data properties). | ||
| 342 | * | ||
| 343 | * Parameter `skolem` is used to determine the skolemization | ||
| 344 | * technique (if any) to use for the translation. | ||
| 345 | * | ||
| 346 | * @todo the "filler" of this OWL expression is currently ignored. | ||
| 347 | * This, in general might not be how we want to handle | ||
| 348 | * [[org.semanticweb.owlapi.model.OWLDataRange OWLDataRanges]]. | ||
| 349 | * | ||
| 350 | * @see [[http://www.w3.org/TR/owl2-syntax/#Existential_Quantification_2]] | ||
| 351 | */ | ||
| 352 | case e: OWLDataSomeValuesFrom => { | ||
| 353 | val role = e.getProperty() | ||
| 354 | // TODO: simplify this: | ||
| 355 | // Computes the result of rule skolemization. Depending on the used | ||
| 356 | // technique it might involve the introduction of additional atoms, | ||
| 357 | // and/or fresh constants and variables. | ||
| 358 | val (head, body, term1) = skolem match { | ||
| 359 | case SkolemStrategy.None => | ||
| 360 | (List(), List(), RSAOntology.genFreshVariable) | ||
| 361 | case SkolemStrategy.Constant(c) => (List(), List(), c) | ||
| 362 | case SkolemStrategy.ConstantRSA(c) => { | ||
| 363 | if (unsafe.contains(role)) | ||
| 364 | (List(RSA.PE(term, c), RSA.U(c)), List(), c) | ||
| 365 | else | ||
| 366 | (List(), List(), c) | ||
| 367 | } | ||
| 368 | case SkolemStrategy.Standard(f) => { | ||
| 369 | val y = RSAOntology.genFreshVariable() | ||
| 370 | ( | ||
| 371 | List(), | ||
| 372 | List(BindAtom.create(FunctionCall.create("SKOLEM", f, term), y)), | ||
| 373 | y | ||
| 374 | ) | ||
| 375 | } | ||
| 376 | } | ||
| 377 | val prop = convert(role, term, term1, suffix) | ||
| 378 | (prop :: head, body) | ||
| 379 | } | ||
| 380 | |||
| 381 | /** Maximum cardinality restriction class | ||
| 382 | * | ||
| 383 | * @note we only admit classes with cardinality set to 1. | ||
| 384 | * | ||
| 385 | * @throws `RuntimeException` when dealing with a restriction | ||
| 386 | * with cardinality != 1. | ||
| 387 | * | ||
| 388 | * @see [[http://www.w3.org/TR/owl2-syntax/#Maximum_Cardinality_2]] | ||
| 389 | */ | ||
| 390 | case e: OWLObjectMaxCardinality => { | ||
| 391 | if (e.getCardinality != 1) | ||
| 392 | throw new RuntimeException( | ||
| 393 | s"Class expression '$e' has cardinality restriction != 1." | ||
| 394 | ) | ||
| 395 | val vars @ (y :: z :: _) = | ||
| 396 | Seq(RSAOntology.genFreshVariable(), RSAOntology.genFreshVariable()) | ||
| 397 | val cls = e.getFiller | ||
| 398 | val role = e.getProperty | ||
| 399 | val (res, ext) = vars.map(convert(cls, _, unsafe, skolem, suffix)).unzip | ||
| 400 | val props = vars.map(convert(role, term, _, suffix)) | ||
| 401 | val eq = TupleTableAtom.rdf(y, IRI.SAME_AS, z) | ||
| 402 | (List(eq), res.flatten ++ props) | ||
| 403 | } | ||
| 404 | |||
| 405 | /** Catch-all case for all unhandled class expressions. */ | ||
| 406 | case e => | ||
| 407 | throw new RuntimeException( | ||
| 408 | s"Class expression '$e' is not supported (yet?)" | ||
| 409 | ) | ||
| 410 | } | ||
| 411 | |||
| 412 | /** Converts an object property expression into an atom. */ | ||
| 413 | def convert( | ||
| 414 | expr: OWLObjectPropertyExpression, | ||
| 415 | term1: Term, | ||
| 416 | term2: Term, | ||
| 417 | suffix: RSASuffix | ||
| 418 | ): TupleTableAtom = | ||
| 419 | expr match { | ||
| 420 | |||
| 421 | /** Simple named role/object property. | ||
| 422 | * | ||
| 423 | * @see [[http://www.w3.org/TR/owl2-syntax/#Object_Properties Object Properties]] | ||
| 424 | */ | ||
| 425 | case e: OWLObjectProperty => { | ||
| 426 | val role = IRI.create(e.getIRI.getIRIString :: suffix) | ||
| 427 | TupleTableAtom.rdf(term1, role, term2) | ||
| 428 | } | ||
| 429 | |||
| 430 | /** Inverse of a named role/property | ||
| 431 | * | ||
| 432 | * OWLAPI does not admit nesting of negation, and double | ||
| 433 | * negations are always simplified. | ||
| 434 | * | ||
| 435 | * @see [[https://www.w3.org/TR/owl2-syntax/#Inverse_Object_Properties Inverse Object Properties]] | ||
| 436 | */ | ||
| 437 | case e: OWLObjectInverseOf => | ||
| 438 | convert(e.getInverse, term1, term2, suffix + Inverse) | ||
| 439 | |||
| 440 | /** The infamous impossible case. | ||
| 441 | * | ||
| 442 | * @note all relevant cases are taken care of, and this branch | ||
| 443 | * throws a runtime exception to notify of the problem. | ||
| 444 | */ | ||
| 445 | case e => | ||
| 446 | throw new RuntimeException( | ||
| 447 | s"Unable to convert '$e' into a logic expression. This should be happening (TM)." | ||
| 448 | ) | ||
| 449 | } | ||
| 450 | |||
| 451 | /** Converts a data property expression into an atom. */ | ||
| 452 | def convert( | ||
| 453 | expr: OWLDataPropertyExpression, | ||
| 454 | term1: Term, | ||
| 455 | term2: Term, | ||
| 456 | suffix: RSASuffix | ||
| 457 | ): TupleTableAtom = | ||
| 458 | expr match { | ||
| 459 | |||
| 460 | /** Simple named role/data property | ||
| 461 | * | ||
| 462 | * @see [[https://www.w3.org/TR/owl2-syntax/#Datatypes Data Properties]] | ||
| 463 | */ | ||
| 464 | case e: OWLDataProperty => { | ||
| 465 | val role = IRI.create(e.getIRI.getIRIString :: suffix) | ||
| 466 | TupleTableAtom.rdf(term1, role, term2) | ||
| 467 | } | ||
| 468 | |||
| 469 | /** The infamous impossible case. | ||
| 470 | * | ||
| 471 | * @note all relevant cases are taken care of, and this branch | ||
| 472 | * throws a runtime exception to notify of the problem. | ||
| 473 | */ | ||
| 474 | case e => | ||
| 475 | throw new RuntimeException( | ||
| 476 | s"Unable to convert '$e' into a logic expression. This should be happening (TM)." | ||
| 477 | ) | ||
| 478 | } | ||
| 479 | |||
| 480 | } | ||
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 @@ | |||
| 1 | package rsacomb | ||
| 2 | |||
| 3 | import org.scalatest.LoneElement | ||
| 4 | import org.scalatest.flatspec.AnyFlatSpec | ||
| 5 | import org.scalatest.matchers.should.Matchers | ||
| 6 | import org.semanticweb.owlapi.apibinding.OWLManager | ||
| 7 | import org.semanticweb.owlapi.model.OWLOntologyManager | ||
| 8 | |||
| 9 | import tech.oxfordsemantic.jrdfox.logic.datalog.TupleTableAtom | ||
| 10 | import tech.oxfordsemantic.jrdfox.logic.expression.{Variable, IRI} | ||
| 11 | import uk.ac.ox.cs.rsacomb.converter.RDFoxConverter | ||
| 12 | import uk.ac.ox.cs.rsacomb.suffix.{Empty, Forward, Backward, Inverse} | ||
| 13 | import uk.ac.ox.cs.rsacomb.converter.SkolemStrategy | ||
| 14 | |||
| 15 | object RDFoxConverterSpec { | ||
| 16 | |||
| 17 | val manager = OWLManager.createOWLOntologyManager() | ||
| 18 | val factory = manager.getOWLDataFactory | ||
| 19 | |||
| 20 | val term0 = Variable.create("X") | ||
| 21 | val term1 = Variable.create("Y") | ||
| 22 | val iriString0 = "http://example.com/rsacomb/iri0" | ||
| 23 | val iriString1 = "http://example.com/rsacomb/iri1" | ||
| 24 | val iriString2 = "http://example.com/rsacomb/iri2" | ||
| 25 | val suffixes = Seq( | ||
| 26 | Empty, | ||
| 27 | Forward, | ||
| 28 | Backward, | ||
| 29 | Inverse, | ||
| 30 | Forward + Inverse, | ||
| 31 | Backward + Inverse | ||
| 32 | ) | ||
| 33 | } | ||
| 34 | |||
| 35 | class RDFoxConverterSpec extends AnyFlatSpec with Matchers with LoneElement { | ||
| 36 | |||
| 37 | import RDFoxConverterSpec._ | ||
| 38 | |||
| 39 | "A class name" should "be converted into a single atom" in { | ||
| 40 | val cls = factory.getOWLClass(iriString0) | ||
| 41 | val atom = TupleTableAtom.rdf(term0, IRI.RDF_TYPE, IRI.create(iriString0)) | ||
| 42 | val (res, ext) = | ||
| 43 | RDFoxConverter.convert(cls, term0, List(), SkolemStrategy.None, Empty) | ||
| 44 | res.loneElement shouldEqual atom | ||
| 45 | ext shouldBe empty | ||
| 46 | } | ||
| 47 | |||
| 48 | "A intersection of classes" should "be converted into the union of the conversion of the classes" in { | ||
| 49 | val cls0 = factory.getOWLClass(iriString0) | ||
| 50 | val cls1 = factory.getOWLClass(iriString1) | ||
| 51 | val cls2 = factory.getOWLClass(iriString2) | ||
| 52 | val conj = factory.getOWLObjectIntersectionOf(cls0, cls1, cls2) | ||
| 53 | val (res0, ext0) = | ||
| 54 | RDFoxConverter.convert(cls0, term0, List(), SkolemStrategy.None, Empty) | ||
| 55 | val (res1, ext1) = | ||
| 56 | RDFoxConverter.convert(cls1, term0, List(), SkolemStrategy.None, Empty) | ||
| 57 | val (res2, ext2) = | ||
| 58 | RDFoxConverter.convert(cls2, term0, List(), SkolemStrategy.None, Empty) | ||
| 59 | val (res, ext) = | ||
| 60 | RDFoxConverter.convert(conj, term0, List(), SkolemStrategy.None, Empty) | ||
| 61 | res should contain theSameElementsAs (res0 ::: res1 ::: res2) | ||
| 62 | ext should contain theSameElementsAs (ext0 ::: ext1 ::: ext2) | ||
| 63 | } | ||
| 64 | |||
| 65 | "A singleton intersection" should "correspond to the conversion of the internal class" in { | ||
| 66 | val cls0 = factory.getOWLClass(iriString0) | ||
| 67 | val conj = factory.getOWLObjectIntersectionOf(cls0) | ||
| 68 | val (res0, ext0) = | ||
| 69 | RDFoxConverter.convert(cls0, term0, List(), SkolemStrategy.None, Empty) | ||
| 70 | val (res, ext) = | ||
| 71 | RDFoxConverter.convert(conj, term0, List(), SkolemStrategy.None, Empty) | ||
| 72 | res should contain theSameElementsAs res0 | ||
| 73 | ext should contain theSameElementsAs ext0 | ||
| 74 | } | ||
| 75 | |||
| 76 | "An object property" should "be converted into an atom with matching predicate" in { | ||
| 77 | val prop = factory.getOWLObjectProperty(iriString0) | ||
| 78 | for (sx <- suffixes) { | ||
| 79 | val atom = | ||
| 80 | TupleTableAtom.rdf(term0, IRI.create(iriString0 :: sx), term1) | ||
| 81 | RDFoxConverter.convert(prop, term0, term1, sx) shouldEqual atom | ||
| 82 | } | ||
| 83 | } | ||
| 84 | |||
| 85 | "The inverse of an object property" should "be converted into an atom with matching negated predicate" in { | ||
| 86 | val prop = factory.getOWLObjectProperty(iriString0) | ||
| 87 | val inv = factory.getOWLObjectInverseOf(prop) | ||
| 88 | for (sx <- Seq(Empty, Forward, Backward)) { | ||
| 89 | val atom = | ||
| 90 | TupleTableAtom.rdf(term0, IRI.create(iriString0 :: sx + Inverse), term1) | ||
| 91 | RDFoxConverter.convert(inv, term0, term1, sx) shouldEqual atom | ||
| 92 | } | ||
| 93 | } | ||
| 94 | |||
| 95 | "A data property" should "be converted into an atom with matching predicate" in { | ||
| 96 | val prop = factory.getOWLDataProperty(iriString0) | ||
| 97 | for (suffix <- suffixes) { | ||
| 98 | val atom = | ||
| 99 | TupleTableAtom.rdf(term0, IRI.create(iriString0 :: suffix), term1) | ||
| 100 | RDFoxConverter.convert(prop, term0, term1, suffix) shouldEqual atom | ||
| 101 | } | ||
| 102 | } | ||
| 103 | |||
| 104 | } | ||
