/* ValidationConsumer.java --
   Copyright (C) 1999,2000,2001 Free Software Foundation, Inc.

This file is part of GNU Classpath.

GNU Classpath 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, or (at your option)
any later version.

GNU Classpath 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 GNU Classpath; see the file COPYING.  If not, write to the
Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301 USA.

Linking this library statically or dynamically with other modules is
making a combined work based on this library.  Thus, the terms and
conditions of the GNU General Public License cover the whole
combination.

As a special exception, the copyright holders of this library give you
permission to link this library with independent modules to produce an
executable, regardless of the license terms of these independent
modules, and to copy and distribute the resulting executable under
terms of your choice, provided that you also meet, for each linked
independent module, the terms and conditions of the license of that
module.  An independent module is a module which is not derived from
or based on this library.  If you modify this library, you may extend
this exception to your version of the library, but you are not
obligated to do so.  If you do not wish to do so, delete this
exception statement from your version. */

package gnu.xml.pipeline;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.EmptyStackException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Stack;
import java.util.StringTokenizer;
import java.util.Vector;

import org.xml.sax.Attributes;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;

/**
 * This class checks SAX2 events to report validity errors; it works as
 * both a filter and a terminus on an event pipeline.  It relies on the
 * producer of SAX events to:  </p> <ol>
 *
 *      <li> Conform to the specification of a non-validating XML parser that
 *      reads all external entities, reported using SAX2 events. </li>
 *
 *      <li> Report ignorable whitespace as such (through the ContentHandler
 *      interface).  This is, strictly speaking, optional for nonvalidating
 *      XML processors.  </li>
 *
 *      <li> Make SAX2 DeclHandler callbacks, with default
 *      attribute values already normalized (and without "&lt;").</li>
 *
 *      <li> Make SAX2 LexicalHandler startDTD() and endDTD ()
 *      callbacks. </li>
 *
 *      <li> Act as if the <em>(URI)/namespace-prefixes</em> property were
 *      set to true, by providing XML 1.0 names and all <code>xmlns*</code>
 *      attributes (rather than omitting either or both). </li>
 *
 *      </ol>
 *
 * <p> At this writing, the major SAX2 parsers (such as &AElig;lfred2,
 * Crimson, and Xerces) meet these requirements, and this validation
 * module is used by the optional &AElig;lfred2 validation support.
 * </p>
 *
 * <p> Note that because this is a layered validator, it has to duplicate some
 * work that the parser is doing; there are also other cost to layering.
 * However, <em>because of layering it doesn't need a parser</em> in order
 * to work! You can use it with anything that generates SAX events, such
 * as an application component that wants to detect invalid content in
 * a changed area without validating an entire document, or which wants to
 * ensure that it doesn't write invalid data to a communications partner.</p>
 *
 * <p> Also, note that because this is a layered validator, the line numbers
 * reported for some errors may seem strange.  For example, if an element does
 * not permit character content, the validator
 * will use the locator provided to it.
 * That might reflect the last character of a <em>characters</em> event
 * callback, rather than the first non-whitespace character. </p>
 *
 * <hr />
 *
 * <!--
 * <p> Of interest is the fact that unlike most currently known XML validators,
 * this one can report some cases of non-determinism in element content models.
 * It is a compile-time option, enabled by default.  This will only report
 * such XML errors if they relate to content actually appearing in a document;
 * content models aren't aggressively scanned for non-deterministic structure.
 * Documents which trigger such non-deterministic transitions may be handled
 * differently by different validating parsers, without losing conformance
 * to the XML specification. </p>
 * -->
 *
 * <p> Current limitations of the validation performed are in roughly three
 * categories.  </p>
 *
 * <p> The first category represents constraints which demand violations
 * of software layering:  exposing lexical details, one of the first things
 * that <em>application</em> programming interfaces (APIs) hide.  These
 * invariably relate to XML entity handling, and to historical oddities
 * of the XML validation semantics.  Curiously,
 * recent (Autumn 1999) conformance testing showed that these constraints are
 * among those handled worst by existing XML validating parsers.  Arguments
 * have been made that each of these VCs should be turned into WFCs (most
 * of them) or discarded (popular for the standalone declaration); in short,
 * that these are bugs in the XML specification (not all via SGML): </p><ul>
 *
 *      <li> The <em>Proper Declaration/PE Nesting</em> and
 *      <em>Proper Group/PE Nesting</em> VCs can't be tested because they
 *      require access to particularly low level lexical level information.
 *      In essence, the reason XML isn't a simple thing to parse is that
 *      it's not a context free grammar, and these constraints elevate that
 *      SGML-derived context sensitivity to the level of a semantic rule.
 *
 *      <li> The <em>Standalone Document Declaration</em> VC can't be
 *      tested.  This is for two reasons.  First, this flag isn't made
 *      available through SAX2.  Second, it also requires breaking that
 *      lexical layering boundary.  (If you ever wondered why classes
 *      in compiler construction or language design barely mention the
 *      existence of context-sensitive grammars, it's because of messy
 *      issues like these.)
 *
 *      <li> The <em>Entity Declared</em> VC can't be tested, because it
 *      also requires breaking that lexical layering boundary!  There's also
 *      another issue: the VC wording (and seemingly intent) is ambiguous.
 *      (This is still true in the "Second edition" XML spec.)
 *      Since there is a WFC of the same name, everyone's life would be
 *      easier if references to undeclared parsed entities were always well
 *      formedness errors, regardless of whether they're parameter entities
 *      or not.  (Note that nonvalidating parsers are not required
 *      to report all such well formedness errors if they don't read external
 *      parameter entities, although currently most XML parsers read them
 *      in an attempt to avoid problems from inconsistent parser behavior.)
 *
 *      </ul>
 *
 * <p> The second category of limitations on this validation represent
 * constraints associated with information that is not guaranteed to be
 * available (or in one case, <em>is guaranteed not to be available</em>,
 * through the SAX2 API: </p><ul>
 *
 *      <li> The <em>Unique Element Type Declaration</em> VC may not be
 *      reportable, if the underlying parser happens not to expose
 *      multiple declarations.   (&AElig;lfred2 reports these validity
 *      errors directly.)</li>
 *
 *      <li> Similarly, the <em>Unique Notation Name</em> VC, added in the
 *      14-January-2000 XML spec errata to restrict typing models used by
 *      elements, may not be reportable.  (&AElig;lfred reports these
 *      validity errors directly.) </li>
 *
 *      </ul>
 *
 * <p> A third category relates to ease of implementation.  (Think of this
 * as "bugs".)  The most notable issue here is character handling.  Rather
 * than attempting to implement the voluminous character tables in the XML
 * specification (Appendix B), Unicode rules are used directly from
 * the java.lang.Character class.  Recent JVMs have begun to diverge from
 * the original specification for that class (Unicode 2.0), meaning that
 * different JVMs may handle that aspect of conformance differently.
 * </p>
 *
 * <p> Note that for some of the validity errors that SAX2 does not
 * expose, a nonvalidating parser is permitted (by the XML specification)
 * to report validity errors.  When used with a parser that does so for
 * the validity constraints mentioned above (or any other SAX2 event
 * stream producer that does the same thing), overall conformance is
 * substantially improved.
 *
 * @see gnu.xml.aelfred2.SAXDriver
 * @see gnu.xml.aelfred2.XmlReader
 *
 * @author David Brownell
 */
public final class ValidationConsumer extends EventFilter
{
    // report error if we happen to notice a non-deterministic choice?
    // we won't report buggy content models; just buggy instances
    private static final boolean        warnNonDeterministic = false;

    // for tracking active content models
    private String              rootName;
    private Stack               contentStack = new Stack ();

    // flags for "saved DTD" processing
    private boolean             disableDeclarations;
    private boolean             disableReset;

    //
    // most VCs get tested when we see element start tags.  the per-element
    // info (including attributes) recorded here duplicates that found inside
    // many nonvalidating parsers, hence dual lookups etc ... that's why a
    // layered validator isn't going to be as fast as a non-layered one.
    //

    // key = element name; value = ElementInfo
    private Hashtable           elements = new Hashtable ();

    // some VCs relate to ID/IDREF/IDREFS attributes
    // key = id; value = boolean true (defd) or false (refd)
    private Hashtable           ids = new Hashtable ();

    // we just record declared notation and unparsed entity names.
    // the implementation here is simple/slow; these features
    // are seldom used, one hopes they'll wither away soon
    private Vector              notations = new Vector (5, 5);
    private Vector              nDeferred = new Vector (5, 5);
    private Vector              unparsed = new Vector (5, 5);
    private Vector              uDeferred = new Vector (5, 5);

        // note: DocBk 3.1.7 XML defines over 2 dozen notations,
        // used when defining unparsed entities for graphics
        // (and maybe in other places)



    /**
     * Creates a pipeline terminus which consumes all events passed to
     * it; this will report validity errors as if they were fatal errors,
     * unless an error handler is assigned.
     *
     * @see #setErrorHandler
     */
        // constructor used by PipelineFactory
            // ... and want one taking system ID of an external subset
    public ValidationConsumer ()
    {
        this (null);
    }

    /**
     * Creates a pipeline filter which reports validity errors and then
     * passes events on to the next consumer if they were not fatal.
     *
     * @see #setErrorHandler
     */
        // constructor used by PipelineFactory
            // ... and want one taking system ID of an external subset
            // (which won't send declaration events)
    public ValidationConsumer (EventConsumer next)
    {
        super (next);

        setContentHandler (this);
        setDTDHandler (this);
        try { setProperty (DECL_HANDLER, this); }
        catch (Exception e) { /* "can't happen" */ }
        try { setProperty (LEXICAL_HANDLER, this); }
        catch (Exception e) { /* "can't happen" */ }
    }


    private static final String fakeRootName
        = ":Nobody:in:their_Right.Mind_would:use:this-name:1x:";

    /**
     * Creates a validation consumer which is preloaded with the DTD provided.
     * It does this by constructing a document with that DTD, then parsing
     * that document and recording its DTD declarations.  Then it arranges
     * not to modify that information.
     *
     * <p> The resulting validation consumer will only validate against
     * the specified DTD, regardless of whether some other DTD is found
     * in a document being parsed.
     *
     * @param rootName The name of the required root element; if this is
     *  null, any root element name will be accepted.
     * @param publicId If non-null and there is a non-null systemId, this
     *  identifier provides an alternate access identifier for the DTD's
     *  external subset.
     * @param systemId If non-null, this is a URI (normally URL) that
     *  may be used to access the DTD's external subset.
     * @param internalSubset If non-null, holds literal markup declarations
     *  comprising the DTD's internal subset.
     * @param resolver If non-null, this will be provided to the parser for
     *  use when resolving parameter entities (including any external subset).
     * @param resolver If non-null, this will be provided to the parser for
     *  use when resolving parameter entities (including any external subset).
     * @param minimalElement If non-null, a minimal valid document.
     *
     * @exception SAXNotSupportedException If the default SAX parser does
     *  not support the standard lexical or declaration handlers.
     * @exception SAXParseException If the specified DTD has either
     *  well-formedness or validity errors
     * @exception IOException If the specified DTD can't be read for
     *  some reason
     */
    public ValidationConsumer (
        String          rootName,
        String          publicId,
        String          systemId,
        String          internalSubset,
        EntityResolver  resolver,
        String          minimalDocument
    ) throws SAXException, IOException
    {
        this (null);

        disableReset = true;
        if (rootName == null)
            rootName = fakeRootName;

        //
        // Synthesize document with that DTD; is it possible to do
        // better for the declaration of the root element?
        //
        // NOTE:  can't use SAX2 to write internal subsets.
        //
        StringWriter    writer = new StringWriter ();

        writer.write ("<!DOCTYPE ");
        writer.write (rootName);
        if (systemId != null) {
            writer.write ("\n  ");
            if (publicId != null) {
                writer.write ("PUBLIC '");
                writer.write (publicId);
                writer.write ("'\n\t'");
            } else
                writer.write ("SYSTEM '");
            writer.write (systemId);
            writer.write ("'");
        }
        writer.write (" [ ");
        if (rootName == fakeRootName) {
            writer.write ("\n<!ELEMENT ");
            writer.write (rootName);
            writer.write (" EMPTY>");
        }
        if (internalSubset != null)
            writer.write (internalSubset);
        writer.write ("\n ]>");

        if (minimalDocument != null) {
            writer.write ("\n");
            writer.write (minimalDocument);
            writer.write ("\n");
        } else {
            writer.write (" <");
            writer.write (rootName);
            writer.write ("/>\n");
        }
        minimalDocument = writer.toString ();

        //
        // OK, load it
        //
        XMLReader       producer;

        producer = XMLReaderFactory.createXMLReader ();
        bind (producer, this);

        if (resolver != null)
            producer.setEntityResolver (resolver);

        InputSource     in;

        in = new InputSource (new StringReader (minimalDocument));
        producer.parse (in);

        disableDeclarations = true;
        if (rootName == fakeRootName)
            this.rootName = null;
    }

    private void resetState ()
    {
        if (!disableReset) {
            rootName = null;
            contentStack.removeAllElements ();
            elements.clear ();
            ids.clear ();

            notations.removeAllElements ();
            nDeferred.removeAllElements ();
            unparsed.removeAllElements ();
            uDeferred.removeAllElements ();
        }
    }


    private void warning (String description)
    throws SAXException
    {
        ErrorHandler            errHandler = getErrorHandler ();
        Locator                 locator = getDocumentLocator ();
        SAXParseException       err;

        if (errHandler == null)
            return;

        if (locator == null)
            err = new SAXParseException (description, null, null, -1, -1);
        else
            err = new SAXParseException (description, locator);
        errHandler.warning (err);
    }

    // package private (for ChildrenRecognizer)
    private void error (String description)
    throws SAXException
    {
        ErrorHandler            errHandler = getErrorHandler ();
        Locator                 locator = getDocumentLocator ();
        SAXParseException       err;

        if (locator == null)
            err = new SAXParseException (description, null, null, -1, -1);
        else
            err = new SAXParseException (description, locator);
        if (errHandler != null)
            errHandler.error (err);
        else    // else we always treat it as fatal!
            throw err;
    }

    private void fatalError (String description)
    throws SAXException
    {
        ErrorHandler            errHandler = getErrorHandler ();
        Locator                 locator = getDocumentLocator ();
        SAXParseException       err;

        if (locator != null)
            err = new SAXParseException (description, locator);
        else
            err = new SAXParseException (description, null, null, -1, -1);
        if (errHandler != null)
            errHandler.fatalError (err);
        // we always treat this as fatal, regardless of the handler
        throw err;
    }


    private static boolean isExtender (char c)
    {
        // [88] Extender ::= ...
        return c == 0x00b7 || c == 0x02d0 || c == 0x02d1 || c == 0x0387
               || c == 0x0640 || c == 0x0e46 || c == 0x0ec6 || c == 0x3005
               || (c >= 0x3031 && c <= 0x3035)
               || (c >= 0x309d && c <= 0x309e)
               || (c >= 0x30fc && c <= 0x30fe);
    }


    // use augmented Unicode rules, not full XML rules
    private boolean isName (String name, String context, String id)
    throws SAXException
    {
        char    buf [] = name.toCharArray ();
        boolean pass = true;

        if (!Character.isUnicodeIdentifierStart (buf [0])
                && ":_".indexOf (buf [0]) == -1)
            pass = false;
        else {
            int max = buf.length;
            for (int i = 1; pass && i < max; i++) {
                char c = buf [i];
                if (!Character.isUnicodeIdentifierPart (c)
                        && ":-_.".indexOf (c) == -1
                        && !isExtender (c))
                    pass = false;
            }
        }

        if (!pass)
            error ("In " + context + " for " + id
                + ", '" + name + "' is not a name");
        return pass;    // true == OK
    }

    // use augmented Unicode rules, not full XML rules
    private boolean isNmtoken (String nmtoken, String context, String id)
    throws SAXException
    {
        char    buf [] = nmtoken.toCharArray ();
        boolean pass = true;
        int     max = buf.length;

        // XXX make this share code with isName

        for (int i = 0; pass && i < max; i++) {
                char c = buf [i];
            if (!Character.isUnicodeIdentifierPart (c)
                    && ":-_.".indexOf (c) == -1
                    && !isExtender (c))
                pass = false;
        }

        if (!pass)
            error ("In " + context + " for " + id
                + ", '" + nmtoken + "' is not a name token");
        return pass;    // true == OK
    }

    private void checkEnumeration (String value, String type, String name)
    throws SAXException
    {
        if (!hasMatch (value, type))
            // VC: Enumeration
            error ("Value '" + value
                + "' for attribute '" + name
                + "' is not permitted: " + type);
    }

    // used to test enumerated attributes and mixed content models
    // package private
    static boolean hasMatch (String value, String orList)
    {
        int len = value.length ();
        int max = orList.length () - len;

        for (int start = 0;
                (start = orList.indexOf (value, start)) != -1;
                start++) {
            char c;

            if (start > max)
                break;
            c = orList.charAt (start - 1);
            if (c != '|' && c != '('/*)*/)
                continue;
            c = orList.charAt (start + len);
            if (c != '|' && /*(*/ c != ')')
                continue;
            return true;
        }
        return false;
    }

    /**
     * <b>LexicalHandler</b> Records the declaration of the root
     * element, so it can be verified later.
     * Passed to the next consumer, unless this one was
     * preloaded with a particular DTD.
     */
    public void startDTD (String name, String publicId, String systemId)
    throws SAXException
    {
        if (disableDeclarations)
            return;

        rootName = name;
        super.startDTD (name, publicId, systemId);
    }

    /**
     * <b>LexicalHandler</b> Verifies that all referenced notations
     * and unparsed entities have been declared.
     * Passed to the next consumer, unless this one was
     * preloaded with a particular DTD.
     */
    public void endDTD ()
    throws SAXException
    {
        if (disableDeclarations)
            return;

        // this is a convenient hook for end-of-dtd checks, but we
        // could also trigger it in the first startElement call.
        // locator info is more appropriate here though.

        // VC: Notation Declared (NDATA can refer to them before decls,
        //      as can NOTATION attribute enumerations and defaults)
        int length = nDeferred.size ();
        for (int i = 0; i < length; i++) {
            String notation = (String) nDeferred.elementAt (i);
            if (!notations.contains (notation)) {
                error ("A declaration referred to notation '" + notation
                        + "' which was never declared");
            }
        }
        nDeferred.removeAllElements ();

        // VC: Entity Name (attribute values can refer to them
        //      before they're declared); VC Attribute Default Legal
        length = uDeferred.size ();
        for (int i = 0; i < length; i++) {
            String entity = (String) uDeferred.elementAt (i);
            if (!unparsed.contains (entity)) {
                error ("An attribute default referred to entity '" + entity
                        + "' which was never declared");
            }
        }
        uDeferred.removeAllElements ();
        super.endDTD ();
    }


    // These are interned, so we can rely on "==" to find the type of
    // all attributes except enumerations ...
    // "(this|or|that|...)" and "NOTATION (this|or|that|...)"
    static final String types [] = {
        "CDATA",
        "ID", "IDREF", "IDREFS",
        "NMTOKEN", "NMTOKENS",
        "ENTITY", "ENTITIES"
    };


    /**
     * <b>DecllHandler</b> Records attribute declaration for later use
     * in validating document content, and checks validity constraints
     * that are applicable to attribute declarations.
     * Passed to the next consumer, unless this one was
     * preloaded with a particular DTD.
     */
    public void attributeDecl (
        String eName,
        String aName,
        String type,
        String mode,
        String value
    ) throws SAXException
    {
        if (disableDeclarations)
            return;

        ElementInfo     info = (ElementInfo) elements.get (eName);
        AttributeInfo   ainfo = new AttributeInfo ();
        boolean         checkOne = false;
        boolean         interned = false;

        // cheap interning of type names and #FIXED, #REQUIRED
        // for faster startElement (we can use "==")
        for (int i = 0; i < types.length; i++) {
            if (types [i].equals (type)) {
                type = types [i];
                interned = true;
                break;
            }
        }
        if ("#FIXED".equals (mode))
            mode = "#FIXED";
        else if ("#REQUIRED".equals (mode))
            mode = "#REQUIRED";

        ainfo.type = type;
        ainfo.mode = mode;
        ainfo.value = value;

        // we might not have seen the content model yet
        if (info == null) {
            info = new ElementInfo (eName);
            elements.put (eName, info);
        }
        if ("ID" == type) {
            checkOne = true;
            if (!("#REQUIRED" == mode || "#IMPLIED".equals (mode))) {
                // VC: ID Attribute Default
                error ("ID attribute '" + aName
                    + "' must be #IMPLIED or #REQUIRED");
            }

        } else if (!interned && type.startsWith ("NOTATION ")) {
            checkOne = true;

            // VC: Notation Attributes (notations must be declared)
            StringTokenizer     tokens = new StringTokenizer (
                type.substring (10, type.lastIndexOf (')')),
                "|");
            while (tokens.hasMoreTokens ()) {
                String  token = tokens.nextToken ();
                if (!notations.contains (token))
                    nDeferred.addElement (token);
            }
        }
        if (checkOne) {
            for (Enumeration e = info.attributes.keys ();
                    e.hasMoreElements ();
                    /* NOP */) {
                String          name;
                AttributeInfo   ainfo2;

                name = (String) e.nextElement ();
                ainfo2 = (AttributeInfo) info.attributes.get (name);
                if (type == ainfo2.type || !interned /* NOTATION */) {
                    // VC: One ID per Element Type
                    // VC: One Notation per Element TYpe
                    error ("Element '" + eName
                        + "' already has an attribute of type "
                        + (interned ? "NOTATION" : type)
                        + " ('" + name
                        + "') so '" + aName
                        + "' is a validity error");
                }
            }
        }

        // VC: Attribute Default Legal
        if (value != null) {

            if ("CDATA" == type) {
                // event source rejected '<'

            } else if ("NMTOKEN" == type) {
                // VC: Name Token (is a nmtoken)
                isNmtoken (value, "attribute default", aName);

            } else if ("NMTOKENS" == type) {
                // VC: Name Token (is a nmtoken; at least one value)
                StringTokenizer tokens = new StringTokenizer (value);
                if (!tokens.hasMoreTokens ())
                    error ("Default for attribute '" + aName
                        + "' must have at least one name token.");
                else do {
                    String token = tokens.nextToken ();
                    isNmtoken (token, "attribute default", aName);
                } while (tokens.hasMoreTokens ());

            } else if ("IDREF" == type || "ENTITY" == type) {
                // VC: Entity Name (is a name)
                // VC: IDREF (is a name) (is declared)
                isName (value, "attribute default", aName);
                if ("ENTITY" == type && !unparsed.contains (value))
                    uDeferred.addElement (value);

            } else if ("IDREFS" == type || "ENTITIES" == type) {
                // VC: Entity Name (is a name; at least one value)
                // VC: IDREF (is a name; at least one value)
                StringTokenizer names = new StringTokenizer (value);
                if (!names.hasMoreTokens ())
                    error ("Default for attribute '" + aName
                        + "' must have at least one name.");
                else do {
                    String name = names.nextToken ();
                    isName (name, "attribute default", aName);
                    if ("ENTITIES" == type && !unparsed.contains (name))
                        uDeferred.addElement (value);
                } while (names.hasMoreTokens ());

            } else if (type.charAt (0) == '(' /*)*/ ) {
                // VC: Enumeration (must match)
                checkEnumeration (value, type, aName);

            } else if (!interned && checkOne) { /* NOTATION */
                // VC: Notation attributes (must be names)
                isName (value, "attribute default", aName);

                // VC: Notation attributes (must be declared)
                if (!notations.contains (value))
                    nDeferred.addElement (value);

                // VC: Enumeration (must match)
                checkEnumeration (value, type, aName);

            } else if ("ID" != type)
                throw new RuntimeException ("illegal attribute type: " + type);
        }

        if (info.attributes.get (aName) == null)
            info.attributes.put (aName, ainfo);
        /*
        else
            warning ("Element '" + eName
                + "' already has an attribute named '" + aName + "'");
        */

        if ("xml:space".equals (aName)) {
            if (!("(default|preserve)".equals (type)
                    || "(preserve|default)".equals (type)
                        // these next two are arguable; XHTML's DTD doesn't
                        // deserve errors.  After all, it's not like any
                        // illegal _value_ could pass ...
                    || "(preserve)".equals (type)
                    || "(default)".equals (type)
                    ))
                error (
                    "xml:space attribute type must be like '(default|preserve)'"
                    + " not '" + type + "'"
                    );

        }
        super.attributeDecl (eName, aName, type, mode, value);
    }

    /**
     * <b>DecllHandler</b> Records the element declaration for later use
     * when checking document content, and checks validity constraints that
     * apply to element declarations.  Passed to the next consumer, unless
     * this one was preloaded with a particular DTD.
     */
    public void elementDecl (String name, String model)
    throws SAXException
    {
        if (disableDeclarations)
            return;

        ElementInfo     info = (ElementInfo) elements.get (name);

        // we might have seen an attribute decl already
        if (info == null) {
            info = new ElementInfo (name);
            elements.put (name, info);
        }
        if (info.model != null) {
            // NOTE:  not all parsers can report such duplicates.
            // VC: Unique Element Type Declaration
            error ("Element type '" + name
                + "' was already declared.");
        } else {
            info.model = model;

            // VC: No Duplicate Types (in mixed content models)
            if (model.charAt (1) == '#')        // (#PCDATA...
                info.getRecognizer (this);
        }
        super.elementDecl (name, model);
    }

    /**
     * <b>DecllHandler</b> passed to the next consumer, unless this
     * one was preloaded with a particular DTD
     */
    public void internalEntityDecl (String name, String value)
    throws SAXException
    {
        if (!disableDeclarations)
            super.internalEntityDecl (name, value);
    }

    /**
     * <b>DecllHandler</b> passed to the next consumer, unless this
     * one was preloaded with a particular DTD
     */
    public void externalEntityDecl (String name,
        String publicId, String systemId)
    throws SAXException
    {
        if (!disableDeclarations)
            super.externalEntityDecl (name, publicId, systemId);
    }


    /**
     * <b>DTDHandler</b> Records the notation name, for checking
     * NOTATIONS attribute values and declararations of unparsed
     * entities.  Passed to the next consumer, unless this one was
     * preloaded with a particular DTD.
     */
    public void notationDecl (String name, String publicId, String systemId)
    throws SAXException
    {
        if (disableDeclarations)
            return;

        notations.addElement (name);
        super.notationDecl (name, publicId, systemId);
    }

    /**
     * <b>DTDHandler</b> Records the entity name, for checking
     * ENTITY and ENTITIES attribute values; records the notation
     * name if it hasn't yet been declared.  Passed to the next consumer,
     * unless this one was preloaded with a particular DTD.
     */
    public void unparsedEntityDecl (
        String name,
        String publicId,
        String systemId,
        String notationName
    ) throws SAXException
    {
        if (disableDeclarations)
            return;

        unparsed.addElement (name);
        if (!notations.contains (notationName))
            nDeferred.addElement (notationName);
        super.unparsedEntityDecl (name, publicId, systemId, notationName);
    }


    /**
     * <b>ContentHandler</b> Ensures that state from any previous parse
     * has been deleted.
     * Passed to the next consumer.
     */
    public void startDocument ()
    throws SAXException
    {
        resetState ();
        super.startDocument ();
    }


    private static boolean isAsciiLetter (char c)
    {
        return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
    }


    /**
     * <b>ContentHandler</b> Reports a fatal exception.  Validating
     * XML processors may not skip any entities.
     */
    public void skippedEntity (String name)
    throws SAXException
    {
        fatalError ("may not skip entities");
    }

    /*
     * SAX2 doesn't expand non-PE refs in attribute defaults...
     */
    private String expandDefaultRefs (String s)
    throws SAXException
    {
        if (s.indexOf ('&') < 0)
            return s;

// FIXME: handle &#nn; &#xnn; &name;
        String message = "Can't expand refs in attribute default: " + s;
        warning (message);

        return s;
    }

    /**
     * <b>ContentHandler</b> Performs validity checks against element
     * (and document) content models, and attribute values.
     * Passed to the next consumer.
     */
    public void startElement (
        String          uri,
        String          localName,
        String          qName,
        Attributes      atts
    ) throws SAXException
    {
        //
        // First check content model for the enclosing scope.
        //
        if (contentStack.isEmpty ()) {
            // VC:  Root Element Type
            if (!qName.equals (rootName)) {
                if (rootName == null)
                    warning ("This document has no DTD, can't be valid");
                else
                    error ("Root element type '" + qName
                        + "' was declared to be '" + rootName + "'");
            }
        } else {
            Recognizer state = (Recognizer) contentStack.peek ();

            if (state != null) {
                Recognizer newstate = state.acceptElement (qName);

                if (newstate == null)
                    error ("Element type '" + qName
                        + "' in element '" + state.type.name
                        + "' violates content model " + state.type.model
                        );
                if (newstate != state) {
                    contentStack.pop ();
                    contentStack.push (newstate);
                }
            }
        }

        //
        // Then check that this element was declared, and push the
        // object used to validate its content model onto our stack.
        //
        // This is where the recognizer gets created, if needed; if
        // it's a "children" (elements) content model, an NDFA is
        // created.  (One recognizer is used per content type, no
        // matter how complex that recognizer is.)
        //
        ElementInfo             info;

        info = (ElementInfo) elements.get (qName);
        if (info == null || info.model == null) {
            // VC: Element Valid (base clause)
            error ("Element type '" + qName + "' was not declared");
            contentStack.push (null);

            // for less diagnostic noise, fake a declaration.
            elementDecl (qName, "ANY");
        } else
            contentStack.push (info.getRecognizer (this));

        //
        // Then check each attribute present
        //
        int                     len;
        String                  aname;
        AttributeInfo           ainfo;

        if (atts != null)
            len = atts.getLength ();
        else
            len = 0;

        for (int i = 0; i < len; i++) {
            aname = atts.getQName (i);

            if (info == null
                    || (ainfo = (AttributeInfo) info.attributes.get (aname))
                            == null) {
                // VC: Attribute Value Type
                error ("Attribute '" + aname
                    + "' was not declared for element type " + qName);
                continue;
            }

            String value = atts.getValue (i);

            // note that "==" for type names and "#FIXED" is correct
            // (and fast) since we've interned those literals.

            if ("#FIXED" == ainfo.mode) {
                String expanded = expandDefaultRefs (ainfo.value);

                // VC: Fixed Attribute Default
                if (!value.equals (expanded)) {
                    error ("Attribute '" + aname
                        + "' must match " + expanded
                        );
                    continue;
                }
            }

            if ("CDATA" == ainfo.type)
                continue;

            //
            // For all other attribute types, there are various
            // rules to follow.
            //

            if ("ID" == ainfo.type) {
                // VC: ID (must be a name)
                if (isName (value, "ID attribute", aname)) {
                    if (Boolean.TRUE == ids.get (value))
                        // VC: ID (appears once)
                        error ("ID attribute " + aname
                            + " uses an ID value '" + value
                            + "' which was already declared.");
                    else
                        // any forward refs are no longer problems
                        ids.put (value, Boolean.TRUE);
                }
                continue;
            }

            if ("IDREF" == ainfo.type) {
                // VC: IDREF (value must be a name)
                if (isName (value, "IDREF attribute", aname)) {
                    // VC: IDREF (must match some ID attribute)
                    if (ids.get (value) == null)
                        // new -- assume it's a forward ref
                        ids.put (value, Boolean.FALSE);
                }
                continue;
            }

            if ("IDREFS" == ainfo.type) {
                StringTokenizer tokens = new StringTokenizer (value, " ");

                if (!tokens.hasMoreTokens ()) {
                    // VC: IDREF (one or more values)
                    error ("IDREFS attribute " + aname
                        + " must have at least one ID ref");
                } else do {
                    String id = tokens.nextToken ();

                    // VC: IDREF (value must be a name)
                    if (isName (id, "IDREFS attribute", aname)) {
                        // VC: IDREF (must match some ID attribute)
                        if (ids.get (id) == null)
                            // new -- assume it's a forward ref
                            ids.put (id, Boolean.FALSE);
                    }
                } while (tokens.hasMoreTokens ());
                continue;
            }

            if ("NMTOKEN" == ainfo.type) {
                // VC: Name Token (is a name token)
                isNmtoken (value, "NMTOKEN attribute", aname);
                continue;
            }

            if ("NMTOKENS" == ainfo.type) {
                StringTokenizer tokens = new StringTokenizer (value, " ");

                if (!tokens.hasMoreTokens ()) {
                    // VC: Name Token (one or more values)
                    error ("NMTOKENS attribute " + aname
                        + " must have at least one name token");
                } else do {
                    String token = tokens.nextToken ();

                    // VC: Name Token (is a name token)
                    isNmtoken (token, "NMTOKENS attribute", aname);
                } while (tokens.hasMoreTokens ());
                continue;
            }

            if ("ENTITY" == ainfo.type) {
                if (!unparsed.contains (value))
                    // VC: Entity Name
                    error ("Value of attribute '" + aname
                        + "' refers to unparsed entity '" + value
                        + "' which was not declared.");
                continue;
            }

            if ("ENTITIES" == ainfo.type) {
                StringTokenizer tokens = new StringTokenizer (value, " ");

                if (!tokens.hasMoreTokens ()) {
                    // VC: Entity Name (one or more values)
                    error ("ENTITIES attribute " + aname
                        + " must have at least one name token");
                } else do {
                    String entity = tokens.nextToken ();

                    if (!unparsed.contains (entity))
                        // VC: Entity Name
                        error ("Value of attribute '" + aname
                            + "' refers to unparsed entity '" + entity
                            + "' which was not declared.");
                } while (tokens.hasMoreTokens ());
                continue;
            }

            //
            // check for enumerations last; more expensive
            //
            if (ainfo.type.charAt (0) == '(' /*)*/
                    || ainfo.type.startsWith ("NOTATION ")
                    ) {
                // VC: Enumeration (value must be defined)
                checkEnumeration (value, ainfo.type, aname);
                continue;
            }
        }

        //
        // Last, check that all #REQUIRED attributes were provided
        //
        if (info != null) {
            Hashtable   table = info.attributes;

            if (table.size () != 0) {
                Enumeration     e = table.keys ();

                // XXX table.keys uses the heap, bleech -- slows things

                while (e.hasMoreElements ()) {
                    aname = (String) e.nextElement ();
                    ainfo = (AttributeInfo) table.get (aname);

                    // "#REQUIRED" mode was interned in attributeDecl
                    if ("#REQUIRED" == ainfo.mode
                            && atts.getValue (aname) == null) {
                        // VC: Required Attribute
                        error ("Attribute '" + aname + "' must be specified "
                            + "for element type " + qName);
                    }
                }
            }
        }
        super.startElement (uri, localName, qName, atts);
    }

    /**
     * <b>ContentHandler</b> Reports a validity error if the element's content
     * model does not permit character data.
     * Passed to the next consumer.
     */
    public void characters (char ch [], int start, int length)
    throws SAXException
    {
        Recognizer state;

        if (contentStack.empty ())
            state = null;
        else
            state = (Recognizer) contentStack.peek ();

        // NOTE:  if this ever supports with SAX parsers that don't
        // report ignorable whitespace as such (only XP?), this class
        // needs to morph it into ignorableWhitespace() as needed ...

        if (state != null && !state.acceptCharacters ())
            // VC: Element Valid (clauses three, four -- see recognizer)
            error ("Character content not allowed in element "
                + state.type.name);

        super.characters (ch, start, length);
    }


    /**
     * <b>ContentHandler</b> Reports a validity error if the element's content
     * model does not permit end-of-element yet, or a well formedness error
     * if there was no matching startElement call.
     * Passed to the next consumer.
     */
    public void endElement (String uri, String localName, String qName)
    throws SAXException
    {
        try {
            Recognizer state = (Recognizer) contentStack.pop ();

            if (state != null && !state.completed ())
                // VC: Element valid (clauses two, three, four; see Recognizer)
                error ("Premature end for element '"
                    + state.type.name
                    + "', content model "
                    + state.type.model);

            // could insist on match of start element, but that's
            // something the input stream must to guarantee.

        } catch (EmptyStackException e) {
            fatalError ("endElement without startElement: " + qName
                + ((uri == null)
                    ? ""
                    : ( " { '" + uri + "', " + localName + " }")));
        }
        super.endElement (uri, localName, qName);
    }

    /**
     * <b>ContentHandler</b> Checks whether all ID values that were
     * referenced have been declared, and releases all resources.
     * Passed to the next consumer.
     *
     * @see #setDocumentLocator
     */
    public void endDocument ()
    throws SAXException
    {
        for (Enumeration idNames = ids.keys ();
                idNames.hasMoreElements ();
                /* NOP */) {
            String id = (String) idNames.nextElement ();

            if (Boolean.FALSE == ids.get (id)) {
                // VC: IDREF (must match ID)
                error ("Undeclared ID value '" + id
                    + "' was referred to by an IDREF/IDREFS attribute");
            }
        }

        resetState ();
        super.endDocument ();
    }


    /** Holds per-element declarations */
    static private final class ElementInfo
    {
        String                  name;
        String                  model;

        // key = attribute name; value = AttributeInfo
        Hashtable               attributes = new Hashtable (11);

        ElementInfo (String n) { name = n; }

        private Recognizer      recognizer;

        // for validating content models:  one per type, shared,
        // and constructed only on demand ... so unused elements do
        // not need to consume resources.
        Recognizer      getRecognizer (ValidationConsumer consumer)
        throws SAXException
        {
            if (recognizer == null) {
                if ("ANY".equals (model))
                    recognizer = ANY;
                else if ("EMPTY".equals (model))
                    recognizer = new EmptyRecognizer (this);
                else if ('#' == model.charAt (1))
                    // n.b. this constructor does a validity check
                    recognizer = new MixedRecognizer (this, consumer);
                else
                    recognizer = new ChildrenRecognizer (this, consumer);
            }
            return recognizer;
        }
    }

    /** Holds per-attribute declarations */
    static private final class AttributeInfo
    {
        String  type;
        String  mode;           // #REQUIRED, etc (or null)
        String  value;          // or null
    }


    //
    // Content model validation
    //

    static private final Recognizer     ANY = new Recognizer (null);


    // Base class defines the calls used to validate content,
    // and supports the "ANY" content model
    static private class Recognizer
    {
        final ElementInfo       type;

        Recognizer (ElementInfo t) { type = t; }

        // return true iff character data is legal here
        boolean acceptCharacters ()
        throws SAXException
            // VC: Element Valid (third and fourth clauses)
            { return true; }

        // null return = failure
        // otherwise, next state (like an FSM)
        // prerequisite: tested that name was declared
        Recognizer acceptElement (String name)
        throws SAXException
            // VC: Element Valid (fourth clause)
            { return this; }

        // return true iff model is completed, can finish
        boolean completed ()
        throws SAXException
            // VC: Element Valid (fourth clause)
            { return true; }

        public String toString ()
            // n.b. "children" is the interesting case!
            { return (type == null) ? "ANY" : type.model; }
    }

    // "EMPTY" content model -- no characters or elements
    private static final class EmptyRecognizer extends Recognizer
    {
        public EmptyRecognizer (ElementInfo type)
            { super (type); }

        // VC: Element Valid (first clause)
        boolean acceptCharacters ()
            { return false; }

        // VC: Element Valid (first clause)
        Recognizer acceptElement (String name)
            { return null; }
    }

    // "Mixed" content model -- ANY, but restricts elements
    private static final class MixedRecognizer extends Recognizer
    {
        private String  permitted [];

        // N.B. constructor tests for duplicated element names (VC)
        public MixedRecognizer (ElementInfo t, ValidationConsumer v)
        throws SAXException
        {
            super (t);

            // (#PCDATA...)* or (#PCDATA) ==> ... or empty
            // with the "..." being "|elname|..."
            StringTokenizer     tokens = new StringTokenizer (
                t.model.substring (8, t.model.lastIndexOf (')')),
                "|");
            Vector              vec = new Vector ();

            while (tokens.hasMoreTokens ()) {
                String token = tokens.nextToken ();

                if (vec.contains (token))
                    v.error ("element " + token
                        + " is repeated in mixed content model: "
                        + t.model);
                else
                    vec.addElement (token.intern ());
            }
            permitted = new String [vec.size ()];
            for (int i = 0; i < permitted.length; i++)
                permitted [i] = (String) vec.elementAt (i);

            // in one large machine-derived DTD sample, most of about
            // 250 mixed content models were empty, and 25 had ten or
            // more entries.  2 had over a hundred elements.  Linear
            // search isn't obviously wrong.
        }

        // VC: Element Valid (third clause)
        Recognizer acceptElement (String name)
        {
            int         length = permitted.length;

            // first pass -- optimistic w.r.t. event source interning
            // (and document validity)
            for (int i = 0; i < length; i++)
                if (permitted [i] == name)
                    return this;
            // second pass -- pessimistic w.r.t. event source interning
            for (int i = 0; i < length; i++)
                if (permitted [i].equals (name))
                    return this;
            return null;
        }
    }


    // recognizer loop flags, see later
    private static final int            F_LOOPHEAD = 0x01;
    private static final int            F_LOOPNEXT = 0x02;

    // for debugging -- used to label/count nodes in toString()
    private static int                  nodeCount;

    /**
     * "Children" content model -- these are nodes in NDFA state graphs.
     * They work in fixed space.  Note that these graphs commonly have
     * cycles, handling features such as zero-or-more and one-or-more.
     *
     * <p>It's readonly, so only one copy is ever needed.  The content model
     * stack may have any number of pointers into each graph, when a model
     * happens to be needed more than once due to element nesting.  Since
     * traversing the graph just moves to another node, and never changes
     * it, traversals never interfere with each other.
     *
     * <p>There is an option to report non-deterministic models.  These are
     * always XML errors, but ones which are not often reported despite the
     * fact that they can lead to different validating parsers giving
     * different results for the same input.  (The XML spec doesn't require
     * them to be reported.)
     *
     * <p><b>FIXME</b> There's currently at least one known bug here, in that
     * it's not actually detecting the non-determinism it tries to detect.
     * (Of the "optional.xml" test, the once-or-twice-2* tests are all non-D;
     * maybe some others.)  This may relate to the issue flagged below as
     * "should not" happen (but it was), which showed up when patching the
     * graph to have one exit node (or more EMPTY nodes).
     */
    private static final class ChildrenRecognizer extends Recognizer
        implements Cloneable
    {
        // for reporting non-deterministic content models
        // ... a waste of space if we're not reporting those!
        // ... along with the 'model' member (in base class)
        private ValidationConsumer      consumer;

        // for CHOICE nodes -- each component is an arc that
        // accepts a different NAME (or is EMPTY indicating
        // NDFA termination).
        private Recognizer              components [];

        // for NAME/SEQUENCE nodes -- accepts that NAME and
        // then goes to the next node (CHOICE, NAME, EMPTY).
        private String                  name;
        private Recognizer              next;

        // loops always point back to a CHOICE node. we mark such choice
        // nodes (F_LOOPHEAD) for diagnostics and faster deep cloning.
        // We also mark nodes before back pointers (F_LOOPNEXT), to ensure
        // termination when we patch sequences and loops.
        private int                     flags;


        // prevent a needless indirection between 'this' and 'node'
        private void copyIn (ChildrenRecognizer node)
        {
            // model & consumer are already set
            components = node.components;
            name = node.name;
            next = node.next;
            flags = node.flags;
        }

        // used to construct top level "children" content models,
        public ChildrenRecognizer (ElementInfo type, ValidationConsumer vc)
        {
            this (vc, type);
            populate (type.model.toCharArray (), 0);
            patchNext (new EmptyRecognizer (type), null);
        }

        // used internally; populating is separate
        private ChildrenRecognizer (ValidationConsumer vc, ElementInfo type)
        {
            super (type);
            consumer = vc;
        }


        //
        // When rewriting some graph nodes we need deep clones in one case;
        // mostly shallow clones (what the JVM handles for us) are fine.
        //
        private ChildrenRecognizer shallowClone ()
        {
            try {
                return (ChildrenRecognizer) clone ();
            } catch (CloneNotSupportedException e) {
                throw new Error ("clone");
            }
        }

        private ChildrenRecognizer deepClone ()
        {
            return deepClone (new Hashtable (37));
        }

        private ChildrenRecognizer deepClone (Hashtable table)
        {
            ChildrenRecognizer retval;

            if ((flags & F_LOOPHEAD) != 0) {
                retval = (ChildrenRecognizer) table.get (this);
                if (retval != null)
                    return this;

                retval = shallowClone ();
                table.put (this, retval);
            } else
                retval = shallowClone ();

            if (next != null) {
                if (next instanceof ChildrenRecognizer)
                    retval.next = ((ChildrenRecognizer)next)
                            .deepClone (table);
                else if (!(next instanceof EmptyRecognizer))
                    throw new RuntimeException ("deepClone");
            }

            if (components != null) {
                retval.components = new Recognizer [components.length];
                for (int i = 0; i < components.length; i++) {
                    Recognizer temp = components [i];

                    if (temp == null)
                        retval.components [i] = null;
                    else if (temp instanceof ChildrenRecognizer)
                        retval.components [i] = ((ChildrenRecognizer)temp)
                                .deepClone (table);
                    else if (!(temp instanceof EmptyRecognizer))
                        throw new RuntimeException ("deepClone");
                }
            }

            return retval;
        }

        // connect subgraphs, first to next (sequencing)
        private void patchNext (Recognizer theNext, Hashtable table)
        {
            // backpointers must not be repatched or followed
            if ((flags & F_LOOPNEXT) != 0)
                return;

            // XXX this table "shouldn't" be needed, right?
            // but some choice nodes looped if it isn't there.
            if (table != null && table.get (this) != null)
                return;
            if (table == null)
                table = new Hashtable ();

            // NAME/SEQUENCE
            if (name != null) {
                if (next == null)
                    next = theNext;
                else if (next instanceof ChildrenRecognizer) {
                    ((ChildrenRecognizer)next).patchNext (theNext, table);
                } else if (!(next instanceof EmptyRecognizer))
                    throw new RuntimeException ("patchNext");
                return;
            }

            // CHOICE
            for (int i = 0; i < components.length; i++) {
                if (components [i] == null)
                    components [i] = theNext;
                else if (components [i] instanceof ChildrenRecognizer) {
                    ((ChildrenRecognizer)components [i])
                            .patchNext (theNext, table);
                } else if (!(components [i] instanceof EmptyRecognizer))
                    throw new RuntimeException ("patchNext");
            }

            if (table != null && (flags & F_LOOPHEAD) != 0)
                table.put (this, this);
        }

        /**
         * Parses a 'children' spec (or recursively 'cp') and makes this
         * become a regular graph node.
         *
         * @return index after this particle
         */
        private int populate (char parseBuf [], int startPos)
        {
            int         nextPos = startPos + 1;
            char        c;

            if (nextPos < 0 || nextPos >= parseBuf.length)
                throw new IndexOutOfBoundsException ();

            // Grammar of the string is from the XML spec, but
            // with whitespace removed by the SAX parser.

            // children ::= (choice | seq) ('?' | '*' | '+')?
            // cp ::= (Name | choice | seq) ('?' | '*' | '+')?
            // choice ::= '(' cp ('|' choice)* ')'
            // seq ::= '(' cp (',' choice)* ')'

            // interior nodes only
            //   cp ::= name ...
            if (parseBuf [startPos] != '('/*)*/) {
                boolean         done = false;
                do {
                    switch (c = parseBuf [nextPos]) {
                        case '?': case '*': case '+':
                        case '|': case ',':
                        case /*(*/ ')':
                            done = true;
                            continue;
                        default:
                            nextPos++;
                            continue;
                    }
                } while (!done);
                name = new String (parseBuf, startPos, nextPos - startPos);

            // interior OR toplevel nodes
            //   cp ::= choice ..
            //   cp ::= seq ..
            } else {
                // collect everything as a separate list, and merge it
                // into "this" later if we can (SEQUENCE or singleton)
                ChildrenRecognizer      first;

                first = new ChildrenRecognizer (consumer, type);
                nextPos = first.populate (parseBuf, nextPos);
                c = parseBuf [nextPos++];

                if (c == ',' || c == '|') {
                    ChildrenRecognizer  current = first;
                    char                separator = c;
                    Vector              v = null;

                    if (separator == '|') {
                        v = new Vector ();
                        v.addElement (first);
                    }

                    do {
                        ChildrenRecognizer link;

                        link = new ChildrenRecognizer (consumer, type);
                        nextPos = link.populate (parseBuf, nextPos);

                        if (separator == ',') {
                            current.patchNext (link, null);
                            current = link;
                        } else
                            v.addElement (link);

                        c = parseBuf [nextPos++];
                    } while (c == separator);

                    // choice ... collect everything into one array.
                    if (separator == '|') {
                        // assert v.size() > 1
                        components = new Recognizer [v.size ()];
                        for (int i = 0; i < components.length; i++) {
                            components [i] = (Recognizer)
                                    v.elementAt (i);
                        }
                        // assert flags == 0

                    // sequence ... merge into "this" to be smaller.
                    } else
                        copyIn (first);

                // treat singletons like one-node sequences.
                } else
                    copyIn (first);

                if (c != /*(*/ ')')
                    throw new RuntimeException ("corrupt content model");
            }

            //
            // Arity is optional, and the root of all fun.  We keep the
            // FSM state graph simple by only having NAME/SEQUENCE and
            // CHOICE nodes (or EMPTY to terminate a model), easily
            // evaluated.  So we rewrite each node that has arity, using
            // those primitives.  We create loops here, if needed.
            //
            if (nextPos < parseBuf.length) {
                c = parseBuf [nextPos];
                if (c == '?' || c == '*' || c == '+') {
                    nextPos++;

                    // Rewrite 'zero-or-one' "?" arity to a CHOICE:
                    //   - SEQUENCE (clone, what's next)
                    //   - or, what's next
                    // Size cost: N --> N + 1
                    if (c == '?') {
                        Recognizer              once = shallowClone ();

                        components = new Recognizer [2];
                        components [0] = once;
                        // components [1] initted to null
                        name = null;
                        next = null;
                        flags = 0;


                    // Rewrite 'zero-or-more' "*" arity to a CHOICE.
                    //   - LOOP (clone, back to this CHOICE)
                    //   - or, what's next
                    // Size cost: N --> N + 1
                    } else if (c == '*') {
                        ChildrenRecognizer      loop = shallowClone ();

                        loop.patchNext (this, null);
                        loop.flags |= F_LOOPNEXT;
                        flags = F_LOOPHEAD;

                        components = new Recognizer [2];
                        components [0] = loop;
                        // components [1] initted to null
                        name = null;
                        next = null;


                    // Rewrite 'one-or-more' "+" arity to a SEQUENCE.
                    // Basically (a)+ --> ((a),(a)*).
                    //   - this
                    //   - CHOICE
                    //      * LOOP (clone, back to the CHOICE)
                    //      * or, whatever's next
                    // Size cost: N --> 2N + 1
                    } else if (c == '+') {
                        ChildrenRecognizer loop = deepClone ();
                        ChildrenRecognizer choice;

                        choice = new ChildrenRecognizer (consumer, type);
                        loop.patchNext (choice, null);
                        loop.flags |= F_LOOPNEXT;
                        choice.flags = F_LOOPHEAD;

                        choice.components = new Recognizer [2];
                        choice.components [0] = loop;
                        // choice.components [1] initted to null
                        // choice.name, choice.next initted to null

                        patchNext (choice, null);
                    }
                }
            }

            return nextPos;
        }

        // VC: Element Valid (second clause)
        boolean acceptCharacters ()
            { return false; }

        // VC: Element Valid (second clause)
        Recognizer acceptElement (String type)
        throws SAXException
        {
            // NAME/SEQUENCE
            if (name != null) {
                if (name.equals (type))
                    return next;
                return null;
            }

            // CHOICE ... optionally reporting nondeterminism we
            // run across.  we won't check out every transition
            // for nondeterminism; only the ones we follow.
            Recognizer  retval = null;

            for (int i = 0; i < components.length; i++) {
                Recognizer temp = components [i].acceptElement (type);

                if (temp == null)
                    continue;
                else if (!warnNonDeterministic)
                    return temp;
                else if (retval == null)
                    retval = temp;
                else if (retval != temp)
                    consumer.error ("Content model " + this.type.model
                        + " is non-deterministic for " + type);
            }
            return retval;
        }

        // VC: Element Valid (second clause)
        boolean completed ()
        throws SAXException
        {
            // expecting a specific element
            if (name != null)
                return false;

            // choice, some sequences
            for (int i = 0; i < components.length; i++) {
                if (components [i].completed ())
                    return true;
            }

            return false;
        }

/** /
        // FOR DEBUGGING ... flattens the graph for printing.

        public String toString ()
        {
            StringBuffer buf = new StringBuffer ();

            // only one set of loop labels can be generated
            // at a time...
            synchronized (ANY) {
                nodeCount = 0;

                toString (buf, new Hashtable ());
                return buf.toString ();
            }
        }

        private void toString (StringBuffer buf, Hashtable table)
        {
            // When we visit a node, label and count it.
            // Nodes are never visited/counted more than once.
            // For small models labels waste space, but if arity
            // mappings were used the savings are substantial.
            // (Plus, the output can be more readily understood.)
            String temp = (String) table.get (this);

            if (temp != null) {
                buf.append ('{');
                buf.append (temp);
                buf.append ('}');
                return;
            } else {
                StringBuffer scratch = new StringBuffer (15);

                if ((flags & F_LOOPHEAD) != 0)
                    scratch.append ("loop");
                else
                    scratch.append ("node");
                scratch.append ('-');
                scratch.append (++nodeCount);
                temp = scratch.toString ();

                table.put (this, temp);
                buf.append ('[');
                buf.append (temp);
                buf.append (']');
                buf.append (':');
            }

            // NAME/SEQUENCE
            if (name != null) {
                // n.b. some output encodings turn some name chars into '?'
                // e.g. with Japanese names and ASCII output
                buf.append (name);
                if (components != null)         // bug!
                    buf.append ('$');
                if (next == null)
                    buf.append (",*");
                else if (next instanceof EmptyRecognizer) // patch-to-next
                    buf.append (",{}");
                else if (next instanceof ChildrenRecognizer) {
                    buf.append (',');
                    ((ChildrenRecognizer)next).toString (buf, table);
                } else                          // bug!
                    buf.append (",+");
                return;
            }

            // CHOICE
            buf.append ("<");
            for (int i = 0; i < components.length; i++) {
                if (i != 0)
                    buf.append ("|");
                if (components [i] instanceof EmptyRecognizer) {
                    buf.append ("{}");
                } else if (components [i] == null) {    // patch-to-next
                    buf.append ('*');
                } else {
                    ChildrenRecognizer r;

                    r = (ChildrenRecognizer) components [i];
                    r.toString (buf, table);
                }
            }
            buf.append (">");
        }
/**/
    }
}
