diff --git a/README.md b/README.md index 00cfa3f..7553d85 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ data‑ignore‑owl‑imports | By default, `owl:imports` URLs ar data-view | When set, turns the web component into a viewer that displays the given data graph without editing functionality data-collapse | When set, `sh:group`s and properties with `sh:node` and `sh:maxCount` != 1 are displayed in a collapsible accordion-like widget to reduce visual complexity of the form. The collapsible element is initially shown closed, except when this attribute's value is `"open"` data-submit-button | [Ignored when `data-view` attribute is set] Whether to add a submit button to the form. The value of this attribute is used as the button label. `submit` events get emitted only when the form data validates +data-generate-node-shape-reference | When generating the RDF data graph, <shacl-form> can create a triple that references the root `sh:NodeShape` of the data. Supported values of this attribute are `rdf:type` or `dcterms:conformsTo`. Default is empty, so that no such triple is created ### Element functions diff --git a/demo/complex-example.ttl b/demo/complex-example.ttl index 0a8fe40..b5cdfe2 100644 --- a/demo/complex-example.ttl +++ b/demo/complex-example.ttl @@ -120,7 +120,7 @@ example:Location sh:minCount 1 ; sh:name "Coordinates" ; sh:path geo:asWKT ; - sh:pattern "^POINT\\([+\\-]?(?:[0-9]*[.])?[0-9]+ [+\\-]?(?:[0-9]*[.])?[0-9]+\\)|POLYGON\\(\\((?:[+\\-]?(?:[0-9]*[.])?[0-9]+[ ,]?){3,}\\)\\)$" + sh:pattern "^POINT\\([+\\-]?(?:[0-9]*[.])?[0-9]+ [+\\-]?(?:[0-9]*[.])?[0-9]+\\)$|^POLYGON\\(\\((?:[+\\-]?(?:[0-9]*[.])?[0-9]+[ ,]?){3,}\\)\\)$" ] ; sh:property [ dash:singleLine false ; sh:description "Description of the location" ; diff --git a/demo/index.html b/demo/index.html index 6fb83f9..5fa58a1 100644 --- a/demo/index.html +++ b/demo/index.html @@ -6,12 +6,12 @@ <shacl-form> demo - + diff --git a/package.json b/package.json index 7295ae8..e1d377e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ulb-darmstadt/shacl-form", - "version": "1.4.9", + "version": "1.5.0", "description": "SHACL form generator", "main": "dist/form-default.js", "module": "dist/form-default.js", diff --git a/src/config.ts b/src/config.ts index 5e70a12..213c4f8 100644 --- a/src/config.ts +++ b/src/config.ts @@ -23,6 +23,7 @@ export class ElementAttributes { ignoreOwlImports: string | null = null collapse: string | null = null submitButton: string | null = null + generateNodeShapeReference: string | null = null } export class Config { diff --git a/src/constants.ts b/src/constants.ts index e0f6f5e..3697f2d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -8,11 +8,13 @@ export const PREFIX_RDFS = 'http://www.w3.org/2000/01/rdf-schema#' export const PREFIX_SKOS = 'http://www.w3.org/2004/02/skos/core#' export const PREFIX_OWL = 'http://www.w3.org/2002/07/owl#' export const PREFIX_OA = 'http://www.w3.org/ns/oa#' +export const PREFIX_DCTERMS = 'http://purl.org/dc/terms/' export const SHAPES_GRAPH = DataFactory.namedNode('shapes') export const OWL_IMPORTS = DataFactory.namedNode(PREFIX_OWL + 'imports') export const RDF_PREDICATE_TYPE = DataFactory.namedNode(PREFIX_RDF + 'type') +export const DCTERMS_PREDICATE_CONFORMS_TO = DataFactory.namedNode(PREFIX_DCTERMS + 'conformsTo') export const RDFS_PREDICATE_SUBCLASS_OF = DataFactory.namedNode(PREFIX_RDFS + 'subClassOf') export const SKOS_PREDICATE_BROADER = DataFactory.namedNode(PREFIX_SKOS + 'broader') export const OWL_OBJECT_NAMED_INDIVIDUAL = DataFactory.namedNode(PREFIX_OWL + 'NamedIndividual') diff --git a/src/form.ts b/src/form.ts index 401f2a6..8ece24b 100644 --- a/src/form.ts +++ b/src/form.ts @@ -2,7 +2,7 @@ import { ShaclNode } from './node' import { Config } from './config' import { ClassInstanceProvider, Plugin, listPlugins, registerPlugin } from './plugin' import { Quad, Store, NamedNode, DataFactory } from 'n3' -import { RDF_PREDICATE_TYPE, SHACL_OBJECT_NODE_SHAPE, SHACL_PREDICATE_TARGET_CLASS, SHAPES_GRAPH } from './constants' +import { DCTERMS_PREDICATE_CONFORMS_TO, RDF_PREDICATE_TYPE, SHACL_OBJECT_NODE_SHAPE, SHACL_PREDICATE_TARGET_CLASS, SHAPES_GRAPH } from './constants' import { Editor, Theme } from './theme' import { serialize } from './serialize' import SHACLValidator from 'rdf-validate-shacl' @@ -240,12 +240,15 @@ export class ShaclForm extends HTMLElement { // if we have a data graph and data-values-subject is set, use shape of that if (this.config.attributes.valuesSubject && this.config.dataGraph.size > 0) { const rootValueSubject = DataFactory.namedNode(this.config.attributes.valuesSubject) - const rootValueSubjectTypes = this.config.dataGraph.getQuads(rootValueSubject, RDF_PREDICATE_TYPE, null, null) + const rootValueSubjectTypes = [ + ...this.config.dataGraph.getQuads(rootValueSubject, RDF_PREDICATE_TYPE, null, null), + ...this.config.dataGraph.getQuads(rootValueSubject, DCTERMS_PREDICATE_CONFORMS_TO, null, null) + ] if (rootValueSubjectTypes.length === 0) { - console.warn(`value subject '${this.config.attributes.valuesSubject}' has no ${RDF_PREDICATE_TYPE.id} statement`) + console.warn(`value subject '${this.config.attributes.valuesSubject}' has neither ${RDF_PREDICATE_TYPE.id} nor ${DCTERMS_PREDICATE_CONFORMS_TO.id} statement`) return } - // if type refers to a node shape, prioritize that over targetClass resolution + // if type/conformsTo refers to a node shape, prioritize that over targetClass resolution for (const rootValueSubjectType of rootValueSubjectTypes) { if (this.config.shapesGraph.has(new Quad(rootValueSubjectType.object as NamedNode, RDF_PREDICATE_TYPE, SHACL_OBJECT_NODE_SHAPE, SHAPES_GRAPH))) { rootShapeShaclSubject = rootValueSubjectType.object as NamedNode diff --git a/src/loader.ts b/src/loader.ts index 0ae1da1..38039aa 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -1,6 +1,6 @@ import { Store, Parser, Quad, Prefixes, NamedNode } from 'n3' import { toRDF } from 'jsonld' -import { OWL_IMPORTS, SHACL_PREDICATE_CLASS, SHAPES_GRAPH } from './constants' +import { DCTERMS_PREDICATE_CONFORMS_TO, OWL_IMPORTS, RDF_PREDICATE_TYPE, SHACL_PREDICATE_CLASS, SHAPES_GRAPH } from './constants' import { Config } from './config' import { isURL } from './util' @@ -12,7 +12,7 @@ const loadedClassesCache: Record> = {} export class Loader { private config: Config - private loadedOwlImports: string[] = [] + private loadedExternalUrls: string[] = [] private loadedClasses: string[] = [] constructor(config: Config) { @@ -21,19 +21,42 @@ export class Loader { async loadGraphs() { // clear local caches - this.loadedOwlImports = [] + this.loadedExternalUrls = [] this.loadedClasses = [] - const store = new Store() + const shapesStore = new Store() const valuesStore = new Store() this.config.prefixes = {} await Promise.all([ - this.importRDF(this.config.attributes.shapes ? this.config.attributes.shapes : this.config.attributes.shapesUrl ? this.fetchRDF(this.config.attributes.shapesUrl) : '', store, SHAPES_GRAPH), + this.importRDF(this.config.attributes.shapes ? this.config.attributes.shapes : this.config.attributes.shapesUrl ? this.fetchRDF(this.config.attributes.shapesUrl) : '', shapesStore, SHAPES_GRAPH), this.importRDF(this.config.attributes.values ? this.config.attributes.values : this.config.attributes.valuesUrl ? this.fetchRDF(this.config.attributes.valuesUrl) : '', valuesStore, undefined, new Parser({ blankNodePrefix: '' })), ]) - this.config.shapesGraph = store + // if shapes graph is empty, but we have the following triples: + // a or dcterms:conformsTo + // then try to load the referenced object into the shapes graph + if (shapesStore.size == 0 && this.config.attributes.valuesSubject) { + const shapeCandidates = [ + ...valuesStore.getObjects(this.config.attributes.valuesSubject, RDF_PREDICATE_TYPE, null), + ...valuesStore.getObjects(this.config.attributes.valuesSubject, DCTERMS_PREDICATE_CONFORMS_TO, null) + ] + const promises: Promise[] = [] + for (const uri of shapeCandidates) { + const url = this.toURL(uri.value) + if (url && this.loadedExternalUrls.indexOf(url) < 0) { + this.loadedExternalUrls.push(url) + promises.push(this.importRDF(this.fetchRDF(url), shapesStore, SHAPES_GRAPH)) + } + } + try { + await Promise.all(promises) + } catch (e) { + console.warn(e) + } + } + + this.config.shapesGraph = shapesStore this.config.dataGraph = valuesStore } @@ -52,8 +75,8 @@ export class Loader { if (this.config.attributes.ignoreOwlImports === null && OWL_IMPORTS.equals(quad.predicate)) { const url = this.toURL(quad.object.value) // import url only once - if (url && this.loadedOwlImports.indexOf(url) < 0) { - this.loadedOwlImports.push(url) + if (url && this.loadedExternalUrls.indexOf(url) < 0) { + this.loadedExternalUrls.push(url) dependencies.push(this.importRDF(this.fetchRDF(url), store, graph, parser)) } } diff --git a/src/node.ts b/src/node.ts index 9a4e4dc..3ce1e83 100644 --- a/src/node.ts +++ b/src/node.ts @@ -1,6 +1,6 @@ import { BlankNode, DataFactory, NamedNode, Store } from 'n3' import { Term } from '@rdfjs/types' -import { PREFIX_SHACL, SHAPES_GRAPH, RDF_PREDICATE_TYPE } from './constants' +import { PREFIX_SHACL, SHAPES_GRAPH, RDF_PREDICATE_TYPE, DCTERMS_PREDICATE_CONFORMS_TO } from './constants' import { ShaclProperty } from './property' import { createShaclGroup } from './group' import { v4 as uuidv4 } from 'uuid' @@ -134,9 +134,13 @@ export class ShaclNode extends HTMLElement { if (this.targetClass) { graph.addQuad(subject, RDF_PREDICATE_TYPE, this.targetClass) } - // if this is the root shacl node, add the type predicate - if (!this.closest('shacl-node shacl-node')) { - graph.addQuad(subject, RDF_PREDICATE_TYPE, this.shaclSubject) + // if this is the root shacl node, check if we should add one of the rdf:type or dcterms:conformsTo predicates + if (this.config.attributes.generateNodeShapeReference && !this.closest('shacl-node shacl-node')) { + if (this.config.attributes.generateNodeShapeReference === 'rdf:type') { + graph.addQuad(subject, RDF_PREDICATE_TYPE, this.shaclSubject) + } else if (this.config.attributes.generateNodeShapeReference === 'dcterms:conformsTo') { + graph.addQuad(subject, DCTERMS_PREDICATE_CONFORMS_TO, this.shaclSubject) + } } return subject }