/*
 * $Id: Driver.java,v 1.1.1.1 2002/09/30 15:08:52 smartine Exp $
 * Copyright (C) 1999-2001 David Brownell
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package xml.testing;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import org.xml.sax.*;
import org.xml.sax.helpers.*;
import org.xml.sax.ext.*;

import xml.XhtmlEchoHandler;

import xml.aelfred2.SAXDriver; // for bootstrapping


// $Id: Driver.java,v 1.1.1.1 2002/09/30 15:08:52 smartine Exp $

// XXX split most of the inner classes out.

/**
 * Runs a SAX (preferably 2.0, but alternatively 1.0) parser
 * through its paces by using a parser conformance test suite.
 *
 * <P> The test suite must conform to the DTD defined by the
 * NIST and OASIS XML Conformance test suite, available at
 * <a href="http://www.oasis-open.org/committees/xmltest/testsuite.htm">
 * http://www.oasis-open.org/committees/xmltest/testsuite.htm</a>.
 * As of this writing, the current draft is 12 July 1999, and
 * a "feb05.tar.gz" update (to one set of XML 1.0 errata)
 * behaves reasonably well.  (Note that some versions of ZIP
 * are known to corrupt the test data; don't use them to
 * extract files from any of the XML test databases.)
 * The "March 15, 20001" update, to test the second edition
 * errata set, is currently known to have some bugs (at least
 * some missing files); be careful what you test against.
 *
 * <P> This reads the test suite database into memory (using the
 * reference SAX2 implementation &AElig;lfred2) into memory
 * and then uses that database to drive
 * the five types of tests defined by the NIST/OASIS framework.  The
 * results of those tests are printed as valid XHTML, mostly as tables,
 * which may then be viewed using most web browsers.
 * The XHTML is currently generated through a document template.
 *
 * @author David Brownell (dbrownell@users.sourceforge.net)
 * @version $Date: 2002/09/30 15:08:52 $
 */
public class Driver
{
    private static final boolean	debug = false;
    private static final String		version = "($Date: 2002/09/30 15:08:52 $)";

    private static String		description = "(no description)";

    private static DefaultHandler	nopHandler = new DefaultHandler ();

    private Driver () { /* no can do! */ }

    /**
     * Generates a conformance report using a given test suite and
     * report template, for a specified SAX parser.  The report will
     * appear on the standard output, in US-ASCII encoded XHTML.
     *
     * <p> Command line parameters are, in order: <ul>
     *	<li> Filename or URL for test suite's XML data;
     *	<li> Name of SAX parser being tested;
     *  <li> Filename or URL for report template.
     *  <li> [IN FLUX] -- "nv" or "val", ignored (see -D...validation)
     *  <li> (optional) parser descriptive string
     *	</ul>
     *
     * <p> See the associated report template for information about
     * the syntax used there; a number of XML processing instructions
     * are used to indicate that this driver should substitute certain
     * data in the template, such as tables of test results or data
     * about the test run used to generate the report.
     *
     * <p> If the parser supports the SAX2 feature API, it is
     * queried for <em>validation</em>, <em>external-general-entities</em>,
     * and <em>external-parameter-entities</em> support.  If that fails
     * (perhaps because the parser does not support SAX2), then three
     * system properties are used instead.  These all share a common
     * prefix, <em>xml.testing.Driver.</em>, to which
     * are appended <em>validation</em>, <em>general-entities</em>, and
     * <em>parameter-entities</em> respectively.  (The only legal values
     * for those properties are "true" and "false".)  This driver
     * <em>must</em> be able to determine whether the parser being tested
     * parser performs XML validation, else it can neither test nor
     * report correctly.
     *
     * <p> Some SAX2 parsers will be able to set those three features.
     * If that is the case, this driver can use the system properties
     * noted above to determine the value the feature should be assigned.
     * For example, if a parser can validate, it will have a default
     * value for the setting of its <em>validation</em> feature.  If that
     * isn't the intended value, then the feature value can be changed
     * by setting the appropriate system property value.
     *
     * <p> Some parsers have been observed to have severe failure modes
     * (notably, infinite loops) triggered by some test cases.  So that test
     * reports may still be generated for those processors, an exception
     * mechanism has been defined to arrange that those cases are not run
     * and are treated as failures.  Using the same system property naming
     * convention defined above, the <em>exceptions</em> string is appended
     * to the common prefix.  That names an XML file.  Within that file are
     * <em>&lt;EXCEPTION ID="..."&gt;</em> elements; the ID value is the
     * ID of a test which will be marked as failing without running it.
     */
    public static void main (String argv [])
    {
	if (argv.length < 4) {
	    System.err.println ("usage: "
		+ "java xml.testing.Driver [args]");
	    System.err.println ("  arg 0 = testsuite XML source, file or URL");
	    System.err.println ("  arg 1 = parser class name");
	    System.err.println ("  arg 2 = report template, file or URL");
	    System.err.println ("  arg 3 = nv or val");
	    System.err.println ("  arg 4 = (optional) parser description");
	    // other args, optional, for non-default feature settings
	    System.exit (1);
	}

	if (argv.length >= 5) {
	    // e.g. "MyParser v1.5"
	    description = argv [4];
	}

	String testsuiteURI = getURL (argv [0]);
	
	//
	// Load test database using bootstrap parser (AElfred2).
	// Other parsers should work, unless they're very broken.
	// Validation is desirable.
	//
	// The data is saved as attributes of the handler (lazy ... :-)
	//
	XMLReader 	parser = null;
	Handler		handler = new Handler ();

	try {
	    InputSource	source = new InputSource (testsuiteURI);

	    parser = new SAXDriver ();
	    parser.setContentHandler (handler);
	    parser.setErrorHandler (handler);
	    parser.parse (source);

	    if (debug) {
		System.out.println ("Suite name:  " + handler.suite);

		System.out.println ();
		System.out.println ("Cases with valid documents ("
			+ handler.valid.size () + "):");
		for (Enumeration iter = handler.valid.elements ();
			iter.hasMoreElements ();
			/* implicit in iter.nextElement */) {
		    System.out.println (iter.nextElement ());
		}

		System.out.println ();
		System.out.println ("Cases with invalid documents ("
			+ handler.invalid.size () + "):");
		for (Enumeration iter = handler.invalid.elements ();
			iter.hasMoreElements ();
			/* implicit in iter.nextElement */) {
		    System.out.println (iter.nextElement ());
		}

		System.out.println ();
		System.out.println ("Cases with not-WF documents ("
			+ handler.notWF.size () + "):");
		for (Enumeration iter = handler.notWF.elements ();
			iter.hasMoreElements ();
			/* implicit in iter.nextElement */) {
		    System.out.println (iter.nextElement ());
		}

		System.out.println ();
		System.out.println ("Informative cases ("
			+ handler.optional.size () + "):");
		for (Enumeration iter = handler.optional.elements ();
			iter.hasMoreElements ();
			/* implicit in iter.nextElement */) {
		    System.out.println (iter.nextElement ());
		}
		System.out.flush ();
	    }
	} catch (SAXException e) {
	    Exception	x = e.getException ();

	    System.err.println ("Parsing problem; " + e.getMessage ());
	    if (e instanceof SAXParseException) {
		SAXParseException pe = (SAXParseException) e;
		System.err.println ("** uri  = " + pe.getSystemId ());
		System.err.println ("** line = " + pe.getLineNumber ());
	    }
	    if (x == null) x = e;
	    x.printStackTrace ();

	    System.exit (1);
	
	} catch (Throwable t) {
	    System.err.println ("Can't load test suite; " + t.getMessage ());
	    System.err.println ("Error class: " + t.getClass ().getName ());
	    t.printStackTrace ();

	    System.exit (1);
	}

	//
	// See if there were any directed failures.  Normally there are
	// none -- except for really severe bugs like parser hangs, which
	// are strong hints not to use that parser!
	//
	Vector		exceptions = null;

	try {
	    String	temp;

	    if ((temp = System.getProperty (
			"xml.testing.Driver.exceptions"))
		    != null) {
		// Just collect the directed failures and save them;
		// the list is checked before each test is run.
		//
		// TODO: For performance testing, best to prune them
		// out much earlier, and get the overhead out the
		// loop we're timing.  It's always safe to do.
		ExceptionHandler	h = new ExceptionHandler ();

		parser.setErrorHandler (h);
		parser.setContentHandler (h);
		parser.parse (getURL (temp));
		exceptions = h.exceptions;
	    }
	} catch (Exception e) {
	}

	//
	// OK, get the parser being tested.  When tests are run on a
	// SAX2 processor, processor features must be set to
	// non-default values due to namespace issues; and optionally
	// to enable or disable validation.
	//
	// TODO:  there currently aren't namespace-specific tests.
	//
	XMLReader 		parserToTest = null;

	try {
	    Object		obj;

	    obj = Class.forName (argv [1]).newInstance ();
	    if (obj instanceof XMLReader)
		parserToTest = (XMLReader) obj;
	    else
		parserToTest = new ParserAdapter ((Parser) obj);

	    // If parser can't deliver XML 1.0, it's trouble.
	    parserToTest.setFeature (
		    "http://xml.org/sax/features/namespace-prefixes",
		    true
		    );

	    // This to correctly handle some XML 1.0 tests with respect to
	    // non-namespaces -- names like ":" and "com::mycorp:fxswap"
	    // are legit for XML, but not for namespaces
	    try {
		parserToTest.setFeature (
			"http://xml.org/sax/features/namespaces",
			false
			);
	    } catch (Exception e) {
		// some parsers can't turn off namespace processing
	    }

	    /*
	     * XXX this would desupport testing of all parsers that don't
	     * support settable validation settings.  For one example, that
	     * includes all wrapped SAX1 parsers.  (Like XP 0.5, which is
	     * a useful reference implementation.)
	     *
	     * the existing "-Dxml.testing.Driver.validation={true|false}"
	     * facility needs to get completely transitioned
	     * 
	    if (argv [3].equals ("val")) {
	    	parserToTest.setFeature (
		    	"http://xml.org/sax/features/validation",
		    	true
		    	);
	    }
	    */

	    
	} catch (Throwable e) {
	    System.err.println ("Can't init parser under test; " + e.getMessage ());
	    e.printStackTrace ();

	    System.exit (1);
	}

	//
	// Now we have most of the key ingredients ... run the tests,
	// generate the report using the template!   (Maybe someday
	// just write output, and let report generation be separate.)
	//
	// Note that the output gets all set up early, before the
	// tests are run, because some parses have been so rude as
	// to leave open files.  We make that cause test failures,
	// not harness failures.
	//
	String templateURI = getURL (argv [2]);

	try {
	    long	before, after;
	    Reporter	reporter = new Reporter (handler, parserToTest);

	    // TIMING ... be as consistent as we can with the initial state.
	    // No JIT applied to the parser under test, and a wide open heap.
	    System.gc ();

	    System.err.flush ();
	    before = System.currentTimeMillis ();

	    System.err.println ("**Testing valid documents...");
	    runTests (parserToTest, handler.valid, exceptions);
	    System.err.println ("\n**Testing invalid documents...");
	    runTests (parserToTest, handler.invalid, exceptions);
	    System.err.println ("\n**Testing not-wf documents...");
	    runTests (parserToTest, handler.notWF, exceptions);
	    System.err.println ("\n**Informative tests...");
	    runTests (parserToTest, handler.optional, exceptions);

	    after = System.currentTimeMillis ();

	    // Several parsers have been just on the edge here,
	    // due to file descriptor and memory usage issues.
	    // Do what we can.
	    parserToTest = null;
	    System.gc ();

	    // n.b. time includes time to print pass/fail/skip;
	    // saving to (buffered) file reduces the cost.
	    System.err.println ("\n**TEST TIME: "
		+ (after - before) + "ms. for "
		+ argv [1]);

	    InputSource	source = new InputSource (templateURI);

	    parser.setContentHandler (reporter);
	    parser.parse (source);

	} catch (SAXException e) {
	    Exception	x = e.getException ();

	    System.err.println ("Reporting problem; " + e.getMessage ());
	    if (x == null) x = e;
	    x.printStackTrace ();

	    System.exit (1);
	
	} catch (Throwable t) {
	    System.err.println ("Can't generate report; " + t.getMessage ());
	    t.printStackTrace ();

	    System.exit (1);
	}

	
    }

    // we use this instead of JDK 1.2 "File.toURL"
    // so we can run in JDK 1.1 environments 
    private static String fileToURL (String filename)
    throws IOException
    {
	File	f = new File (filename);
	String	temp;

	if (!f.exists ())
	    throw new IOException ("not a file");
	temp = f.getAbsolutePath ();
	if (File.separatorChar != '/')
	    temp = temp.replace (File.separatorChar, '/');
	if (!temp.startsWith ("/"))
	    temp = "/" + temp;
	if (!temp.endsWith ("/") && f.isDirectory ())
	    temp = temp + "/";
	return "file:" + temp;
    }

    private static String getURL (String fileOrURL)
    {
	try {
	    return fileToURL (fileOrURL);
	} catch (Exception e) {
	    return fileOrURL;
	}
    }

    /**
     * Parses the input document that describes testcases to be used.
     */
    private static void runTests (
	XMLReader	parser,
	Sorter	tests,
	Vector	exceptions
    ) {
	int	validating;
	int	general;
	int	parameter;
	boolean	names;

	// doesn't hurt to do this for each group of tests, but
	// it only really needs to be checked once.
	validating = getFeature (parser,
		    "http://xml.org/sax/features/validation",
		    "xml.testing.Driver.validation");
	general = getFeature (parser,
		    "http://xml.org/sax/features/external-general-entities",
		    "xml.testing.Driver.general-entities");
	parameter = getFeature (parser,
		    "http://xml.org/sax/features/external-parameter-entities",
		    "xml.testing.Driver.parameter-entities");
	if (validating == -1)
	    throw new IllegalStateException (
		"Can't tell if this XML processor validates or not.");
	
	try {
	    names = parser.getFeature (
		"http://xml.org/sax/features/namespaces");
	    // normally false by now
	} catch (Exception e) {
	    throw new IllegalStateException (
		"Parser doesn't expose namespaces feature");
	}

	for (Enumeration iter = tests.elements ();
		iter.hasMoreElements ();) {
	    Test 		test = (Test) iter.nextElement ();

	    // Immediately fail any tests marked as such (e.g. parser hangs)
	    if (exceptions != null && exceptions.contains (test.id)) {
		    // it's nice not to need this lately...
		System.err.println ("EXCEPTION (FAIL) " + test.id);
		test.pass = false;
		test.diagnostic = "(EXCEPTION - DIRECTED FAILURE)";
		test.diagnosticLevel = 1;
		continue;
	    }

	    // Skip tests that require handling external entities unless
	    // we know that we handle that type of entity.  (Only
	    // nonvalidating parsers may ignore external entities.)
	    if (validating == 0 && !"none".equals (test.entities)) {
		if ("general".equals (test.entities)
			|| "both".equals (test.entities)) {
		    if (general != 1) {
			test.skipped = true;
			System.err.println ("SKIP (ge) " + test.id);
			continue;
		    }
		}
		if ("parameter".equals (test.entities)
			|| "both".equals (test.entities)) {
		    if (parameter != 1) {
			test.skipped = true;
			System.err.println ("SKIP (pe) " + test.id);
			continue;
		    }
		}
	    }

	    // Skip tests that use names:that:namespaces:disallow
	    if (!test.namespace && names) {
		test.skipped = true;
		System.err.println ("SKIP (name:space:names) " + test.id);
		continue;
	    }

	    //
	    // "--verbose" output assumed here
	    //
	    System.err.print (test.id);
	    System.err.print (" ... ");
	    System.err.flush ();
	    test.run (parser, validating == 1);

	    String status = (test.pass ? "PASS " : "FAIL ");
	    if ("valid".equals (test.type)
		    && (((validating == 1) && test.valOutputURI != null)
			|| test.nvOutputURI != null)) {
		if (test.pass)
		    status += (test.outputDiagnostic == null)
			? " (output PASS)" : " (output FAIL)";
		else
		    status += " (output AUTOFAIL)";
	    }
	    System.err.println (status);
	}

	// run the GC to clean things up between runs
	System.gc ();
    }

    /**
     * Determine feature of the XML parser... and if it's a SAX2
     * parser, perhaps set the mode of that feature!!
     *
     * @param parser ... the parser
     * @param featureId ... SAX2 feature URI
     * @param property ... if no SAX2 or feature unrecognized, boolean sys property
     * @return trivalue ... -1 = don't know, 0 = false, 1 = true
     */
    private static int getFeature (
	XMLReader	parser,
	String	featureId,
	String	property
    ) {
	String	tmp = System.getProperty (property, "undefined");
	int	value = -1;

	// Get the system property, for fallback or SAX2 assignment
	if ("true".equals (tmp))
	    value = 1;
	else if ("false".equals (tmp))
	    value = 0;

	// Try using SAX2 feature getting/setting facility
	if (parser instanceof XMLReader) {
	    XMLReader		p = (XMLReader) parser;
	    boolean		defValue = false;

	    try {
		defValue = p.getFeature (featureId);
		if (value != -1)
		    p.setFeature (featureId, (value == 1));
		else
		    value = defValue ? 1 : 0;

	    } catch (SAXNotSupportedException e) {

		// best thrown by setFeature ...
		// "unsettable" feature --> uses processor's default
		// value = defValue ? 1 : 0;

		// getFeature throws it if value can't be determined
		// "at this time"??

		return value;

	    } catch (Exception e) {
		// "ungettable" feature --> uses system property
	    }
	}

	return value;
    }


    /**
     * Representation of an individual XML test case.
     */
    static final class Test implements ErrorHandler
    {
	// note:  input documents must be DTD-valid

	public final String	id;		// "ID"

	// URI to the input file
	public final String	inputURI;	// f(baseURI,"URI")

	// just what's being tested?
	public final String	sections;	// "SECTIONS"
	public String		description;	// children

	//
	// XML-SPECIFIC ... lots of test categories because
	// of the optional use of DTD validation.
	//
	// TODO:  check against infoset, and decide how to
	// simplify.  Perhaps sort the "dtd-valid" cases,
	// "schema-valid", "xdr-valid", "namespace-correct",
	// etc into separately evaluated test documents.
	//

	// test type -- "valid", "invalid", "not-wf", or "error"
	public final String	type;		// "TYPE"


	// OPTIONAL: output URIs for use with nonvalidating and
	// validating parsers (for some "valid" cases)
// FIXME: restore "final" here
// when GCJ 3.0 PR 2424 is fixed (not an issue in 2.96rh)
	public /*final*/ String	nvOutputURI;	// f(baseURI,"OUTPUT")
	public /*final*/ String	valOutputURI;	// f(baseURI,"OUTPUT3")

	// "both", "none", "parameter", "general" -- for "not-wf" cases
	public final String	entities;	// "ENTITIES"

	public boolean		namespace;	// "NAMESPACES"

	// END XML-SPECIFIC

	// RESULTS/STATUS ...
	
	// true iff the test was skipped (notably, nonvalidating
	// parsers that don't match 'entities' requirements)
	public boolean		skipped;

	// diagnostic generated from parse
	public String		diagnostic;

	// SAX-SPECIFIC

	// cached between errorHandler() call and test completion
	private Exception	exception;

	// END SAX-SPECIFIC


	// XML-SPECIFIC

	// -1 = warning, 0 = error, 1 = fatal error,
	// 2 = reported incorrectly (via throw, not errorHandler)
	public int		diagnosticLevel;

	// for valid documents, description of any output error
	public String		outputDiagnostic;

	// END XML-SPECIFIC


	// status -- pass/fail
	public boolean		pass;

	Test (Attributes attrs, URL baseURI)
	throws IOException
	{
	    String	tmp;

	    // Get core of test description
	    id = attrs.getValue ("ID");
	    inputURI = new URL (baseURI, attrs.getValue ("URI")).toString ();
	    type = attrs.getValue ("TYPE");
	    sections = attrs.getValue ("SECTIONS");
	    description = null;		// constructed separately

	    // Remember optional output data
	    if ((tmp = attrs.getValue ("OUTPUT")) != null)
		nvOutputURI = new URL (baseURI, tmp).toString ();
	    else
		nvOutputURI = null;
	    if ((tmp = attrs.getValue ("OUTPUT3")) != null)
		valOutputURI = new URL (baseURI, tmp).toString ();
	    else
		valOutputURI = null;

	    // Remember optional entity processing requirements
	    entities = attrs.getValue ("ENTITIES");

	    // Remember optional namespace processing requirements
	    // (dtd default is "yes"...)
	    namespace = !"no".equals (attrs.getValue ("NAMESPACE"));
	}

	// non-HTML version 
	public String toString ()
	{
	    StringBuffer	buf = new StringBuffer ();

	    buf.append ("{XML Test");
	    buf.append (", id="); buf.append (id);
	    buf.append (", type="); buf.append (type);
	    buf.append (", input="); buf.append (inputURI);
	    buf.append ("}");
	    return buf.toString ();
	}

	/** SAX-SPECIFIC */
	public void run (XMLReader parser, boolean isValidating)
	{
	    try {
		InputSource	in = new InputSource (inputURI);
		Outputter	handler = null;

		// This is tricky ... we set test status as if we got a
		// fatal error reported or some exception thrown (which is
		// a pass for some types of test) and then if we don't get
		// one we later update that status appropriately.
		if ("valid".equals (type) || "invalid".equals (type))
		    pass = false;
		else	// not-wf, and error
		    pass = true;

		// Arrange error reporting ...
		diagnosticLevel = -1;
		exception = null;
		parser.setErrorHandler (this);

		// Set up parse event handling ...
		if ("valid".equals (type)) {
		    // use best available tests for nonvalidating
		    // and validating parsers; OASIS/NIST currently
		    // doesn't have tests for the output requirements
		    // of validating parsers (e.g. reporting ignorable
		    // whitespace as such, and reporting decls for
		    // unparsed entities).
		    if (isValidating && valOutputURI != null)
			handler = new Outputter (true, valOutputURI);
		    else if (nvOutputURI != null)
			handler = new Outputter (false, nvOutputURI);
		}
		if (handler != null) {
		    // we've an output test to perform
		    parser.setContentHandler (handler);
		    parser.setDTDHandler (handler);
		} else {
		    // no output test
		    parser.setContentHandler (nopHandler);
		    parser.setDTDHandler (nopHandler);
		}

		parser.parse (in);

		// "optional" tests never fail in ways we can tell.
		// Eyeballing diagnostics may identify conformance
		// bugs, though.
		if ("error".equals (type))
		    pass = true;

		// Documents that aren't WF must always cause fatal errors.
		// Someone must eyeball the diagnostics to ensure the _right_
		// error was reported ...
		else if ("not-wf".equals (type)) {
		    pass = false;
		    if (diagnostic == null)
			diagnostic = "[Document not WF; no error reported]";
		}

		// Valid docs may produce warnings, no more
		else if ("valid".equals (type)) {
		    if (diagnosticLevel != -1) {
			pass = false;
			if (diagnostic == null)
			    diagnostic = "[no diagnostic]";
		    } else
			pass = true;
		
		// Invalid documents
		//	- may never produce fatal errors
		//	- must produce non-fatal errors if validating
		} else if ("invalid".equals (type)) {
		    if (diagnosticLevel == 1
			    || (isValidating && diagnosticLevel != 0)) {
			pass = false;
			if (diagnostic == null)
			    diagnostic = "[no diagnostic]";
		    } else
			pass = true;
		}

		// else ... what else could there be?
		else
		    diagnostic = "[ ?? ]";

		// Collect any diagnostic for the output test.
		if (handler != null)
		    outputDiagnostic = handler.getDiagnostic ();

	    //
	    // When we catch thrown exceptions below, they all indicate
	    // fatal errors of some kind ... we report them in preference
	    // to warnings, otherwise we report any earlier ParseException
	    // that was reported via the ErrorHandler.
	    //

	    } catch (SAXParseException e) {
		if (exception == null || diagnosticLevel == -1) {
		    // clobbers cleanly reported warning
		    diagnostic = "(thrown SAXParseException) ";
		    if (e.getMessage () != null)
			diagnostic += e.getMessage ();
		    else
			diagnostic += "[no exception message]";
		    diagnosticLevel = 2;
		} else {
		    // exception thrown after ErrorHandler call,
		    // which wasn't reported through that call!
		    if (e != exception)
			diagnostic = "(odd SAXParseException) " + diagnostic;
		    else if (diagnostic == null)
			diagnostic = "(odd SAXParseException) [null]";
		}
	    } catch (SAXException e) {
		if (exception == null || diagnosticLevel == -1) {
		    diagnostic = "(thrown SAXException) ";
		    if (e.getMessage () != null)
			diagnostic += e.getMessage ();
		    else
			diagnostic += "[no exception message]";
		    diagnosticLevel = 2;
		} else
		    // exception thrown after non-warning ErrorHandler call
		    diagnostic = "(odd SAXException) " + diagnostic;
	    } catch (IOException e) {
		if (exception == null || diagnosticLevel == -1) {
		    diagnostic = "(thrown IOException) ";
		    if (e.getMessage () != null)
			diagnostic += e.getMessage ();
		    else
			diagnostic += "[no exception message]";
		    diagnosticLevel = 2;
		} else
		    // exception thrown after non-warning ErrorHandler call
		    diagnostic = "(odd IOException) " + diagnostic;
	    } catch (Exception e) {
		String	name = e.getClass ().getName ();
		if (exception == null || diagnosticLevel == -1) {
		    diagnostic = "(thrown " + name + ") ";
		    if (e.getMessage () != null)
			diagnostic += e.getMessage ();
		    else
			diagnostic += "[no exception message]";
		    diagnosticLevel = 2;
		} else
		    // exception thrown after non-warning ErrorHandler call
		    diagnostic = "(odd " + name + ") " + diagnostic;
		
		// it's clearly an internal parser bug; it should
		// only have thrown a SAX or IO exception.  Print it
		// for any diagnostic merit found therein.

		// not always.  It's caused null pointer exceptions too.
		System.err.println ("** " + diagnostic);
		// e.printStackTrace (System.err);
	    }

	    exception = null;
	}

	void appendLocation (SAXParseException x)
	{
	    int temp;

	    temp = x.getLineNumber ();
	    if (temp > 0)
		diagnostic += " [line " + temp + "]";
	    temp = x.getColumnNumber ();
	    if (temp > 0)
		diagnostic += " [column " + temp + "]";
	}

	/**
	 * SAX-SPECIFIC error handler, used to track the diagnostics that
	 * the parser reports.
	 */
	public void warning (SAXParseException x)
	throws SAXException
	{
	    if (exception != null)
		return;
	    diagnostic = "(warning) ";
	    if (x.getMessage () != null)
		diagnostic += x.getMessage ();
	    else
		diagnostic += "[no exception message]";
	    appendLocation (x);
	    diagnosticLevel = -1;
	    exception = x;
	}

	/**
	 * SAX-SPECIFIC error handler, used to track the diagnostics that
	 * the parser reports.
	 */
	public void error (SAXParseException x)
	throws SAXException
	{
	    if (exception != null && diagnosticLevel != -1)
		return;
	    diagnostic = "(error) ";
	    if (x.getMessage () != null)
		diagnostic += x.getMessage ();
	    else
		diagnostic += "[no exception message]";
	    appendLocation (x);
	    diagnosticLevel = 0;
	    exception = x;
	}

	/**
	 * SAX-SPECIFIC error handler, used to track the diagnostics that
	 * the parser reports.
	 */
	public void fatalError (SAXParseException x)
	throws SAXException
	{
	    diagnostic = "(fatal) ";
	    if (x.getMessage () != null)
		diagnostic += x.getMessage ();
	    else
		diagnostic += "[no exception message]";
	    appendLocation (x);
	    diagnosticLevel = 1;
	    exception = x;
	    throw x;
	}
    }

    /**
     * SAX2 Handler used to parse the test suite document.  This
     * one does things specific to testing XML conformance, using SAX.
     *
     * Can't use DOM, since we need to resolve URLs relative to
     * the entity in which they're found, not the base document
     * URL (which we'd know since it's our command line input).
     * Does xml:base solve this?
     *
     * NOTE: this doesn't save/use profile names.  Since tests
     * have unique ID attributes, they're not needed.
     */
    static final class Handler extends DefaultHandler
    {
	// used to resolve relative URIs for output files
	private Locator		locator;

	// [VISIBLE] sorted dictionaries for each type of test
	final Sorter		valid = new Sorter ();
	final Sorter		invalid = new Sorter ();
	final Sorter		notWF = new Sorter ();
	final Sorter		optional = new Sorter ();

	// [VISIBLE] name of overall suite
	String			suite;

	// test description being constructed
	private Test		test;
	private StringBuffer	description;

	public void setDocumentLocator (Locator l)
	{
	    locator = l;
	}
	
	/**
	 * Resets everything.
	 * @exception SAXException if there's no Locator.
	 */
	public void startDocument ()
	throws SAXException
	{
	    if (locator == null)
		throw new SAXException ("XMLReader didn't provide Locator.");

	    // these are just in case the handler gets reused
	    valid.clear ();
	    invalid.clear ();
	    notWF.clear ();
	    optional.clear ();
	    description = new StringBuffer ();
	}

	public void startElement (
	    String	namespace,
	    String	local,
	    String	tag,
	    Attributes	attrs
	) throws SAXException
	{
	    if ("TEST".equals (tag)) {
		try {
		    test = new Test (attrs, new URL (locator.getSystemId ()));
		} catch (IOException e) {
		    throw new SAXParseException (e.getMessage (),
			locator, e);
		}
		description.setLength (0);
	    } else if ("EM".equals (tag)
		    || "B".equals (tag)) {
		// NOTE:  We don't save this markup at this time since
		// we can't pass it through to the report.
		/*
		description.append ("<");
		description.append (tag);
		description.append (">");
		*/
	    } else if ("TESTSUITE".equals (tag)
		    || "TESTCASES".equals (tag)) {
		if ("TESTSUITE".equals (tag) || suite == null)
		    suite = attrs.getValue ("PROFILE");
	    } else
		throw new SAXParseException (
		    "invalid input element: " + tag,
		    locator);
	}

	public void endElement (String namespace, String local, String tag)
	throws SAXException
	{
	    if ("TEST".equals (tag)) {
		test.description = description.toString ();
		if ("valid".equals (test.type))
		    valid.put (test, test);
		else if ("invalid".equals (test.type))
		    invalid.put (test, test);
		else if ("not-wf".equals (test.type))
		    notWF.put (test, test);
		else if ("error".equals (test.type))
		    optional.put (test, test);
		else
		    throw new SAXParseException ("invalid input data", locator);
		test = null;
	    } else if ("EM".equals (tag)
		    || "B".equals (tag)) {
		// As in startElement -- we can't pass this through yet.
		/*
		description.append ("</");
		description.append (tag);
		description.append (">");
		*/
	    } else if ("TESTSUITE".equals (tag)
		    || "TESTCASES".equals (tag)) {
		// nothing
	    } else
		throw new SAXParseException ("broken input data", locator);
	}

	public void characters (char buf [], int offset, int len)
	throws SAXException
	{
	    if (test != null)
		description.append (buf, offset, len);
	    // else probably we're using an input database that
	    // has no DTD and we've got whitespace...
	}

	public void endDocument ()
	{
	    locator = null;
	    description = null;
	}

	//
	// Treat validity (and similar) errors rudely
	//
	public void error (SAXParseException e)
	throws SAXParseException
	{
	    fatalError (e);
	}

	public void fatalError (SAXParseException e)
	throws SAXParseException
	{
	    System.err.println ("** Parsing Problem: " + e.getMessage ());
	    System.err.println ("   URI  = " + e.getSystemId ());
	    System.err.println ("   Line = " + e.getLineNumber ());
	    throw e;
	}
    }

    /**
     * Collect exception cases which get special case
     * treatment on this run (being totally ignored).
     * For those "crashes the VM" or "never returns"
     * types of errors (less common lately).
     */
    static final class ExceptionHandler extends DefaultHandler
    {
	Vector		exceptions = new Vector ();

	public void startElement (
	    String	namespace,
	    String	local,
	    String	name,
	    Attributes	attrs
	) throws SAXException
	{
	    String	id;

	    if (!"EXCEPTION".equals (name))
		return;
	    if ((id = attrs.getValue ("ID")) == null) {
		System.err.println ("bogus exception file, no ID");
		return;
	    }
	    exceptions.addElement (id);
	}
    }


    /**
     * XHTML report output is written using a template, with the key
     * data inserted according to certain processing instructions.
     * An alternative would be just to have a test case result
     * format that an XSLT stylesheet would massage.
     *
     * <p> This knows a lot of XML-specific things, but not
     * stuff like being schema-valid.
     */
    static final class Reporter implements ContentHandler
    {
	private Handler		handler;
	private XMLReader	parser;

	private ContentHandler	out;

	// NOTE:  JVMs often report times in PST regardless
	// of the true local timezone ...
	private String		now = new Date ().toString ();

	private boolean		validating;
	private String		generalEntities;
	private String		parameterEntities;

	private int		skipCount;
	private int		pass;
	private int		fail;
	private int		negativePass;

	private boolean		echo = true;

	private AttributesImpl	failAttrs = new AttributesImpl ();
	private AttributesImpl	valign = new AttributesImpl ();


	Reporter (Handler h, XMLReader p)
	throws IOException
	{
	    handler = h;
	    parser = p;

	    // Write as US-ASCII ... the writer won't provide an XML
	    // declaration (increasing interoperability) and will also
	    // use the builtin XHTML entities as needed.

	    // The curious construct here is for portability, since
	    // many JVMs have no writers with encoding "US-ASCII".
	    // ASCII is all that'll be written to the stream; but
	    // its extended 8859/1 cousin is the 'writer' we get.
	    out = new XhtmlEchoHandler (
		new OutputStreamWriter (System.out, "8859_1"),
		"US-ASCII");

	    // defer the rest of the initialization
	}

	private String entityDescr (int value)
	{
	    switch (value) {
		case 1: return "included";
		case 0: return "ignored";
		default: return "unknown";
	    }
	}

	// At least one parser has been known to include non-XML
	// characters in its output diagnostic (e.g. U+C); even
	// though it's slow, we'll just do this character at a time
	// for _all_ parser reports.
	private void outStringCheck (String toWrite)
	throws SAXException
	{
	    if (toWrite == null)
		toWrite = "";

	    char buf [] = toWrite.toCharArray ();

	    for (int i = 0; i < buf.length; i++) {
		char c = buf [i];

		if (c == 0x09 || c == 0x0a || c == 0x0d
			|| (c >= 0x20 && c <= 0xfffd))
		    out.characters (buf, i, 1);
		else
		    outString ("<U+" + Integer.toHexString (c) + ">");
	    }
	}

	private void outString (String toWrite)
	throws SAXException
	{
	    if (toWrite == null)
		toWrite = "";

	    char buf [] = toWrite.toCharArray ();
	    out.characters (buf, 0, buf.length);
	}

	private void outNL () throws SAXException
	{
	    outString ("\n");
	}

	private void dumpTest (Test test)
	throws SAXException
	{
	    out.startElement ("", "", "tr", valign);
	    outNL ();

	    // section/rules
	    out.startElement ("", "", "td", null);
	    outString (test.sections);
	    out.endElement ("", "", "td");
	    outNL ();

	    // test ID 
	    out.startElement ("", "", "td", null);
	    outString (test.id);
	    out.endElement ("", "", "td");
	    outNL ();

	    // test description
	    out.startElement ("", "", "td", null);
	    if (test.description != null || "".equals (test.description))
		outString (test.description);
	    out.endElement ("", "", "td");
	    outNL ();

	    // resulting diagnostic
	    if (test.pass || "error".equals (test.type))
		out.startElement ("", "", "td", null);
	    else {
		out.startElement ("", "", "td", failAttrs);
		out.startElement ("", "", "em", null);
		out.startElement ("", "", "b", null);
		outString ("FAIL ");
		out.endElement ("", "", "b");
	    }
	    if (test.diagnostic != null)
		outStringCheck (test.diagnostic);
	    else
		outString ("[diagnostic not provided]");
	    if (!(test.pass || "error".equals (test.type)))
		out.endElement ("", "", "em");
	    out.endElement ("", "", "td");
	    outNL ();

	    out.endElement ("", "", "tr");
	    outNL ();
	}


	// for output tests that failed in their own right, or
	// because their input side failed.
	private void dumpOutput (Test test)
	throws SAXException
	{
	    out.startElement ("", "", "tr", valign);
	    outNL ();

	    // test ID 
	    out.startElement ("", "", "td", null);
	    outString (test.id);
	    out.endElement ("", "", "td");
	    outNL ();

	    // test description
	    out.startElement ("", "", "td", failAttrs);
	    if (test.outputDiagnostic == null)
		outString ("(Input failed, no output available)");
	    else
		outString (test.outputDiagnostic);
	    out.endElement ("", "", "td");
	    outNL ();

	    out.endElement ("", "", "tr");
	    outNL ();
	}

	public void processingInstruction (String target, String params)
	throws SAXException
	{
	    if ("run-id".equals (target)) {
		if (!echo)
		    return;

		// parser info
		if ("name".equals (params)) {
		    outString (parser.getClass ().getName ());
		} else if ("description".equals (params)) {
		    outString (description);
		} else if ("general-entities".equals (params)) {
		    outString (generalEntities);
		} else if ("parameter-entities".equals (params)) {
		    outString (parameterEntities);
		} else if ("type".equals (params)) {
		    if (validating)
			outString ("Validating");
		    else
			outString ("Non-Validating");

		// test run/environment info
		} else if ("date".equals (params)) {
		    outString (now);
		} else if ("harness".equals (params)) {
		    outString (Driver.class.getName ());
		} else if ("java".equals (params)) {
		    StringBuffer	output = new StringBuffer ();

		    // "java.vm.*" aren't always there; cope.
		    String		temp;
		    
		    temp = System.getProperty ("java.vm.name", "");
		    if ("".equals (temp))
			temp = "java";
		    output.append (temp);

		    output.append (" ");
		    temp = System.getProperty ("java.vm.version", "");
		    if ("".equals (temp))
			temp = System.getProperty ("java.version", "");
		    output.append (temp);

		    output.append (" ");
		    temp = System.getProperty ("java.vm.vendor", "");
		    if ("".equals (temp))
			temp = System.getProperty ("java.vendor", "");
		    output.append (temp);

		    outString (output.toString ());

		} else if ("os".equals (params)) {
		    StringBuffer	output = new StringBuffer ();

		    output.append (System.getProperty ("os.name"));
		    output.append ("/");
		    output.append (System.getProperty ("os.arch"));
		    output.append (" ");
		    output.append (System.getProperty ("os.version"));

		    outString (output.toString ());
		} else if ("testsuite".equals (params)) {
		    outString (handler.suite);
		} else if ("version".equals (params)) {
		    outString (version);

		// test result info
		} else if ("failed".equals (params)) {
		    outString (Integer.toString (fail));
		} else if ("passed".equals (params)) {
		    outString (Integer.toString (pass));
		} else if ("passed-negative".equals (params)) {
		    outString (Integer.toString (negativePass));
		} else if ("skipped".equals (params)) {
		    outString (Integer.toString (skipCount));
		} else if ("status".equals (params)) {
		    // Conformance is only provisional since the negative
		    // tests need to be individually examined
		    outString ((fail == 0)
			? ((pass == 0) ? "N/A" : "CONFORMS (provisionally)")
			: "DOES NOT CONFORM");

		} else
		    outString ("ILLEGAL run-id PI, " + params);
	    } else if ("table".equals (target)) {
		boolean empty = true;
		String	colspan = "4";

		if (!echo)
		    return;
		if ("valid".equals (params)) {
		    for (Enumeration iter = handler.valid.elements ();
			    iter.hasMoreElements ();) {
			// For "positive" tests -- only dump failed tests,
			// or ones emitting diagnostics
			Test test = (Test) iter.nextElement ();
			if (test.skipped)
			    continue;
			if (!test.pass || test.diagnostic != null) {
			    empty = false;
			    dumpTest (test);
			}
		    }
		} else if ("valid output".equals (params)) {
		    colspan = "2";
		    for (Enumeration iter = handler.valid.elements ();
			    iter.hasMoreElements ();) {
			// For "positive" tests -- only dump failed tests,
			// or ones emitting diagnostics
			Test test = (Test) iter.nextElement ();
			if (test.skipped)
			    continue;
			if (test.pass && test.outputDiagnostic == null)
			    continue;
			// FIXME -- only the right one of these should matter
			if (test.nvOutputURI == null && test.valOutputURI == null)
			    continue;
			empty = false;
			dumpOutput (test);
		    }
		} else if ("invalid positive".equals (params)) {
		    for (Enumeration iter = handler.invalid.elements ();
			    iter.hasMoreElements ();) {
			// For "positive" tests -- only dump failed tests,
			// or ones emitting diagnostics
			Test test = (Test) iter.nextElement ();
			if (test.skipped)
			    continue;
			if (!test.pass || test.diagnostic != null) {
			    empty = false;
			    dumpTest (test);
			}
		    }
		} else if ("invalid negative".equals (params)) {
		    for (Enumeration iter = handler.invalid.elements ();
			    iter.hasMoreElements (); ) {
			Test test = (Test) iter.nextElement ();
			if (test.skipped)
			    continue;
			dumpTest (test);
			empty = false;
		    }
		} else if ("not-wf".equals (params)) {
		    for (Enumeration iter = handler.notWF.elements ();
			    iter.hasMoreElements (); ) {
			Test test = (Test) iter.nextElement ();
			if (test.skipped)
			    continue;
			dumpTest (test);
			empty = false;
		    }
		} else if ("error".equals (params)) {
		    for (Enumeration iter = handler.optional.elements ();
			    iter.hasMoreElements (); ) {
			Test test = (Test) iter.nextElement ();
			if (test.skipped)
			    continue;
			dumpTest (test);
			empty = false;
		    }
		} else
		    outString ("ILLEGAL table PI, " + params);

		if (empty) {
		    AttributesImpl	tmp = new AttributesImpl ();
		    tmp.addAttribute ("", "", "colspan", "CDATA", colspan);
		    out.startElement ("", "", "tr", valign);
		    out.startElement ("", "", "td", tmp);
		    out.startElement ("", "", "center", null);
		    out.startElement ("", "", "em", null);
		    outString ("Nothing to report!");
		    out.endElement ("", "", "em");
		    out.endElement ("", "", "center");
		    out.endElement ("", "", "td");
		    out.endElement ("", "", "tr");
		}
	    } else if ("if".equals (target)) {
		if (!echo) {
		    // the single case we can detect...
		    outString ("ILLEGAL nested 'if' PI");
		    return;
		}
		if ("validating".equals (params))
		    echo = validating;
		else if ("nonvalidating".equals (params))
		    echo = !validating;
		else
		    outString ("ILLEGAL 'if' PI, " + params);
	    } else if ("endif".equals (target)) {
		echo = true;
	    } else
		outString ("ILLEGAL PI, " + target + " " + params);
	}

	// For everything except PIs (above), we pass things through
	// unaltered -- except to filter out some end tag construction
	// for certain HTML elements.

	public void setDocumentLocator (Locator l)
	{
	    out.setDocumentLocator (l);

	    // We use this call to trigger initialization.
	    
	    // if we got this far, we know if we're validating!
	    validating = (1 == getFeature (parser,
		    "http://xml.org/sax/features/validation",
		    "xml.testing.Driver.validation"));
	    
	    // ... but general and parameter entity handling may be
	    // unknown (causing tests to be skipped)
	    generalEntities = entityDescr (getFeature (parser,
		    "http://xml.org/sax/features/external-general-entities",
		    "xml.testing.Driver.general-entities"));
	    parameterEntities = entityDescr (getFeature (parser,
		    "http://xml.org/sax/features/external-parameter-entities",
		    "xml.testing.Driver.parameter-entities"));
	    
	    // TODO:  use CSS for all colors 

	    failAttrs.addAttribute ("", "", "bgcolor", "CDATA", "#ffaacc");
	    valign.addAttribute ("", "", "valign", "CDATA", "top");
	    
	    for (Enumeration iter = handler.valid.elements ();
		    iter.hasMoreElements ();) {
		Test test = (Test) iter.nextElement ();

		// count the basic positive test ...
		if (test.skipped)
		    skipCount++;
		else if (test.pass)
		    pass++;
		else
		    fail++;

		// ... and any output test!
		if (validating && test.valOutputURI != null) {
		    if (test.skipped)
			skipCount++;
		    else if (test.outputDiagnostic == null)
			pass++;
		    else
			fail++;
		} else if (test.nvOutputURI != null) {
		    if (test.skipped)
			skipCount++;
		    else if (test.outputDiagnostic == null)
			pass++;
		    else
			fail++;
		}
	    }
	    for (Enumeration iter = handler.invalid.elements ();
		    iter.hasMoreElements ();) {
		Test test = (Test) iter.nextElement ();
		if (test.skipped)
		    skipCount++;
		else if (test.pass) {
		    pass++;
		    if (validating)
			negativePass++;
		} else
		    fail++;
	    }
	    for (Enumeration iter = handler.notWF.elements ();
		    iter.hasMoreElements ();) {
		Test test = (Test) iter.nextElement ();
		if (test.skipped)
		    skipCount++;
		else if (test.pass) {
		    pass++;
		    negativePass++;
		} else
		    fail++;
	    }
	    // ignore the "optional" tests here!
	}

	public void startDocument ()
	throws SAXException
	{
	    out.startDocument ();
	}

	public void startPrefixMapping (String prefix, String uri)
	    { /* ignored */ } 

	public void endPrefixMapping (String prefix)
	    { /* ignored */ } 
	
	public void skippedEntity (String name)
	    { /* ignored */ } 

	public void startElement (
	    String	namespace,
	    String	local,
	    String	name,
	    Attributes	attrs
	) throws SAXException
	{
	    if (!echo)
		return;
	    out.startElement (namespace, local, name, attrs);
	}

	public void endElement (String namespace, String local, String name)
	throws SAXException
	{
	    if (!echo)
		return;

	    // XhtmlEchoHandler deals with (X)HTML empty elements
	    out.endElement (namespace, local, name);
	}

	public void characters (char buf [], int offset, int len)
	throws SAXException
	{
	    if (!echo)
		return;
	    out.characters (buf, offset, len);
	}

	public void ignorableWhitespace (char buf [], int offset, int len)
	throws SAXException
	{
	    if (!echo)
		return;
	    out.ignorableWhitespace (buf, offset, len);
	}

	public void endDocument ()
	throws SAXException
	{
	    if (!echo)
		System.err.println ("unterminated 'if' PI!");
	    out.endDocument ();
	}
    }


    /**
     * Output tests, using second and third XML canonical forms.
     * These build on James Clark's "first" XML Canonical form,
     * recording additional information XML processors must report
     * to applications.  (Output generation code is modified from
     * James' code.)
     *
     * <p>TODO:  decide what to do about third canon; maybe the
     * infoset folk said those data don't matter any more??
     * we don't have any 3cf testdata, and some of the 2cf bits
     * have been missing too.  If we don't test it, then it's
     * effectively going to get written out of the spec ...
     */
    final static class Outputter
	implements ContentHandler, DTDHandler
    {
	// controls second vs third form
	private boolean			isValidating;

	// what we'll compare against
	private String			compareURI;

	// buffers canonicalized data
	private StringBuffer		utf16 = new StringBuffer ();

	// collects final output
	private ByteArrayOutputStream	utf8 = new ByteArrayOutputStream ();

	// internal DTD subset sometimes must be created
	private Sorter			notations;
	private Sorter			entities;


	Outputter (boolean validating, String uri)
	{
	    isValidating = validating;
	    compareURI = uri;
	}

	//
	// After collecting the output, this is what does the work
	// of comparing the canonicalized output with what it's
	// supposed to look like.
	//
	// Return data is null to indicate no problems, else is a
	// diagnostic describing the differences in as useful a
	// manner as possible (and in pure ASCII).
	//
	public String getDiagnostic ()
	{
	    byte	actual [] = utf8.toByteArray ();
	    byte	canonical [];
	    String	retval = null;

	    // Get the canonical data
	    try {
		URL		uri = new URL (compareURI);
		InputStream	in = uri.openStream ();
		byte		buf [] = new byte [1024];
		int		len;
		
		utf8.reset();
		while ((len = in.read (buf)) >= 0)
		    utf8.write (buf, 0, len);
		in.close ();
		canonical = utf8.toByteArray ();

	    } catch (Exception e) {
		return "[I/O problem checking output: " + e.getMessage () + "]";
	    }

	    // generate diagnostic

	    // XXX this is quick and dirty for now ... the most useful
	    // output has context, preferably both sets of data, and
	    // doesn't just sketch the first difference!!

	    for (int i = 0; retval == null && i < canonical.length; i++) {
		if (i >= actual.length)
		    retval = "Output ends at byte " + i
			+ " but should continue to byte "
			+ canonical.length;
		else if (actual [i] != canonical [i])
		    retval = "Output byte " + i
			+ " has the wrong value; actual 0x"
			+ Integer.toHexString (0x0ff & actual [i])
			+ " should instead be 0x"
			+ Integer.toHexString (0x0ff & canonical [i]);
	    }
	    if (retval == null && actual.length != canonical.length)
		retval = "Too much output; length is " + actual.length
		    + " but should only be " + canonical.length;

	    // null iff no error
	    return retval;
	}

	// JDK's UTF-8 writing isn't quite right; so we use our own.
	// (Surrogates are a basic issue.)
	private void writeUTF8 (char buf [], int offset, int len)
	throws IOException
	{
	    for (int i = 0; i < len; i++) {
		char c = buf [offset + i];

		// 1 byte encoding?
		if (c < 0x80) {
		    utf8.write (c);	
		    continue;
		}

		switch (c & 0xF800) {
		  case 0:		// 2 byte encoding?
		    utf8.write ((((c >> 6) & 0x1F) | 0xC0));
		    utf8.write (((c & 0x3F) | 0x80));
		    continue;

		  case 0xD800:		// surrogate pair

		    // surrogate pairs use 4 byte encodings
		    if (i + 1 < len) {
			char c2 = buf [offset + i + 1];

			if ((c & 0xFC00) == 0xD800
				&& (c2 & 0xFC00) == 0xDC00) {
			    ++i;
			    int n = ((c & 0x3FF) << 10) | (c2 & 0x3FF);
			    n += 0x10000;
			    utf8.write ((((n >> 18) & 0x7) | 0xF0));
			    utf8.write ((((n >> 12) & 0x3F) | 0x80));
			    utf8.write ((((n >> 6) & 0x3F) | 0x80));
			    utf8.write (((n & 0x3F) | 0x80));
			    continue;
			}
			// else ERROR, unpaired surrogate
		    }
		    // fall through; output comparisons will fail
		    // (we're writing malformed UTF-8)

		  default:		// else 3 byte encoding
		    utf8.write ((((c >> 12) & 0xF) | 0xE0));
		    utf8.write ((((c >> 6) & 0x3F) | 0x80));
		    utf8.write (((c & 0x3F) | 0x80));
		    continue;
		}
	    }
	}

	// canonical output is in two steps; first canonicalize
	// Unicode/UTF-16 characters, then encode them as UTF-8.
	private void escapeUTF16 (char c)
	{
	    switch (c) {
	      case '&':		utf16.append ("&amp;");	break;
	      case '<':		utf16.append ("&lt;");	break;
	      case '>':		utf16.append ("&gt;");	break;
	      case '"':		utf16.append ("&quot;"); break;
	      case '\t':	utf16.append ("&#9;");	break;
	      case '\n':	utf16.append ("&#10;");	break;
	      case '\r':	utf16.append ("&#13;");	break;
	      default:		utf16.append (c);	break;
	    }
	}

	public void escapeUTF16 (char buf [], int off, int len)
	{
	    while (len-- > 0)
		escapeUTF16 (buf [off++]);
	}

	private void escapeUTF16 (String s)
	{
	    char buf [] = s.toCharArray ();
	    escapeUTF16 (buf, 0, buf.length);
	}

	// many strings are "pre-canonicalized", like names and
	// other XML syntax.
	private void writeUTF16 (String s)
	{
	    utf16.append (s);
	}


	// write the UTF-16 buffer to UTF-8 data, knowing that
	// there "should" be no partial surrogate pairs.
	private void flushUTF16 ()
	throws SAXException
	{
	    try {
		char buf [] = utf16.toString ().toCharArray ();
		utf16.setLength (0);
		writeUTF8 (buf, 0, buf.length);
	    } catch (IOException e) {
		throw new SAXException ("I/O error", e);
	    }
	}

	// NOTE:  this doesn't do anything intelligent with URIs
	// found in public IDs ... for now, it's an error if the
	// input data uses relative URIs that'll need to show up
	// with NOTATION and unparsed ENTITY declarations.

	// here begin the SAX callbacks

	public void setDocumentLocator (Locator l)
	    { /* IGNORE */ }

	public void startDocument ()
	throws SAXException
	    { /* IGNORE */ }

	public void processingInstruction (String target, String params)
	throws SAXException
	{
	    writeUTF16 ("<?");
	    writeUTF16 (target);
	    writeUTF16 (" ");
	    writeUTF16 (params);
	    writeUTF16 ("?>");
	    flushUTF16 ();
	}

	public void notationDecl (
	    String name,
	    String publicID,
	    String systemID
	) throws SAXException
	{
	    String	decl;

	    decl = "<!NOTATION " + name;
	    if (publicID != null) {
		decl += " PUBLIC '" + publicID;
		if (systemID != null)
		    decl += "' '" + systemID;
	    } else {
		decl += " SYSTEM '" + systemID;
	    }
	    decl += "'>\n";

	    if (notations == null)
		notations = new Sorter ();
	    notations.put (name, decl);
	}

	// First difference between the two basic canonical forms:
	// validating parsers are required to report unparsed entity
	// decls, nonvalidating ones aren't.
	public void unparsedEntityDecl (
	    String name,
	    String publicID,
	    String systemID,
	    String notation
	) throws SAXException
	{
	    String	decl;

	    if (!isValidating)
		return;

	    decl = "<!ENTITY " +name;
	    if (publicID != null)
		decl += "PUBLIC '" + publicID +"' '";
	    else
		decl += "SYSTEM '";
	    decl += systemID + "' NDATA " + notation +">\n";

	    if (entities == null)
		entities = new Sorter ();
	    entities.put (name, decl);
	}

	public void startPrefixMapping (String prefix, String uri)
	    { /* ignored */ }

	public void endPrefixMapping (String prefix)
	    { /* ignored */ }

	public void skippedEntity (String name)
	    { /* ignored */ }

	public void startElement (
	    String	namespace,
	    String	local,
	    String	name,
	    Attributes	attrs
	) throws SAXException
	{
	    if (notations != null) {
		writeUTF16 ("<!DOCTYPE ");
		writeUTF16 (name);	// assumed
		writeUTF16 (" [\n");
		flushUTF16 ();
		for (Enumeration iter = notations.elements ();
			iter.hasMoreElements ();)
		    writeUTF16 ((String) iter.nextElement ());
		flushUTF16 ();
		if (entities != null) {
		    for (Enumeration iter = entities.elements ();
			    iter.hasMoreElements ();)
			writeUTF16 ((String) iter.nextElement ());
		}
		writeUTF16 ("]>\n");
		flushUTF16 ();

		notations = null;
		entities = null;
	    }

	    writeUTF16 ("<");
	    writeUTF16 (name);
	    if (attrs != null && attrs.getLength () != 0) {
		int	len = attrs.getLength ();
		int	v [] = new int [len];

		for (int i = 0; i < len; i++)
		    v [i] = i;

		// insertion sort by attribute name
		for (int i = 1; i < len; i++) {
		    int		n = v [i], j;
		    String	s = attrs.getQName (n);

		    for (j = i - 1; j >= 0; j--) {
			if (s.compareTo (attrs.getQName (v[j])) >= 0)
			    break;
			v [j + 1] = v [j];
		    }
		    v [j + 1] = n;
		}

		// write attributes in order
		for (int i = 0; i < len; i++) {
		    writeUTF16 (" ");
		    writeUTF16 (attrs.getQName (v [i]));
		    writeUTF16 ("=\"");
		    escapeUTF16 (attrs.getValue (v [i]));
		    writeUTF16 ("\"");
		    flushUTF16 ();
		}
	    }
	    writeUTF16 (">");
	    flushUTF16 ();
	}

	public void endElement (String namespace, String local, String name)
	throws SAXException
	{
	    writeUTF16 ("</");
	    writeUTF16 (name);
	    writeUTF16 (">");
	    flushUTF16 ();
	}

	public void characters (char buf [], int offset, int len)
	throws SAXException
	{
	    escapeUTF16 (buf, offset, len);
	    flushUTF16 ();
	}

	// Second difference between the two canonical forms:
	// validating parsers report an additional bit of information,
	// ignorable whitespace is used instead of characters().
	// Output reflects such reportage by discarding these characters,
	// not reporting that bit causes a comparison error.
	public void ignorableWhitespace (char buf [], int offset, int len)
	throws SAXException
	{
	    if (!isValidating)
		characters (buf, offset, len);
	}

	public void endDocument ()
	throws SAXException
	    { /* IGNORE */ }
    }

    /**
     * In order to run on VMs that only have JDK 1.1 functionality, such
     * as the GNU "GCJ" native compiler and the Microsoft JVM, we use this
     * instead of the JDK 1.2 collections classes.  It's just a hashtable
     * which when asked for its values enumeration, returns a custom one
     * such that the values are sorted by order of keys.
     * 
     * <p>We know that the keys are only String or Test values; and also (to
     * avoid resorting) that the table won't be modified after it's first
     * populated.
     */
    final static class Sorter extends Hashtable
    {
	private Vector		v;

	public Enumeration elements ()
	{
	    if (size () == 0)
		return super.elements ();
	    if (v != null && v.size () == size ())
		return v.elements ();

	    // create of keys
	    Object	keys [] = new Object [size ()];
	    int		i = 0;

	    for (Enumeration e = keys (); e.hasMoreElements (); i++)
	    	keys [i] = e.nextElement ();
	    boolean isString = (keys [0] instanceof String);

	    // insertion sort by key
	    for (i = 1; i < keys.length; i++) {
		int		j;
		Object		temp;

		for (j = i - 1; j >= 0; j--) {
		    boolean doSwap;

		    if (isString)
			doSwap = (((String) keys [j]).compareTo (
				   (String) keys [j + 1])) > 0;
		    else
			doSwap = compare ((Test) keys [j],
					  (Test) keys [j + 1]) > 0;

		    if (doSwap) {
			temp = keys [j];
			keys [j] = keys [j + 1];
			keys [j + 1] = temp;
		    } else
			break;
		}
	    }

	    // construct a vector whose values are sorted
	    v = new Vector (keys.length);

	    for (i = 0; i < keys.length; i++)
		v.addElement (get (keys [i]));

	    // return the vector's (ordered) enumeration
	    return v.elements ();
	}

	private int compare (Test left, Test right)
	{
	    int		temp;

	    // object is same as self
	    if (left == right)
		return 0;

	    // order first by section and rules
	    // XXX this wrongly reports "2.3 [3]" after "2.3 [12]"
	    if ((temp = left.sections.compareTo (right.sections)) != 0)
		return temp;

	    // tiebreaker for tests of the same section and rules
	    if ((temp = left.id.compareTo (right.id)) != 0)
		return temp;

	    // if ID is the same, the test objects must be the same!
	    throw new IllegalArgumentException ();
	}
    }
}
