diff options
Diffstat (limited to 'src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala')
-rw-r--r-- | src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala | 297 |
1 files changed, 199 insertions, 98 deletions
diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala index 8d5bf4c..0f1552a 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala | |||
@@ -46,6 +46,7 @@ import tech.oxfordsemantic.jrdfox.logic.expression.{ | |||
46 | import tech.oxfordsemantic.jrdfox.logic.sparql.statement.SelectQuery | 46 | import tech.oxfordsemantic.jrdfox.logic.sparql.statement.SelectQuery |
47 | 47 | ||
48 | /* Scala imports */ | 48 | /* Scala imports */ |
49 | import scala.util.{Try, Success, Failure} | ||
49 | import scala.collection.JavaConverters._ | 50 | import scala.collection.JavaConverters._ |
50 | import scala.collection.mutable.Set | 51 | import scala.collection.mutable.Set |
51 | import scalax.collection.immutable.Graph | 52 | import scalax.collection.immutable.Graph |
@@ -70,56 +71,75 @@ object RSAOntology { | |||
70 | /** Name of the RDFox data store used for CQ answering */ | 71 | /** Name of the RDFox data store used for CQ answering */ |
71 | private val DataStore = "answer_computation" | 72 | private val DataStore = "answer_computation" |
72 | 73 | ||
73 | def apply(ontology: OWLOntology): RSAOntology = new RSAOntology(ontology) | 74 | def apply(ontology: File, data: File*): RSAOntology = |
74 | 75 | new RSAOntology(ontology, data: _*) | |
75 | def apply(ontologies: File*): RSAOntology = | ||
76 | new RSAOntology(loadOntology(ontologies: _*)) | ||
77 | 76 | ||
78 | def genFreshVariable(): Variable = { | 77 | def genFreshVariable(): Variable = { |
79 | counter += 1 | 78 | counter += 1 |
80 | Variable.create(f"I$counter%03d") | 79 | Variable.create(f"I$counter%03d") |
81 | } | 80 | } |
82 | 81 | ||
83 | private def loadOntology(ontologies: File*): OWLOntology = { | ||
84 | val manager = OWLManager.createOWLOntologyManager() | ||
85 | ontologies.foreach { manager.loadOntologyFromOntologyDocument(_) } | ||
86 | val merger = new OWLOntologyMerger(manager) | ||
87 | merger.createMergedOntology(manager, OWLIRI.create("_:merged")) | ||
88 | } | ||
89 | } | 82 | } |
90 | 83 | ||
91 | class RSAOntology(val ontology: OWLOntology) { | 84 | class RSAOntology(_ontology: File, val datafiles: File*) { |
92 | 85 | ||
86 | /** Simplify conversion between OWLAPI and RDFox concepts */ | ||
87 | import implicits.RDFox._ | ||
93 | import uk.ac.ox.cs.rsacomb.implicits.RSAAxiom._ | 88 | import uk.ac.ox.cs.rsacomb.implicits.RSAAxiom._ |
94 | import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._ | 89 | import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._ |
95 | 90 | ||
96 | // Gather TBox/RBox/ABox from original ontology | 91 | /** Manager instance to interface with OWLAPI */ |
92 | private val manager = OWLManager.createOWLOntologyManager() | ||
93 | |||
94 | /** TBox + RBox of the input knowledge base. */ | ||
95 | val ontology: OWLOntology = | ||
96 | manager.loadOntologyFromOntologyDocument(_ontology) | ||
97 | |||
98 | /** OWLAPI internal reasoner some preliminary reasoning task. */ | ||
99 | private val reasoner = | ||
100 | (new StructuralReasonerFactory()).createReasoner(ontology) | ||
101 | |||
102 | /** Imported knowledge base. */ | ||
103 | //lazy val kbase: OWLOntology = { | ||
104 | // val merger = new OWLOntologyMerger(manager) | ||
105 | // _data.foreach { manager.loadOntologyFromOntologyDocument(_) } | ||
106 | // merger.createMergedOntology(manager, OWLIRI.create("_:merged")) | ||
107 | //} | ||
108 | |||
109 | /** TBox axioms */ | ||
97 | val tbox: List[OWLLogicalAxiom] = | 110 | val tbox: List[OWLLogicalAxiom] = |
98 | ontology | 111 | ontology |
99 | .tboxAxioms(Imports.INCLUDED) | 112 | .tboxAxioms(Imports.INCLUDED) |
100 | .collect(Collectors.toList()) | 113 | .collect(Collectors.toList()) |
101 | .collect { case a: OWLLogicalAxiom => a } | 114 | .collect { case a: OWLLogicalAxiom => a } |
115 | Logger.print(s"Original TBox: ${tbox.length}", Logger.DEBUG) | ||
102 | 116 | ||
117 | /** RBox axioms */ | ||
103 | val rbox: List[OWLLogicalAxiom] = | 118 | val rbox: List[OWLLogicalAxiom] = |
104 | ontology | 119 | ontology |
105 | .rboxAxioms(Imports.INCLUDED) | 120 | .rboxAxioms(Imports.INCLUDED) |
106 | .collect(Collectors.toList()) | 121 | .collect(Collectors.toList()) |
107 | .collect { case a: OWLLogicalAxiom => a } | 122 | .collect { case a: OWLLogicalAxiom => a } |
123 | Logger.print(s"Original RBox: ${rbox.length}", Logger.DEBUG) | ||
108 | 124 | ||
125 | /** ABox axioms | ||
126 | * | ||
127 | * @note this represents only the set of assertions contained in the | ||
128 | * ontology file. Data files specified in `datafiles` are directly | ||
129 | * imported in RDFox due to performance issues when trying to import | ||
130 | * large data files via OWLAPI. | ||
131 | */ | ||
109 | val abox: List[OWLLogicalAxiom] = | 132 | val abox: List[OWLLogicalAxiom] = |
110 | ontology | 133 | ontology |
111 | .aboxAxioms(Imports.INCLUDED) | 134 | .aboxAxioms(Imports.INCLUDED) |
112 | .collect(Collectors.toList()) | 135 | .collect(Collectors.toList()) |
113 | .collect { case a: OWLLogicalAxiom => a } | 136 | .collect { case a: OWLLogicalAxiom => a } |
137 | Logger.print(s"Original RBox: ${abox.length}", Logger.DEBUG) | ||
114 | 138 | ||
115 | val axioms: List[OWLLogicalAxiom] = abox ::: tbox ::: rbox | 139 | /** Collection of logical axioms in the input ontology */ |
116 | 140 | lazy val axioms: List[OWLLogicalAxiom] = abox ::: tbox ::: rbox | |
117 | Logger.print(s"Original TBox: ${tbox.length}", Logger.DEBUG) | ||
118 | Logger.print(s"Original RBox: ${rbox.length}", Logger.DEBUG) | ||
119 | Logger.print(s"Original ABox: ${abox.length}", Logger.DEBUG) | ||
120 | 141 | ||
121 | /* Retrieve individuals in the original ontology | 142 | /* Retrieve individuals in the original ontology */ |
122 | */ | ||
123 | val individuals: List[IRI] = | 143 | val individuals: List[IRI] = |
124 | ontology | 144 | ontology |
125 | .getIndividualsInSignature() | 145 | .getIndividualsInSignature() |
@@ -129,7 +149,7 @@ class RSAOntology(val ontology: OWLOntology) { | |||
129 | .toList | 149 | .toList |
130 | 150 | ||
131 | val literals: List[Literal] = | 151 | val literals: List[Literal] = |
132 | abox | 152 | axioms |
133 | .collect { case a: OWLDataPropertyAssertionAxiom => a } | 153 | .collect { case a: OWLDataPropertyAssertionAxiom => a } |
134 | .map(_.getObject) | 154 | .map(_.getObject) |
135 | .map(implicits.RDFox.owlapiToRdfoxLiteral) | 155 | .map(implicits.RDFox.owlapiToRdfoxLiteral) |
@@ -137,18 +157,13 @@ class RSAOntology(val ontology: OWLOntology) { | |||
137 | val concepts: List[OWLClass] = | 157 | val concepts: List[OWLClass] = |
138 | ontology.getClassesInSignature().asScala.toList | 158 | ontology.getClassesInSignature().asScala.toList |
139 | 159 | ||
160 | // This is needed in the computation of rules in the canonical model. | ||
161 | // Can we avoid this using RDFox built-in functions? | ||
140 | val roles: List[OWLObjectPropertyExpression] = | 162 | val roles: List[OWLObjectPropertyExpression] = |
141 | axioms | 163 | (tbox ++ rbox) |
142 | .flatMap(_.objectPropertyExpressionsInSignature) | 164 | .flatMap(_.objectPropertyExpressionsInSignature) |
143 | .distinct | 165 | .distinct |
144 | 166 | ||
145 | /** OWLAPI reasoner | ||
146 | * | ||
147 | * Used to carry out some preliminary reasoning task. | ||
148 | */ | ||
149 | private val reasoner = | ||
150 | (new StructuralReasonerFactory()).createReasoner(ontology) | ||
151 | |||
152 | /* Steps for RSA check | 167 | /* Steps for RSA check |
153 | * 1) convert ontology axioms into LP rules | 168 | * 1) convert ontology axioms into LP rules |
154 | * 2) call RDFox on the onto and compute materialization | 169 | * 2) call RDFox on the onto and compute materialization |
@@ -157,19 +172,16 @@ class RSAOntology(val ontology: OWLOntology) { | |||
157 | * ideally this annotates the graph with info about the reasons | 172 | * ideally this annotates the graph with info about the reasons |
158 | * why the ontology might not be RSA. This could help a second | 173 | * why the ontology might not be RSA. This could help a second |
159 | * step of approximation of an Horn-ALCHOIQ to RSA | 174 | * step of approximation of an Horn-ALCHOIQ to RSA |
175 | * | ||
176 | * TODO: Implement additional checks (taking into account equality) | ||
177 | * | ||
178 | * To check if the graph is tree-like we check for acyclicity in a | ||
179 | * undirected graph. | ||
160 | */ | 180 | */ |
161 | lazy val isRSA: Boolean = Logger.timed( | 181 | lazy val isRSA: Boolean = Logger.timed( |
162 | { | 182 | { |
163 | val unsafe = this.unsafeRoles | 183 | val unsafe = this.unsafeRoles |
164 | 184 | ||
165 | // val renderer = new DLSyntaxObjectRenderer() | ||
166 | // println() | ||
167 | // println("Unsafe roles:") | ||
168 | // println(unsafe) | ||
169 | // println() | ||
170 | // println("DL rules:") | ||
171 | // tbox.foreach(x => println(renderer.render(x))) | ||
172 | |||
173 | object RSAConverter extends RDFoxConverter { | 185 | object RSAConverter extends RDFoxConverter { |
174 | 186 | ||
175 | override def convert( | 187 | override def convert( |
@@ -195,66 +207,78 @@ class RSAOntology(val ontology: OWLOntology) { | |||
195 | 207 | ||
196 | case _ => super.convert(expr, term, unsafe, skolem, suffix) | 208 | case _ => super.convert(expr, term, unsafe, skolem, suffix) |
197 | } | 209 | } |
198 | |||
199 | } | 210 | } |
200 | 211 | ||
201 | /* Ontology convertion into LP rules */ | 212 | /* Ontology convertion into LP rules */ |
202 | val term = RSAOntology.genFreshVariable() | 213 | val term = RSAOntology.genFreshVariable() |
203 | val datalog = axioms | 214 | val conversion = Try( |
204 | .map(a => RSAConverter.convert(a, term, unsafe, new Constant(a), Empty)) | 215 | axioms.map(a => |
205 | .unzip | 216 | RSAConverter.convert(a, term, unsafe, new Constant(a), Empty) |
206 | val facts = datalog._1.flatten | 217 | ) |
207 | val rules = datalog._2.flatten | ||
208 | |||
209 | //println("Datalog rules:") | ||
210 | //rules foreach println | ||
211 | |||
212 | // Open connection with RDFox | ||
213 | val (server, data) = RDFoxUtil.openConnection("RSACheck") | ||
214 | |||
215 | /* Add built-in rules | ||
216 | * TODO: substitute with RDFoxUtil.addRules | ||
217 | */ | ||
218 | data.importData( | ||
219 | UpdateType.ADDITION, | ||
220 | RSA.Prefixes, | ||
221 | "rsa:E[?X,?Y] :- rsa:PE[?X,?Y], rsa:U[?X], rsa:U[?Y] ." | ||
222 | ) | 218 | ) |
223 | 219 | ||
224 | /* Add ontology facts and rules */ | 220 | conversion match { |
225 | RDFoxUtil.addFacts(data, facts) | 221 | case Success(result) => { |
226 | RDFoxUtil.addRules(data, rules) | 222 | val datalog = result.unzip |
227 | 223 | val facts = datalog._1.flatten | |
228 | /* Build graph */ | 224 | var rules = datalog._2.flatten |
229 | val graph = this.rsaGraph(data); | 225 | |
230 | //println("Graph:") | 226 | /* Open connection with RDFox */ |
231 | //println(graph) | 227 | val (server, data) = RDFoxUtil.openConnection("RSACheck") |
232 | 228 | ||
233 | // Close connection to RDFox | 229 | /* Add additional built-in rules */ |
234 | RDFoxUtil.closeConnection(server, data) | 230 | val varX = Variable.create("X") |
235 | 231 | val varY = Variable.create("Y") | |
236 | /* To check if the graph is tree-like we check for acyclicity in a | 232 | rules = Rule.create( |
237 | * undirected graph. | 233 | RSA.E(varX, varY), |
238 | * | 234 | RSA.PE(varX, varY), |
239 | * TODO: Implement additional checks (taking into account equality) | 235 | RSA.U(varX), |
240 | */ | 236 | RSA.U(varY) |
241 | graph.isAcyclic | 237 | ) :: rules |
238 | |||
239 | /* Load facts and rules from ontology */ | ||
240 | RDFoxUtil.addFacts(data, facts) | ||
241 | RDFoxUtil.addRules(data, rules) | ||
242 | /* Load data files */ | ||
243 | RDFoxUtil.addData(data, datafiles: _*) | ||
244 | |||
245 | /* Build graph */ | ||
246 | val graph = this.rsaGraph(data); | ||
247 | |||
248 | /* Close connection to RDFox */ | ||
249 | RDFoxUtil.closeConnection(server, data) | ||
250 | |||
251 | /* Acyclicity test */ | ||
252 | graph.isAcyclic | ||
253 | } | ||
254 | case Failure(e) => { | ||
255 | Logger print s"Unsupported axiom: $e" | ||
256 | false | ||
257 | } | ||
258 | } | ||
242 | }, | 259 | }, |
243 | "RSA check", | 260 | "RSA check", |
244 | Logger.DEBUG | 261 | Logger.DEBUG |
245 | ) | 262 | ) |
246 | 263 | ||
264 | /** Unsafe roles of a given ontology. | ||
265 | * | ||
266 | * Unsafety conditions are the following: | ||
267 | * | ||
268 | * 1) For all roles r1 appearing in an axiom of type T5, r1 is unsafe | ||
269 | * if there exists a role r2 (different from top) appearing in an | ||
270 | * axiom of type T3 and r1 is a subproperty of the inverse of r2. | ||
271 | * | ||
272 | * 2) For all roles p1 appearing in an axiom of type T5, p1 is unsafe | ||
273 | * if there exists a role p2 appearing in an axiom of type T4 and | ||
274 | * p1 is a subproperty of either p2 or the inverse of p2. | ||
275 | */ | ||
247 | lazy val unsafeRoles: List[OWLObjectPropertyExpression] = { | 276 | lazy val unsafeRoles: List[OWLObjectPropertyExpression] = { |
248 | 277 | ||
249 | /* DEBUG: print rules in DL syntax */ | 278 | /* DEBUG: print rules in DL syntax */ |
250 | //val renderer = new DLSyntaxObjectRenderer() | 279 | //val renderer = new DLSyntaxObjectRenderer() |
251 | 280 | ||
252 | /* Checking for (1) unsafety condition: | 281 | /* Checking for unsafety condition (1) */ |
253 | * | ||
254 | * For all roles r1 appearing in an axiom of type T5, r1 is unsafe | ||
255 | * if there exists a role r2 (different from top) appearing in an axiom | ||
256 | * of type T3 and r1 is a subproperty of the inverse of r2. | ||
257 | */ | ||
258 | val unsafe1 = for { | 282 | val unsafe1 = for { |
259 | axiom <- tbox | 283 | axiom <- tbox |
260 | if axiom.isT5 | 284 | if axiom.isT5 |
@@ -271,13 +295,7 @@ class RSAOntology(val ontology: OWLOntology) { | |||
271 | if roleSuperInv.contains(role2) | 295 | if roleSuperInv.contains(role2) |
272 | } yield role1 | 296 | } yield role1 |
273 | 297 | ||
274 | /* Checking for (2) unsafety condition: | 298 | /* Checking for unsafety condition (2) */ |
275 | * | ||
276 | * For all roles p1 appearing in an axiom of type T5, p1 is unsafe if | ||
277 | * there exists a role p2 appearing in an axiom of type T4 and p1 is a | ||
278 | * subproperty of either p2 or the inverse of p2. | ||
279 | * | ||
280 | */ | ||
281 | val unsafe2 = for { | 299 | val unsafe2 = for { |
282 | axiom <- tbox | 300 | axiom <- tbox |
283 | if axiom.isT5 | 301 | if axiom.isT5 |
@@ -307,12 +325,76 @@ class RSAOntology(val ontology: OWLOntology) { | |||
307 | Graph(edges: _*) | 325 | Graph(edges: _*) |
308 | } | 326 | } |
309 | 327 | ||
310 | def filteringProgram(query: ConjunctiveQuery): FilteringProgram = | 328 | /** Top axiomatization rules |
311 | Logger.timed( | 329 | * |
312 | new FilteringProgram(query, individuals ++ literals), | 330 | * For each concept/role *in the ontology file* introduce a rule to |
313 | "Generating filtering program", | 331 | * derive `owl:Thing`. |
314 | Logger.DEBUG | 332 | * |
333 | * @note this might not be enough in cases where data files contain | ||
334 | * concept/roles that are not in the ontology file. While this is | ||
335 | * non-standard, it is not forbidden either and may cause problems | ||
336 | * since not all individuals are considered part of `owl:Thing`. | ||
337 | * | ||
338 | * @note this is a naïve implementation of top axiomatization and | ||
339 | * might change in the future. The ideal solution would be for RDFox | ||
340 | * to take care of this, but at the time of writing this is not | ||
341 | * compatible with the way we are using the tool. | ||
342 | */ | ||
343 | private val topAxioms: List[Rule] = { | ||
344 | val varX = Variable.create("X") | ||
345 | val varY = Variable.create("Y") | ||
346 | concepts | ||
347 | .map(c => { | ||
348 | Rule.create( | ||
349 | RSA.Thing(varX), | ||
350 | TupleTableAtom.rdf(varX, IRI.RDF_TYPE, c.getIRI) | ||
351 | ) | ||
352 | }) ++ roles.map(r => { | ||
353 | val name = r match { | ||
354 | case x: OWLObjectProperty => x.getIRI.getIRIString | ||
355 | case x: OWLObjectInverseOf => | ||
356 | x.getInverse.getNamedProperty.getIRI.getIRIString :: Inverse | ||
357 | } | ||
358 | Rule.create( | ||
359 | List(RSA.Thing(varX), RSA.Thing(varY)), | ||
360 | List(TupleTableAtom.rdf(varX, name, varY)) | ||
361 | ) | ||
362 | }) | ||
363 | } | ||
364 | |||
365 | /** Equality axiomatization rules | ||
366 | * | ||
367 | * Introduce reflexivity, simmetry and transitivity rules for a naïve | ||
368 | * equality axiomatization. | ||
369 | * | ||
370 | * @note that we are using a custom `congruent` predicate to indicate | ||
371 | * equality. This is to avoid interfering with the standard | ||
372 | * `owl:sameAs`. | ||
373 | * | ||
374 | * @note RDFox is able to handle equality in a "smart" way, but this | ||
375 | * behaviour is incompatible with other needed features like | ||
376 | * negation-as-failure and aggregates. | ||
377 | * | ||
378 | * @todo to complete the equality axiomatization we need to introduce | ||
379 | * substitution rules to explicate a complete "equality" semantics. | ||
380 | */ | ||
381 | private val equalityAxioms: List[Rule] = { | ||
382 | val varX = Variable.create("X") | ||
383 | val varY = Variable.create("Y") | ||
384 | val varZ = Variable.create("Z") | ||
385 | List( | ||
386 | // Reflexivity | ||
387 | Rule.create(RSA.Congruent(varX, varX), RSA.Thing(varX)), | ||
388 | // Simmetry | ||
389 | Rule.create(RSA.Congruent(varY, varX), RSA.Congruent(varX, varY)), | ||
390 | // Transitivity | ||
391 | Rule.create( | ||
392 | RSA.Congruent(varX, varZ), | ||
393 | RSA.Congruent(varX, varY), | ||
394 | RSA.Congruent(varY, varZ) | ||
395 | ) | ||
315 | ) | 396 | ) |
397 | } | ||
316 | 398 | ||
317 | lazy val canonicalModel = Logger.timed( | 399 | lazy val canonicalModel = Logger.timed( |
318 | new CanonicalModel(this), | 400 | new CanonicalModel(this), |
@@ -320,6 +402,13 @@ class RSAOntology(val ontology: OWLOntology) { | |||
320 | Logger.DEBUG | 402 | Logger.DEBUG |
321 | ) | 403 | ) |
322 | 404 | ||
405 | def filteringProgram(query: ConjunctiveQuery): FilteringProgram = | ||
406 | Logger.timed( | ||
407 | new FilteringProgram(query), | ||
408 | "Generating filtering program", | ||
409 | Logger.DEBUG | ||
410 | ) | ||
411 | |||
323 | // TODO: the following functions needs testing | 412 | // TODO: the following functions needs testing |
324 | def confl( | 413 | def confl( |
325 | role: OWLObjectPropertyExpression | 414 | role: OWLObjectPropertyExpression |
@@ -356,18 +445,30 @@ class RSAOntology(val ontology: OWLOntology) { | |||
356 | val canon = this.canonicalModel | 445 | val canon = this.canonicalModel |
357 | val filter = this.filteringProgram(query) | 446 | val filter = this.filteringProgram(query) |
358 | 447 | ||
359 | //data.beginTransaction(TransactionType.READ_WRITE) | 448 | /* Upload data from data file */ |
449 | RDFoxUtil.addData(data, datafiles: _*) | ||
450 | |||
451 | /* Top / equality axiomatization */ | ||
452 | RDFoxUtil.addRules(data, topAxioms ++ equalityAxioms) | ||
453 | |||
454 | /* Generate `named` predicates */ | ||
455 | RDFoxUtil.addFacts(data, (individuals ++ literals) map RSA.Named) | ||
456 | data.evaluateUpdate( | ||
457 | RSA.Prefixes, | ||
458 | "INSERT { ?X a rsa:Named } WHERE { ?X a owl:Thing }", | ||
459 | new java.util.HashMap[String, String] | ||
460 | ) | ||
360 | 461 | ||
361 | Logger print s"Canonical model rules: ${canon.rules.length}" | 462 | Logger print s"Canonical model rules: ${canon.rules.length}" |
362 | RDFoxUtil.addRules(data, this.canonicalModel.rules) | 463 | RDFoxUtil.addRules(data, canon.rules) |
363 | 464 | ||
364 | Logger print s"Canonical model facts: ${canon.facts.length}" | 465 | Logger print s"Canonical model facts: ${canon.facts.length}" |
365 | RDFoxUtil.addFacts(data, this.canonicalModel.facts) | 466 | RDFoxUtil.addFacts(data, canon.facts) |
366 | 467 | ||
367 | RDFoxUtil printStatisticsFor data | 468 | //canon.facts.foreach(println) |
469 | //canon.rules.foreach(println) | ||
368 | 470 | ||
369 | Logger print s"Filtering program facts: ${filter.facts.length}" | 471 | RDFoxUtil printStatisticsFor data |
370 | RDFoxUtil.addFacts(data, filter.facts) | ||
371 | 472 | ||
372 | Logger print s"Filtering program rules: ${filter.rules.length}" | 473 | Logger print s"Filtering program rules: ${filter.rules.length}" |
373 | RDFoxUtil.addRules(data, filter.rules) | 474 | RDFoxUtil.addRules(data, filter.rules) |