XQuery/Validation with Schematron

Motivation edit

You want to check your XML document against a set of business rules. These rules may be more complex then a single pass XML Schema can perform.

Overview edit

Schematron provides a different approach to validation than a standard Schema. Rather than stating the correct form of a document, Schematron allows you to define rules which can be applied as needed. You can also specify rules in one part of a document that reference other parts of the document using XPath expressions. This is very useful when a document is created in stages and specific business rules need to be checked prior to using the data.

Schematron may also work along side of traditional XML Schemas. You can use XML Schemas to check the overall document structure and data types and use Schematron to express rules that can be done best with XPath rules.

Schematron is also ideal because it allows the rule designer to specify the exact error messages for rules. XML Schema error messages are often not easy for users to understand.

Example edit

Here's an example: prior to sending a confirmation email for a "school-booking", the booking must contain a school/id, a Teacher's email-address, and a Date for the booking. The Date must be in the future.

   let $schema := 
   <sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron" queryBinding="xslt2">
     <sch:title>School-booking Prior to Send-by-email</sch:title>
     <sch:pattern id="Send-email-check">
         <sch:rule context="school/id">
             <sch:assert test=". ne ''">Missing School ID</sch:assert>
         </sch:rule>
         <sch:rule context="school/teacher-email">
             <sch:assert test=". ne ''">Missing Teach-Email</sch:assert>
         </sch:rule>
         <sch:rule context="day">
             <sch:assert test="date">Missing booking Date</sch:assert>
         </sch:rule>
         <sch:rule context="day/date">
             <sch:assert test=". castable as xs:date and xs:date(.) gt current-date()">The Date must be in the future</sch:assert>
         </sch:rule>
     </sch:pattern>
   </sch:schema>

To apply this schema from an XQuery:

  xquery version "1.0"
   let $send-email-validator := 
      transform:transform(
         $schema,
         xs:anyURI("xmldb:///db/iso-schematron-xslt2/iso_svrl_for_xslt2.xsl"),() )
   return transform:transform(
      $the-data,
      $send-email-validator,() )

And the results (edited for clarity):

   <schematron-output  title="School-booking Prior to Send-by-email" >
     <failed-assert test=". ne ''" location="/school-booking[1]/school[1]/teacher-email[1]">
        <text>Missing Teach-Email</svrl:text>
     </failed-assert>
     <failed-assert test=". castable as xs:date and xs:date(.) gt current-date()" location="/school-booking[1]/day[1]/date[1]">
        <text>The Date must be in the future</svrl:text>
     </failed-assert>
   </schematron-output>

The results can be used to highlight the error and display a message to the user.

Setup in eXist-db edit

I've chosen the XSLT 2.0 version of the Schematron validation - so eXist-db needs to have Saxon 9 installed. (...)

  1. Get the iso-schematron-xslt2 and upload the (unzipped) contents to your database.
  2. Create a schema in the db
  3. Compile and run the schema on your data

XQuery Module for ISO Schematron edit

Jing only supports Schematron 1.5. A more advanced version of Schematron is the ISO Schematron, which Jing will not run. To get this to work you must "compile" your Schematron rules using XSLT.

The following is a sample XQuery module that does this compilation.

module namespace v="http://gspring.com.au/pekoe/validator";

declare variable $v:schematron-compiler := xs:anyURI("xmldb:///db/iso-schematron-xslt2/iso_svrl_for_xslt2.xsl");

(: Call like this: v:validate($xml, xs:anyURI("/db/path/to/schema.sch") ) :)

declare function v:validate($doc as node(), $schematron as xs:anyURI ) {
    let $validator := local:get-compiled-validator($schematron)
    return transform:transform(
        $doc,
        $validator,()
        )
};

 declare function local:get-compiled-validator($schematron-path as xs:anyURI) {
    let $s-path := xs:string($schematron-path)
    let $xsl-path := concat(substring-before($s-path,"."),".xsl")
    return  (: check that the compiled version is up-to-date :)
        if (exists(doc($xsl-path)) and 
            xmldb:last-modified(util:collection-name($xsl-path), util:document-name($xsl-path)) gt 
            xmldb:last-modified(util:collection-name($s-path), util:document-name($s-path))) 
        then doc($xsl-path) 
        else local:compile-schematron($s-path,$xsl-path)
 };
 
 declare function local:compile-schematron($schematron-path, $xsl-path) {
    let $compiled := transform:transform(
                    doc($schematron-path), 
                    $v:schematron-compiler, ())
    let $stored := xmldb:store(util:collection-name($schematron-path), $xsl-path, $compiled)
    return doc($stored)
 };

Note that the xslt assumes an include file.