Combining RELAX NG and Schematron
by Eddie Robertsson
|
Pages: 1, 2, 3, 4, 5
The assertion test itself does in this case specify a predicate in
conjunction with the document() function. Here the
predicate is used to select the product element that has
an id that matches the id of
the item element that is currently being checked. The
assertion then checks that the numberInStock child
element (of product) has a value that is greater than or
equal to the value of the quantity child element
(of item).
Now we know how the rule selects the context node, and how the
assertion performs the validation, but what is the reason for the
added restriction on which item elements are selected?
Why can't the context simply be all the item elements in
the document and then the assertion for both the above constraints can
be included in the same rule?
The answer has its roots in the fact that a Schematron assertion will fire if its test condition evaluates to false. Part of the assertion expression look like this:
document('Products.xml')/products/product[@id = current()/@id]
This part of the assertion is specified to select
the product element from the database that has the
same id as the item currently being checked. If no such
product exists, the document() function will not return
any element at all, and this will cause the whole assertion expression
to fail. This is not the desired result since this assertion should
check that there are enough products in stock to make the
purchase. However, by specifying a rule that only selects the item
elements that do exist in the database, this situation will never
occur.
Another important issue when defining the context of a rule is that
an element can only be used once as the context for each pattern. This
means that if more than one rule is specified in the same pattern with
the same context element, only the first matching rule is used. If a
pattern defines multiple rules with the same context element, the most
restrictive rule must be specified first, followed by the other rules
in descending order, based on the restrictive features of each
rule. For programmers, this is analogous to how a long if-else chain
is specified: you start with the most restrictive condition and finish
with the most general condition. If done in reverse order, the first
statement will always be true and the others will never execute. To
illustrate, we will take a look at how to specify the above two rules
in one pattern, since both rules use the same context
(the item element).
<sch:pattern name="Combined pattern."
xmlns:sch="http://www.ascc.net/xml/schematron">
<sch:rule context="purchaseOrder/items/item">
...
</sch:rule>
<sch:rule
context="purchaseOrder/items/item[@id = document('Products.xml')/products/product/@id]">
...
</sch:rule>
</sch:pattern>
If the rules were specified in the above order (which is the order
in which they were defined and specified in the example), validation
would not be performed correctly. The reason is because both rules
specify the same context element and in this case the most general
rule (context="purchaseOrder/items/item") is specified
first. Since this rule will match all the item elements, there will
not be any item elements left to match the second rule. To make this
work as expected, the rules must be specified in the reverse order
(the most restrictive rule first):
<sch:pattern name="Combined pattern."
xmlns:sch="http://www.ascc.net/xml/schematron">
<sch:rule
context="purchaseOrder/items/item[@id = document('Products.xml')/products/product/@id]">
...
</sch:rule>
<sch:rule context="purchaseOrder/items/item">
...
</sch:rule>
</sch:pattern>
Now validation will be performed as expected. Since the most
restrictive rule (selects only the item elements that do
exist in the database) is specified first, the second rule will still
be applied to all item elements that do not exist in the
database. This means that the assertion in the second rule can be
simplified to always fail (test="false()") because if the
assertion is ever checked, it is certain that it is an invalid item
that does not exist in the database.
Here is the complete specification of the pattern for the two constraints after the appropriate changes have been made:
<sch:pattern name="Check each item against the database."
xmlns:sch="http://www.ascc.net/xml/schematron">
<sch:rule
context="purchaseOrder/items/item[@id = document('Products.xml')/products/product/@id]">
<sch:assert
test="number(document('Products.xml')/products/product[@id = current()/@id]/numberInStock)
>= number(quantity)">
There are not enough items of this type in stock for this quantity.
</sch:assert>
</sch:rule>
<sch:rule context="purchaseOrder/items/item">
<sch:assert test="false()"
>The item doesn't exist in the database.</sch:assert>
</sch:rule>
</sch:pattern>
The complete RELAX NG schema with embedded Schematron rules for both co-occurrence constraints and the database checks will look like this:
<?xml version="1.0" encoding="UTF-8"?>
<grammar xmlns="http://relaxng.org/ns/structure/1.0"
datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes"
xmlns:sch="http://www.ascc.net/xml/schematron">
<start>
<ref name="purchaseOrder"/>
</start>
<define name="purchaseOrder">
<element name="purchaseOrder">
<attribute name="date">
<data type="date"/>
</attribute>
<ref name="deliveryDetails"/>
<element name="items">
<oneOrMore>
<ref name="item"/>
</oneOrMore>
</element>
<ref name="payment"/>
</element>
</define>
<define name="deliveryDetails">
<element name="deliveryDetails">
<element name="name"><text/></element>
<element name="address"><text/></element>
<element name="phone"><text/></element>
</element>
</define>
<define name="item">
<element name="item">
<sch:pattern name="Validate each item.">
<sch:rule
context="purchaseOrder/items/item[@id = document(
'Products.xml')/products/product/@id]">
<sch:assert
test="number(document('Products.xml')
/products/product[@id = current()/@id]/numberInStock) >= number(quantity)">
There are not enough items of this type in stock for this quantity.
</sch:assert>
<sch:assert
test="number(price) * number(quantity) = number(totalAmount)">
The total amount for the item doesn't add up to (quantity * price).
</sch:assert>
<sch:assert test="price/@currency = totalAmount/@currency"
>The currency in price doesn't match the currency in totalAmount.
</sch:assert>
</sch:rule>
<sch:rule context="purchaseOrder/items/item">
<sch:assert test="false()"
>The item doesn't exist in the database.</sch:assert>
</sch:rule>
</sch:pattern>
<attribute name="id">
<data type="string">
<param name="pattern">\d{3}-[A-Z]{2}</param>
</data>
</attribute>
<element name="productName"><text/></element>
<element name="quantity">
<data type="int"/>
</element>
<element name="price">
<ref name="currency"/>
</element>
<element name="totalAmount">
<ref name="currency"/>
</element>
</element>
</define>
<define name="payment">
<element name="payment">
<attribute name="type">
<choice>
<value>Prepaid</value>
<value>OnArrival</value>
</choice>
</attribute>
<element name="amount">
<sch:pattern
name="Check that the total amount is correct and that the currencies match">
<sch:rule
context="purchaseOrder/payment/amount">
<sch:assert
test="number(.) = sum(/purchaseOrder/items/item/totalAmount)">
The total purchase amount doesn't match the cost of all items.
</sch:assert>
<sch:assert
test="not(/purchaseOrder/items/item/totalAmount/@currency != @currency)">
</sch:rule>
</sch:pattern>
<ref name="currency"/>
</element>
</element>
</define>
<define name="currency">
<attribute name="currency">
<choice>
<value>AUD</value>
<value>USD</value>
<value>SEK</value>
</choice>
</attribute>
<data type="int"/>
</define>
</grammar>