Coverage for lode / reader / logic / owl_logic.py: 73%
658 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-10-05 16:02 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-10-05 16:02 +0000
1# owl_logic.py
3from rdflib import Graph, URIRef, Node, Literal as RDFlibLiteral, BNode
4from rdflib.namespace import RDF, RDFS, OWL, SKOS, XSD
5from rdflib.collection import Collection as RDFLibCollection
7from lode.models import *
8from lode.reader.logic.base_logic import BaseLogic
9from lode.reader.warnings import owl_warnings
12class OwlLogic(BaseLogic):
13 """
14 OWL-specific parsing logic.
16 Extends BaseLogic with:
17 - Phase 1 BNode classification for OWL restrictions and truth functions
18 - OWL defaults for domain, range (owl:Thing) and Individual type
19 - Handlers for property characteristics, cardinality, truth functions,
20 quantifiers, and group axioms (AllDisjointClasses, AllDifferent, etc.)
21 - Property type inference for properties not explicitly typed in the artefact
22 """
24 # _get_allowed_namespaces: ereditato da BaseLogic, legge config YAML
26 # ========== HOOK per resolve custom OWL ==========
28 def _pre_resolve_hook(self, python_class: type, id: Node) -> type | None:
29 """
30 OWL override: if the URI is already cached with a non-Individual type,
31 return that type to prevent silent downcasting during MRO resolution.
32 Returns None to fall through to the default MRO walk.
33 """
34 if id and id in self._instance_cache:
35 for existing in self._instance_cache[id]:
36 if type(existing) is not Individual:
37 return type(existing)
38 return None
40 # ========== READER PHASES ==========
42 def phase1_classify_from_predicates(self):
43 """Scans the RDF graph for subjects of mapped predicates.
44 - BNodes with inferred_class predicates -> classified and registered (mainly Restrictions)
45 - URIRefs with any mapped predicate but no rdf:type -> registered with
46 the inferred type from classify_by_predicate (only if unambiguous)
47 """
49 classified = {}
51 for pred in self._property_mapping:
52 for uri in self.graph.subjects(pred, None):
53 if uri in self._instance_cache:
54 continue
55 python_class = self._strategy.classify_by_predicate(uri, self.graph)
56 if not python_class:
57 continue
58 if isinstance(uri, BNode):
59 # [ owl:inverseOf <prop> ] used as object of owl:onProperty is an
60 # anonymous inline property expression inside a Restriction, not a
61 # standalone entity — skipped it to avoid spurious cache entries
62 if (None, OWL.onProperty, uri) in self.graph:
63 continue
64 if uri not in classified:
65 classified[uri] = python_class
66 elif isinstance(uri, URIRef):
67 if not issubclass(python_class, Restriction):
68 self.get_or_create(uri, python_class, populate=False)
70 owl_warnings.flag_malformed_restrictions(self, classified)
72 self._classify_nested(classified, self._strategy.get_classifier_predicates())
74 for uri, py_class in classified.items():
75 self.get_or_create(uri, py_class, populate=False)
77 def phase2_create_from_types(self):
78 type_mapping = self._strategy.get_type_mapping()
80 for rdf_type, config in type_mapping.items():
81 py_class = config.get('target_class')
82 if not py_class:
83 continue
84 for uri in self.graph.subjects(RDF.type, rdf_type):
85 inst = self.get_or_create(uri, py_class, populate=False)
86 if inst is not None:
87 self._track(inst, (uri, RDF.type, rdf_type))
88 # Apply static setters (e.g. set_is_transitive: True) always,
89 # even if instance already existed in cache from phase1.
90 if 'setters' in config:
91 for instance in self._instance_cache.get(uri, set()):
92 self._apply_setters_immediate(instance, config['setters'])
94 # Subjects with rdf:type not covered by config -> Individual
95 for s, o in self.graph.subject_objects(RDF.type):
96 if isinstance(s, BNode) or s in self._instance_cache:
97 continue
98 if o not in type_mapping:
99 if isinstance(o, URIRef):
100 o_str = str(o)
101 if any(o_str.startswith(ns) for ns in self._allowed_namespaces) and o not in (OWL.Thing, OWL.Nothing, RDFS.Literal):
102 continue
103 self.get_or_create(s, Individual, populate=True)
105 # Subjects with an rdf:type not mapped in config -> Individual
106 for s, o in self.graph.subject_objects(RDF.type):
107 if o not in type_mapping and not isinstance(s, BNode) and s not in self._instance_cache:
108 self.get_or_create(s, Individual, populate=True)
110 def phase3_populate_properties(self):
111 """Iterates over all cached instances and populates their properties
112 by dispatching each predicate-object pair through the configured
113 setters and handlers defined in the property mapping.
114 """
116 for uri in list(self._instance_cache.keys()):
117 for instance in list(self._instance_cache[uri]):
118 self.populate_instance(instance, uri)
120 def phase4_process_group_axioms(self):
121 """Processes group axioms defined in the enricher section of the config.
122 For each axiom type, retrieves all matching subjects from the graph
123 and dispatches them to the handler specified in the config entry.
124 """
126 axioms = self._strategy.get_group_axioms()
127 for axiom_type, handler_name in axioms.items():
128 for uri in self.graph.subjects(RDF.type, axiom_type):
129 # handler existence guaranteed by _validate_handlers (defined in base_logic)
130 getattr(self, handler_name)(uri)
132 def phase5_fallback(self):
133 """
134 Classifies or reclassifies any entity whose concrete type could not be
135 determined in earlier phases, in particular generic Property instances
136 whose subtype (Relation, Attribute, Annotation) is not explicitly
137 asserted in the semantic artefact via rdf:type (handled by phase 2). It uses _infer_property_type to resolve the concrete subtype.
139 After reclassification, applies OWL defaults (e.g., domain, range, thing) to all instances via _enrich_or_apply_owl_defaults.
140 """
142 for uri, instances in list(self._instance_cache.items()):
143 for instance in list(instances):
145 if type(instance) is Property:
146 # Check if a more specific Property subclass already exists for this URI
148 has_concrete = any(
149 type(i) in (Relation, Attribute, Annotation)
150 for i in instances if i is not instance
151 )
153 if has_concrete:
154 # Remove generic Property, keep the concrete one
155 self._instance_cache[uri].discard(instance)
156 else:
157 inferred = self._infer_property_type(instance)
158 old_dict = instance.__dict__.copy()
159 instance.__class__ = inferred
160 inferred.__init__(instance)
161 instance.__dict__.update(old_dict)
162 self.populate_instance(instance, uri)
164 self._enrich_or_apply_owl_defaults(instance, uri)
166 def handle_domain(self, instance, uri, predicate, obj, setter=None):
167 concept = self.get_or_create(obj, Concept)
168 if concept is None:
169 concept = self._force_as_concept(obj)
170 instance.set_has_domain(concept)
171 concept.set_is_in_domain_of(instance)
174 def handle_range(self, instance, uri, predicate, obj, setter=None):
175 if isinstance(instance, Attribute):
176 resource = self.get_or_create(obj, Datatype)
177 else:
178 resource = self.get_or_create(obj, Concept)
179 if resource is None:
180 resource = self._force_as_concept(obj)
181 instance.set_has_range(resource)
182 resource.set_is_in_range_of(instance)
184 def handle_equivalent_class(self, instance, uri, predicate, obj, setter=None):
185 """§9.1.2 EquivalentClasses — simmetria garantita."""
187 # add WARNING
188 if not isinstance(instance, Concept):
189 return
191 other = self.get_or_create(obj, Concept)
192 # add WARNING
193 if not other or not isinstance(other, Concept):
194 return
196 if other not in instance.get_is_equivalent_to():
197 instance.set_is_equivalent_to(other)
198 if instance not in other.get_is_equivalent_to():
199 other.set_is_equivalent_to(instance)
201 def handle_disjoint_with(self, instance, uri, predicate, obj, setter=None):
202 """§9.1.3 DisjointClasses — simmetria garantita."""
203 other = self.get_or_create(obj, Concept)
204 if other:
205 if other not in instance.get_is_disjoint_with():
206 instance.set_is_disjoint_with(other)
207 if instance not in other.get_is_disjoint_with():
208 other.set_is_disjoint_with(instance)
210 def handle_equivalent_property(self, instance, uri, predicate, obj, setter=None):
211 if obj in self._instance_cache:
212 other = self.get_or_create(obj, Property)
213 else:
214 other = self.get_or_create(obj, type(instance))
215 if other:
216 if other not in instance.get_is_equivalent_to():
217 instance.set_is_equivalent_to(other)
218 if instance not in other.get_is_equivalent_to():
219 other.set_is_equivalent_to(instance)
221 def handle_property_disjoint_with(self, instance, uri, predicate, obj, setter=None):
222 """§9.2.3 DisjointObjectProperties — simmetria garantita."""
223 other = self.get_or_create(obj, Property)
224 if other:
225 if other not in instance.get_is_disjoint_with():
226 instance.set_is_disjoint_with(other)
227 if instance not in other.get_is_disjoint_with():
228 other.set_is_disjoint_with(instance)
230 def _ensure_individual(self, node):
231 """Get or create an Individual for node, preserving existing types (punning)."""
232 if isinstance(node, RDFlibLiteral):
233 return None
234 cached = self._instance_cache.get(node, set())
235 ind = next((i for i in cached if isinstance(i, Individual)), None)
236 if ind:
237 return ind
238 ind = Individual()
239 ind.set_has_identifier(str(node))
240 self._instance_cache.setdefault(node, set()).add(ind)
241 return ind
243 def handle_same_as(self, instance, uri, predicate, obj, setter=None):
244 """§9.6.1 SameIndividual — simmetria garantita."""
245 subj_ind = self._ensure_individual(uri)
246 other = self._ensure_individual(obj)
247 if subj_ind and other:
248 if other not in subj_ind.get_is_same_as():
249 subj_ind.set_is_same_as(other)
250 if subj_ind not in other.get_is_same_as():
251 other.set_is_same_as(subj_ind)
253 def handle_different_from(self, instance, uri, predicate, obj, setter=None):
254 """§9.6.2 DifferentIndividuals — simmetria garantita."""
255 subj_ind = self._ensure_individual(uri)
256 other = self._ensure_individual(obj)
257 if subj_ind and other:
258 if other not in subj_ind.get_is_different_from():
259 subj_ind.set_is_different_from(other)
260 if subj_ind not in other.get_is_different_from():
261 other.set_is_different_from(subj_ind)
263 def handle_inverse_of(self, instance, uri, predicate, obj, setter=None):
264 if isinstance(instance, Restriction) and not isinstance(instance, Relation):
265 prop = self.get_or_create(obj, Property)
266 if prop:
267 instance.set_applies_on_property(prop)
268 return
269 other = self.get_or_create(obj, Relation)
270 if other:
271 if instance.get_is_inverse_of() is not other:
272 instance.set_is_inverse_of(other)
273 if other.get_is_inverse_of() is not instance:
274 other.set_is_inverse_of(instance)
276 def handle_on_property(self, instance, uri, predicate, obj, setter=None):
277 """If obj is a BNode with owl:inverseOf, resolve the real property and mark as inverse.
278 Undeclared properties default to Relation (ObjectProperty) per OWL DL.
279 """
280 if isinstance(obj, BNode) and (obj, OWL.inverseOf, None) in self.graph:
281 inverse_of = self.graph.value(obj, OWL.inverseOf)
282 if inverse_of:
283 prop = self.get_or_create(inverse_of, Property)
284 if prop and type(prop) is Property:
285 prop = self.get_or_create(inverse_of, Relation)
286 if prop and isinstance(instance, PropertyConceptRestriction):
287 instance.set_applies_on_property(prop)
288 instance.set_is_inverse(True)
289 return
290 prop = self.get_or_create(obj, Property)
291 if prop and type(prop) is Property:
292 prop = self.get_or_create(obj, Relation)
293 if prop:
294 instance.set_applies_on_property(prop)
296 def _force_as_concept(self, uri):
297 """Aggiunge Concept in cache per URI già classificato come Individual/Resource (punning OWL)."""
298 cached = list(self._instance_cache.get(uri, set()))
299 concept = Concept()
300 if cached:
301 concept.__dict__.update(cached[0].__dict__)
302 else:
303 concept.set_has_identifier(str(uri))
304 self._instance_cache.setdefault(uri, set()).add(concept)
305 return concept
307 def _enrich_or_apply_owl_defaults(self, instance, uri):
308 """
309 Applies OWL-mandated defaults to instances lacking explicit declarations.
310 [...docstring invariata...]
311 """
313 if uri not in self._instance_cache:
314 return
316 owl_thing = self.get_or_create(OWL.Thing, Concept)
317 rdfs_string = self.get_or_create(RDFS.Literal, Datatype)
319 def _safe_inherited(instance, getter_name):
320 values = self._get_inherited_property_values(instance, getter_name)
321 if isinstance(instance, Attribute):
322 if getter_name == "get_has_range":
323 return [v for v in values if isinstance(v, (Datatype, DatatypeRestriction))]
324 if isinstance(instance, Relation):
325 if getter_name == "get_has_range":
326 return [v for v in values if isinstance(v, Concept)]
327 return values
329 def _is_only_default(values, default_id):
330 return values and all(d.get_has_identifier() == default_id for d in values)
332 # Type-specific defaults: always apply, even for punning subordinates
333 # (these are constitutive of the class, not generic Resource enrichments)
335 if isinstance(instance, Relation):
336 current_domains = instance.get_has_domain()
337 if not current_domains or _is_only_default(current_domains, owl_thing):
338 inherited_domain = _safe_inherited(instance, "get_has_domain")
339 if inherited_domain:
340 instance.has_domain.clear()
341 for domain in inherited_domain:
342 instance.set_has_domain(domain)
343 elif not current_domains:
344 instance.set_has_domain(owl_thing)
346 current_ranges = instance.get_has_range()
347 if not current_ranges or _is_only_default(current_ranges, owl_thing):
348 inherited_range = _safe_inherited(instance, "get_has_range")
349 if inherited_range:
350 instance.has_range.clear()
351 for r in inherited_range:
352 instance.set_has_range(r)
353 elif not current_ranges:
354 instance.set_has_range(owl_thing)
356 if isinstance(instance, Attribute):
357 current_domains = instance.get_has_domain()
358 if not current_domains or _is_only_default(current_domains, owl_thing):
359 inherited_domain = _safe_inherited(instance, "get_has_domain")
360 if inherited_domain:
361 instance.has_domain.clear()
362 for domain in inherited_domain:
363 instance.set_has_domain(domain)
364 elif not current_domains:
365 instance.set_has_domain(owl_thing)
367 current_ranges = instance.get_has_range()
368 if not current_ranges or _is_only_default(current_ranges, rdfs_string):
369 inherited_range = _safe_inherited(instance, "get_has_range")
370 if inherited_range:
371 instance.has_range.clear()
372 for r in inherited_range:
373 instance.set_has_range(r)
374 elif not current_ranges:
375 instance.set_has_range(rdfs_string)
377 if isinstance(instance, Individual):
378 if not instance.get_has_type():
379 instance.set_has_type(owl_thing)
381 # Generic Resource-level enrichments: skip for punning subordinates
382 if self._is_punning_subordinate(instance, uri):
383 return
385 if isinstance(instance, Resource):
386 if len(self._instance_cache[uri]) > 1:
387 existing = instance.get_also_defined_as() or []
388 for other in self._instance_cache[uri]:
389 if other is instance:
390 continue
391 if other not in existing:
392 instance.set_also_defined_as(other)
393 if instance not in (other.get_also_defined_as() or []):
394 other.set_also_defined_as(instance)
397 def _get_inherited_property_values(self, property_instance, getter_name: str) -> list:
398 def collect(node):
399 if node is property_instance:
400 return None
401 getter = getattr(node, getter_name, None)
402 if getter:
403 values = getter()
404 if values:
405 return values if isinstance(values, list) else [values]
406 if isinstance(node, Relation):
407 swapped_getter = {"get_has_domain": "get_has_range", "get_has_range": "get_has_domain"}.get(getter_name)
408 if swapped_getter:
409 for inverse in (node.get_is_inverse_of() or []):
410 swapped = getattr(inverse, swapped_getter, lambda: None)()
411 if swapped:
412 return swapped if isinstance(swapped, list) else [swapped]
413 inverse_uri = URIRef(inverse.get_has_identifier())
414 graph_pred = RDFS.range if swapped_getter == "get_has_range" else RDFS.domain
415 for obj in self.graph.objects(inverse_uri, graph_pred):
416 if isinstance(obj, BNode):
417 continue
418 inst = self.get_or_create(obj, Concept)
419 if inst:
420 return [inst]
421 return None
423 if isinstance(property_instance, Relation):
424 swapped_getter = {"get_has_domain": "get_has_range", "get_has_range": "get_has_domain"}.get(getter_name)
425 if swapped_getter:
426 owl_thing_id = str(OWL.Thing)
427 for inverse in (property_instance.get_is_inverse_of() or []):
428 swapped = getattr(inverse, swapped_getter, lambda: None)()
429 if swapped:
430 filtered = [v for v in (swapped if isinstance(swapped, list) else [swapped])
431 if v.get_has_identifier() != owl_thing_id]
432 if filtered:
433 return filtered
434 inverse_uri = URIRef(inverse.get_has_identifier())
435 graph_pred = RDFS.range if swapped_getter == "get_has_range" else RDFS.domain
436 for obj in self.graph.objects(inverse_uri, graph_pred):
437 if isinstance(obj, BNode):
438 continue
439 inst = self.get_or_create(obj, Concept)
440 if inst:
441 return [inst]
443 result = self._traverse_hierarchy(
444 property_instance,
445 next_getter="get_is_sub_property_of",
446 direction="up",
447 collect=collect,
448 )
449 return result if result is not None else []
452 def _infer_property_type(self, instance) -> type:
453 """
454 Infers the concrete subtype (Relation, Attribute, Annotation) for a
455 generic Property instance.
457 Strategy:
458 1. owl:inverseOf in graph — if present, this is a Relation.
459 2. Traverse UP subPropertyOf — inherit type from ancestor.
460 3. Traverse DOWN subPropertyOf — inherit type from descendant.
461 4. Declared rdfs:range — XSD/Literal range -> Attribute, URIRef range -> Relation.
462 5. Graph usage — all literal objects -> Attribute, any URIRef object -> Relation.
463 6. Fallback to Annotation.
464 """
465 uri_ref = URIRef(instance.get_has_identifier()) if instance.get_has_identifier() else None
467 # (1) inverseOf in graph (instance not yet populated as Property)
468 if uri_ref:
469 if any(self.graph.objects(uri_ref, OWL.inverseOf)) or \
470 any(self.graph.subjects(OWL.inverseOf, uri_ref)):
471 return Relation
473 CONCRETE = (Relation, Attribute, Annotation)
475 # (2) UP THE HIERARCHY
476 def collect_type(node):
477 if type(node) in CONCRETE:
478 return type(node)
479 return None
481 result = self._traverse_hierarchy(
482 instance,
483 next_getter="get_is_sub_property_of",
484 direction="up",
485 collect=collect_type,
486 )
487 if result:
488 return result
490 # (3) DOWN THE HIERARCHY
491 result = self._traverse_hierarchy(
492 instance,
493 next_getter="get_is_sub_property_of",
494 direction="down",
495 collect=collect_type,
496 )
497 if result:
498 return result
500 # (4) Declared rdfs:range
501 if uri_ref:
502 for range_obj in self.graph.objects(uri_ref, RDFS.range):
503 range_str = str(range_obj)
504 if range_str.startswith(str(XSD)) or range_obj in (RDFS.Literal,):
505 return Attribute
506 if isinstance(range_obj, URIRef):
507 return Relation
509 # (5) Graph usage: how the property is used as predicate
510 if uri_ref:
511 has_literal = False
512 has_uriref = False
513 for s, p, o in self.graph.triples((None, uri_ref, None)):
514 if isinstance(o, RDFlibLiteral):
515 has_literal = True
516 elif isinstance(o, URIRef):
517 has_uriref = True
518 if has_uriref:
519 return Relation
520 if has_literal:
521 return Attribute
523 # (6) fallback
524 return Annotation
526 # ========== HELPERS & RELATED FUNCTIONS PHASE 1 ==========
528 def _classify_nested(self, classified, predicates):
529 """
530 Recursively classifies BNodes found inside OWL list collections
531 (owl:intersectionOf, owl:unionOf, owl:oneOf) hanging off already-classified
532 BNodes. Extends the classified dict in place with any newly discovered
533 BNode-to-class mappings.
534 """
536 list_preds = [OWL.intersectionOf, OWL.unionOf, OWL.oneOf, OWL.onDataRange]
538 for bnode in list(classified.keys()):
539 for pred, obj in self.graph.predicate_objects(bnode):
540 if pred in list_preds:
541 try:
542 collection = RDFLibCollection(self.graph, obj)
543 for item in collection:
544 if isinstance(item, BNode) and item not in classified:
545 py_class = self._strategy.classify_by_predicate(item, self.graph)
546 if py_class:
547 classified[item] = py_class
548 except:
549 pass
551 # ========== HELPERS & RELATED FUNCTIONS PHASE 2 ==========
553 def _apply_setters_immediate(self, instance, setters_config):
554 """Applies static setter values from the config directly to an instance,
555 without resolving any RDF object. Used in phase2 to apply constant values
556 (e.g. set_is_symmetric: True) defined alongside is: class entries.
557 """
558 for setter_item in setters_config:
559 if isinstance(setter_item, dict):
560 for setter_name, value in setter_item.items():
561 if hasattr(instance, setter_name):
562 getattr(instance, setter_name)(value)
563 else:
564 if hasattr(instance, setter_item):
565 getattr(instance, setter_item)()
567 # ========== HANDLERS PHASE 4 (GROUP AXIOMS) ==========
569 def process_all_disjoint_classes(self, uri: Node):
570 """
571 Handles owl:AllDisjointClasses axioms by resolving the owl:members
572 collection and marking every pair of member Concepts as mutually
573 disjoint via set_is_disjoint_with.
574 """
575 members_list = self.graph.value(uri, OWL.members)
576 if not members_list:
577 return
578 try:
579 members = list(RDFLibCollection(self.graph, members_list))
580 for i, class_a_uri in enumerate(members):
581 class_a = self.get_or_create(class_a_uri, Concept)
582 for class_b_uri in members[i + 1:]:
583 class_b = self.get_or_create(class_b_uri, Concept)
584 class_a.set_is_disjoint_with(class_b)
585 class_b.set_is_disjoint_with(class_a)
586 except Exception as e:
587 print(f"Errore AllDisjointClasses: {e}")
589 def process_all_different(self, uri: Node):
590 """
591 Handles owl:AllDifferent axioms by resolving the owl:distinctMembers
592 collection and marking every pair of member Individuals as mutually
593 different via set_is_different_from.
594 """
595 members_list = self.graph.value(uri, OWL.distinctMembers)
596 if not members_list:
597 return
598 try:
599 members = list(RDFLibCollection(self.graph, members_list))
600 for i, ind_a_uri in enumerate(members):
601 ind_a = self.get_or_create(ind_a_uri, Individual)
602 for ind_b_uri in members[i + 1:]:
603 ind_b = self.get_or_create(ind_b_uri, Individual)
604 ind_a.set_is_different_from(ind_b)
605 ind_b.set_is_different_from(ind_a)
606 except Exception as e:
607 print(f"Errore AllDifferent: {e}")
609 def process_all_disjoint_properties(self, uri: Node):
610 """
611 Handles owl:AllDisjointProperties axioms by resolving the owl:members
612 collection and marking every pair of member Properties as mutually
613 disjoint via set_is_disjoint_with.
614 """
615 members_list = self.graph.value(uri, OWL.members)
616 if not members_list:
617 return
618 try:
619 members = list(RDFLibCollection(self.graph, members_list))
620 for i, prop_a_uri in enumerate(members):
621 prop_a = self.get_or_create(prop_a_uri, Property)
622 for prop_b_uri in members[i + 1:]:
623 prop_b = self.get_or_create(prop_b_uri, Property)
624 prop_a.set_is_disjoint_with(prop_b)
625 prop_b.set_is_disjoint_with(prop_a)
626 except Exception as e:
627 print(f"Errore AllDisjointProperties: {e}")
629 # ========== HANDLER CALLED BY PHASE 3 FOR POPULATING INSTANCES ==========
631 def handle_property_chain(self, instance, uri, predicate, obj, setter=None):
632 """Handler per owl:propertyChainAxiom"""
634 if not self._is_rdf_collection(obj):
635 raise ValueError(f"owl:propertyChainAxiom object is not an RDF list: {obj}")
636 collection = RDFLibCollection(self.graph, obj)
637 chain_instances = []
638 for chain_uri in collection:
639 chain_instance = self.get_or_create(chain_uri, Relation)
640 chain_instances.append(chain_instance)
641 instance.set_has_property_chain(chain_instances)
643 def handle_has_key(self, instance, uri, predicate, obj, setter=None):
644 try:
645 for prop_uri in RDFLibCollection(self.graph, obj):
646 prop = self.get_or_create(prop_uri, Property)
647 if prop:
648 instance.set_has_key(prop)
649 except Exception as e:
650 print(f"Errore handle_has_key: {e}")
652 # ========== RESTRICTIONS ================================================
654 def handle_disjoint_union(self, instance, uri, predicate, obj, setter=None):
655 """§9.1.4 DisjointUnion(C CE1...CEn) — equivalent to:
656 EquivalentClasses(C ObjectUnionOf(CE1...CEn)) + DisjointClasses(CE1...CEn).
657 """
658 if not isinstance(instance, Concept):
659 return
660 try:
661 collection = RDFLibCollection(self.graph, obj)
662 members = list(collection)
664 # 1. C equivalentClass ObjectUnionOf(CE1...CEn)
665 tf = self.get_or_create(obj, TruthFunction)
666 if tf:
667 tf.set_has_logical_operator("or")
668 for member_uri in members:
669 concept = self.get_or_create(member_uri, Concept)
670 if concept:
671 tf.set_applies_on_concept(concept)
672 instance.set_is_equivalent_to(tf)
674 # 2. DisjointClasses(CE1...CEn) — pairwise
675 for i, uri_a in enumerate(members):
676 concept_a = self.get_or_create(uri_a, Concept)
677 for uri_b in members[i+1:]:
678 concept_b = self.get_or_create(uri_b, Concept)
679 if concept_a and concept_b:
680 concept_a.set_is_disjoint_with(concept_b)
681 concept_b.set_is_disjoint_with(concept_a)
683 except Exception as e:
684 print(f"Errore disjointUnion: {e}")
686 def handle_datatype_restriction(self, instance, uri, predicate, obj, setter=None):
687 on_datatype = self.graph.value(uri, OWL.onDatatype)
688 if on_datatype:
689 dt = self.get_or_create(on_datatype, Datatype)
690 if dt:
691 instance.set_applies_on_concept(dt)
692 try:
693 collection = RDFLibCollection(self.graph, obj)
694 for facet_node in collection:
695 for facet_pred, facet_val in self.graph.predicate_objects(facet_node):
696 if facet_pred == RDF.type:
697 continue
698 if isinstance(facet_val, RDFlibLiteral):
699 FACET_MAP = {
700 str(XSD.minInclusive): ">=",
701 str(XSD.maxInclusive): "<=",
702 str(XSD.minExclusive): ">",
703 str(XSD.maxExclusive): "<",
704 str(XSD.pattern): "pattern",
705 str(XSD.length): "length",
706 str(XSD.minLength): "minLength",
707 str(XSD.maxLength): "maxLength",
708 }
709 manchester_facet = FACET_MAP.get(str(facet_pred), str(facet_pred))
710 instance.set_applies_on_concept(manchester_facet)
711 literal = self._create_literal(facet_val)
712 instance.set_has_restriction_value(literal)
714 # Register facet node in cache keyed to the parent DatatypeRestriction
715 # and mark all its triples as mapped, so phase6 does not create
716 # spurious Statements or Individuals for the facet BNode.
717 if facet_node not in self._instance_cache:
718 self._instance_cache[facet_node] = set()
719 self._instance_cache[facet_node].add(instance)
720 self._track(instance, *((facet_node, fp, fv) for fp, fv in self.graph.predicate_objects(facet_node)))
722 except Exception as e:
723 print(f"Errore handle_datatype_restriction: {e}")
725 def handle_cardinality_exactly(self, instance, uri, predicate, obj, setter=None):
726 instance.set_has_cardinality_type("exactly")
727 instance.set_has_cardinality(obj)
728 instance.set_applies_on_concept(self.get_or_create(OWL.Thing, Concept))
730 def handle_cardinality_min(self, instance, uri, predicate, obj, setter=None):
731 instance.set_has_cardinality_type("min")
732 instance.set_has_cardinality(obj)
733 instance.set_applies_on_concept(self.get_or_create(OWL.Thing, Concept))
735 def handle_cardinality_max(self, instance, uri, predicate, obj, setter=None):
736 instance.set_has_cardinality_type("max")
737 instance.set_has_cardinality(obj)
738 instance.set_applies_on_concept(self.get_or_create(OWL.Thing, Concept))
740 # ========== HANDLER TRUTH FUNCTIONS ==========
742 def _build_truth_function(self, instance, obj, operator):
743 """
744 If instance is TruthFunction: populate in-place, return None.
745 If instance is Concept: create a separate TruthFunction keyed on obj, return tf.
747 If the list has a single element, log a warning and:
748 - if instance is TruthFunction: populate applies_on_concept anyway, return None
749 - if instance is Concept: return the Concept directly (no TruthFunction created)
751 If the list is empty, log a warning and return None.
752 """
753 try:
754 items = list(RDFLibCollection(self.graph, obj))
755 except Exception as e:
756 print(f"Errore build_truth_function: {e}")
757 return None
759 if len(items) == 0:
760 owl_warnings.empty_truth_function(self, instance, obj, operator)
761 return None
763 if len(items) == 1:
764 owl_warnings.singleton_truth_function(self, instance, obj, operator)
765 single_concept = self.get_or_create(items[0], Concept)
766 if type(instance) is TruthFunction:
767 instance.set_has_logical_operator(operator)
768 if single_concept:
769 instance.set_applies_on_concept(single_concept)
770 return None
771 else:
772 return single_concept
774 if type(instance) is TruthFunction:
775 instance.set_has_logical_operator(operator)
776 for item in items:
777 concept = self.get_or_create(item, Concept)
778 if concept:
779 instance.set_applies_on_concept(concept)
780 return None
781 else:
782 tf = self.get_or_create(obj, TruthFunction)
783 if tf:
784 tf.set_has_logical_operator(operator)
785 for item in items:
786 concept = self.get_or_create(item, Concept)
787 if concept:
788 tf.set_applies_on_concept(concept)
789 return tf
791 def handle_intersection(self, instance, uri, predicate, obj, setter=None):
792 if type(instance) not in (TruthFunction, Concept):
793 return
794 tf = self._build_truth_function(instance, obj, "and")
795 if tf and isinstance(instance, Concept):
796 instance.set_is_equivalent_to(tf)
798 def handle_union(self, instance, uri, predicate, obj, setter=None):
799 if type(instance) not in (TruthFunction, Concept):
800 return
801 tf = self._build_truth_function(instance, obj, "or")
802 if tf and isinstance(instance, Concept):
803 instance.set_is_equivalent_to(tf)
805 def handle_complement(self, instance, uri, predicate, obj, setter=None):
806 if type(instance) not in (TruthFunction, Concept):
807 return
808 if type(instance) is TruthFunction:
809 instance.set_has_logical_operator("not")
810 concept = self.get_or_create(obj, Concept)
811 if concept:
812 instance.set_applies_on_concept(concept)
813 else:
814 tf = self.get_or_create(obj, TruthFunction)
815 if tf:
816 tf.set_has_logical_operator("not")
817 concept = self.get_or_create(obj, Concept)
818 if concept:
819 tf.set_applies_on_concept(concept)
820 instance.set_is_equivalent_to(tf)
822 def handle_one_of(self, instance, uri, predicate, obj, setter=None):
823 if type(instance) not in (OneOf, Concept):
824 return
825 if type(instance) is Concept:
826 one_of = self.get_or_create(uri, OneOf)
827 if one_of:
828 try:
829 collection = RDFLibCollection(self.graph, obj)
830 for item in collection:
831 resource = self.get_or_create(item, Individual)
832 if resource:
833 one_of.set_applies_on_resource(resource)
834 except Exception as e:
835 print(f"Errore oneOf su Concept: {e}")
836 instance.set_is_equivalent_to(one_of)
837 else: # type(instance) is OneOf
838 try:
839 collection = RDFLibCollection(self.graph, obj)
840 for item in collection:
841 resource = self.get_or_create(item, Individual)
842 if resource:
843 instance.set_applies_on_resource(resource)
844 except Exception as e:
845 print(f"Errore oneOf: {e}")
847 def handle_has_value(self, instance, uri, predicate, obj, setter=None):
848 resource = self.get_or_create(obj, Resource)
849 if resource:
850 instance.set_applies_on_resource(resource)
851 on_prop = self.graph.value(uri, OWL.onProperty)
852 if on_prop:
853 prop = self.get_or_create(on_prop, Property)
854 if prop:
855 instance.set_applies_on_property(prop)
858 def handle_rdf_type(self, instance, uri, predicate, obj, setter=None):
859 """
860 Handler bidirezionale per rdf:type.
862 - subject.has_type += object (lato Individual/Resource)
863 - object.individuals_with_this_type += subject (lato Concept, solo se l'oggetto e'
864 effettivamente classificato come Concept e non e' un URI strutturale
865 del vocabolario)
867 Replica la filter logic di _apply_setters per set_has_type: scarta
868 oggetti nei namespace OWL/RDF/RDFS, eccetto owl:Thing, owl:Nothing,
869 rdfs:Literal.
870 """
871 if not isinstance(obj, URIRef) and not isinstance(obj, BNode):
872 return
874 # Filtro URI strutturali (stessa logica di _apply_setters)
875 if isinstance(obj, URIRef):
876 obj_str = str(obj)
877 if any(obj_str.startswith(ns) for ns in self._allowed_namespaces) \
878 and obj not in (OWL.Thing, OWL.Nothing, RDFS.Literal):
879 return
881 concept = self.get_or_create(obj, Concept)
882 if concept is None:
883 return
885 # Lato diretto
886 if hasattr(instance, 'set_has_type'):
887 instance.set_has_type(concept)
889 # Lato inverso: solo se l'oggetto e' realmente un Concept
890 # (get_or_create puo' promuovere; type() esatto evita di sporcare
891 # TruthFunction/Restriction/OneOf/etc.)
892 if type(concept) is Concept and isinstance(instance, Individual):
893 concept.set_individuals_with_this_type(instance)
896 # ========== OVERRIDE Statement per OWL (typed subject/object) ==========
898 def _create_statement_for_triple(self, subj, pred, obj):
899 """
900 Creates a Statement instance for a triple that was not mapped during
901 population phases.
903 Skips rdf:type triples — those are handled by phase2 and must not be
904 reified as Statements.
906 Subject is resolved as Individual; if the namespace filter blocks it
907 (e.g. OWL/RDF/RDFS URIs), the triple is silently dropped.
909 Predicate is looked up in the instance cache first. If not found, a bare
910 Annotation is created directly, bypassing the namespace filter — predicates
911 must always be reifiable regardless of their namespace.
913 Object is resolved based on its RDF type: RDF collections become
914 Containers, RDFLib Literals become Literal instances, everything else
915 is resolved as Individual.
917 OWL defaults (owl:Thing type for untyped Individuals) are applied to
918 subject and object after the Statement is built, since these instances
919 may have been created here for the first time, after phase3 defaults
920 have already run.
921 """
922 if pred == RDF.type or pred in self._property_mapping:
923 return
925 # Subject: an unmapped structured BNode is a Statement, not an Individual.
926 # This handles the case where phase6 visits the BNode's outgoing triples
927 # before visiting the triple that points to it.
928 if self._is_unmapped_structured_bnode(subj):
929 # Just materialise it; its outgoing triples will be absorbed inside.
930 # Skip emitting a wrapper Statement for this triple — _create_nested_statement
931 # already records (subj, pred, obj) in the BNode-Statement's triples_map.
932 self._create_nested_statement(subj)
933 return
935 subj_inst = self._resolve_statement_endpoint(subj, Individual)
936 if subj_inst is None:
937 # Fallback for structural-namespace subjects: create a bare Resource
938 # so the predicate is still reifiable.
939 subj_inst = Resource()
940 subj_inst.set_has_identifier(str(subj))
941 if subj not in self._instance_cache:
942 self._instance_cache[subj] = set()
943 self._instance_cache[subj].add(subj_inst)
945 if pred in self._instance_cache:
946 pred_inst = next(
947 (i for i in self._instance_cache[pred] if isinstance(i, Property)),
948 None
949 )
950 else:
951 pred_inst = None
953 if pred_inst is None:
954 pred_inst = Annotation()
955 pred_inst.set_has_identifier(str(pred))
956 if pred not in self._instance_cache:
957 self._instance_cache[pred] = set()
958 self._instance_cache[pred].add(pred_inst)
960 statement = Statement()
961 stmt_bnode = BNode()
962 statement.set_has_identifier(str(stmt_bnode))
964 self._track(statement, (subj, pred, obj))
966 statement.set_has_subject(subj_inst)
967 statement.set_has_predicate(pred_inst)
969 if self._is_rdf_collection(obj):
970 obj_inst = self._convert_collection_to_container(obj)
971 elif isinstance(obj, RDFlibLiteral):
972 obj_inst = self._create_literal(obj)
973 elif self._is_unmapped_structured_bnode(obj):
974 obj_inst = self._create_nested_statement(obj)
975 else:
976 # oggetto mai visto: Individual (fallback class — _resolve_statement_endpoint
977 # ritorna il dominante in cache se presente, altrimenti crea Individual)
978 obj_inst = self._resolve_statement_endpoint(obj, Resource)
980 if obj_inst:
981 statement.set_has_object(obj_inst)
983 if stmt_bnode not in self._instance_cache:
984 self._instance_cache[stmt_bnode] = set()
985 self._instance_cache[stmt_bnode].add(statement)
987 self._enrich_or_apply_owl_defaults(subj_inst, subj)
988 if obj_inst and not isinstance(obj, RDFlibLiteral):
989 self._enrich_or_apply_owl_defaults(obj_inst, obj)
991 def _resolve_statement_endpoint(self, node, fallback_class):
992 dom = self._get_punning_dominant(node)
993 if dom is not None:
994 return dom
995 return self.get_or_create(node, fallback_class)
997 # ========== PROVENANCE OWL-SPECIFIC ==========
999 _PROVENANCE_AXIOM_TYPES = (
1000 OWL.AllDisjointClasses,
1001 OWL.AllDifferent,
1002 OWL.AllDisjointProperties,
1003 )
1005 def _add_axiom_provenance(self, instance, sub):
1006 """Include general axioms that mention `instance` URI in members/distinctMembers."""
1007 uri_str = getattr(instance, 'has_identifier', None)
1008 if not uri_str:
1009 return
1010 uri_ref = URIRef(uri_str)
1011 for axiom_type in self._PROVENANCE_AXIOM_TYPES:
1012 for axiom in self.graph.subjects(RDF.type, axiom_type):
1013 if self._uri_in_axiom_members(axiom, uri_ref):
1014 self._expand_bnode_into(axiom, sub, set())
1016 def _uri_in_axiom_members(self, axiom_node, uri_ref):
1017 """True if uri_ref appears in owl:members or owl:distinctMembers list of axiom_node."""
1018 for list_pred in (OWL.members, OWL.distinctMembers):
1019 head = self.graph.value(axiom_node, list_pred)
1020 if head is None:
1021 continue
1022 node = head
1023 while node and node != RDF.nil:
1024 first = self.graph.value(node, RDF.first)
1025 if first == uri_ref:
1026 return True
1027 node = self.graph.value(node, RDF.rest)
1028 return False