1. What is xchecker?
xchecker is both a test framework for XML and XSLT, and a wrapper for an XML Schema processor that allows XML Schema documents to be augmented with xchecker checks.
XML Schema, XPath 2.0, XSLT 2.0, XQuery and Relax NG can be combined to check an XML document, or the XML result of an XSLT transform.
Augmenting schema checks with XPath/XSLT checks gives you the power to fully check the correctness of an XML document or transform - XML Schema or Relax NG alone aren't sufficient to fully constrain an XML document, but when used in combination with XSLT / XPath it's possible to check anything - like having a grammar and rules based checker in one.
xchecker can run from the command line, from ANT, or from Java using its API/JAXP schema interfaces.
See the samples dir and the Readme for more examples.
2. How does it work?
The xchecker processor evaluates each check and looks for a result of 'true' - any other values are treated as failures. This means the failure message can be constructed by the test writer and be as descriptive as needed, using the source XML and the expressive power of XPath/XSLT/XQuery. The complexity of the check, and the quality of the failure message aren't limited in any way.
XPath 2.0 checks can return a sequence of items - any item not equal to 'true' is considered a test failure and the value returned to the user as the failure message.
The schema checks are considered a pass if the XML validates, and false if any validation errors are returned. It's intended that the schema checks be used in combination with XPath/XSLT/XQuery checks to fully check the correctness of the XML.
3. Using xchecker as a test framework
Checks are written in a "Check Config" file:
<!---->
<xchecker xmlns="http://xchecker.sf.net/">
<xml src="xml/books.xml">
<!---->
<check>xsd/books.xsd</check>
<!---->
<check>not(exists(/booklist/books/item[author eq 'Thomas Hardy'][not(contains(publisher, 'Classics'))]))</check>
<!---->
<check>xslt/Hardy.xslt</check>
<!---->
<check>if (/booklist/books/item/@cat[not(. = /booklist/categories/category/@code)]) then
concat('
Category not found: ', /booklist/books/item/@cat[not(. = /booklist/categories/category/@code)])
else 'true'</check>
</xml>
</xchecker>
(
books.xml is copied from the samples dir of the Saxon download)
Here the test subject is "books.xml" and there are 4 checks applied - an XML Schema, two XPaths and an XSLT check.
For the first check the XML is validated using the schema and the result is stored. If the XML is valid according to the schema the result for that check is a pass, if an error is returned then the result is that error message.
The second check is an XPath which checks a co-occurrence constraint - that all
- elements with an of "Thomas Hardy" should also have a element that contains the string "Classics". The result is a boolean - true for a pass and false otherwise.
The next check demonstrates how a check written in XSLT is defined in the config. It checks exactly the same thing, but uses the extra expressive power of XSLT to provide a better message in the event of a failure:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<!---->
<xsl:output method="text" />
<xsl:template match="/">
<!---->
<xsl:variable name="hardy" select="for $x in /booklist/books/item[author eq 'Thomas Hardy'] return if (not(contains($x/publisher, 'Classics'))) then concat(' Suspect publisher for Thomas Hardy: ', $x/publisher) else 'true'" />
<!---->
<xsl:sequence select="if ($hardy[. != 'true']) then $hardy[. != 'true'] else true()" />
</xsl:template>
</xsl:stylesheet>
Here the message "Suspect publisher for Thomas Hardy: Penguin" would be returned if the
element contained "Penguin" instead of "Penguin Classics". The message can be whatever the test writer wants - as helpful or as useless as they see fit.
The final check is written in XPath again, and demonstrates how to output useful messages from XPath. Its reasonably verbose, so it could be moved into an XSLT or XQuery file - but its perfectly possible to write a complex XPath directly in the Check Config.
The xchecker processor takes the XML file and applies each of checks, if there are any error messages or non "true" results they are returned, otherwise the XML is considered correct.
4. Checking transforms
xchecker can be used to test XSLT transforms are correct by performing the transform and then applying the checks to the result. Here's an example Check Config that checks a transform:
<xchecker xmlns="http://xchecker.sf.net/">
<transform xml="xml/books.xml" xslt="xslt/books.xslt" result="result/output.html">
<check>/html/head/title = 'Books by publisher'</check>
<check>exists(/html/head/meta)</check>
</transform>
</xchecker>
books.xml is transformed by books.xslt and the result is written to output.html. The two XPaths are then applied to the result (which must be a well-formed XML file) - the first checks the title element and the second checks that a <meta> is present.
In this simple example both checks are written in XPath, but XML Schema, Relax NG, XSLT and XQuery can also be used.
5. Augmenting XML Schema with xchecker
The following XML consists of a father, mother, then some children in any order then pets:
<family>
<father>Jack</father>
<mother>Jill</mother>
<son>Billy</son>
<daughter>Jane</daughter>
<son>Joey</son>
<daughter>Jane</daughter>
<pet>Fido</pet>
</family>
The XML can be described by this XML Schema, which has an extra XPath 2.0 check which ensures all of children's names are different:
<xs:schema xmlns:xck="http://xchecker.sf.net/" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:annotation>
<xs:appinfo>
<xck:check>count(/family/(son|daughter)) = count(distinct-values(/family/(son|daughter)))</xck:check>
</xs:appinfo>
</xs:annotation>
<xs:element name="father" type="xs:string" />
<xs:element name="mother" type="xs:string" />
<xs:element name="son" type="xs:string" />
<xs:element name="daughter" type="xs:string" />
<xs:element name="pet" type="xs:string" />
<xs:group name="children">
<xs:choice>
<xs:element ref="son" />
<xs:element ref="daughter" />
</xs:choice>
</xs:group>
<xs:element name="family">
<xs:complexType>
<xs:sequence>
<xs:element ref="father" />
<xs:element ref="mother" />
<xs:group ref="children" minOccurs="0" maxOccurs="10" />
<xs:element ref="pet" minOccurs="0" maxOccurs="10" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
As two <daughter>
's have the value "Jane" xchecker returns the message:
org.xml.sax.SAXException: XCHECKER check failed: false
for: C:/xchecker/samples/xml/family.xml
using: count(/family/(son|daughter)) = count(distinct-values(/family/(son|daughter)))
6. Running xchecker
To run the validation from Java create an XCheckerSchemaFactory, and then validate as normal:
SchemaFactory schemaFactory = new XCheckerSchemaFactory();
Schema schema = schemaFactory.newSchema(xsd);
Validator validator = schema.newValidator();
validator.validate(new StreamSource(xml));
To run xchecker from Java:
XChecker xchecker = new XChecker();
xchecker.processCheckConfig(checkConfig);
// or xchecker.processCheckSuite(checkSuite);
To run xchecker from the command line:
Use the -c switch when parsing a Check Config, using the -s when its a Check Suite or just pass in the XML and XSD when validating. For example:
java -cp xchecker.jar;lib/*.jar net.sf.xchecker.XChecker -c samples/CheckConfig.xml
java -cp xchecker.jar;lib/*.jar net.sf.xchecker.XChecker -s samples/CheckSuite.xml
java -cp xchecker.jar;lib/*.jar net.sf.xchecker.XChecker samples/xml/stock.xml samples/xsd/stock.xsd
7. An XML Schema for the Check Config
Below is the XML Schema that describes an xchecker Check Config file. It's been augmented with XPath 2.0 checks that ensure the paths used in the config point at parsable XML files.
<xs:schema xmlns:xck="http://xchecker.sf.net/" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://xchecker.sf.net/" targetNamespace="http://xchecker.sf.net/" elementFormDefault="qualified" attributeFormDefault="unqualified">
<xs:annotation>
<xs:appinfo>
<xck:check>//xck:xml/@src/doc-available(resolve-uri(., base-uri(/)))</xck:check>
<xck:check>//xck:transform/@xml/doc-available(resolve-uri(., base-uri(/)))</xck:check>
<xck:check>//xck:transform/@xslt/doc-available(resolve-uri(., base-uri(/)))</xck:check>
</xs:appinfo>
</xs:annotation>
<xs:element name="xchecker" type="xchecker" />
<xs:element name="xml" type="xml-subject" />
<xs:element name="check" type="non-empty-string" />
<xs:element name="transform" type="transform-subject" />
<xs:element name="param" type="param" />
<xs:group name="subjects">
<xs:choice>
<xs:element ref="xml" />
<xs:element ref="transform" />
</xs:choice>
</xs:group>
<xs:complexType name="xchecker">
<xs:sequence>
<xs:group ref="subjects" maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="xml-subject">
<xs:sequence>
<xs:element ref="check" maxOccurs="unbounded" />
</xs:sequence>
<xs:attribute name="src" use="required" type="non-empty-string" />
</xs:complexType>
<xs:complexType name="transform-subject">
<xs:sequence>
<xs:element ref="param" minOccurs="0" maxOccurs="unbounded" />
<xs:element ref="check" maxOccurs="unbounded" />
</xs:sequence>
<xs:attribute name="xml" use="required" type="non-empty-string" />
<xs:attribute name="xslt" use="required" type="non-empty-string" />
<xs:attribute name="result" use="required" type="non-empty-string" />
</xs:complexType>
<xs:complexType name="param">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="name" type="non-empty-string" use="required" />
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:simpleType name="non-empty-string">
<xs:restriction base="xs:string">
<xs:minLength value="1" />
</xs:restriction>
</xs:simpleType>
</xs:schema>