graph-explorer
Version:
Graph Explorer can be used to explore and RDF graphs in SPARQL endpoints or on the web.
794 lines (722 loc) • 22.1 kB
text/typescript
import {
LinkConfiguration,
PropertyConfiguration,
} from "./sparqlDataProviderSettings";
import {
RdfLiteral,
isRdfLiteral,
SparqlResponse,
ClassBinding,
ElementBinding,
LinkBinding,
isRdfIri,
isRdfBlank,
RdfIri,
ElementImageBinding,
LinkCountBinding,
LinkTypeBinding,
PropertyBinding,
ElementTypeBinding,
FilterBinding,
Triple,
} from "./sparqlModels";
import {
Dictionary,
LocalizedString,
LinkType,
ClassModel,
ElementModel,
LinkModel,
Property,
PropertyModel,
LinkCount,
ElementIri,
ElementTypeIri,
LinkTypeIri,
PropertyTypeIri,
isIriProperty,
isLiteralProperty,
sameLink,
hashLink,
} from "../model";
import { HashMap, getOrCreateSetInMap } from "../../viewUtils/collections";
const LABEL_URI = "http://www.w3.org/2000/01/rdf-schema#label";
const RDF_TYPE_URI = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
const EMPTY_MAP: ReadonlyMap<any, any> = new Map();
export function getClassTree(
response: SparqlResponse<ClassBinding>
): ClassModel[] {
const treeNodes = createClassMap(response.results.bindings);
const allNodes: ClassModel[] = [];
// createClassMap ensures we get both elements and parents and we can use treeNodes[treeNode.parent] safely
treeNodes.forEach((node) => {
allNodes.push(node);
node.parents.forEach((parent) => {
treeNodes.get(parent).children.push(node);
});
node.parents = undefined;
});
const withoutCycles = breakCyclesAndCalculateCounts(allNodes);
const leafs = new Set<ElementTypeIri>();
for (const node of withoutCycles) {
for (const child of node.children) {
leafs.add(child.id);
}
}
const tree = withoutCycles.filter((node) => !leafs.has(node.id));
return tree;
}
export function flattenClassTree(classTree: readonly ClassModel[]) {
const all: ClassModel[] = [];
const visitClasses = (classes: readonly ClassModel[]) => {
for (const model of classes) {
all.push(model);
visitClasses(model.children);
}
};
visitClasses(classTree);
return all;
}
/**
* This extension of ClassModel is used only in processing, parent links are not needed in UI (yet?)
*/
interface HierarchicalClassModel extends ClassModel {
parents?: Set<ElementTypeIri>;
}
function createClassMap(
bindings: ClassBinding[]
): Map<ElementTypeIri, HierarchicalClassModel> {
const treeNodes = new Map<ElementTypeIri, HierarchicalClassModel>();
for (const binding of bindings) {
if (!isRdfIri(binding.class)) {
continue;
}
const classIri = binding.class.value as ElementTypeIri;
let node = treeNodes.get(classIri);
if (!node) {
node = createEmptyModel(classIri);
treeNodes.set(classIri, node);
}
if (binding.label) {
appendLabel(node.label, getLocalizedString(binding.label));
}
if (binding.parent) {
const parentIri = binding.parent.value as ElementTypeIri;
node.parents.add(parentIri);
}
if (binding.instcount) {
node.count = getInstCount(binding.instcount);
}
}
// ensuring parent will always be there
for (const binding of bindings) {
if (binding.parent) {
const parentIri = binding.parent.value as ElementTypeIri;
if (!treeNodes.has(parentIri)) {
treeNodes.set(parentIri, createEmptyModel(parentIri));
}
}
}
function createEmptyModel(iri: ElementTypeIri): HierarchicalClassModel {
return {
id: iri as ElementTypeIri,
children: [],
label: { values: [] },
count: undefined,
parents: new Set<ElementTypeIri>(),
};
}
return treeNodes;
}
function breakCyclesAndCalculateCounts(tree: ClassModel[]): ClassModel[] {
const visiting = new Set<ElementTypeIri>();
function reduceChildren(acc: ClassModel[], node: ClassModel): ClassModel[] {
if (visiting.has(node.id)) {
// prevent unbounded recursion
return acc;
}
// no more to count
if (!node.children) {
return;
}
// ensure all children have their counts completed;
visiting.add(node.id);
node.children = node.children.reduce(reduceChildren, []);
visiting.delete(node.id);
// we have to preserve no data here. If nor element nor childs have no count information,
// we just pass undefined upwards.
let childCount: number;
node.children.forEach(({ count }) => {
if (count === undefined) {
return;
}
childCount = childCount === undefined ? count : childCount + count;
});
if (childCount !== undefined) {
node.count =
node.count === undefined ? childCount : node.count + childCount;
}
acc.push(node);
return acc;
}
return tree.reduce(reduceChildren, []);
}
export function getClassInfo(
response: SparqlResponse<ClassBinding>
): ClassModel[] {
const classes: Record<string, ClassModel> = {};
for (const binding of response.results.bindings) {
if (!binding.class) {
continue;
}
const id = binding.class.value as ElementTypeIri;
const model = classes[id];
if (model) {
appendLabel(model.label, getLocalizedString(binding.label));
const instanceCount = getInstCount(binding.instcount);
if (instanceCount !== undefined) {
model.count = Math.max(model.count, instanceCount);
}
} else {
const label = getLocalizedString(binding.label);
classes[id] = {
id,
children: [],
label: { values: label ? [label] : [] },
count: getInstCount(binding.instcount),
};
}
}
const classesList: ClassModel[] = [];
for (const id in classes) {
if (!Object.prototype.hasOwnProperty.call(classes, id)) {
continue;
}
const model = classes[id];
classesList.push(model);
}
return classesList;
}
export function getPropertyInfo(
response: SparqlResponse<PropertyBinding>
): Dictionary<PropertyModel> {
const models: Dictionary<PropertyModel> = {};
for (const sProperty of response.results.bindings) {
const sPropertyTypeId = sProperty.property.value as PropertyTypeIri;
if (models[sPropertyTypeId]) {
if (sProperty.label) {
const label = models[sPropertyTypeId].label;
if (label.values.length === 1 && !label.values[0].language) {
label.values = [];
}
label.values.push(getLocalizedString(sProperty.label));
}
} else {
models[sPropertyTypeId] = getPropertyModel(sProperty);
}
}
return models;
}
export function getLinkTypes(
response: SparqlResponse<LinkTypeBinding>
): LinkType[] {
const sInst = response.results.bindings;
const linkTypes: LinkType[] = [];
const linkTypesMap: Dictionary<LinkType> = {};
for (const sLink of sInst) {
const sInstTypeId = sLink.link.value as LinkTypeIri;
if (linkTypesMap[sInstTypeId]) {
if (sLink.label) {
const label = linkTypesMap[sInstTypeId].label;
if (label.values.length === 1 && !label.values[0].language) {
label.values = [];
}
label.values.push(getLocalizedString(sLink.label));
}
} else {
linkTypesMap[sInstTypeId] = getLinkTypeInfo(sLink);
linkTypes.push(linkTypesMap[sInstTypeId]);
}
}
return linkTypes;
}
export function triplesToElementBinding(
triples: Triple[]
): SparqlResponse<ElementBinding> {
const map: Dictionary<ElementBinding> = {};
const convertedResponse: SparqlResponse<ElementBinding> = {
head: {
vars: ["inst", "class", "label", "blankType", "propType", "propValue"],
},
results: {
bindings: [],
},
};
for (const triple of triples) {
const trippleId = triple.subject.value;
if (!map[trippleId]) {
map[trippleId] = createAndPushBinding(triple);
}
if (triple.predicate.value === LABEL_URI && isRdfLiteral(triple.object)) {
// Label
if (map[trippleId].label) {
map[trippleId] = createAndPushBinding(triple);
}
map[trippleId].label = triple.object;
} else if (
// Class
triple.predicate.value === RDF_TYPE_URI &&
isRdfIri(triple.object) &&
isRdfIri(triple.predicate)
) {
if (map[trippleId].class) {
map[trippleId] = createAndPushBinding(triple);
}
map[trippleId].class = triple.object;
} else if (!isRdfBlank(triple.object) && isRdfIri(triple.predicate)) {
// Property
if (map[trippleId].propType) {
map[trippleId] = createAndPushBinding(triple);
}
map[trippleId].propType = triple.predicate;
map[trippleId].propValue = triple.object;
}
}
function createAndPushBinding(tripple: Triple): ElementBinding {
const binding: ElementBinding = {
inst: tripple.subject as RdfIri,
};
convertedResponse.results.bindings.push(binding);
return binding;
}
return convertedResponse;
}
export function getElementsInfo(
response: SparqlResponse<ElementBinding>,
types: ReadonlyMap<ElementIri, ReadonlySet<ElementTypeIri>> = EMPTY_MAP,
propertyByPredicate: ReadonlyMap<
string,
readonly PropertyConfiguration[]
> = EMPTY_MAP,
openWorldProperties = true
): Dictionary<ElementModel> {
const instancesMap: Dictionary<ElementModel> = {};
for (const binding of response.results.bindings) {
if (!isRdfIri(binding.inst)) {
continue;
}
const iri = binding.inst.value as ElementIri;
let model = instancesMap[iri];
if (!model) {
model = emptyElementInfo(iri);
instancesMap[iri] = model;
}
enrichElement(model, binding);
}
if (!openWorldProperties || propertyByPredicate.size > 0) {
for (const iri in instancesMap) {
if (!Object.hasOwnProperty.call(instancesMap, iri)) {
continue;
}
const model = instancesMap[iri];
const modelTypes = types.get(model.id);
model.properties = mapPropertiesByConfig(
model,
modelTypes,
propertyByPredicate,
openWorldProperties
);
}
}
return instancesMap;
}
function mapPropertiesByConfig(
model: ElementModel,
modelTypes: ReadonlySet<ElementTypeIri> | undefined,
propertyByPredicate: ReadonlyMap<string, readonly PropertyConfiguration[]>,
openWorldProperties: boolean
): ElementModel["properties"] {
const mapped: ElementModel["properties"] = {};
for (const propertyIri in model.properties) {
if (!Object.hasOwnProperty.call(model.properties, propertyIri)) {
continue;
}
const properties = propertyByPredicate.get(propertyIri);
if (properties && properties.length > 0) {
for (const property of properties) {
if (typeMatchesDomain(property, modelTypes)) {
mapped[property.id] = model.properties[propertyIri];
}
}
} else if (openWorldProperties) {
mapped[propertyIri] = model.properties[propertyIri];
}
}
return mapped;
}
export function enrichElementsWithImages(
response: SparqlResponse<ElementImageBinding>,
elementsInfo: Dictionary<ElementModel>
): void {
const respElements = response.results.bindings;
for (const respEl of respElements) {
const elementInfo = elementsInfo[respEl.inst.value];
if (elementInfo) {
elementInfo.image = respEl.image.value;
}
}
}
export function getElementTypes(
response: SparqlResponse<ElementTypeBinding>
): Map<ElementIri, Set<ElementTypeIri>> {
const types = new Map<ElementIri, Set<ElementTypeIri>>();
for (const binding of response.results.bindings) {
if (isRdfIri(binding.inst) && isRdfIri(binding.class)) {
const element = binding.inst.value as ElementIri;
const type = binding.class.value as ElementTypeIri;
getOrCreateSetInMap(types, element).add(type);
}
}
return types;
}
export function getLinksInfo(
response: SparqlResponse<LinkBinding>,
types: ReadonlyMap<ElementIri, ReadonlySet<ElementTypeIri>> = EMPTY_MAP,
linkByPredicateType: ReadonlyMap<
string,
readonly LinkConfiguration[]
> = EMPTY_MAP,
openWorldLinks = true
): LinkModel[] {
const sparqlLinks = response.results.bindings;
const links = new HashMap<LinkModel, LinkModel>(hashLink, sameLink);
for (const binding of sparqlLinks) {
const model: LinkModel = {
sourceId: binding.source.value as ElementIri,
linkTypeId: binding.type.value as LinkTypeIri,
targetId: binding.target.value as ElementIri,
properties: {},
};
if (links.has(model)) {
// this can only happen due to error in sparql or when merging properties
if (binding.propType) {
const existing = links.get(model);
mergeProperties(
existing.properties,
binding.propType,
binding.propValue
);
}
} else {
if (binding.propType) {
mergeProperties(model.properties, binding.propType, binding.propValue);
}
const linkConfigs = linkByPredicateType.get(model.linkTypeId);
if (linkConfigs && linkConfigs.length > 0) {
for (const linkConfig of linkConfigs) {
if (typeMatchesDomain(linkConfig, types.get(model.sourceId))) {
const mappedModel: LinkModel = isDirectLink(linkConfig)
? { ...model, linkTypeId: linkConfig.id as LinkTypeIri }
: model;
links.set(mappedModel, mappedModel);
}
}
} else if (openWorldLinks) {
links.set(model, model);
}
}
}
const linkArray: LinkModel[] = [];
links.forEach((value) => linkArray.push(value));
return linkArray;
}
export function getLinksTypesOf(
response: SparqlResponse<LinkCountBinding>
): LinkCount[] {
const sparqlLinkTypes = response.results.bindings.filter(
(b) => !isRdfBlank(b.link)
);
return sparqlLinkTypes.map((sLink: LinkCountBinding) => getLinkCount(sLink));
}
export function getLinksTypeIds(
response: SparqlResponse<LinkTypeBinding>,
linkByPredicateType: ReadonlyMap<
string,
readonly LinkConfiguration[]
> = EMPTY_MAP,
openWorldLinks = true
): LinkTypeIri[] {
const linkTypes: LinkTypeIri[] = [];
for (const binding of response.results.bindings) {
if (!isRdfIri(binding.link)) {
continue;
}
const linkConfigs = linkByPredicateType.get(binding.link.value);
if (linkConfigs && linkConfigs.length > 0) {
for (const linkConfig of linkConfigs) {
const mappedLinkType = isDirectLink(linkConfig)
? linkConfig.id
: binding.link.value;
linkTypes.push(mappedLinkType as LinkTypeIri);
}
} else if (openWorldLinks) {
linkTypes.push(binding.link.value as LinkTypeIri);
}
}
return linkTypes;
}
export function getLinkStatistics(
response: SparqlResponse<LinkCountBinding>
): LinkCount | undefined {
for (const binding of response.results.bindings) {
if (isRdfIri(binding.link)) {
return getLinkCount(binding);
}
}
return undefined;
}
export function getFilteredData(
response: SparqlResponse<ElementBinding & FilterBinding>,
sourceTypes?: ReadonlySet<ElementTypeIri>,
linkByPredicateType: ReadonlyMap<
string,
readonly LinkConfiguration[]
> = EMPTY_MAP,
openWorldLinks = true
): Dictionary<ElementModel> {
const instancesMap: Dictionary<ElementModel> = {};
const resultTypes = new Map<ElementIri, Set<ElementTypeIri>>();
const outPredicates = new Map<ElementIri, Set<string>>();
const inPredicates = new Map<ElementIri, Set<string>>();
for (const binding of response.results.bindings) {
if (!isRdfIri(binding.inst) && !isRdfBlank(binding.inst)) {
continue;
}
const iri = binding.inst.value as ElementIri;
let model = instancesMap[iri];
if (!model) {
model = emptyElementInfo(iri);
instancesMap[iri] = model;
}
enrichElement(model, binding);
if (isRdfIri(binding.classAll)) {
getOrCreateSetInMap(resultTypes, iri).add(
binding.classAll.value as ElementTypeIri
);
}
if (!openWorldLinks && binding.link && binding.direction) {
const predicates =
binding.direction.value === "out" ? outPredicates : inPredicates;
getOrCreateSetInMap(predicates, model.id).add(binding.link.value);
}
}
if (!openWorldLinks) {
for (const id of Object.keys(instancesMap)) {
const model = instancesMap[id];
const targetTypes = resultTypes.get(model.id);
const doesMatchesDomain =
matchesDomainForLink(
sourceTypes,
outPredicates.get(model.id),
linkByPredicateType
) &&
matchesDomainForLink(
targetTypes,
inPredicates.get(model.id),
linkByPredicateType
);
if (!doesMatchesDomain) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete instancesMap[id];
}
}
}
return instancesMap;
}
function matchesDomainForLink(
types: ReadonlySet<ElementTypeIri> | undefined,
predicates: Set<string> | undefined,
linkByPredicateType: ReadonlyMap<string, readonly LinkConfiguration[]>
) {
if (!predicates) {
return true;
}
let hasMatch = false;
predicates.forEach((predicate) => {
const matched = linkByPredicateType.get(predicate);
if (matched) {
for (const link of matched) {
if (typeMatchesDomain(link, types)) {
hasMatch = true;
}
}
}
});
return hasMatch;
}
export function isDirectLink(link: LinkConfiguration) {
// link configuration is path-based if includes any variables
const pathBased = /[?$][a-zA-Z]+\b/.test(link.path);
return !pathBased;
}
export function isDirectProperty(property: PropertyConfiguration) {
// property configuration is path-based if includes any variables
const pathBased = /[?$][a-zA-Z]+\b/.test(property.path);
return !pathBased;
}
function typeMatchesDomain(
config: { readonly domain?: readonly string[] },
types: ReadonlySet<ElementTypeIri> | undefined
): boolean {
if (!config.domain || config.domain.length === 0) {
return true;
} else if (!types) {
return false;
} else {
for (const type of config.domain) {
if (types.has(type as ElementTypeIri)) {
return true;
}
}
return false;
}
}
/**
* Modifies properties with merging with new values, couls be new peroperty or new value for existing properties.
* @param properties
* @param propType
* @param propValue
*/
function mergeProperties(
properties: Record<string, Property>,
propType: RdfIri,
propValue: RdfIri | RdfLiteral
) {
let property = properties[propType.value];
if (isRdfIri(propValue)) {
if (!property) {
property = { type: "uri", values: [] };
}
if (
isIriProperty(property) &&
property.values.every(({ value }) => value !== propValue.value)
) {
property.values = [...property.values, propValue];
}
} else if (isRdfLiteral(propValue)) {
if (!property) {
property = { type: "string", values: [] };
}
const propertyValue = getLocalizedString(propValue);
if (
isLiteralProperty(property) &&
property.values.every((value) => !isLocalizedEqual(value, propertyValue))
) {
property.values = [...property.values, propertyValue];
}
}
properties[propType.value] = property;
}
export function enrichElement(element: ElementModel, sInst: ElementBinding) {
if (!element) {
return;
}
if (sInst.label) {
const label = getLocalizedString(sInst.label);
if (
element.label.values.every((value) => !isLocalizedEqual(value, label))
) {
element.label.values.push(label);
}
}
if (
sInst.class &&
element.types.indexOf(sInst.class.value as ElementTypeIri) < 0
) {
element.types.push(sInst.class.value as ElementTypeIri);
}
if (sInst.propType && sInst.propType.value !== LABEL_URI) {
mergeProperties(element.properties, sInst.propType, sInst.propValue);
}
}
function appendLabel(
container: { values: LocalizedString[] },
newLabel: LocalizedString | undefined
) {
if (!newLabel) {
return;
}
for (const existing of container.values) {
if (isLocalizedEqual(existing, newLabel)) {
return;
}
}
container.values.push(newLabel);
}
function isLocalizedEqual(left: LocalizedString, right: LocalizedString) {
return left.language === right.language && left.value === right.value;
}
export function getLocalizedString(
label: RdfLiteral
): LocalizedString | undefined {
if (label) {
return {
value: label.value,
language: label["xml:lang"],
datatype: label.datatype ? { value: label.datatype } : undefined,
};
} else {
return undefined;
}
}
export function getInstCount(instcount: RdfLiteral): number | undefined {
return instcount ? +instcount.value : undefined;
}
export function getPropertyModel(node: PropertyBinding): PropertyModel {
const label = getLocalizedString(node.label);
return {
id: node.property.value as PropertyTypeIri,
label: { values: label ? [label] : [] },
};
}
export function getLinkCount(sLinkType: LinkCountBinding): LinkCount {
return {
id: sLinkType.link.value as LinkTypeIri,
inCount: getInstCount(sLinkType.inCount),
outCount: getInstCount(sLinkType.outCount),
};
}
export function emptyElementInfo(id: ElementIri): ElementModel {
const elementInfo: ElementModel = {
id: id,
label: { values: [] },
types: [],
properties: {},
};
return elementInfo;
}
export function getLinkTypeInfo(sLinkInfo: LinkTypeBinding): LinkType {
if (!sLinkInfo) {
return undefined;
}
const label = getLocalizedString(sLinkInfo.label);
return {
id: sLinkInfo.link.value as LinkTypeIri,
label: { values: label ? [label] : [] },
count: getInstCount(sLinkInfo.instcount),
};
}
export function prependAdditionalBindings<Binding>(
base: SparqlResponse<Binding>,
additional: SparqlResponse<Binding> | undefined
): SparqlResponse<Binding> {
if (!additional) {
return base;
}
return {
head: { vars: base.head.vars },
results: {
bindings: [...additional.results.bindings, ...base.results.bindings],
},
};
}