Skip to content

Commit

Permalink
support for loading RDF with owl:imports
Browse files Browse the repository at this point in the history
  • Loading branch information
s-tittel committed Jul 28, 2023
1 parent 235527d commit 92b6978
Show file tree
Hide file tree
Showing 11 changed files with 1,776 additions and 1,576 deletions.
3,087 changes: 1,571 additions & 1,516 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@
"webpack-dev-server": "^4.11.1"
},
"dependencies": {
"n3": "^1.16.2",
"rdf-ext": "^2.1.0",
"n3": "^1.17.0",
"rdf-ext": "^2.2.0",
"rdf-validate-shacl": "^0.4.5",
"uuid": "^9.0.0"
}
Expand Down
34 changes: 11 additions & 23 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Store, Parser, StreamParser } from 'n3'
import { Store } from 'n3'
import { Term } from '@rdfjs/types'
import { PREFIX_SHACL, PREFIX_RDF } from './prefixes'
import { DefaultTheme, Theme } from './theme'
import { PREFIX_RDF, PREFIX_SHACL } from './prefixes'
import { SHAPES_GRAPH } from './util'

export class Config {
static abortController = new AbortController()

shapes: string | null = null
shapesUrl: string | null = null
Expand All @@ -16,10 +16,10 @@ export class Config {
addEmptyElementToLists: string | null = null

private _theme: Theme = new DefaultTheme()
private _shapesGraph: Store = new Store()
private _valuesGraph: Store = new Store()
private _lists: Record<string, Term[]> = {}
private _groups: Array<string> = []
private _graph: Store = new Store()
private _valuesGraph: Store = new Store()

equals(other: Config): boolean {
if (!other) {
Expand All @@ -33,27 +33,15 @@ export class Config {
return true
}

async loadGraphs() {
Config.abortController.abort()
Config.abortController = new AbortController()
this.shapesGraph = new Store(new Parser().parse(this.shapes ? this.shapes : this.shapesUrl ? await fetch(this.shapesUrl, { signal: Config.abortController.signal }).then(resp => resp.text()) : ''))
this.valuesGraph = new Store(new Parser({
blankNodePrefix: ''
}).parse(this.values ? this.values : this.valuesUrl ? await fetch(this.valuesUrl, { signal: Config.abortController.signal }).then(resp => resp.text()) : ''))

// this.shapesGraph.addQuads(new Parser().parse(await fetch('https://nfdi4ing.pages.rwth-aachen.de/metadata4ing/metadata4ing/ontology.ttl', { signal: Config.abortController.signal }).then(resp => resp.text())))
// this.shapesGraph.addQuads(new Parser().parse(await fetch('m4i.ttl').then(resp => resp.text())))
}

get shapesGraph() {
return this._shapesGraph
get graph() {
return this._graph
}

set shapesGraph(graph: Store) {
this._shapesGraph = graph
this._lists = this._shapesGraph.extractLists()
set graph(graph: Store) {
this._graph = graph
this._lists = graph.extractLists()
this._groups = []
this._shapesGraph.getQuads(null, `${PREFIX_RDF}type`, `${PREFIX_SHACL}PropertyGroup`, null).forEach(groupQuad => {
graph.getQuads(null, `${PREFIX_RDF}type`, `${PREFIX_SHACL}PropertyGroup`, SHAPES_GRAPH).forEach(groupQuad => {
this._groups.push(groupQuad.subject.value)
})
}
Expand Down
35 changes: 24 additions & 11 deletions src/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ import { Config } from './config'
import { Plugin, Plugins } from './plugin'
import { Writer, Quad, Store, DataFactory, NamedNode } from 'n3'
import { DEFAULT_PREFIXES, PREFIX_RDF, PREFIX_SHACL } from './prefixes'
import { focusFirstInputElement } from './util'
import { SHAPES_GRAPH, focusFirstInputElement } from './util'
import SHACLValidator from 'rdf-validate-shacl'
import factory from 'rdf-ext'
import './styles.css'
import { Loader } from './loader'

export class ShaclForm extends HTMLElement {
static get observedAttributes() { return Config.keysAsDataAttributes }

config: Config = new Config()
loader: Loader = new Loader()
shape: ShaclNode | null = null
form: HTMLFormElement
plugins: Plugins = {}
Expand Down Expand Up @@ -45,7 +47,7 @@ export class ShaclForm extends HTMLElement {
private initialize() {
clearTimeout(this.initDebounceTimeout)
this.initDebounceTimeout = setTimeout(() => {
this.config.loadGraphs().then(_ => {
this.loader.loadGraphs(this.config).then(_ => {
if (this.form.contains(this.shape)) {
this.form.removeChild(this.shape as ShaclNode)
}
Expand Down Expand Up @@ -99,13 +101,24 @@ export class ShaclForm extends HTMLElement {
elem.remove()
}

const shapes = factory.dataset(this.config.shapesGraph)
const validator = new SHACLValidator(shapes, { factory })
const report = await validator.validate(factory.dataset(this.toRDF()))
const validator = new SHACLValidator(factory.dataset(this.config.graph), { factory })
this.config.graph.deleteGraph("")
this.config.graph.addQuads(this.toRDF())
const report = await validator.validate(factory.dataset(this.config.graph))

// for (const result of report.results) {
// // See https://www.w3.org/TR/shacl/#results-validation-result for details
// // about each property
// console.log(result.message)
// console.log(result.path)
// console.log(result.focusNode)
// console.log(result.severity)
// console.log(result.sourceConstraintComponent)
// console.log(result.sourceShape)
// }

if (showHints) {
for (const result of report.results) {
// See https://www.w3.org/TR/shacl/#results-validation-result for details about each property
const invalidElement = this.querySelector(`:scope [data-node-id='${result.focusNode.id}'] [data-path='${result.path.id}']`)
const messageElement = document.createElement('pre')
messageElement.classList.add('validation')
Expand All @@ -128,7 +141,7 @@ export class ShaclForm extends HTMLElement {
// if data-shape-subject is set, use that
if (this.config.shapeSubject) {
rootShapeShaclSubject = new NamedNode(this.config.shapeSubject)
if (!this.config.shapesGraph.has(DataFactory.triple(rootShapeShaclSubject, new NamedNode(`${PREFIX_RDF}type`), new NamedNode(`${PREFIX_SHACL}NodeShape`)))) {
if (!this.config.graph.has(new Quad(rootShapeShaclSubject, new NamedNode(`${PREFIX_RDF}type`), new NamedNode(`${PREFIX_SHACL}NodeShape`), SHAPES_GRAPH))) {
console.warn(`shapes graph does not contain requested root shape ${this.config.shapeSubject}`)
return
}
Expand All @@ -144,21 +157,21 @@ export class ShaclForm extends HTMLElement {
}
// if type refers to a node shape, prioritize that over targetClass resolution
for (const rootValueSubjectType of rootValueSubjectTypes) {
if (this.config.shapesGraph.has(DataFactory.triple(rootValueSubjectType.object as NamedNode, new NamedNode(`${PREFIX_RDF}type`), new NamedNode(`${PREFIX_SHACL}NodeShape`)))) {
if (this.config.graph.has(new Quad(rootValueSubjectType.object as NamedNode, new NamedNode(`${PREFIX_RDF}type`), new NamedNode(`${PREFIX_SHACL}NodeShape`), SHAPES_GRAPH))) {
rootShapeShaclSubject = rootValueSubjectType.object as NamedNode
break
}
}
if (!rootShapeShaclSubject) {
const rootShapes = this.config.shapesGraph.getQuads(null, `${PREFIX_SHACL}targetClass`, rootValueSubjectTypes[0].object, null)
const rootShapes = this.config.graph.getQuads(null, `${PREFIX_SHACL}targetClass`, rootValueSubjectTypes[0].object, SHAPES_GRAPH)
if (rootShapes.length === 0) {
console.warn(`value subject '${this.config.valueSubject}' has no shacl shape definition in the shapes graph`)
return
}
if (rootShapes.length > 1) {
console.warn(`value subject '${this.config.valueSubject}' has multiple shacl shape definitions in the shapes graph, choosing the first found (${rootShapes[0].subject})`)
}
if (this.config.shapesGraph.getQuads(rootShapes[0].subject, `${PREFIX_RDF}type`, `${PREFIX_SHACL}NodeShape`, null).length === 0) {
if (this.config.graph.getQuads(rootShapes[0].subject, `${PREFIX_RDF}type`, `${PREFIX_SHACL}NodeShape`, SHAPES_GRAPH).length === 0) {
console.error(`value subject '${this.config.valueSubject}' references a shape which is not a NodeShape (${rootShapes[0].subject})`)
return
}
Expand All @@ -167,7 +180,7 @@ export class ShaclForm extends HTMLElement {
}
else {
// choose first of all defined root shapes
const rootShapes = this.config.shapesGraph.getQuads(null, `${PREFIX_RDF}type`, `${PREFIX_SHACL}NodeShape`, null)
const rootShapes = this.config.graph.getQuads(null, `${PREFIX_RDF}type`, `${PREFIX_SHACL}NodeShape`, SHAPES_GRAPH)
if (rootShapes.length == 0) {
console.warn('shapes graph does not contain any root shapes')
return
Expand Down
4 changes: 2 additions & 2 deletions src/group.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { PREFIX_RDFS } from './prefixes'
import { Config } from './config'
import { findObjectValueByPredicate } from './util'
import { SHAPES_GRAPH, findObjectValueByPredicate } from './util'

export class ShaclGroup extends HTMLElement {
constructor(groupSubject: string, config: Config) {
Expand All @@ -9,7 +9,7 @@ export class ShaclGroup extends HTMLElement {
this.dataset['subject'] = groupSubject

let name = groupSubject
const quads = config.shapesGraph.getQuads(groupSubject, null, null, null)
const quads = config.graph.getQuads(groupSubject, null, null, SHAPES_GRAPH)
const label = findObjectValueByPredicate(quads, "label", PREFIX_RDFS, config.language)
if (label) {
name = label
Expand Down
12 changes: 6 additions & 6 deletions src/inputs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Quad, DataFactory, NamedNode, Store } from 'n3'
import { Quad, DataFactory, NamedNode } from 'n3'
import { Term, Literal } from '@rdfjs/types'
import { PREFIX_SHACL, PREFIX_XSD, PREFIX_DASH, PREFIX_RDFS } from './prefixes'
import { findObjectValueByPredicate, findObjectByPredicate } from './util'
import { findObjectValueByPredicate, findObjectByPredicate, SHAPES_GRAPH } from './util'
import { Config } from './config'

export type Editor = HTMLElement & { value: string }
Expand Down Expand Up @@ -181,7 +181,7 @@ export class InputNumber extends InputBase {
}
}

export type InputListEntry = Term | { label?: string, value: string }
export type InputListEntry = Term | { value: string, label?: string }
const isTerm = (o: any): o is Term => {
return o && typeof o.termType === "string";
}
Expand All @@ -207,16 +207,16 @@ export class InputList extends InputBase {
label = item.label ? item.label : null
}
const option = document.createElement('option')
option.innerHTML = label ? label : item.value
option.innerHTML = label ? label : item.value.toString()
if (label && item.value) {
option.value = item.value
option.value = item.value.toString()
}
select.options.add(option)
}
}

findLabel(subject: NamedNode, config: Config): string | null {
const quads = config.shapesGraph.getQuads(subject, null, null, null)
const quads = config.graph.getQuads(subject, null, null, SHAPES_GRAPH)
return findObjectValueByPredicate(quads, 'label', PREFIX_RDFS, config.language)
}

Expand Down
118 changes: 118 additions & 0 deletions src/loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { Store, Parser, Quad, Prefixes, NamedNode } from 'n3'
import { OWL_IMPORTS, SHAPES_GRAPH } from './util'
import { Config } from './config'

export class Loader {
private abortController: AbortController | null = null

async loadGraphs(config: Config) {
if (this.abortController) {
this.abortController.abort()
}
this.abortController = new AbortController()

const graph = new Store()
const valuesGraph = new Store()

await Promise.all([
this.importRDF(config.shapes ? config.shapes : config.shapesUrl ? this.fetchRDF(config.shapesUrl) : '', graph, SHAPES_GRAPH),
this.importRDF(config.values ? config.values : config.valuesUrl ? this.fetchRDF(config.valuesUrl) : '', valuesGraph, undefined, new Parser({ blankNodePrefix: '' })),
])

config.graph = graph
config.valuesGraph = valuesGraph
}

async importRDF(input: string | Promise<string>, store: Store, graph?: NamedNode, parser?: Parser): Promise<null> {
const p = parser ? parser : new Parser()
const parse = (text: string, resolve, reject) => {
const owlImports: Array<string> = []
p.parse(text, (error: Error, quad: Quad, prefixes: Prefixes) => {
if (error) {
reject(error)
}
else {
if (quad) {
store.add(new Quad(quad.subject, quad.predicate, quad.object, graph))
// see if this is an owl:imports
if (OWL_IMPORTS.equals(quad.predicate)) {
owlImports.push(quad.object.value)
}
}
else {
if (prefixes) {
for (const owlImport of owlImports) {
const url = this.toURL(owlImport, prefixes)
if (url) {
this.fetchRDF(url).then(text => {
this.importRDF(text, store, graph, parser)
}).catch(e => {
console.log(e)
})
}
}
}
resolve(null)
}
}
}, undefined)
}
return new Promise<null>((resolve, reject) => {
if (input instanceof Promise) {
input.then(text => parse(text, resolve, reject)).catch(e => {
reject(e)
})
}
else {
parse(input, resolve, reject)
}
})
}

fetchRDF(url: string, accept = 'text/turtle'): Promise<string> {
return fetch(url, {
headers: {
'Accept': accept
},
signal: this.abortController?.signal
}).then(resp => {
if (resp.ok) {
return resp.text()
}
else {
throw new Error('failed loading ' + url)
}
}).catch(e => {
throw new Error('failed loading ' + url + ', reason:' + e)
})
}

toURL(id: string, prefixes: Prefixes): string | null {
if (this.isURL(id)) {
return id
}
const splitted = id.split(':')
if (splitted.length === 2) {
const prefix = prefixes[splitted[0]]
if (prefix) {
// need to ignore type check. 'prefix' is a string and not a NamedNode<string> (seems to be a bug in n3 typings)
// @ts-ignore
id = id.replace(`${splitted[0]}:`, prefix)
if (this.isURL(id)) {
return id
}
}
}
return null
}

isURL(input: string): boolean {
let url: URL
try {
url = new URL(input)
} catch (_) {
return false
}
return url.protocol === 'http:' || url.protocol === 'https:'
}
}
10 changes: 5 additions & 5 deletions src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { BlankNode, NamedNode, Store } from 'n3'
import { PREFIX_SHACL, PREFIX_RDF, PREFIX_RDFS } from './prefixes'
import { ShaclProperty } from './property'
import { ShaclForm } from './form'
import { findObjectValueByPredicate } from './util'
import { SHAPES_GRAPH, findObjectValueByPredicate } from './util'
import { ShaclGroup } from './group'
import { v4 as uuidv4 } from 'uuid'

Expand All @@ -19,7 +19,7 @@ export class ShaclNode extends HTMLElement {
this.form = form
this.parent = parent
this.shaclSubject = shaclSubject
const quads = form.config.shapesGraph.getQuads(shaclSubject, null, null, null)
const quads = form.config.graph.getQuads(shaclSubject, null, null, SHAPES_GRAPH)
const targetClass = findObjectValueByPredicate(quads, 'targetClass')
if (targetClass) {
this.targetClass = new NamedNode(targetClass)
Expand All @@ -32,7 +32,7 @@ export class ShaclNode extends HTMLElement {
}
this.dataset.nodeId = this.exportValueSubject.id

const shaclProperties = form.config.shapesGraph.getQuads(shaclSubject, `${PREFIX_SHACL}property`, null, null)
const shaclProperties = form.config.graph.getQuads(shaclSubject, `${PREFIX_SHACL}property`, null, SHAPES_GRAPH)
if (shaclProperties.length == 0) {
console.warn('node shape', shaclSubject, 'has no shacl properties')
}
Expand Down Expand Up @@ -63,7 +63,7 @@ export class ShaclNode extends HTMLElement {
}
}
// check shape inheritance via sh:node
const nodes = form.config.shapesGraph.getQuads(shaclSubject, `${PREFIX_SHACL}node`, null, null)
const nodes = form.config.graph.getQuads(shaclSubject, `${PREFIX_SHACL}node`, null, SHAPES_GRAPH)
for (const node of nodes) {
inheritedShapes.push(node.object as NamedNode)
}
Expand All @@ -76,7 +76,7 @@ export class ShaclNode extends HTMLElement {
if (shaclProperty.object instanceof NamedNode || shaclProperty.object instanceof BlankNode) {
let parent: HTMLElement = this
// check if property belongs to a group
const groupRef = form.config.shapesGraph.getQuads(shaclProperty.object, `${PREFIX_SHACL}group`, null, null)
const groupRef = form.config.graph.getQuads(shaclProperty.object, `${PREFIX_SHACL}group`, null, SHAPES_GRAPH)
if (groupRef.length > 0) {
const groupSubject = groupRef[0].object.value
if (form.config.groups.indexOf(groupSubject) > -1) {
Expand Down
Loading

0 comments on commit 92b6978

Please sign in to comment.