All files / rdf/reasoners rdfs-reasoner.ts

100% Statements 118/118
100% Branches 73/73
100% Functions 16/16
100% Lines 117/117

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304            12x           45x   45x   45x   45x         45x         45x         45x     45x       52510x               13537x   38973x         17982x             118x     37470x   118x 3109x     118x               118x 3846x 9075x 4791x     9075x 2231x                           3846x 3846x 3846x   3846x 9780x   9780x 705x     9075x 9075x   9075x 5741x       3846x       118x   118x 118x 118x 118x 118x 118x 118x       543698x     543698x   52510x 38973x     52510x 83x       543698x 543698x       95624x   95624x       543698x 543698x 543698x   543698x 231104x     312594x   72338x   5053x 67285x 19659x 47626x 3354x   72338x     25796x   25796x 25448x 348x 75x 273x 19x 19x 254x 5x 5x 249x 9x 9x   25796x       7295x 4592x   7295x           94284x   94284x                 543698x 476468x     67230x       543698x 543698x 543698x         543698x   543698x 231104x     312594x   72338x   8829x     72338x     4039x   4039x     4492x 4492x   4492x     2803x 2803x   2803x                     4039x   4039x 3846x   3846x     4039x                   7295x   7295x 12x   7283x      
import * as rdfjs from "@rdfjs/types";
import { owl, rdf, rdfs, skos, sh } from "../../ontologies";
import { SkosReasoner } from "./skos-reasoner";
import { GraphUriGenerator, DefaultInferenceGraphHandler } from "./reasoner";
import { RdfDataFactory } from "../rdf-data-factory";
 
const { quad } = RdfDataFactory;
 
/**
 * A simple RDFS reasoner that expands the graph with inferred triples.
 */
export class RdfsReasoner extends SkosReasoner {
    protected ontologies: Set<string> = new Set();
 
    protected classes: Set<string> = new Set();
 
    protected properties: Set<string> = new Set();
 
    protected individuals: Set<rdfjs.Quad_Subject> = new Set();
 
    /**
     * The super properties of each property, as asserted with `rdfs:subPropertyOf`.
     */
    private readonly _superProperties: Map<string, { property: rdfjs.Quad_Subject, superProperties: Set<string> }> = new Map();
 
    /**
     * The ranges asserted for each property.
     */
    private readonly _ranges: Map<string, rdfjs.Quad_Object[]> = new Map();
 
    /**
     * The domains asserted for each property.
     */
    private readonly _domains: Map<string, rdfjs.Quad_Object[]> = new Map();
 
    constructor(targetUriGenerator: GraphUriGenerator = new DefaultInferenceGraphHandler()) {
        super(targetUriGenerator);
    }
 
    protected isIgnoredNode(term: rdfjs.Quad_Subject | rdfjs.Quad_Object): boolean {
        switch (term.value) {
            case skos.Concept.value:
            case skos.ConceptScheme.value:
            case skos.Collection.value:
            case skos.OrderedCollection.value:
            case sh.Shape.value:
            case sh.NodeShape.value:
            case sh.PropertyShape.value:
                return true;
            default:
                return false;
        }
    }
 
    protected isClass(id: string): boolean {
        return super.isClass(id) ||
            this.classes.has(id) ||
            this.properties.has(id) ||
            this.ontologies.has(id);
    }
 
    protected afterInference() {
        super.afterInference();
 
        // After all axioms have been inferred, add the inferred individuals to the graph.
        const individuals = [...this.individuals].filter(x => !this.isClass(x.value));
 
        for (let individual of individuals) {
            this.store.add(quad(individual, rdf.type, owl.NamedIndividual, this.targetGraph));
        }
 
        this._inferInheritedRangesAndDomains();
    }
 
    /**
     * Infers the ranges and domains that properties inherit from their super properties
     * (OWL 2 RL rules scm-rng2 and scm-dom2), following `rdfs:subPropertyOf` chains.
     */
    private _inferInheritedRangesAndDomains() {
        for (const { property, superProperties } of this._superProperties.values()) {
            for (const superProperty of this._getAncestorProperties(property.value, superProperties)) {
                for (const range of this._ranges.get(superProperty) ?? []) {
                    this.store.add(quad(property, rdfs.range, range, this.targetGraph));
                }
 
                for (const domain of this._domains.get(superProperty) ?? []) {
                    this.store.add(quad(property, rdfs.domain, domain, this.targetGraph));
                }
            }
        }
    }
 
    /**
     * Gets the transitive super properties of a property, nearest first. Cycles in the
     * property hierarchy are followed only once.
     * @param property The IRI or blank node id of a property.
     * @param superProperties The direct super properties of the property.
     * @returns The IRIs of all super properties, excluding the property itself.
     */
    private _getAncestorProperties(property: string, superProperties: Set<string>): string[] {
        const result: string[] = [];
        const visited = new Set<string>([property]);
        const queue = [...superProperties];
 
        while (queue.length > 0) {
            const current = queue.shift()!;
 
            if (visited.has(current)) {
                continue;
            }
 
            visited.add(current);
            result.push(current);
 
            for (const next of this._superProperties.get(current)?.superProperties ?? []) {
                queue.push(next);
            }
        }
 
        return result;
    }
 
    protected override resetState(): void {
        super.resetState();
 
        this.ontologies.clear();
        this.classes.clear();
        this.properties.clear();
        this.individuals.clear();
        this._superProperties.clear();
        this._ranges.clear();
        this._domains.clear();
    }
 
    override applyInference(quad: rdfjs.Quad) {
        super.applyInference(quad);
 
        // Treat all named nodes with rdf:type definitions as potential individuals.
        if (quad.subject.termType == "NamedNode" && quad.predicate.equals(rdf.type)) {
            // Only consider individuals that are not of a type that is ignored such as skos:Concept.
            if (!this.isIgnoredNode(quad.object)) {
                this.individuals.add(quad.subject);
            }
 
            if (quad.object.value == owl.Ontology.value) {
                this.ontologies.add(quad.subject.value);
            }
        }
 
        this.inferClassAxioms(quad as rdfjs.Quad);
        this.inferPropertyAxioms(quad as rdfjs.Quad);
    }
 
    protected assertClass(subject: rdfjs.Quad_Subject) {
        this.store.add(quad(subject, rdf.type, rdfs.Class, this.targetGraph));
 
        this.classes.add(subject.value);
    }
 
    protected inferClassAxioms(q: rdfjs.Quad) {
        let s = q.subject;
        let p = q.predicate;
        let o = q.object.termType != "Literal" ? q.object : undefined;
 
        if (!o) {
            return;
        }
 
        switch (p.value) {
            case rdf.type.value: {
                if (o.equals(rdfs.Class)) {
                    // No need to infer the class type, as it is already asserted.
                    this.classes.add(s.value);
                } else if (o.equals(owl.Class)) {
                    this.assertClass(s);
                } else if (!this.isW3CNode(o)) {
                    this.assertClass(o);
                }
                return;
            }
            case rdfs.subClassOf.value: {
                this.assertClass(s);
 
                if (!this.isW3CNode(o)) {
                    this.assertClass(o);
                } else if (o.equals(rdfs.Resource)) {
                    this.store.add(quad(rdfs.Resource, rdf.type, rdfs.Class, this.targetGraph));
                } else if (o.equals(rdfs.Class)) {
                    this.store.add(quad(rdfs.Class, rdf.type, rdfs.Class, this.targetGraph));
                    this.store.add(quad(rdfs.Class, rdfs.subClassOf, rdfs.Resource, this.targetGraph));
                } else if (o.equals(rdfs.Datatype)) {
                    this.store.add(quad(rdfs.Datatype, rdf.type, rdfs.Class, this.targetGraph));
                    this.store.add(quad(rdfs.Datatype, rdfs.subClassOf, rdfs.Class, this.targetGraph));
                } else if (o.equals(owl.Class)) {
                    this.store.add(quad(owl.Class, rdf.type, rdfs.Class, this.targetGraph));
                    this.store.add(quad(owl.Class, rdfs.subClassOf, rdfs.Class, this.targetGraph));
                }
                return;
            }
            case rdfs.range.value:
            case rdfs.domain.value: {
                if (!this.isW3CNode(o)) {
                    this.assertClass(o);
                }
                return;
            }
        }
    }
 
    protected assertProperty(subject: rdfjs.Quad_Subject) {
        this.store.add(quad(subject, rdf.type, rdf.Property, this.targetGraph));
 
        this.properties.add(subject.value);
    }
 
    /**
     * Assert that a predicate denotes a property (RDFS entailment rule rdf1). Predicates of the
     * W3C vocabularies are left out, mirroring how classes are inferred from usage.
     * @param predicate The predicate of a statement.
     */
    protected assertPropertyFromUsage(predicate: rdfjs.Quad_Predicate) {
        if (predicate.termType != "NamedNode" || this.isW3CNode(predicate)) {
            return;
        }
 
        this.assertProperty(predicate);
    }
 
    protected inferPropertyAxioms(quad: rdfjs.Quad) {
        let s = quad.subject;
        let p = quad.predicate;
        let o = quad.object.termType != "Literal" ? quad.object : undefined;
 
        // Note: A predicate denotes a property even if the statement has a literal object, so this
        // is asserted before the literals are filtered out below. Without it, a data document that
        // only *uses* the predicates of an ontology would not show any properties.
        this.assertPropertyFromUsage(p);
 
        if (!o) {
            return;
        }
 
        switch (p.value) {
            case rdf.type.value: {
                if (o.equals(rdf.Property)) {
                    // No need to infer the property type, as it is already asserted.
                    this.properties.add(s.value);
                }
 
                return;
            }
            case rdfs.subPropertyOf.value: {
                this._addSuperProperty(s, o);
 
                return;
            }
            case rdfs.range.value: {
                this.assertProperty(s);
                this._addToList(this._ranges, s.value, o);
 
                return;
            }
            case rdfs.domain.value: {
                this.assertProperty(s);
                this._addToList(this._domains, s.value, o);
 
                return;
            }
        }
    }
 
    /**
     * Records that a property is a sub property of another property.
     * @param property The sub property.
     * @param superProperty The super property.
     */
    private _addSuperProperty(property: rdfjs.Quad_Subject, superProperty: rdfjs.Quad_Object) {
        let entry = this._superProperties.get(property.value);
 
        if (!entry) {
            entry = { property, superProperties: new Set() };
 
            this._superProperties.set(property.value, entry);
        }
 
        entry.superProperties.add(superProperty.value);
    }
 
    /**
     * Adds a value to the list stored under a key.
     * @param map The map of lists.
     * @param key The key of the list.
     * @param value The value to add.
     */
    private _addToList(map: Map<string, rdfjs.Quad_Object[]>, key: string, value: rdfjs.Quad_Object) {
        const list = map.get(key);
 
        if (list) {
            list.push(value);
        } else {
            map.set(key, [value]);
        }
    }
}