XML.com: XML From the Inside Out
oreilly.comSafari Bookshelf.Conferences.

advertisement

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>

Pages: 1, 2, 3, 4, 5

Next Pagearrow