UNPKG

sparnatural

Version:

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

578 lines (516 loc) 18.2 kB
import { BgpPattern, BlankTerm, FilterPattern, FunctionCallExpression, IriTerm, LiteralTerm, Pattern, Triple, ValuePatternRow, ValuesPattern, } from "sparqljs"; import { SelectedVal } from "../../components/SelectedVal"; import { RDFTerm, RdfTermValue, WidgetValue, } from "../../components/widgets/AbstractWidget"; import SparqlFactory from "./SparqlFactory"; import { DataFactory, NamedNode } from "rdf-data-factory"; import { SelectAllValue } from "../../components/builder-section/groupwrapper/criteriagroup/edit-components/EditComponents"; import { BooleanWidgetValue } from "../../components/widgets/BooleanWidget"; import { NumberWidgetValue } from "../../components/widgets/NumberWidget"; import ISparnaturalSpecification from "../../spec-providers/ISparnaturalSpecification"; import { Config } from "../../ontologies/SparnaturalConfig"; import { SearchRegexWidgetValue } from "../../components/widgets/SearchRegexWidget"; import { DateTimePickerValue } from "../../components/widgets/timedatepickerwidget/TimeDatePickerWidget"; import { MapValue } from "../../components/widgets/MapWidget"; import { LatLng } from "leaflet"; import ISpecificationProperty from "../../spec-providers/ISpecificationProperty"; import { SHACLSpecificationEntity } from "../../spec-providers/shacl/SHACLSpecificationEntity"; const factory = new DataFactory(); /** * A factory for creating ValueBuilders from the widgetType. This is the association between the widget type * and the corresponding ValueBuilder */ export class ValueBuilderFactory { buildValueBuilder(widgetType: string): ValueBuilderIfc { switch (widgetType) { case Config.LITERAL_LIST_PROPERTY: case Config.LIST_PROPERTY: case Config.TREE_PROPERTY: case Config.AUTOCOMPLETE_PROPERTY: return new RdfTermValueBuilder(); case Config.VIRTUOSO_SEARCH_PROPERTY: case Config.GRAPHDB_SEARCH_PROPERTY: case Config.STRING_EQUALS_PROPERTY: case Config.SEARCH_PROPERTY: return new SearchRegexValueBuilder(); case Config.NON_SELECTABLE_PROPERTY: return new NonSelectableValueBuilder(); case Config.BOOLEAN_PROPERTY: return new BooleanValueBuilder(); case Config.MAP_PROPERTY: return new MapValueBuilder(); case Config.NUMBER_PROPERTY: return new NumberValueBuilder(); case Config.TIME_PROPERTY_YEAR: case Config.TIME_PROPERTY_DATE: return new DateTimePickerValueBuilder(); case Config.TIME_PROPERTY_PERIOD: console.warn(Config.TIME_PROPERTY_PERIOD + " is not implement yet"); break; default: throw new Error(`WidgetType ${widgetType} not recognized`); } } } /** * Builds a SPARQL pattern from a (list of) widget values */ export default interface ValueBuilderIfc { init( specProvider: ISparnaturalSpecification, startClassVal: SelectedVal, propertyVal: SelectedVal, endClassVal: SelectedVal, endClassVarSelected: boolean, values: Array<WidgetValue["value"]> ): void; /** * main method : builds the SPARQL pattern */ build(): Pattern[]; /** * @returns true if the rdf:type criteria of the subject must not be generated */ isBlockingStart(): boolean; /** * @returns true if the rdf:type criteria of the object variable must not be generated */ isBlockingEnd(): boolean; /** * @returns true if the triple criteria between the subject and the object must not be generated */ isBlockingObjectProp(): boolean; } export abstract class BaseValueBuilder implements ValueBuilderIfc { protected specProvider: ISparnaturalSpecification; protected startClassVal: SelectedVal; protected propertyVal: SelectedVal; protected endClassVal: SelectedVal; protected values: Array<WidgetValue["value"]>; protected endClassVarSelected: boolean; init( specProvider: ISparnaturalSpecification, startClassVal: SelectedVal, propertyVal: SelectedVal, endClassVal: SelectedVal, endClassVarSelected: boolean, values: Array<WidgetValue["value"]> ): void { this.specProvider = specProvider; this.startClassVal = startClassVal; this.propertyVal = propertyVal; this.endClassVal = endClassVal; this.endClassVarSelected = endClassVarSelected; this.values = values; } abstract build(): Pattern[]; isBlockingStart(): boolean { return false; } isBlockingEnd(): boolean { return false; } isBlockingObjectProp(): boolean { return false; } } /** * A ValueBuilder that can work from an RdfTermValue and tests the equality either * by inserting the sole unique value as the object of the triple or by using a VALUES clause */ export class RdfTermValueBuilder extends BaseValueBuilder implements ValueBuilderIfc { build(): Pattern[] { let widgetValues = this.values as RdfTermValue["value"][]; if (this.isBlockingObjectProp()) { let singleTriple: Triple = SparqlFactory.buildTriple( factory.variable(this.startClassVal.variable), factory.namedNode(this.propertyVal.type), this.#rdfTermToSparqlQuery(widgetValues[0].rdfTerm) ); let ptrn: BgpPattern = { type: "bgp", triples: [singleTriple], }; return [ptrn]; } else { let vals = widgetValues.map((v) => { let vl: ValuePatternRow = {}; vl["?" + this.endClassVal.variable] = this.#rdfTermToSparqlQuery( v.rdfTerm ); return vl; }); let valuePattern: ValuesPattern = { type: "values", values: vals, }; return [valuePattern]; } } /** * Translates an IRI, Literal or BNode into the corresponding SPARQL query term * to be inserted in a SPARQL query. * @returns */ #rdfTermToSparqlQuery(rdfTerm: RDFTerm): IriTerm | BlankTerm | LiteralTerm { if (rdfTerm.type == "uri") { return factory.namedNode(rdfTerm.value); } else if (rdfTerm.type == "literal") { if (rdfTerm["xml:lang"]) { return factory.literal(rdfTerm.value, rdfTerm["xml:lang"]); } else if (rdfTerm.datatype) { // if the second parameter is a NamedNode, then it is considered a datatype, otherwise it is // considered like a language // so we make the datatype a NamedNode let namedNodeDatatype = factory.namedNode(rdfTerm.datatype); return factory.literal(rdfTerm.value, namedNodeDatatype); } else { return factory.literal(rdfTerm.value); } } else if (rdfTerm.type == "bnode") { // we don't know what to do with this, but don't trigger an error return factory.blankNode(rdfTerm.value); } else { throw new Error("Unexpected rdfTerm type " + rdfTerm.type); } } /** * @returns true if there is at least one value, because in that case the rdf:type criteria is redundant */ isBlockingEnd(): boolean { return this.values?.length > 0; } /** * @returns true if there is a single value and the end class is not selected (in which case we need the variable * to put it in the SELECT clause), and the target entity is not associated to a SPARQL query, in which case the variable * must not disappear from the query */ isBlockingObjectProp(): boolean { return ( this.values?.length == 1 && !this.endClassVarSelected && !( this.specProvider.getEntity(this.endClassVal.type) instanceof SHACLSpecificationEntity && (this.specProvider.getEntity(this.endClassVal.type) as SHACLSpecificationEntity).hasShTarget() ) ); } } export class BooleanValueBuilder extends BaseValueBuilder implements ValueBuilderIfc { build(): Pattern[] { let widgetValues = this.values as BooleanWidgetValue["value"][]; // if we are blocking the object prop, we create it directly here with the value as the object if (this.isBlockingObjectProp()) { let ptrn: BgpPattern = { type: "bgp", triples: [ { subject: factory.variable(this.startClassVal.variable), predicate: factory.namedNode(this.propertyVal.type), object: factory.literal( widgetValues[0].boolean.toString(), factory.namedNode("http://www.w3.org/2001/XMLSchema#boolean") ), }, ], }; return [ptrn]; } else { // otherwise the object prop is created and we create a VALUES clause with the actual boolean let vals = (this.values as BooleanWidgetValue["value"][]).map((v) => { let vl: ValuePatternRow = {}; vl["?" + this.endClassVal.variable] = factory.literal( widgetValues[0].boolean.toString(), factory.namedNode("http://www.w3.org/2001/XMLSchema#boolean") ); return vl; }); let valuePattern: ValuesPattern = { type: "values", values: vals, }; return [valuePattern]; } } /** * Blocks if a value is selected and this is not the "all" special value * @returns true */ isBlockingObjectProp() { return ( this.values?.length == 1 && !(this.values[0] instanceof SelectAllValue) && !this.endClassVarSelected ); } } export class NumberValueBuilder extends BaseValueBuilder implements ValueBuilderIfc { build(): Pattern[] { let widgetValues = this.values as NumberWidgetValue["value"][]; return [ SparqlFactory.buildFilterRangeDateOrNumber( widgetValues[0].min != undefined ? factory.literal( widgetValues[0].min.toString(), factory.namedNode("http://www.w3.org/2001/XMLSchema#decimal") ) : null, widgetValues[0].max != undefined ? factory.literal( widgetValues[0].max.toString(), factory.namedNode("http://www.w3.org/2001/XMLSchema#decimal") ) : null, factory.variable(this.endClassVal.variable) ), ]; } } export class NonSelectableValueBuilder extends BaseValueBuilder implements ValueBuilderIfc { build(): Pattern[] { return []; } } export class SearchRegexValueBuilder extends BaseValueBuilder implements ValueBuilderIfc { build(): Pattern[] { let widgetType = this.specProvider .getProperty(this.propertyVal.type) .getPropertyType(this.endClassVal.type); let widgetValues = this.values as SearchRegexWidgetValue["value"][]; switch (widgetType) { case Config.STRING_EQUALS_PROPERTY: { // builds a FILTER(lcase(...) = lcase(...)) return [ SparqlFactory.buildFilterStringEquals( factory.literal(`${widgetValues[0].regex}`), factory.variable(this.endClassVal.variable) ), ]; } case Config.SEARCH_PROPERTY: { // builds a FILTER(regex(...,...,"i")) return [ SparqlFactory.buildFilterRegex( factory.literal(`${widgetValues[0].regex}`), factory.variable(this.endClassVal.variable) ), ]; } case Config.GRAPHDB_SEARCH_PROPERTY: { // builds a GraphDB-specific search pattern let ptrn: BgpPattern = { type: "bgp", triples: [ { subject: factory.variable(this.startClassVal.variable), predicate: factory.namedNode( "http://www.ontotext.com/connectors/lucene#query" ), object: factory.literal(`text:${widgetValues[0].regex}`), }, { subject: factory.variable(this.startClassVal.variable), predicate: factory.namedNode( "http://www.ontotext.com/connectors/lucene#entities" ), object: factory.variable(this.endClassVal.variable), }, ], }; return [ptrn]; } case Config.VIRTUOSO_SEARCH_PROPERTY: { let bif_query = widgetValues[0].label .replace(/[\"']/g, " ") .split(" ") .map((e) => `'${e}'`) .join(" and "); console.log(bif_query); let ptrn: BgpPattern = { type: "bgp", triples: [ { subject: factory.variable(this.endClassVal.variable), predicate: factory.namedNode( "http://www.openlinksw.com/schemas/bif#contains" ), object: factory.literal(`${bif_query}`), }, ], }; return [ptrn]; } case Config.JENA_SEARCH_PROPERTY: { throw new Error("Not implemented yet"); } } } } export class DateTimePickerValueBuilder extends BaseValueBuilder implements ValueBuilderIfc { build(): Pattern[] { let widgetValues = this.values as DateTimePickerValue["value"][]; let specProperty:ISpecificationProperty = this.specProvider.getProperty(this.propertyVal.type); let beginDateProp = specProperty.getBeginDateProperty(); let endDateProp = specProperty.getEndDateProperty(); if(beginDateProp && endDateProp) { // special config with a begin and end date let exactDateProp = specProperty.getExactDateProperty(); // we have some values, generate the filters return [ SparqlFactory.buildDateRangeOrExactDatePattern( widgetValues[0].start?factory.literal( this.#formatSparqlDate(widgetValues[0].start), factory.namedNode("http://www.w3.org/2001/XMLSchema#dateTime") ):null, widgetValues[0].stop?factory.literal( this.#formatSparqlDate(widgetValues[0].stop), factory.namedNode("http://www.w3.org/2001/XMLSchema#dateTime") ):null, factory.variable(this.startClassVal.variable), factory.namedNode(beginDateProp), factory.namedNode(endDateProp), exactDateProp != null?factory.namedNode(exactDateProp):null, factory.variable(this.endClassVal.variable) ) ]; } else { // normal case, standard config return [ SparqlFactory.buildFilterRangeDateOrNumber( widgetValues[0].start?factory.literal( this.#formatSparqlDate(widgetValues[0].start), factory.namedNode("http://www.w3.org/2001/XMLSchema#dateTime") ):null, widgetValues[0].stop?factory.literal( this.#formatSparqlDate(widgetValues[0].stop), factory.namedNode("http://www.w3.org/2001/XMLSchema#dateTime") ):null, factory.variable(this.endClassVal.variable) ) ]; } } /** * We are blocking the generation of the predicate between start and end class * if the property is configured with a begin and end date (because the triples will then be generated by this class) * @returns true if the property has been configured with a begin and an end date property */ isBlockingObjectProp() { let beginDateProp = this.specProvider.getProperty(this.propertyVal.type).getBeginDateProperty(); let endDateProp = this.specProvider.getProperty(this.propertyVal.type).getEndDateProperty(); return ( this.values?.length == 1 && !(this.values[0] instanceof SelectAllValue) && beginDateProp != null && endDateProp != null ); } /** * * @param date Formats the date to insert in the SPARQL query. We cannot rely on toISOString() method * since it does not properly handle negative year and generates "-000600-12-31" while we want "-0600-12-31" * @returns */ #formatSparqlDate(date:Date) { if(date == null) return null; return this.#padYear(date.getUTCFullYear()) + '-' + this.#pad(date.getUTCMonth() + 1) + '-' + this.#pad(date.getUTCDate()) + 'T' + this.#pad(date.getUTCHours()) + ':' + this.#pad(date.getUTCMinutes()) + ':' + this.#pad(date.getUTCSeconds()) + 'Z'; } #pad(number:number) { if (number < 10) { return '0' + number; } return number; } #padYear(number:number) { let absoluteValue = (number < 0)?-number:number; let absoluteString = (absoluteValue < 1000)?absoluteValue.toString().padStart(4,'0'):absoluteValue.toString(); let finalString = (number < 0)?"-"+absoluteString:absoluteString; return finalString; } } const GEOFUNCTIONS_NAMESPACE = "http://www.opengis.net/def/function/geosparql/"; export const GEOFUNCTIONS = { WITHIN: factory.namedNode(GEOFUNCTIONS_NAMESPACE + "sfWithin") as NamedNode, }; const GEOSPARQL_NAMESPACE = "http://www.opengis.net/ont/geosparql#"; export const GEOSPARQL = { WKT_LITERAL: factory.namedNode( GEOSPARQL_NAMESPACE + "wktLiteral" ) as NamedNode, }; export class MapValueBuilder extends BaseValueBuilder implements ValueBuilderIfc { // reference: https://graphdb.ontotext.com/documentation/standard/geosparql-support.html build(): Pattern[] { let widgetValues = this.values as MapValue["value"][]; // the property between the subject and its position expressed as wkt value, e.g. http://www.w3.org/2003/01/geo/wgs84_pos#geometry let filterPtrn: FilterPattern = { type: "filter", expression: <FunctionCallExpression>(<unknown>{ type: "functionCall", function: GEOFUNCTIONS.WITHIN, args: [ factory.variable(this.endClassVal.variable), this.#buildPolygon(widgetValues[0].coordinates[0]), ], }), }; return [filterPtrn]; } #buildPolygon(coordinates: LatLng[]) { let polygon = ""; coordinates.forEach((coordinat) => { polygon = `${polygon}${coordinat.lng} ${coordinat.lat}, `; }); // polygon must be closed with the starting point let startPt = coordinates[0]; let literal: LiteralTerm = factory.literal( `Polygon((${polygon}${startPt.lng} ${startPt.lat}))`, GEOSPARQL.WKT_LITERAL ); return literal; } }