UNPKG

sparnatural

Version:

Visual client-side SPARQL query builder and knowledge graph exploration tool

354 lines (303 loc) 11.7 kB
import { DataFactory } from 'rdf-data-factory'; import { Config } from "../../ontologies/SparnaturalConfig"; import ISparnaturalSpecification from "../ISparnaturalSpecification"; import { Parser, Generator, SparqlParser, SparqlGenerator } from "sparqljs"; import { BaseRDFReader, RDFS } from "../BaseRDFReader"; import ISpecificationEntity from "../ISpecificationEntity"; import { OWLSpecificationEntity } from "./OWLSpecificationEntity"; import ISpecificationProperty from "../ISpecificationProperty"; import { OWLSpecificationProperty } from "./OWLSpecificationProperty"; import { RdfStore } from "rdf-stores"; import { NamedNode, Quad_Subject, Term } from '@rdfjs/types/data-model'; import { Dag, DagIfc } from '../../dag/Dag'; const factory = new DataFactory(); const OWL_NAMESPACE = "http://www.w3.org/2002/07/owl#"; export const OWL = { THING: factory.namedNode(OWL_NAMESPACE + "Thing") as NamedNode, EQUIVALENT_PROPERTY: factory.namedNode(OWL_NAMESPACE + "equivalentProperty") as NamedNode, EQUIVALENT_CLASS: factory.namedNode(OWL_NAMESPACE + "equivalentClass") as NamedNode, UNION_OF: factory.namedNode(OWL_NAMESPACE + "unionOf") as NamedNode }; export class OWLSpecificationProvider extends BaseRDFReader implements ISparnaturalSpecification { #parser: SparqlParser; #generator: SparqlGenerator; constructor(n3store: RdfStore, lang: string) { super(n3store, lang); // init SPARQL parser and generator once this.#parser = new Parser(); this.#generator = new Generator(); /* var sparql = ` SELECT ?o2 WHERE { { OPTIONAL {?x ?p1 ?var_1 } OPTIONAL {?x ?p2 ?var_2 } BIND(COALESCE(?var_1, ?var_2) AS ?var) } ?x ?p2 ?o2 . } `; var query = this.#parser.parse(sparql); console.dir(query) */ } getLanguages(): string[] { let languages:string[] = this.store .getQuads(null, RDFS.LABEL, null, null) .map((quad: { object: any }) => quad.object.language); // deduplicate the list of languages return [...new Set(languages)]; } getEntity(entityUri: string): ISpecificationEntity { return new OWLSpecificationEntity( entityUri, this, this.store, this.lang ); } getProperty(property: string): ISpecificationProperty { return new OWLSpecificationProperty( property, this, this.store, this.lang ); } getAllSparnaturalEntities() { var classes = this.getEntitiesInDomainOfAnyProperty(); // copy initial array var result = classes.slice(); // now look for all classes we can reach from this class list for (const aClass of classes) { var connectedClasses = this.getEntity(aClass).getConnectedEntities(); for (const aConnectedClass of connectedClasses) { this._pushIfNotExist(aConnectedClass, result); } } return result; } getEntitiesInDomainOfAnyProperty() : string[] { const quadsArray = this.store.getQuads( null, RDFS.DOMAIN, null, null ); var items: string | any[] = []; for (const quad of quadsArray) { // we are not looking at domains of _any_ property // the property we are looking at must be a Sparnatural property, with a known type var objectPropertyId = quad.subject.value; var typeClass = quad.object.termType; var classId = quad.object.value; var classAsRDFTerm = quad.object; // empty string means we are searching without specifying a range if (this.getProperty(objectPropertyId).getPropertyType("")) { // keep only Sparnatural classes in the list if (typeClass == "BlankNode" || this.isSparnaturalClass(classId)) { // always exclude "remote classes" that should not have a type that from first list if (!this.getEntity(classId).hasTypeCriteria()) { if (!this._isUnionClass(classAsRDFTerm)) { this._pushIfNotExist(classId, items); } else { // read union content - /!\ this returns RDFTerm var classesInUnion = this.graph.readAsList(classAsRDFTerm, OWL.UNION_OF); for (const aUnionClass of classesInUnion) { this._pushIfNotExist(aUnionClass.value, items); } } } } } } items = this._sort(items); console.log("Classes in domain of any property " + items); return items; } getEntitiesTreeInDomainOfAnyProperty(): DagIfc<ISpecificationEntity> { // 1. get the entities that are in a domain of a property let entities:OWLSpecificationEntity[] = this.getEntitiesInDomainOfAnyProperty().map(s => this.getEntity(s) as OWLSpecificationEntity) as OWLSpecificationEntity[]; // build dag from flat list let dag:Dag<OWLSpecificationEntity> = new Dag<OWLSpecificationEntity>(); dag.initFlatTreeFromFlatList(entities); return dag; } isSparnaturalClass(classUri: string) { return ( this.store.getQuads( factory.namedNode(classUri), RDFS.SUBCLASS_OF, factory.namedNode(Config.SPARNATURAL_CLASS), null ).length > 0 || this.store.getQuads( factory.namedNode(classUri), RDFS.SUBCLASS_OF, factory.namedNode(Config.NOT_INSTANTIATED_CLASS), null ).length > 0 || this.store.getQuads( factory.namedNode(classUri), RDFS.SUBCLASS_OF, factory.namedNode(Config.RDFS_LITERAL), null ).length > 0 ); } expandSparql(sparql: string, prefixes:{ [key: string]: string }) { // for each owl:equivalentProperty ... var equivalentPropertiesPerProperty: any = {}; this.store .getQuads(null, OWL.EQUIVALENT_PROPERTY, null, null) .forEach( (quad: { subject: { value: string | number }; object: { value: any } }) => { // store it if multiple equivalences are declared if (!equivalentPropertiesPerProperty[quad.subject.value]) { equivalentPropertiesPerProperty[quad.subject.value] = []; } equivalentPropertiesPerProperty[quad.subject.value].push(quad.object.value); } ); // join the equivalences with a | for (let [property, equivalents] of Object.entries( equivalentPropertiesPerProperty )) { var re = new RegExp("<" + property + ">", "g"); sparql = sparql.replace( re, "<" + (equivalents as Array<any>).join(">|<") + ">" ); } // for each owl:equivalentClass ... var equivalentClassesPerClass: any = {}; this.store .getQuads(null, OWL.EQUIVALENT_CLASS, null, null) .forEach( (quad: { subject: { value: string | number }; object: { value: any } }) => { // store it if multiple equivalences are declared if (!equivalentClassesPerClass[quad.subject.value]) { equivalentClassesPerClass[quad.subject.value] = []; } equivalentClassesPerClass[quad.subject.value].push(quad.object.value); } ); // use a VALUES if needed var i = 0; for (let [aClass, equivalents] of Object.entries( equivalentClassesPerClass )) { var re = new RegExp("<" + aClass + ">", "g"); if ((equivalents as Array<any>).length == 1) { sparql = sparql.replace(re, "<" + (equivalents as Array<any>)[0] + ">"); } else { sparql = sparql.replace( re, "?class" + i + " . VALUES ?class" + i + " { <" + (equivalents as Array<any>).join("> <") + "> } " ); } i++; } // for each sparqlString this.store .getQuads(null, factory.namedNode(Config.SPARQL_STRING), null, null) .forEach((quad: { subject: { value: string }; object: { value: any } }) => { // prepare the regex let classUri = quad.subject.value; let sparqlString = quad.object.value; // first pass to work with classes sparqlString that can contain a long expression // with variables and MINUSes, like // <http://exemple.com/MyClass> MINUS { $this <http://exemple.com/myProp> ?x VALUES ?x { A B } } // replace the $this with the name of the original variable in the query // \S matches any non-whitespace character var re = new RegExp("(\\S*) (rdf:type|a) <" + classUri + ">", "g"); // prepare the function that will return the string to replace let replacer = function(match:string, p1:string, offset:number, fullString:string) { // first substitutes any other variable name with a prefix // so that we garantee unicity across the complete query var reVariables = new RegExp("\\?(\\S*)", "g"); let whereClauseReplacedVariables = p1+" rdf:type "+sparqlString.replace(reVariables, "?$1_"+p1.substring(1)); // then, replace the match on the original URI with the whereClause of the target // replacing "$this" with the original variable name var reThis = new RegExp("\\$this", "g"); let whereClauseReplacedThis = whereClauseReplacedVariables.replace(reThis, p1); return whereClauseReplacedThis; } sparql = sparql.replace(re, replacer); // then a second pass simpler one that will work for properties var re = new RegExp("<" + quad.subject.value + ">", "g"); sparql = sparql.replace(re, quad.object.value); }); // reparse the query, apply prefixes, and reserialize the query var query = this.#parser.parse(sparql); for (var key in prefixes) { query.prefixes[key] = prefixes[key]; } let finalString = this.#generator.stringify(query); console.log(finalString); return finalString } _sort(items: any[]) { var me = this; const compareFunction = function (item1: any, item2: any) { // return me.getLabel(item1).localeCompare(me.getLabel(item2)); var entity1 = me.getEntity(item1); var entity2 = me.getEntity(item2); var order1 = entity1.getOrder(); var order2 = entity2.getOrder(); if (order1) { if (order2) { if (order1 == order2) { return entity1.getLabel().localeCompare(entity2.getLabel()); } else { // if the order is actually a number, convert it to number and use a number conversion if(!isNaN(Number(order1)) && !isNaN(Number(order2))) { return Number(order1) - Number(order2); } else { return (order1 > order2) ? 1 : -1; } } } else { return -1; } } else { if (order2) { return 1; } else { return entity1.getLabel().localeCompare(entity2.getLabel()); } } }; // sort according to order or label items.sort(compareFunction); return items; } /** * Reads config:order of an entity and returns it, or null if not set **/ _readOrder(uri: any) { return this.graph.readSingleProperty(uri, factory.namedNode(Config.ORDER))?.value; } /*** Handling of UNION classes ***/ _readUnionContent(classUri: any) { var lists = this.graph.readProperty(factory.namedNode(classUri), OWL.UNION_OF); if (lists.length > 0) { return this.graph.readListContent(lists[0]); } } _isUnionClass(classUriOrBNodeIdentifier: Term) { return this.graph.hasProperty(classUriOrBNodeIdentifier as Quad_Subject, OWL.UNION_OF); } /*** / Handling of UNION classes ***/ }