UNPKG

graph-explorer

Version:

Graph Explorer can be used to explore and RDF graphs in SPARQL endpoints or on the web.

660 lines (597 loc) 19.4 kB
import { Dictionary } from "../model"; import { FilterParams } from "../provider"; import { getUriLocalName } from "../utils"; import { SparqlDataProviderSettings } from "./sparqlDataProviderSettings"; import { ElementBinding, LinkBinding, BlankBinding, FilterBinding, LinkCountBinding, ElementTypeBinding, SparqlResponse, RdfLiteral, isRdfIri, isRdfBlank, isBlankBinding, } from "./sparqlModels"; export const MAX_RECURSION_DEEP = 3; export const ENCODED_PREFIX = "sparql-blank:"; export const BLANK_NODE_QUERY_PARAMETERS = "?blankTrgProp ?blankTrg ?blankSrc ?blankSrcProp ?listHead"; export const BLANK_NODE_QUERY = ` OPTIONAL { FILTER (ISBLANK(?inst)). { ?inst ?blankTrgProp ?blankTrg. ?blankSrc ?blankSrcProp ?inst. FILTER NOT EXISTS { ?inst rdf:first _:smth1 }. BIND("blankNode" as ?blankType) } UNION { ?inst rdf:rest*/rdf:first ?blankTrg. ?blankSrc ?blankSrcProp ?inst. _:smth2 rdf:first ?blankTrg. BIND(?blankSrcProp as ?blankTrgProp) BIND("listHead" as ?blankType) FILTER NOT EXISTS { _:smth3 rdf:rest ?inst }. } UNION { ?listHead rdf:rest* ?inst. FILTER NOT EXISTS { _:smth4 rdf:rest ?listHead }. ?listHead rdf:rest*/rdf:first ?blankTrg. ?blankSrc ?blankSrcProp ?listHead. _:smth5 rdf:first ?blankTrg. BIND(?blankSrcProp as ?blankTrgProp) BIND("listHead" as ?blankType) } } `; export function isEncodedBlank(id: string): boolean { return id.startsWith(ENCODED_PREFIX); } export class QueryExecutor { queryDictionary: Dictionary<Promise<SparqlResponse<BlankBinding>>> = {}; constructor( public queryFunction: ( query: string ) => Promise<SparqlResponse<BlankBinding>> ) {} executeQuery(query: string): Promise<SparqlResponse<BlankBinding>> { const execution = this.queryDictionary[query]; if (execution) { return execution; } else { this.queryDictionary[query] = this.queryFunction(query).then( (response) => { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete this.queryDictionary[query]; return response; } ); return this.queryDictionary[query]; } } } export function updateFilterResults( result: SparqlResponse<ElementBinding & FilterBinding>, queryFunction: (query: string) => Promise<SparqlResponse<BlankBinding>>, settings: SparqlDataProviderSettings ): Promise<SparqlResponse<ElementBinding & FilterBinding>> { const completeBindings: (ElementBinding & FilterBinding)[] = []; const blankBindings: (BlankBinding & FilterBinding)[] = []; for (const binding of result.results.bindings) { if (isBlankBinding(binding)) { blankBindings.push(binding); } else { completeBindings.push(binding); } } return processBlankBindings( blankBindings, (callBackQuery: string) => { return queryFunction(callBackQuery); }, settings ).then((processedBindings) => { result.results.bindings = completeBindings.concat(processedBindings); return result; }); } export function processBlankBindings( blankBindings: BlankBinding[], queryFunction: (query: string) => Promise<SparqlResponse<BlankBinding>>, settings: SparqlDataProviderSettings ): Promise<BlankBinding[]> { const bindingGroupsById: Dictionary<BlankBinding[]> = {}; for (const binding of blankBindings) { if (binding.newInst) { binding.inst = binding.newInst; } if (!bindingGroupsById[binding.inst.value]) { bindingGroupsById[binding.inst.value] = []; } bindingGroupsById[binding.inst.value].push(binding); } const relatedBlankBindnings: BlankBinding[][] = []; for (const b of blankBindings) { if (isRdfBlank(b.blankTrg)) { relatedBlankBindnings.push([b]); } } const queryExecutor = new QueryExecutor(queryFunction); return loadRelatedBlankNodes( relatedBlankBindnings, queryExecutor, settings ).then((loadedGroupsById) => { const idsMap = getEncodedIdDictionary(loadedGroupsById); const groups = Object.keys(bindingGroupsById).map( (key) => bindingGroupsById[key] ); for (const group of groups) { for (const blankBinding of group) { if (!blankBinding.label) { blankBinding.label = createLabelForBlankBinding(blankBinding); } const encodedId4LoadedElement = idsMap[blankBinding.blankTrg.value]; if (encodedId4LoadedElement) { blankBinding.blankTrg.value = encodedId4LoadedElement; } } const encodedId = encodeId(group); updateGroupIds(group, encodedId); } return blankBindings; }); } function getEncodedIdDictionary( blankBindingGroups: Dictionary<BlankBinding[]> ): Dictionary<string> { const idDictionary: Dictionary<string> = {}; const keys = Object.keys(blankBindingGroups); for (const key of keys) { idDictionary[key] = encodeId(blankBindingGroups[key]); updateGroupIds(blankBindingGroups[key], idDictionary[key]); } return idDictionary; } function updateGroupIds(group: BlankBinding[], newId: string) { for (const loadedBlankBinding of group) { loadedBlankBinding.inst.value = newId; } } export function encodeId(blankBindings: BlankBinding[]): string { const bindingSet: Dictionary<BlankBinding> = {}; for (const binding of blankBindings) { // leave out instance unique ID const { inst: _inst, ...exceptInst } = binding; const encodedBinding = JSON.stringify(exceptInst); bindingSet[encodedBinding] = exceptInst as BlankBinding; } const normalizedBindings = Object.keys(bindingSet) .sort() .map((key) => bindingSet[key]); return ENCODED_PREFIX + encodeURI(JSON.stringify(normalizedBindings)); } export function decodeId(id: string): BlankBinding[] { if (!isEncodedBlank(id)) { return undefined; } try { const clearId = id.substring(ENCODED_PREFIX.length, id.length); const parsedBindings: BlankBinding[] = JSON.parse(decodeURI(clearId)); const bindings = parsedBindings.map((binding) => { // restore instance unique ID binding.inst = { type: "uri", value: id }; return binding; }); return bindings; } catch (_error) { /* silent */ return undefined; } } export function createLabelForBlankBinding(bn: BlankBinding): RdfLiteral { if (bn.blankType.value === "listHead") { return { type: "literal", value: "RDFList", "xml:lang": "", }; } else { return { type: "literal", value: bn.class ? getUriLocalName(bn.class.value) || bn.class.value : "anonymous", "xml:lang": "", }; } } function loadRelatedBlankNodes( blankChains: BlankBinding[][], queryExecutor: QueryExecutor, settings: SparqlDataProviderSettings, recursionDeep?: number ): Promise<Dictionary<BlankBinding[]>> { recursionDeep = recursionDeep || 1; if (recursionDeep > MAX_RECURSION_DEEP) { return Promise.resolve({}); } const queryPairs = blankChains.map((chain) => ({ query: getQueryForChain(chain, settings), chain: chain, })); const promises = queryPairs.map((pair) => queryExecutor.executeQuery(pair.query).then((response) => ({ response: response, chain: pair.chain, })) ); return Promise.all(promises).then((results) => { const recursionPromises: Promise<boolean>[] = []; const loadedBlankBindings: Dictionary<BlankBinding[]> = {}; for (const result of results) { const bindings = result.response.results.bindings; if (bindings.length > 0) { const relatedBlankBindings: BlankBinding[][] = []; for (const binding of bindings) { if (isRdfBlank(binding.blankTrg)) { relatedBlankBindings.push(result.chain.concat([binding])); } } recursionPromises.push( loadRelatedBlankNodes( relatedBlankBindings, queryExecutor, settings, recursionDeep + 1 ).then((loadedGroupsById) => { const idsMap = getEncodedIdDictionary(loadedGroupsById); const mergedResults: Dictionary<BlankBinding[]> = {}; for (const binding of bindings) { binding.label = createLabelForBlankBinding(binding); const encodedId = idsMap[binding.blankTrg.value]; if (encodedId) { binding.blankTrg.value = encodedId; } if (!mergedResults[binding.inst.value]) { mergedResults[binding.inst.value] = []; } mergedResults[binding.inst.value].push(binding); } Object.keys(mergedResults).forEach((key) => { const group = mergedResults[key]; const originalId = group[0].inst.value; loadedBlankBindings[originalId] = group; }); return true; }) ); } } return Promise.all(recursionPromises).then(() => { return loadedBlankBindings; }); }); } function getQueryForChain( blankNodes: BlankBinding[], sparqlDataProviderSettings: SparqlDataProviderSettings ): string { function getQueryBlock( blankNode: BlankBinding, index: number, maxIndex: number ) { // if blankNode has type 'listHead' then his target and targetProperty is artificial, // and we can't include this id in chain const trustableTrgProp = index === 0 || blankNode.blankType.value !== "listHead"; const sourceId = index > 0 ? "?inst" + (index - 1) : "<" + blankNode.blankSrc.value + ">"; const sourcePropId = trustableTrgProp ? index > 0 ? "?blankTrgProp" + (index - 1) : "<" + blankNode.blankSrcProp.value + ">" : "?anyType" + index; const instPostfix = index === maxIndex ? "" : index.toString(); const targetPropId = trustableTrgProp ? "<" + blankNode.blankTrgProp.value + ">" : "?anyType0" + index; const firstRelation = index === 0 && blankNode.blankType.value === "listHead" ? ` ?blankSrc${index} rdf:rest*/rdf:first ?inst${instPostfix}. ` : `?blankSrc${index} ${targetPropId} ?inst${instPostfix}.`; return ` # ====================== ${sourceId} ${sourcePropId} ?blankSrc${index}. ${firstRelation} BIND (<${blankNode.blankTrgProp.value}> as ?blankSrcProp${index}). FILTER (ISBLANK(?inst${instPostfix})). { ?inst${instPostfix} ?blankTrgProp${instPostfix} ?blankTrg${instPostfix}. BIND("blankNode" as ?blankType${instPostfix}). FILTER NOT EXISTS { ?inst${instPostfix} rdf:first _:smth1${index} }. } UNION { ?inst${instPostfix} rdf:rest*/rdf:first ?blankTrg${instPostfix}. ?blankSrc${index} ?blankSrcProp${index} ?inst${instPostfix}. _:smth2${index} rdf:first ?blankTrg${instPostfix}. BIND(?blankSrcProp${index} as ?blankTrgProp${instPostfix}) BIND("listHead" as ?blankType${instPostfix}) FILTER NOT EXISTS { _:smth3${index} rdf:rest ?inst${instPostfix} }. } OPTIONAL { ?inst${instPostfix} rdf:type ?class${instPostfix}. } `; } const body = blankNodes .map((bn, index) => getQueryBlock(bn, index, blankNodes.length - 1)) .join("\n"); const query = `${sparqlDataProviderSettings.defaultPrefix} SELECT ?inst ?class ?label ?blankTrgProp ?blankTrg ?blankType WHERE { ${body} } `; return query; } export function elementInfo( elementIds: string[] ): SparqlResponse<ElementBinding> { const ids = elementIds.filter((id) => isEncodedBlank(id)); return { head: undefined, results: { bindings: getElementBindings(ids) }, }; } export function linksInfo(elementIds: string[]): SparqlResponse<LinkBinding> { return { head: undefined, results: { bindings: getLinkBinding(elementIds) }, }; } export function linkTypesOf(params: { elementId: string; }): SparqlResponse<LinkCountBinding> { return { head: undefined, results: { bindings: getLinkCountBinding(params.elementId) }, }; } export function filter(params: FilterParams): SparqlResponse<ElementBinding> { const filterResponse: SparqlResponse<ElementBinding> = { head: undefined, results: { bindings: [] }, }; if (params.limit === 0) { params.limit = 100; } if (params.elementTypeId) { filterResponse.results.bindings = []; } else if (params.refElementId && params.refElementLinkId) { filterResponse.results.bindings = getAllRelatedByLinkTypeElements( params.refElementId, params.refElementLinkId, params.linkDirection ); } else if (params.refElementId) { filterResponse.results.bindings = getAllRelatedElements( params.refElementId ); } if (params.text && filterResponse.results.bindings.length !== 0) { filterResponse.results.bindings = filterResponse.results.bindings.filter( (be) => be.inst.value.toLowerCase().indexOf(params.text) !== -1 ); } return filterResponse; } export function getElementTypes( elementIds: readonly string[] ): SparqlResponse<ElementTypeBinding> { const bindings: ElementTypeBinding[] = []; for (const id of elementIds) { const blankBindings = decodeId(id); if (blankBindings) { for (const be of blankBindings) { if (isRdfIri(be.inst) && be.class) { bindings.push({ inst: be.inst, class: be.class }); } } } } return { head: undefined, results: { bindings } }; } function getAllRelatedByLinkTypeElements( refElementId: string, refElementLinkId: string, linkDirection: string ): ElementBinding[] { const blankElements = (decodeId(refElementId) || []).concat( decodeId(refElementLinkId) || [] ); let bindings: ElementBinding[] = []; if (blankElements.length > 0) { for (const be of blankElements) { if (linkDirection === "in") { if ( be.inst.value === refElementId && (isRdfIri(be.blankSrc) || isRdfBlank(be.blankSrc)) && refElementLinkId === be.blankSrcProp.value ) { if (isRdfIri(be.blankSrc)) { bindings.push({ inst: be.blankSrc, }); } else { bindings = bindings.concat( decodeId(be.blankSrc.value) || [{ inst: be.blankSrc }] ); } } else if ( be.blankTrg.value === refElementId && refElementLinkId === be.blankTrgProp.value ) { bindings.push(be); } } else { if ( be.inst.value === refElementId && (isRdfIri(be.blankTrg) || isRdfBlank(be.blankTrg)) && refElementLinkId === be.blankTrgProp.value ) { if (isRdfIri(be.blankTrg)) { bindings.push({ inst: be.blankTrg, }); } else { bindings = bindings.concat( decodeId(be.blankTrg.value) || [{ inst: be.blankTrg }] ); } } else if ( be.blankSrc.value === refElementId && refElementLinkId === be.blankSrcProp.value ) { bindings.push(be); } } } } return bindings; } function getAllRelatedElements(id: string): ElementBinding[] { const blankElements = decodeId(id); let bindings: ElementBinding[] = []; if (blankElements) { for (const be of blankElements) { if ( be.inst.value === id || id === be.blankSrc.value || id === be.blankTrg.value ) { bindings.push(be); if (isRdfIri(be.blankSrc)) { bindings.push({ inst: be.blankSrc }); } else if (isRdfBlank(be.blankSrc)) { bindings = bindings.concat( decodeId(be.blankSrc.value) || [{ inst: be.blankSrc }] ); } if (isRdfIri(be.blankTrg)) { bindings.push({ inst: be.blankTrg }); } else if (isRdfBlank(be.blankTrg)) { bindings = bindings.concat( decodeId(be.blankTrg.value) || [{ inst: be.blankTrg }] ); } } } } return bindings; } function getElementBindings(ids: string[]): ElementBinding[] { let blankElements: BlankBinding[] = []; for (const id of ids) { const blankBindings = decodeId(id); if (blankBindings) { blankElements = blankElements.concat(decodeId(id)); } } return blankElements.filter((be) => { return ids.indexOf(be.inst.value) !== -1; }); } function getLinkBinding(ids: string[]): LinkBinding[] { let blankElements: BlankBinding[] = []; for (const id of ids) { const blankBindings = decodeId(id); if (blankBindings) { blankElements = blankElements.concat(decodeId(id)); } } const bindings: LinkBinding[] = []; for (const be of blankElements) { if (ids.indexOf(be.inst.value) !== -1) { if ( (isRdfIri(be.blankSrc) || isRdfBlank(be.blankSrc)) && isRdfIri(be.blankSrcProp) && ids.indexOf(be.blankSrc.value) !== -1 ) { bindings.push({ source: be.blankSrc, type: be.blankSrcProp, target: be.inst, }); } if ( (isRdfIri(be.blankTrg) || isRdfBlank(be.blankTrg)) && isRdfIri(be.blankTrgProp) && ids.indexOf(be.blankTrg.value) !== -1 ) { bindings.push({ source: be.inst, type: be.blankTrgProp, target: be.blankTrg, }); } } } return bindings; } function getLinkCountBinding(id: string): LinkCountBinding[] { const blankElements = decodeId(id); const dictionary: Dictionary<LinkCountBinding> = {}; for (const be of blankElements) { if (id === be.inst.value) { if ( (isRdfIri(be.blankTrg) || isRdfBlank(be.blankTrg)) && isRdfIri(be.blankTrgProp) ) { if ( (isRdfIri(be.blankSrc) || isRdfBlank(be.blankSrc)) && isRdfIri(be.blankSrcProp) ) { if (!dictionary[be.blankSrcProp.value]) { dictionary[be.blankSrcProp.value] = { link: be.blankSrcProp, inCount: { type: "literal", value: "1", "xml:lang": "", }, outCount: { type: "literal", value: "0", "xml:lang": "", }, }; } } if (!dictionary[be.blankTrgProp.value]) { dictionary[be.blankTrgProp.value] = { link: be.blankTrgProp, inCount: { type: "literal", value: "0", "xml:lang": "", }, outCount: { type: "literal", value: "1", "xml:lang": "", }, }; } else { dictionary[be.blankTrgProp.value].outCount.value = ( +dictionary[be.blankTrgProp.value].outCount.value + 1 ).toString(); } } } } return Object.keys(dictionary).map((k) => dictionary[k]); }