OWL 2 SHACL semi-closed ruleset
Turns an OWL Ontology into a set of SHACL Shapes. Additionnal Shapes with sh:targetSubjectOf verify the domains of the ontology properties, ensuring each property is asserted on an instance of the correct class. Properties from other ontologies can still be asserted anywhere as the NodeShapes are not closed
This is derived from original work by TopQuadrant at https://www.topquadrant.com/from-owl-to-shacl-in-an-automated-way/, with their authorization.
SELECT ?this WHERE { { { { ?type rdfs:subClassOf* rdfs:Class . ?this a ?type . } UNION { ?this a owl:Class . } } FILTER isIRI(?this) . } }
Copies any intersections within owl:equivalentClass into the host class itself so that subsequent rules convert them further.
Copy owl:intersectionOfs from owl:equivalentClass
CONSTRUCT { $this owl:intersectionOf ?inter . } WHERE { $this owl:equivalentClass ?equi . FILTER isBlank(?equi) . ?equi owl:intersectionOf ?inter . }
Copies the members of an owl:intersectionOf list as superclasses into the host class itself. Subsequent rules then apply.
CONSTRUCT { $this rdfs:subClassOf ?superClass . } WHERE { $this owl:intersectionOf ?list . ?list rdf:rest*/rdf:first ?superClass . FILTER isBlank(?superClass) . }
Creates a NodeShape and a sh:property shape for each property that is mentioned in an owl:Restriction.
CONSTRUCT { $this a sh:NodeShape . $this a rdfs:Class . $this sh:property ?propertyShape . ?propertyShape sh:path ?property . } WHERE { $this rdfs:subClassOf/owl:onProperty ?property . BIND (owl2sh-semi-closed:getPropertyShape(?property, $this) AS ?propertyShape) . }
Creates a sh:property shape for each property that is mentioned in an owl:Restriction.
owl:equivalentClass/owl:hasValue to sh:NodeShape with sh:hasValue
CONSTRUCT { $this a sh:NodeShape . $this a rdfs:Class . $this sh:property ?propertyShape . ?propertyShape sh:path ?propertyProbablySkosInScheme . ?propertyShape sh:hasValue ?somethingProbablyAConceptScheme . } WHERE { $this owl:equivalentClass ?restriction . FILTER(isBlank(?restriction)) . ?restriction owl:hasValue ?somethingProbablyAConceptScheme . ?restriction owl:onProperty ?propertyProbablySkosInScheme . BIND (owl2sh-semi-closed:getPropertyShape(?propertyProbablySkosInScheme, $this) AS ?propertyShape) . }
Creates a NodeShape and a sh:property shape for each property with matching rdfs:domain.
CONSTRUCT { $this a sh:NodeShape . $this a rdfs:Class . $this sh:property ?propertyShape . ?propertyShape sh:path ?property . } WHERE { { ?property rdfs:domain $this . } UNION { ?property rdfs:domain/owl:unionOf ?unionOf . ?unionOf rdf:rest*/rdf:first $this . } BIND (owl2sh-semi-closed:getPropertyShape(?property, $this) AS ?propertyShape) . }
For each relevant property that is owl:FunctionalProperty, create sh:maxCount of 1 (unless there is an OWL cardinality restriction).
CONSTRUCT { ?propertyShape sh:maxCount 1 . } WHERE { $this sh:property ?propertyShape . ?propertyShape sh:path ?property . ?property a owl:FunctionalProperty . FILTER NOT EXISTS { $this rdfs:subClassOf* ?class . ?class rdfs:subClassOf ?restriction . ?restriction a owl:Restriction . ?restriction owl:maxCardinality|owl:cardinality ?any . } }
For each relevant property that has an rdfs:range, creates an sh:node if the range yielded a NodeShape that has an sh:hasValue constraint, instead of an sh:class
rdfs:range with IRI to sh:class or sh:datatype
CONSTRUCT { ?propertyShape sh:node [ sh:property [ sh:path ?propertyProbablySkosInScheme ; sh:hasValue ?somethingProbablyAConceptScheme ; ] ] . } WHERE { { $this sh:property ?propertyShape . } ?propertyShape sh:path ?property . ?property rdfs:range ?range . ?range owl:equivalentClass ?restriction . FILTER(isBlank(?restriction)) . ?restriction owl:hasValue ?somethingProbablyAConceptScheme . ?restriction owl:onProperty ?propertyProbablySkosInScheme . }
For each relevant property that has an rdfs:range, create sh:class or sh:datatype constraint unless it already exists (from a restriction).
CONSTRUCT { ?propertyShape ?parameter ?range . } WHERE { { $this sh:property ?propertyShape . FILTER NOT EXISTS { ?propertyShape sh:node|sh:class|sh:datatype ?any } . } ?propertyShape sh:path ?property . ?property rdfs:range ?range . FILTER isIRI(?range) . # exclude the case where range is rdfs:Literal, this will be handled with an sh:kind FILTER(?range != rdfs:Literal) . BIND ( IF( ?range IN (xsd:boolean, xsd:string, xsd:date, xsd:dateTime, xsd:integer,xsd:integer, xsd:float, xsd:double, xsd:duration, xsd:anyURI, rdf:langString), sh:datatype, sh:class ) AS ?parameter) . }
For each relevant property that has an rdfs:range with value rdfs:Literal, create sh:nodeKind constraint.
rdfs:range rdfs:Literal to sh:nodeKind sh:Literal
CONSTRUCT { ?propertyShape sh:nodeKind sh:Literal . } WHERE { ?propertyShape sh:path ?property . ?property a owl:DatatypeProperty . ?property rdfs:range rdfs:Literal . }
For each owl:maxCardinality restriction, create a corresponding sh:maxCount constraint.
CONSTRUCT { ?propertyShape sh:maxCount ?maxCount . # ?restriction owl2sh-semi-closed:mappedTo ?propertyShape . } WHERE { $this rdfs:subClassOf ?restriction . ?restriction a owl:Restriction . FILTER isBlank(?restriction) . ?restriction owl:onProperty ?property . ?restriction owl:maxCardinality|owl:cardinality ?raw . BIND (xsd:integer(?raw) AS ?maxCount) . BIND (owl2sh-semi-closed:getPropertyShape(?property, $this) AS ?propertyShape) . }
For each owl:maxQualifiedCardinality restriction on an IRI class, create a corresponding sh:maxCount constraint, if the owl:onClass is identical to the rdfs:range of the property.
owl:maxQualifiedCardinality with owl:onClass to sh:maxCount
PREFIX sh: <http://www.w3.org/ns/shacl#> PREFIX owl: <http://www.w3.org/2002/07/owl#> PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> PREFIX owl2sh-semi-closed: <http://data.sparna.fr/ontologies/owl2sh-semi-closed#> CONSTRUCT { ?propertyShape sh:maxCount ?maxCount . # ?restriction owl2sh-semi-closed:mappedTo ?propertyShape . } WHERE { { $this rdfs:subClassOf ?restriction . ?restriction a owl:Restriction . FILTER isBlank(?restriction) . } ?restriction owl:maxQualifiedCardinality ?raw . ?restriction owl:onProperty ?property . ?restriction owl:onClass ?onClass . FILTER isIRI(?onClass) . FILTER EXISTS { ?property rdfs:range ?onClass } . BIND (xsd:integer(?raw) AS ?maxCount) . BIND (owl2sh-semi-closed:getPropertyShape(?property, $this) AS ?propertyShape) . }
For each owl:maxQualifiedCardinality restriction on an IRI class, create a corresponding (new) sh:qualifiedMaxCount constraint, unless the owl:onClass is identical to the rdfs:range of the property.
owl:maxQualifiedCardinality with owl:onClass to sh:qualifiedMaxCount
CONSTRUCT { $this sh:property ?propertyShape . ?propertyShape sh:path ?property . ?propertyShape sh:qualifiedMaxCount ?maxCount . ?propertyShape sh:qualifiedValueShape ?valueShape . ?valueShape sh:class ?onClass . # ?restriction owl2sh-semi-closed:mappedTo ?propertyShape . } WHERE { { $this rdfs:subClassOf ?restriction . ?restriction a owl:Restriction . FILTER isBlank(?restriction) . } ?restriction owl:maxQualifiedCardinality ?raw . ?restriction owl:onProperty ?property . ?restriction owl:onClass ?onClass . FILTER isIRI(?onClass) . FILTER NOT EXISTS { ?property rdfs:range ?onClass } . BIND (xsd:integer(?raw) AS ?maxCount) . BIND (BNODE() AS ?propertyShape) . BIND (BNODE() AS ?valueShape) . }
For each owl:maxQualifiedCardinality restriction on an IRI datatype, create a corresponding (new) sh:qualifiedMaxCount constraint.
owl:maxQualifiedCardinality with owl:onDataRange to sh:qualifiedMaxCount
CONSTRUCT { $this sh:property ?propertyShape . ?propertyShape sh:path ?property . ?propertyShape sh:qualifiedMaxCount ?maxCount . ?propertyShape sh:qualifiedValueShape ?valueShape . ?valueShape sh:datatype ?onDataRange . # ?restriction owl2sh-semi-closed:mappedTo ?propertyShape . } WHERE { { $this rdfs:subClassOf ?restriction . ?restriction a owl:Restriction . FILTER isBlank(?restriction) . } ?restriction owl:maxQualifiedCardinality ?raw . ?restriction owl:onProperty ?property . ?restriction owl:onDataRange ?onDataRange . FILTER isIRI(?onDataRange) . FILTER NOT EXISTS { ?property rdfs:range ?onDataRange } . BIND (xsd:integer(?raw) AS ?maxCount) . BIND (BNODE() AS ?propertyShape) . BIND (BNODE() AS ?valueShape) . }
For each owl:minCardinality restriction, create a corresponding sh:minCount constraint.
CONSTRUCT { ?propertyShape sh:minCount ?maxCount . # ?restriction owl2sh-semi-closed:mappedTo ?propertyShape . } WHERE { { $this rdfs:subClassOf ?restriction . ?restriction a owl:Restriction . FILTER isBlank(?restriction) . } ?restriction owl:minCardinality|owl:cardinality ?raw . ?restriction owl:onProperty ?property . BIND (xsd:integer(?raw) AS ?maxCount) . BIND (owl2sh-semi-closed:getPropertyShape(?property, $this) AS ?propertyShape) . }
For each owl:minQualifiedCardinality restriction on an IRI class, create a corresponding sh:minCount constraint, if the owl:onClass is identical to the rdfs:range of the property.
owl:minQualifiedCardinality with owl:onClass to sh:minCount
CONSTRUCT { ?propertyShape sh:minCount ?minCount . # ?restriction owl2sh-semi-closed:mappedTo ?propertyShape . } WHERE { { $this rdfs:subClassOf ?restriction . ?restriction a owl:Restriction . FILTER isBlank(?restriction) . } ?restriction owl:minQualifiedCardinality ?raw . ?restriction owl:onClass ?onClass . ?restriction owl:onProperty ?property . FILTER isIRI(?onClass) . FILTER EXISTS { ?property rdfs:range ?onClass } . BIND (xsd:integer(?raw) AS ?minCount) . BIND (owl2sh-semi-closed:getPropertyShape(?property, $this) AS ?propertyShape) . }
For each owl:minQualifiedCardinality restriction on an IRI class, create a corresponding (new) sh:qualifiedMinCount constraint, unless the owl:onClass is identical to the rdfs:range of the property.
owl:minQualifiedCardinality with owl:onClass to sh:qualifiedMinCount
CONSTRUCT { $this sh:property ?propertyShape . ?propertyShape sh:path ?property . ?propertyShape sh:qualifiedMinCount ?minCount . ?propertyShape sh:qualifiedValueShape ?valueShape . ?valueShape sh:class ?onClass . # ?restriction owl2sh-semi-closed:mappedTo ?propertyShape . } WHERE { { $this rdfs:subClassOf ?restriction . ?restriction a owl:Restriction . FILTER isBlank(?restriction) . } ?restriction owl:minQualifiedCardinality ?raw . ?restriction owl:onClass ?onClass . ?restriction owl:onProperty ?property . FILTER isIRI(?onClass) . FILTER NOT EXISTS { ?property rdfs:range ?onClass } . BIND (xsd:integer(?raw) AS ?minCount) . BIND (BNODE() AS ?propertyShape) . BIND (BNODE() AS ?valueShape) . }
For each owl:minQualifiedCardinality restriction on an IRI datatype, create a corresponding (new) sh:qualifiedMinCount constraint.
owl:minQualifiedCardinality with owl:onDataRange to sh:qualifiedMinCount
CONSTRUCT { $this sh:property ?propertyShape . ?propertyShape sh:path ?property . ?propertyShape sh:qualifiedMinCount ?minCount . ?propertyShape sh:qualifiedValueShape ?valueShape . ?valueShape sh:datatype ?onDataRange . # ?restriction owl2sh-semi-closed:mappedTo ?propertyShape . } WHERE { { $this rdfs:subClassOf ?restriction . ?restriction a owl:Restriction . FILTER isBlank(?restriction) . } ?restriction owl:minQualifiedCardinality ?raw . ?restriction owl:onDataRange ?onDataRange . ?restriction owl:onProperty ?property . FILTER isIRI(?onDataRange) . FILTER NOT EXISTS { ?property rdfs:range ?onDataRange } . BIND (xsd:integer(?raw) AS ?minCount) . BIND (BNODE() AS ?propertyShape) . BIND (BNODE() AS ?valueShape) . }
For each owl:qualifiedCardinality restriction on an IRI class, create corresponding sh:max/minCount constraints, if the owl:onClass is identical to the rdfs:range of the property.
owl:qualifiedCardinality with owl:onClass to sh:max/minCount
CONSTRUCT { ?propertyShape sh:maxCount ?count . ?propertyShape sh:minCount ?count . # ?restriction owl2sh-semi-closed:mappedTo ?propertyShape . } WHERE { { $this rdfs:subClassOf ?restriction . ?restriction a owl:Restriction . FILTER isBlank(?restriction) . } ?restriction owl:qualifiedCardinality ?raw . ?restriction owl:onClass ?onClass . ?restriction owl:onProperty ?property . FILTER isIRI(?onClass) . FILTER EXISTS { ?property rdfs:range ?onClass } . BIND (xsd:integer(?raw) AS ?count) . BIND (owl2sh-semi-closed:getPropertyShape(?property, $this) AS ?propertyShape) . }
For each owl:qualifiedCardinality restriction on an IRI class, create a corresponding (new) sh:qualifiedMax/MinCount constraint, unless the owl:onClass is identical to the rdfs:range of the property.
owl:qualifiedCardinality with owl:onClass to sh:qualifiedMax/MinCount
CONSTRUCT { $this sh:property ?propertyShape . ?propertyShape sh:path ?property . ?propertyShape sh:qualifiedMaxCount ?count . ?propertyShape sh:qualifiedMinCount ?count . ?propertyShape sh:qualifiedValueShape ?valueShape . ?valueShape sh:class ?onClass . # ?restriction owl2sh-semi-closed:mappedTo ?propertyShape . } WHERE { { $this rdfs:subClassOf ?restriction . ?restriction a owl:Restriction . FILTER isBlank(?restriction) . } ?restriction owl:qualifiedCardinality ?raw . ?restriction owl:onClass ?onClass . ?restriction owl:onProperty ?property . FILTER isIRI(?onClass) . FILTER NOT EXISTS { ?property rdfs:range ?onClass } . BIND (xsd:integer(?raw) AS ?count) . BIND (BNODE() AS ?propertyShape) . BIND (BNODE() AS ?valueShape) . }
For each owl:qualifiedCardinality restriction on an IRI datatype, create a corresponding (new) sh:qualifiedMax/MinCount constraint.
owl:qualifiedCardinality with owl:onDataRange to sh:qualifiedMax/MinCount
CONSTRUCT { $this sh:property ?propertyShape . ?propertyShape sh:path ?property . ?propertyShape sh:qualifiedMaxCount ?count . ?propertyShape sh:qualifiedMinCount ?count . ?propertyShape sh:qualifiedValueShape ?valueShape . ?valueShape sh:datatype ?onDataRange . # ?restriction owl2sh-semi-closed:mappedTo ?propertyShape . } WHERE { { $this rdfs:subClassOf ?restriction . ?restriction a owl:Restriction . FILTER isBlank(?restriction) . } ?restriction owl:qualifiedCardinality ?raw . ?restriction owl:onDataRange ?onDataRange . ?restriction owl:onProperty ?property . FILTER isIRI(?onDataRange) . FILTER NOT EXISTS { ?property rdfs:range ?onDataRange } . BIND (xsd:integer(?raw) AS ?count) . BIND (BNODE() AS ?propertyShape) . BIND (BNODE() AS ?valueShape) . }
For each owl:hasValue restriction, create a corresponding sh:hasValue constraint.
CONSTRUCT { ?propertyShape sh:hasValue ?hasValue . } WHERE { { $this rdfs:subClassOf ?restriction . ?restriction a owl:Restriction . FILTER isBlank(?restriction) . } ?restriction owl:onProperty ?property . ?restriction owl:hasValue ?hasValue . BIND (owl2sh-semi-closed:getPropertyShape(?property, $this) AS ?propertyShape) . }
For each owl:someValuesFrom restriction, create a corresponding sh:minCount 1 constraint.
CONSTRUCT { ?propertyShape sh:minCount 1 . # ?restriction owl2sh-semi-closed:mappedTo ?propertyShape . } WHERE { $this rdfs:subClassOf ?restriction . ?restriction a owl:Restriction . ?restriction owl:someValuesFrom ?someValuesFrom . ?restriction owl:onProperty ?property . BIND (owl2sh-semi-closed:getPropertyShape(?property, $this) AS ?propertyShape) . }
For each owl:someValuesFrom restriction combined with an owl:allValuesFrom on an IRI, create a corresponding dash:hasValueWithClass constraint using a path expression. For example: ex:ConstitutionalOwner a owl:Class ; rdfs:subClassOf [ a owl:Restriction ; owl:onProperty ex:isPlayedBy ; owl:someValuesFrom [ a owl:Restriction ; owl:allValuesFrom ex:StockholdersEquity ; owl:onProperty ex:holdsEquityIn ; ] ; ] . becomes ex:ConstitutionalOwner a sh:NodeShape ; sh:property [ sh:path ( ex:isPlayedBy ex:holdsEquityIn ) ; dash:hasValueWithClass ex:StockholdersEquity ; ] .
owl:someValuesFrom with IRI to dash:hasValueWithClass
CONSTRUCT { $this sh:property ?propertyShape . ?propertyShape dash:hasValueWithClass ?allValuesFrom . ?propertyShape sh:path ?firstNode . ?firstNode rdf:first ?property . ?firstNode rdf:rest ?secondNode . ?secondNode rdf:first ?allValuesFromProperty . ?secondNode rdf:rest rdf:nil . # ?restriction owl2sh-semi-closed:mappedTo ?propertyShape . } WHERE { { { $this rdfs:subClassOf ?restriction . ?restriction a owl:Restriction . FILTER isBlank(?restriction) . } ?restriction owl:someValuesFrom ?someValuesFrom . ?someValuesFrom owl:allValuesFrom ?allValuesFrom . FILTER isIRI(?allValuesFrom) . FILTER (!(?allValuesFrom IN (xsd:boolean, xsd:string, xsd:date, xsd:dateTime, xsd:integer,xsd:integer, xsd:float, xsd:double, xsd:duration, xsd:anyURI))) . FILTER isBlank(?someValuesFrom) . } ?restriction owl:onProperty ?property . ?someValuesFrom owl:onProperty ?allValuesFromProperty . BIND (BNODE() AS ?propertyShape) . BIND (BNODE() AS ?firstNode) . BIND (BNODE() AS ?secondNode) . }
For each owl:unionOf that only consists of named classes, move these classes into "normal" rdfs:subClassOf triples.
CONSTRUCT { $this rdfs:subClassOf ?class . # ?union owl2sh-semi-closed:mappedTo $this . } WHERE { { $this rdfs:subClassOf ?union . ?union owl:unionOf ?unionOf . FILTER isBlank(?union) . } FILTER NOT EXISTS { ?unionOf rdf:rest*/rdf:first ?member . FILTER (!isIRI(?member)) . } . ?unionOf rdf:rest*/rdf:first ?class . }
For each owl:someValuesFrom restriction with an IRI, create a corresponding dash:hasValueWithClass constraint.
CONSTRUCT { ?propertyShape dash:hasValueWithClass ?someValuesFrom . # ?restriction owl2sh-semi-closed:mappedTo ?propertyShape . } WHERE { { $this rdfs:subClassOf ?restriction . ?restriction a owl:Restriction . FILTER isBlank(?restriction) . } ?restriction owl:someValuesFrom ?someValuesFrom . ?restriction owl:onProperty ?property . FILTER (isIRI(?someValuesFrom)) FILTER (!(?someValuesFrom IN (xsd:boolean, xsd:string, xsd:date, xsd:dateTime, xsd:integer,xsd:integer, xsd:float, xsd:double, xsd:duration, xsd:anyURI))) . FILTER NOT EXISTS { ?property rdfs:range ?someValuesFrom } . BIND (owl2sh-semi-closed:getPropertyShape(?property, $this) AS ?propertyShape) . }
For each owl:allValuesFrom restriction, create a corresponding sh:class or sh:datatype constraint.
owl:allValuesFrom with IRI to sh:class or sh:datatype
CONSTRUCT { ?propertyShape ?parameter ?allValuesFrom . # ?restriction owl2sh-semi-closed:mappedTo ?propertyShape . } WHERE { { $this rdfs:subClassOf ?restriction . ?restriction a owl:Restriction . FILTER isBlank(?restriction) . } ?restriction owl:onProperty ?property . ?restriction owl:allValuesFrom ?allValuesFrom . FILTER isIRI(?allValuesFrom) . BIND (owl2sh-semi-closed:getPropertyShape(?property, $this) AS ?propertyShape) . BIND ( IF( (?allValuesFrom IN (xsd:boolean, xsd:string, xsd:date, xsd:dateTime, xsd:integer,xsd:integer, xsd:float, xsd:double, xsd:duration, xsd:anyURI)), sh:datatype, sh:class ) AS ?parameter ) . }
For each rdfs:domain that is an IRI, create a NodeShape with a sh:targetSubjectsOf to this domain and a sh:class constraint.
rdfs:domain with IRI to sh:NodeShape with sh:class
CONSTRUCT { ?propertyShape a sh:NodeShape . ?propertyShape sh:targetSubjectsOf ?property . ?propertyShape sh:class ?domain . } WHERE { ?property rdfs:domain ?domain . FILTER (isIRI(?domain)) . BIND(IRI(CONCAT(STR(?property), '-', 'shape')) AS ?propertyShape) }
For each value of sh:property, add a rdf:type sh:PropertyShape triple.
CONSTRUCT { ?propertyShape a sh:PropertyShape . } WHERE { ?shape sh:property ?propertyShape . }
Gets an existing sh:PropertyShape for a given property at a given shape. If none is found, return a new blank node that will be reused by future calls.
SELECT ?result WHERE { { ?shape sh:property ?result . ?result sh:path ?predicate . } UNION { BIND( IF( CONTAINS(STR(?predicate), '#'), STRAFTER(STR(?predicate), '#'), REPLACE(REPLACE(STR(?predicate), '/', '_'), ':', '_') ) AS ?propertyLocalName ) BIND ( IF(isIRI($shape), IRI(CONCAT( STR($shape), "-", ?propertyLocalName )), BNODE() ) AS ?result) . } }