How to write a Schematron schema with QuickFixes

This page demonstrates with an example how to extend a Schematron schema by using QuickFixes. You should be able to read Schematron schemas and to read and write XPath in order to follow the examples.

Example

This is an abstract example. It shows a list of persons who should be of age. Also the date of birth should fit with the age. This is the XML instance:

1 <?xml-model href="people8.sch" type="application/xml" schematypens="http://purl.oclc.org/dsdl/schematron"?>
2 <people>
3 <person>
4 <name>Nico Kutscherauer</name>
5 <dateOfBirth>1986-04-05</dateOfBirth>
6 <age>23</age>
7 </person>
8 <person>
9 <name>Lisa Simpson</name>
10 <dateOfBirth>2006-01-01</dateOfBirth>
11 <age>9</age>
12 </person>
13 </people>

The first person is of age, but the age does not match the date of birth. The second person is too young.

The following Schematron schema tests these errors:

1 <schema xmlns="http://purl.oclc.org/dsdl/schematron" queryBinding="xslt2">
2 <title>People &#x2013; test</title>
3 <pattern>
4 <rule context="person">
5 <let name="currYear" value="year-from-date(current-date())"/>
6 <let name="maxBirth" value="replace(string(current-date()),
7 string($currYear),
8 string($currYear - age - 1))"/>
9 <let name="minBirth" value="replace(string(current-date()),string($currYear), string($currYear - age))"/>
10 <let name="birth" value="xs:date(dateOfBirth)"/>
11 <assert test="$birth gt xs:date($maxBirth) and $birth le xs:date($minBirth)"><value-of select="name"/> is not <value-of select="age"/> years old or is not born on <value-of select="dateOfBirth"/>.</assert>
12 <assert test="not($birth gt xs:date($maxBirth) and $birth le xs:date($minBirth)) and number(age) ge 18"><value-of select="name"/> is too young.</assert>
13 </rule>
14 </pattern>
15 </schema>

Note: For this example, we ignore the error which occurs if the current-date is February 29.

These should be the error messages:

Nico Kutscherauer is not 23 years old or is not born on 1986-04-05.

Lisa Simpson is too young.

Step 1: Defining fixes

The first step is to think about possible solutions for these problems. The first error could have the following fixes:

Fixes for the second error:

There could also be other fixes for these problems, but the following guide will show how to implement the above described QuickFixes.

Step 2: Adding the QuickFix namespace

You need the correct namespace of the Schematron QuickFix:

2 <schema xmlns="http://purl.oclc.org/dsdl/schematron" queryBinding="xslt2" xmlns:sqf="http://www.schematron-quickfix.com/validator/process">

Step 3: Adding a QuickFix to an <assert> element

Firstly, you need a <sqf:fix> element to add a QuickFix to an <assert> element. You can add it to the same rule or you can put all fixes in a top-level <sqf:fixes> element. The <sqf:fix> element requires an id attribute with the ID of the QuickFix.

Secondly, you have to connect the <assert> with the QuickFix. For this, you add a sqf:fix attribute to the <assert> and set the value to the ID of the QuickFix.

10 <assert test="$birth gt xs:date($maxBirth) and $birth le xs:date($minBirth)" sqf:fix="setAge"><value-of select="name"/> is not <value-of select="age"/> years old or is not born on <value-of select="dateOfBirth"/>.</assert>
   [...]
12 <sqf:fix id="setAge">
   [...]
14 </sqf:fix>

In order to connect an <assert> with more than one QuickFix, you can separate the IDs in the sqf:fix attribute of all QuickFixes with space characters.

11 <assert test="not($birth gt xs:date($maxBirth) and $birth le xs:date($minBirth)) and number(age) ge 18" sqf:fix="delete birthEntry"><value-of select="name"/> is too young.</assert>
   [...]
15 <sqf:fix id="delete">
   [...]
17 </sqf:fix>
18 <sqf:fix id="birthEntry">
   [...]
20 </sqf:fix>

Now you have successfully connected your <assert> elements with QuickFixes, but they don't do anything yet.

Step 4: Defining a description for a QuickFix.

The user wants to know what happens if he chooses your QuickFix. Therefore, the QuickFix requires a description including a title and paragraphs (optional) which are human-readable. The following example shows how it works:

12 <sqf:fix id="setAge">
13 <sqf:description>
14 <sqf:title>Calculate the age from the date of birth.</sqf:title>
15 <sqf:p>This fix assumes that the date of birth is correct but the age not. The new age will be calculated from the date of birth.</sqf:p>
16 </sqf:description>
17 </sqf:fix>
18 <sqf:fix id="delete">
19 <sqf:description>
20 <sqf:title>Delete the person from the list.</sqf:title>
21 <sqf:p>The fix deletes this person from the list, because it is out of age.</sqf:p>
22 </sqf:description>
23 </sqf:fix>
24 <sqf:fix id="birthEntry">
25 <sqf:description>
26 <sqf:title>Enter the correct date of birth.</sqf:title>
27 <sqf:p>If the age and the date of birth are wrong, please enter the correct date of birth. </sqf:p>
28 </sqf:description>
29 </sqf:fix>

The <sqf:title> is required, optional followed by any number of <sqf:p> elements

Now, your QuickFixes have human-readable labels, but they still don't do anything.

Step 5: Activity elements

In order to define an action for a QuickFix, it may contain one or several "activity elements".

Schematron QuickFix offers four "activity elements":

The easiest action is to delete one or more nodes. You just need to specify which nodes you want to delete. For the deletion – surprise! – you need the <sqf:delete> element.

Step 5.1: Deleting

In order to specify the nodes for the deletion, the <sqf:delete> element can have a match attribute. With a XPath expression you point at the nodes. The context of relative expressions is the context the Schematron rule has set. The default value of the match attribute is self::*. However, if you want to delete the context node of the rule, you only need the following:

27 <sqf:fix id="delete">
28 <sqf:description>
29 <sqf:title>Delete the person from the list.</sqf:title>
30 <sqf:p>The fix deletes this person from the list, because it is out of age.</sqf:p>
31 </sqf:description>
32 <sqf:delete/>
33 </sqf:fix>

Step 5.2: Replacing

In order to replace elements, you need of course the <sqf:replace> element. Just like the <sqf:delete> element, the <sqf:replace> element can have a match attribute. If not, it replaces the context node of the rule.

The <sqf:replace> element has three tasks. It identifies the nodes to be replaced, defines the replacing node and the content/value of it.

Other attributes:

  • node-type:

    It determines the type of the replacing node. Permitted values are:

    • keep: keep the node type of the node to be replaced.

    • element: creates an element.

    • attribute: creates an attribute.

    • comment: creates a comment.

    • pi: creates a proccessing instruction.

    If the node-type attribute is not set, the nodes to be replaced will not be replaced by a specific node, but just by the defined content/value (see bellow).

  • target:

    By using a QName, the target attribute gives the replacing node a name. If you use this attribute you need to define the type of the replacing node by using the node-type attribute. This is necessary in case the value of the node-type attribute is not comment. You can use XPath in curly brackets.

  • select:

    With select you can choose (with XPath) the content nodes of the replacing node. It copies the selected nodes. Alternatively, you can create the content by writing it into the <sqf:replace> element. To copy nodes, use the <sqf:copy-of> element as content of the <sqf:replace> element.

The following code example shows that you can use Schematron <let> elements in the <sqf:fix> element.

14 <sqf:fix id="setAge">
15 <sqf:description>
16 <sqf:title>Calculate the age from the date of birth.</sqf:title>
17 <sqf:p>This fix assumes that the date of birth is correct but the age not. The new age will be calculated from the date of birth.</sqf:p>
18 </sqf:description>
19 <let name="yearOfBirth" value="year-from-date($birth)"/>
20 <let name="yearDif" value="$currYear - $yearOfBirth"/>
21 <let name="birthdayInThisYear" value="replace(string($birth), string($yearOfBirth), string($currYear))"/>
22 <let name="newAge" value="if(xs:date($birthdayInThisYear) > current-date())
23 then ($yearDif - 1)
24 else ($yearDif)"/>
25 <sqf:replace match="age" target="age" node-type="element" select="$newAge"/>
26 </sqf:fix>

Here, the <let> elements are used to calculate the correct age within the code. At the end the <sqf:replace> element is used to replace the <age> element (match) with a new <age> element (target, node-type). With the select attribute the $newAge variable is selected which contains the correct age.

Note: This is not the place to explain the XPath constructs for the calculation of the correct age.

Sidestep: Other activity elements

Although we don't use them in the example, the behavior of the other activity elements will be described in the following.

<sqf:add> element

With the <sqf:add> element you can add a node to the instance. You need a node (anchor node) to select the position for the new node. The anchor node is selected by the match attribute. In additon to the attributes of the other activity elements, the <sqf:add> element has an position attribute. It determines the position of the node relatively to the anchor node. You can say that the new node should be the first child of the anchor node (first-child, default value), the last child (last-child) and that it should be inserted before or after the anchor node (before and after). If you want to add an attribute to the anchor node, you should not use the position attribute, just set the node-type attribute to the value attribute.

<sqf:stringReplace> element

This activity element is different from the others. It should be used to find substrings of the text content and replace it with nodes or other strings. With match you can select text nodes which contain the substrings you want to replace. With the regex attribute you match the substrings using a regular expression. The content of the <sqf:stringReplace> element defines the nodes which replace the substrings.

Step 6: Defining an User Entry

Now you have your first two workable QuickFixes. To be able to successfully apply the example, you need to complete the last QuickFix. For this, let us assume that the correct date of birth is always 5 April 1986. With the current knowlege we can write a QuickFix for this:

1 <?xml version="1.0" encoding="UTF-8"?>
2 <schema xmlns="http://purl.oclc.org/dsdl/schematron" queryBinding="xslt2" xmlns:sqf="http://www.schematron-quickfix.com/validator/process">
3 <title>People - test</title>
4 <pattern>
5 <rule context="person">
6 <let name="currYear" value="year-from-date(current-date())"/>
7 <let name="maxBirth" value="replace(string(current-date()),string($currYear), string($currYear - age - 1))"/>
8 <let name="minBirth" value="replace(string(current-date()),string($currYear), string($currYear - age))"/>
9 <let name="birth" value="xs:date(dateOfBirth)"/>
   [...]
12 <assert test="$birth gt xs:date($maxBirth) and $birth le xs:date($minBirth)" sqf:fix="setAge"><value-of select="name"/> is not <value-of select="age"/> years old or is not born on <value-of select="dateOfBirth"/>.</assert>
13 <assert test="not($birth gt xs:date($maxBirth) and $birth le xs:date($minBirth)) and number(age) ge 18" sqf:fix="delete birthEntry"><value-of select="name"/> is too young.</assert>
14 <sqf:fix id="setAge">
15 <sqf:description>
16 <sqf:title>Calculate the age from the date of birth.</sqf:title>
17 <sqf:p>This fix assumes that the date of birth is correct but the age not. The new age will be calculated from the date of birth.</sqf:p>
18 </sqf:description>
19 <let name="yearOfBirth" value="year-from-date($birth)"/>
20 <let name="yearDif" value="$currYear - $yearOfBirth"/>
21 <let name="birthdayInThisYear" value="replace(string($birth), string($yearOfBirth), string($currYear))"/>
22 <let name="newAge" value="if(xs:date($birthdayInThisYear) > current-date())
23 then ($yearDif - 1)
24 else ($yearDif)"/>
25 <sqf:replace match="age" target="age" node-type="element" select="$newAge"/>
26 </sqf:fix>
27 <sqf:fix id="delete">
28 <sqf:description>
29 <sqf:title>Delete the person from the list.</sqf:title>
30 <sqf:p>The fix deletes this person from the list, because it is out of age.</sqf:p>
31 </sqf:description>
32 <sqf:delete/>
33 </sqf:fix>
34 <sqf:fix id="birthEntry">
35 <sqf:description>
36 <sqf:title>Enter the correct date of birth.</sqf:title>
37 <sqf:p>If the age and the date of birth are wrong, please enter the correct date of birth. </sqf:p>
38 </sqf:description>
39 <sqf:replace match="dateOfBirth" target="dateOfBirth" node-type="element">1986-04-05</sqf:replace>
40 </sqf:fix>
41 </rule>
42 </pattern>
43 </schema>

The last task is to parametrise the date and give the user the chance to select the correct date individually. For that let us define the date by a variable as we already know it:

34 <sqf:fix id="birthEntry">
35 <sqf:description>
36 <sqf:title>Enter the correct date of birth.</sqf:title>
37 <sqf:p>If the age and the date of birth are wrong, please enter the correct date of birth. </sqf:p>
38 </sqf:description>
39 <let name="value" value="xs:date('1986-04-05')"/>
40 <sqf:replace match="dateOfBirth" target="dateOfBirth" node-type="element" select="$birth"/>
41 </sqf:fix>

The second step to parametrise the date is replace the variable by an User Entry. For this the <sch:let> element will be replaced by a <sqf:user-entry> element.

The <sqf:user-entry> elements has also a name attribute to define a XPath variable. You can also use the following attributes:

The follwoing code sample shows, how the Schematron variable could be replaced by a <sqf:user-entry> element. There was no change in using the variable:

39 <sqf:user-entry name="birth" type="xs:date">
   [...]
44 </sqf:user-entry>
   [...]
51 <sqf:replace match="dateOfBirth" target="dateOfBirth" node-type="element" select="$birth"/>

The user entry needs a description so that the user knows which information is wanted. For this the user entry should contain a <sqf:description> element similar to the <sqf:fix> element:

39 <sqf:user-entry name="birth" type="xs:date">
40 <sqf:description>
41 <sqf:title>Enter the correct date.</sqf:title>
42 <sqf:p>Set the correct date in the format YYYY-MM-DD.</sqf:p>
43 </sqf:description>
44 </sqf:user-entry>

© Copyright 2014-2017 Nico Kutscherauer

ImprintContactSitemap