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

This file is part of GNU Classpath.

GNU Classpath is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2, or (at your option)
any later version.

GNU Classpath is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.

You should have received a copy of the GNU General Public License
along with GNU Classpath; see the file COPYING.  If not, write to the
Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
02111-1307 USA.

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

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

Partly derived from code which carried the following notice:

  Copyright (c) 1997, 1998 by Microstar Software Ltd.

  AElfred is free for both commercial and non-commercial use and
  redistribution, provided that Microstar's copyright and disclaimer are
  retained intact.  You are free to modify AElfred for your own use and
  to redistribute AElfred with your modifications, provided that the
  modifications are clearly documented.

  This program is distributed in the hope that it will be useful, but
  WITHOUT ANY WARRANTY; without even the implied warranty of
  merchantability or fitness for a particular purpose.  Please use it AT
  YOUR OWN RISK.
*/

package gnu.xml.aelfred2;

import gnu.java.security.action.GetPropertyAction;

import java.io.BufferedInputStream;
import java.io.CharConversionException;
import java.io.EOFException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLConnection;
import java.security.AccessController;

import java.util.Iterator;
import java.util.HashMap;
import java.util.LinkedList;

import org.xml.sax.InputSource;
import org.xml.sax.SAXException;


/**
 * Parse XML documents and return parse events through call-backs.
 * Use the <code>SAXDriver</code> class as your entry point, as all
 * internal parser interfaces are subject to change.
 *
 * @author Written by David Megginson &lt;dmeggins@microstar.com&gt;
 *	(version 1.2a with bugfixes)
 * @author Updated by David Brownell &lt;dbrownell@users.sourceforge.net&gt;
 * @see SAXDriver
 */
final class XmlParser
{

  // avoid slow per-character readCh()
  private final static boolean USE_CHEATS = true;

  ////////////////////////////////////////////////////////////////////////
  // Constants.
  ////////////////////////////////////////////////////////////////////////
  
  //
  // Constants for element content type.
  //
  
  /**
   * Constant: an element has not been declared.
   * @see #getElementContentType
   */
  public final static int CONTENT_UNDECLARED = 0;
  
  /**
   * Constant: the element has a content model of ANY.
   * @see #getElementContentType
   */
  public final static int CONTENT_ANY = 1;
  
  /**
   * Constant: the element has declared content of EMPTY.
   * @see #getElementContentType
   */
  public final static int CONTENT_EMPTY = 2;
  
  /**
   * Constant: the element has mixed content.
   * @see #getElementContentType
   */
  public final static int CONTENT_MIXED = 3;
  
  /**
   * Constant: the element has element content.
   * @see #getElementContentType
   */
  public final static int CONTENT_ELEMENTS = 4;
  
  
  //
  // Constants for the entity type.
  //
  
  /**
   * Constant: the entity has not been declared.
   * @see #getEntityType
   */
  public final static int ENTITY_UNDECLARED = 0;
  
  /**
   * Constant: the entity is internal.
   * @see #getEntityType
   */
  public final static int ENTITY_INTERNAL = 1;
  
  /**
   * Constant: the entity is external, non-parsable data.
   * @see #getEntityType
   */
  public final static int ENTITY_NDATA = 2;
  
  /**
   * Constant: the entity is external XML data.
   * @see #getEntityType
   */
  public final static int ENTITY_TEXT = 3;
    
  //
  // Attribute type constants are interned literal strings.
  //
  
  //
  // Constants for supported encodings.  "external" is just a flag.
  //
  private final static int ENCODING_EXTERNAL = 0;
  private final static int ENCODING_UTF_8 = 1;
  private final static int ENCODING_ISO_8859_1 = 2;
  private final static int ENCODING_UCS_2_12 = 3;
  private final static int ENCODING_UCS_2_21 = 4;
  private final static int ENCODING_UCS_4_1234 = 5;
  private final static int ENCODING_UCS_4_4321 = 6;
  private final static int ENCODING_UCS_4_2143 = 7;
  private final static int ENCODING_UCS_4_3412 = 8;
  private final static int ENCODING_ASCII = 9;
  
  //
  // Constants for attribute default value.
  //
  
  /**
   * Constant: the attribute is not declared.
   * @see #getAttributeDefaultValueType
   */
  public final static int ATTRIBUTE_DEFAULT_UNDECLARED = 30;
  
  /**
   * Constant: the attribute has a literal default value specified.
   * @see #getAttributeDefaultValueType
   * @see #getAttributeDefaultValue
   */
  public final static int ATTRIBUTE_DEFAULT_SPECIFIED = 31;
  
  /**
   * Constant: the attribute was declared #IMPLIED.
   * @see #getAttributeDefaultValueType
   */
  public final static int ATTRIBUTE_DEFAULT_IMPLIED = 32;
  
  /**
   * Constant: the attribute was declared #REQUIRED.
   * @see #getAttributeDefaultValueType
   */
  public final static int ATTRIBUTE_DEFAULT_REQUIRED = 33;
  
  /**
   * Constant: the attribute was declared #FIXED.
   * @see #getAttributeDefaultValueType
   * @see #getAttributeDefaultValue
   */
  public final static int ATTRIBUTE_DEFAULT_FIXED = 34;
    
  //
  // Constants for input.
  //
  private final static int INPUT_NONE = 0;
  private final static int INPUT_INTERNAL = 1;
  private final static int INPUT_STREAM = 3;
  private final static int INPUT_READER = 5;
  
  //
  // Flags for reading literals.
  //
  // expand general entity refs (attribute values in dtd and content)
  private final static int LIT_ENTITY_REF = 2;
  // normalize this value (space chars) (attributes, public ids)
  private final static int LIT_NORMALIZE = 4;
  // literal is an attribute value 
  private final static int LIT_ATTRIBUTE = 8;
  // don't expand parameter entities
  private final static int LIT_DISABLE_PE = 16;
  // don't expand [or parse] character refs
  private final static int LIT_DISABLE_CREF = 32;
  // don't parse general entity refs
  private final static int LIT_DISABLE_EREF = 64;
  // literal is a public ID value 
  private final static int LIT_PUBID = 256;
    
  //
  // Flags affecting PE handling in DTDs (if expandPE is true).
  // PEs expand with space padding, except inside literals.
  //
  private final static int CONTEXT_NORMAL = 0;
  private final static int CONTEXT_LITERAL = 1;
  
  // Emit warnings for relative URIs with no base URI.
  static boolean uriWarnings;
  static
  {
    String key = "gnu.xml.aelfred2.XmlParser.uriWarnings";
    GetPropertyAction a = new GetPropertyAction(key);
    uriWarnings = "true".equals(AccessController.doPrivileged(a));      
  }
    
  //
  // The current XML handler interface.
  //
  private SAXDriver handler;
  
  //
  // I/O information.
  //
  private Reader reader;   // current reader
  private InputStream is;     // current input stream
  private int line;     // current line number
  private int column;   // current column number
  private int sourceType;   // type of input source
  private LinkedList inputStack;   // stack of input soruces
  private URLConnection externalEntity; // current external entity
  private int encoding;   // current character encoding
  private int currentByteCount; // bytes read from current source
  private InputSource scratch;  // temporary
  
  //
  // Buffers for decoded but unparsed character input.
  //
  private char[] readBuffer;
  private int readBufferPos;
  private int readBufferLength;
  private int readBufferOverflow;  // overflow from last data chunk.
  
  //
  // Buffer for undecoded raw byte input.
  //
  private final static int READ_BUFFER_MAX = 16384;
  private byte[] rawReadBuffer;
  
  
  //
  // Buffer for attribute values, char refs, DTD stuff.
  //
  private static int DATA_BUFFER_INITIAL = 4096;
  private char[] dataBuffer;
  private int dataBufferPos;
  
  //
  // Buffer for parsed names.
  //
  private static int NAME_BUFFER_INITIAL = 1024;
  private char[] nameBuffer;
  private int nameBufferPos;
  
  //
  // Save any standalone flag
  //
  private boolean docIsStandalone;
  
  //
  // Hashtables for DTD information on elements, entities, and notations.
  // Populated until we start ignoring decls (because of skipping a PE)
  //
  private HashMap elementInfo;
  private HashMap entityInfo;
  private HashMap notationInfo;
  private boolean skippedPE;
  
  //
  // Element type currently in force.
  //
  private String currentElement;
  private int currentElementContent;
  
  //
  // Stack of entity names, to detect recursion.
  //
  private LinkedList entityStack;
  
  //
  // PE expansion is enabled in most chunks of the DTD, not all.
  // When it's enabled, literals are treated differently.
  //
  private boolean inLiteral;
  private boolean expandPE;
  private boolean peIsError;
  
  //
  // can't report entity expansion inside two constructs:
  // - attribute expansions (internal entities only)
  // - markup declarations (parameter entities only)
  //
  private boolean doReport;
  
  //
  // Symbol table, for caching interned names.
  //
  // These show up wherever XML names or nmtokens are used:  naming elements,
  // attributes, PIs, notations, entities, and enumerated attribute values.
  //
  // NOTE:  This hashtable doesn't grow.  The default size is intended to be
  // rather large for most documents.  Example:  one snapshot of the DocBook
  // XML 4.1 DTD used only about 350 such names.  As a rule, only pathological
  // documents (ones that don't reuse names) should ever see much collision.
  //
  // Be sure that SYMBOL_TABLE_LENGTH always stays prime, for best hashing.
  // "2039" keeps the hash table size at about two memory pages on typical
  // 32 bit hardware.
  //
  private final static int SYMBOL_TABLE_LENGTH = 2039;
  
  private Object[][] symbolTable;
  
  //
  // Hash table of attributes found in current start tag.
  //
  private String[] tagAttributes;
  private int tagAttributePos;
  
  //
  // Utility flag: have we noticed a CR while reading the last
  // data chunk?  If so, we will have to go back and normalise
  // CR or CR/LF line ends.
  //
  private boolean sawCR;
  
  //
  // Utility flag: are we in CDATA?  If so, whitespace isn't ignorable.
  // 
  private boolean inCDATA;
  
  //
  // Xml version.
  //  
  private static final int XML_10 = 0; 
  private static final int XML_11 = 1; 
  private int xmlVersion = XML_10;

  //////////////////////////////////////////////////////////////////////
  // Constructors.
  ////////////////////////////////////////////////////////////////////////
  
  /**
   * Construct a new parser with no associated handler.
   * @see #setHandler
   * @see #parse
   */
  // package private
  XmlParser()
  {
  }

  /**
   * Set the handler that will receive parsing events.
   * @param handler The handler to receive callback events.
   * @see #parse
   */
  // package private
  void setHandler(SAXDriver handler)
  {
    this.handler = handler;
  }

  /**
   * Parse an XML document from the character stream, byte stream, or URI
   * that you provide (in that order of preference).  Any URI that you
   * supply will become the base URI for resolving relative URI, and may
   * be used to acquire a reader or byte stream.
   *
   * <p> Only one thread at a time may use this parser; since it is
   * private to this package, post-parse cleanup is done by the caller,
   * which MUST NOT REUSE the parser (just null it).
   *
   * @param systemId Absolute URI of the document; should never be null,
   *	but may be so iff a reader <em>or</em> a stream is provided.
   * @param publicId The public identifier of the document, or null.
   * @param reader A character stream; must be null if stream isn't.
   * @param stream A byte input stream; must be null if reader isn't.
   * @param encoding The suggested encoding, or null if unknown.
   * @exception java.lang.Exception Basically SAXException or IOException
   */
  // package private 
  void doParse(String systemId, String publicId, Reader reader,
               InputStream stream, String encoding)
    throws Exception
  {
    if (handler == null)
      {
        throw new IllegalStateException("no callback handler");
      }

    initializeVariables();

    // predeclare the built-in entities here (replacement texts)
    // we don't need to intern(), since we're guaranteed literals
    // are always (globally) interned.
    setInternalEntity("amp", "&#38;");
    setInternalEntity("lt", "&#60;");
    setInternalEntity("gt", "&#62;");
    setInternalEntity("apos", "&#39;");
    setInternalEntity("quot", "&#34;");

    try
      {
        // pushURL first to ensure locator is correct in startDocument
        // ... it might report an IO or encoding exception.
        handler.startDocument();
        pushURL(false, "[document]",
                // default baseURI: null
                new ExternalIdentifiers(publicId, systemId, null),
                reader, stream, encoding, false);
        
        parseDocument();
      }
    catch (EOFException e)
      {
        //empty input
        error("empty document, with no root element.");
      }
    finally
      {
        if (reader != null)
          {
            try
              {
                reader.close();
              }
            catch (IOException e)
              {
                /* ignore */
              }
          }
        if (stream != null)
          {
            try
              {
                stream.close();
              }
            catch (IOException e)
              {
                /* ignore */
              }
          }
        if (is != null)
          {
            try
              {
                is.close();
              }
            catch (IOException e)
              {
                /* ignore */
              }
          }
        scratch = null;
      }
  }

  //////////////////////////////////////////////////////////////////////
  // Error reporting.
  //////////////////////////////////////////////////////////////////////
    
  /**
   * Report an error.
   * @param message The error message.
   * @param textFound The text that caused the error (or null).
   * @see SAXDriver#error
   * @see #line
   */
  private void error(String message, String textFound, String textExpected)
    throws SAXException
  {
    if (textFound != null)
      {
        message = message + " (found \"" + textFound + "\")";
      }
    if (textExpected != null)
      {
        message = message + " (expected \"" + textExpected + "\")";
      }
    handler.fatal(message);
    
    // "can't happen"
    throw new SAXException(message);
  }

  /**
   * Report a serious error.
   * @param message The error message.
   * @param textFound The text that caused the error (or null).
   */
  private void error(String message, char textFound, String textExpected)
    throws SAXException
  {
    error(message, new Character(textFound).toString(), textExpected);
  }

  /**
   * Report typical case fatal errors.
   */
  private void error(String message)
    throws SAXException
  {
    handler.fatal(message);
  }

  //////////////////////////////////////////////////////////////////////
  // Major syntactic productions.
  //////////////////////////////////////////////////////////////////////

  /**
   * Parse an XML document.
   * <pre>
   * [1] document ::= prolog element Misc*
   * </pre>
   * <p>This is the top-level parsing function for a single XML
   * document.  As a minimum, a well-formed document must have
   * a document element, and a valid document must have a prolog
   * (one with doctype) as well.
   */
  private void parseDocument()
    throws Exception
  {
    try
      {                                       // added by MHK
        boolean sawDTD = parseProlog();
        require('<');
        parseElement(!sawDTD);
      }
    catch (EOFException ee)
      {                 // added by MHK
        error("premature end of file", "[EOF]", null);
      }
    
    try
      {
        parseMisc();   //skip all white, PIs, and comments
        char c = readCh();    //if this doesn't throw an exception...
        error("unexpected characters after document end", c, null);
      }
    catch (EOFException e)
      {
        return;
      }
  }
  
  static final char[] startDelimComment = { '<', '!', '-', '-' };
  static final char[] endDelimComment = { '-', '-' };

  /**
   * Skip a comment.
   * <pre>
   * [15] Comment ::= '&lt;!--' ((Char - '-') | ('-' (Char - '-')))* "-->"
   * </pre>
   * <p> (The <code>&lt;!--</code> has already been read.)
   */
  private void parseComment()
    throws Exception
  {
    char c;
    boolean saved = expandPE;
    
    expandPE = false;
    parseUntil(endDelimComment);
    require('>');
    expandPE = saved;
    handler.comment(dataBuffer, 0, dataBufferPos);
    dataBufferPos = 0;
  }
  
  static final char[] startDelimPI = { '<', '?' };
  static final char[] endDelimPI = { '?', '>' };

  /**
   * Parse a processing instruction and do a call-back.
   * <pre>
   * [16] PI ::= '&lt;?' PITarget
   *    (S (Char* - (Char* '?&gt;' Char*)))?
   *    '?&gt;'
   * [17] PITarget ::= Name - ( ('X'|'x') ('M'|m') ('L'|l') )
   * </pre>
   * <p> (The <code>&lt;?</code> has already been read.)
   */
  private void parsePI()
    throws SAXException, IOException
  {
    String name;
    boolean saved = expandPE;
    
    expandPE = false;
    name = readNmtoken(true);
    //NE08
    if (name.indexOf(':') >= 0)
      {
        error("Illegal character(':') in processing instruction name ",
              name, null);
      }
    if ("xml".equalsIgnoreCase(name))
      {
        error("Illegal processing instruction target", name, null);
      }
    if (!tryRead(endDelimPI))
      {
        requireWhitespace();
        parseUntil(endDelimPI);
      }
    expandPE = saved;
    handler.processingInstruction(name, dataBufferToString());
  }
  
  static final char[] endDelimCDATA = { ']', ']', '>' };

  private boolean isDirtyCurrentElement;

  /**
   * Parse a CDATA section.
   * <pre>
   * [18] CDSect ::= CDStart CData CDEnd
   * [19] CDStart ::= '&lt;![CDATA['
   * [20] CData ::= (Char* - (Char* ']]&gt;' Char*))
   * [21] CDEnd ::= ']]&gt;'
   * </pre>
   * <p> (The '&lt;![CDATA[' has already been read.)
   */
  private void parseCDSect()
    throws Exception
  {
    parseUntil(endDelimCDATA);
    dataBufferFlush();
  }

  /**
   * Parse the prolog of an XML document.
   * <pre>
   * [22] prolog ::= XMLDecl? Misc* (Doctypedecl Misc*)?
   * </pre>
   * <p>We do not look for the XML declaration here, because it was
   * handled by pushURL ().
   * @see pushURL
   * @return true if a DTD was read.
   */
  private boolean parseProlog()
    throws Exception
  {
    parseMisc();

    if (tryRead("<!DOCTYPE"))
      {
        parseDoctypedecl();
        parseMisc();
        return true;
      }
    return false;
  }

  private void checkLegalVersion(String version)
    throws SAXException
  {
    int len = version.length();
    for (int i = 0; i < len; i++)
      {
        char c = version.charAt(i);
        if ('0' <= c && c <= '9')
          {
            continue;
          }
        if (c == '_' || c == '.' || c == ':' || c == '-')
          {
            continue;
          }
        if ('a' <= c && c <= 'z')
          {
            continue;
          }
        if ('A' <= c && c <= 'Z')
          {
            continue;
          }
        error ("illegal character in version", version, "1.0");
      }
  }

  /**
   * Parse the XML declaration.
   * <pre>
   * [23] XMLDecl ::= '&lt;?xml' VersionInfo EncodingDecl? SDDecl? S? '?&gt;'
   * [24] VersionInfo ::= S 'version' Eq
   *    ("'" VersionNum "'" | '"' VersionNum '"' )
   * [26] VersionNum ::= ([a-zA-Z0-9_.:] | '-')*
   * [32] SDDecl ::= S 'standalone' Eq
   *    ( "'"" ('yes' | 'no') "'"" | '"' ("yes" | "no") '"' )
   * [80] EncodingDecl ::= S 'encoding' Eq
   *    ( "'" EncName "'" | "'" EncName "'" )
   * [81] EncName ::= [A-Za-z] ([A-Za-z0-9._] | '-')*
   * </pre>
   * <p> (The <code>&lt;?xml</code> and whitespace have already been read.)
   * @return the encoding in the declaration, uppercased; or null
   * @see #parseTextDecl
   * @see #setupDecoding
   */
  private String parseXMLDecl(boolean ignoreEncoding)
    throws SAXException, IOException
  {
    String version;
    String encodingName = null;
    String standalone = null;
    int flags = LIT_DISABLE_CREF | LIT_DISABLE_PE | LIT_DISABLE_EREF;
    String inputEncoding = null;
        
    switch (this.encoding)
      {
      case ENCODING_EXTERNAL:
      case ENCODING_UTF_8:
        inputEncoding = "UTF-8";
        break;
      case ENCODING_ISO_8859_1:
        inputEncoding = "ISO-8859-1";
        break;
      case ENCODING_UCS_2_12:
        inputEncoding = "UTF-16BE";
        break;
      case ENCODING_UCS_2_21:
        inputEncoding = "UTF-16LE";
        break;
      }
    
    // Read the version.
    require("version");
    parseEq();
    checkLegalVersion(version = readLiteral(flags));
    if (!version.equals("1.0"))
      {
        if (version.equals("1.1"))
          {
            handler.warn("expected XML version 1.0, not: " + version);
            xmlVersion = XML_11;
          }
        else
          {
            error("illegal XML version", version, "1.0 or 1.1");
          }
      }
    else
      {
        xmlVersion = XML_10;
      }
    // Try reading an encoding declaration.
    boolean white = tryWhitespace();
    
    if (tryRead("encoding"))
      {
        if (!white)
          {
            error("whitespace required before 'encoding='");
          }
        parseEq();
        encodingName = readLiteral(flags);
        if (!ignoreEncoding)
          {
            setupDecoding(encodingName);
          }
      }
    
    // Try reading a standalone declaration
    if (encodingName != null)
      {
        white = tryWhitespace();
      }
    if (tryRead("standalone"))
      {
        if (!white)
          {
            error("whitespace required before 'standalone='");
          }
        parseEq();
        standalone = readLiteral(flags);
        if ("yes".equals(standalone))
          {
            docIsStandalone = true;
          }
        else if (!"no".equals(standalone))
          {
            error("standalone flag must be 'yes' or 'no'");
          }
      }

    skipWhitespace();
    require("?>");

    if (inputEncoding == null)
      {
        inputEncoding = encodingName;
      }
    handler.xmlDecl(version, encodingName, docIsStandalone,
                    inputEncoding);
    
    return encodingName;
  }

  /**
   * Parse a text declaration.
   * <pre>
   * [79] TextDecl ::= '&lt;?xml' VersionInfo? EncodingDecl S? '?&gt;'
   * [80] EncodingDecl ::= S 'encoding' Eq
   *    ( '"' EncName '"' | "'" EncName "'" )
   * [81] EncName ::= [A-Za-z] ([A-Za-z0-9._] | '-')*
   * </pre>
   * <p> (The <code>&lt;?xml</code>' and whitespace have already been read.)
   * @return the encoding in the declaration, uppercased; or null
   * @see #parseXMLDecl
   * @see #setupDecoding
   */
  private String parseTextDecl(boolean ignoreEncoding)
    throws SAXException, IOException
  {
    String encodingName = null;
    int flags = LIT_DISABLE_CREF | LIT_DISABLE_PE | LIT_DISABLE_EREF;

    // Read an optional version.
    if (tryRead ("version"))
      {
        String version;
        parseEq();
        checkLegalVersion(version = readLiteral(flags));
        
        if (version.equals("1.1"))
          {
            if (xmlVersion == XML_10)
              {
                error("external subset has later version number.", "1.0",
                      version);    
              }
            handler.warn("expected XML version 1.0, not: " + version);
            xmlVersion = XML_11;
          }
        else if (!version.equals("1.0"))
          {
            error("illegal XML version", version, "1.0 or 1.1");
          }
        requireWhitespace();
      }
    
    // Read the encoding.
    require("encoding");
    parseEq();
    encodingName = readLiteral(flags);
    if (!ignoreEncoding)
      {
        setupDecoding(encodingName);
      }
    skipWhitespace();
    require("?>");
    
    return encodingName;
  }

  /**
   * Sets up internal state so that we can decode an entity using the
   * specified encoding.  This is used when we start to read an entity
   * and we have been given knowledge of its encoding before we start to
   * read any data (e.g. from a SAX input source or from a MIME type).
   *
   * <p> It is also used after autodetection, at which point only very
   * limited adjustments to the encoding may be used (switching between
   * related builtin decoders).
   *
   * @param encodingName The name of the encoding specified by the user.
   * @exception IOException if the encoding isn't supported either
   *  internally to this parser, or by the hosting JVM.
   * @see #parseXMLDecl
   * @see #parseTextDecl
     */
  private void setupDecoding(String encodingName)
    throws SAXException, IOException
  {
    encodingName = encodingName.toUpperCase();
    
    // ENCODING_EXTERNAL indicates an encoding that wasn't
    // autodetected ... we can use builtin decoders, or
    // ones from the JVM (InputStreamReader).
    
    // Otherwise we can only tweak what was autodetected, and
    // only for single byte (ASCII derived) builtin encodings.
    
    // ASCII-derived encodings
    if (encoding == ENCODING_UTF_8 || encoding == ENCODING_EXTERNAL)
      {
        if (encodingName.equals("ISO-8859-1")
            || encodingName.equals("8859_1")
            || encodingName.equals("ISO8859_1"))
          {
            encoding = ENCODING_ISO_8859_1;
            return;
          }
        else if (encodingName.equals("US-ASCII")
                 || encodingName.equals("ASCII"))
          {
            encoding = ENCODING_ASCII;
            return;
          }
        else if (encodingName.equals("UTF-8")
                 || encodingName.equals("UTF8"))
          {
            encoding = ENCODING_UTF_8;
            return;
          }
        else if (encoding != ENCODING_EXTERNAL)
          {
            // used to start with a new reader ...
            throw new UnsupportedEncodingException(encodingName);
          }
        // else fallthrough ...
        // it's ASCII-ish and something other than a builtin
      }
    
    // Unicode and such
    if (encoding == ENCODING_UCS_2_12 || encoding == ENCODING_UCS_2_21)
      {
        if (!(encodingName.equals("ISO-10646-UCS-2")
              || encodingName.equals("UTF-16")
              || encodingName.equals("UTF-16BE")
              || encodingName.equals("UTF-16LE")))
          {
            error("unsupported Unicode encoding", encodingName, "UTF-16");
          }
        return;
      }
    
    // four byte encodings
    if (encoding == ENCODING_UCS_4_1234
        || encoding == ENCODING_UCS_4_4321
        || encoding == ENCODING_UCS_4_2143
        || encoding == ENCODING_UCS_4_3412)
      {
        // Strictly:  "UCS-4" == "UTF-32BE"; also, "UTF-32LE" exists
        if (!encodingName.equals("ISO-10646-UCS-4"))
          {
            error("unsupported 32-bit encoding", encodingName,
                  "ISO-10646-UCS-4");
          }
        return;
      }
    
    // assert encoding == ENCODING_EXTERNAL
    // if (encoding != ENCODING_EXTERNAL)
    //     throw new RuntimeException ("encoding = " + encoding);
    
    if (encodingName.equals("UTF-16BE"))
      {
        encoding = ENCODING_UCS_2_12;
        return;
      }
    if (encodingName.equals("UTF-16LE"))
      {
        encoding = ENCODING_UCS_2_21;
        return;
      }
    
    // We couldn't use the builtin decoders at all.  But we can try to
    // create a reader, since we haven't messed up buffering.  Tweak
    // the encoding name if necessary.
    
    if (encodingName.equals("UTF-16")
        || encodingName.equals("ISO-10646-UCS-2"))
      {
        encodingName = "Unicode";
      }
    // Ignoring all the EBCDIC aliases here
    
    reader = new InputStreamReader(is, encodingName);
    sourceType = INPUT_READER;
  }
  
  /**
   * Parse miscellaneous markup outside the document element and DOCTYPE
   * declaration.
   * <pre>
   * [27] Misc ::= Comment | PI | S
   * </pre>
   */
  private void parseMisc()
    throws Exception
  {
    while (true)
      {
        skipWhitespace();
        if (tryRead(startDelimPI))
          {
            parsePI();
          }
        else if (tryRead(startDelimComment))
          {
            parseComment();
          }
        else
          {
            return;
          }
      }
  }

  /**
   * Parse a document type declaration.
   * <pre>
   * [28] doctypedecl ::= '&lt;!DOCTYPE' S Name (S ExternalID)? S?
   *    ('[' (markupdecl | PEReference | S)* ']' S?)? '&gt;'
   * </pre>
   * <p> (The <code>&lt;!DOCTYPE</code> has already been read.)
   */
  private void parseDoctypedecl()
    throws Exception
  {
    String rootName;
    ExternalIdentifiers ids;

    // Read the document type name.
    requireWhitespace();
    rootName = readNmtoken(true);

    // Read the External subset's IDs
    skipWhitespace();
    ids = readExternalIds(false, true);

    // report (a) declaration of name, (b) lexical info (ids)
    handler.doctypeDecl(rootName, ids.publicId, ids.systemId);
    
    // Internal subset is parsed first, if present
    skipWhitespace();
    if (tryRead('['))
      {
        
        // loop until the subset ends
        while (true)
          {
            doReport = expandPE = true;
            skipWhitespace();
            doReport = expandPE = false;
            if (tryRead(']'))
              {
                break;     // end of subset
              }
            else
              {
                // WFC, PEs in internal subset (only between decls)
                peIsError = expandPE = true;
                parseMarkupdecl();
                peIsError = expandPE = false;
              }
          }
      }
    skipWhitespace();
    require('>');
    
    // Read the external subset, if any
    InputSource subset;
    
    if (ids.systemId == null)
      {
        subset = handler.getExternalSubset(rootName,
                                           handler.getSystemId());
      }
    else
      {
        subset = null;
      }
    if (ids.systemId != null || subset != null)
      {
        pushString(null, ">");
      
        // NOTE:  [dtd] is so we say what SAX2 expects,
        // though it's misleading (subset, not entire dtd)
        if (ids.systemId != null)
          {
            pushURL(true, "[dtd]", ids, null, null, null, true);
          }
        else
          {
            handler.warn("modifying document by adding external subset");
            pushURL(true, "[dtd]",
                    new ExternalIdentifiers(subset.getPublicId(),
                                            subset.getSystemId(),
                                            null),
                    subset.getCharacterStream(),
                    subset.getByteStream(),
                    subset.getEncoding(),
                    false);
          }
        
        // Loop until we end up back at '>'
        while (true)
          {
            doReport = expandPE = true;
            skipWhitespace();
            doReport = expandPE = false;
            if (tryRead('>'))
              {
                break;
              }
            else
              {
                expandPE = true;
                parseMarkupdecl();
                expandPE = false;
              }
          }
        
        // the ">" string isn't popped yet
        if (inputStack.size() != 1)
          {
            error("external subset has unmatched '>'");
          }
      }
    
    // done dtd
    handler.endDoctype();
    expandPE = false;
    doReport = true;
  }
  
  /**
   * Parse a markup declaration in the internal or external DTD subset.
   * <pre>
   * [29] markupdecl ::= elementdecl | Attlistdecl | EntityDecl
   *    | NotationDecl | PI | Comment
   * [30] extSubsetDecl ::= (markupdecl | conditionalSect
   *    | PEReference | S) *
   * </pre>
   * <p> Reading toplevel PE references is handled as a lexical issue
   * by the caller, as is whitespace.
   */
  private void parseMarkupdecl()
    throws Exception
  {
    char[] saved = null;
    boolean savedPE = expandPE;

    // prevent "<%foo;" and ensures saved entity is right
    require('<');
    unread('<');
    expandPE = false;
    
    if (tryRead("<!ELEMENT"))
      {
        saved = readBuffer;
        expandPE = savedPE;
        parseElementDecl();
      }
    else if (tryRead("<!ATTLIST"))
      {
        saved = readBuffer;
        expandPE = savedPE;
        parseAttlistDecl();
      }
    else if (tryRead("<!ENTITY"))
      {
        saved = readBuffer;
        expandPE = savedPE;
        parseEntityDecl();
      }
    else if (tryRead("<!NOTATION"))
      {
        saved = readBuffer;
        expandPE = savedPE;
        parseNotationDecl();
      }
    else if (tryRead(startDelimPI))
      {
        saved = readBuffer;
        expandPE = savedPE;
        parsePI();
      }
    else if (tryRead(startDelimComment))
      {
        saved = readBuffer;
        expandPE = savedPE;
        parseComment();
      }
    else if (tryRead("<!["))
      {
        saved = readBuffer;
        expandPE = savedPE;
        if (inputStack.size() > 0)
          {
            parseConditionalSect(saved);
          }
        else
          {
            error("conditional sections illegal in internal subset");
          }
      }
    else
      {
        error("expected markup declaration");
      }

    // VC: Proper Decl/PE Nesting
    if (readBuffer != saved)
      {
        handler.verror("Illegal Declaration/PE nesting");
      }
  }
  
  /**
   * Parse an element, with its tags.
   * <pre>
   * [39] element ::= EmptyElementTag | STag content ETag
   * [40] STag ::= '&lt;' Name (S Attribute)* S? '&gt;'
   * [44] EmptyElementTag ::= '&lt;' Name (S Attribute)* S? '/&gt;'
   * </pre>
   * <p> (The '&lt;' has already been read.)
   * <p>NOTE: this method actually chains onto parseContent (), if necessary,
   * and parseContent () will take care of calling parseETag ().
   */
  private void parseElement(boolean maybeGetSubset)
    throws Exception
  {
    String gi;
    char c;
    int oldElementContent = currentElementContent;
    String oldElement = currentElement;
    ElementDecl element;

    // This is the (global) counter for the
    // array of specified attributes.
    tagAttributePos = 0;
    
    // Read the element type name.
    gi = readNmtoken(true);
    
    // If we saw no DTD, and this is the document root element,
    // let the application modify the input stream by providing one.
    if (maybeGetSubset)
      {
        InputSource subset = handler.getExternalSubset(gi,
                                                       handler.getSystemId());
        if (subset != null)
          {
            String publicId = subset.getPublicId();
            String systemId = subset.getSystemId();
            
            handler.warn("modifying document by adding DTD");
            handler.doctypeDecl(gi, publicId, systemId);
            pushString(null, ">");
            
            // NOTE:  [dtd] is so we say what SAX2 expects,
            // though it's misleading (subset, not entire dtd)
            pushURL(true, "[dtd]",
                    new ExternalIdentifiers(publicId, systemId, null),
                    subset.getCharacterStream(),
                    subset.getByteStream(),
                    subset.getEncoding(),
                    false);
            
            // Loop until we end up back at '>'
            while (true)
              {
                doReport = expandPE = true;
                skipWhitespace();
                doReport = expandPE = false;
                if (tryRead('>'))
                  {
                    break;
                  }
                else
                  {
                    expandPE = true;
                    parseMarkupdecl();
                    expandPE = false;
                  }
              }
            
            // the ">" string isn't popped yet
            if (inputStack.size() != 1)
              {
                error("external subset has unmatched '>'");
              }
            
            handler.endDoctype();
          }
      }
    
    // Determine the current content type.
    currentElement = gi;
    element = (ElementDecl) elementInfo.get(gi);
    currentElementContent = getContentType(element, CONTENT_ANY);

    // Read the attributes, if any.
    // After this loop, "c" is the closing delimiter.
    boolean white = tryWhitespace();
    c = readCh();
    while (c != '/' && c != '>')
      {
        unread(c);
        if (!white)
          {
            error("need whitespace between attributes");
          }
        parseAttribute(gi);
        white = tryWhitespace();
        c = readCh();
      }
    
    // Supply any defaulted attributes.
    Iterator atts = declaredAttributes(element);
    if (atts != null)
      {
        String aname;
loop:
        while (atts.hasNext())
          {
            aname = (String) atts.next();
            // See if it was specified.
            for (int i = 0; i < tagAttributePos; i++)
              {
                if (tagAttributes[i] == aname)
                  {
                    continue loop;
                  }
              }
            // ... or has a default
            String value = getAttributeDefaultValue(gi, aname);
            
            if (value == null)
              {
                continue;
              }
            handler.attribute(aname, value, false);
          }
      }

    // Figure out if this is a start tag
    // or an empty element, and dispatch an
    // event accordingly.
    switch (c)
      {
      case '>':
        handler.startElement(gi);
        parseContent();
        break;
      case '/':
        require('>');
        handler.startElement(gi);
        handler.endElement(gi);
        break;
      }

    // Restore the previous state.
    currentElement = oldElement;
    currentElementContent = oldElementContent;
  }
    
  /**
   * Parse an attribute assignment.
   * <pre>
   * [41] Attribute ::= Name Eq AttValue
   * </pre>
   * @param name The name of the attribute's element.
   * @see SAXDriver#attribute
   */
  private void parseAttribute(String name)
    throws Exception
  {
    String aname;
    String type;
    String value;
    int flags = LIT_ATTRIBUTE |  LIT_ENTITY_REF;
    
    // Read the attribute name.
    aname = readNmtoken(true);
    type = getAttributeType(name, aname);
    
    // Parse '='
    parseEq();

    // Read the value, normalizing whitespace
    // unless it is CDATA.
    if (handler.stringInterning)
      {
        if (type == "CDATA" || type == null)
          {
            value = readLiteral(flags);
          }
        else
          {
            value = readLiteral(flags | LIT_NORMALIZE);
          }
      }
    else
      {
        if (type.equals("CDATA") || type == null)
          {
            value = readLiteral(flags);
          }
        else
          {
            value = readLiteral(flags | LIT_NORMALIZE);
          }
      }

    // WFC: no duplicate attributes
    for (int i = 0; i < tagAttributePos; i++)
      {
        if (aname.equals(tagAttributes [i]))
          {
            error("duplicate attribute", aname, null);
          }
      }

    // Inform the handler about the
    // attribute.
    handler.attribute(aname, value, true);
    dataBufferPos = 0;
    
    // Note that the attribute has been
    // specified.
    if (tagAttributePos == tagAttributes.length)
      {
        String newAttrib[] = new String[tagAttributes.length * 2];
        System.arraycopy(tagAttributes, 0, newAttrib, 0, tagAttributePos);
        tagAttributes = newAttrib;
      }
    tagAttributes[tagAttributePos++] = aname;
  }

  /**
   * Parse an equals sign surrounded by optional whitespace.
   * <pre>
   * [25] Eq ::= S? '=' S?
   * </pre>
   */
  private void parseEq()
    throws SAXException, IOException
  {
    skipWhitespace();
    require('=');
    skipWhitespace();
  }

  /**
   * Parse an end tag.
   * <pre>
   * [42] ETag ::= '</' Name S? '>'
   * </pre>
   * <p>NOTE: parseContent () chains to here, we already read the
   * "&lt;/".
   */
  private void parseETag()
    throws Exception
  {
    require(currentElement);
    skipWhitespace();
    require('>');
    handler.endElement(currentElement);
    // not re-reporting any SAXException re bogus end tags,
    // even though that diagnostic might be clearer ...
  }
  
  /**
   * Parse the content of an element.
   * <pre>
   * [43] content ::= (element | CharData | Reference
   *    | CDSect | PI | Comment)*
   * [67] Reference ::= EntityRef | CharRef
   * </pre>
   * <p> NOTE: consumes ETtag.
   */
  private void parseContent()
    throws Exception
  {
    char c;
    
    while (true)
      {
        // consume characters (or ignorable whitspace) until delimiter
        parseCharData();

        // Handle delimiters
        c = readCh();
        switch (c)
          {
          case '&':       // Found "&"
            c = readCh();
            if (c == '#')
              {
                parseCharRef();
              }
            else
              {
                unread(c);
                parseEntityRef(true);
              }
            isDirtyCurrentElement = true;
            break;
            
          case '<':       // Found "<"
            dataBufferFlush();
            c = readCh();
            switch (c)
              {
              case '!':       // Found "<!"
                c = readCh();
                switch (c)
                  {
                  case '-':     // Found "<!-"
                    require('-');
                    isDirtyCurrentElement = false;
                    parseComment();
                    break;
                  case '[':     // Found "<!["
                    isDirtyCurrentElement = false;
                    require("CDATA[");
                    handler.startCDATA();
                    inCDATA = true;
                    parseCDSect();
                    inCDATA = false;
                    handler.endCDATA();
                    break;
                  default:
                    error("expected comment or CDATA section", c, null);
                    break;
                  }
                break;
              
              case '?':     // Found "<?"
                isDirtyCurrentElement = false;
                parsePI();
                break;
                
              case '/':     // Found "</"
                isDirtyCurrentElement = false;
                parseETag();
                return;
                
              default:     // Found "<" followed by something else
                isDirtyCurrentElement = false;
                unread(c);
                parseElement(false);
                break;
              }
          }
      }
  }
  
  /**
   * Parse an element type declaration.
   * <pre>
   * [45] elementdecl ::= '&lt;!ELEMENT' S Name S contentspec S? '&gt;'
   * </pre>
   * <p> NOTE: the '&lt;!ELEMENT' has already been read.
   */
  private void parseElementDecl()
    throws Exception
  {
    String name;
    
    requireWhitespace();
    // Read the element type name.
    name = readNmtoken(true);

    requireWhitespace();
    // Read the content model.
    parseContentspec(name);
    
    skipWhitespace();
    require('>');
  }

  /**
   * Content specification.
   * <pre>
   * [46] contentspec ::= 'EMPTY' | 'ANY' | Mixed | elements
   * </pre>
   */
  private void parseContentspec(String name)
    throws Exception
  {
    // FIXME: move elementDecl() into setElement(), pass EMTPY/ANY ...
    if (tryRead("EMPTY"))
      {
        setElement(name, CONTENT_EMPTY, null, null);
        if (!skippedPE)
          {
            handler.getDeclHandler().elementDecl(name, "EMPTY");
          }
        return;
      }
    else if (tryRead("ANY"))
      {
        setElement(name, CONTENT_ANY, null, null);
        if (!skippedPE)
          {
            handler.getDeclHandler().elementDecl(name, "ANY");
          }
        return;
      }
    else
      {
        String model;
        char[] saved; 
        
        require('(');
        saved = readBuffer;
        dataBufferAppend('(');
        skipWhitespace();
        if (tryRead("#PCDATA"))
          {
            dataBufferAppend("#PCDATA");
            parseMixed(saved);
            model = dataBufferToString();
            setElement(name, CONTENT_MIXED, model, null);
          }
        else
          {
            parseElements(saved);
            model = dataBufferToString();
            setElement(name, CONTENT_ELEMENTS, model, null);
          }
        if (!skippedPE)
          {
            handler.getDeclHandler().elementDecl(name, model);
          }
      }
  }
  
  /**
   * Parse an element-content model.
   * <pre>
   * [47] elements ::= (choice | seq) ('?' | '*' | '+')?
   * [49] choice ::= '(' S? cp (S? '|' S? cp)+ S? ')'
   * [50] seq ::= '(' S? cp (S? ',' S? cp)* S? ')'
   * </pre>
   *
   * <p> NOTE: the opening '(' and S have already been read.
   *
   * @param saved Buffer for entity that should have the terminal ')'
   */
  private void parseElements(char[] saved)
    throws Exception
  {
    char c;
    char sep;
    
    // Parse the first content particle
    skipWhitespace();
    parseCp();
    
    // Check for end or for a separator.
    skipWhitespace();
    c = readCh();
    switch (c)
      {
      case ')':
        // VC: Proper Group/PE Nesting
        if (readBuffer != saved)
          {
            handler.verror("Illegal Group/PE nesting");
          }
        
        dataBufferAppend(')');
        c = readCh();
        switch (c)
          {
          case '*':
          case '+':
          case '?':
            dataBufferAppend(c);
            break;
          default:
            unread(c);
          }
        return;
      case ',':       // Register the separator.
      case '|':
        sep = c;
        dataBufferAppend(c);
        break;
      default:
        error("bad separator in content model", c, null);
        return;
      }
    
    // Parse the rest of the content model.
    while (true)
      {
        skipWhitespace();
        parseCp();
        skipWhitespace();
        c = readCh();
        if (c == ')')
          {
            // VC: Proper Group/PE Nesting
            if (readBuffer != saved)
              {
                handler.verror("Illegal Group/PE nesting");
              }
            
            dataBufferAppend(')');
            break;
          }
        else if (c != sep)
          {
            error("bad separator in content model", c, null);
            return;
          }
        else
          {
            dataBufferAppend(c);
          }
      }
    
    // Check for the occurrence indicator.
    c = readCh();
    switch (c)
      {
      case '?':
      case '*':
      case '+':
        dataBufferAppend(c);
        return;
      default:
        unread(c);
        return;
      }
  }
   
  /**
   * Parse a content particle.
   * <pre>
   * [48] cp ::= (Name | choice | seq) ('?' | '*' | '+')?
   * </pre>
   */
  private void parseCp()
    throws Exception
  {
    if (tryRead('('))
      {
        dataBufferAppend('(');
        parseElements(readBuffer);
      }
    else
      {
        dataBufferAppend(readNmtoken(true));
        char c = readCh();
        switch (c)
          {
          case '?':
          case '*':
          case '+':
            dataBufferAppend(c);
            break;
          default:
            unread(c);
            break;
          }
      }
  }

  /**
   * Parse mixed content.
   * <pre>
   * [51] Mixed ::= '(' S? ( '#PCDATA' (S? '|' S? Name)*) S? ')*'
   *        | '(' S? ('#PCDATA') S? ')'
   * </pre>
   *
   * @param saved Buffer for entity that should have the terminal ')'
   */
  private void parseMixed(char[] saved)
    throws Exception
  {
    // Check for PCDATA alone.
    skipWhitespace();
    if (tryRead(')'))
      {
        // VC: Proper Group/PE Nesting
        if (readBuffer != saved)
          {
            handler.verror("Illegal Group/PE nesting");
          }
        
        dataBufferAppend(")*");
        tryRead('*');
        return;
      }
    
    // Parse mixed content.
    skipWhitespace();
    while (!tryRead(")"))
      {
        require('|');
        dataBufferAppend('|');
        skipWhitespace();
        dataBufferAppend(readNmtoken(true));
        skipWhitespace();
      }
    
    // VC: Proper Group/PE Nesting
    if (readBuffer != saved)
      {
        handler.verror("Illegal Group/PE nesting");
      }
    
    require('*');
    dataBufferAppend(")*");
  }
  
  /**
   * Parse an attribute list declaration.
   * <pre>
   * [52] AttlistDecl ::= '&lt;!ATTLIST' S Name AttDef* S? '&gt;'
   * </pre>
   * <p>NOTE: the '&lt;!ATTLIST' has already been read.
   */
  private void parseAttlistDecl()
    throws Exception
  {
    String elementName;
    
    requireWhitespace();
    elementName = readNmtoken(true);
    boolean white = tryWhitespace();
    while (!tryRead('>'))
      {
        if (!white)
          {
            error("whitespace required before attribute definition");
          }
        parseAttDef(elementName);
        white = tryWhitespace();
      }
  }
  
  /**
   * Parse a single attribute definition.
   * <pre>
   * [53] AttDef ::= S Name S AttType S DefaultDecl
   * </pre>
   */
  private void parseAttDef(String elementName)
    throws Exception
  {
    String name;
    String type;
    String enumer = null;
    
    // Read the attribute name.
    name = readNmtoken(true);

    // Read the attribute type.
    requireWhitespace();
    type = readAttType();

    // Get the string of enumerated values if necessary.
    if (handler.stringInterning)
      {
        if ("ENUMERATION" == type || "NOTATION" == type)
          {
            enumer = dataBufferToString();
          }
      }
    else
      {
        if ("ENUMERATION".equals(type) || "NOTATION".equals(type))
          {
            enumer = dataBufferToString();
          }
      }
    
    // Read the default value.
    requireWhitespace();
    parseDefault(elementName, name, type, enumer);
  }

  /**
   * Parse the attribute type.
   * <pre>
   * [54] AttType ::= StringType | TokenizedType | EnumeratedType
   * [55] StringType ::= 'CDATA'
   * [56] TokenizedType ::= 'ID' | 'IDREF' | 'IDREFS' | 'ENTITY'
   *    | 'ENTITIES' | 'NMTOKEN' | 'NMTOKENS'
   * [57] EnumeratedType ::= NotationType | Enumeration
   * </pre>
   */
  private String readAttType()
    throws Exception
  {
    if (tryRead('('))
      {
        parseEnumeration(false);
        return "ENUMERATION";
      }
    else
      {
        String typeString = readNmtoken(true);
        if (handler.stringInterning)
          {
            if ("NOTATION" == typeString)
              {
                parseNotationType();
                return typeString;
              }
            else if ("CDATA" == typeString
                     || "ID" == typeString
                     || "IDREF" == typeString
                     || "IDREFS" == typeString
                     || "ENTITY" == typeString
                     || "ENTITIES" == typeString
                     || "NMTOKEN" == typeString
                     || "NMTOKENS" == typeString)
              {
                return typeString;
              }
          }
        else
          {
            if ("NOTATION".equals(typeString))
              {
                parseNotationType();
                return typeString;
              }
            else if ("CDATA".equals(typeString)
                     || "ID".equals(typeString)
                     || "IDREF".equals(typeString)
                     || "IDREFS".equals(typeString)
                     || "ENTITY".equals(typeString)
                     || "ENTITIES".equals(typeString)
                     || "NMTOKEN".equals(typeString)
                     || "NMTOKENS".equals(typeString))
              {
                return typeString;
              }
          }
        error("illegal attribute type", typeString, null);
        return null;
      }
  }
  
  /**
   * Parse an enumeration.
   * <pre>
   * [59] Enumeration ::= '(' S? Nmtoken (S? '|' S? Nmtoken)* S? ')'
   * </pre>
   * <p>NOTE: the '(' has already been read.
   */
  private void parseEnumeration(boolean isNames)
    throws Exception
  {
    dataBufferAppend('(');

    // Read the first token.
    skipWhitespace();
    dataBufferAppend(readNmtoken(isNames));
    // Read the remaining tokens.
    skipWhitespace();
    while (!tryRead(')'))
      {
        require('|');
        dataBufferAppend('|');
        skipWhitespace();
        dataBufferAppend(readNmtoken (isNames));
        skipWhitespace();
      }
    dataBufferAppend(')');
  }

  /**
   * Parse a notation type for an attribute.
   * <pre>
   * [58] NotationType ::= 'NOTATION' S '(' S? NameNtoks
   *    (S? '|' S? name)* S? ')'
   * </pre>
   * <p>NOTE: the 'NOTATION' has already been read
   */
  private void parseNotationType()
    throws Exception
  {
    requireWhitespace();
    require('(');
    
    parseEnumeration(true);
  }

  /**
   * Parse the default value for an attribute.
   * <pre>
   * [60] DefaultDecl ::= '#REQUIRED' | '#IMPLIED'
   *    | (('#FIXED' S)? AttValue)
   * </pre>
   */
  private void parseDefault(String elementName, String name,
                            String type, String enumer)
    throws Exception
  {
    int valueType = ATTRIBUTE_DEFAULT_SPECIFIED;
    String value = null;
    int flags = LIT_ATTRIBUTE;
    boolean saved = expandPE;
    String defaultType = null;
    
    // LIT_ATTRIBUTE forces '<' checks now (ASAP) and turns whitespace
    // chars to spaces (doesn't matter when that's done if it doesn't
    // interfere with char refs expanding to whitespace).
    
    if (!skippedPE)
      {
        flags |= LIT_ENTITY_REF;
        if (handler.stringInterning)
          {
            if ("CDATA" != type)
              {
                flags |= LIT_NORMALIZE;
              }
          }
        else
          {
            if (!"CDATA".equals(type))
              {
                flags |= LIT_NORMALIZE;
              }
          }
      }
    
    expandPE = false;
    if (tryRead('#'))
      {
        if (tryRead("FIXED"))
          {
            defaultType = "#FIXED";
            valueType = ATTRIBUTE_DEFAULT_FIXED;
            requireWhitespace();
            value = readLiteral(flags);
          }
        else if (tryRead("REQUIRED"))
          {
            defaultType = "#REQUIRED";
            valueType = ATTRIBUTE_DEFAULT_REQUIRED;
          }
        else if (tryRead("IMPLIED"))
          {
            defaultType = "#IMPLIED";
            valueType = ATTRIBUTE_DEFAULT_IMPLIED;
          }
        else
          {
            error("illegal keyword for attribute default value");
          }
      }
    else
      {
        value = readLiteral(flags);
      }
    expandPE = saved;
    setAttribute(elementName, name, type, enumer, value, valueType);
    if (handler.stringInterning)
      {
        if ("ENUMERATION" == type)
          {
            type = enumer;
          }
        else if ("NOTATION" == type)
          {
            type = "NOTATION " + enumer;
          }
      }
    else
      {
        if ("ENUMERATION".equals(type))
          {
            type = enumer;
          }
        else if ("NOTATION".equals(type))
          {
            type = "NOTATION " + enumer;
          }
      }
    if (!skippedPE)
      {
        handler.getDeclHandler().attributeDecl(elementName, name, type,
                                               defaultType, value);
      }
  }
  
  /**
   * Parse a conditional section.
   * <pre>
   * [61] conditionalSect ::= includeSect || ignoreSect
   * [62] includeSect ::= '&lt;![' S? 'INCLUDE' S? '['
   *    extSubsetDecl ']]&gt;'
   * [63] ignoreSect ::= '&lt;![' S? 'IGNORE' S? '['
   *    ignoreSectContents* ']]&gt;'
   * [64] ignoreSectContents ::= Ignore
   *    ('&lt;![' ignoreSectContents* ']]&gt;' Ignore )*
   * [65] Ignore ::= Char* - (Char* ( '&lt;![' | ']]&gt;') Char* )
   * </pre>
   * <p> NOTE: the '&gt;![' has already been read.
   */
  private void parseConditionalSect(char[] saved)
    throws Exception
  {
    skipWhitespace();
    if (tryRead("INCLUDE"))
      {
        skipWhitespace();
        require('[');
        // VC: Proper Conditional Section/PE Nesting
        if (readBuffer != saved)
          {
            handler.verror("Illegal Conditional Section/PE nesting");
          }
        skipWhitespace();
        while (!tryRead("]]>"))
          {
            parseMarkupdecl();
            skipWhitespace();
          }
      }
    else if (tryRead("IGNORE"))
      {
        skipWhitespace();
        require('[');
        // VC: Proper Conditional Section/PE Nesting
        if (readBuffer != saved)
          {
            handler.verror("Illegal Conditional Section/PE nesting");
          }
        int nesting = 1;
        char c;
        expandPE = false;
        for (int nest = 1; nest > 0; )
          {
            c = readCh();
            switch (c)
              {
              case '<':
                if (tryRead("!["))
                  {
                    nest++;
                  }
              case ']':
                if (tryRead("]>"))
                  {
                    nest--;
                  }
              }
          }
        expandPE = true;
      }
    else
      {
        error("conditional section must begin with INCLUDE or IGNORE");
      }
  }
  
  private void parseCharRef()
    throws SAXException, IOException
  {
    parseCharRef(true /* do flushDataBuffer by default */);
  }

  /**
   * Try to read a character reference without consuming data from buffer.
   * <pre>
   * [66] CharRef ::= '&#' [0-9]+ ';' | '&#x' [0-9a-fA-F]+ ';'
   * </pre>
   * <p>NOTE: the '&#' has already been read.
   */
  private void tryReadCharRef()
    throws SAXException, IOException
  {
    int value = 0;
    char c;
    
    if (tryRead('x'))
      {
loop1:
        while (true)
          {
            c = readCh();
            if (c == ';')
              {
                break loop1;
              }
            else
              {
                int n = Character.digit(c, 16);
                if (n == -1)
                  {
                    error("illegal character in character reference", c, null);
                    break loop1;
                  }
                value *= 16;
                value += n;
              }
          }
      }
    else
      {
loop2:
        while (true)
          {
            c = readCh();
            if (c == ';')
              {
                break loop2;
              }
            else
              {
                int n = Character.digit(c, 10);
                if (n == -1)
                  {
                    error("illegal character in character reference", c, null);
                    break loop2;
                  }
                value *= 10;
                value += n;
              }
          }
      }
    
    // check for character refs being legal XML
    if ((value < 0x0020
         && ! (value == '\n' || value == '\t' || value == '\r'))
        || (value >= 0xD800 && value <= 0xDFFF)
        || value == 0xFFFE || value == 0xFFFF
        || value > 0x0010ffff)
      {
        error("illegal XML character reference U+"
              + Integer.toHexString(value));
      }
    
    // Check for surrogates: 00000000 0000xxxx yyyyyyyy zzzzzzzz
    //  (1101|10xx|xxyy|yyyy + 1101|11yy|zzzz|zzzz:
    if (value > 0x0010ffff)
      {
        // too big for surrogate
        error("character reference " + value + " is too large for UTF-16",
              new Integer(value).toString(), null);
      }
    
  }
  
  /**
   * Read and interpret a character reference.
   * <pre>
   * [66] CharRef ::= '&#' [0-9]+ ';' | '&#x' [0-9a-fA-F]+ ';'
   * </pre>
   * <p>NOTE: the '&#' has already been read.
   */
  private void parseCharRef(boolean doFlush)
    throws SAXException, IOException
  {
    int value = 0;
    char c;
    
    if (tryRead('x'))
      {
loop1:
        while (true)
          {
            c = readCh();
            if (c == ';')
              {
                break loop1;
              }
            else
              {
                int n = Character.digit(c, 16);
                if (n == -1)
                  {
                    error("illegal character in character reference", c, null);
                    break loop1;
                  }
                value *= 16;
                value += n;
              }
          }
      }
    else
      {
loop2:
        while (true)
          {
            c = readCh();
            if (c == ';')
              {
                break loop2;
              }
            else
              {
                int n = Character.digit(c, 10);
                if (n == -1)
                  {
                    error("illegal character in character reference", c, null);
                    break loop2;
                  }
                value *= 10;
                value += c - '0';
              }
          }
      }
    
    // check for character refs being legal XML
    if ((value < 0x0020
         && ! (value == '\n' || value == '\t' || value == '\r'))
        || (value >= 0xD800 && value <= 0xDFFF)
        || value == 0xFFFE || value == 0xFFFF
        || value > 0x0010ffff)
      {
        error("illegal XML character reference U+"
              + Integer.toHexString(value));
      }
    
    // Check for surrogates: 00000000 0000xxxx yyyyyyyy zzzzzzzz
    //  (1101|10xx|xxyy|yyyy + 1101|11yy|zzzz|zzzz:
    if (value <= 0x0000ffff)
      {
        // no surrogates needed
        dataBufferAppend((char) value);
      }
    else if (value <= 0x0010ffff)
      {
        value -= 0x10000;
        // > 16 bits, surrogate needed
        dataBufferAppend((char) (0xd800 | (value >> 10)));
        dataBufferAppend((char) (0xdc00 | (value & 0x0003ff)));
      }
    else
      {
        // too big for surrogate
        error("character reference " + value + " is too large for UTF-16",
              new Integer(value).toString(), null);
      }
    if (doFlush)
      {
        dataBufferFlush();
      }
  }
  
  /**
   * Parse and expand an entity reference.
   * <pre>
   * [68] EntityRef ::= '&' Name ';'
   * </pre>
   * <p>NOTE: the '&amp;' has already been read.
   * @param externalAllowed External entities are allowed here.
   */
  private void parseEntityRef(boolean externalAllowed)
    throws SAXException, IOException
  {
    String name;
    
    name = readNmtoken(true);
    require(';');
    switch (getEntityType(name))
      {
      case ENTITY_UNDECLARED:
        // NOTE:  XML REC describes amazingly convoluted handling for
        // this case.  Nothing as meaningful as being a WFness error
        // unless the processor might _legitimately_ not have seen a
        // declaration ... which is what this implements.
        String message;
        
        message = "reference to undeclared general entity " + name;
        if (skippedPE && !docIsStandalone)
          {
            handler.verror(message);
            // we don't know this entity, and it might be external...
            if (externalAllowed)
              {
                handler.skippedEntity(name);
              }
          }
        else
          {
            error(message);
          }
        break;
      case ENTITY_INTERNAL:
          pushString(name, getEntityValue(name));
          
          //workaround for possible input pop before marking
          //the buffer reading position  
          char t = readCh();
          unread(t);
          int bufferPosMark = readBufferPos;
          
          int end = readBufferPos + getEntityValue(name).length();
          for (int k = readBufferPos; k < end; k++)
            {
              t = readCh();
              if (t == '&')
                {
                  t = readCh();   
                  if (t  == '#')
                    { 
                      //try to match a character ref
                      tryReadCharRef();
                
                      //everything has been read
                      if (readBufferPos >= end)
                        {
                          break;
                        }
                      k = readBufferPos;
                      continue;
                    }
                  else if (Character.isLetter(t))
                    {
                      //looks like an entity ref
                      unread(t);
                      readNmtoken(true);
                      require(';');
                      
                      //everything has been read
                      if (readBufferPos >= end)
                        {
                          break;
                        }
                      k = readBufferPos;
                      continue;
                    }
                  error(" malformed entity reference");
                }
              
            }
          readBufferPos = bufferPosMark;
          break;
      case ENTITY_TEXT:
          if (externalAllowed)
            {
              pushURL(false, name, getEntityIds(name),
                      null, null, null, true);
            }
          else
            {
              error("reference to external entity in attribute value.",
                    name, null);
            }
          break;
      case ENTITY_NDATA:
          if (externalAllowed)
            {
              error("unparsed entity reference in content", name, null);
            }
          else
            {
              error("reference to external entity in attribute value.",
                    name, null);
            }
          break;
      default:
          throw new RuntimeException();
      }
  }
    
  /**
   * Parse and expand a parameter entity reference.
   * <pre>
   * [69] PEReference ::= '%' Name ';'
   * </pre>
   * <p>NOTE: the '%' has already been read.
   */
  private void parsePEReference()
    throws SAXException, IOException
  {
    String name;
    
    name = "%" + readNmtoken(true);
    require(';');
    switch (getEntityType(name))
      {
      case ENTITY_UNDECLARED:
        // VC: Entity Declared
        handler.verror("reference to undeclared parameter entity " + name);
        
        // we should disable handling of all subsequent declarations
        // unless this is a standalone document (info discarded)
        break;
      case ENTITY_INTERNAL:
        if (inLiteral)
          {
            pushString(name, getEntityValue(name));
          }
        else
          {
            pushString(name, ' ' + getEntityValue(name) + ' ');
          }
        break;
      case ENTITY_TEXT:
        if (!inLiteral)
          {
            pushString(null, " ");
          }
        pushURL(true, name, getEntityIds(name), null, null, null, true);
        if (!inLiteral)
          {
            pushString(null, " ");
          }
        break;
      }
  }
  
  /**
   * Parse an entity declaration.
   * <pre>
   * [70] EntityDecl ::= GEDecl | PEDecl
   * [71] GEDecl ::= '&lt;!ENTITY' S Name S EntityDef S? '&gt;'
   * [72] PEDecl ::= '&lt;!ENTITY' S '%' S Name S PEDef S? '&gt;'
   * [73] EntityDef ::= EntityValue | (ExternalID NDataDecl?)
   * [74] PEDef ::= EntityValue | ExternalID
   * [75] ExternalID ::= 'SYSTEM' S SystemLiteral
   *       | 'PUBLIC' S PubidLiteral S SystemLiteral
   * [76] NDataDecl ::= S 'NDATA' S Name
   * </pre>
   * <p>NOTE: the '&lt;!ENTITY' has already been read.
   */
  private void parseEntityDecl()
    throws Exception
  {
    boolean peFlag = false;
    int flags = 0;
    
    // Check for a parameter entity.
    expandPE = false;
    requireWhitespace();
    if (tryRead('%'))
      {
        peFlag = true;
        requireWhitespace();
      }
    expandPE = true;
    
    // Read the entity name, and prepend
    // '%' if necessary.
    String name = readNmtoken(true);
    //NE08
    if (name.indexOf(':') >= 0)
      {
        error("Illegal character(':') in entity name ", name, null);
      }
    if (peFlag)
      {
        name = "%" + name;
      }

    // Read the entity value.
    requireWhitespace();
    char c = readCh();
    unread (c);
    if (c == '"' || c == '\'')
      {
        // Internal entity ... replacement text has expanded refs
        // to characters and PEs, but not to general entities
        String value = readLiteral(flags);
        setInternalEntity(name, value);
      }
    else
      {
        // Read the external IDs
        ExternalIdentifiers ids = readExternalIds(false, false);
        
        // Check for NDATA declaration.
        boolean white = tryWhitespace();
        if (!peFlag && tryRead("NDATA"))
          {
            if (!white)
              {
                error("whitespace required before NDATA");
              }
            requireWhitespace();
            String notationName = readNmtoken(true);
            if (!skippedPE)
              {
                setExternalEntity(name, ENTITY_NDATA, ids, notationName);
                handler.unparsedEntityDecl(name, ids.publicId, ids.systemId,
                                           ids.baseUri, notationName);
              }
          }
        else if (!skippedPE)
          {
            setExternalEntity(name, ENTITY_TEXT, ids, null);
            handler.getDeclHandler()
              .externalEntityDecl(name, ids.publicId,
                                   handler.resolveURIs()
                                   // FIXME: ASSUMES not skipped
                                   // "false" forces error on bad URI
                                   ? handler.absolutize(ids.baseUri,
                                                        ids.systemId,
                                                        false)
                                   : ids.systemId);
          }
      }
    
    // Finish the declaration.
    skipWhitespace();
    require('>');
  }

  /**
   * Parse a notation declaration.
   * <pre>
   * [82] NotationDecl ::= '&lt;!NOTATION' S Name S
   *    (ExternalID | PublicID) S? '&gt;'
   * [83] PublicID ::= 'PUBLIC' S PubidLiteral
   * </pre>
   * <P>NOTE: the '&lt;!NOTATION' has already been read.
   */
  private void parseNotationDecl()
    throws Exception
  {
    String nname;
    ExternalIdentifiers ids;

    requireWhitespace();
    nname = readNmtoken(true);
    //NE08
    if (nname.indexOf(':') >= 0)
      {
        error("Illegal character(':') in notation name ", nname, null);
      }
    requireWhitespace();

    // Read the external identifiers.
    ids = readExternalIds(true, false);

    // Register the notation.
    setNotation(nname, ids);
    
    skipWhitespace();
    require('>');
  }
  
  /**
   * Parse character data.
   * <pre>
   * [14] CharData ::= [^&lt;&amp;]* - ([^&lt;&amp;]* ']]&gt;' [^&lt;&amp;]*)
   * </pre>
   */
  private void parseCharData()
    throws Exception
  {
    char c;
    int state = 0;
    boolean pureWhite = false;

    // assert (dataBufferPos == 0);
    
    // are we expecting pure whitespace?  it might be dirty...
    if ((currentElementContent == CONTENT_ELEMENTS) && !isDirtyCurrentElement)
      {
        pureWhite = true;
      }

    // always report right out of readBuffer
    // to minimize (pointless) buffer copies
    while (true)
      {
        int lineAugment = 0;
        int columnAugment = 0;
        int i;
        
loop:
        for (i = readBufferPos; i < readBufferLength; i++)
          {
            switch (c = readBuffer[i])
              {
              case '\n':
                lineAugment++;
                columnAugment = 0;
                // pureWhite unmodified
                break;
              case '\r':  // should not happen!!
              case '\t':
              case ' ':
                // pureWhite unmodified
                columnAugment++;
                break;
              case '&':
              case '<':
                columnAugment++;
                // pureWhite unmodified
                // CLEAN end of text sequence
                state = 1;
                break loop;
              case ']':
                // that's not a whitespace char, and
                // can not terminate pure whitespace either
                pureWhite = false;
                if ((i + 2) < readBufferLength)
                  {
                    if (readBuffer [i + 1] == ']'
                        && readBuffer [i + 2] == '>')
                      {
                        // ERROR end of text sequence
                        state = 2;
                        break loop;
                      }
                  }
                else
                  {
                    // FIXME missing two end-of-buffer cases
                  }
                columnAugment++;
                break;
              default:
                if ((c < 0x0020 || c > 0xFFFD)
                    || ((c >= 0x007f) && (c <= 0x009f) && (c != 0x0085) 
                        && xmlVersion == XML_11)) 
                  {
                    error("illegal XML character U+"
                          + Integer.toHexString(c));
                  }
                // that's not a whitespace char
                pureWhite = false;
                columnAugment++;
              }
          }
        
        // report text thus far
        if (lineAugment > 0)
          {
            line += lineAugment;
            column = columnAugment;
          }
        else
          {
            column += columnAugment;
          }
        
        // report characters/whitspace
        int length = i - readBufferPos;
        
        if (length != 0)
          {
            if (pureWhite)
              {
                handler.ignorableWhitespace(readBuffer,
                                            readBufferPos, length);
              }
            else
              {
                handler.charData(readBuffer, readBufferPos, length);
              }
            readBufferPos = i;
          }
        
        if (state != 0)
          {
            break;
          }
        
        // fill next buffer from this entity, or
        // pop stack and continue with previous entity
        unread(readCh());
      }
    if (!pureWhite)
      {
        isDirtyCurrentElement = true;
      }
    // finish, maybe with error
    if (state != 1)  // finish, no error
      {
        error("character data may not contain ']]>'");
      }
  }
  
  //////////////////////////////////////////////////////////////////////
  // High-level reading and scanning methods.
  //////////////////////////////////////////////////////////////////////
  
  /**
   * Require whitespace characters.
   */
  private void requireWhitespace()
    throws SAXException, IOException
  {
    char c = readCh();
    if (isWhitespace(c))
      {
        skipWhitespace();
      }
    else
      {
        error("whitespace required", c, null);
      }
  }

  /**
   * Skip whitespace characters.
   * <pre>
   * [3] S ::= (#x20 | #x9 | #xd | #xa)+
   * </pre>
   */
  private void skipWhitespace()
    throws SAXException, IOException
  {
    // Start with a little cheat.  Most of
    // the time, the white space will fall
    // within the current read buffer; if
    // not, then fall through.
    if (USE_CHEATS)
      {
        int lineAugment = 0;
        int columnAugment = 0;
        
loop:
        for (int i = readBufferPos; i < readBufferLength; i++)
          {
            switch (readBuffer[i])
              {
              case ' ':
              case '\t':
              case '\r':
                columnAugment++;
                break;
              case '\n':
                lineAugment++;
                columnAugment = 0;
                break;
              case '%':
                if (expandPE)
                  {
                    break loop;
                  }
                // else fall through...
              default:
                readBufferPos = i;
                if (lineAugment > 0)
                  {
                    line += lineAugment;
                    column = columnAugment;
                  }
                else
                  {
                    column += columnAugment;
                  }
                return;
              }
          }
      }
    
    // OK, do it the slow way.
    char c = readCh ();
    while (isWhitespace(c))
      {
        c = readCh();
      }
    unread(c);
  }
  
  /**
   * Read a name or (when parsing an enumeration) name token.
   * <pre>
   * [5] Name ::= (Letter | '_' | ':') (NameChar)*
   * [7] Nmtoken ::= (NameChar)+
   * </pre>
   */
  private String readNmtoken(boolean isName)
    throws SAXException, IOException
  {
    char c;
    
    if (USE_CHEATS)
      {
loop:
        for (int i = readBufferPos; i < readBufferLength; i++)
          {
            c = readBuffer[i];
            switch (c)
              {
              case '%':
                if (expandPE)
                  {
                    break loop;
                  }
                // else fall through...
                
                // What may legitimately come AFTER a name/nmtoken?
              case '<': case '>': case '&':
              case ',': case '|': case '*': case '+': case '?':
              case ')':
              case '=':
              case '\'': case '"':
              case '[':
              case ' ': case '\t': case '\r': case '\n':
              case ';':
              case '/':
                int start = readBufferPos;
                if (i == start)
                  {
                    error("name expected", readBuffer[i], null);
                  }
                readBufferPos = i;
                return intern(readBuffer, start, i - start);
                
              default:
                // FIXME ... per IBM's OASIS test submission, these:
                //   ?    U+06dd 
                //   Combining  U+309B
                //these switches are kind of ugly but at least we won't
                //have to go over the whole lits for each char
                if (isName && i == readBufferPos)
                  {
                    char c2 = (char) (c & 0x00f0);
                    switch (c & 0xff00)
                      {
                        //starting with 01
                      case 0x0100:
                        switch (c2)
                          {
                          case 0x0030:
                            if (c == 0x0132 || c == 0x0133 || c == 0x013f)
                              {
                                error("Not a name start character, U+"
                                      + Integer.toHexString(c));
                              }
                            break;
                          case 0x0040:
                            if (c == 0x0140 || c == 0x0149)
                              {
                                error("Not a name start character, U+"
                                      + Integer.toHexString(c));
                              }
                            break;
                          case 0x00c0:
                            if (c == 0x01c4 || c == 0x01cc)
                              {
                                error("Not a name start character, U+"
                                      + Integer.toHexString(c));
                              }
                            break;
                          case 0x00f0:
                            if (c == 0x01f1 || c == 0x01f3)
                              {
                                error("Not a name start character, U+"
                                      + Integer.toHexString(c));
                              }
                            break;
                          case 0x00b0:
                            if (c == 0x01f1 || c == 0x01f3)
                              {
                                error("Not a name start character, U+"
                                      + Integer.toHexString(c));
                              }
                            break;
                          default:
                            if (c == 0x017f)
                              {
                                error("Not a name start character, U+"
                                      + Integer.toHexString(c));
                              }
                          }
                        
                        break;
                        //starting with 11
                      case 0x1100:
                        switch (c2)
                          {
                          case 0x0000:
                            if (c == 0x1104 || c == 0x1108 ||
                                c == 0x110a || c == 0x110d)
                              {
                                error("Not a name start character, U+"
                                      + Integer.toHexString(c));
                              }
                            break;
                          case 0x0030:
                            if (c == 0x113b || c == 0x113f)
                              {
                                error("Not a name start character, U+"
                                      + Integer.toHexString(c));
                              }
                            break;
                          case 0x0040:
                            if (c == 0x1141 || c == 0x114d
                                || c == 0x114f )
                              {
                                error("Not a name start character, U+"
                                      + Integer.toHexString(c));
                              }
                            break;
                          case 0x0050:
                            if (c == 0x1151 || c == 0x1156)
                              {
                                error("Not a name start character, U+"
                                      + Integer.toHexString(c));
                              }
                            break;
                          case 0x0060:
                            if (c == 0x1162 || c == 0x1164
                                || c == 0x1166 || c == 0x116b
                                || c == 0x116f)
                              {
                                error("Not a name start character, U+"
                                      + Integer.toHexString(c));
                              }
                            break;
                          case 0x00b0:
                            if (c == 0x11b6 || c == 0x11b9
                                || c == 0x11bb || c == 0x116f)
                              {
                                error("Not a name start character, U+"
                                      + Integer.toHexString(c));
                              }
                            break;
                          default:
                            if (c == 0x1174 || c == 0x119f
                                || c == 0x11ac || c == 0x11c3
                                || c == 0x11f1)
                              {
                                error("Not a name start character, U+"
                                      + Integer.toHexString(c));
                              }
                          }
                        break;
                      default:
                        if (c == 0x0e46 || c == 0x1011 
                            || c == 0x212f || c == 0x0587
                            || c == 0x0230 )
                          {
                            error("Not a name start character, U+"
                                  + Integer.toHexString(c));
                          }
                      }
                  }
                // punt on exact tests from Appendix A; approximate
                // them using the Unicode ID start/part rules
                if (i == readBufferPos && isName)
                  {
                    if (!Character.isUnicodeIdentifierStart(c)
                        && c != ':' && c != '_')
                      {
                        error("Not a name start character, U+"
                              + Integer.toHexString(c));
                      }
                  }
                else if (!Character.isUnicodeIdentifierPart(c)
                         && c != '-' && c != ':' && c != '_' && c != '.'
                         && !isExtender(c))
                  {
                    error("Not a name character, U+"
                          + Integer.toHexString(c));
                  }
              }
          }
      }
    
    nameBufferPos = 0;

    // Read the first character.
loop:
    while (true)
      {
        c = readCh();
        switch (c)
          {
          case '%':
          case '<': case '>': case '&':
          case ',': case '|': case '*': case '+': case '?':
          case ')':
          case '=':
          case '\'': case '"':
          case '[':
          case ' ': case '\t': case '\n': case '\r':
          case ';':
          case '/':
            unread(c);
            if (nameBufferPos == 0)
              {
                error ("name expected");
              }
            // punt on exact tests from Appendix A, but approximate them
            if (isName
                && !Character.isUnicodeIdentifierStart(nameBuffer[0])
                && ":_".indexOf(nameBuffer[0]) == -1)
              {
                error("Not a name start character, U+"
                      + Integer.toHexString(nameBuffer[0]));
              }
            String s = intern(nameBuffer, 0, nameBufferPos);
            nameBufferPos = 0;
            return s;
          default:
            // punt on exact tests from Appendix A, but approximate them
            
            if ((nameBufferPos != 0 || !isName)
                && !Character.isUnicodeIdentifierPart(c)
                && ":-_.".indexOf(c) == -1
                && !isExtender(c))
              {
                error("Not a name character, U+"
                      + Integer.toHexString(c));
              }
            if (nameBufferPos >= nameBuffer.length)
              {
                nameBuffer =
                  (char[]) extendArray(nameBuffer,
                                       nameBuffer.length, nameBufferPos);
              }
            nameBuffer[nameBufferPos++] = c;
          }
      }
  }
  
  private static boolean isExtender(char c)
  {
    // [88] Extender ::= ...
    return c == 0x00b7 || c == 0x02d0 || c == 0x02d1 || c == 0x0387
      || c == 0x0640 || c == 0x0e46 || c == 0x0ec6 || c == 0x3005
      || (c >= 0x3031 && c <= 0x3035)
      || (c >= 0x309d && c <= 0x309e)
      || (c >= 0x30fc && c <= 0x30fe);
  }

  /**
   * Read a literal.  With matching single or double quotes as
   * delimiters (and not embedded!) this is used to parse:
   * <pre>
   *  [9] EntityValue ::= ... ([^%&amp;] | PEReference | Reference)* ...
   *  [10] AttValue ::= ... ([^<&] | Reference)* ...
   *  [11] SystemLiteral ::= ... (URLchar - "'")* ...
   *  [12] PubidLiteral ::= ... (PubidChar - "'")* ...
   * </pre>
   * as well as the quoted strings in XML and text declarations
   * (for version, encoding, and standalone) which have their
   * own constraints.
   */
  private String readLiteral(int flags)
    throws SAXException, IOException
  {
    char delim, c;
    int startLine = line;
    boolean saved = expandPE;
    boolean savedReport = doReport;
    
    // Find the first delimiter.
    delim = readCh();
    if (delim != '"' && delim != '\'')
      {
        error("expected '\"' or \"'\"", delim, null);
        return null;
      }
    inLiteral = true;
    if ((flags & LIT_DISABLE_PE) != 0)
      {
        expandPE = false;
      }
    doReport = false;
    
    // Each level of input source has its own buffer; remember
    // ours, so we won't read the ending delimiter from any
    // other input source, regardless of entity processing.
    char[] ourBuf = readBuffer;

    // Read the literal.
    try
      {
        c = readCh();
        boolean ampRead = false;
loop:
        while (! (c == delim && readBuffer == ourBuf))
          {
            switch (c)
              {
                // attributes and public ids are normalized
                // in almost the same ways
              case '\n':
              case '\r':
                if ((flags & (LIT_ATTRIBUTE | LIT_PUBID)) != 0)
                  {
                    c = ' ';
                  }
                break;
              case '\t':
                if ((flags & LIT_ATTRIBUTE) != 0)
                  {
                    c = ' ';
                  }
                break;
              case '&':
                c = readCh();
                // Char refs are expanded immediately, except for
                // all the cases where it's deferred.
                if (c == '#')
                  {
                    if ((flags & LIT_DISABLE_CREF) != 0)
                      {
                        dataBufferAppend('&');
                        break;
                      }
                    parseCharRef(false /* Do not do flushDataBuffer */);
                    
                    // exotic WFness risk: this is an entity literal,
                    // dataBuffer [dataBufferPos - 1] == '&', and
                    // following chars are a _partial_ entity/char ref
                    
                    // It looks like an entity ref ...
                  }
                else
                  {
                    unread(c);
                    // Expand it?
                    if ((flags & LIT_ENTITY_REF) > 0)
                      {
                        parseEntityRef(false);
                        if (String.valueOf(readBuffer).equals("&#38;"))
                          {
                            ampRead = true;
                          }
                        //Is it just data?
                      }
                    else if ((flags & LIT_DISABLE_EREF) != 0)
                      {
                        dataBufferAppend('&');
                        
                        // OK, it will be an entity ref -- expanded later.
                      }
                    else
                      {
                        String name = readNmtoken(true);
                        require(';');
                        dataBufferAppend('&');
                        dataBufferAppend(name);
                        dataBufferAppend(';');
                      }
                  }
                c = readCh();
                continue loop;
                
              case '<':
                // and why?  Perhaps so "&foo;" expands the same
                // inside and outside an attribute?
                if ((flags & LIT_ATTRIBUTE) != 0)
                  {
                    error("attribute values may not contain '<'");
                  }
                break;

                // We don't worry about case '%' and PE refs, readCh does.
                
              default:
                break;
              }
            dataBufferAppend(c);
            c = readCh();
          }
      }
    catch (EOFException e)
      {
        error("end of input while looking for delimiter (started on line "
              + startLine + ')', null, new Character(delim).toString());
      }
    inLiteral = false;
    expandPE = saved;
    doReport = savedReport;
    
    // Normalise whitespace if necessary.
    if ((flags & LIT_NORMALIZE) > 0)
      {
        dataBufferNormalize();
      }
    
    // Return the value.
    return dataBufferToString();
  }
  
  /**
   * Try reading external identifiers.
   * A system identifier is not required for notations.
   * @param inNotation Are we parsing a notation decl?
   * @param isSubset Parsing external subset decl (may be omitted)?
   * @return A three-member String array containing the identifiers,
   *  or nulls. Order: public, system, baseURI.
   */
  private ExternalIdentifiers readExternalIds(boolean inNotation,
                                              boolean isSubset)
    throws Exception
  {
    char c;
    ExternalIdentifiers ids = new ExternalIdentifiers();
    int flags = LIT_DISABLE_CREF | LIT_DISABLE_PE | LIT_DISABLE_EREF;
    
    if (tryRead("PUBLIC"))
      {
        requireWhitespace();
        ids.publicId = readLiteral(LIT_NORMALIZE | LIT_PUBID | flags);
        if (inNotation)
          {
            skipWhitespace();
            c = readCh();
            unread(c);
            if (c == '"' || c == '\'')
              {
                ids.systemId = readLiteral(flags);
              }
          }
        else
          {
            requireWhitespace();
            ids.systemId = readLiteral(flags);
          }
        
        for (int i = 0; i < ids.publicId.length(); i++)
          {
            c = ids.publicId.charAt(i);
            if (c >= 'a' && c <= 'z')
              {
                continue;
              }
            if (c >= 'A' && c <= 'Z')
              {
                continue;
              }
            if (" \r\n0123456789-' ()+,./:=?;!*#@$_%".indexOf(c) != -1)
              {
                continue;
              }
            error("illegal PUBLIC id character U+"
                  + Integer.toHexString(c));
          }
      }
    else if (tryRead("SYSTEM"))
      {
        requireWhitespace();
        ids.systemId = readLiteral(flags);
      }
    else if (!isSubset)
      {
        error("missing SYSTEM or PUBLIC keyword");
      }
      
    if (ids.systemId != null)
      {
        if (ids.systemId.indexOf('#') != -1)
          {
            handler.verror("SYSTEM id has an URI fragment: " + ids.systemId);
          }
        ids.baseUri = handler.getSystemId();
        if (ids.baseUri == null && uriWarnings)
          {
            handler.warn("No base URI; hope URI is absolute: "
                         + ids.systemId);
          }
      }
    
    return ids;
  }

  /**
   * Test if a character is whitespace.
   * <pre>
   * [3] S ::= (#x20 | #x9 | #xd | #xa)+
   * </pre>
   * @param c The character to test.
   * @return true if the character is whitespace.
   */
  private final boolean isWhitespace(char c)
  {
    if (c > 0x20)
      {
        return false;
      }
    if (c == 0x20 || c == 0x0a || c == 0x09 || c == 0x0d)
      {
        return true;
      }
    return false;  // illegal ...
  }

  //////////////////////////////////////////////////////////////////////
  // Utility routines.
  //////////////////////////////////////////////////////////////////////
    
  /**
   * Add a character to the data buffer.
   */
  private void dataBufferAppend(char c)
  {
    // Expand buffer if necessary.
    if (dataBufferPos >= dataBuffer.length)
      {
        dataBuffer = (char[]) extendArray(dataBuffer,
                                          dataBuffer.length, dataBufferPos);
      }
    dataBuffer[dataBufferPos++] = c;
  }

  /**
   * Add a string to the data buffer.
   */
  private void dataBufferAppend(String s)
  {
    dataBufferAppend(s.toCharArray(), 0, s.length());
  }

  /**
   * Append (part of) a character array to the data buffer.
   */
  private void dataBufferAppend(char[] ch, int start, int length)
  {
    dataBuffer = (char[]) extendArray(dataBuffer, dataBuffer.length,
                                      dataBufferPos + length);
    
    System.arraycopy(ch, start, dataBuffer, dataBufferPos, length);
    dataBufferPos += length;
  }

  /**
   * Normalise space characters in the data buffer.
   */
  private void dataBufferNormalize()
  {
    int i = 0;
    int j = 0;
    int end = dataBufferPos;
    
    // Skip spaces at the start.
    while (j < end && dataBuffer[j] == ' ')
      {
        j++;
      }
    
    // Skip whitespace at the end.
    while (end > j && dataBuffer[end - 1] == ' ')
      {
        end --;
      }

    // Start copying to the left.
    while (j < end)
      {
        
        char c = dataBuffer[j++];
        
        // Normalise all other spaces to
        // a single space.
        if (c == ' ')
          {
            while (j < end && dataBuffer[j++] == ' ')
              {
                continue;
              }
            dataBuffer[i++] = ' ';
            dataBuffer[i++] = dataBuffer[j - 1];
          }
        else
          {
            dataBuffer[i++] = c;
          }
      }
    
    // The new length is <= the old one.
    dataBufferPos = i;
  }

  /**
   * Convert the data buffer to a string.
   */
  private String dataBufferToString()
  {
    String s = new String(dataBuffer, 0, dataBufferPos);
    dataBufferPos = 0;
    return s;
  }

  /**
   * Flush the contents of the data buffer to the handler, as
   * appropriate, and reset the buffer for new input.
   */
  private void dataBufferFlush()
    throws SAXException
  {
    if (currentElementContent == CONTENT_ELEMENTS
        && dataBufferPos > 0
        && !inCDATA)
      {
        // We can't just trust the buffer to be whitespace, there
        // are (error) cases when it isn't
        for (int i = 0; i < dataBufferPos; i++)
          {
            if (!isWhitespace(dataBuffer[i]))
              {
                handler.charData(dataBuffer, 0, dataBufferPos);
                dataBufferPos = 0;
              }
          }
        if (dataBufferPos > 0)
          {
            handler.ignorableWhitespace(dataBuffer, 0, dataBufferPos);
            dataBufferPos = 0;
          }
      }
    else if (dataBufferPos > 0)
      {
        handler.charData(dataBuffer, 0, dataBufferPos);
        dataBufferPos = 0;
      }
  }

  /**
   * Require a string to appear, or throw an exception.
   * <p><em>Precondition:</em> Entity expansion is not required.
   * <p><em>Precondition:</em> data buffer has no characters that
   * will get sent to the application.
   */
  private void require(String delim)
    throws SAXException, IOException
  {
    int length = delim.length();
    char[] ch;
    
    if (length < dataBuffer.length)
      {
        ch = dataBuffer;
        delim.getChars(0, length, ch, 0);
      }
    else
      {
        ch = delim.toCharArray();
      }
      
    if (USE_CHEATS && length <= (readBufferLength - readBufferPos))
      {
        int offset = readBufferPos;
        
        for (int i = 0; i < length; i++, offset++)
          {
            if (ch[i] != readBuffer[offset])
              {
                error ("required string", null, delim);
              }
          }
        readBufferPos = offset;
        
      }
    else
      {
        for (int i = 0; i < length; i++)
          {
            require(ch[i]);
          }
      }
  }

  /**
   * Require a character to appear, or throw an exception.
   */
  private void require(char delim)
    throws SAXException, IOException
  {
    char c = readCh();
    
    if (c != delim)
      {
        error("required character", c, new Character(delim).toString());
      }
  }
  
  /**
   * Create an interned string from a character array.
   * &AElig;lfred uses this method to create an interned version
   * of all names and name tokens, so that it can test equality
   * with <code>==</code> instead of <code>String.equals ()</code>.
   *
   * <p>This is much more efficient than constructing a non-interned
   * string first, and then interning it.
   *
   * @param ch an array of characters for building the string.
   * @param start the starting position in the array.
   * @param length the number of characters to place in the string.
   * @return an interned string.
   * @see #intern (String)
   * @see java.lang.String#intern
   */
  public String intern(char[] ch, int start, int length)
  {
    int index = 0;
    int hash = 0;
    Object[] bucket;

    // Generate a hash code.  This is a widely used string hash,
    // often attributed to Brian Kernighan.
    for (int i = start; i < start + length; i++)
      {
        hash = 31 * hash + ch[i];
      }
    hash = (hash & 0x7fffffff) % SYMBOL_TABLE_LENGTH;
    
    // Get the bucket -- consists of {array,String} pairs
    if ((bucket = symbolTable[hash]) == null)
      {
        // first string in this bucket
        bucket = new Object[8];
        
        // Search for a matching tuple, and
        // return the string if we find one.
      }
    else
      {
        while (index < bucket.length)
          {
            char[] chFound = (char[]) bucket[index];
        
            // Stop when we hit an empty entry.
            if (chFound == null)
              {
                break;
              }
            
            // If they're the same length, check for a match.
            if (chFound.length == length)
              {
                for (int i = 0; i < chFound.length; i++)
                  {
                    // continue search on failure
                    if (ch[start + i] != chFound[i])
                      {
                        break;
                      }
                    else if (i == length - 1)
                      {
                        // That's it, we have a match!
                        return (String) bucket[index + 1];
                      }
                  }
              }
            index += 2;
          }
        // Not found -- we'll have to add it.
        
        // Do we have to grow the bucket?
        bucket = (Object[]) extendArray(bucket, bucket.length, index);
      }
    symbolTable[hash] = bucket;
    
    // OK, add it to the end of the bucket -- "local" interning.
    // Intern "globally" to let applications share interning benefits.
    // That is, "!=" and "==" work on our strings, not just equals().
    String s = new String(ch, start, length).intern();
    bucket[index] = s.toCharArray();
    bucket[index + 1] = s;
    return s;
  }

  /**
   * Ensure the capacity of an array, allocating a new one if
   * necessary.  Usually extends only for name hash collisions. 
   */
  private Object extendArray(Object array, int currentSize, int requiredSize)
  {
    if (requiredSize < currentSize)
      {
        return array;
      }
    else
      {
        Object newArray = null;
        int newSize = currentSize * 2;
        
        if (newSize <= requiredSize)
          {
            newSize = requiredSize + 1;
          }
        
        if (array instanceof char[])
          {
            newArray = new char[newSize];
          }
        else if (array instanceof Object[])
          {
            newArray = new Object[newSize];
          }
        else
          {
            throw new RuntimeException();
          }
        
        System.arraycopy(array, 0, newArray, 0, currentSize);
        return newArray;
      }
  }

  //////////////////////////////////////////////////////////////////////
  // XML query routines.
  //////////////////////////////////////////////////////////////////////
  
  boolean isStandalone()
  {
    return docIsStandalone;
  }
    
  //
  // Elements
  //
  
  private int getContentType(ElementDecl element, int defaultType)
  {
    int retval;
    
    if (element == null)
      {
        return defaultType;
      }
    retval = element.contentType;
    if (retval == CONTENT_UNDECLARED)
      {
        retval = defaultType;
      }
    return retval;
  }

  /**
   * Look up the content type of an element.
   * @param name The element type name.
   * @return An integer constant representing the content type.
   * @see #CONTENT_UNDECLARED
   * @see #CONTENT_ANY
   * @see #CONTENT_EMPTY
   * @see #CONTENT_MIXED
   * @see #CONTENT_ELEMENTS
   */
  public int getElementContentType(String name)
  {
    ElementDecl element = (ElementDecl) elementInfo.get(name);
    return getContentType(element, CONTENT_UNDECLARED);
  }
  
  /**
   * Register an element.
   * Array format:
   *  [0] element type name
   *  [1] content model (mixed, elements only)
   *  [2] attribute hash table
   */
  private void setElement(String name, int contentType,
                          String contentModel, HashMap attributes)
    throws SAXException
  {
    if (skippedPE)
      {
        return;
      }

    ElementDecl element = (ElementDecl) elementInfo.get(name);
    
    // first <!ELEMENT ...> or <!ATTLIST ...> for this type?
    if (element == null)
      {
        element = new ElementDecl();
        element.contentType = contentType;
        element.contentModel = contentModel;
        element.attributes = attributes;
        elementInfo.put(name, element);
        return;
      }
    
    // <!ELEMENT ...> declaration?
    if (contentType != CONTENT_UNDECLARED)
      {
        // ... following an associated <!ATTLIST ...>
        if (element.contentType == CONTENT_UNDECLARED)
          {
            element.contentType = contentType;
            element.contentModel = contentModel;
          }
        else
          {
            // VC: Unique Element Type Declaration
            handler.verror("multiple declarations for element type: "
                           + name);
          }
      }
    
    // first <!ATTLIST ...>, before <!ELEMENT ...> ?
    else if (attributes != null)
      {
        element.attributes = attributes;
      }
  }
  
  /**
   * Look up the attribute hash table for an element.
   * The hash table is the second item in the element array.
   */
  private HashMap getElementAttributes(String name)
  {
    ElementDecl element = (ElementDecl) elementInfo.get(name);
    return (element == null) ? null : element.attributes;
  }

  //
  // Attributes
  //
  
  /**
   * Get the declared attributes for an element type.
   * @param elname The name of the element type.
   * @return An iterator over all the attributes declared for
   *   a specific element type.  The results will be valid only
   *   after the DTD (if any) has been parsed.
   * @see #getAttributeType
   * @see #getAttributeEnumeration
   * @see #getAttributeDefaultValueType
   * @see #getAttributeDefaultValue
   * @see #getAttributeExpandedValue
   */
  private Iterator declaredAttributes(ElementDecl element)
  {
    HashMap attlist;
    
    if (element == null)
      {
        return null;
      }
    if ((attlist = element.attributes) == null)
      {
        return null;
      }
    return attlist.keySet().iterator();
  }

  /**
   * Get the declared attributes for an element type.
   * @param elname The name of the element type.
   * @return An iterator over all the attributes declared for
   *   a specific element type.  The results will be valid only
   *   after the DTD (if any) has been parsed.
   * @see #getAttributeType
   * @see #getAttributeEnumeration
   * @see #getAttributeDefaultValueType
   * @see #getAttributeDefaultValue
   * @see #getAttributeExpandedValue
   */
  public Iterator declaredAttributes(String elname)
  {
    return declaredAttributes((ElementDecl) elementInfo.get(elname));
  }

  /**
   * Retrieve the declared type of an attribute.
   * @param name The name of the associated element.
   * @param aname The name of the attribute.
   * @return An interend string denoting the type, or null
   *  indicating an undeclared attribute.
   */
  public String getAttributeType(String name, String aname)
  {
    AttributeDecl attribute = getAttribute(name, aname);
    return (attribute == null) ? null : attribute.type;
  }

  /**
   * Retrieve the allowed values for an enumerated attribute type.
   * @param name The name of the associated element.
   * @param aname The name of the attribute.
   * @return A string containing the token list.
   */
  public String getAttributeEnumeration(String name, String aname)
  {
    AttributeDecl attribute = getAttribute(name, aname);
    // assert:  attribute.enumeration is "ENUMERATION" or "NOTATION"
    return (attribute == null) ? null : attribute.enumeration;
  }

  /**
   * Retrieve the default value of a declared attribute.
   * @param name The name of the associated element.
   * @param aname The name of the attribute.
   * @return The default value, or null if the attribute was
   *   #IMPLIED or simply undeclared and unspecified.
   * @see #getAttributeExpandedValue
   */
  public String getAttributeDefaultValue(String name, String aname)
  {
    AttributeDecl attribute = getAttribute(name, aname);
    return (attribute == null) ? null : attribute.value;
  }

    /*

// FIXME:  Leaving this in, until W3C finally resolves the confusion
// between parts of the XML 2nd REC about when entity declararations
// are guaranteed to be known.  Current code matches what section 5.1
// (conformance) describes, but some readings of the self-contradicting
// text in 4.1 (the "Entity Declared" WFC and VC) seem to expect that
// attribute expansion/normalization must be deferred in some cases
// (just TRY to identify them!).

     * Retrieve the expanded value of a declared attribute.
     * <p>General entities (and char refs) will be expanded (once).
     * @param name The name of the associated element.
     * @param aname The name of the attribute.
     * @return The expanded default value, or null if the attribute was
     *   #IMPLIED or simply undeclared
     * @see #getAttributeDefaultValue
    public String getAttributeExpandedValue (String name, String aname)
    throws Exception
    {
  AttributeDecl attribute = getAttribute (name, aname);

  if (attribute == null) {
      return null;
  } else if (attribute.defaultValue == null && attribute.value != null) {
      // we MUST use the same buf for both quotes else the literal
      // can't be properly terminated
      char buf [] = new char [1];
      int  flags = LIT_ENTITY_REF | LIT_ATTRIBUTE;
      String type = getAttributeType (name, aname);

      if (type != "CDATA" && type != null)
    flags |= LIT_NORMALIZE;
      buf [0] = '"';
      pushCharArray (null, buf, 0, 1);
      pushString (null, attribute.value);
      pushCharArray (null, buf, 0, 1);
      attribute.defaultValue = readLiteral (flags);
  }
  return attribute.defaultValue;
    }
     */

  /**
   * Retrieve the default value mode of a declared attribute.
   * @see #ATTRIBUTE_DEFAULT_SPECIFIED
   * @see #ATTRIBUTE_DEFAULT_IMPLIED
   * @see #ATTRIBUTE_DEFAULT_REQUIRED
   * @see #ATTRIBUTE_DEFAULT_FIXED
   */
  public int getAttributeDefaultValueType(String name, String aname)
  {
    AttributeDecl attribute = getAttribute(name, aname);
    return (attribute == null) ? ATTRIBUTE_DEFAULT_UNDECLARED :
      attribute.valueType;
  }  
  
  /**
   * Register an attribute declaration for later retrieval.
   * Format:
   * - String type
   * - String default value
   * - int value type
   * - enumeration
   * - processed default value
   */
  private void setAttribute(String elName, String name, String type,
                            String enumeration, String value, int valueType)
    throws Exception
  {
    HashMap attlist;
    
    if (skippedPE)
      {
        return;
      }
    
    // Create a new hashtable if necessary.
    attlist = getElementAttributes(elName);
    if (attlist == null)
      {
        attlist = new HashMap();
      }
    
    // ignore multiple attribute declarations!
    if (attlist.get(name) != null)
      {
        // warn ...
        return;
      }
    else
      {
        AttributeDecl attribute = new AttributeDecl();
        attribute.type = type;
        attribute.value = value;
        attribute.valueType = valueType;
        attribute.enumeration = enumeration;
        attlist.put(name, attribute);
      
        // save; but don't overwrite any existing <!ELEMENT ...>
        setElement(elName, CONTENT_UNDECLARED, null, attlist);
      }
  }

  /**
   * Retrieve the attribute declaration for the given element name and name.
   */
  private AttributeDecl getAttribute(String elName, String name)
  {
    HashMap attlist = getElementAttributes(elName);
    return (attlist == null) ? null : (AttributeDecl) attlist.get(name);
  }

  //
  // Entities
  //
  
  /**
   * Find the type of an entity.
   * @returns An integer constant representing the entity type.
   * @see #ENTITY_UNDECLARED
   * @see #ENTITY_INTERNAL
   * @see #ENTITY_NDATA
   * @see #ENTITY_TEXT
   */
  public int getEntityType(String ename)
  {
    EntityInfo entity = (EntityInfo) entityInfo.get(ename);
    return (entity == null) ?  ENTITY_UNDECLARED : entity.type;
  }

  /**
   * Return an external entity's identifiers.
   * @param ename The name of the external entity.
   * @return The entity's public identifier, system identifier, and base URI.
   *  Null if the entity was not declared as an external entity.
   * @see #getEntityType
   */
  public ExternalIdentifiers getEntityIds(String ename)
  {
    EntityInfo entity = (EntityInfo) entityInfo.get(ename);
    return (entity == null) ? null : entity.ids;
  }

  /**
   * Return an internal entity's replacement text.
   * @param ename The name of the internal entity.
   * @return The entity's replacement text, or null if
   *   the entity was not declared as an internal entity.
   * @see #getEntityType
   */
  public String getEntityValue(String ename)
  {
    EntityInfo entity = (EntityInfo) entityInfo.get(ename);
    return (entity == null) ? null : entity.value;
  }

  /**
   * Register an entity declaration for later retrieval.
   */
  private void setInternalEntity(String eName, String value)
    throws SAXException
  {
    if (skippedPE)
      {
        return;
      }

    if (entityInfo.get(eName) == null)
      {
        EntityInfo entity = new EntityInfo();
        entity.type = ENTITY_INTERNAL;
        entity.value = value;
        entityInfo.put(eName, entity);
      }
    if (handler.stringInterning)
      {
        if ("lt" == eName || "gt" == eName || "quot" == eName
            || "apos" == eName || "amp" == eName)
          {
            return;
          }
      }
    else
      {
        if ("lt".equals(eName) || "gt".equals(eName) || "quot".equals(eName)
            || "apos".equals(eName) || "amp".equals(eName))
          {
            return;
          }
      }
    handler.getDeclHandler().internalEntityDecl(eName, value);
  }

  /**
   * Register an external entity declaration for later retrieval.
   */
  private void setExternalEntity(String eName, int eClass,
                                 ExternalIdentifiers ids, String nName)
  {
    if (entityInfo.get(eName) == null)
      {
        EntityInfo entity = new EntityInfo();
        entity.type = eClass;
        entity.ids = ids;
        entity.notationName = nName;
        entityInfo.put(eName, entity);
      }
  }

  //
  // Notations.
  //
  
  /**
   * Report a notation declaration, checking for duplicates.
   */
  private void setNotation(String nname, ExternalIdentifiers ids)
    throws SAXException
  {
    if (skippedPE)
      {
        return;
      }
    
    handler.notationDecl(nname, ids.publicId, ids.systemId, ids.baseUri);
    if (notationInfo.get(nname) == null)
      {
        notationInfo.put(nname, nname);
      }
    else
      {
        // VC: Unique Notation Name
        handler.verror("Duplicate notation name decl: " + nname);
      }
  }
  
  //
  // Location.
  //
  
  /**
   * Return the current line number.
   */
  public int getLineNumber()
  {
    return line;
  }

  /**
   * Return the current column number.
   */
  public int getColumnNumber()
  {
    return column;
  }

  //////////////////////////////////////////////////////////////////////
  // High-level I/O.
  //////////////////////////////////////////////////////////////////////
  
  /**
   * Read a single character from the readBuffer.
   * <p>The readDataChunk () method maintains the buffer.
   * <p>If we hit the end of an entity, try to pop the stack and
   * keep going.
   * <p> (This approach doesn't really enforce XML's rules about
   * entity boundaries, but this is not currently a validating
   * parser).
   * <p>This routine also attempts to keep track of the current
   * position in external entities, but it's not entirely accurate.
   * @return The next available input character.
   * @see #unread (char)
   * @see #readDataChunk
   * @see #readBuffer
   * @see #line
   * @return The next character from the current input source.
   */
  private char readCh()
    throws SAXException, IOException
  {
    // As long as there's nothing in the
    // read buffer, try reading more data
    // (for an external entity) or popping
    // the entity stack (for either).
    while (readBufferPos >= readBufferLength)
      {
        switch (sourceType)
          {
          case INPUT_READER:
          case INPUT_STREAM:
            readDataChunk();
            while (readBufferLength < 1)
              {
                popInput();
                if (readBufferLength < 1)
                  {
                    readDataChunk();
                  }
              }
            break;
            
          default:
            
            popInput();
            break;
          }
      }
    
    char c = readBuffer[readBufferPos++];
    
    if (c == '\n')
      {
        line++;
        column = 0;
      }
    else
      {
        if (c == '<')
          {
            /* the most common return to parseContent () ... NOP */
          }
        else if (((c < 0x0020 && (c != '\t') && (c != '\r')) || c > 0xFFFD)
                 || ((c >= 0x007f) && (c <= 0x009f) && (c != 0x0085) 
                     && xmlVersion == XML_11))
          {
            error("illegal XML character U+" + Integer.toHexString(c));
          }

        // If we're in the DTD and in a context where PEs get expanded,
        // do so ... 1/14/2000 errata identify those contexts.  There
        // are also spots in the internal subset where PE refs are fatal
        // errors, hence yet another flag.
        else if (c == '%' && expandPE)
          {
            if (peIsError)
              {
                error("PE reference within decl in internal subset.");
              }
            parsePEReference();
            return readCh();
          }
        column++;
      }

    return c;
  }

  /**
   * Push a single character back onto the current input stream.
   * <p>This method usually pushes the character back onto
   * the readBuffer.
   * <p>I don't think that this would ever be called with 
   * readBufferPos = 0, because the methods always reads a character
   * before unreading it, but just in case, I've added a boundary
   * condition.
   * @param c The character to push back.
   * @see #readCh
   * @see #unread (char[])
   * @see #readBuffer
   */
  private void unread(char c)
    throws SAXException
  {
    // Normal condition.
    if (c == '\n')
      {
        line--;
        column = -1;
      }
    if (readBufferPos > 0)
      {
        readBuffer[--readBufferPos] = c;
      }
    else
      {
        pushString(null, new Character(c).toString());
      }
  }

  /**
   * Push a char array back onto the current input stream.
   * <p>NOTE: you must <em>never</em> push back characters that you
   * haven't actually read: use pushString () instead.
   * @see #readCh
   * @see #unread (char)
   * @see #readBuffer
   * @see #pushString
   */
  private void unread(char[] ch, int length)
    throws SAXException
  {
    for (int i = 0; i < length; i++)
      {
        if (ch[i] == '\n')
          {
            line--;
            column = -1;
          }
      }
    if (length < readBufferPos)
      {
        readBufferPos -= length;
      }
    else
      {
        pushCharArray(null, ch, 0, length);
      }
  }

  /**
   * Push, or skip, a new external input source.
   * The source will be some kind of parsed entity, such as a PE
   * (including the external DTD subset) or content for the body.
   *
   * @param url The java.net.URL object for the entity.
   * @see SAXDriver#resolveEntity
   * @see #pushString
   * @see #sourceType
   * @see #pushInput
   * @see #detectEncoding
   * @see #sourceType
   * @see #readBuffer
   */
  private void pushURL(boolean isPE,
                       String ename,
                       ExternalIdentifiers ids,
                       Reader reader,
                       InputStream stream,
                       String encoding,
                       boolean doResolve)
    throws SAXException, IOException
  {
    boolean ignoreEncoding;
    String systemId;
    InputSource source;

    if (!isPE)
      {
        dataBufferFlush();
      }

    scratch.setPublicId(ids.publicId);
    scratch.setSystemId(ids.systemId);

    // See if we should skip or substitute the entity.
    // If we're not skipping, resolving reports startEntity()
    // and updates the (handler's) stack of URIs.
    if (doResolve)
      {
        // assert (stream == null && reader == null && encoding == null)
        source = handler.resolveEntity(isPE, ename, scratch, ids.baseUri);
        if (source == null)
          {
            handler.warn("skipping entity: " + ename);
            handler.skippedEntity(ename);
            if (isPE)
              {
                skippedPE = true;
              }
            return;
          }

        // we might be using alternate IDs/encoding
        systemId = source.getSystemId();
        // The following warning and setting systemId was deleted bcause
        // the application has the option of not setting systemId
        // provided that it has set the characte/byte stream.
        /*
           if (systemId == null) {
           handler.warn ("missing system ID, using " + ids.systemId);
           systemId = ids.systemId;
           }
         */
      }
    else
      {
        // "[document]", or "[dtd]" via getExternalSubset()
        scratch.setCharacterStream(reader);
        scratch.setByteStream(stream);
        scratch.setEncoding(encoding);
        source = scratch;
        systemId = ids.systemId;
        if (handler.stringInterning)
          {
            handler.startExternalEntity(ename, systemId,
                                        "[document]" == ename);
          }
        else
          {
            handler.startExternalEntity(ename, systemId,
                                        "[document]".equals(ename));
          }
      }

    // we may have been given I/O streams directly
    if (source.getCharacterStream() != null)
      {
        if (source.getByteStream() != null)
          error("InputSource has two streams!");
        reader = source.getCharacterStream();
      }
    else if (source.getByteStream() != null)
      {
        encoding = source.getEncoding();
        if (encoding == null)
          {
            stream = source.getByteStream();
          }
        else
          {
            try
              {
                reader = new InputStreamReader(source.getByteStream(),
                                               encoding);
              }
            catch (IOException e)
              {
                stream = source.getByteStream();
              }
          }
      }
    else if (systemId == null)
      {
        error("InputSource has no URI!");
      }
    scratch.setCharacterStream(null);
    scratch.setByteStream(null);
    scratch.setEncoding(null);
    
    // Push the existing status.
    pushInput(ename);

    // Create a new read buffer.
    // (Note the four-character margin)
    readBuffer = new char[READ_BUFFER_MAX + 4];
    readBufferPos = 0;
    readBufferLength = 0;
    readBufferOverflow = -1;
    is = null;
    line = 1;
    column = 0;
    currentByteCount = 0;

    // If there's an explicit character stream, just
    // ignore encoding declarations.
    if (reader != null)
      {
        sourceType = INPUT_READER;
        this.reader = reader;
        tryEncodingDecl(true);
        return;
      }
  
    // Else we handle the conversion, and need to ensure
    // it's done right.
    sourceType = INPUT_STREAM;
    if (stream != null)
      {
        is = stream;
      }
    else
      {
        // We have to open our own stream to the URL.
        URL url = new URL(systemId);
        
        externalEntity = url.openConnection();
        externalEntity.connect();
        is = externalEntity.getInputStream();
      }
    
    // If we get to here, there must be
    // an InputStream available.
    if (!is.markSupported())
      {
        is = new BufferedInputStream(is);
      }

    // Get any external encoding label.
    if (encoding == null && externalEntity != null)
      {
        // External labels can be untrustworthy; filesystems in
        // particular often have the wrong default for content
        // that wasn't locally originated.  Those we autodetect.
        if (!"file".equals(externalEntity.getURL().getProtocol()))
          {
            int temp;
          
            // application/xml;charset=something;otherAttr=...
            // ... with many variants on 'something'
            encoding = externalEntity.getContentType();
          
            // MHK code (fix for Saxon 5.5.1/007):
            // protect against encoding==null
            if (encoding == null)
              {
                temp = -1;
              }
            else
              {
                temp = encoding.indexOf("charset");
              }
          
            // RFC 2376 sez MIME text defaults to ASCII, but since the
            // JDK will create a MIME type out of thin air, we always
            // autodetect when there's no explicit charset attribute.
            if (temp < 0)
              {
                encoding = null;  // autodetect
              }
            else
              {
                // only this one attribute
                if ((temp = encoding.indexOf(';')) > 0)
                  {
                    encoding = encoding.substring(0, temp);
                  }
                
                if ((temp = encoding.indexOf('=', temp + 7)) > 0)
                  {
                    encoding = encoding.substring(temp + 1);
                    
                    // attributes can have comment fields (RFC 822)
                    if ((temp = encoding.indexOf('(')) > 0)
                      {
                        encoding = encoding.substring(0, temp);
                      }
                    // ... and values may be quoted
                    if ((temp = encoding.indexOf('"')) > 0)
                      {
                        encoding =
                          encoding.substring(temp + 1,
                                             encoding.indexOf('"', temp + 2));
                      }
                    encoding.trim();
                  }
                else
                  {
                    handler.warn("ignoring illegal MIME attribute: "
                                 + encoding);
                    encoding = null;
                  }
              }
          }
      }
    
    // if we got an external encoding label, use it ...
    if (encoding != null)
      {
        this.encoding = ENCODING_EXTERNAL;
        setupDecoding(encoding);
        ignoreEncoding = true;
        
        // ... else autodetect from first bytes.
      }
    else
      {
        detectEncoding();
        ignoreEncoding = false;
      }

    // Read any XML or text declaration.
    // If we autodetected, it may tell us the "real" encoding.
    try
      {
        tryEncodingDecl(ignoreEncoding);
      }
    catch (UnsupportedEncodingException x)
      {
        encoding = x.getMessage();

        // if we don't handle the declared encoding,
        // try letting a JVM InputStreamReader do it
        try
          {
            if (sourceType != INPUT_STREAM)
              {
                throw x;
              }

            is.reset();
            readBufferPos = 0;
            readBufferLength = 0;
            readBufferOverflow = -1;
            line = 1;
            currentByteCount = column = 0;
            
            sourceType = INPUT_READER;
            this.reader = new InputStreamReader(is, encoding);
            is = null;
            
            tryEncodingDecl(true);
            
          }
        catch (IOException e)
          {
            error("unsupported text encoding",
                  encoding,
                  null);
          }
      }
  }

  /**
   * Check for an encoding declaration.  This is the second part of the
   * XML encoding autodetection algorithm, relying on detectEncoding to
   * get to the point that this part can read any encoding declaration
   * in the document (using only US-ASCII characters).
   *
   * <p> Because this part starts to fill parser buffers with this data,
   * it's tricky to setup a reader so that Java's built-in decoders can be
   * used for the character encodings that aren't built in to this parser
   * (such as EUC-JP, KOI8-R, Big5, etc).
   *
   * @return any encoding in the declaration, uppercased; or null
   * @see detectEncoding
   */
  private String tryEncodingDecl(boolean ignoreEncoding)
    throws SAXException, IOException
  {
    // Read the XML/text declaration.
    if (tryRead("<?xml"))
      {
        if (tryWhitespace())
          {
            if (inputStack.size() > 0)
              {
                return parseTextDecl(ignoreEncoding);
              }
            else
              {
                return parseXMLDecl(ignoreEncoding);
              }
          }
        else
          {
            // <?xml-stylesheet ...?> or similar
            unread('l');
            unread('m');
            unread('x');
            unread('?');
            unread('<');
          }
      }
    return null;
  }

  /**
   * Attempt to detect the encoding of an entity.
   * <p>The trick here (as suggested in the XML standard) is that
   * any entity not in UTF-8, or in UCS-2 with a byte-order mark, 
   * <b>must</b> begin with an XML declaration or an encoding
   * declaration; we simply have to look for "&lt;?xml" in various
   * encodings.
   * <p>This method has no way to distinguish among 8-bit encodings.
   * Instead, it sets up for UTF-8, then (possibly) revises its assumption
   * later in setupDecoding ().  Any ASCII-derived 8-bit encoding
   * should work, but most will be rejected later by setupDecoding ().
   * @see #tryEncoding (byte[], byte, byte, byte, byte)
   * @see #tryEncoding (byte[], byte, byte)
   * @see #setupDecoding
   */
  private void detectEncoding()
    throws SAXException, IOException
  {
    byte[] signature = new byte[4];

    // Read the first four bytes for
    // autodetection.
    is.mark(4);
    is.read(signature);
    is.reset();

    //
    // FIRST:  four byte encodings (who uses these?)
    //
    if (tryEncoding(signature, (byte) 0x00, (byte) 0x00,
                    (byte) 0x00, (byte) 0x3c))
      {
        // UCS-4 must begin with "<?xml"
        // 0x00 0x00 0x00 0x3c: UCS-4, big-endian (1234)
        // "UTF-32BE"
        encoding = ENCODING_UCS_4_1234;
      }
    else if (tryEncoding(signature, (byte) 0x3c, (byte) 0x00,
                         (byte) 0x00, (byte) 0x00))
      {
        // 0x3c 0x00 0x00 0x00: UCS-4, little-endian (4321)
        // "UTF-32LE"
        encoding = ENCODING_UCS_4_4321;
      }
    else if (tryEncoding(signature, (byte) 0x00, (byte) 0x00,
                         (byte) 0x3c, (byte) 0x00))
      {
        // 0x00 0x00 0x3c 0x00: UCS-4, unusual (2143)
        encoding = ENCODING_UCS_4_2143;
      }
    else if (tryEncoding(signature, (byte) 0x00, (byte) 0x3c,
                         (byte) 0x00, (byte) 0x00))
      {
        // 0x00 0x3c 0x00 0x00: UCS-4, unusual (3421)
        encoding = ENCODING_UCS_4_3412;

        // 00 00 fe ff UCS_4_1234 (with BOM)
        // ff fe 00 00 UCS_4_4321 (with BOM)
      }

    //
    // SECOND:  two byte encodings
    // note ... with 1/14/2000 errata the XML spec identifies some
    // more "broken UTF-16" autodetection cases, with no XML decl,
    // which we don't handle here (that's legal too).
    //
    else if (tryEncoding(signature, (byte) 0xfe, (byte) 0xff))
      {
        // UCS-2 with a byte-order marker. (UTF-16)
        // 0xfe 0xff: UCS-2, big-endian (12)
        encoding = ENCODING_UCS_2_12;
        is.read(); is.read();
      }
    else if (tryEncoding(signature, (byte) 0xff, (byte) 0xfe))
      {
        // UCS-2 with a byte-order marker. (UTF-16)
        // 0xff 0xfe: UCS-2, little-endian (21)
        encoding = ENCODING_UCS_2_21;
        is.read(); is.read();
      }
    else if (tryEncoding(signature, (byte) 0x00, (byte) 0x3c,
                         (byte) 0x00, (byte) 0x3f))
      {
        // UTF-16BE (otherwise, malformed UTF-16)
        // 0x00 0x3c 0x00 0x3f: UCS-2, big-endian, no byte-order mark
        encoding = ENCODING_UCS_2_12;
        error("no byte-order mark for UCS-2 entity");
      }
    else if (tryEncoding(signature, (byte) 0x3c, (byte) 0x00,
                         (byte) 0x3f, (byte) 0x00))
      {
        // UTF-16LE (otherwise, malformed UTF-16)
        // 0x3c 0x00 0x3f 0x00: UCS-2, little-endian, no byte-order mark
        encoding = ENCODING_UCS_2_21;
        error("no byte-order mark for UCS-2 entity");
      }

    //
    // THIRD:  ASCII-derived encodings, fixed and variable lengths
    //
    else if (tryEncoding(signature, (byte) 0x3c, (byte) 0x3f,
                         (byte) 0x78, (byte) 0x6d))
      {
        // ASCII derived
        // 0x3c 0x3f 0x78 0x6d: UTF-8 or other 8-bit markup (read ENCODING)
        encoding = ENCODING_UTF_8;
        prefetchASCIIEncodingDecl();
      }
    else if (signature[0] == (byte) 0xef
             && signature[1] == (byte) 0xbb
             && signature[2] == (byte) 0xbf)
      {
        // 0xef 0xbb 0xbf: UTF-8 BOM (not part of document text)
        // this un-needed notion slipped into XML 2nd ed through a
        // "non-normative" erratum; now required by MSFT and UDDI,
        // and E22 made it normative.
        encoding = ENCODING_UTF_8;
        is.read(); is.read(); is.read();
      }
    else
      {
        // 4c 6f a7 94 ... we don't understand EBCDIC flavors
        // ... but we COULD at least kick in some fixed code page
        
        // (default) UTF-8 without encoding/XML declaration
        encoding = ENCODING_UTF_8;
      }
  }

  /**
   * Check for a four-byte signature.
   * <p>Utility routine for detectEncoding ().
   * <p>Always looks for some part of "<?XML" in a specific encoding.
   * @param sig The first four bytes read.
   * @param b1 The first byte of the signature
   * @param b2 The second byte of the signature
   * @param b3 The third byte of the signature
   * @param b4 The fourth byte of the signature
   * @see #detectEncoding
   */
  private static boolean tryEncoding(byte[] sig, byte b1, byte b2,
                                     byte b3, byte b4)
  {
    return (sig[0] == b1 && sig[1] == b2
            && sig[2] == b3 && sig[3] == b4);
  }

  /**
   * Check for a two-byte signature.
   * <p>Looks for a UCS-2 byte-order mark.
   * <p>Utility routine for detectEncoding ().
   * @param sig The first four bytes read.
   * @param b1 The first byte of the signature
   * @param b2 The second byte of the signature
   * @see #detectEncoding
   */
  private static boolean tryEncoding(byte[] sig, byte b1, byte b2)
  {
    return ((sig[0] == b1) && (sig[1] == b2));
  }

  /**
   * This method pushes a string back onto input.
   * <p>It is useful either as the expansion of an internal entity, 
   * or for backtracking during the parse.
   * <p>Call pushCharArray () to do the actual work.
   * @param s The string to push back onto input.
   * @see #pushCharArray
   */
  private void pushString(String ename, String s)
    throws SAXException
  {
    char[] ch = s.toCharArray();
    pushCharArray(ename, ch, 0, ch.length);
  }

  /**
   * Push a new internal input source.
   * <p>This method is useful for expanding an internal entity,
   * or for unreading a string of characters.  It creates a new
   * readBuffer containing the characters in the array, instead
   * of characters converted from an input byte stream.
   * @param ch The char array to push.
   * @see #pushString
   * @see #pushURL
   * @see #readBuffer
   * @see #sourceType
   * @see #pushInput
   */
  private void pushCharArray(String ename, char[] ch, int start, int length)
    throws SAXException
  {
    // Push the existing status
    pushInput(ename);
    if (ename != null && doReport)
      {
        dataBufferFlush();
        handler.startInternalEntity(ename);
      }
    sourceType = INPUT_INTERNAL;
    readBuffer = ch;
    readBufferPos = start;
    readBufferLength = length;
    readBufferOverflow = -1;
  }

  /**
   * Save the current input source onto the stack.
   * <p>This method saves all of the global variables associated with
   * the current input source, so that they can be restored when a new
   * input source has finished.  It also tests for entity recursion.
   * <p>The method saves the following global variables onto a stack
   * using a fixed-length array:
   * <ol>
   * <li>sourceType
   * <li>externalEntity
   * <li>readBuffer
   * <li>readBufferPos
   * <li>readBufferLength
   * <li>line
   * <li>encoding
   * </ol>
   * @param ename The name of the entity (if any) causing the new input.
   * @see #popInput
   * @see #sourceType
   * @see #externalEntity
   * @see #readBuffer
   * @see #readBufferPos
   * @see #readBufferLength
   * @see #line
   * @see #encoding
   */
  private void pushInput(String ename)
    throws SAXException
  {
    // Check for entity recursion.
    if (ename != null)
      {
        Iterator entities = entityStack.iterator();
        while (entities.hasNext())
          {
            String e = (String) entities.next();
            if (e != null && e == ename)
              {
                error("recursive reference to entity", ename, null);
              }
          }
      }
    entityStack.addLast(ename);
    
    // Don't bother if there is no current input.
    if (sourceType == INPUT_NONE)
      {
        return;
      }
    
    // Set up a snapshot of the current
    // input source.
    Input input = new Input();

    input.sourceType = sourceType;
    input.externalEntity = externalEntity;
    input.readBuffer = readBuffer;
    input.readBufferPos = readBufferPos;
    input.readBufferLength = readBufferLength;
    input.line = line;
    input.encoding = encoding;
    input.readBufferOverflow = readBufferOverflow;
    input.is = is;
    input.currentByteCount = currentByteCount;
    input.column = column;
    input.reader = reader;
    
    // Push it onto the stack.
    inputStack.addLast(input);
  }

  /**
   * Restore a previous input source.
   * <p>This method restores all of the global variables associated with
   * the current input source.
   * @exception java.io.EOFException
   *    If there are no more entries on the input stack.
   * @see #pushInput
   * @see #sourceType
   * @see #externalEntity
   * @see #readBuffer
   * @see #readBufferPos
   * @see #readBufferLength
   * @see #line
   * @see #encoding
   */
  private void popInput()
    throws SAXException, IOException
  {
    String ename = (String) entityStack.removeLast();

    if (ename != null && doReport)
      {
        dataBufferFlush();
      }
    switch (sourceType)
      {
      case INPUT_STREAM:
        handler.endExternalEntity(ename);
        is.close();
        break;
      case INPUT_READER:
        handler.endExternalEntity(ename);
        reader.close();
        break;
      case INPUT_INTERNAL:
        if (ename != null && doReport)
          {
            handler.endInternalEntity(ename);
          }
        break;
      }

    // Throw an EOFException if there
    // is nothing else to pop.
    if (inputStack.isEmpty())
      {
        throw new EOFException("no more input");
      }

    Input input = (Input) inputStack.removeLast();

    sourceType = input.sourceType;
    externalEntity = input.externalEntity;
    readBuffer = input.readBuffer;
    readBufferPos = input.readBufferPos;
    readBufferLength = input.readBufferLength;
    line = input.line;
    encoding = input.encoding;
    readBufferOverflow = input.readBufferOverflow;
    is = input.is;
    currentByteCount = input.currentByteCount;
    column = input.column;
    reader = input.reader;
  }
  
  /**
   * Return true if we can read the expected character.
   * <p>Note that the character will be removed from the input stream
   * on success, but will be put back on failure.  Do not attempt to
   * read the character again if the method succeeds.
   * @param delim The character that should appear next.  For a
   *        insensitive match, you must supply this in upper-case.
   * @return true if the character was successfully read, or false if
   *   it was not.
   * @see #tryRead (String)
   */
  private boolean tryRead(char delim)
    throws SAXException, IOException
  {
    char c;
    
    // Read the character
    c = readCh();

    // Test for a match, and push the character
    // back if the match fails.
    if (c == delim)
      {
        return true;
      }
    else
      {
        unread(c);
        return false;
      }
  }

  /**
   * Return true if we can read the expected string.
   * <p>This is simply a convenience method.
   * <p>Note that the string will be removed from the input stream
   * on success, but will be put back on failure.  Do not attempt to
   * read the string again if the method succeeds.
   * <p>This method will push back a character rather than an
   * array whenever possible (probably the majority of cases).
   * @param delim The string that should appear next.
   * @return true if the string was successfully read, or false if
   *   it was not.
   * @see #tryRead (char)
   */
  private boolean tryRead(String delim)
    throws SAXException, IOException
  {
    return tryRead(delim.toCharArray());
  }

  private boolean tryRead(char[] ch)
    throws SAXException, IOException
  {
    char c;

    // Compare the input, character-
    // by character.
    
    for (int i = 0; i < ch.length; i++)
      {
        c = readCh();
        if (c != ch[i])
          {
            unread(c);
            if (i != 0)
              {
                unread(ch, i);
              }
            return false;
          }
      }
    return true;
  }

  /**
   * Return true if we can read some whitespace.
   * <p>This is simply a convenience method.
   * <p>This method will push back a character rather than an
   * array whenever possible (probably the majority of cases).
   * @return true if whitespace was found.
   */
  private boolean tryWhitespace()
    throws SAXException, IOException
  {
    char c;
    c = readCh();
    if (isWhitespace(c))
      {
        skipWhitespace();
        return true;
      }
    else
      {
        unread(c);
        return false;
      }
  }
  
  /**
   * Read all data until we find the specified string.
   * This is useful for scanning CDATA sections and PIs.
   * <p>This is inefficient right now, since it calls tryRead ()
   * for every character.
   * @param delim The string delimiter
   * @see #tryRead (String, boolean)
   * @see #readCh
   */
  private void parseUntil(String delim)
    throws SAXException, IOException
  {
    parseUntil(delim.toCharArray());
  }

  private void parseUntil(char[] delim)
    throws SAXException, IOException
  {
    char c;
    int startLine = line;
    
    try
      {
        while (!tryRead(delim))
          {
            c = readCh();
            dataBufferAppend(c);
          }
      }
    catch (EOFException e)
      {
        error("end of input while looking for delimiter "
              + "(started on line " + startLine
              + ')', null, new String(delim));
      }
  }

  //////////////////////////////////////////////////////////////////////
  // Low-level I/O.
  //////////////////////////////////////////////////////////////////////
  
  /**
   * Prefetch US-ASCII XML/text decl from input stream into read buffer.
   * Doesn't buffer more than absolutely needed, so that when an encoding
   * decl says we need to create an InputStreamReader, we can discard our
   * buffer and reset().  Caller knows the first chars of the decl exist
   * in the input stream.
   */
  private void prefetchASCIIEncodingDecl()
    throws SAXException, IOException
  {
    int ch;
    readBufferPos = readBufferLength = 0;
    
    is.mark(readBuffer.length);
    while (true)
      {
        ch = is.read();
        readBuffer[readBufferLength++] = (char) ch;
        switch (ch)
          {
          case (int) '>':
            return;
          case -1:
            error("file ends before end of XML or encoding declaration.",
                  null, "?>");
          }
        if (readBuffer.length == readBufferLength)
          {
            error("unfinished XML or encoding declaration");
          }
      }
  }

  /**
   * Read a chunk of data from an external input source.
   * <p>This is simply a front-end that fills the rawReadBuffer
   * with bytes, then calls the appropriate encoding handler.
   * @see #encoding
   * @see #rawReadBuffer
   * @see #readBuffer
   * @see #filterCR
   * @see #copyUtf8ReadBuffer
   * @see #copyIso8859_1ReadBuffer
   * @see #copyUcs_2ReadBuffer
   * @see #copyUcs_4ReadBuffer
   */
  private void readDataChunk()
    throws SAXException, IOException
  {
    int count;
    
    // See if we have any overflow (filterCR sets for CR at end)
    if (readBufferOverflow > -1)
      {
        readBuffer[0] = (char) readBufferOverflow;
        readBufferOverflow = -1;
        readBufferPos = 1;
        sawCR = true;
      }
    else
      {
        readBufferPos = 0;
        sawCR = false;
      }

    // input from a character stream.
    if (sourceType == INPUT_READER)
      {
        count = reader.read(readBuffer,
                            readBufferPos, READ_BUFFER_MAX - readBufferPos);
        if (count < 0)
          {
            readBufferLength = readBufferPos;
          }
        else
          {
            readBufferLength = readBufferPos + count;
          }
        if (readBufferLength > 0)
          {
            filterCR(count >= 0);
          }
        sawCR = false;
        return;
      }
    
    // Read as many bytes as possible into the raw buffer.
    count = is.read(rawReadBuffer, 0, READ_BUFFER_MAX);

    // Dispatch to an encoding-specific reader method to populate
    // the readBuffer.  In most parser speed profiles, these routines
    // show up at the top of the CPU usage chart.
    if (count > 0)
      {
        switch (encoding)
          {
            // one byte builtins
          case ENCODING_ASCII:
            copyIso8859_1ReadBuffer(count, (char) 0x0080);
            break;
          case ENCODING_UTF_8:
            copyUtf8ReadBuffer(count);
            break;
          case ENCODING_ISO_8859_1:
            copyIso8859_1ReadBuffer(count, (char) 0);
            break;

            // two byte builtins
          case ENCODING_UCS_2_12:
            copyUcs2ReadBuffer(count, 8, 0);
            break;
          case ENCODING_UCS_2_21:
            copyUcs2ReadBuffer(count, 0, 8);
            break;
            
            // four byte builtins
          case ENCODING_UCS_4_1234:
            copyUcs4ReadBuffer(count, 24, 16, 8, 0);
            break;
          case ENCODING_UCS_4_4321:
            copyUcs4ReadBuffer(count, 0, 8, 16, 24);
            break;
          case ENCODING_UCS_4_2143:
            copyUcs4ReadBuffer(count, 16, 24, 0, 8);
            break;
          case ENCODING_UCS_4_3412:
            copyUcs4ReadBuffer(count, 8, 0, 24, 16);
            break;
          }
      }
    else
      {
        readBufferLength = readBufferPos;
      }

    readBufferPos = 0;
    
    // Filter out all carriage returns if we've seen any
    // (including any saved from a previous read)
    if (sawCR)
      {
        filterCR(count >= 0);
        sawCR = false;
        
        // must actively report EOF, lest some CRs get lost.
        if (readBufferLength == 0 && count >= 0)
          {
            readDataChunk();
          }
      }
    
    if (count > 0)
      {
        currentByteCount += count;
      }
  }
  
  /**
   * Filter carriage returns in the read buffer.
   * CRLF becomes LF; CR becomes LF.
   * @param moreData true iff more data might come from the same source
   * @see #readDataChunk
   * @see #readBuffer
   * @see #readBufferOverflow
   */
  private void filterCR(boolean moreData)
  {
    int i, j;

    readBufferOverflow = -1;
    
loop:
    for (i = j = readBufferPos; j < readBufferLength; i++, j++)
      {
        switch (readBuffer[j])
          {
          case '\r':
            if (j == readBufferLength - 1)
              {
                if (moreData)
                  {
                    readBufferOverflow = '\r';
                    readBufferLength--;
                  }
                else   // CR at end of buffer
                  {
                    readBuffer[i++] = '\n';
                  }
                break loop;
              }
            else if (readBuffer[j + 1] == '\n')
              {
                j++;
              }
            readBuffer[i] = '\n';
            break;

          case '\n':
          default:
            readBuffer[i] = readBuffer[j];
            break;
          }
      }
    readBufferLength = i;
  }

  /**
   * Convert a buffer of UTF-8-encoded bytes into UTF-16 characters.
   * <p>When readDataChunk () calls this method, the raw bytes are in 
   * rawReadBuffer, and the final characters will appear in 
   * readBuffer.
   * <p>Note that as of Unicode 3.1, good practice became a requirement,
   * so that each Unicode character has exactly one UTF-8 representation.
   * @param count The number of bytes to convert.
   * @see #readDataChunk
   * @see #rawReadBuffer
   * @see #readBuffer
   * @see #getNextUtf8Byte
   */
  private void copyUtf8ReadBuffer(int count)
    throws SAXException, IOException
  {
    int i = 0;
    int j = readBufferPos;
    int b1;
    char c = 0;
    
    /*
    // check once, so the runtime won't (if it's smart enough)
    if (count < 0 || count > rawReadBuffer.length)
    throw new ArrayIndexOutOfBoundsException (Integer.toString (count));
     */

    while (i < count)
      {
        b1 = rawReadBuffer[i++];

        // Determine whether we are dealing
        // with a one-, two-, three-, or four-
        // byte sequence.
        if (b1 < 0)
          {
            if ((b1 & 0xe0) == 0xc0)
              {
                // 2-byte sequence: 00000yyyyyxxxxxx = 110yyyyy 10xxxxxx
                c = (char) (((b1 & 0x1f) << 6)
                            | getNextUtf8Byte(i++, count));
                if (c < 0x0080)
                  {
                    encodingError("Illegal two byte UTF-8 sequence",
                                  c, 0);
                  }
                
                //Sec 2.11
                // [1] the two-character sequence #xD #xA
                // [2] the two-character sequence #xD #x85
                if ((c == 0x0085 || c == 0x000a) && sawCR)
                  {
                    continue;
                  }
                
                // Sec 2.11
                // [3] the single character #x85
                
                if (c == 0x0085 && xmlVersion == XML_11)
                  {
                    readBuffer[j++] = '\r';
                  }
              }
            else if ((b1 & 0xf0) == 0xe0)
              {
                // 3-byte sequence:
                // zzzzyyyyyyxxxxxx = 1110zzzz 10yyyyyy 10xxxxxx
                // most CJKV characters
                c = (char) (((b1 & 0x0f) << 12) |
                            (getNextUtf8Byte(i++, count) << 6) |
                            getNextUtf8Byte(i++, count));
                //sec 2.11
                //[4] the single character #x2028
                if (c == 0x2028 && xmlVersion == XML_11)
                  {
                    readBuffer[j++] = '\r';
                    sawCR = true;
                    continue;
                  }
                if (c < 0x0800 || (c >= 0xd800 && c <= 0xdfff))
                  {
                    encodingError("Illegal three byte UTF-8 sequence",
                                  c, 0);
                  }
              }
            else if ((b1 & 0xf8) == 0xf0)
              {
                // 4-byte sequence: 11101110wwwwzzzzyy + 110111yyyyxxxxxx
                //     = 11110uuu 10uuzzzz 10yyyyyy 10xxxxxx
                // (uuuuu = wwww + 1)
                // "Surrogate Pairs" ... from the "Astral Planes"
                // Unicode 3.1 assigned the first characters there
                int iso646 = b1 & 07;
                iso646 = (iso646 << 6) + getNextUtf8Byte(i++, count);
                iso646 = (iso646 << 6) + getNextUtf8Byte(i++, count);
                iso646 = (iso646 << 6) + getNextUtf8Byte(i++, count);
                
                if (iso646 <= 0xffff)
                  {
                    encodingError("Illegal four byte UTF-8 sequence",
                                  iso646, 0);
                  }
                else
                  {
                    if (iso646 > 0x0010ffff)
                      {
                        encodingError("UTF-8 value out of range for Unicode",
                                      iso646, 0);
                      }
                    iso646 -= 0x010000;
                    readBuffer[j++] = (char) (0xd800 | (iso646 >> 10));
                    readBuffer[j++] = (char) (0xdc00 | (iso646 & 0x03ff));
                    continue;
                  }
              }
            else
              {
                // The five and six byte encodings aren't supported;
                // they exceed the Unicode (and XML) range.
                encodingError("unsupported five or six byte UTF-8 sequence",
                              0xff & b1, i);
                // NOTREACHED
                c = 0;
              }
          }
        else
          {
            // 1-byte sequence: 000000000xxxxxxx = 0xxxxxxx
            // (US-ASCII character, "common" case, one branch to here)
            c = (char) b1;
          }
        readBuffer[j++] = c;
        if (c == '\r')
          {
            sawCR = true;
          }
      }
    // How many characters have we read?
    readBufferLength = j;
  }
   
  /**
   * Return the next byte value in a UTF-8 sequence.
   * If it is not possible to get a byte from the current
   * entity, throw an exception.
   * @param pos The current position in the rawReadBuffer.
   * @param count The number of bytes in the rawReadBuffer
   * @return The significant six bits of a non-initial byte in
   *   a UTF-8 sequence.
   * @exception EOFException If the sequence is incomplete.
   */
  private int getNextUtf8Byte(int pos, int count)
    throws SAXException, IOException
  {
    int val;
    
    // Take a character from the buffer
    // or from the actual input stream.
    if (pos < count)
      {
        val = rawReadBuffer[pos];
      }
    else
      {
        val = is.read();
        if (val == -1)
          {
            encodingError("unfinished multi-byte UTF-8 sequence at EOF",
                          -1, pos);
          }
      }

    // Check for the correct bits at the start.
    if ((val & 0xc0) != 0x80)
      {
        encodingError("bad continuation of multi-byte UTF-8 sequence",
                      val, pos + 1);
      }

    // Return the significant bits.
    return (val & 0x3f);
  }

  /**
   * Convert a buffer of US-ASCII or ISO-8859-1-encoded bytes into
   * UTF-16 characters.
   *
   * <p>When readDataChunk () calls this method, the raw bytes are in 
   * rawReadBuffer, and the final characters will appear in 
   * readBuffer.
   *
   * @param count The number of bytes to convert.
   * @param mask For ASCII conversion, 0x7f; else, 0xff.
   * @see #readDataChunk
   * @see #rawReadBuffer
   * @see #readBuffer
   */
  private void copyIso8859_1ReadBuffer(int count, char mask)
    throws IOException
  {
    int i, j;
    for (i = 0, j = readBufferPos; i < count; i++, j++)
      {
        char c = (char) (rawReadBuffer[i] & 0xff);
        if ((c & mask) != 0)
          {
            throw new CharConversionException("non-ASCII character U+"
                                              + Integer.toHexString(c));
          }
        if (c == 0x0085 && xmlVersion == XML_11)
          {
            c = '\r';
          }
        readBuffer[j] = c;
        if (c == '\r')
          {
            sawCR = true;
          }
      }
    readBufferLength = j;
  }

  /**
   * Convert a buffer of UCS-2-encoded bytes into UTF-16 characters
   * (as used in Java string manipulation).
   *
   * <p>When readDataChunk () calls this method, the raw bytes are in 
   * rawReadBuffer, and the final characters will appear in 
   * readBuffer.
   * @param count The number of bytes to convert.
   * @param shift1 The number of bits to shift byte 1.
   * @param shift2 The number of bits to shift byte 2
   * @see #readDataChunk
   * @see #rawReadBuffer
   * @see #readBuffer
   */
  private void copyUcs2ReadBuffer(int count, int shift1, int shift2)
    throws SAXException
  {
    int j = readBufferPos;
    
    if (count > 0 && (count % 2) != 0)
      {
        encodingError("odd number of bytes in UCS-2 encoding", -1, count);
      }
    // The loops are faster with less internal brancing; hence two
    if (shift1 == 0)
      {  // "UTF-16-LE"
        for (int i = 0; i < count; i += 2)
          {
            char c = (char) (rawReadBuffer[i + 1] << 8);
            c |= 0xff & rawReadBuffer[i];
            readBuffer[j++] = c;
            if (c == '\r')
              {
                sawCR = true;
              }
          }
      }
    else
      {  // "UTF-16-BE"
        for (int i = 0; i < count; i += 2)
          {
            char c = (char) (rawReadBuffer[i] << 8);
            c |= 0xff & rawReadBuffer[i + 1];
            readBuffer[j++] = c;
            if (c == '\r')
              {
                sawCR = true;
              }
          }
      }
    readBufferLength = j;
  }

  /**
   * Convert a buffer of UCS-4-encoded bytes into UTF-16 characters.
   *
   * <p>When readDataChunk () calls this method, the raw bytes are in 
   * rawReadBuffer, and the final characters will appear in 
   * readBuffer.
   * <p>Java has Unicode chars, and this routine uses surrogate pairs
   * for ISO-10646 values between 0x00010000 and 0x000fffff.  An
   * exception is thrown if the ISO-10646 character has no Unicode
   * representation.
   *
   * @param count The number of bytes to convert.
   * @param shift1 The number of bits to shift byte 1.
   * @param shift2 The number of bits to shift byte 2
   * @param shift3 The number of bits to shift byte 2
   * @param shift4 The number of bits to shift byte 2
   * @see #readDataChunk
   * @see #rawReadBuffer
   * @see #readBuffer
   */
  private void copyUcs4ReadBuffer(int count, int shift1, int shift2,
                                  int shift3, int shift4)
    throws SAXException
  {
    int j = readBufferPos;
    
    if (count > 0 && (count % 4) != 0)
      {
        encodingError("number of bytes in UCS-4 encoding " +
                      "not divisible by 4",
                      -1, count);
      }
    for (int i = 0; i < count; i += 4)
      {
        int value = (((rawReadBuffer [i] & 0xff) << shift1) |
                     ((rawReadBuffer [i + 1] & 0xff) << shift2) |
                     ((rawReadBuffer [i + 2] & 0xff) << shift3) |
                     ((rawReadBuffer [i + 3] & 0xff) << shift4));
        if (value < 0x0000ffff)
          {
            readBuffer [j++] = (char) value;
            if (value == (int) '\r')
              {
                sawCR = true;
              }
          }
        else if (value < 0x0010ffff)
          {
            value -= 0x010000;
            readBuffer[j++] = (char) (0xd8 | ((value >> 10) & 0x03ff));
            readBuffer[j++] = (char) (0xdc | (value & 0x03ff));
          }
        else
          {
            encodingError("UCS-4 value out of range for Unicode",
                          value, i);
          }
      }
    readBufferLength = j;
  }

  /**
   * Report a character encoding error.
   */
  private void encodingError(String message, int value, int offset)
    throws SAXException
  {
    if (value != -1)
      {
        message = message + " (character code: 0x" +
          Integer.toHexString(value) + ')';
        error(message);
      }
  }
  
  //////////////////////////////////////////////////////////////////////
  // Local Variables.
  //////////////////////////////////////////////////////////////////////
  
  /**
   * Re-initialize the variables for each parse.
   */
  private void initializeVariables()
  {
    // First line
    line = 1;
    column = 0;
    
    // Set up the buffers for data and names
    dataBufferPos = 0;
    dataBuffer = new char[DATA_BUFFER_INITIAL];
    nameBufferPos = 0;
    nameBuffer = new char[NAME_BUFFER_INITIAL];

    // Set up the DTD hash tables
    elementInfo = new HashMap();
    entityInfo = new HashMap();
    notationInfo = new HashMap();
    skippedPE = false;

    // Set up the variables for the current
    // element context.
    currentElement = null;
    currentElementContent = CONTENT_UNDECLARED;
    
    // Set up the input variables
    sourceType = INPUT_NONE;
    inputStack = new LinkedList();
    entityStack = new LinkedList();
    externalEntity = null;
    tagAttributePos = 0;
    tagAttributes = new String[100];
    rawReadBuffer = new byte[READ_BUFFER_MAX];
    readBufferOverflow = -1;

    scratch = new InputSource();

    inLiteral = false;
    expandPE = false;
    peIsError = false;
    
    doReport = false;
    
    inCDATA = false;
    
    symbolTable = new Object[SYMBOL_TABLE_LENGTH][];
  }

  static class ExternalIdentifiers
  {

    String publicId;
    String systemId;
    String baseUri;

    ExternalIdentifiers()
    {
    }

    ExternalIdentifiers(String publicId, String systemId, String baseUri)
    {
      this.publicId = publicId;
      this.systemId = systemId;
      this.baseUri = baseUri;
    }
    
  }

  static class EntityInfo
  {

    int type;
    ExternalIdentifiers ids;
    String value;
    String notationName;
    
  }

  static class AttributeDecl
  {
    
    String type;
    String value;
    int valueType;
    String enumeration;
    String defaultValue;

  }

  static class ElementDecl
  {
    
    int contentType;
    String contentModel;
    HashMap attributes;
  
  }
 
  static class Input
  {
    
    int sourceType;
    URLConnection externalEntity;
    char[] readBuffer;
    int readBufferPos;
    int readBufferLength;
    int line;
    int encoding;
    int readBufferOverflow;
    InputStream is;
    int currentByteCount;
    int column;
    Reader reader;
    
  }
  
}

