/*
 * $Id: Parser2.java,v 1.16 2001/09/29 04:17:47 edwingo Exp $
 *
 * The Apache Software License, Version 1.1
 *
 *
 * Copyright (c) 2000 The Apache Software Foundation.  All rights 
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer. 
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:  
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. The names "Crimson" and "Apache Software Foundation" must
 *    not be used to endorse or promote products derived from this
 *    software without prior written permission. For written 
 *    permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache",
 *    nor may "Apache" appear in their name, without prior written
 *    permission of the Apache Software Foundation.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation and was
 * originally based on software copyright (c) 1999, Sun Microsystems, Inc., 
 * http://www.sun.com.  For more information on the Apache Software 
 * Foundation, please see <http://www.apache.org/>.
 */


package org.apache.crimson.parser;

import java.io.IOException;
import java.io.Reader;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Locale;
import java.util.Vector;

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

import org.apache.crimson.util.MessageCatalog;
import org.apache.crimson.util.XmlChars;
import org.apache.crimson.util.XmlNames;


//
// NOTE:  when maintaining this code, take care to keep the message
// catalogue(s) up to date!!  It's important that the diagnostics
// be informative.
//


/**
 * This implements a fast non-validating SAX2 parser.  This one always
 * processes external parsed entities, strictly adheres to the XML 1.0
 * specification, and provides useful diagnostics.  It supports an
 * optimization allowing faster processing of valid standalone XML
 * documents.  For multi-language applications (such as web servers using
 * XML processing to create dynamic content), a method supports choosing a
 * locale for parser diagnostics which is both understood by the message
 * recipient and supported by the parser.
 *
 * <P> This conforms to the XML 1.0 specification.  To configure an XML
 * processor which tests document conformance against XML Namespaces,
 * provide a <em>DtdEventListener</em> which examines declarations of
 * entities and notations, and have your document listener check other
 * constraints such as ensuring <em>xmlns*</em> attribute values properly
 * declare all namespace prefixes.  (Only element and attribute names may
 * contain colons, and even then the name prefix before the colon must be
 * properly declared.)
 *
 * <P> SAX parsers produce a stream of parse events, which applications
 * process to create an object model which is specific to their tasks.
 * Applications which do not want to process event streams in that way
 * should use an API producing a standardized object model, such as the
 * W3C's <em>Document Object Model</em> (DOM).  This parser supports
 * building fully conformant DOM <em>Document</em> objects, through
 * use of DtdEventListener extensions to SAX in conjunction with an
 * appropriate implementation of a SAX <em>DocumentHandler</em>.  In
 * addition, it supports some features (exposing comments, CDATA sections,
 * and entity references) which are allowed by DOM but not required to
 * be reported by conformant XML processors.  (As usual, the default
 * handler for parsing events other than fatal errors ignores them.)
 *
 * @see ValidatingParser
 *
 * @author David Brownell
 * @author Rajiv Mordani
 * @author Edwin Goei
 * @version $Revision: 1.16 $
 */
public class Parser2
{
    // stack of input entities being merged
    private InputEntity         in;

    // temporaries reused during parsing
    private AttributesExImpl    attTmp;
    private StringBuffer        strTmp;
    private char                nameTmp [];
    private NameCache           nameCache;
    private char                charTmp [] = new char [2];
    private String[]            namePartsTmp = new String[3];

    // temporaries local to namespace attribute processing in elements
    private boolean             seenNSDecl;
    private NamespaceSupport    nsSupport;
    /**
     * nsAttTmp holds a list of namespace attributes used to check for
     * #REQUIRED when validating and (namespaces == true && prefixes ==
     * false)
     */
    private Vector              nsAttTmp;

    // NOTE:  odd heap behavior, at least with classic VM: if "strTmp" is
    // reused, LOTS of extra memory is consumed in some simple situations.
    // JVM bug filed; it's no longer a win to reuse it as much, in any case. 

    // parsing modes
    private boolean             isValidating = false;
    private boolean             fastStandalone = false;
    private boolean             isInAttribute = false;
    private boolean             namespaces;             // new in SAX2
    private boolean             prefixes;               // new in SAX2

    // temporary DTD parsing state
    private boolean             inExternalPE;
    private boolean             doLexicalPE;
    private boolean             donePrologue;

    // info about the document
    private boolean             isStandalone;
    private String              rootElementName;

    // DTD state, used during parsing
    private boolean             ignoreDeclarations;
    private SimpleHashtable     elements = new SimpleHashtable (47);
    private SimpleHashtable     params = new SimpleHashtable (7);

    // exposed to package-private subclass
    Hashtable                   notations = new Hashtable (7);
    SimpleHashtable             entities = new SimpleHashtable (17);

    // stuff associated with SAX
    private ContentHandler      contentHandler;
    private DTDHandler          dtdHandler;
    private EntityResolver      resolver;
    private ErrorHandler        errHandler;
    private Locale              locale;
    private Locator             locator;

    // SAX2 extension API support
    private DeclHandler         declHandler;
    private LexicalHandler      lexicalHandler;


    // Compile time option:  disable validation support for a better
    // fit in memory-critical environments (P-Java etc).  Doing that
    // and removing the validating parser support saves (at this time)
    // about 15% in size.

    private static final boolean        supportValidation = true;


    // string constants -- use these copies so "==" works
    // package private
    static final String         strANY = "ANY";
    static final String         strEMPTY = "EMPTY";


    ////////////////////////////////////////////////////////////////
    //
    // PARSER methods
    //
    ////////////////////////////////////////////////////////////////

    /**
     * Construct a SAX2 parser object
     */
    public Parser2 ()
    {
        locator = new DocLocator ();
        setHandlers ();
    }

    /**
     * Set up the namespace related features for this parser.  SAX2 specifies
     * these are read-only during a parse, read-write otherwise.
     */
    void setNamespaceFeatures(boolean namespaces, boolean prefixes) {
        this.namespaces = namespaces;
        this.prefixes = prefixes;
    }

    void setEntityResolver(EntityResolver resolver) {
        this.resolver = resolver;
    }

    public void setDTDHandler(DTDHandler handler) {
        dtdHandler = handler;
    }

    void setContentHandler(ContentHandler handler) {
        contentHandler = handler;
    }

    void setErrorHandler (ErrorHandler handler) {
        errHandler = handler;
    }

    void setLexicalHandler (LexicalHandler handler) {
        lexicalHandler = handler;
    }

    void setDeclHandler (DeclHandler handler) {
        declHandler = handler;
    }


    // XXX Maybe we can remove some of these old locale methods
    /**
     * <b>SAX:</b> Used by applications to request locale for diagnostics.
     *
     * @param l The locale to use, or null to use system defaults
     *  (which may include only message IDs).
     * @throws SAXException If no diagnostic messages are available
     *  in that locale.
     */
    public void setLocale (Locale l)
    throws SAXException
    {
        if (l != null && !messages.isLocaleSupported (l.toString ()))
            throw new SAXException (messages.getMessage (locale,
                    "P-078", new Object [] { l }));
        locale = l;
    }

    /** Returns the diagnostic locale. */
    public Locale getLocale ()
        { return locale; }
    
    /**
     * Chooses a client locale to use for diagnostics, using the first
     * language specified in the list that is supported by this parser.
     * That locale is then set using <a href="#setLocale(java.util.Locale)">
     * setLocale()</a>.  Such a list could be provided by a variety of user
     * preference mechanisms, including the HTTP <em>Accept-Language</em>
     * header field.
     *
     * @see org.apache.crimson.util.MessageCatalog
     *
     * @param languages Array of language specifiers, ordered with the most
     *  preferable one at the front.  For example, "en-ca" then "fr-ca",
     *  followed by "zh_CN".  Both RFC 1766 and Java styles are supported.
     * @return The chosen locale, or null.
     */
    public Locale chooseLocale (String languages [])
    throws SAXException
    {
        Locale  l = messages.chooseLocale (languages);

        if (l != null)
            setLocale (l);
        return l;
    }


    /** <b>SAX:</b> Parse a document. */
    public void parse (InputSource in)
    throws SAXException, IOException
    {
        init ();
        parseInternal (in);
    }

    /**
     * Setting this flag enables faster processing of valid standalone
     * documents: external DTD information is not processed, and no
     * attribute normalization or defaulting is done.  This optimization
     * is only permitted in non-validating parsers; for validating
     * parsers, this mode is silently disabled.
     *
     * <P> For documents which are declared as standalone, but which are
     * not valid, a fatal error may be reported for references to externally
     * defined entities.  That could happen in any nonvalidating parser which
     * did not read externally defined entities.  Also, if any attribute
     * values need normalization or defaulting, it will not be done.
     */
    public void setFastStandalone (boolean value)
        { fastStandalone = value && !isValidating; }

    /**
     * Returns true if standalone documents skip processing of
     * all external DTD information.
     */
    public boolean isFastStandalone ()
        { return fastStandalone; }


    /**
     * In support of the HTML DOM model of client side
     * <em>&lt;xhtml:script&gt;</em> tag processing, this method permits
     * data to be spliced into the input stream.  This method would
     * normally be called from an <em>endElement</em> callback to put the
     * buffered result of calls such as DOM <em>HTMLDocument.write</em>
     * into the input stream.
     */
    public void pushInputBuffer (char buf [], int offset, int len)
    throws SAXException
    {
        if (len <= 0)
            return;

        // arraycopy is inelegant, but that's the worst penalty for now
        if (offset != 0 || len != buf.length) {
            char tmp [] = new char [len];
            System.arraycopy (buf, offset, tmp, 0, len);
            buf = tmp;
        }
        pushReader (buf, null, false);
    }


    // package private
    void setIsValidating (boolean value)
    {
        if (supportValidation)
            isValidating = value;
        else
            throw new RuntimeException (messages.getMessage (locale, "V-000"));
        if (value)
            fastStandalone = false;
    }


    // makes sure the parser's reset to "before a document"
    private void init ()
    {
        in = null;

        // alloc temporary data used in parsing
        attTmp = new AttributesExImpl ();
        strTmp = new StringBuffer ();
        nameTmp = new char [20];
        nameCache = new NameCache ();

        if (namespaces) {
            nsSupport = new NamespaceSupport();
            if (supportValidation && isValidating && !prefixes) {
                nsAttTmp = new Vector();
            }
        }

        // reset doc info
        isStandalone = false;
        rootElementName = null;
        isInAttribute = false;

        inExternalPE = false;
        doLexicalPE = false;
        donePrologue = false;

        entities.clear ();
        notations.clear ();
        params.clear ();
        elements.clear ();
        ignoreDeclarations = false;

        // initialize predefined references ... re-interpreted later
        builtin ("amp", "&#38;");
        builtin ("lt", "&#60;");
        builtin ("gt", ">");
        builtin ("quot", "\"");
        builtin ("apos", "'");

        if (locale == null)
            locale = Locale.getDefault ();
        if (resolver == null)
            resolver = new Resolver ();
        
        setHandlers ();
    }

    static private final NullHandler nullHandler = new NullHandler();

    private void setHandlers ()
    {
        if (contentHandler == null) {
            contentHandler = nullHandler;
        }
        if (errHandler == null) {
            errHandler = nullHandler;
        }
        if (dtdHandler == null) {
            dtdHandler = nullHandler;
        }
        if (lexicalHandler == null) {
            lexicalHandler = nullHandler;
        }
        if (declHandler == null) {
            declHandler = nullHandler;
        }
    }

    private void builtin (String entityName, String entityValue)
    {
        InternalEntity entity;
        entity = new InternalEntity (entityName, entityValue.toCharArray ());
        entities.put (entityName, entity);
    }



    ////////////////////////////////////////////////////////////////
    //
    // parsing is by recursive descent, code roughly
    // following the BNF rules except tweaked for simple
    // lookahead.  rules are more or less in numeric order,
    // except where code sharing suggests other structures.
    //
    // a classic benefit of recursive descent parsers:  it's
    // relatively easy to get diagnostics that make sense.
    //
    ////////////////////////////////////////////////////////////////


    //
    // CHAPTER 2:  Documents
    //

    private void parseInternal (InputSource input)
    throws SAXException, IOException
    {
        if (input == null)
            fatal ("P-000");

        try {
            in = InputEntity.getInputEntity (errHandler, locale);
            in.init (input, null, null, false);

            //
            // doc handler sees the locator, lots of PIs, DTD info
            // about external entities and notations, then the body.
            //Need to initialize this after InputEntity cos locator uses
            //InputEntity's systemid, publicid, line no. etc

            contentHandler.setDocumentLocator (locator);

            contentHandler.startDocument ();

            // [1] document ::= prolog element Misc*
            // [22] prolog ::= XMLDecl? Misc* (DoctypeDecl Misc *)?

            maybeXmlDecl ();
            maybeMisc (false);

            if (!maybeDoctypeDecl ()) {
                if (supportValidation && isValidating)
                    warning ("V-001", null);
            }
            
            maybeMisc (false);
            donePrologue = true;

            //
            // One root element ... then basically PIs before EOF.
            //
            if (!in.peekc ('<') || !maybeElement (null))
                fatal ("P-067");
            //Check subclass. Used for validation of id refs.
            afterRoot ();
            maybeMisc (true);
            if (!in.isEOF ())
                fatal ("P-001", new Object []
                        { Integer.toHexString (((int)getc ())) } );
            contentHandler.endDocument ();

        } catch (EndOfInputException e) {
            if (!in.isDocument ()) {
                String name = in.getName ();
                do {    // force a relevant URI and line number  
                    in = in.pop ();
                } while (in.isInternal ());
                fatal ("P-002", new Object []
                        { name },
                        e);
            } else
                fatal ("P-003", null, e);

        } catch (RuntimeException e) {
            // Don't discard location that triggered the exception
            throw new SAXParseException (
                e.getMessage () != null
                    ? e.getMessage ()
                    : e.getClass ().getName (),
                locator.getPublicId (), locator.getSystemId (),
                locator.getLineNumber (), locator.getColumnNumber (),
                e);

        } finally {
            // recycle temporary data used during parsing
            strTmp = null;
            attTmp = null;
            nameTmp = null;
            nameCache = null;
            nsAttTmp = null;

            // ditto input sources etc
            if (in != null) {
                in.close ();
                in = null;
            }

            // get rid of all DTD info ... some of it would be
            // useful for editors etc, investigate later.

            params.clear ();
            entities.clear ();
            notations.clear ();
            elements.clear ();

            afterDocument ();
        }
    }

    // package private -- for subclass 
    void afterRoot () throws SAXException { }

    // package private -- for subclass 
    void afterDocument () { }

    // role is for diagnostics
    private void whitespace (String roleId) throws IOException, SAXException
        // [3] S ::= (#x20 | #x9 | #xd | #xa)+
    {
        if (!maybeWhitespace ())
            fatal ("P-004", new Object []
                    { messages.getMessage (locale, roleId) });
    }

        // S?
    private boolean maybeWhitespace () throws IOException, SAXException
    {
        if (!(inExternalPE && doLexicalPE))
            return in.maybeWhitespace ();

        // see getc() for the PE logic -- this lets us splice
        // expansions of PEs in "anywhere".  getc() has smarts,
        // so for external PEs we don't bypass it.

        // XXX we can marginally speed PE handling, and certainly
        // be cleaner (hence potentially more correct), by using
        // the observations that expanded PEs only start and stop
        // where whitespace is allowed.  getc wouldn't need any
        // "lexical" PE expansion logic, and no other method needs
        // to handle termination of PEs.  (parsing of literals would
        // still need to pop entities, but not parsing of references
        // in content.)

        char c = getc();
        boolean saw = false;

        while (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
            saw = true;

            // this gracefully ends things when we stop playing
            // with internal parameters.  caller should have a
            // grammar rule allowing whitespace at end of entity.
            if (in.isEOF () && !in.isInternal ())
                return saw;
            c = getc ();
        }
        ungetc ();
        return saw;
    }

    private String maybeGetName ()
    throws IOException, SAXException
    {
        NameCacheEntry  entry = maybeGetNameCacheEntry ();
        return (entry == null) ? null : entry.name;
    }

    private NameCacheEntry maybeGetNameCacheEntry ()
    throws IOException, SAXException
    {
        // [5] Name ::= (Letter|'_'|':') (Namechar)*
        char            c = getc ();

        if (!XmlChars.isLetter (c) && c != ':' && c != '_') {
            ungetc ();
            return null;
        }
        return nameCharString (c);
    }

    // Used when parsing enumerations
    private String getNmtoken ()
    throws SAXException, IOException
    {
        // [7] Nmtoken ::= (Namechar)+
        char c = getc ();
        if (!XmlChars.isNameChar (c))
            fatal ("P-006", new Object [] { new Character (c) });
        return nameCharString (c).name;
    }

    // n.b. this gets used when parsing attribute values (for
    // internal references) so we can't use strTmp; it's also
    // a hotspot for CPU and memory in the parser (called at least
    // once for each element) so this has been optimized a bit.

    private NameCacheEntry nameCharString (char c)
    throws IOException, SAXException
    {
        int     i = 1;

        nameTmp [0] = c;
        for (;;) {
            if ((c = in.getNameChar ()) == 0)
                break;
            if (i >= nameTmp.length) {
                char tmp [] = new char [nameTmp.length + 10];
                System.arraycopy (nameTmp, 0, tmp, 0, nameTmp.length);
                nameTmp = tmp;
            }
            nameTmp [i++] = c;
        }
        return nameCache.lookupEntry (nameTmp, i);
    }

    //
    // much similarity between parsing entity values in DTD
    // and attribute values (in DTD or content) ... both follow
    // literal parsing rules, newline canonicalization, etc
    //
    // leaves value in 'strTmp' ... either a "replacement text" (4.5),
    // or else partially normalized attribute value (the first bit
    // of 3.3.3's spec, without the "if not CDATA" bits).
    //
    private void parseLiteral (boolean isEntityValue)
    throws IOException, SAXException
    {
        // [9] EntityValue ::=
        //      '"' ([^"&%] | Reference | PEReference)* '"'
        //    | "'" ([^'&%] | Reference | PEReference)* "'"
        // [10] AttValue ::=
        //      '"' ([^"&]  | Reference              )* '"'
        //    | "'" ([^'&]  | Reference              )* "'"

        // Only expand PEs in getc() when processing entity value literals
        // and do not expand when processing AttValue.  Save state of
        // doLexicalPE and restore it before returning.
        boolean savedLexicalPE = doLexicalPE;
//         doLexicalPE = isEntityValue;

        char            quote = getc ();
        char            c;
        InputEntity     source = in;

        if (quote != '\'' && quote != '"')
            fatal ("P-007");

        // don't report entity expansions within attributes,
        // they're reported "fully expanded" via SAX
        isInAttribute = !isEntityValue;

        // get value into strTmp
        strTmp = new StringBuffer ();

        // scan, allowing entity push/pop wherever ...
        // expanded entities can't terminate the literal!
        for (;;) {
            if (in != source && in.isEOF ()) {
                // we don't report end of parsed entities
                // within attributes (no SAX hooks)
                in = in.pop ();
                continue;
            }
            if ((c = getc ()) == quote && in == source)
                break;

            //
            // Basically the "reference in attribute value"
            // row of the chart in section 4.4 of the spec
            //
            if (c == '&') {
                String  entityName = maybeGetName ();

                if (entityName != null) {
                    nextChar (';', "F-020", entityName);

                    // 4.4 says:  bypass these here ... we'll catch
                    // forbidden refs to unparsed entities on use
                    if (isEntityValue) {
                        strTmp.append ('&');
                        strTmp.append (entityName);
                        strTmp.append (';');
                        continue;
                    }
                    expandEntityInLiteral (entityName, entities, isEntityValue);


                // character references are always included immediately
                } else if ((c = getc ()) == '#') {
                    int tmp = parseCharNumber ();

                    if (tmp > 0xffff) {
                        tmp = surrogatesToCharTmp (tmp);
                        strTmp.append (charTmp [0]);
                        if (tmp == 2)
                            strTmp.append (charTmp [1]);
                    } else
                        strTmp.append ((char) tmp);
                } else
                    fatal ("P-009");
                continue;

            }

            // expand parameter entities only within entity value literals
            if (c == '%' && isEntityValue) {
                String  entityName = maybeGetName ();

                if (entityName != null) {
                    nextChar (';', "F-021", entityName);
                    if (inExternalPE)
                        expandEntityInLiteral (entityName,
                                params, isEntityValue);
                    else
                        fatal ("P-010", new Object [] { entityName });
                    continue;
                } else
                    fatal ("P-011");
            }

            // For attribute values ...
            if (!isEntityValue) {
                // 3.3.3 says whitespace normalizes to space...
                if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
                    strTmp.append (' ');
                    continue;
                }

                // "<" not legal in parsed literals ...
                if (c == '<')
                    fatal ("P-012");
            }

            strTmp.append (c);
        }

        isInAttribute = false;
//         doLexicalPE = savedLexicalPE;
    }

    // does a SINGLE expansion of the entity (often reparsed later)
    private void expandEntityInLiteral (
        String          name,
        SimpleHashtable table,
        boolean         isEntityValue
    ) throws SAXException, IOException
    {
        Object  entity = table.get (name);

        //
        // Note:  if entity is a PE (value.isPE) there is an XML
        // requirement that the content be "markkupdecl", but that error
        // is ignored here (as permitted by the XML spec).
        //
        if (entity instanceof InternalEntity) {
            InternalEntity value = (InternalEntity) entity;
            if (supportValidation && isValidating
                    && isStandalone
                    && !value.isFromInternalSubset)
                error ("V-002", new Object [] { name });
            pushReader (value.buf, name, !value.isPE);

        } else if (entity instanceof ExternalEntity) {
            if (!isEntityValue) // must be a PE ...
                fatal ("P-013", new Object [] { name });
            // XXX if this returns false ...
            pushReader ((ExternalEntity) entity);

        } else if (entity == null) {
            //
            // Note:  much confusion about whether spec requires such
            // errors to be fatal in many cases, but none about whether
            // it allows "normal" errors to be unrecoverable!
            //
            fatal (
                (table == params) ? "V-022" : "P-014",
                new Object [] { name });
        }
    }

    // [11] SystemLiteral ::= ('"' [^"]* '"') | ("'" [^']* "'")
    // for PUBLIC and SYSTEM literals, also "<?xml ...type='literal'?>'
    
    // NOTE:  XML spec should explicitly say that PE ref syntax is
    // ignored in PIs, comments, SystemLiterals, and Pubid Literal
    // values ... can't process the XML spec's own DTD without doing
    // that for comments.

    private String getQuotedString (String type, String extra)
    throws IOException, SAXException
    {
        // use in.getc to bypass PE processing
        char             quote = in.getc ();

        if (quote != '\'' && quote != '"')
            fatal ("P-015", new Object [] {
                messages.getMessage (locale, type, new Object [] { extra })
                });

        char            c;

        strTmp = new StringBuffer ();
        while ((c = in.getc ()) != quote)
            strTmp.append ((char)c);
        return strTmp.toString ();
    }


    private String parsePublicId ()
    throws IOException, SAXException
    {
        // [12] PubidLiteral ::= ('"' PubidChar* '"') | ("'" PubidChar* "'")
        // [13] PubidChar ::= #x20|#xd|#xa|[a-zA-Z0-9]|[-'()+,./:=?;!*#@$_%]
        String retval = getQuotedString ("F-033", null);
        for (int i = 0; i < retval.length (); i++) {
            char c = retval.charAt (i);
            if (" \r\n-'()+,./:=?;!*#@$_%0123456789".indexOf(c) == -1
                    && !(c >= 'A' && c <= 'Z')
                    && !(c >= 'a' && c <= 'z'))
                fatal ("P-016", new Object [] { new Character (c) });
        }
        strTmp = new StringBuffer ();
        strTmp.append (retval);
        return normalize (false);
    }

        // [14] CharData ::= [^<&]* - ([^<&]* ']]>' [^<&]*)
        // handled by:  InputEntity.parsedContent()

    private boolean maybeComment (boolean skipStart)
    throws IOException, SAXException
    {
        // [15] Comment ::= '<!--'
        //              ( (Char - '-') | ('-' (Char - '-'))*
        //              '-->'
        if (!in.peek (skipStart ? "!--" : "<!--", null))
            return false;

        boolean         savedLexicalPE = doLexicalPE;

        doLexicalPE = false;
        boolean saveCommentText = lexicalHandler != nullHandler;
        if (saveCommentText) {
            strTmp = new StringBuffer ();
        }

    oneComment:
        for (;;) {
            try {
                // bypass PE expansion, but permit PEs
                // to complete ... valid docs won't care.
                for (;;) {
                    int c = getc ();
                    if (c == '-') {
                        c = getc ();
                        if (c != '-') {
                            if (saveCommentText)
                                strTmp.append ('-');
                            ungetc ();
                            continue;
                        }
                        nextChar ('>', "F-022", null);
                        break oneComment;
                    }
                    if (saveCommentText)
                        strTmp.append ((char)c);
                }
            } catch (EndOfInputException e) {
                //
                // This is fatal EXCEPT when we're processing a PE...
                // in which case a validating processor reports an error.
                // External PEs are easy to detect; internal ones we
                // infer by being an internal entity outside an element.
                //
                if (inExternalPE || (!donePrologue && in.isInternal ())) {
                    if (supportValidation && isValidating)
                        error ("V-021", null);
                    in = in.pop ();
                    continue;
                }
                fatal ("P-017");
            }
        }
        doLexicalPE = savedLexicalPE;
        if (saveCommentText) {
            // Convert string to array of chars
            int length = strTmp.length();
            char[] charArray = new char[length];
            if (length != 0) {
                // XXX Avoid calling getChars on zero-size array as a
                // workaround for a bug that occurs in at least JDK1.2.2
                // which has since been fixed in JDK1.3
                strTmp.getChars(0, length, charArray, 0);
            }
            lexicalHandler.comment(charArray, 0, length);
        }
        return true;
    }

    private boolean maybePI (boolean skipStart)
    throws IOException, SAXException
    {
        // [16] PI ::= '<?' PITarget
        //              (S (Char* - (Char* '?>' Char*)))?
        //              '?>'
        // [17] PITarget ::= Name - (('X'|'x')('M'|'m')('L'|'l')
        boolean         savedLexicalPE = doLexicalPE;

        if (!in.peek (skipStart ? "?" : "<?", null))
            return false;
        doLexicalPE = false;

        String          target = maybeGetName ();

        if (target == null)
            fatal ("P-018");
        if ("xml".equals (target))
            fatal ("P-019");
        if ("xml".equalsIgnoreCase (target))
            fatal ("P-020", new Object [] { target });

        if (maybeWhitespace ()) {
            strTmp = new StringBuffer ();
            try {
                for (;;) {
                    // use in.getc to bypass PE processing
                    char c = in.getc ();
                    //Reached the end of PI.
                    if (c == '?' && in.peekc ('>'))
                        break;
                    strTmp.append (c);
                }
            } catch (EndOfInputException e) {
                fatal ("P-021");
            }
            contentHandler.processingInstruction (target, strTmp.toString ());
        } else {
            if (!in.peek ("?>", null))
                fatal ("P-022");
            contentHandler.processingInstruction (target, "");
        }

        doLexicalPE = savedLexicalPE;
        return true;
    }

        // [18] CDSect ::= CDStart CData CDEnd
        // [19] CDStart ::= '<![CDATA['
        // [20] CData ::= (Char* - (Char* ']]>' Char*))
        // [21] CDEnd ::= ']]>'
        //
        //      ... handled by InputEntity.unparsedContent()


    private void maybeXmlDecl ()
    throws IOException, SAXException
    {
        // [23] XMLDecl ::= '<?xml' VersionInfo EncodingDecl?
        //                      SDDecl? S? '>'

        if (!in.isXmlDeclOrTextDeclPrefix()) {
            return;
        }
        // Consume '<?xml'
        peek("<?xml");

        readVersion (true, "1.0");
        readEncoding (false);
        readStandalone ();
        maybeWhitespace ();
        if (!peek ("?>")) {
            char c = getc ();
            fatal ("P-023", new Object []
                { Integer.toHexString (c), new Character (c) });
        }
    }

    // collapsing several rules together ... 
    // simpler than attribute literals -- no reference parsing!
    private String maybeReadAttribute (String name, boolean must)
    throws IOException, SAXException
    {
        // [24] VersionInfo ::= S 'version' Eq \'|\" versionNum \'|\"
        // [80] EncodingDecl ::= S 'encoding' Eq \'|\" EncName \'|\"
        // [32] SDDecl ::=  S 'standalone' Eq \'|\" ... \'|\"
        if (!maybeWhitespace ()) {
            if (!must)
                return null;
            fatal ("P-024", new Object [] { name });
            // NOTREACHED
        }

        if (!peek (name))
            if (must)
                fatal ("P-024", new Object [] { name });
            else {
                // To ensure that the whitespace is there so that when we
                // check for the next attribute we assure that the
                // whitespace still exists.
                ungetc ();
                return null;
            }

        // [25] Eq ::= S? '=' S?
        maybeWhitespace ();
        nextChar ('=', "F-023", null);
        maybeWhitespace ();

        return getQuotedString ("F-035", name);
    }

    private void readVersion (boolean must, String versionNum)
    throws IOException, SAXException
    {
        String  value = maybeReadAttribute ("version", must);

        // [26] versionNum ::= ([a-zA-Z0-9_.:]| '-')+

        if (must && value == null)
            fatal ("P-025", new Object [] { versionNum });
        if (value != null) {
            int length = value.length ();
            for (int i = 0; i < length; i++) {
                char c = value.charAt (i);
                if (!(    (c >= '0' && c <= '9')
                        || c == '_' || c == '.'
                        || (c >= 'a' && c <= 'z')
                        || (c >= 'A' && c <= 'Z')
                        || c == ':' || c == '-')
                        )
                    fatal ("P-026", new Object [] { value });
            }
        }
        if (value != null && !value.equals (versionNum))
            error ("P-027", new Object [] { versionNum, value });
    }

    private void maybeMisc (boolean eofOK)
    throws IOException, SAXException
    {
        // Misc*
        while (!eofOK || !in.isEOF ()) {
            // [27] Misc ::= Comment | PI | S
            if (maybeComment (false)
                    || maybePI (false)
                    || maybeWhitespace ())
                continue;
            else
                break;
        }
    }

    // common code used by most markup declarations
    // ... S (Q)Name ...
    private String getMarkupDeclname (String roleId, boolean qname)
    throws IOException, SAXException
    {
        String  name;

        whitespace (roleId);
        name = maybeGetName ();
        if (name == null)
            fatal ("P-005", new Object []
                { messages.getMessage (locale, roleId) });
        return name;
    }

    private boolean maybeDoctypeDecl ()
    throws IOException, SAXException
    {
        // [28] doctypedecl ::= '<!DOCTYPE' S Name
        //      (S ExternalID)?
        //      S? ('[' (markupdecl|PEReference|S)* ']' S?)?
        //      '>'
        if (!peek ("<!DOCTYPE"))
            return false;

        ExternalEntity  externalSubset = null;

        rootElementName = getMarkupDeclname ("F-014", true);
        if (maybeWhitespace ()
                && (externalSubset = maybeExternalID ()) != null) {
            lexicalHandler.startDTD(rootElementName, externalSubset.publicId,
                                    externalSubset.verbatimSystemId);
            maybeWhitespace ();
        } else {
            lexicalHandler.startDTD(rootElementName, null, null);
        }
        if (in.peekc ('[')) {
            for (;;) {
                //Pop PEs when they are done.
                if (in.isEOF () && !in.isDocument ()) {
                    in = in.pop ();
                    continue;
                }
                if (maybeMarkupDecl ()
                        || maybePEReference ()
                        || maybeWhitespace ()
                        )
                    continue;
                else if (peek ("<!["))
                    fatal ("P-028");
                else
                    break;
            }
            nextChar (']', "F-024", null);
            maybeWhitespace ();
        }
        nextChar ('>', "F-025", null);

        // [30] extSubset ::= TextDecl? extSubsetDecl
        // [31] extSubsetDecl ::= ( markupdecl | conditionalSect
        //              | PEReference | S )*
        //      ... same as [79] extPE, which is where the code is

        if (externalSubset != null) {
            externalSubset.name = "[dtd]";  // SAX2 ext specifies this name
            externalSubset.isPE = true;
            externalParameterEntity (externalSubset);
        }

        // params are no good to anyone starting now -- bye!
        params.clear ();

        lexicalHandler.endDTD();

        // make sure notations mentioned in attributes
        // and entities were declared ... those are validity
        // errors, but we must always clean up after them!
        Vector  v = new Vector ();

        for (Enumeration e = notations.keys ();
                e.hasMoreElements ();
                ) {
            String name = (String) e.nextElement ();
            Object value = notations.get (name);

            if (value == Boolean.TRUE) {
                if (supportValidation && isValidating)
                    error ("V-003", new Object [] { name });
                v.addElement (name);
            } else if (value instanceof String) {
                if (supportValidation && isValidating)
                    error ("V-004", new Object [] { name });
                v.addElement (name);
            }
        }
        while (!v.isEmpty ()) {
            Object name = v.firstElement ();
            v.removeElement (name);
            notations.remove (name);
        }

        return true;
    }

    private boolean maybeMarkupDecl ()
    throws IOException, SAXException
    {
            // [29] markupdecl ::= elementdecl | Attlistdecl
            //         | EntityDecl | NotationDecl | PI | Comment
        return maybeElementDecl ()
                || maybeAttlistDecl ()
                || maybeEntityDecl ()
                || maybeNotationDecl ()
                || maybePI (false)
                || maybeComment (false)
                ;
    }


    private void readStandalone ()
    throws IOException, SAXException
    {
        String  value = maybeReadAttribute ("standalone", false);

        // [32] SDDecl ::= ... "yes" or "no"
        if (value == null || "no".equals (value))
            return;
        if ("yes".equals (value)) {
            isStandalone = true;
            return;
        }
        fatal ("P-029", new Object [] { value });
    }

    private static final String         XmlLang = "xml:lang";

    private boolean isXmlLang (String value)
    {
        // [33] LanguageId ::= Langcode ('-' Subcode)*
        // [34] Langcode ::= ISO639Code | IanaCode | UserCode
        // [35] ISO639Code ::= [a-zA-Z] [a-zA-Z]
        // [36] IanaCode ::= [iI] '-' SubCode
        // [37] UserCode ::= [xX] '-' SubCode
        // [38] SubCode ::= [a-zA-Z]+

        // the ISO and IANA codes (and subcodes) are registered,
        // but that's neither a WF nor a validity constraint.

        int     nextSuffix;
        char    c;
        
        if (value.length () < 2)
            return false;
        c = value.charAt (1);
        if (c == '-') {         // IANA, or user, code
            c = value.charAt (0);
            if (!(c == 'i' || c == 'I' || c == 'x' || c == 'X'))
                return false;
            nextSuffix = 1;
        } else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
                                // 2 letter ISO code, or error
            c = value.charAt (0);
            if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')))
                return false;
            nextSuffix = 2;
        } else
            return false;
        
        // here "suffix" ::= '-' [a-zA-Z]+ suffix*
        while (nextSuffix < value.length ()) {
            c = value.charAt (nextSuffix);
            if (c != '-')
                break;
            while (++nextSuffix < value.length ()) {
                c = value.charAt (nextSuffix);
                if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')))
                    break;
            }
        }
        return value.length () == nextSuffix && c != '-';
    }



    //
    // CHAPTER 3:  Logical Structures
    //

    private boolean maybeElement (ElementValidator validator)
    throws IOException, SAXException
    {
        // [39] element ::= EmptyElemTag | Stag content ETag
        // [40] STag ::= '<' Name (S Attribute)* S? '>'

        NameCacheEntry          name;
        ElementDecl             element;
        boolean                 haveAttributes = false;
        boolean                 hasContent = true;
        int                     startLine;

        // the leading "<" has already been consumed
        name = maybeGetNameCacheEntry ();

        // n.b. InputEntity guarantees 1+N char pushback always,
        // and maybeGetName won't use more than one to see if
        // it's instead "<?", "<!--", "<![CDATA[", or an error.
        if (name == null)
            return false;

        // XXX Test for namespace conformance here
        // if (namespaces) {
            // some code testing name.name
        // }
            
        // report validity errors ASAP
        if (validator != null)
            validator.consume (name.name);

        element = (ElementDecl) elements.get (name.name);
        if (supportValidation && isValidating) {
            if (element == null || element.contentType == null) {
                error ("V-005", new Object [] { name.name });
                // minimize repetitive diagnostics
                element = new ElementDecl (name.name);
                element.contentType = strANY;
                elements.put (name.name, element);
            }
            if (validator == null
                    && rootElementName != null
                    && !rootElementName.equals (name.name))
                error ("V-006", new Object [] { name.name, rootElementName });
        }

        // save the line number here so we can give better diagnostics
        // by identifying where the element started; WF errors may be
        // reported thousands of lines "late".
        startLine = in.getLineNumber ();

        // Invariant: attTmp and nsAttTmp are empty except briefly in this
        // method they are not empty iff haveAttributes is true

        // Track whether we saw whitespace before an attribute;
        // in some cases it's required, though superfluous
        boolean         sawWhite = in.maybeWhitespace ();

        // These are exceptions from the first pass; they should be ignored
        // if there's a second pass, but reported otherwise.  A second pass
        // occurs when a namespace declaration is found in the first pass.
	Vector exceptions = null;

        // SAX2 Namespace processing
        if (namespaces) {
            nsSupport.pushContext();
            seenNSDecl = false;
        }

        // Each pass through this loop reads
        //      Name eq AttValue S?
        // Loop exits on ">", "/>", or error
        for (;;) {
            if (in.peekc ('>'))
                break;

            // [44] EmptyElementTag ::= '<' Name (S Attribute)* S? '/>'
            if (in.peekc ('/')) {
                hasContent = false;
                break;
            }

            //Need to have a whitespace between attributes.
            if (!sawWhite)
                fatal ("P-030");

            // [41] Attribute ::= Name Eq AttValue

            String              attQName;
            AttributeDecl       info;
            String              value;

            attQName = maybeGetName ();
            // Need to do this as we have already consumed the 
            // whitespace and didn't see the end tag.
            if (attQName == null)
                fatal ("P-031", new Object [] { new Character (getc ()) });

            if (attTmp.getValue (attQName) != null)
                fatal ("P-032", new Object [] { attQName });

            // [25] Eq ::= S? '=' S?
            in.maybeWhitespace ();
            nextChar ('=', "F-026", attQName);
            in.maybeWhitespace ();

            // We are not in the DTD => PEs are not recognized => we no
            // longer need to expand PEs => don't expand PEs in AttValue =>
            // doLexicalPE = false and call parseLiteral(isEntityValue =
            // false) both
            doLexicalPE = false;
            parseLiteral (false);
            // We are no longer in the DTD so we never need to expand PEs

            sawWhite = in.maybeWhitespace ();

            // normalize and check values right away.

            info = (element == null)
                    ? null
                    : (AttributeDecl) element.attributes.get (attQName);
            if (info == null) {
                if (supportValidation && isValidating)
                    error ("V-007", new Object [] { attQName, name.name });
                value = strTmp.toString ();
            } else {
                if (!AttributeDecl.CDATA.equals (info.type)) {
                    value = normalize (!info.isFromInternalSubset);
                    if (supportValidation && isValidating)
                        validateAttributeSyntax (info, value);
                } else
                    value = strTmp.toString ();
                if (supportValidation && isValidating
                        && info.isFixed
                        && !value.equals (info.defaultValue))
                    error ("V-008",
                        new Object [] {attQName, name.name, info.defaultValue});
            }

            // assert(value != null)

            if (XmlLang.equals (attQName) && !isXmlLang (value))
                error ("P-033", new Object [] { value });

            String type = (info == null) ? AttributeDecl.CDATA : info.type;
            String defaultValue = (info == null) ? null : info.defaultValue;

            if (namespaces) {
                exceptions = processAttributeNS(attQName, type, value,
                                                defaultValue, true, false,
                                                exceptions);
            } else {
                // No namespaces case
                attTmp.addAttribute("", "", attQName, type, value,
                                    defaultValue, true);
            }

            haveAttributes = true;
        }
        if (element != null)
            attTmp.setIdAttributeName (element.id);

        // if we had ATTLIST decls, handle required & defaulted attributes
        // before telling next layer about this element
        if (element != null && element.attributes.size () != 0) {
            haveAttributes = defaultAttributes(element) || haveAttributes;
        }

        // Ensure that this element's namespace declarations apply to all of
        // this element's attributes as well.  If there was a Namespace
        // declaration, we have to make a second pass just to be safe -- this
        // will happen very rarely, possibly only once for each document.
        if (seenNSDecl) {
            // assert(namespaces == true)
            int length = attTmp.getLength();
            for (int i = 0; i < length; i++) {
                String attQName = attTmp.getQName(i);
                if (attQName.startsWith("xmlns")) {
                    // Could be a namespace declaration

                    if (attQName.length() == 5 || attQName.charAt(5) == ':') {
                        // Default or non-default NS declaration
                        continue;
                    }
                }

                // assert(not a namespace declaration)
                String attName[] = processName(attQName, true, false);
                attTmp.setURI(i, attName[0]);
                attTmp.setLocalName(i, attName[1]);
            }
	} else if (exceptions != null && errHandler != null) {
	    for (int i = 0; i < exceptions.size(); i++) {
		errHandler.error((SAXParseException)(exceptions.elementAt(i)));
            }
        }

        // OK, finally report the event.
        if (namespaces) {
            String[] parts = processName(name.name, false, false);
            contentHandler.startElement(parts[0], parts[1], parts[2], attTmp);
        } else {
            contentHandler.startElement("", "", name.name, attTmp);
        }

        // Clear temporaries only when necessary because this may be
        // expensive and a doc may have lots of elements w/o attributes
        if (haveAttributes) {
            attTmp.clear();
            if (supportValidation && isValidating && namespaces && !prefixes) {
                nsAttTmp.removeAllElements();
            }
        }

        // prepare to validate the content of this element.
        // in nonvalidating parsers, this accepts ANY content
        validator = newValidator (element);
        
        if (hasContent) {
            content (element, false, validator);

            // [42] ETag ::= '</' Name S? '>'
            // ... content swallowed "</"

            if (!in.peek (name.name, name.chars))
                fatal ("P-034", new Object []
                    { name.name, new Integer (startLine) });
            in.maybeWhitespace ();
        }

        nextChar ('>', "F-027", name.name);
        validator.done ();

        if (namespaces) {
            // Split the name.  Unfortunately, we can't always reuse the
            // info from the startElement event above b/c this element may
            // have subelements and a global temporary is used.
            String[] parts = processName(name.name, false, false);

            // Report appropriate events...
            contentHandler.endElement(parts[0], parts[1], parts[2]);
            Enumeration prefixes = nsSupport.getDeclaredPrefixes();
            while (prefixes.hasMoreElements()) {
                String prefix = (String)prefixes.nextElement();
                contentHandler.endPrefixMapping(prefix);
            }
            nsSupport.popContext();
        } else {
            contentHandler.endElement("", "", name.name);
        }

        return true;
    }

    /**
     * Process attributes for namespace support.  This is mostly common
     * code that gets called from two places and was factored out.  The
     * <code>isDefaulting</code> param specifies where the code is called
     * from.
     *
     * @param isDefaulting true iff we are processing this attribute from
     *                     the <code>defaultAttributes(...)</code> method
     *
     * The namespace processing code is derived from the SAX2 ParserAdapter
     * code.  This code should be kept in sync with ParserAdapter bug
     * fixes.
     *
     * Note: Modifies <code>seenNSDecl</code> iff a xmlns attribute, ie a
     * namespace decl, was found.  Modifies <code>attTmp</code> and
     * <code>nsAttTmp</code>.
     */
    private Vector processAttributeNS(String attQName, String type,
                                      String value, String defaultValue,
                                      boolean isSpecified, boolean isDefaulting,
                                      Vector exceptions)
        throws SAXException
    {
        // assert(namespaces == true)

      nonNamespace:
        if (attQName.startsWith("xmlns")) {
            // Could be a namespace declaration

            boolean defaultNSDecl = attQName.length() == 5;
            if (!defaultNSDecl && attQName.charAt(5) != ':') {
                // Not a namespace declaration
                break nonNamespace;
            }

            // Must be some kind of namespace declaration
            String prefix;
            if (defaultNSDecl) {
                // Default namespace, so use empty string as prefix
                prefix = "";
            } else {
                // Non-default namespace decl, extract the prefix
                prefix = attQName.substring(6);
            }

            if (!nsSupport.declarePrefix(prefix, value)) {
                error("P-083", new Object[] { prefix });
            }
            contentHandler.startPrefixMapping(prefix, value);

            // We may need to add this attribute to appropriate lists
            if (prefixes) {
                attTmp.addAttribute("", prefix, attQName.intern(),
                                    type, value, defaultValue, isSpecified);
            } else if (supportValidation && isValidating && !isDefaulting) {
                // Add this namespace attribute to a different list that
                // will be used to check for #REQUIRED attributes later.
                // Since "prefixes" is false, these are not reported to the
                // ContentHandler.  This step is not needed during the
                // second pass of attribute processing where default values
                // are provided.
                nsAttTmp.addElement(attQName);
            }
            seenNSDecl = true;
            return exceptions;
        }

        // This isn't a namespace declaration.
        try {
            String attName[] = processName(attQName, true, true);
            attTmp.addAttribute(attName[0], attName[1], attName[2], type,
                                value, defaultValue, isSpecified);
        } catch (SAXException e) {
            if (exceptions == null) {
                exceptions = new Vector();
            }
            exceptions.addElement(e);
            attTmp.addAttribute("", attQName, attQName, type, value,
                                defaultValue, isSpecified);
        }
        return exceptions;
    }

    /**
     * Process a qualified (prefixed) name.
     *
     * <p>If the name has an undeclared prefix, use only the qname
     * and make an ErrorHandler.error callback in case the app is
     * interested.</p>
     *
     * @param qName The qualified (prefixed) name.
     * @param isAttribute true if this is an attribute name.
     * @return The name split into three parts.
     * @exception org.xml.sax.SAXException The client may throw
     *            an exception if there is an error callback.
     */
    private String[] processName(String qName, boolean isAttribute,
                                 boolean useException)
        throws SAXException
    {
        // assert(namespaces == true)
        String parts[] = nsSupport.processName(qName, namePartsTmp,
                                               isAttribute);
        if (parts == null) {
            parts = new String[3];
            // SAX should use "" instead of null for parts 0 and 1 ???
            parts[0] = "";
            String localName = XmlNames.getLocalPart(qName);
            parts[1] = localName != null ? localName.intern() : "";
            parts[2] = qName.intern();

            String messageId = "P-084";
            Object[] parameters = new Object[] { qName };
	    if (useException) {
                throw new SAXParseException(
                    messages.getMessage(locale, messageId, parameters),
                    locator);
            }
            error(messageId, parameters);
        }
        return parts;
    }


    /**
     * To validate, subclassers should create an object that can
     * accept valid streams of element names, text, and terminate.
     */
    // package private ... overriden in validating subclass
    ElementValidator newValidator (ElementDecl element)
    {
        return ElementValidator.ANY;            // "ANY" content is OK
    }


    /**
     * To validate, subclassers should at this time make sure that
     * values are of the declared types:<UL>
     *  <LI> ID and IDREF(S) values are Names
     *  <LI> NMTOKEN(S) are Nmtokens
     *  <LI> ENUMERATION values match one of the tokens
     *  <LI> NOTATION values match a notation name
     *  <LI> ENTITIY(IES) values match an unparsed external entity
     *  </UL>
     *
     * <P> Separately, make sure IDREF values match some ID
     * provided in the document (in the afterRoot method).
     */
    // package private
    void validateAttributeSyntax (AttributeDecl attr, String value)
    throws SAXException
    {
        return;
    }

    /**
     * Provide default attributes for an element and check for #REQUIRED
     * attributes.
     *
     * Note: this method accesses <code>attTmp</code> and
     * <code>nsAttTmp</code>
     */
    private boolean defaultAttributes(ElementDecl element)
        throws SAXException
    {
        boolean         didDefault = false;

        // Go through all declared attributes and:
        // 1) Default anything the document didn't provide.
        // 2) Check #REQUIRED values.
        for (Enumeration e = element.attributes.keys();
                e.hasMoreElements(); ) {

            // Declared attribute name
            String declAttName = (String)e.nextElement();

            if (attTmp.getValue(declAttName) != null) {
                // Attribute already has value so no defaulting necessary
                continue;
            }

            // If we get here, then declared attribute is not in the list
            // of attributes to be reported to ContentHandler.

            // Get more info on the declared attribute
            AttributeDecl info =
                    (AttributeDecl)element.attributes.get(declAttName);

            // If this is a #REQUIRED attribute...
            if (supportValidation && isValidating && info.isRequired) {
                // Under certain conditions, check the auxiliary nsAttTmp
                // list for #REQUIRED attributes since these are not in the
                // list to be reported to the ContentHandler.
                if (namespaces && !prefixes) {
                    if (nsAttTmp.contains(declAttName)) {
                        // Namespace attribute is #REQUIRED and already has
                        // a value
                        continue;
                    }
                }
                error("V-009", new Object [] { declAttName });
            }

            String defaultValue = info.defaultValue;
            if (defaultValue != null) {
                if (supportValidation && isValidating
                        && isStandalone && !info.isFromInternalSubset)
                    error ("V-010", new Object [] { declAttName });

                if (namespaces) {
                    processAttributeNS(declAttName, info.type, defaultValue,
                                       defaultValue, false, true, null);
                } else {
                    attTmp.addAttribute("", "", declAttName, info.type,
                                        defaultValue, defaultValue, false);
                }
                didDefault = true;
            }
        }
        return didDefault;
    }

    // parses content inside a given element (or parsed entity), optionally
    // allowing EOF (when expanding internal or external entities) and
    // optionally validating elements/#PCDATA that we see
    private void content (
        ElementDecl             element,
        boolean                 allowEOF,
        ElementValidator        validator
    ) throws IOException, SAXException
    {
        for (;;) {
            // [43] content ::= (element|CharData|Reference
            //                  |CDSect|PI|Comment)*

            // markup?
            if (in.peekc ('<')) {
                if (maybeElement (validator))
                    continue;

                // Three cases:  Error, and either EOF or ETag.
                // Here we check Etag as a common exit. 
                if (in.peekc ('/'))
                    return;

                // Less commonly, it's a comment, PI, CDATA ...
                if (maybeComment (true) || maybePI (true))
                    continue;

                // ... CDATA are specially delimited characters; can be
                // #PCDATA or whitespace (the latter has validity issues).
                if (in.peek("![CDATA[", null)) {
                    lexicalHandler.startCDATA();
                    in.unparsedContent(contentHandler, validator,
                        (element != null) && element.ignoreWhitespace,
                        (isStandalone
                                && supportValidation && isValidating
                                && !element.isFromInternalSubset)
                            ? "V-023"
                            : null
                        );
                    lexicalHandler.endCDATA();
                    continue;
                }

                // ... or a grammatical error (WF violation).
                char    c = getc ();

                fatal ("P-079", new Object [] {
                    Integer.toHexString (c), new Character (c) });
                // NOTREACHED
            }

            // characters? ... whitespace or #PCDATA
            if (element != null
                    && element.ignoreWhitespace
                    && in.ignorableWhitespace (contentHandler)) {
                // XXX prefer to report validity error before the
                // whitespace was reported ...
                if (supportValidation && isValidating
                        && isStandalone && !element.isFromInternalSubset)
                    error ("V-011", new Object [] { element.name });
                continue;
            }
            if (in.parsedContent (contentHandler, validator))
                continue;

            if (in.isEOF ())
                break;

            // else MUST be an entity reference
            if (!maybeReferenceInContent (element, validator))
                throw new InternalError ();
        }
        if (!allowEOF)
            fatal ("P-035");
    }

    private boolean maybeElementDecl ()
    throws IOException, SAXException
    {
        // [45] elementDecl ::= '<!ELEMENT' S Name S contentspec S? '>'
        // [46] contentspec ::= 'EMPTY' | 'ANY' | Mixed | children
        InputEntity     start = peekDeclaration ("!ELEMENT");

        if (start == null)
            return false;

        // n.b. for content models where inter-element whitespace is 
        // ignorable, we mark that fact here.
        String          name = getMarkupDeclname ("F-015", true);
        ElementDecl     element = (ElementDecl) elements.get (name);
        boolean         declEffective = false;

        if (element != null) {
            if (element.contentType != null) {
                if (supportValidation && isValidating
                        && element.contentType != null)
                    error ("V-012", new Object [] { name });
                // don't override previous declaration
                element = new ElementDecl (name);
            } // else <!ATTLIST name ...> came first
        } else {
            element = new ElementDecl (name);
            if (!ignoreDeclarations) {
                elements.put (element.name, element);
                declEffective = true;
            }
        }
        element.isFromInternalSubset = !inExternalPE;

        whitespace ("F-000");
        if (peek (strEMPTY)) {
            element.contentType = strEMPTY;
            element.ignoreWhitespace = true;
        } else if (peek (strANY)) {
            element.contentType = strANY;
            element.ignoreWhitespace = false;
        } else
            element.contentType = getMixedOrChildren (element);

        maybeWhitespace ();
        char c = getc ();
        if (c != '>')
            fatal ("P-036", new Object [] { name, new Character (c) });
        if (supportValidation && isValidating && start != in)
            error ("V-013", null);

        if (declEffective) {
            declHandler.elementDecl(element.name, element.contentType);
        }
        return true;
    }

    // We're leaving the content model as a regular expression;
    // it's an efficient natural way to express such things, and
    // libraries often interpret them.  No whitespace in the
    // model we store, though!

    private String getMixedOrChildren (ElementDecl element)
    throws IOException, SAXException
    {           
        InputEntity     start;

        // [47] children ::= (choice|seq) ('?'|'*'|'+')?
        strTmp = new StringBuffer ();

        nextChar ('(', "F-028", element.name);
        start = in;
        maybeWhitespace ();
        strTmp.append ('(');

        if (peek ("#PCDATA")) {
            strTmp.append ("#PCDATA");
            getMixed (element.name, start);
            element.ignoreWhitespace = false;
        } else {
            element.model = getcps (element.name, start);
            element.ignoreWhitespace = true;
        }
        return strTmp.toString ();
    }

    // package private -- overridden by validating subclass
    ContentModel newContentModel (String tag)
    {
        return null;
    }

    // package private -- overridden by validating subclass
    ContentModel newContentModel (char type, ContentModel next)
    {
        return null;
    }

    // '(' S? already consumed
    // matching ')' must be in "start" entity if validating
    private ContentModel getcps (
        String          element,
        InputEntity     start
    ) throws IOException, SAXException
    {
        // [48] cp ::= (Name|choice|seq) ('?'|'*'|'+')?
        // [49] choice ::= '(' S? cp (S? '|' S? cp)* S? ')'
        // [50] seq    ::= '(' S? cp (S? ',' S? cp)* S? ')'
        boolean         decided = false;
        char            type = 0;
        ContentModel    retval, current, temp;

        retval = current = temp = null;

        do {
            String      tag;

            tag = maybeGetName ();
            if (tag != null) {
                strTmp.append (tag);
                temp = getFrequency (newContentModel (tag));
            } else if (peek ("(")) {
                InputEntity     next = in;
                strTmp.append ('(');
                maybeWhitespace ();
                temp = getFrequency (getcps (element, next));
            } else
                fatal ((type == 0) ? "P-039" :
                        ((type == ',') ? "P-037" : "P-038"),
                    new Object [] { new Character (getc ()) });

            maybeWhitespace ();
            if (decided) {
                char    c = getc ();

                if (current != null) {
                    current.next = newContentModel (type, temp);
                    current = current.next;
                }
                if (c == type) {
                    strTmp.append (type);
                    maybeWhitespace ();
                    continue;
                } else if (c == '\u0029') {     // rparen
                    ungetc ();
                    continue;
                } else {
                    fatal ((type == 0) ? "P-041" : "P-040",
                        new Object [] {
                            new Character (c),
                            new Character (type)
                            });
                }
            } else {
                type = getc ();
                if (type == '|' || type == ',') {
                    decided = true;
                    retval = current = newContentModel (type, temp);
                } else {
                    retval = current = temp;
                    ungetc ();
                    continue;
                }
                strTmp.append (type);
            }
            maybeWhitespace ();
        } while (!peek (")"));
        if (supportValidation && isValidating && in != start)
            error ("V-014", new Object [] { element });
        strTmp.append (')');
        return getFrequency (retval);
    }

    private ContentModel getFrequency (ContentModel original)
    throws IOException, SAXException
    {
        char    c = getc ();

        if (c == '?' || c == '+' || c == '*') {
            strTmp.append (c);
            if (original == null)
                return null;
            if (original.type == 0) {   // foo* etc
                original.type = c;
                return original;
            }
            return newContentModel (c, original);
        } else {
            ungetc ();
            return original;
        }
    }

    // '(' S? '#PCDATA' already consumed 
    // matching ')' must be in "start" entity if validating
    private void getMixed (String element, InputEntity start)
    throws IOException, SAXException
    {
        // [51] Mixed ::= '(' S? '#PCDATA' (S? '|' S? Name)* S? ')*'
        //              | '(' S? '#PCDATA'                   S? ')'
        maybeWhitespace ();
        if (peek ("\u0029*") || peek ("\u0029")) {
            if (supportValidation && isValidating && in != start)
                error ("V-014", new Object [] { element });
            strTmp.append (')');
            return;
        }

        Vector  v = null;

        if (supportValidation && isValidating)
            v = new Vector ();

        while (peek ("|")) {
            String name;

            strTmp.append ('|');
            maybeWhitespace ();

            name = maybeGetName ();
            if (name == null)
                fatal ("P-042", new Object []
                    { element, Integer.toHexString (getc ()) });
            if (supportValidation && isValidating) {
                if (v.contains (name))
                    error ("V-015", new Object [] { name });
                else
                    v.addElement (name);
            }
            strTmp.append (name);
            maybeWhitespace ();
        }
        
        if (!peek ("\u0029*"))  // right paren
            fatal ("P-043", new Object []
                { element, new Character (getc ()) });
        if (supportValidation && isValidating && in != start)
            error ("V-014", new Object [] { element });
        strTmp.append (')');
    }

    private boolean maybeAttlistDecl ()
    throws IOException, SAXException
    {
        // [52] AttlistDecl ::= '<!ATTLIST' S Name AttDef* S? '>'
        InputEntity start = peekDeclaration ("!ATTLIST");

        if (start == null)
            return false;

        String          name = getMarkupDeclname ("F-016", true);
        ElementDecl     element = (ElementDecl) elements.get (name);

        if (element == null) {
            // not yet declared -- no problem.
            element = new ElementDecl (name);
            if (!ignoreDeclarations)
                elements.put (name, element);
        }

        maybeWhitespace ();
        while (!peek (">")) {

            // [53] AttDef ::= S Name S AttType S DefaultDecl
            // [54] AttType ::= StringType | TokenizedType | EnumeratedType
            name = maybeGetName ();
            if (name == null)
                fatal ("P-044", new Object [] { new Character (getc ()) });
            whitespace ("F-001");

            AttributeDecl       a = new AttributeDecl (name);
            a.isFromInternalSubset = !inExternalPE;

            // Note:  use the type constants from AttributeDecl
            // so that "==" may be used (faster)

            // [55] StringType ::= 'CDATA'
            if (peek (AttributeDecl.CDATA))
                a.type = AttributeDecl.CDATA;

            // [56] TokenizedType ::= 'ID' | 'IDREF' | 'IDREFS'
            //          | 'ENTITY' | 'ENTITIES'
            //          | 'NMTOKEN' | 'NMTOKENS'
            // n.b. if "IDREFS" is there, both "ID" and "IDREF"
            // match peekahead ... so this order matters!
            else if (peek (AttributeDecl.IDREFS))
                a.type = AttributeDecl.IDREFS;
            else if (peek (AttributeDecl.IDREF))
                a.type = AttributeDecl.IDREF;
            else if (peek (AttributeDecl.ID)) {
                a.type = AttributeDecl.ID;
                if (element.id != null) {
                    if (supportValidation && isValidating)
                        error ("V-016", new Object [] { element.id });
                } else
                    element.id = name;
            } else if (peek (AttributeDecl.ENTITY))
                a.type = AttributeDecl.ENTITY;
            else if (peek (AttributeDecl.ENTITIES))
                a.type = AttributeDecl.ENTITIES;
            else if (peek (AttributeDecl.NMTOKENS))
                a.type = AttributeDecl.NMTOKENS;
            else if (peek (AttributeDecl.NMTOKEN))
                a.type = AttributeDecl.NMTOKEN;

            // [57] EnumeratedType ::= NotationType | Enumeration
            // [58] NotationType ::= 'NOTATION' S '(' S? Name
            //          (S? '|' S? Name)* S? ')'
            else if (peek (AttributeDecl.NOTATION)) {
                a.type = AttributeDecl.NOTATION;
                whitespace ("F-002");
                nextChar ('(', "F-029", null);
                maybeWhitespace ();

                Vector v = new Vector ();
                do {
                    if ((name = maybeGetName ()) == null)
                        fatal ("P-068");
                    // permit deferred declarations
                    if (supportValidation && isValidating
                            && notations.get (name) == null)
                        notations.put (name, name);
                    v.addElement (name);
                    maybeWhitespace ();
                    if (peek ("|"))
                        maybeWhitespace ();
                } while (!peek (")"));
                a.values = new String [v.size ()];
                for (int i = 0; i < v.size (); i++)
                    a.values [i] = (String)v.elementAt (i);

            // [59] Enumeration ::= '(' S? Nmtoken (S? '|' Nmtoken)* S? ')'
            } else if (peek ("(")) {
                a.type = AttributeDecl.ENUMERATION;
                maybeWhitespace ();

                Vector v = new Vector ();
                do {
                    name = getNmtoken ();
                    v.addElement (name);
                    maybeWhitespace ();
                    if (peek ("|"))
                        maybeWhitespace ();
                } while (!peek (")"));
                a.values = new String [v.size ()];
                for (int i = 0; i < v.size (); i++)
                    a.values [i] = (String)v.elementAt (i);
            } else
                fatal ("P-045",
                    new Object [] { name, new Character (getc ()) });


            // [60] DefaultDecl ::= '#REQUIRED' | '#IMPLIED'
            //          | (('#FIXED' S)? AttValue)
            whitespace ("F-003");
            if (peek ("#REQUIRED")) {
                a.valueDefault = AttributeDecl.REQUIRED;
                a.isRequired = true;
            } else if (peek ("#FIXED")) {
                if (supportValidation && isValidating
                        && a.type == AttributeDecl.ID)
                    error ("V-017", new Object [] { a.name });
                a.valueDefault = AttributeDecl.FIXED;
                a.isFixed = true;
                whitespace ("F-004");

                // Don't expand PEs in AttValue => doLexicalPE = false and
                // call parseLiteral(isEntityValue = false) both
                doLexicalPE = false;
                parseLiteral(false);

                // We are in DTD so set this back to true
                doLexicalPE = true;

                if (a.type != AttributeDecl.CDATA)
                    a.defaultValue = normalize (false);
                else
                    a.defaultValue = strTmp.toString ();
                if (a.type != AttributeDecl.CDATA)
                    validateAttributeSyntax (a, a.defaultValue);
            } else if (peek ("#IMPLIED")) {
                a.valueDefault = AttributeDecl.IMPLIED;
            } else {
                if (supportValidation && isValidating
                        && a.type == AttributeDecl.ID)
                    error ("V-018", new Object [] { a.name });
                // By default a.valueDefault == null here

                // Don't expand PEs in AttValue => doLexicalPE = false and
                // call parseLiteral(isEntityValue = false) both
                doLexicalPE = false;
                parseLiteral(false);

                // We are in DTD so set this back to true
                doLexicalPE = true;

                if (a.type != AttributeDecl.CDATA)
                    a.defaultValue = normalize (false);
                else
                    a.defaultValue = strTmp.toString ();
                if (a.type != AttributeDecl.CDATA)
                    validateAttributeSyntax (a, a.defaultValue);
            }

            if (XmlLang.equals (a.name)
                    && a.defaultValue != null
                    && !isXmlLang (a.defaultValue))
                error ("P-033", new Object [] { a.defaultValue });

            if (!ignoreDeclarations
                    && element.attributes.get (a.name) == null) {
                element.attributes.put (a.name, a);

                // Report attribute declaration to SAX DeclHandler
                String saxType;
                if (a.type == AttributeDecl.ENUMERATION
                        || a.type == AttributeDecl.NOTATION) {
                    StringBuffer fullType = new StringBuffer();

                    if (a.type == AttributeDecl.NOTATION) {
                        fullType.append(a.type);
                        fullType.append(" ");
                    }

                    if (a.values.length > 1) {
                        fullType.append("(");
                    }
                    for (int i = 0; i < a.values.length; i++) {
                        fullType.append(a.values[i]);
                        if (i + 1 < a.values.length) {
                            fullType.append("|");
                        }
                    }
                    if (a.values.length > 1) {
                        fullType.append(")");
                    }

                    saxType = fullType.toString();
                } else {
                    saxType = a.type;
                }
                declHandler.attributeDecl(element.name, a.name, saxType,
                                          a.valueDefault, a.defaultValue);
            }
            maybeWhitespace ();
        }
        if (supportValidation && isValidating && start != in)
            error ("V-013", null);
        return true;
    }

    // used when parsing literal attribute values,
    // or public identifiers.
    //
    // input in strTmp
    private String normalize (boolean invalidIfNeeded)
    throws SAXException
    {
        // this can allocate an extra string...

        String  s = strTmp.toString ();
        String  s2 = s.trim ();
        boolean didStrip = false;

        if (s != s2) {
            s = s2;
            s2 = null;
            didStrip = true;
        }
        strTmp = new StringBuffer ();
        for (int i = 0; i < s.length (); i++) {
            char        c = s.charAt (i);
            if (!XmlChars.isSpace (c)) {
                strTmp.append (c);
                continue;
            }
            strTmp.append (' ');
            while (++i < s.length () && XmlChars.isSpace (s.charAt (i)))
                didStrip = true;
            i--;
        }
        if (supportValidation && isValidating && isStandalone) {
            if (invalidIfNeeded && (s2 == null || didStrip))
                // XXX would like to tell the name of the attribute
                // which shouldn't have needed normalization
                error ("V-019", null);
        }
        if (didStrip)
            return strTmp.toString ();
        else
            return s;
    }

    private boolean maybeConditionalSect ()
    throws IOException, SAXException
    {
        // [61] conditionalSect ::= includeSect | ignoreSect

        if (!peek ("<!["))
            return false;

        String          keyword;
        InputEntity     start = in;

        maybeWhitespace ();

        if ((keyword = maybeGetName ()) == null)
            fatal ("P-046");
        maybeWhitespace ();
        nextChar ('[', "F-030", null);

        // [62] includeSect ::= '<![' S? 'INCLUDE' S? '['
        //                              extSubsetDecl ']]>'
        if ("INCLUDE".equals (keyword)) {
            for (;;) {
                while (in.isEOF () && in != start)
                    in = in.pop ();
                if (in.isEOF ()) {
                    if (supportValidation && isValidating)
                        error ("V-020", null);
                    in = in.pop ();
                }
                if (peek ("]]>"))
                    break;

                doLexicalPE = false;
                if (maybeWhitespace ())
                    continue;
                if (maybePEReference ())
                    continue;
                doLexicalPE = true;
                if (maybeMarkupDecl () || maybeConditionalSect ())
                    continue;

                fatal ("P-047");
            }

        // [63] ignoreSect ::= '<![' S? 'IGNORE' S? '['
        //                      ignoreSectcontents ']]>'
        // [64] ignoreSectcontents ::= Ignore ('<!['
        //                      ignoreSectcontents ']]>' Ignore)*
        // [65] Ignore ::= Char* - (Char* ('<![' | ']]>') Char*)
        } else if ("IGNORE".equals (keyword)) {
            int nestlevel = 1;
            // ignoreSectcontents
            doLexicalPE = false;
            while (nestlevel > 0) {
                char c = getc ();       // will pop input entities
                if (c == '<') {
                    if (peek ("!["))
                        nestlevel++;
                } else if (c == ']') {
                    if (peek ("]>"))
                        nestlevel--;
                } else
                    continue;
            }
        } else
            fatal ("P-048", new Object [] { keyword });
        return true;
    }


    //
    // CHAPTER 4:  Physical Structures
    //

    private boolean maybeReferenceInContent (
        ElementDecl             element,
        ElementValidator        validator
    ) throws IOException, SAXException
    {
        // [66] CharRef ::= ('&#' [0-9]+) | ('&#x' [0-9a-fA-F]*) ';'
        // [67] Reference ::= EntityRef | CharRef
        // [68] EntityRef ::= '&' Name ';'
        if (!in.peekc ('&'))
            return false;

        if (!in.peekc ('#')) {
            String      name = maybeGetName ();
            if (name == null)
                fatal ("P-009");
            nextChar (';', "F-020", name);
            expandEntityInContent (element, name, validator);
            return true;
        }

        validator.text ();
        contentHandler.characters (charTmp, 0,
                surrogatesToCharTmp (parseCharNumber ()));
        return true;
    }

    // parse decimal or hex numeric character reference
    private int parseCharNumber ()
    throws SAXException, IOException
    {
        char    c;
        int     retval = 0;

        // n.b. we ignore overflow ...
        if (getc () != 'x') {
            ungetc ();
            for (;;) {
                c = getc ();
                if (c >= '0' && c <= '9') {
                    retval *= 10;
                    retval += (c - '0');
                    continue;
                }
                if (c == ';')
                    return retval;
                fatal ("P-049");
            }
        } else for (;;) {
            c = getc ();
            if (c >= '0' && c <= '9') {
                retval <<= 4;
                retval += (c - '0');
                continue;
            }
            if (c >= 'a' && c <= 'f') {
                retval <<= 4;
                retval += 10 + (c - 'a');
                continue;
            }
            if (c >= 'A' && c <= 'F') {
                retval <<= 4;
                retval += 10 + (c - 'A');
                continue;
            }
            if (c == ';')
                return retval;
            fatal ("P-050");
        }
    }

    // parameter is a UCS-4 character ... i.e. not just 16 bit UNICODE,
    // though still subject to the 'Char' construct in XML
    private int surrogatesToCharTmp (int ucs4)
    throws SAXException
    {
        if (ucs4 <= 0xffff) {
            if (XmlChars.isChar (ucs4)) {
                charTmp [0] = (char) ucs4;
                return 1;
            } 
        } else if (ucs4 <= 0x0010ffff) {
            // we represent these as UNICODE surrogate pairs
            ucs4 -= 0x10000;
            charTmp [0] = (char) (0xd800 | ((ucs4 >> 10) & 0x03ff));
            charTmp [1] = (char) (0xdc00 | (ucs4 & 0x03ff));
            return 2;
        }
        fatal ("P-051", new Object [] { Integer.toHexString (ucs4) });
        // NOTREACHED
        return -1;
    }

    private void expandEntityInContent (
        ElementDecl             element,
        String                  name,
        ElementValidator        validator
    ) throws SAXException, IOException
    {
        Object                  entity = entities.get (name);
        InputEntity             last = in;

        if (entity == null) {
            //
            // Note:  much confusion about whether spec requires such
            // errors to be fatal in many cases, but none about whether
            // it allows "normal" errors to be unrecoverable!
            //
            fatal ("P-014", new Object [] { name });
        }

        if (entity instanceof InternalEntity) {
            InternalEntity      e = (InternalEntity) entity;

            //
            // we need to expand both entities and markup here...
            //
            if (supportValidation && isValidating
                    && isStandalone
                    && !e.isFromInternalSubset)
                error ("V-002", new Object [] { name });
            pushReader (e.buf, name, true);
            content (element, true, validator);
            if (in != last && !in.isEOF ()) {
                while (in.isInternal ())
                    in = in.pop ();
                fatal ("P-052", new Object [] { name });
            }
            lexicalHandler.endEntity(name);
            in = in.pop ();
        } else if (entity instanceof ExternalEntity) {
            ExternalEntity      e = (ExternalEntity) entity;
            if (e.notation != null)
                fatal ("P-053", new Object [] { name });

            if (supportValidation && isValidating
                    && isStandalone
                    && !e.isFromInternalSubset)
                error ("V-002", new Object [] { name });

            externalParsedEntity (element, e, validator);
        } else
            throw new InternalError (name);
    }

    private boolean maybePEReference ()
    throws IOException, SAXException
    {
        // This is the SYNTACTIC version of this construct.
        // When processing external entities, there is also
        // a LEXICAL version; see getc() and doLexicalPE.

        // [69] PEReference ::= '%' Name ';'
        if (!in.peekc ('%'))
            return false;

        String  name = maybeGetName ();
        Object  entity;

        if (name == null)
            fatal ("P-011");
        nextChar (';', "F-021", name);
        entity = params.get (name);

        if (entity instanceof InternalEntity) {
            InternalEntity      value = (InternalEntity) entity;
            pushReader (value.buf, name, false);

        } else if (entity instanceof ExternalEntity) {
            externalParameterEntity ((ExternalEntity)entity);

        } else if (entity == null) {
            //
            // NOTE:  by treating undefined parameter entities as 
            // nonfatal, we are assuming that the contradiction
            // between them being a WFC versus a VC is resolved in
            // favor of the latter.  Further, we are assuming that
            // validating parsers should behave like nonvalidating
            // ones in such a case:  ignoring further declarations.
            //
            ignoreDeclarations = true;
            if (supportValidation && isValidating)
                error ("V-022", new Object [] { name });
            else
                warning ("V-022", new Object [] { name });
        }
        return true;
    }

    private boolean maybeEntityDecl ()
    throws IOException, SAXException
    {
        // [70] EntityDecl ::= GEDecl | PEDecl
        // [71] GEDecl ::= '<!ENTITY' S       Name S EntityDef S? '>'
        // [72] PEDecl ::= '<!ENTITY' S '%' S Name S PEDEF     S? '>'
        // [73] EntityDef ::= EntityValue | (ExternalID NDataDecl?)
        // [74] PEDef     ::= EntityValue |  ExternalID
        //
        InputEntity     start = peekDeclaration ("!ENTITY");

        if (start == null)
            return false;

        String          entityName;
        SimpleHashtable defns;
        ExternalEntity  externalId;
        boolean         doStore;

        // PE expansion gets selectively turned off several places:
        // in ENTITY declarations (here), in comments, in PIs.
        
        // Here, we allow PE entities to be declared, and allows
        // literals to include PE refs without the added spaces
        // required with their expansion in markup decls.

        doLexicalPE = false;
        whitespace ("F-005");
        if (in.peekc ('%')) {
            whitespace ("F-006");
            defns = params;
        } else
            defns = entities;

        ungetc ();      // leave some whitespace
        doLexicalPE = true;
        entityName = getMarkupDeclname ("F-017", false);
        whitespace ("F-007");
        externalId = maybeExternalID ();

        //
        // first definition sticks ... e.g. internal subset PEs are used
        // to override DTD defaults.  It's also an "error" to incorrectly
        // redefine builtin internal entities, but since reporting such
        // errors is optional we only give warnings ("just in case") for
        // non-parameter entities.
        //
        doStore = (defns.get (entityName) == null);
        if (!doStore && defns == entities)
            warning ("P-054", new Object [] { entityName });
        
        // if we skipped a PE, ignore declarations since the
        // PE might have included an ovrriding declaration
        doStore &= !ignoreDeclarations;

        // internal entities
        if (externalId == null) {
            char                value [];
            InternalEntity      entity;

            doLexicalPE = false;                // "ab%bar;cd" -maybe-> "abcd"
            parseLiteral (true);
            doLexicalPE = true;
            if (doStore) {
                value = new char [strTmp.length ()];
                if (value.length != 0)
                    strTmp.getChars (0, value.length, value, 0);
                entity = new InternalEntity (entityName, value);
                entity.isPE = (defns == params);
                entity.isFromInternalSubset = !inExternalPE;
                defns.put (entityName, entity);

                // Report event
                if (defns == params) {
                    entityName = "%" + entityName;
                }
                declHandler.internalEntityDecl(entityName, new String(value));
            }

        // external entities (including unparsed)
        } else {
            // [76] NDataDecl ::= S 'NDATA' S Name
            if (defns == entities && maybeWhitespace ()
                    && peek ("NDATA")) {
                externalId.notation = getMarkupDeclname ("F-018", false);

                // flag undeclared notation for checking after
                // the DTD is fully processed
                if (supportValidation && isValidating
                        && notations.get (externalId.notation) == null)
                    notations.put (externalId.notation, Boolean.TRUE);
            }
            externalId.name = entityName;
            externalId.isPE = (defns == params);
            externalId.isFromInternalSubset = !inExternalPE;
            if (doStore) {
                defns.put (entityName, externalId);
                if (externalId.notation != null) {
                    dtdHandler.unparsedEntityDecl (entityName,
                            externalId.publicId, externalId.systemId,
                            externalId.notation);
                } else {
                    // Parsed external entity, either general or parameter
                    if (defns == params) {
                        entityName = "%" + entityName;
                    }
                    declHandler.externalEntityDecl(entityName,
                            externalId.publicId, externalId.systemId);
                }
            }
        }
        maybeWhitespace ();
        nextChar ('>', "F-031", entityName);
        if (supportValidation && isValidating && start != in)
            error ("V-013", null);
        return true;
    }

    private ExternalEntity maybeExternalID ()
        throws IOException, SAXException
    {
        // [75] ExternalID ::= 'SYSTEM' S SystemLiteral
        //              | 'PUBLIC' S' PubidLiteral S Systemliteral
        String          temp = null;
        ExternalEntity  retval;

        if (peek ("PUBLIC")) {
            whitespace ("F-009");
            temp = parsePublicId ();
        } else if (!peek ("SYSTEM"))
            return null;

        retval = new ExternalEntity (in);
        retval.publicId = temp;
        whitespace ("F-008");
        retval.verbatimSystemId = getQuotedString("F-034", null);
        retval.systemId = resolveURI(retval.verbatimSystemId);
        return retval;
    }

    private String parseSystemId()
        throws IOException, SAXException
    {
        String uri = getQuotedString("F-034", null);
        return resolveURI(uri);
    }

    private String resolveURI(String uri)
        throws SAXException
    {
        int     temp = uri.indexOf (':');

        // resolve relative URIs ... must do it here since
        // it's relative to the source file holding the URI!

        // "new java.net.URL (URL, string)" conforms to RFC 1630,
        // but we can't use that except when the URI is a URL.
        // The entity resolver is allowed to handle URIs that are
        // not URLs, so we pass URIs through with scheme intact
        if (temp == -1 || uri.indexOf ('/') < temp) {
            String      baseURI;

            baseURI = in.getSystemId ();
            if (baseURI == null)
                fatal ("P-055", new Object [] { uri });
            if (uri.length () == 0)
                uri = ".";
            baseURI = baseURI.substring (0, baseURI.lastIndexOf ('/') + 1);
            if (uri.charAt (0) != '/')
                uri = baseURI + uri;
            else {
                // We have relative URI that begins with a '/'

                // Extract scheme including colon from baseURI
                String baseURIScheme;
                int colonIndex = baseURI.indexOf(':');
                if (colonIndex == -1) {
                    // Base URI does not have a scheme so default to
                    // "file:" scheme
                    baseURIScheme = "file:";
                } else {
                    baseURIScheme = baseURI.substring(0, colonIndex + 1);
                }

                uri = baseURIScheme + uri;
            }

            // letting other code map any "/xxx/../" or "/./" to "/",
            // since all URIs must handle it the same.
        }
        // check for fragment ID in URI
        if (uri.indexOf ('#') != -1)
            error ("P-056", new Object [] { uri });
        return uri;
    }

    private void maybeTextDecl ()
    throws IOException, SAXException
    {
        // [77] TextDecl ::= '<?xml' VersionInfo? EncodingDecl S? '?>'

        if (!in.isXmlDeclOrTextDeclPrefix()) {
            return;
        }
        // Consume '<?xml'
        peek("<?xml");

        readVersion (false, "1.0");
        readEncoding (true);
        maybeWhitespace ();
        if (!peek ("?>"))
            fatal ("P-057");
    }

    // returns true except in case of nonvalidating parser which
    // chose to ignore the entity.

    private boolean externalParsedEntity (
        ElementDecl             element,
        ExternalEntity          next,
        ElementValidator        validator
    ) throws IOException, SAXException
    {
        // [78] ExtParsedEnt ::= TextDecl? content

        if (!pushReader (next)) {
            if (!isInAttribute) {
                lexicalHandler.endEntity(next.name);
            }
            return false;
        }

        maybeTextDecl ();
        content (element, true, validator);
        if (!in.isEOF ())
            fatal ("P-058", new Object [] { next.name });
        in = in.pop ();
        if (!isInAttribute) {
            lexicalHandler.endEntity(next.name);
        }
        return true;
    }

    private void externalParameterEntity (ExternalEntity next)
    throws IOException, SAXException
    {
        //
        // Reap the intended benefits of standalone declarations:
        // don't deal with external parameter entities, except to
        // validate the standalone declaration.
        //
        // XXX perhaps:  also add an option to skip reading external
        // PEs when not validating, so this behaves like the parsers
        // in Gecko and IE5.  Means setting ignoreDeclarations ...
        //
        if (isStandalone && fastStandalone)
            return;
        
        // n.b. "in external parameter entities" (and external
        // DTD subset, same grammar) parameter references can
        // occur "within" markup declarations ... expansions can
        // cross syntax rules.  Flagged here; affects getc().

        // [79] ExtPE ::= TextDecl? extSubsetDecl
        // [31] extSubsetDecl ::= ( markupdecl | conditionalSect
        //              | PEReference | S )*
        InputEntity     pe;

        inExternalPE = true;

        // Check for common case of file not found and throw a
        // SAXParseException
        try {
            // XXX if this returns false ...
            pushReader (next);
        } catch (IOException e) {
            fatal ("P-082", new Object [] { next.systemId }, e);
        }

        pe = in;

        // Check for common case of bad URL and throw a SAXParseException.
        // For bad URL case, JDK does not throw an exception when
        // URLConnection.getInputStream() is called but later when the app
        // tries to read from the stream in maybeTextDecl().
        try {
            maybeTextDecl ();
        } catch (IOException e) {
            // Pop invalid InputEntity so Locator info will be correct
            in = in.pop ();
            fatal ("P-082", new Object [] { next.systemId }, e);
        }
        while (!pe.isEOF ()) {
            // pop internal PEs (and whitespace before/after)
            if (in.isEOF ()) {
                in = in.pop ();
                continue;
            }
            doLexicalPE = false;
            if (maybeWhitespace ())
                continue;
            if (maybePEReference ())
                continue;
            doLexicalPE = true;
            if (maybeMarkupDecl () || maybeConditionalSect ())
                continue;
            break;
        }
        // if (in != pe) throw new InternalError ("who popped my PE?");
        if (!pe.isEOF ())
            fatal ("P-059", new Object [] { in.getName () });
        in = in.pop ();
        inExternalPE = !in.isDocument ();
        doLexicalPE = false;
    }

    private void readEncoding (boolean must)
    throws IOException, SAXException
    {
        // [81] EncName ::= [A-Za-z] ([A-Za-z0-9._] | '-')*
        String name = maybeReadAttribute ("encoding", must);

        if (name == null)
            return;
        for (int i = 0; i < name.length (); i++) {
            char c = name.charAt (i);
            if ((c >= 'A' && c <= 'Z')
                    || (c >= 'a' && c <= 'z'))
                continue;
            if (i != 0
                    && ((c >= '0' && c <= '9')
                        || c == '-'
                        || c == '_'
                        || c == '.'
                        ))
                continue;
            fatal ("P-060", new Object [] { new Character (c) });
        }

        //
        // This should be the encoding in use, and it's even an error for
        // it to be anything else (in certain cases that are impractical to
        // to test, and may even be insufficient).  So, we do the best we
        // can, and warn if things look suspicious.  Note that Java doesn't
        // uniformly expose the encodings, and that the names it uses
        // internally are nonstandard.  Also, that the XML spec allows
        // such "errors" not to be reported at all.
        //
        String  currentEncoding = in.getEncoding ();

        if (currentEncoding != null
                && !name.equalsIgnoreCase (currentEncoding))
            warning ("P-061", new Object [] { name, currentEncoding });
    }

    private boolean maybeNotationDecl ()
    throws IOException, SAXException
    {
        // [82] NotationDecl ::= '<!NOTATION' S Name S
        //              (ExternalID | PublicID) S? '>'
        // [83] PublicID ::= 'PUBLIC' S PubidLiteral
        InputEntity     start = peekDeclaration ("!NOTATION");

        if (start == null)
            return false;

        String          name = getMarkupDeclname ("F-019", false);
        ExternalEntity  entity = new ExternalEntity (in);

        whitespace ("F-011");
        if (peek ("PUBLIC")) {
            whitespace ("F-009");
            entity.publicId = parsePublicId ();
            if (maybeWhitespace ()) {
                if (!peek (">"))
                    entity.systemId = parseSystemId ();
                else 
                    ungetc ();
            }
        } else if (peek ("SYSTEM")) {
            whitespace ("F-008");
            entity.systemId = parseSystemId ();
        } else
            fatal ("P-062");
        maybeWhitespace ();
        nextChar ('>', "F-032", name);
        if (supportValidation && isValidating && start != in)
            error ("V-013", null);
        if (entity.systemId != null && entity.systemId.indexOf ('#') != -1)
            error ("P-056", new Object [] { entity.systemId });

        Object  value = notations.get (name);
        if (value != null && value instanceof ExternalEntity)
            warning ("P-063", new Object [] { name });

        // if we skipped a PE, ignore declarations since the
        // PE might have included an ovrriding declaration
        else if (!ignoreDeclarations) {
            notations.put (name, entity);
            dtdHandler.notationDecl (name, entity.publicId,
                    entity.systemId);
        }
        return true;
    }


    ////////////////////////////////////////////////////////////////
    //
    //  UTILITIES
    //
    ////////////////////////////////////////////////////////////////

    private char getc () throws IOException, SAXException
    {
        if (!(inExternalPE && doLexicalPE)) {
            char c = in.getc ();
            if (c == '%' && doLexicalPE)
                fatal ("P-080");
            return c;
        }

        //
        // External parameter entities get funky processing of '%param;'
        // references.  It's not clearly defined in the XML spec; but it
        // boils down to having those refs be _lexical_ in most cases to
        // include partial syntax productions.  It also needs selective
        // enabling; "<!ENTITY % foo ...>" must work, for example, and
        // if "bar" is an empty string PE, "ab%bar;cd" becomes "abcd"
        // if it's expanded in a literal, else "ab  cd".  PEs also do
        // not expand within comments or PIs, and external PEs are only
        // allowed to have markup decls (and so aren't handled lexically).
        //
        // This PE handling should be merged into maybeWhitespace, where
        // it can be dealt with more consistently.
        //
        // Also, there are some validity constraints in this area.
        //
        char c;

        while (in.isEOF ()) {
            if (in.isInternal () || (doLexicalPE && !in.isDocument ()))
                in = in.pop ();
            else {
                fatal ("P-064", new Object [] { in.getName () });
            }
        }
        if ((c = in.getc ()) == '%' && doLexicalPE) {
            // PE ref ::= '%' name ';'
            String      name = maybeGetName ();
            Object      entity;

            if (name == null)
                fatal ("P-011");
            nextChar (';', "F-021", name);
            entity = params.get (name);

            // push a magic "entity" before and after the
            // real one, so ungetc() behaves uniformly
            pushReader (" ".toCharArray (), null, false);
            if (entity instanceof InternalEntity)
                pushReader (((InternalEntity) entity).buf, name, false);
            else if (entity instanceof ExternalEntity)
                // PEs can't be unparsed!
                // XXX if this returns false ...
                pushReader ((ExternalEntity) entity);
            else if (entity == null)
                // see note in maybePEReference re making this be nonfatal.
                fatal ("V-022");
            else
                throw new InternalError ();
            pushReader (" ".toCharArray (), null, false);
            return in.getc ();
        }
        return c;
    }

    private void ungetc () // throws IOException, SAXException
        { in.ungetc (); }

    private boolean peek (String s) throws IOException, SAXException
        { return in.peek (s, null); }
    
    // Return the entity starting the specified declaration
    // (for validating declaration nesting) else null.
    private InputEntity peekDeclaration (String s)
    throws IOException, SAXException
    {
        InputEntity     start;

        if (!in.peekc ('<'))
            return null;
        start = in;
        if (in.peek (s, null))
            return start;
        in.ungetc ();
        return null;
    }

    private void nextChar (char c, String location, String near)
    throws IOException, SAXException
    {
        while (in.isEOF () && !in.isDocument ())
            in = in.pop ();
        if (!in.peekc (c))
            fatal ("P-008", new Object []
                { new Character (c),
                    messages.getMessage (locale, location),
                    (near == null ? "" : ('"' + near + '"'))});
    }
    
    

    private void pushReader (char buf [], String name, boolean isGeneral)
    throws SAXException
    {
        if (isGeneral && !isInAttribute) {
            lexicalHandler.startEntity(name);
        }

        InputEntity     r = InputEntity.getInputEntity (errHandler, locale);
        r.init (buf, name, in, !isGeneral);
        in = r;
    }

    // returns false if the external entity is being ignored ...
    // potentially possible in nonvalidating parsers, but not
    // currently supported.  (See notes everywhere this is called;
    // both error handling, and reporting start/stop of entity
    // expansion, are issues!  Also, SAX has no way to say "don't
    // read this entity".)

    private boolean pushReader (ExternalEntity next)
    throws SAXException, IOException
    {
        if (!next.isPE && !isInAttribute) {
            lexicalHandler.startEntity(next.name);
        }

        InputEntity     r = InputEntity.getInputEntity (errHandler, locale);
        InputSource     s = next.getInputSource (resolver);

        r.init (s, next.name, in, next.isPE);
        in = r;
        return true;
    }


    // error handling convenience routines

    private void warning (String messageId, Object parameters [])
    throws SAXException
    {
        SAXParseException       x;

        x = new SAXParseException (
            messages.getMessage (locale, messageId, parameters),
            locator);

        // continuable, minor ... "this may matter to you..."
        errHandler.warning (x);
    }

    // package private ... normally returns.
    void error (String messageId, Object parameters [])
    throws SAXException
    {
        SAXParseException       x = new SAXParseException (
            messages.getMessage (locale, messageId, parameters),
            locator);

        // continuable, major ... e.g. invalid document
        errHandler.error (x);
    }

    private void fatal (String message) throws SAXException
    {
        fatal (message, null, null);
    }

    private void fatal (String message, Object parameters [])
    throws SAXException
    {
        fatal (message, parameters, null);
    }

    private void fatal (String messageId, Object parameters [], Exception e)
    throws SAXException
    {
        SAXParseException       x = new SAXParseException (
            messages.getMessage (locale, messageId, parameters),
            locator, e);
        errHandler.fatalError (x);

        // not continuable ... e.g. basic well-formedness errors
        throw x;
    }


    //
    // LOCATOR -- used for err reporting. the app calls us,
    // we tell where the parsing current event happened.
    //
    class DocLocator implements Locator {

        public String getPublicId ()
        {
            return (in == null) ? null : in.getPublicId ();
        }

        public String getSystemId ()
        {
            return (in == null) ? null : in.getSystemId ();
        }

        public int getLineNumber ()
        {
            return (in == null) ? -1 : in.getLineNumber ();
        }

        public int getColumnNumber ()
        {
            return (in == null) ? -1 : in.getColumnNumber ();
        }
    }


    //
    // Map char arrays to strings ... cuts down both on memory and
    // CPU usage for element/attribute/other names that are reused.
    //
    // Documents typically repeat names a lot, so we more or less
    // intern all the strings within the document; since some strings
    // are repeated in multiple documents (e.g. stylesheets) we go
    // a bit further, and intern globally.
    //
    static class NameCache {
        //
        // Unless we auto-grow this, the default size should be a
        // reasonable bit larger than needed for most XML files
        // we've yet seen (and be prime).  If it's too small, the
        // penalty is just excess cache collisions.
        //
        NameCacheEntry  hashtable [] = new NameCacheEntry [541];

        //
        // Usually we just want to get the 'symbol' for these chars
        //
        String lookup (char value [], int len)
        {
            return lookupEntry (value, len).name;
        }

        //
        // Sometimes we need to scan the chars in the resulting
        // string, so there's an accessor which exposes them.
        // (Mostly for element end tags.)
        //
        NameCacheEntry lookupEntry (char value [], int len)
        {
            int                 index = 0;
            NameCacheEntry      entry;

            // hashing to get index
            for (int i = 0; i < len; i++)
                index = index * 31 + value [i];
            index &= 0x7fffffff;
            index %= hashtable.length;

            // return entry if one's there ...
            for (entry = hashtable [index];
                    entry != null;
                    entry = entry.next) {
                if (entry.matches (value, len))
                    return entry;
            }

            // else create new one
            entry = new NameCacheEntry ();
            entry.chars = new char [len];
            System.arraycopy (value, 0, entry.chars, 0, len);
            entry.name = new String (entry.chars);
                //
                // NOTE:  JDK 1.1 has a fixed size string intern table,
                // with non-GC'd entries.  It can panic here; that's a
                // JDK problem, use 1.2 or later with many identifiers.
                //
            entry.name = entry.name.intern ();          // "global" intern
            entry.next = hashtable [index];
            hashtable [index] = entry;
            return entry;
        }
    }

    static class NameCacheEntry {
        String          name;
        char            chars [];
        NameCacheEntry  next;

        boolean matches (char value [], int len)
        {
            if (chars.length != len)
                return false;
            for (int i = 0; i < len; i++)
                if (value [i] != chars [i])
                    return false;
            return true;
        }
    }

    //
    // A combined handler class that does nothing
    //
    private static class NullHandler extends DefaultHandler
        implements LexicalHandler, DeclHandler
    {
        public void startDTD (String name, String publicId, String systemId) {}
        public void endDTD () {}
        public void startEntity (String name) {}
        public void endEntity (String name) {}
        public void startCDATA () {}
        public void endCDATA () {}
        public void comment (char ch[], int start, int length) {}
        public void elementDecl (String name, String model) {}
        public void attributeDecl (String eName, String aName, String type,
                                   String valueDefault, String value) {}
        public void internalEntityDecl (String name, String value) {}
        public void externalEntityDecl (String name, String publicId,
                                        String systemId) {}
    }

    //
    // Message catalog for diagnostics.
    //
    static final Catalog messages = new Catalog();

    static final class Catalog extends MessageCatalog {
        Catalog() {
            super(Parser2.class);
        }
    }
}
