OWL 2 SHACL closed ruleset
Turns an OWL Ontology into a set of SHACL Shapes. Each shape will be sh:closed with all the properties from its superclasses and subclasses in sh:ignoredProperties.
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-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-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-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 ?range . } WHERE { { $this sh:property ?propertyShape . } ?propertyShape sh:path ?property . ?property rdfs:range ?range . ?range sh:property ?propertyWithHasValue . ?propertyWithHasValue sh:hasValue ?anything . }
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) . # exclude the case where range is owl:Thing BIND ( IF( (?range IN (xsd:boolean, xsd:string, xsd:date, xsd:dateTime, xsd:integer, xsd:float, 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-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-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-closed: <http://data.sparna.fr/ontologies/owl2sh-closed#> CONSTRUCT { ?propertyShape sh:maxCount ?maxCount . # ?restriction owl2sh-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-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-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-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-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-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-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-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-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-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-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-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-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-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 . # ?restriction owl2sh-closed:mappedTo ?propertyShape . } WHERE { { $this rdfs:subClassOf ?restriction . ?restriction a owl:Restriction . FILTER isBlank(?restriction) . } ?restriction owl:onProperty ?property . ?restriction owl:hasValue ?hasValue . BIND (owl2sh-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-closed:mappedTo ?propertyShape . } WHERE { $this rdfs:subClassOf ?restriction . ?restriction a owl:Restriction . ?restriction owl:someValuesFrom ?someValuesFrom . ?restriction owl:onProperty ?property . BIND (owl2sh-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-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:float, 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-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-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:float, xsd:duration, xsd:anyURI))) . FILTER NOT EXISTS { ?property rdfs:range ?someValuesFrom } . BIND (owl2sh-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-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-closed:getPropertyShape(?property, $this) AS ?propertyShape) . BIND ( IF( (?allValuesFrom IN (xsd:boolean, xsd:string, xsd:date, xsd:dateTime, xsd:integer, xsd:float, xsd:duration, xsd:anyURI)), sh:datatype, sh:class ) AS ?parameter ) . }
For every ancestor or descendant class of this class, fetch the paths of the associated property shape, and add this property as an sh:ignoredProperty value of this class. NOTE : SHACL-Play does a post-processing on this to convert the values of sh:ignoredProperties into an RDF List.
Add sh:ignoredProperties with properties from subClasses and super classes.
CONSTRUCT { $this sh:ignoredProperties ?property . } WHERE { { $this a sh:NodeShape . { { ?descendant rdfs:subClassOf+ $this . ?descendant sh:property/sh:path ?property . } UNION { $this rdfs:subClassOf+ ?ancestor . ?ancestor sh:property/sh:path ?property . } } } UNION { $this a sh:NodeShape . BIND(rdf:type AS ?property) } }
CONSTRUCT { $this sh:closed true . } WHERE { $this a sh:NodeShape . }
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) . } }