sparnatural
Version:
Visual client-side SPARQL query builder and knowledge graph exploration tool
578 lines (516 loc) • 18.2 kB
text/typescript
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;
}
}