Index: CMDIValidator/trunk/lib/xerces/xercesImpl-patched/2.11.0/xercesImpl-patched-2.11.0.jar.md5
===================================================================
--- CMDIValidator/trunk/lib/xerces/xercesImpl-patched/2.11.0/xercesImpl-patched-2.11.0.jar.md5	(revision 5052)
+++ CMDIValidator/trunk/lib/xerces/xercesImpl-patched/2.11.0/xercesImpl-patched-2.11.0.jar.md5	(revision 5052)
@@ -0,0 +1,1 @@
+b7254ab06d249d7de96cd581e7c3e6fb
Index: CMDIValidator/trunk/lib/xerces/xercesImpl-patched/2.11.0/xercesImpl-patched-2.11.0.jar.sha1
===================================================================
--- CMDIValidator/trunk/lib/xerces/xercesImpl-patched/2.11.0/xercesImpl-patched-2.11.0.jar.sha1	(revision 5052)
+++ CMDIValidator/trunk/lib/xerces/xercesImpl-patched/2.11.0/xercesImpl-patched-2.11.0.jar.sha1	(revision 5052)
@@ -0,0 +1,1 @@
+528ddde0273c406cff6b95210b5729e9aa9694a2
Index: CMDIValidator/trunk/lib/xerces/xercesImpl-patched/2.11.0/xercesImpl-patched-2.11.0.pom
===================================================================
--- CMDIValidator/trunk/lib/xerces/xercesImpl-patched/2.11.0/xercesImpl-patched-2.11.0.pom	(revision 5052)
+++ CMDIValidator/trunk/lib/xerces/xercesImpl-patched/2.11.0/xercesImpl-patched-2.11.0.pom	(revision 5052)
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>xerces</groupId>
+  <artifactId>xercesImpl-patched</artifactId>
+  <version>2.11.0</version>
+  <description>POM was created from install:install-file</description>
+</project>
Index: CMDIValidator/trunk/lib/xerces/xercesImpl-patched/2.11.0/xercesImpl-patched-2.11.0.pom.md5
===================================================================
--- CMDIValidator/trunk/lib/xerces/xercesImpl-patched/2.11.0/xercesImpl-patched-2.11.0.pom.md5	(revision 5052)
+++ CMDIValidator/trunk/lib/xerces/xercesImpl-patched/2.11.0/xercesImpl-patched-2.11.0.pom.md5	(revision 5052)
@@ -0,0 +1,1 @@
+2cdacbe6efb9acd371ddc60a7972a4f7
Index: CMDIValidator/trunk/lib/xerces/xercesImpl-patched/2.11.0/xercesImpl-patched-2.11.0.pom.sha1
===================================================================
--- CMDIValidator/trunk/lib/xerces/xercesImpl-patched/2.11.0/xercesImpl-patched-2.11.0.pom.sha1	(revision 5052)
+++ CMDIValidator/trunk/lib/xerces/xercesImpl-patched/2.11.0/xercesImpl-patched-2.11.0.pom.sha1	(revision 5052)
@@ -0,0 +1,1 @@
+3de028d22bd042913988bba0b66a01c56d0de98c
Index: CMDIValidator/trunk/lib/xerces/xercesImpl-patched/maven-metadata-local.xml
===================================================================
--- CMDIValidator/trunk/lib/xerces/xercesImpl-patched/maven-metadata-local.xml	(revision 5052)
+++ CMDIValidator/trunk/lib/xerces/xercesImpl-patched/maven-metadata-local.xml	(revision 5052)
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<metadata>
+  <groupId>xerces</groupId>
+  <artifactId>xercesImpl-patched</artifactId>
+  <versioning>
+    <release>2.11.0</release>
+    <versions>
+      <version>2.11.0</version>
+    </versions>
+    <lastUpdated>20140407180824</lastUpdated>
+  </versioning>
+</metadata>
Index: CMDIValidator/trunk/lib/xerces/xercesImpl-patched/maven-metadata-local.xml.md5
===================================================================
--- CMDIValidator/trunk/lib/xerces/xercesImpl-patched/maven-metadata-local.xml.md5	(revision 5052)
+++ CMDIValidator/trunk/lib/xerces/xercesImpl-patched/maven-metadata-local.xml.md5	(revision 5052)
@@ -0,0 +1,1 @@
+d2bbebbf1b1241e28dcb5bda620494f1
Index: CMDIValidator/trunk/lib/xerces/xercesImpl-patched/maven-metadata-local.xml.sha1
===================================================================
--- CMDIValidator/trunk/lib/xerces/xercesImpl-patched/maven-metadata-local.xml.sha1	(revision 5052)
+++ CMDIValidator/trunk/lib/xerces/xercesImpl-patched/maven-metadata-local.xml.sha1	(revision 5052)
@@ -0,0 +1,1 @@
+69484c27d7b2c8a5e6f2545ed79e7b61586cfc7f
Index: CMDIValidator/trunk/pom.xml
===================================================================
--- CMDIValidator/trunk/pom.xml	(revision 5052)
+++ CMDIValidator/trunk/pom.xml	(revision 5052)
@@ -0,0 +1,224 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<groupId>eu.clarin.cmdi</groupId>
+	<artifactId>cmdi-validator</artifactId>
+	<version>0.0.1-SNAPSHOT</version>
+
+	<properties>
+		<!-- project settings -->
+		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+		<!-- plug-in settings -->
+		<maven.compiler.source>1.6</maven.compiler.source>
+		<maven.compiler.target>1.6</maven.compiler.target>
+		<!-- versions of dependencies -->
+		<slf4j.version>1.7.6</slf4j.version>
+		<truevfs.version>0.10.6</truevfs.version>
+		<truecommons.version>2.3.4</truecommons.version>
+	</properties>
+
+	<dependencies>
+		<dependency>
+			<groupId>org.slf4j</groupId>
+			<artifactId>slf4j-api</artifactId>
+			<version>${slf4j.version}</version>
+		</dependency>
+
+		<dependency>
+			<groupId>org.slf4j</groupId>
+			<artifactId>slf4j-log4j12</artifactId>
+			<version>${slf4j.version}</version>
+		</dependency>
+
+		<dependency>
+			<groupId>org.apache.httpcomponents</groupId>
+			<artifactId>httpclient</artifactId>
+			<version>4.2.5</version>
+		</dependency>
+
+		<dependency>
+			<groupId>commons-cli</groupId>
+			<artifactId>commons-cli</artifactId>
+			<version>1.2</version>
+		</dependency>
+
+        <dependency>
+            <groupId>com.github.mfornos</groupId>
+            <artifactId>humanize-slim</artifactId>
+            <version>1.1.5</version>
+        </dependency>
+
+ 		<dependency>
+			<groupId>xerces</groupId>
+			<artifactId>xercesImpl-patched</artifactId>
+			<version>2.11.0</version>
+		</dependency>
+
+        <!-- required for Xerces (patched version has no proper dependencies) -->
+        <dependency>
+            <groupId>xml-apis</groupId>
+            <artifactId>xml-apis</artifactId>
+            <version>1.4.01</version>
+        </dependency>
+
+ 		<dependency>
+			<groupId>net.sf.saxon</groupId>
+			<artifactId>Saxon-HE</artifactId>
+			<version>9.5.1-4</version>
+		</dependency>
+
+		<!-- TrueVFS -->
+		<dependency>
+			<groupId>net.java.truevfs</groupId>
+			<artifactId>truevfs-access</artifactId>
+			<version>${truevfs.version}</version>
+		</dependency>
+
+		<dependency>
+			<groupId>net.java.truevfs</groupId>
+			<artifactId>truevfs-kernel-impl</artifactId>
+			<version>${truevfs.version}</version>
+			<scope>runtime</scope>
+		</dependency>
+
+		<dependency>
+			<groupId>net.java.truevfs</groupId>
+			<artifactId>truevfs-ext-logging</artifactId>
+			<version>${truevfs.version}</version>
+			<scope>runtime</scope>
+		</dependency>
+
+		<dependency>
+			<groupId>net.java.truevfs</groupId>
+			<artifactId>truevfs-driver-file</artifactId>
+			<version>${truevfs.version}</version>
+			<scope>runtime</scope>
+		</dependency>
+
+		<dependency>
+			<groupId>net.java.truevfs</groupId>
+			<artifactId>truevfs-driver-zip</artifactId>
+			<version>${truevfs.version}</version>
+			<scope>runtime</scope>
+		</dependency>
+
+		<dependency>
+			<groupId>net.java.truecommons</groupId>
+			<artifactId>truecommons-key-default</artifactId>
+			<version>${truecommons.version}</version>
+			<scope>runtime</scope>
+		</dependency>
+
+		<dependency>
+			<groupId>org.apache.commons</groupId>
+			<artifactId>commons-lang3</artifactId>
+			<version>3.1</version>
+		</dependency>
+
+		<dependency>
+			<groupId>org.apache.commons</groupId>
+			<artifactId>commons-math3</artifactId>
+			<version>3.2</version>
+		</dependency>
+	</dependencies>
+
+	<repositories>
+		<repository>
+			<id>lib</id>
+			<url>file://${basedir}/lib</url>
+		</repository>
+	</repositories>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<version>2.5.1</version>
+				<configuration>
+					<source>${maven.compiler.source}</source>
+					<target>${maven.compiler.target}</target>
+				</configuration>
+			</plugin>
+
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-jar-plugin</artifactId>
+				<version>2.4</version>
+				<configuration>
+					<archive>
+						<manifest>
+							<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
+							<mainClass>eu.clarin.cmdi.validator.CMDIValidatorTool</mainClass>
+						</manifest>
+					</archive>
+				</configuration>
+			</plugin>
+
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-assembly-plugin</artifactId>
+				<version>2.4</version>
+				<executions>
+					<execution>
+						<id>assemble-binary</id>
+						<phase>package</phase>
+						<goals>
+							<goal>single</goal>
+						</goals>
+					</execution>
+				</executions>
+				<configuration>
+					<descriptors>
+						<descriptor>src/main/assembly/bin.xml</descriptor>
+					</descriptors>
+				</configuration>
+			</plugin>
+		</plugins>
+
+		<pluginManagement>
+			<plugins>
+				<!-- This plugin's configuration is used to store Eclipse m2e settings 
+					only. It has no influence on the Maven build itself. -->
+				<plugin>
+					<groupId>org.eclipse.m2e</groupId>
+					<artifactId>lifecycle-mapping</artifactId>
+					<version>1.0.0</version>
+					<configuration>
+						<lifecycleMappingMetadata>
+							<pluginExecutions>
+								<pluginExecution>
+									<pluginExecutionFilter>
+										<groupId>org.apache.maven.plugins</groupId>
+										<artifactId>maven-jar-plugin</artifactId>
+										<versionRange>[0.0,)</versionRange>
+										<goals>
+											<goal>jar</goal>
+										</goals>
+									</pluginExecutionFilter>
+									<action>
+										<ignore />
+									</action>
+								</pluginExecution>
+
+								<pluginExecution>
+									<pluginExecutionFilter>
+										<groupId>org.apache.maven.plugins</groupId>
+										<artifactId>maven-assembly-plugin</artifactId>
+										<versionRange>[0.0,)</versionRange>
+										<goals>
+											<goal>single</goal>
+										</goals>
+									</pluginExecutionFilter>
+									<action>
+										<ignore />
+									</action>
+								</pluginExecution>
+							</pluginExecutions>
+						</lifecycleMappingMetadata>
+					</configuration>
+				</plugin>
+			</plugins>
+		</pluginManagement>
+	</build>
+</project>
Index: CMDIValidator/trunk/src/main/assembly/bin.xml
===================================================================
--- CMDIValidator/trunk/src/main/assembly/bin.xml	(revision 5052)
+++ CMDIValidator/trunk/src/main/assembly/bin.xml	(revision 5052)
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<assembly
+    xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
+    <id>bin</id>
+    <formats>
+        <format>tar.gz</format>
+        <!--
+        <format>zip</format>
+        -->
+    </formats>
+    <baseDirectory>${project.artifactId}</baseDirectory>
+    <files>
+        <file>
+            <source>src/main/bin/cmdi-validator.sh</source>
+            <outputDirectory>bin</outputDirectory>
+            <destName>cmdi-validator</destName>
+            <fileMode>755</fileMode>
+            <lineEnding>unix</lineEnding>
+        </file>
+    </files>
+    <fileSets>
+        <!-- hack to create empty "endorsed" directory -->
+        <fileSet>
+            <directory>src/main/bin/</directory>
+            <outputDirectory>lib/endorsed</outputDirectory>
+            <directoryMode>0755</directoryMode>
+            <excludes>
+                <exclude>*</exclude>
+            </excludes>
+        </fileSet>
+    </fileSets>
+    <dependencySets>
+        <dependencySet>
+            <useProjectArtifact>true</useProjectArtifact>
+            <useTransitiveDependencies>true</useTransitiveDependencies>
+            <outputDirectory>lib</outputDirectory>
+            <directoryMode>0755</directoryMode>
+            <fileMode>0644</fileMode>
+            <excludes>
+                <exclude>*:sources</exclude>
+            </excludes>
+        </dependencySet>
+    </dependencySets>
+</assembly>
Index: CMDIValidator/trunk/src/main/bin/cmdi-validator.sh
===================================================================
--- CMDIValidator/trunk/src/main/bin/cmdi-validator.sh	(revision 5052)
+++ CMDIValidator/trunk/src/main/bin/cmdi-validator.sh	(revision 5052)
@@ -0,0 +1,35 @@
+#!/bin/sh
+
+PRG=`readlink -f $0`
+DIRNAME=`dirname ${PRG}`
+DIRNAME=`readlink -f "${DIRNAME}/../lib/"`
+
+CP=""
+for JAR in ${DIRNAME}/*.jar; do
+    JAR=`readlink -f ${JAR}`
+    if [ -z "${CP}" ]; then
+        CP="${JAR}"
+    else 
+        CP="${CP}":"${JAR}"
+    fi
+done
+
+if [ ! -z "${SAXON_HOME}" ]; then
+    for JAR in ${SAXON_HOME}/*.jar; do
+        JAR=`readlink -f ${JAR}`
+        CP="${JAR}":"${CP}"
+    done
+fi
+
+if [ -d ${DIRNAME}/endorsed ]; then
+    for JAR in ${DIRNAME}/endorsed/*.jar; do
+        JAR=`readlink -f ${JAR}`
+        CP="${JAR}":"${CP}"
+    done
+fi
+
+VM_OPTS=""
+if java -version 2>&1 | grep -q -i '64-bit'; then
+  VM_OPTS="-d64 $VM_OPTS"
+fi
+exec java ${VM_OPTS} -cp "${CP}" eu.clarin.cmdi.validator.tool.CMDIValidatorTool "$@"
Index: CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidator.java
===================================================================
--- CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidator.java	(revision 5052)
+++ CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidator.java	(revision 5052)
@@ -0,0 +1,453 @@
+package eu.clarin.cmdi.validator;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+import javax.xml.XMLConstants;
+import javax.xml.transform.dom.DOMSource;
+
+import net.sf.saxon.s9api.DocumentBuilder;
+import net.sf.saxon.s9api.Processor;
+import net.sf.saxon.s9api.SaxonApiException;
+import net.sf.saxon.s9api.WhitespaceStrippingPolicy;
+import net.sf.saxon.s9api.XdmNode;
+
+import org.apache.xerces.impl.xs.XMLSchemaLoader;
+import org.apache.xerces.impl.xs.XSDDescription;
+import org.apache.xerces.parsers.DOMParser;
+import org.apache.xerces.parsers.XML11Configuration;
+import org.apache.xerces.util.SymbolTable;
+import org.apache.xerces.xni.XMLResourceIdentifier;
+import org.apache.xerces.xni.XNIException;
+import org.apache.xerces.xni.grammars.Grammar;
+import org.apache.xerces.xni.grammars.XMLGrammarDescription;
+import org.apache.xerces.xni.grammars.XMLGrammarPool;
+import org.apache.xerces.xni.parser.XMLEntityResolver;
+import org.apache.xerces.xni.parser.XMLErrorHandler;
+import org.apache.xerces.xni.parser.XMLInputSource;
+import org.apache.xerces.xni.parser.XMLParseException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import eu.clarin.cmdi.validator.CMDIValidatorResult.Severity;
+
+
+public final class CMDIValidator {
+    private static final Logger logger =
+            LoggerFactory.getLogger(CMDIValidator.class);
+    private static final String XML_SCHEMA_LOCATION =
+            "http://www.w3.org/2001/xml.xsd";
+    private static final String XML_SCHEMA_GRAMMAR_TYPE =
+            "http://www.w3.org/2001/XMLSchema";
+    private static final String GRAMMAR_POOL =
+            "http://apache.org/xml/properties/internal/grammar-pool";
+    private static final String NAMESPACES_FEATURE_ID =
+            "http://xml.org/sax/features/namespaces";
+    private static final String VALIDATION_FEATURE_ID =
+            "http://xml.org/sax/features/validation";
+    private static final String SCHEMA_VALIDATION_FEATURE_ID =
+            "http://apache.org/xml/features/validation/schema";
+    private static final String SCHEMA_FULL_CHECKING_FEATURE_ID =
+            "http://apache.org/xml/features/validation/schema-full-checking";
+    private static final String HONOUR_ALL_SCHEMA_LOCATIONS_ID =
+            "http://apache.org/xml/features/honour-all-schemaLocations";
+    private static final int INITAL_SYMBOL_TABLE_SIZE = 16141;
+    private final Processor processor;
+    private final XML11Configuration config;
+    private final DocumentBuilder builder;
+    private final CMDIValidatorResultImpl result;
+
+
+    CMDIValidator(final Processor processor, final SchemaLoader schemaLoader)
+            throws CMDIValidatorInitException {
+        if (processor == null) {
+            throw new NullPointerException("validator == null");
+        }
+        this.processor = processor;
+
+        try {
+            /*
+             * initialize Xerces
+             */
+            XMLEntityResolver resolver = new XMLEntityResolver() {
+                @Override
+                public XMLInputSource resolveEntity(
+                        XMLResourceIdentifier identifier) throws XNIException,
+                        IOException {
+                    final String uri = identifier.getExpandedSystemId();
+                    if (uri == null) {
+                        throw new IOException("bad schema location for namespace '" +
+                                        identifier.getNamespace() + "': " +
+                                        identifier.getLiteralSystemId());
+                    }
+                    InputStream stream = schemaLoader.loadSchemaFile(
+                            identifier.getNamespace(), uri);
+                    return new XMLInputSource(null, null, null, stream, null);
+                }
+            };
+
+            SymbolTable symbols = new SymbolTable(INITAL_SYMBOL_TABLE_SIZE);
+            ShadowCacheXMLGrammarPool pool = new ShadowCacheXMLGrammarPool(8);
+
+            XMLSchemaLoader xsdLoader = new XMLSchemaLoader(symbols);
+            xsdLoader.setParameter(GRAMMAR_POOL, pool);
+            xsdLoader.setEntityResolver(resolver);
+            xsdLoader.setErrorHandler(new XMLErrorHandler() {
+                @Override
+                public void warning(String domain, String key,
+                        XMLParseException e) throws XNIException {
+                    /* ignore warnings */
+                }
+
+                @Override
+                public void error(String domain, String key, XMLParseException e)
+                        throws XNIException {
+                    throw e;
+                }
+
+                @Override
+                public void fatalError(String domain, String key,
+                        XMLParseException e) throws XNIException {
+                    throw e;
+                }
+            });
+
+            InputStream stream = null;
+            try {
+                stream = schemaLoader.loadSchemaFile(XMLConstants.XML_NS_URI,
+                        XML_SCHEMA_LOCATION);
+                Grammar grammar = xsdLoader.loadGrammar(
+                        new XMLInputSource(XMLConstants.XML_NS_URI,
+                                XML_SCHEMA_LOCATION, null, stream, null));
+                if (grammar != null) {
+                    pool.cacheGrammars(XML_SCHEMA_GRAMMAR_TYPE,
+                            new Grammar[] { grammar });
+                }
+                pool.lockPool();
+            } finally {
+                if (stream != null) {
+                    stream.close();
+                }
+            }
+
+            config = new XML11Configuration(symbols, pool);
+            config.setFeature(NAMESPACES_FEATURE_ID, true);
+            config.setFeature(VALIDATION_FEATURE_ID, true);
+            config.setFeature(SCHEMA_VALIDATION_FEATURE_ID, true);
+            config.setFeature(SCHEMA_FULL_CHECKING_FEATURE_ID, true);
+            config.setFeature(HONOUR_ALL_SCHEMA_LOCATIONS_ID, true);
+            config.setEntityResolver(resolver);
+            config.setErrorHandler(new XMLErrorHandler() {
+                @Override
+                public void warning(String domain, String key,
+                        XMLParseException e) throws XNIException {
+                    reportWarning(e.getLineNumber(), e.getColumnNumber(),
+                            e.getMessage(), e);
+                }
+
+                @Override
+                public void error(String domain, String key, XMLParseException e)
+                        throws XNIException {
+                    reportError(e.getLineNumber(), e.getColumnNumber(),
+                            e.getMessage(), e);
+                    throw e;
+                }
+
+                @Override
+                public void fatalError(String domain, String key,
+                        XMLParseException e) throws XNIException {
+                    reportError(e.getLineNumber(), e.getColumnNumber(),
+                            e.getMessage(), e);
+                    throw e;
+                }
+            });
+
+            /*
+             * initialize Saxon
+             */
+            this.builder = this.processor.newDocumentBuilder();
+            this.builder.setWhitespaceStrippingPolicy(WhitespaceStrippingPolicy.IGNORABLE);
+
+            /*
+             * initialize other stuff
+             */
+            this.result =  new CMDIValidatorResultImpl();
+        } catch (IOException e) {
+            throw new CMDIValidatorInitException("initialization failed", e);
+        }
+}
+
+
+    public void validate(InputStream stream, File file,
+            CMDIValidatorJobHandler handler) throws CMDIValidatorException {
+        if (stream == null) {
+            throw new NullPointerException("stream == null");
+        }
+
+        try {
+            result.setFile(file);
+
+            /*
+             * step 1: parse document and perform schema validation
+             */
+            final XdmNode document = parseInstance(stream);
+
+            /*
+             * step 2: perform Schematron validation
+             */
+            if (document != null) {
+                logger.debug("document = {}", document.getNodeName());
+            } else {
+                logger.debug("parseInstance() returned no result");
+            }
+        } catch (CMDIValidatorException e) {
+            throw e;
+        } finally {
+            if (handler != null) {
+                if (result.isHighestSeverity(Severity.ERROR)) {
+                    handler.onValidationFailure(result);
+                } else {
+                    handler.onValidationSuccess(result);
+                }
+            }
+            result.reset();
+        }
+    }
+
+
+    private XdmNode parseInstance(InputStream stream)
+            throws CMDIValidatorException {
+        try {
+            final DOMParser parser = new DOMParser(config);
+            try {
+                parser.parse(new InputSource(stream));
+                stream.close();
+
+                final Document dom = parser.getDocument();
+                if (dom == null) {
+                    throw new CMDIValidatorException(
+                            "parser returned no return result");
+                }
+                return builder.build(new DOMSource(dom));
+            } finally {
+                parser.reset();
+            }
+        } catch (XNIException e) {
+            throw new CMDIValidatorException(e.getMessage(), e);
+        } catch (SaxonApiException e) {
+            throw new CMDIValidatorException(e.getMessage(), e);
+        } catch (SAXException e) {
+            throw new CMDIValidatorException(e.getMessage(), e);
+        } catch (IOException e) {
+            throw new CMDIValidatorException(e.getMessage(), e);
+        } finally {
+            /* make sure, steam is closed */
+            try {
+                stream.close();
+            } catch (IOException e) {
+                /* IGNORE */
+            }
+        }
+    }
+
+
+    //    private void validateSchematron(XdmNode document)
+//            throws CMDIValidatorException {
+//        try {
+//            logger.debug("performing schematron validation ...");
+//            schematronValidator.setSource(document.asSource());
+//            final XdmDestination destination = new XdmDestination();
+//            schematronValidator.setDestination(destination);
+//            schematronValidator.transform();
+//
+////            XdmNode validationReport = destination.getXdmNode();
+//
+//            // return ((net.sf.saxon.value.BooleanValue)
+//            // Saxon.evaluateXPath(validationReport,
+//            // "empty(//svrl:failed-assert[(preceding-sibling::svrl:fired-rule)[last()][empty(@role) or @role!='warning']])").evaluateSingle().getUnderlyingValue()).getBooleanValue();
+//        } catch (SaxonApiException e) {
+//            throw new CMDIValidatorException(
+//                    "error performing schematron validation", e);
+//        }
+//    }
+
+
+    private void reportWarning(int line, int col, String message, Throwable cause) {
+        logger.debug("reporting warning: [{}:{}]: {}", line, col, message);
+        if (result != null) {
+            result.reportWarning(line, col, message, cause);
+        }
+    }
+
+
+    private void reportError(int line, int col, String message, Throwable cause) {
+        logger.debug("reporting error: [{}:{}]: {}", line, col, message);
+        if (result != null) {
+            result.reportError(line, col, message, cause);
+        }
+    }
+
+
+    private static class ShadowCacheXMLGrammarPool implements XMLGrammarPool {
+        private final Set<Grammar> cache =
+                new LinkedHashSet<Grammar>();
+        private final Map<String, Grammar> shadowCache;
+        private boolean locked = false;
+
+
+        private ShadowCacheXMLGrammarPool(int shadowCacheSize) {
+            this.shadowCache = new LRUCache<String, Grammar>(shadowCacheSize);
+        }
+
+
+        @Override
+        public Grammar[] retrieveInitialGrammarSet(String grammarType) {
+            if (XML_SCHEMA_GRAMMAR_TYPE.equals(grammarType) &&
+                    !cache.isEmpty()) {
+                final Grammar[] result = new Grammar[cache.size()];
+                return cache.toArray(result);
+            } else {
+                return null;
+            }
+        }
+
+
+        @Override
+        public Grammar retrieveGrammar(XMLGrammarDescription d) {
+            logger.debug("search for grammar: {} / {} / {} / {}",
+                    d.getNamespace(),
+                    d.getLiteralSystemId(),
+                    d.getExpandedSystemId(),
+                    d.getBaseSystemId());
+            if ((d.getNamespace() == null) || !(d instanceof XSDDescription)) {
+                logger.debug("-> miss (invalid arguments supplied by caller)");
+                return null;
+            }
+
+            final XSDDescription desc = (XSDDescription) d;
+            final String namespace = desc.getNamespace();
+            Grammar result = findGrammerFromCache(desc);
+            if (result != null) {
+                logger.debug("-> match from cache: {} -> {} / {}",
+                        namespace,
+                        desc.getNamespace(),
+                        desc.getLiteralSystemId());
+                return result;
+            }
+
+            String locationHint = null;
+            if (desc.getLocationHints() != null) {
+                String[] h = desc.getLocationHints();
+                if (h.length > 0) {
+                    locationHint = h[0];
+                }
+                logger.debug("-> hint: {}", locationHint);
+            } else if (desc.getLiteralSystemId() != null) {
+                locationHint = desc.getLiteralSystemId();
+            }
+
+            if (locationHint != null) {
+                Grammar grammar = shadowCache.get(locationHint);
+                if (grammar != null) {
+                    logger.debug("-> match from shadow cache: {} -> {}",
+                            grammar.getGrammarDescription().getNamespace(),
+                            locationHint);
+                    return grammar;
+                }
+            }
+            logger.debug("-> miss");
+            return null;
+        }
+
+
+        @Override
+        public void lockPool() {
+            locked = true;
+        }
+
+
+        @Override
+        public void unlockPool() {
+            locked = false;
+        }
+
+
+        @Override
+        public void clear() {
+            if (!locked) {
+                cache.clear();
+            }
+        }
+
+
+        @Override
+        public void cacheGrammars(String grammarType, Grammar[] grammars) {
+            if (XML_SCHEMA_GRAMMAR_TYPE.equals(grammarType) &&
+                    (grammars != null) &&
+                    (grammars.length > 0)) {
+                for (Grammar grammar : grammars) {
+                    final XMLGrammarDescription gd =
+                            grammar.getGrammarDescription();
+                    if (findGrammerFromCache(gd) == null) {
+                        if (!locked) {
+                            logger.debug("cached grammar: {} / {}",
+                                    gd.getNamespace(),
+                                    gd.getLiteralSystemId());
+                            cache.add(grammar);
+                        } else {
+                            final String literalSystemId =
+                                        gd.getLiteralSystemId();
+                            if (!shadowCache.containsKey(literalSystemId)) {
+                                logger.debug("shadow cached grammar: {} / {}",
+                                        gd.getNamespace(),
+                                        gd.getLiteralSystemId());
+                                shadowCache.put(literalSystemId, grammar);
+                            }
+                        }
+                    }
+                } // for
+            }
+        }
+
+
+        private Grammar findGrammerFromCache(XMLGrammarDescription desc) {
+            if (!cache.isEmpty()) {
+                for (Grammar grammar : cache) {
+                    final XMLGrammarDescription gd =
+                            grammar.getGrammarDescription();
+                    if (gd.getNamespace().equals(desc.getNamespace())) {
+                        return grammar;
+                    }
+                }
+            }
+            return null;
+        }
+    }  // class ShadowCacheGrammarPool
+
+
+    @SuppressWarnings("serial")
+    private static class LRUCache<A, B> extends LinkedHashMap<A, B> {
+        private final int maxEntries;
+
+
+        public LRUCache(final int maxEntries) {
+            super(maxEntries + 1, 1.0f, true);
+            this.maxEntries = maxEntries;
+        }
+
+
+        @Override
+        protected boolean removeEldestEntry(final Map.Entry<A, B> eldest) {
+            return super.size() > maxEntries;
+        }
+    } // class LRUCache
+
+} // class CMDIValidator
Index: CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidatorEngine.java
===================================================================
--- CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidatorEngine.java	(revision 5052)
+++ CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidatorEngine.java	(revision 5052)
@@ -0,0 +1,225 @@
+package eu.clarin.cmdi.validator;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.java.truevfs.access.TFile;
+import net.java.truevfs.access.TFileInputStream;
+
+
+public class CMDIValidatorEngine {
+    private static final Logger logger =
+            LoggerFactory.getLogger(CMDIValidatorEngine.class);
+    private final CMDIValidatorFactory validatorFactory;
+    private final int threadCount;
+    private boolean running = false;
+    private ThreadGroup workers;
+    private ExecutorService executor;
+    private JobList jobList;
+
+
+    public CMDIValidatorEngine(CMDIValidatorFactory validatorFactory,
+            int threadCount) {
+        if (validatorFactory == null) {
+            throw new NullPointerException("validatorFactory == null");
+        }
+        this.validatorFactory = validatorFactory;
+        this.threadCount      = threadCount;
+    }
+
+
+    public CMDIValidatorEngine(CMDIValidatorFactory validatorFactory) {
+        this(validatorFactory, Runtime.getRuntime().availableProcessors());
+    }
+
+
+    public synchronized void start() throws CMDIValidatorInitException {
+        if (running) {
+            throw new IllegalStateException("engine already started");
+        }
+        logger.debug("start validation engine");
+
+        jobList = new JobList();
+
+        workers = new ThreadGroup("Validation-Workers");
+        final ThreadFactory threadFactory = new ThreadFactory() {
+            private int id = 0;
+            @Override
+            public Thread newThread(Runnable target) {
+                final String name = String.format("worker-%02x", id++);
+                return new Thread(workers, target, name);
+            }
+        };
+        final CountDownLatch goLatch = new CountDownLatch(1);
+        executor = Executors.newFixedThreadPool(threadCount, threadFactory);
+        for (int i = 0; i < threadCount; i++) {
+            final CMDIValidator validator = validatorFactory.newValidator();
+            final Worker worker = new Worker(jobList, validator, goLatch);
+            executor.submit(worker);
+        }
+        goLatch.countDown();
+        running = true;
+    }
+
+
+    public synchronized void shutdown() {
+        logger.debug("shutdown validation engine");
+        executor.shutdownNow();
+        try {
+            executor.awaitTermination(5000, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+        }
+    }
+
+
+    public void submit(CMDIValidatorJob job) {
+        if (job == null) {
+            throw new NullPointerException("job == null");
+        }
+        if (!running) {
+            throw new IllegalStateException("engine not started");
+        }
+        jobList.put(job);
+    }
+
+
+    private static void validateFile(final CMDIValidator validator,
+            final CMDIValidatorJob job, final TFile file) {
+        try {
+            TFileInputStream stream = new TFileInputStream(file);
+            try {
+                logger.debug("validating file '{}' ({} bytes)",
+                        file, file.length());
+                validator.validate(stream, file, job.getHandler());
+            } catch (CMDIValidatorException e) {
+                logger.debug("validation of file '{}' failed", file, e);
+            } finally {
+                stream.close();
+            }
+        } catch (IOException e) {
+            logger.error("error reading file '{}': {}",
+                    file, e.getMessage());
+        }
+    }
+
+
+    private static final class JobList {
+        private static final class Holder {
+            private CMDIValidatorJob job;
+            private TFile file;
+
+            private void clear() {
+                job  = null;
+                file = null;
+            }
+        }
+        private final List<CMDIValidatorJob> jobs =
+                    new ArrayList<CMDIValidatorJob>();
+        private int idx = 0;
+
+
+        private void put(CMDIValidatorJob job) {
+            synchronized (jobs) {
+                jobs.add(job);
+
+                /* let the waiting herd stampede ... */
+                jobs.notifyAll();
+            } // synchronized (jobs)
+        }
+
+
+        private void get(JobList.Holder holder)
+                throws InterruptedException {
+            holder.clear();
+
+            for (;;) {
+                synchronized (jobs) {
+                    if (!jobs.isEmpty()) {
+                        holder.job  = jobs.get(idx++);
+                        holder.file = holder.job.getFileForProcessing(jobs);
+                        if (idx >= jobs.size()) {
+                            idx = 0;
+                        }
+                        if (holder.file != null) {
+                            break;
+                        } else {
+                            holder.clear();
+                        }
+                    }
+
+                    /* no cookie for me, wait for someone to fill the jar ... */
+                    jobs.wait();
+                } // synchronized (jobs)
+            } // for
+        }
+
+
+        private void remove(CMDIValidatorJob job) {
+            synchronized (jobs) {
+                jobs.remove(job);
+                if (idx >= jobs.size()) {
+                    idx = 0;
+                }
+            } // synchronized (jobs)
+        }
+    } // inner class JobList
+
+
+    private static final class Worker implements Runnable {
+        private final CountDownLatch goLatch;
+        private final JobList jobList;
+        private final CMDIValidator validator;
+
+
+        private Worker(JobList jobList, CMDIValidator validator,
+                CountDownLatch goLatch) {
+            this.jobList   = jobList;
+            this.validator = validator;
+            this.goLatch   = goLatch;
+        }
+
+
+        @Override
+        public void run() {
+            try {
+                final JobList.Holder holder = new JobList.Holder();
+
+                // wait for start signal
+                goLatch.await();
+
+                // loop for work ...
+                for (;;) {
+                    if (Thread.currentThread().isInterrupted()) {
+                        break;
+                    }
+                    jobList.get(holder);
+
+                    final CMDIValidatorJob job = holder.job;
+                    final TFile file           = holder.file;
+                    try {
+                        validateFile(validator, job, file);
+                    } finally {
+                        if (job.returnFileFromProcessing(file)) {
+                            jobList.remove(job);
+                        }
+                    }
+                }
+            } catch (InterruptedException e) {
+                /* IGNORE */
+            } catch (Throwable e) {
+                logger.error("unexpected error in worker thread: {}",
+                        e.getMessage(), e);
+            }
+        }
+    }
+
+} // class CMDIValidatorExecutor
Index: CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidatorException.java
===================================================================
--- CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidatorException.java	(revision 5052)
+++ CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidatorException.java	(revision 5052)
@@ -0,0 +1,13 @@
+package eu.clarin.cmdi.validator;
+
+@SuppressWarnings("serial")
+public class CMDIValidatorException extends Exception {
+    public CMDIValidatorException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+
+    public CMDIValidatorException(String message) {
+        super(message);
+    }
+} // class CMDIValidatorException
Index: CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidatorFactory.java
===================================================================
--- CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidatorFactory.java	(revision 5052)
+++ CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidatorFactory.java	(revision 5052)
@@ -0,0 +1,74 @@
+package eu.clarin.cmdi.validator;
+
+import java.io.File;
+
+import net.sf.saxon.s9api.Processor;
+
+import org.apache.commons.lang3.SystemUtils;
+
+public class CMDIValidatorFactory {
+    private final Processor processor = new Processor(true);
+    private final SchemaLoader schemaLoader;
+
+
+    private CMDIValidatorFactory(File cacheDirectory)
+            throws CMDIValidatorInitException {
+        if (cacheDirectory == null) {
+            if (SystemUtils.IS_OS_WINDOWS &&
+                    (SystemUtils.JAVA_IO_TMPDIR != null)) {
+                cacheDirectory = new File(SystemUtils.JAVA_IO_TMPDIR,
+                        "cmdi-validator");
+            } else if (SystemUtils.IS_OS_UNIX &&
+                    (SystemUtils.USER_HOME != null)) {
+                cacheDirectory = new File(SystemUtils.USER_HOME,
+                        ".cmdi-validator");
+            }
+            if (cacheDirectory != null) {
+                if (!cacheDirectory.exists()) {
+                    if (!cacheDirectory.mkdir()) {
+                        throw new CMDIValidatorInitException(
+                                "cannot create cache directory: " +
+                                        cacheDirectory);
+                    }
+                }
+            } else {
+                if (SystemUtils.JAVA_IO_TMPDIR == null) {
+                    throw new CMDIValidatorInitException(
+                            "cannot determine temporary directory");
+                }
+                cacheDirectory = new File(SystemUtils.JAVA_IO_TMPDIR);
+            }
+        } else {
+            if (!cacheDirectory.isDirectory()) {
+                throw new CMDIValidatorInitException(
+                        "supplied cache dircetory '" +
+                                cacheDirectory.getAbsolutePath() +
+                                "' is not a directory");
+            }
+            if (!cacheDirectory.canWrite()) {
+                throw new CMDIValidatorInitException("cache dircetory '" +
+                        cacheDirectory.getAbsolutePath() + "' is not writable");
+            }
+        }
+        schemaLoader = new SchemaLoader(cacheDirectory,
+                SchemaLoader.DISABLE_CACHE_AGING);
+    }
+
+
+    public CMDIValidator newValidator() throws CMDIValidatorInitException {
+        return new CMDIValidator(processor, schemaLoader);
+    }
+
+
+    public static CMDIValidatorFactory newInstance(File cacheDircetory)
+            throws CMDIValidatorInitException {
+        return new CMDIValidatorFactory(cacheDircetory);
+    }
+
+
+    public static CMDIValidatorFactory newInstance()
+            throws CMDIValidatorInitException {
+        return new CMDIValidatorFactory(null);
+    }
+
+} // class CMDIValidatorFactory
Index: CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidatorInitException.java
===================================================================
--- CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidatorInitException.java	(revision 5052)
+++ CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidatorInitException.java	(revision 5052)
@@ -0,0 +1,15 @@
+package eu.clarin.cmdi.validator;
+
+@SuppressWarnings("serial")
+public class CMDIValidatorInitException extends Exception {
+
+    public CMDIValidatorInitException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+
+    public CMDIValidatorInitException(String message) {
+        super(message);
+    }
+
+} // class CMDIValidatorInitException
Index: CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidatorJob.java
===================================================================
--- CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidatorJob.java	(revision 5052)
+++ CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidatorJob.java	(revision 5052)
@@ -0,0 +1,167 @@
+package eu.clarin.cmdi.validator;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import net.java.truevfs.access.TFile;
+
+
+public class CMDIValidatorJob {
+    private final CMDIValidatorJobHandler handler;
+    private final FileEnumerator fileEnumerator;
+    private int filesInProcessing = 0;
+    private boolean canceled = false;
+
+
+    public CMDIValidatorJob(TFile root, CMDIValidatorJobHandler handler) {
+        if (root == null) {
+            throw new NullPointerException("root = null");
+        }
+
+        /*
+         * XXX: maybe allow null handler or even a list of handlers?
+         */
+        if (handler == null) {
+            throw new NullPointerException("handler == null");
+        }
+        this.handler = handler;
+        this.fileEnumerator = new FileEnumerator(root);
+        this.handler.onJobStarted();
+    }
+
+
+    public void cancel() {
+        synchronized (this) {
+            if (canceled) {
+                throw new IllegalStateException("already canceled");
+            }
+            canceled = true;
+            fileEnumerator.flush();
+            try {
+                this.wait();
+            } catch (InterruptedException e) {
+                /* IGNORE */
+            }
+        }
+    }
+
+
+    final CMDIValidatorJobHandler getHandler() {
+        return handler;
+    }
+
+
+    final TFile getFileForProcessing(List<CMDIValidatorJob> jobList) {
+        synchronized (this) {
+            TFile file = null;
+            if (!canceled) {
+                file = fileEnumerator.nextFile();
+                if (file != null) {
+                    filesInProcessing++;
+                }
+            }
+
+            if (fileEnumerator.isEmpty() || (file == null)) {
+                jobList.remove(this);
+            }
+            return file;
+        } // synchronized (this)
+    }
+
+
+    final boolean returnFileFromProcessing(TFile file) {
+        synchronized (this) {
+            filesInProcessing--;
+            if (filesInProcessing < 0) {
+                throw new IllegalStateException("filesInProcessing < 0");
+            }
+            if (fileEnumerator.isEmpty() && (filesInProcessing == 0)) {
+                handler.onJobFinished(canceled);
+                if (canceled) {
+                    this.notifyAll();
+                    return true;
+                }
+            }
+        } // synchronized (this)
+        return false;
+    }
+
+
+    private final static class FileEnumerator {
+        private static final class FileList {
+            private final TFile[] fileList;
+            private int idx = 0;
+
+
+            private FileList(TFile[] fileList) {
+                this.fileList = fileList;
+            }
+
+
+            private TFile nextFile() {
+                if (idx < fileList.length) {
+                    return fileList[idx++];
+                } else {
+                    return null;
+                }
+            }
+
+            private int size() {
+                return (fileList.length - idx);
+            }
+        }
+        private final LinkedList<FileList> stack =
+                new LinkedList<FileList>();
+
+
+        private FileEnumerator(TFile root) {
+            if (root == null) {
+                throw new NullPointerException("root == null");
+            }
+            if (!root.isDirectory()) {
+                throw new IllegalArgumentException("root is not a directory");
+            }
+            pushDirectory(root);
+        }
+
+
+        private boolean isEmpty() {
+            return stack.isEmpty();
+        }
+
+
+        private TFile nextFile() {
+            for (;;) {
+                if (stack.isEmpty()) {
+                    break;
+                }
+                final FileList list = stack.peek();
+                final TFile file = list.nextFile();
+                if ((list.size() == 0) || (file == null)) {
+                    stack.pop();
+                    if (file == null) {
+                        continue;
+                    }
+                }
+                if (file.isDirectory()) {
+                    pushDirectory(file);
+                    continue;
+                }
+                return file;
+            }
+            return null;
+        }
+
+        private void flush() {
+            stack.clear();
+        }
+
+        private void pushDirectory(TFile directory) {
+            final TFile[] files = directory.listFiles();
+            if ((files != null) && (files.length > 0)) {
+                stack.push(new FileList(files));
+            }
+        }
+    }
+
+} // class CMDIValidatorJob
Index: CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidatorJobHandler.java
===================================================================
--- CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidatorJobHandler.java	(revision 5052)
+++ CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidatorJobHandler.java	(revision 5052)
@@ -0,0 +1,16 @@
+package eu.clarin.cmdi.validator;
+
+
+public interface CMDIValidatorJobHandler {
+    public void onJobStarted();
+
+
+    public void onJobFinished(boolean wasCanceled);
+
+
+    public void onValidationSuccess(CMDIValidatorResult result);
+
+
+    public void onValidationFailure(CMDIValidatorResult result);
+
+} // interface CMDIValidatorJobHandler
Index: CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidatorJobHandlerAdapter.java
===================================================================
--- CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidatorJobHandlerAdapter.java	(revision 5052)
+++ CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidatorJobHandlerAdapter.java	(revision 5052)
@@ -0,0 +1,25 @@
+package eu.clarin.cmdi.validator;
+
+
+public class CMDIValidatorJobHandlerAdapter implements CMDIValidatorJobHandler {
+
+    @Override
+    public void onJobStarted() {
+    }
+
+
+    @Override
+    public void onJobFinished(boolean wasCanceled) {
+    }
+
+        
+    @Override
+    public void onValidationSuccess(CMDIValidatorResult result) {
+    }
+
+
+    @Override
+    public void onValidationFailure(CMDIValidatorResult result) {
+    }
+
+} // class CMDIValidatorJobHandlerAdapter
Index: CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidatorResult.java
===================================================================
--- CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidatorResult.java	(revision 5052)
+++ CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidatorResult.java	(revision 5052)
@@ -0,0 +1,71 @@
+package eu.clarin.cmdi.validator;
+
+import java.io.File;
+import java.util.List;
+
+
+public interface CMDIValidatorResult {
+    public enum Severity {
+        INFO {
+            @Override
+            public String getShortcut() {
+                return "I";
+            }
+        },
+        WARNING {
+            @Override
+            public String getShortcut() {
+                return "W";
+            }
+        },
+        ERROR {
+            @Override
+            public String getShortcut() {
+                return "E";
+            }
+        };
+
+        public abstract String getShortcut();
+    } // enum Severity
+
+    public interface Message {
+        public Severity getSeverity();
+
+
+        public int getLineNumber();
+
+
+        public int getColumnNumber();
+
+
+        public String getMessage();
+
+
+        public Throwable getCause();
+    } // interface Message
+
+
+    public File getFile();
+
+
+    public Severity getHighestSeverity();
+
+
+    public boolean isHighestSeverity(Severity severity);
+
+
+    public List<Message> getMessages();
+
+
+    public Message getFirstMessage();
+
+
+    public Message getFirstMessage(Severity severity);
+
+
+    public int getMessageCount();
+
+
+    public int getMessageCount(Severity severity);
+
+} // CMDIValidatorResult
Index: CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidatorResultImpl.java
===================================================================
--- CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidatorResultImpl.java	(revision 5052)
+++ CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/CMDIValidatorResultImpl.java	(revision 5052)
@@ -0,0 +1,204 @@
+package eu.clarin.cmdi.validator;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+
+final class CMDIValidatorResultImpl implements CMDIValidatorResult {
+    private static class MessageImpl implements CMDIValidatorResult.Message {
+        private Severity severity;
+        private int line;
+        private int col;
+        private String message;
+        private Throwable cause;
+
+
+        private MessageImpl(Severity severity, int line, int col, String message,
+                Throwable cause) {
+            this.severity = severity;
+            this.line = line;
+            this.col = col;
+            this.message = message;
+        }
+
+
+        @Override
+        public Severity getSeverity() {
+            return severity;
+        }
+
+
+        @Override
+        public int getLineNumber() {
+            return line;
+        }
+
+
+        @Override
+        public int getColumnNumber() {
+            return col;
+        }
+
+
+        @Override
+        public String getMessage() {
+            return message;
+        }
+
+
+        @Override
+        public Throwable getCause() {
+            return cause;
+        }
+    }
+    private final List<Message> entries = new LinkedList<Message>();
+    private File file;
+    private Severity highestSeverity;
+
+
+    CMDIValidatorResultImpl() {
+        reset();
+    }
+
+
+    @Override
+    public File getFile() {
+        return file;
+    }
+
+
+    @Override
+    public Severity getHighestSeverity() {
+        return highestSeverity;
+    }
+
+
+    @Override
+    public boolean isHighestSeverity(Severity severity) {
+        if (severity == null) {
+            throw new NullPointerException("severity == null");
+        }
+        return this.highestSeverity.equals(severity);
+    }
+
+
+    public boolean isEmpty() {
+        return entries.isEmpty();
+    }
+
+
+    @Override
+    public List<Message> getMessages() {
+        if (entries.isEmpty()) {
+            return Collections.emptyList();
+        } else {
+            return Collections.unmodifiableList(entries);
+        }
+    }
+
+
+    @Override
+    public Message getFirstMessage() {
+        return entries.isEmpty() ? null : entries.get(0);
+    }
+
+
+    @Override
+    public Message getFirstMessage(Severity severity) {
+        if (severity == null) {
+            throw new NullPointerException("severity == null");
+        }
+
+        if (!entries.isEmpty()) {
+            for (Message msg : entries) {
+                if (severity.equals(msg.getSeverity())) {
+                    return msg;
+                }
+            }
+        }
+        return null;
+    }
+
+
+    @Override
+    public int getMessageCount() {
+        return entries.size();
+    }
+
+
+    @Override
+    public int getMessageCount(Severity severity) {
+        if (severity == null) {
+            throw new NullPointerException("severity == null");
+        }
+
+        int count = 0;
+        if (!entries.isEmpty()) {
+            for (Message msg : entries) {
+                if (severity.equals(msg.getSeverity())) {
+                    count++;
+                }
+            }
+        }
+        return count;
+    }
+
+
+    void setFile(File file) {
+        this.file = file;
+    }
+
+
+    void reportInfo(int line, int col, String message) {
+        reportWarning(line, col, message, null);
+    }
+
+
+    void reportInfo(int line, int col, String message, Throwable cause) {
+        entries.add(new MessageImpl(Severity.INFO, line, col, message, cause));
+    }
+
+
+    void reportWarning(int line, int col, String message) {
+        reportWarning(line, col, message, null);
+    }
+
+
+    void reportWarning(int line, int col, String message, Throwable cause) {
+        switch (highestSeverity) {
+        case WARNING:
+            /* FALL-THROUGH */
+        case ERROR:
+            break;
+        default:
+            highestSeverity = Severity.WARNING;
+        }
+        entries.add(new MessageImpl(Severity.WARNING, line, col, message, cause));
+    }
+
+
+    void reportError(int line, int col, String message) {
+        reportError(line, col, message, null);
+    }
+
+
+    void reportError(int line, int col, String message, Throwable cause) {
+        switch (highestSeverity) {
+        case ERROR:
+            break;
+        default:
+            highestSeverity = Severity.ERROR;
+        }
+        entries.add(new MessageImpl(Severity.ERROR, line, col, message, cause));
+    }
+
+
+    void reset() {
+        this.entries.clear();
+        this.file = null;
+        this.highestSeverity = Severity.INFO;
+    }
+
+} // class CMDIValidatorMessages
Index: CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/SchemaLoader.java
===================================================================
--- CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/SchemaLoader.java	(revision 5052)
+++ CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/SchemaLoader.java	(revision 5052)
@@ -0,0 +1,214 @@
+package eu.clarin.cmdi.validator;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import javax.xml.XMLConstants;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.StatusLine;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.params.ClientPNames;
+import org.apache.http.client.utils.HttpClientUtils;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.params.CoreProtocolPNames;
+import org.apache.http.params.HttpParams;
+import org.apache.http.util.EntityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+final class SchemaLoader {
+    static final long DISABLE_CACHE_AGING = -1;
+    private static final Logger logger =
+            LoggerFactory.getLogger(SchemaLoader.class);
+    private static final String XML_XSD_RESSOURCE = "/xml.xsd";
+    private final File cacheDirectory;
+    private final long maxCacheAge;
+    private final HttpClient httpClient;
+
+
+    SchemaLoader(File cacheDirectory, long maxCacheAge) {
+        if (cacheDirectory == null) {
+            throw new NullPointerException("cacheDirectory == null");
+        }
+        if (maxCacheAge < -1) {
+            throw new IllegalArgumentException("maxCacheAge < -1");
+        }
+        this.cacheDirectory = cacheDirectory;
+        this.maxCacheAge = maxCacheAge;
+        this.httpClient = new DefaultHttpClient();
+
+        final HttpParams params = this.httpClient.getParams();
+        params.setParameter(CoreProtocolPNames.USER_AGENT,
+                this.getClass().getPackage().getName() + "/0.0.1");
+        params.setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, Boolean.TRUE);
+        params.setBooleanParameter(ClientPNames.ALLOW_CIRCULAR_REDIRECTS, true);
+        params.setIntParameter(ClientPNames.MAX_REDIRECTS, 16);
+    }
+
+
+    public synchronized InputStream loadSchemaFile(String targetNamespace,
+            String schemaLocation) throws IOException {
+        if (targetNamespace == null) {
+            throw new NullPointerException("targetNamespace == null");
+        }
+        if (schemaLocation == null) {
+            throw new NullPointerException("schemaLocation == null");
+        }
+
+        logger.debug("loading schema: targetNamespace={}, location={}",
+                targetNamespace, schemaLocation);
+        InputStream stream = null;
+        if (XMLConstants.XML_NS_URI.equalsIgnoreCase(targetNamespace)) {
+            stream = this.getClass().getResourceAsStream(XML_XSD_RESSOURCE);
+            if (stream != null) {
+                logger.debug("using bundled schema for '{}'", schemaLocation);
+                return stream;
+            }
+            logger.warn("unable to load bundled schema for '{}', " +
+                    "falling back to download.", schemaLocation);
+        }
+
+        final String key = stripBadChars(schemaLocation);
+
+        // fall back to file cache ...
+        final File cacheFile = makeCacheFile(key);
+        if (cacheFile.exists()) {
+            final long age =
+                    System.currentTimeMillis() - cacheFile.lastModified();
+            if ((maxCacheAge != DISABLE_CACHE_AGING) && (age > maxCacheAge)) {
+                logger.debug("-> cached file '{}' expired", cacheFile);
+                cacheFile.delete();
+            } else {
+                logger.debug("-> from file cache");
+                return new FileInputStream(cacheFile);
+            }
+        }
+
+        try {
+            logger.debug("-> downloading schema from '{}'", schemaLocation);
+            final URI uri = new URI(schemaLocation);
+            final HttpResponse response = executeRequest(uri);
+            final HttpEntity entity = response.getEntity();
+            if (entity == null) {
+                throw new IOException("the request returned no message body");
+            }
+
+            try {
+                final InputStream in = entity.getContent();
+
+                final FileOutputStream out =
+                        new FileOutputStream(cacheFile);
+                int read;
+                final byte[] buffer = new byte[4096];
+                while ((read = in.read(buffer)) != -1) {
+                    out.write(buffer, 0, read);
+                }
+                out.close();
+
+                return new FileInputStream(cacheFile);
+            } catch (IllegalStateException e) {
+                throw new IOException("error reading response", e);
+            } catch (IOException e) {
+                /* delete broken cache file */
+                if (cacheFile != null) {
+                    cacheFile.delete();
+                }
+                throw e;
+            } finally {
+                /* make sure to release allocated resources */
+                HttpClientUtils.closeQuietly(response);
+            }
+        } catch (URISyntaxException e) {
+            throw new IOException("schemaLocation uri is invalid: " +
+                    schemaLocation, e);
+        }
+    }
+
+
+    private File makeCacheFile(String key) {
+        return new File(cacheDirectory, key + ".xsd");
+    }
+
+
+    private static String stripBadChars(String uri) {
+        final StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < uri.length(); i++) {
+            final char c = uri.charAt(i);
+            switch (c) {
+            case ':':
+                /* FALL-THROUGH */
+            case '/':
+                /* FALL-THROUGH */
+            case '?':
+                /* FALL-THROUGH */
+            case '&':
+                /* FALL-THROUGH */
+            case '=':
+                /* FALL-THROUGH */
+            case '.':
+                /* FALL-THROUGH */
+            case ';':
+                /* FALL-THROUGH */
+            case '"':
+                /* FALL-THROUGH */
+            case '\'':
+                /* FALL-THROUGH */
+            case '\\':
+                sb.append('_');
+                break;
+            default:
+                sb.append(c);
+            }
+        }
+        return sb.toString();
+    }
+
+
+    private HttpResponse executeRequest(URI uri) throws IOException {
+        HttpGet request = null;
+        HttpResponse response = null;
+        try {
+            logger.debug("submitting HTTP request: {}", uri.toString());
+            request = new HttpGet(uri);
+            response = httpClient.execute(request);
+            StatusLine status = response.getStatusLine();
+            if (status.getStatusCode() != HttpStatus.SC_OK) {
+                if (status.getStatusCode() == HttpStatus.SC_NOT_FOUND) {
+                    throw new IOException("not found: " + uri);
+                } else {
+                    throw new IOException("unexpected status: " +
+                            status.getStatusCode());
+                }
+            }
+            return response;
+        } catch (IOException e) {
+            /*
+             * if an error occurred, make sure we are freeing up the resources
+             * we've used
+             */
+            if (response != null) {
+                try {
+                    EntityUtils.consume(response.getEntity());
+                } catch (IOException ex) {
+                    /* IGNORE */
+                }
+
+                /* make sure to release allocated resources */
+                HttpClientUtils.closeQuietly(response);
+            }
+            if (request != null) {
+                request.abort();
+            }
+            throw e;
+        }
+    }
+
+} // class SchemaLoader
Index: CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/tool/CMDIValidatorTool.java
===================================================================
--- CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/tool/CMDIValidatorTool.java	(revision 5052)
+++ CMDIValidator/trunk/src/main/java/eu/clarin/cmdi/validator/tool/CMDIValidatorTool.java	(revision 5052)
@@ -0,0 +1,512 @@
+package eu.clarin.cmdi.validator.tool;
+
+import humanize.Humanize;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.util.Locale;
+import java.util.concurrent.TimeUnit;
+
+import net.java.truevfs.access.TFile;
+import net.java.truevfs.access.TVFS;
+import net.java.truevfs.kernel.spec.FsSyncException;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.GnuParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.OptionBuilder;
+import org.apache.commons.cli.OptionGroup;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import eu.clarin.cmdi.validator.CMDIValidator;
+import eu.clarin.cmdi.validator.CMDIValidatorEngine;
+import eu.clarin.cmdi.validator.CMDIValidatorFactory;
+import eu.clarin.cmdi.validator.CMDIValidatorInitException;
+import eu.clarin.cmdi.validator.CMDIValidatorJob;
+import eu.clarin.cmdi.validator.CMDIValidatorJobHandlerAdapter;
+import eu.clarin.cmdi.validator.CMDIValidatorResult;
+import eu.clarin.cmdi.validator.CMDIValidatorResult.Message;
+import eu.clarin.cmdi.validator.CMDIValidatorResult.Severity;
+
+
+public class CMDIValidatorTool {
+    private static final String PRG_NAME                = "cmdi-xml-validator";
+    private static final long DEFAULT_PROGRESS_INTERVAL = 15000;
+    private static final Locale LOCALE                  = Locale.ENGLISH;
+    private static final char OPT_DEBUG                 = 'd';
+    private static final char OPT_QUIET                 = 'q';
+    private static final char OPT_VERBOSE               = 'v';
+    private static final char OPT_NO_PROGRESS           = 'P';
+    private static final char OPT_THREAD_COUNT          = 't';
+    private static final char OPT_NO_THREADS            = 'T';
+    private static final char OPT_NO_ESTIMATE           = 'E';
+    private static final char OPT_SCHEMA_CACHE_DIR      = 'S';
+    private static final Logger logger =
+            LoggerFactory.getLogger(CMDIValidatorTool.class);
+    private static final org.apache.log4j.ConsoleAppender appender;
+
+
+    public static void main(String[] args) {
+        /*
+         * application defaults
+         */
+        boolean debugging     = false;
+        boolean quiet         = false;
+        boolean verbose       = false;
+        int threadCount       = Runtime.getRuntime().availableProcessors();
+        boolean estimate      = true;
+        long progressInterval = DEFAULT_PROGRESS_INTERVAL;
+        File schemaCacheDir   = null;
+
+        /*
+         * setup command line parser
+         */
+        final Options options = createCommandLineOptions();
+        try {
+            final CommandLineParser parser = new GnuParser();
+            final CommandLine line = parser.parse(options, args);
+            // check incompatible combinations
+            if (line.hasOption(OPT_THREAD_COUNT) && line.hasOption(OPT_NO_THREADS)) {
+                throw new ParseException("The -t and -T option are mutually exclusive");
+            }
+            if (line.hasOption(OPT_DEBUG) && line.hasOption(OPT_QUIET)) {
+                throw new ParseException("The -d and -q switch are mutually exclusive");
+            }
+            if (line.hasOption(OPT_VERBOSE) && line.hasOption(OPT_QUIET)) {
+                throw new ParseException("The -v and -q switch are mutually exclusive");
+            }
+            // extract options
+            if (line.hasOption(OPT_DEBUG)) {
+                debugging = true;
+            }
+            if (line.hasOption(OPT_QUIET)) {
+                quiet = true;
+            }
+            if (line.hasOption(OPT_VERBOSE)) {
+                verbose = true;
+            }
+            if (line.hasOption(OPT_NO_PROGRESS) || quiet) {
+                progressInterval = -1;
+            }
+            if (line.hasOption(OPT_THREAD_COUNT)) {
+                try {
+                    threadCount = Integer.parseInt(
+                            line.getOptionValue(OPT_THREAD_COUNT));
+                    if (threadCount < 1) {
+                        throw new ParseException(
+                                "thread count must be larger then 0");
+                    }
+                } catch (NumberFormatException e) {
+                    throw new ParseException("invalid number");
+                }
+            }
+            if (line.hasOption(OPT_NO_THREADS)) {
+                threadCount = 1;
+            }
+            if (line.hasOption(OPT_NO_ESTIMATE) || (progressInterval < 0)) {
+                estimate = false;
+            }
+            if (line.hasOption(OPT_SCHEMA_CACHE_DIR)) {
+                String dir = line.getOptionValue(OPT_SCHEMA_CACHE_DIR);
+                if ((dir == null) || dir.isEmpty()) {
+                    throw new ParseException("invalid argument for -S");
+                }
+                schemaCacheDir = new File(dir);
+            }
+
+            final String[] remaining = line.getArgs();
+            if ((remaining == null) || (remaining.length == 0)) {
+                throw new ParseException("require <DIRECTORY> or <FILE> as " +
+                        "additional command line parameter");
+            }
+
+            final org.apache.log4j.Logger log =
+                    org.apache.log4j.Logger.getLogger(
+                            CMDIValidator.class.getPackage().getName());
+            if (debugging) {
+                appender.setLayout(
+                        new org.apache.log4j.PatternLayout("[%p] %t: %m%n"));
+                log.setLevel(org.apache.log4j.Level.DEBUG);
+            } else {
+                if (quiet) {
+                    log.setLevel(org.apache.log4j.Level.ERROR);
+                } else {
+                    log.setLevel(org.apache.log4j.Level.INFO);
+                }
+            }
+
+            TFile archive = null;
+            try {
+                /*
+                 * initialize CMDIValidatorFactory
+                 */
+                final CMDIValidatorFactory factory =
+                        CMDIValidatorFactory.newInstance(schemaCacheDir);
+                if (schemaCacheDir != null) {
+                    logger.info("using schema cache directory: {}", schemaCacheDir);
+                }
+
+                /*
+                 * process archive
+                 */
+                archive = new TFile(remaining[0]);
+
+                if (archive.exists()) {
+                    if (archive.isArchive()) {
+                        logger.info("reading archive '{}'", archive);
+                    } else {
+                        logger.info("reading directory '{}'", archive);
+                    }
+
+                    int totalFileCount = -1;
+                    if (estimate && logger.isInfoEnabled()) {
+                        logger.debug("counting files ...");
+                        totalFileCount = countFiles(archive);
+                    }
+
+                    if (threadCount > 1) {
+                      logger.debug("using {} threads", threadCount);
+                    }
+
+                    final CMDIValidatorEngine engine =
+                            new CMDIValidatorEngine(factory, threadCount);
+                    final JobHandler handler = new JobHandler(verbose);
+                    final CMDIValidatorJob job =
+                            new CMDIValidatorJob(archive, handler);
+                    try {
+                        engine.start();
+                        logger.debug("submitting batch validation job ...");
+                        engine.submit(job);
+
+                        /*
+                         * Wait until validation is done and report about
+                         * progress every now and then ...
+                         */
+                        for (;;) {
+                            try {
+                                if (handler.await(progressInterval)) {
+                                    break;
+                                }
+                            } catch (InterruptedException e) {
+                                /* IGNORE */
+                            }
+                            if ((progressInterval > 0) && logger.isInfoEnabled()) {
+                                final long now = System.currentTimeMillis();
+                                int fps    = -1;
+                                long bps   = -1;
+                                int count  = 0;
+                                long delta = -1;
+                                synchronized (handler) {
+                                    delta = (now - handler.getTimeStarted()) / 1000;
+                                    if (delta > 0) {
+                                        fps = (int) (handler.getTotalFileCount() / delta);
+                                        bps = (handler.getTotalBytes() / delta);
+                                    }
+                                    count = handler.getTotalFileCount();
+                                } // synchronized (result)
+                                if (totalFileCount > 0) {
+                                    float complete = (count / (float)  totalFileCount) * 100f;
+                                    logger.info("processed {} files ({}%) in {} ({} files/second, {}/second) ...",
+                                            count,
+                                            String.format(LOCALE, "%.2f", complete),
+                                            Humanize.duration(delta, LOCALE),
+                                            ((fps != -1) ? fps : "N/A"),
+                                            ((bps != -1) ? Humanize.binaryPrefix(bps, LOCALE) : "N/A MB"));
+                                } else {
+                                    logger.info("processed {} files in {} ({} files/second, {}/second) ...",
+                                            count,
+                                            Humanize.duration(delta, LOCALE),
+                                            ((fps != -1) ? fps : "N/A"),
+                                            ((bps != -1) ? Humanize.binaryPrefix(bps, LOCALE) : "N/A MB"));
+                                }
+                            }
+                        } // for (;;)
+                    } finally {
+                        engine.shutdown();
+                    }
+
+                    int fps = -1;
+                    long bps = -1;
+                    if (handler.getTimeElapsed() > 0) {
+                        fps = (int) (handler.getTotalFileCount() / handler.getTimeElapsed());
+                        bps = handler.getTotalBytes() / handler.getTimeElapsed();
+                    }
+
+                    logger.info("time elapsed: {}, validation result: {}% failure rate (files: {} total, {} passed, {} failed; {} total, {} files/second, {}/second)",
+                            Humanize.duration(handler.getTimeElapsed(), LOCALE),
+                            String.format(LOCALE, "%.2f", handler.getFailureRate() * 100f),
+                            handler.getTotalFileCount(),
+                            handler.getValidFileCount(),
+                            handler.getInvalidFileCount(),
+                            Humanize.binaryPrefix(handler.getTotalBytes(), LOCALE),
+                            ((fps != -1) ? fps : "N/A"),
+                            ((bps != -1) ? Humanize.binaryPrefix(bps, LOCALE) : "N/A MB"));
+                    logger.debug("... done");
+                } else {
+                    logger.error("not found: {}", archive);
+                }
+            } finally {
+                if (archive != null) {
+                    try {
+                        TVFS.umount(archive);
+                    } catch (FsSyncException e) {
+                        logger.error("error unmounting archive", e);
+                    }
+                }
+            }
+        } catch (CMDIValidatorInitException e) {
+            logger.error("error initializing validator: {}", e.getMessage());
+            if (debugging) {
+                logger.error(e.getMessage(), e);
+            }
+            System.exit(2);
+        } catch (ParseException e) {
+            PrintWriter writer = new PrintWriter(System.err);
+            if (e.getMessage() != null) {
+                writer.print("ERROR: ");
+                writer.println(e.getMessage());
+            }
+            HelpFormatter formatter = new HelpFormatter();
+            formatter.printHelp(writer, HelpFormatter.DEFAULT_WIDTH, PRG_NAME,
+                    null, options, HelpFormatter.DEFAULT_LEFT_PAD,
+                    HelpFormatter.DEFAULT_DESC_PAD, null, true);
+            writer.flush();
+            writer.close();
+            System.exit(64); /* EX_USAGE */
+        }
+    }
+
+
+    @SuppressWarnings("static-access")
+    private static Options createCommandLineOptions() {
+        final Options options = new Options();
+        OptionGroup g1 = new OptionGroup();
+        g1.addOption(OptionBuilder
+                .withDescription("enable debugging output")
+                .withLongOpt("debug")
+                .create(OPT_DEBUG));
+        g1.addOption(OptionBuilder
+                .withDescription("be quiet")
+                .withLongOpt("quiet")
+                .create(OPT_QUIET));
+        options.addOptionGroup(g1);
+        options.addOption(OptionBuilder
+                .withDescription("be verbose")
+                .withLongOpt("verbose")
+                .create(OPT_VERBOSE));
+        options.addOption(OptionBuilder
+                .withDescription("no progress reporting")
+                .withLongOpt("no-progress")
+                .create(OPT_NO_PROGRESS));
+        OptionGroup g2 = new OptionGroup();
+        g2.addOption(OptionBuilder
+                .withDescription("number of validator threads")
+                .hasArg()
+                .withArgName("COUNT")
+                .withLongOpt("threads")
+                .create(OPT_THREAD_COUNT));
+        g2.addOption(OptionBuilder
+                .withDescription("disable threading")
+                .withLongOpt("no-threads")
+                .create(OPT_NO_THREADS));
+        options.addOptionGroup(g2);
+        options.addOption(OptionBuilder
+            .withDescription("disable gathering of total file count for progress reporting")
+            .withLongOpt("no-estimate")
+            .create(OPT_NO_ESTIMATE));
+        options.addOption(OptionBuilder
+                .withDescription("schema caching directory")
+                .hasArg()
+                .withArgName("DIRECTORY")
+                .withLongOpt("schema-cache-dir")
+                .create(OPT_SCHEMA_CACHE_DIR));
+        return options;
+    }
+
+
+    private static final int countFiles(TFile directory) {
+        int count = 0;
+        final TFile[] entries = directory.listFiles();
+        if ((entries != null) && (entries.length > 0)) {
+            for (TFile entry : entries) {
+                if (entry.isDirectory()) {
+                    count += countFiles(entry);
+                } else {
+                    count++;
+                }
+            }
+        }
+        return count;
+    }
+
+
+    private static class JobHandler extends CMDIValidatorJobHandlerAdapter {
+        private final boolean verbose;
+        private long started     = System.currentTimeMillis();
+        private long finished    = -1;
+        private int filesTotal   = 0;
+        private int filesInvalid = 0;
+        private long totalBytes  = 0;
+        private boolean isCompleted = false;
+        private final Object waiter = new Object();
+
+
+        private JobHandler(boolean verbose) {
+            this.verbose = verbose;
+        }
+
+
+        public long getTimeStarted() {
+            return started;
+        }
+
+
+        public long getTimeElapsed() {
+            long duration = (finished != -1)
+                    ? (finished - started)
+                    : (System.currentTimeMillis() - started);
+            return TimeUnit.MILLISECONDS.toSeconds(duration);
+        }
+
+
+        public int getTotalFileCount() {
+            return filesTotal;
+        }
+
+
+        public int getValidFileCount() {
+            return filesTotal - filesInvalid;
+        }
+
+        public int getInvalidFileCount() {
+            return filesInvalid;
+        }
+
+
+        public float getFailureRate() {
+            return (filesTotal > 0)
+                    ? ((float) filesInvalid / (float) filesTotal)
+                    : 0.0f;
+        }
+
+
+        public long getTotalBytes() {
+            return totalBytes;
+        }
+
+
+        public boolean await(long timeout) throws InterruptedException {
+            synchronized (waiter) {
+                if (isCompleted) {
+                    return true;
+                }
+                if (timeout > 0) {
+                    waiter.wait(timeout);
+                } else {
+                    waiter.wait();
+                }
+                return isCompleted;
+            }
+        }
+
+
+        @Override
+        public void onJobFinished(boolean wasCancled) {
+            finished = System.currentTimeMillis();
+            synchronized (waiter) {
+                isCompleted = true;
+                waiter.notifyAll();
+            } // synchronized (waiter)
+            if (wasCancled) {
+                logger.info("processing was canceled ...");
+            }
+        }
+
+
+        @Override
+        public void onValidationSuccess(CMDIValidatorResult result) {
+            final File file = result.getFile();
+            synchronized (this) {
+                filesTotal++;
+                if (file != null) {
+                    totalBytes += file.length();
+                }
+            } // synchronized (this) {
+
+            if (result.isHighestSeverity(Severity.WARNING)) {
+                if (verbose) {
+                    logger.warn("file '{}' is valid (with warnings):", file);
+                    for (Message msg : result.getMessages()) {
+                        logger.warn(" ({}) {} [line={}, column={}]",
+                                msg.getSeverity().getShortcut(),
+                                msg.getMessage(),
+                                msg.getLineNumber(),
+                                msg.getColumnNumber());
+                    }
+                } else {
+                    Message msg = result.getFirstMessage(Severity.WARNING);
+                    int count = result.getMessageCount(Severity.WARNING);
+                    if (count > 1) {
+                        logger.warn("file '{}' is valid (with warnings): {} ({} more warnings)",
+                                file, msg, (count - 1));
+                    } else {
+                        logger.warn("file '{}' is valid (with warnings): {}",
+                                file, msg);
+                    }
+                }
+            } else {
+                logger.debug("file '{}' is valid", file, result.getFile());
+            }
+        }
+
+
+        @Override
+        public void onValidationFailure(CMDIValidatorResult result) {
+            final File file = result.getFile();
+            synchronized (this) {
+                filesTotal++;
+                filesInvalid++;
+                if (file != null) {
+                    totalBytes += file.length();
+                }
+            } // synchronized (this)
+
+            if (verbose) {
+            logger.error("file '{}' is invalid:", file);
+                for (Message msg : result.getMessages()) {
+                    logger.error(" ({}) {} [line={}, column={}]",
+                            msg.getSeverity().getShortcut(),
+                            msg.getMessage(),
+                            msg.getLineNumber(),
+                            msg.getColumnNumber());
+                }
+            } else {
+                Message msg = result.getFirstMessage(Severity.ERROR);
+                int count = result.getMessageCount(Severity.ERROR);
+                if (count > 1) {
+                    logger.error("file '{}' is invalid: {} ({} more errors)",
+                            file, msg.getMessage(), (count - 1));
+                } else {
+                    logger.error("file '{}' is invalid: {}",
+                            file, msg.getMessage());
+                }
+            }
+        }
+    } // class JobHandler
+
+
+    static {
+        appender = new org.apache.log4j.ConsoleAppender(
+                new org.apache.log4j.PatternLayout("%m%n"),
+                org.apache.log4j.ConsoleAppender.SYSTEM_OUT);
+        org.apache.log4j.BasicConfigurator.configure(appender);
+        org.apache.log4j.Logger logger =
+                org.apache.log4j.Logger.getRootLogger();
+        logger.setLevel(org.apache.log4j.Level.WARN);
+    }
+
+} // class CMDIValidatorTool
Index: CMDIValidator/trunk/src/main/resources/xml.xsd
===================================================================
--- CMDIValidator/trunk/src/main/resources/xml.xsd	(revision 5052)
+++ CMDIValidator/trunk/src/main/resources/xml.xsd	(revision 5052)
@@ -0,0 +1,287 @@
+<?xml version='1.0'?>
+<?xml-stylesheet href="../2008/09/xsd.xsl" type="text/xsl"?>
+<xs:schema targetNamespace="http://www.w3.org/XML/1998/namespace" 
+  xmlns:xs="http://www.w3.org/2001/XMLSchema" 
+  xmlns   ="http://www.w3.org/1999/xhtml"
+  xml:lang="en">
+
+ <xs:annotation>
+  <xs:documentation>
+   <div>
+    <h1>About the XML namespace</h1>
+
+    <div class="bodytext">
+     <p>
+      This schema document describes the XML namespace, in a form
+      suitable for import by other schema documents.
+     </p>
+     <p>
+      See <a href="http://www.w3.org/XML/1998/namespace.html">
+      http://www.w3.org/XML/1998/namespace.html</a> and
+      <a href="http://www.w3.org/TR/REC-xml">
+      http://www.w3.org/TR/REC-xml</a> for information 
+      about this namespace.
+     </p>
+     <p>
+      Note that local names in this namespace are intended to be
+      defined only by the World Wide Web Consortium or its subgroups.
+      The names currently defined in this namespace are listed below.
+      They should not be used with conflicting semantics by any Working
+      Group, specification, or document instance.
+     </p>
+     <p>   
+      See further below in this document for more information about <a
+      href="#usage">how to refer to this schema document from your own
+      XSD schema documents</a> and about <a href="#nsversioning">the
+      namespace-versioning policy governing this schema document</a>.
+     </p>
+    </div>
+   </div>
+  </xs:documentation>
+ </xs:annotation>
+
+ <xs:attribute name="lang">
+  <xs:annotation>
+   <xs:documentation>
+    <div>
+     
+      <h3>lang (as an attribute name)</h3>
+      <p>
+       denotes an attribute whose value
+       is a language code for the natural language of the content of
+       any element; its value is inherited.  This name is reserved
+       by virtue of its definition in the XML specification.</p>
+     
+    </div>
+    <div>
+     <h4>Notes</h4>
+     <p>
+      Attempting to install the relevant ISO 2- and 3-letter
+      codes as the enumerated possible values is probably never
+      going to be a realistic possibility.  
+     </p>
+     <p>
+      See BCP 47 at <a href="http://www.rfc-editor.org/rfc/bcp/bcp47.txt">
+       http://www.rfc-editor.org/rfc/bcp/bcp47.txt</a>
+      and the IANA language subtag registry at
+      <a href="http://www.iana.org/assignments/language-subtag-registry">
+       http://www.iana.org/assignments/language-subtag-registry</a>
+      for further information.
+     </p>
+     <p>
+      The union allows for the 'un-declaration' of xml:lang with
+      the empty string.
+     </p>
+    </div>
+   </xs:documentation>
+  </xs:annotation>
+  <xs:simpleType>
+   <xs:union memberTypes="xs:language">
+    <xs:simpleType>    
+     <xs:restriction base="xs:string">
+      <xs:enumeration value=""/>
+     </xs:restriction>
+    </xs:simpleType>
+   </xs:union>
+  </xs:simpleType>
+ </xs:attribute>
+
+ <xs:attribute name="space">
+  <xs:annotation>
+   <xs:documentation>
+    <div>
+     
+      <h3>space (as an attribute name)</h3>
+      <p>
+       denotes an attribute whose
+       value is a keyword indicating what whitespace processing
+       discipline is intended for the content of the element; its
+       value is inherited.  This name is reserved by virtue of its
+       definition in the XML specification.</p>
+     
+    </div>
+   </xs:documentation>
+  </xs:annotation>
+  <xs:simpleType>
+   <xs:restriction base="xs:NCName">
+    <xs:enumeration value="default"/>
+    <xs:enumeration value="preserve"/>
+   </xs:restriction>
+  </xs:simpleType>
+ </xs:attribute>
+ 
+ <xs:attribute name="base" type="xs:anyURI"> <xs:annotation>
+   <xs:documentation>
+    <div>
+     
+      <h3>base (as an attribute name)</h3>
+      <p>
+       denotes an attribute whose value
+       provides a URI to be used as the base for interpreting any
+       relative URIs in the scope of the element on which it
+       appears; its value is inherited.  This name is reserved
+       by virtue of its definition in the XML Base specification.</p>
+     
+     <p>
+      See <a
+      href="http://www.w3.org/TR/xmlbase/">http://www.w3.org/TR/xmlbase/</a>
+      for information about this attribute.
+     </p>
+    </div>
+   </xs:documentation>
+  </xs:annotation>
+ </xs:attribute>
+ 
+ <xs:attribute name="id" type="xs:ID">
+  <xs:annotation>
+   <xs:documentation>
+    <div>
+     
+      <h3>id (as an attribute name)</h3> 
+      <p>
+       denotes an attribute whose value
+       should be interpreted as if declared to be of type ID.
+       This name is reserved by virtue of its definition in the
+       xml:id specification.</p>
+     
+     <p>
+      See <a
+      href="http://www.w3.org/TR/xml-id/">http://www.w3.org/TR/xml-id/</a>
+      for information about this attribute.
+     </p>
+    </div>
+   </xs:documentation>
+  </xs:annotation>
+ </xs:attribute>
+
+ <xs:attributeGroup name="specialAttrs">
+  <xs:attribute ref="xml:base"/>
+  <xs:attribute ref="xml:lang"/>
+  <xs:attribute ref="xml:space"/>
+  <xs:attribute ref="xml:id"/>
+ </xs:attributeGroup>
+
+ <xs:annotation>
+  <xs:documentation>
+   <div>
+   
+    <h3>Father (in any context at all)</h3> 
+
+    <div class="bodytext">
+     <p>
+      denotes Jon Bosak, the chair of 
+      the original XML Working Group.  This name is reserved by 
+      the following decision of the W3C XML Plenary and 
+      XML Coordination groups:
+     </p>
+     <blockquote>
+       <p>
+	In appreciation for his vision, leadership and
+	dedication the W3C XML Plenary on this 10th day of
+	February, 2000, reserves for Jon Bosak in perpetuity
+	the XML name "xml:Father".
+       </p>
+     </blockquote>
+    </div>
+   </div>
+  </xs:documentation>
+ </xs:annotation>
+
+ <xs:annotation>
+  <xs:documentation>
+   <div xml:id="usage" id="usage">
+    <h2><a name="usage">About this schema document</a></h2>
+
+    <div class="bodytext">
+     <p>
+      This schema defines attributes and an attribute group suitable
+      for use by schemas wishing to allow <code>xml:base</code>,
+      <code>xml:lang</code>, <code>xml:space</code> or
+      <code>xml:id</code> attributes on elements they define.
+     </p>
+     <p>
+      To enable this, such a schema must import this schema for
+      the XML namespace, e.g. as follows:
+     </p>
+     <pre>
+          &lt;schema . . .>
+           . . .
+           &lt;import namespace="http://www.w3.org/XML/1998/namespace"
+                      schemaLocation="http://www.w3.org/2001/xml.xsd"/>
+     </pre>
+     <p>
+      or
+     </p>
+     <pre>
+           &lt;import namespace="http://www.w3.org/XML/1998/namespace"
+                      schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
+     </pre>
+     <p>
+      Subsequently, qualified reference to any of the attributes or the
+      group defined below will have the desired effect, e.g.
+     </p>
+     <pre>
+          &lt;type . . .>
+           . . .
+           &lt;attributeGroup ref="xml:specialAttrs"/>
+     </pre>
+     <p>
+      will define a type which will schema-validate an instance element
+      with any of those attributes.
+     </p>
+    </div>
+   </div>
+  </xs:documentation>
+ </xs:annotation>
+
+ <xs:annotation>
+  <xs:documentation>
+   <div id="nsversioning" xml:id="nsversioning">
+    <h2><a name="nsversioning">Versioning policy for this schema document</a></h2>
+    <div class="bodytext">
+     <p>
+      In keeping with the XML Schema WG's standard versioning
+      policy, this schema document will persist at
+      <a href="http://www.w3.org/2009/01/xml.xsd">
+       http://www.w3.org/2009/01/xml.xsd</a>.
+     </p>
+     <p>
+      At the date of issue it can also be found at
+      <a href="http://www.w3.org/2001/xml.xsd">
+       http://www.w3.org/2001/xml.xsd</a>.
+     </p>
+     <p>
+      The schema document at that URI may however change in the future,
+      in order to remain compatible with the latest version of XML
+      Schema itself, or with the XML namespace itself.  In other words,
+      if the XML Schema or XML namespaces change, the version of this
+      document at <a href="http://www.w3.org/2001/xml.xsd">
+       http://www.w3.org/2001/xml.xsd 
+      </a> 
+      will change accordingly; the version at 
+      <a href="http://www.w3.org/2009/01/xml.xsd">
+       http://www.w3.org/2009/01/xml.xsd 
+      </a> 
+      will not change.
+     </p>
+     <p>
+      Previous dated (and unchanging) versions of this schema 
+      document are at:
+     </p>
+     <ul>
+      <li><a href="http://www.w3.org/2009/01/xml.xsd">
+	http://www.w3.org/2009/01/xml.xsd</a></li>
+      <li><a href="http://www.w3.org/2007/08/xml.xsd">
+	http://www.w3.org/2007/08/xml.xsd</a></li>
+      <li><a href="http://www.w3.org/2004/10/xml.xsd">
+	http://www.w3.org/2004/10/xml.xsd</a></li>
+      <li><a href="http://www.w3.org/2001/03/xml.xsd">
+	http://www.w3.org/2001/03/xml.xsd</a></li>
+     </ul>
+    </div>
+   </div>
+  </xs:documentation>
+ </xs:annotation>
+
+</xs:schema>
+
Index: CMDIValidator/trunk/xerces.patch
===================================================================
--- CMDIValidator/trunk/xerces.patch	(revision 5052)
+++ CMDIValidator/trunk/xerces.patch	(revision 5052)
@@ -0,0 +1,33 @@
+Index: src/org/apache/xerces/impl/xs/XMLSchemaValidator.java
+===================================================================
+--- src/org/apache/xerces/impl/xs/XMLSchemaValidator.java   (revision 1584342)
++++ src/org/apache/xerces/impl/xs/XMLSchemaValidator.java   (working copy)
+@@ -1345,8 +1345,11 @@
+ 
+         fIdConstraint = false;
+         //reset XSDDescription
++        fXSDDescription.reset();
+         fLocationPairs.clear();
+         fExpandedLocationPairs.clear();
++        fUnparsedLocations.clear();
++        fGrammarBucket.reset();
+ 
+         // cleanup id table
+         fValidationState.resetIDTables();
+@@ -2693,6 +2696,16 @@
+                 }
+             }
+ 
++            /*
++             * XXX: evil hack to revisit grammar pool with location information
++             */
++            if (grammar == null) {
++                grammar = (SchemaGrammar) fGrammarPool.retrieveGrammar(fXSDDescription);
++                if (grammar != null) {
++                    fXSDDescription.fLocationHints = null;
++                }
++            }
++
+             if (grammar == null || fXSDDescription.fLocationHints != null) {
+                 boolean toParseSchema = true;
+                 if (grammar != null) {
