diff options
-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 | } | ||