/* SAXDriver.java -- 
 Copyright (C) 1999,2000,2001,2004 Free Software Foundation, Inc.
 Portions Copyright 2006-2007 Henri Sivonen
 Portions Copyright 2007-2016 Mozilla Foundation

 This file is part of GNU JAXP.

 GNU JAXP 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 JAXP 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 JAXP; 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.

 Portions 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 nu.validator.gnu.xml.aelfred2;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Stack;

import nu.validator.htmlparser.common.CharacterHandler;

import org.xml.sax.AttributeList;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.DTDHandler;
import org.xml.sax.DocumentHandler;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.Parser;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.ext.Attributes2;
import org.xml.sax.ext.DeclHandler;
import org.xml.sax.ext.DefaultHandler2;
import org.xml.sax.ext.EntityResolver2;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.ext.Locator2;
import org.xml.sax.helpers.NamespaceSupport;

/**
 * An enhanced SAX2 version of Microstar's &AElig;lfred XML parser. The
 * enhancements primarily relate to significant improvements in conformance to
 * the XML specification, and SAX2 support. Performance has been improved. See
 * the package level documentation for more information.
 * 
 * <table border="1" width='100%' cellpadding='3' cellspacing='0'>
 * <tr bgcolor='#ccccff'>
 * <th><font size='+1'>Name</font></th>
 * <th><font size='+1'>Notes</font></th>
 * </tr>
 * 
 * <tr>
 * <td colspan=2><center><em>Features ... URL prefix is
 * <b>http://xml.org/sax/features/</b></em></center></td>
 * </tr>
 * 
 * <tr>
 * <td>(URL)/external-general-entities</td>
 * <td>Value defaults to <em>true</em></td>
 * </tr>
 * <tr>
 * <td>(URL)/external-parameter-entities</td>
 * <td>Value defaults to <em>true</em></td>
 * </tr>
 * <tr>
 * <td>(URL)/is-standalone</td>
 * <td>(PRELIMINARY) Returns true iff the document's parsing has started (some
 * non-error event after <em>startDocument()</em> was reported) and the
 * document's standalone flag is set.</td>
 * </tr>
 * <tr>
 * <td>(URL)/namespace-prefixes</td>
 * <td>Value defaults to <em>false</em> (but XML 1.0 names are always
 * reported)</td>
 * </tr>
 * <tr>
 * <td>(URL)/lexical-handler/parameter-entities</td>
 * <td>Value is fixed at <em>true</em></td>
 * </tr>
 * <tr>
 * <td>(URL)/namespaces</td>
 * <td>Value defaults to <em>true</em></td>
 * </tr>
 * <tr>
 * <td>(URL)/resolve-dtd-uris</td>
 * <td>(PRELIMINARY) Value defaults to <em>true</em></td>
 * </tr>
 * <tr>
 * <td>(URL)/string-interning</td>
 * <td>Value is fixed at <em>true</em></td>
 * </tr>
 * <tr>
 * <td>(URL)/use-attributes2</td>
 * <td>(PRELIMINARY) Value is fixed at <em>true</em></td>
 * </tr>
 * <tr>
 * <td>(URL)/use-entity-resolver2</td>
 * <td>(PRELIMINARY) Value defaults to <em>true</em></td>
 * </tr>
 * <tr>
 * <td>(URL)/validation</td>
 * <td>Value is fixed at <em>false</em></td>
 * </tr>
 * 
 * <tr>
 * <td colspan=2><center><em>Handler Properties ... URL prefix is
 * <b>http://xml.org/sax/properties/</b></em></center></td>
 * </tr>
 * 
 * <tr>
 * <td>(URL)/declaration-handler</td>
 * <td>A declaration handler may be provided. </td>
 * </tr>
 * <tr>
 * <td>(URL)/lexical-handler</td>
 * <td>A lexical handler may be provided. </td>
 * </tr>
 * </table>
 * 
 * <p>
 * This parser currently implements the SAX1 Parser API, but it may not continue
 * to do so in the future.
 * 
 * @author Written by David Megginson (version 1.2a from Microstar)
 * @author Updated by David Brownell &lt;dbrownell@users.sourceforge.net&gt;
 * @see org.xml.sax.Parser
 */
@SuppressWarnings("deprecation") final public class SAXDriver implements Locator2, Attributes2, XMLReader,
        Parser, AttributeList {

    private final DefaultHandler2 base = new DefaultHandler2();

    private XmlParser parser;

    private EntityResolver entityResolver = base;

    private EntityResolver2 resolver2 = null;

    private ContentHandler contentHandler = base;

    private DTDHandler dtdHandler = base;

    private ErrorHandler errorHandler = base;

    private DeclHandler declHandler = base;

    private LexicalHandler lexicalHandler = base;

    private String elementName;

    private Stack<String> entityStack;

    // one vector (of object/struct): faster, smaller
    private List<Attribute> attributesList;

    private boolean namespaces = true;

    private boolean xmlNames = false;

    private boolean extGE = true;

    private boolean extPE = true;

    private boolean resolveAll = true;

    private boolean useResolver2 = true;

    // package private to allow (read-only) access in XmlParser
    boolean stringInterning = true;

    private int attributeCount;

    private boolean attributes;

    private String[] nsTemp;

    private NamespaceSupport prefixStack;

    boolean checkNormalization = false;
    
    private boolean errorHandlerLocked = false;

    CharacterHandler characterHandler = null;
    
    //
    // Constructor.
    //

    /**
     * Constructs a SAX Parser.
     */
    public SAXDriver() {
        reset();
    }

    private void reset() {
        elementName = null;
        entityStack = new Stack<>();
        attributesList = Collections.synchronizedList(new ArrayList<Attribute>());
        attributeCount = 0;
        attributes = false;
        nsTemp = new String[3];
        prefixStack = null;
    }

    //
    // Implementation of org.xml.sax.Parser.
    //

    /**
     * <b>SAX1</b>: Sets the locale used for diagnostics; currently, only
     * locales using the English language are supported.
     * 
     * @param locale
     *            The locale for which diagnostics will be generated
     */
    @Override
    public void setLocale(Locale locale) throws SAXException {
        if ("en".equals(locale.getLanguage())) {
            return;
        }
        throw new SAXException("AElfred2 only supports English locales.");
    }

    /**
     * <b>SAX2</b>: Returns the object used when resolving external entities
     * during parsing (both general and parameter entities).
     */
    @Override
    public EntityResolver getEntityResolver() {
        return (entityResolver == base) ? null : entityResolver;
    }

    /**
     * <b>SAX1, SAX2</b>: Set the entity resolver for this parser.
     *
     * @param resolver
     */
    @Override
    public void setEntityResolver(EntityResolver resolver) {
        if (resolver instanceof EntityResolver2) {
            resolver2 = (EntityResolver2) resolver;
        } else {
            resolver2 = null;
        }
        if (resolver == null) {
            resolver = base;
        }
        entityResolver = resolver;
    }

    /**
     * <b>SAX2</b>: Returns the object used to process declarations related to
     * notations and unparsed entities.
     */
    @Override
    public DTDHandler getDTDHandler() {
        return (dtdHandler == base) ? null : dtdHandler;
    }

    /**
     * <b>SAX1, SAX2</b>: Set the DTD handler for this parser.
     * 
     * @param handler
     *            The object to receive DTD events.
     */
    @Override
    public void setDTDHandler(DTDHandler handler) {
        if (handler == null) {
            handler = base;
        }
        this.dtdHandler = handler;
    }

    /**
     * <b>SAX1</b>: Set the document handler for this parser. If a content
     * handler was set, this document handler will supplant it. The parser is
     * set to report all XML 1.0 names rather than to filter out "xmlns"
     * attributes (the "namespace-prefixes" feature is set to true).
     * 
     * @deprecated SAX2 programs should use the XMLReader interface and a
     *             ContentHandler.
     * 
     * @param handler
     *            The object to receive document events.
     */
    @Deprecated
    @Override
    public void setDocumentHandler(DocumentHandler handler) {
        contentHandler = new Adapter(handler);
        xmlNames = true;
    }

    /**
     * <b>SAX2</b>: Returns the object used to report the logical content of an
     * XML document.
     */
    @Override
    public ContentHandler getContentHandler() {
        return (contentHandler == base) ? null : contentHandler;
    }

    /**
     * <b>SAX2</b>: Assigns the object used to report the logical content of an
     * XML document. If a document handler was set, this content handler will
     * supplant it (but XML 1.0 style name reporting may remain enabled).
     */
    @Override
    public void setContentHandler(ContentHandler handler) {
        if (handler == null) {
            handler = base;
        }
        contentHandler = handler;
    }

    public void lockErrorHandler() {
        errorHandlerLocked = true;
    }
    
    /**
     * <b>SAX1, SAX2</b>: Set the error handler for this parser.
     * 
     * @param handler
     *            The object to receive error events.
     */
    @Override
    public void setErrorHandler(ErrorHandler handler) {
        if (errorHandlerLocked) {
            return;
        }
        if (handler == null) {
            handler = base;
        }
        this.errorHandler = handler;
    }

    /**
     * <b>SAX2</b>: Returns the object used to receive callbacks for XML errors
     * of all levels (fatal, nonfatal, warning); this is never null;
     */
    @Override
    public ErrorHandler getErrorHandler() {
        return (errorHandler == base) ? null : errorHandler;
    }

    /**
     * <b>SAX1, SAX2</b>: Auxiliary API to parse an XML document, used mostly
     * when no URI is available. If you want anything useful to happen, you
     * should set at least one type of handler.
     * 
     * @param source
     *            The XML input source. Don't set 'encoding' unless you know for
     *            a fact that it's correct.
     * @see #setEntityResolver
     * @see #setDTDHandler
     * @see #setContentHandler
     * @see #setErrorHandler
     * @exception SAXException
     *                The handlers may throw any SAXException, and the parser
     *                normally throws SAXParseException objects.
     * @exception IOException
     *                IOExceptions are normally through through the parser if
     *                there are problems reading the source document.
     */
    @Override
    public void parse(InputSource source) throws SAXException, IOException {
        synchronized (base) {
            parser = new XmlParser();
            if (namespaces) {
                prefixStack = new NamespaceSupport();
            } else if (!xmlNames) {
                throw new IllegalStateException();
            }
            parser.setHandler(this);

            try {
                Reader r = source.getCharacterStream();
                InputStream in = source.getByteStream();

                parser.doParse(source.getSystemId(), source.getPublicId(), r,
                        in, source.getEncoding());
            } catch (SAXException e) {
                throw e;
            } catch (IOException e) {
                throw e;
            } catch (RuntimeException e) {
                throw e;
            } catch (Exception e) {
                throw new SAXParseException(e.getMessage(), this, e);
            } finally {
                contentHandler.endDocument();
                reset();
            }
        }
    }

    /**
     * <b>SAX1, SAX2</b>: Preferred API to parse an XML document, using a
     * system identifier (URI).
     */
    @Override
    public void parse(String systemId) throws SAXException, IOException {
        parse(new InputSource(systemId));
    }

    //
    // Implementation of SAX2 "XMLReader" interface
    //
    static final String FEATURE = "http://xml.org/sax/features/";

    static final String PROPERTY = "http://xml.org/sax/properties/";

    /**
     * <b>SAX2</b>: Tells the value of the specified feature flag.
     * 
     * @exception SAXNotRecognizedException
     *                thrown if the feature flag is neither built in, nor yet
     *                assigned.
     */
    @Override
    public boolean getFeature(String featureId)
            throws SAXNotRecognizedException, SAXNotSupportedException {
        if ((FEATURE + "validation").equals(featureId)) {
            return false;
        }

        // external entities (both types) are optionally included
        if ((FEATURE + "external-general-entities").equals(featureId)) {
            return extGE;
        }
        if ((FEATURE + "external-parameter-entities").equals(featureId)) {
            return extPE;
        }

        // element/attribute names are as written in document; no mangling
        if ((FEATURE + "namespace-prefixes").equals(featureId)) {
            return xmlNames;
        }

        // report element/attribute namespaces?
        if ((FEATURE + "namespaces").equals(featureId)) {
            return namespaces;
        }

        // all PEs and GEs are reported
        if ((FEATURE + "lexical-handler/parameter-entities").equals(featureId)) {
            return true;
        }

        // default is true
        if ((FEATURE + "string-interning").equals(featureId)) {
            return stringInterning;
        }

        // EXTENSIONS 1.1

        // always returns isSpecified info
        if ((FEATURE + "use-attributes2").equals(featureId)) {
            return true;
        }

        // meaningful between startDocument/endDocument
        if ((FEATURE + "is-standalone").equals(featureId)) {
            if (parser == null) {
                throw new SAXNotSupportedException(featureId);
            }
            return parser.isStandalone();
        }

        // optionally don't absolutize URIs in declarations
        if ((FEATURE + "resolve-dtd-uris").equals(featureId)) {
            return resolveAll;
        }

        // optionally use resolver2 interface methods, if possible
        if ((FEATURE + "use-entity-resolver2").equals(featureId)) {
            return useResolver2;
        }

        if ("http://xml.org/sax/features/unicode-normalization-checking".equals(featureId)) {
            return checkNormalization;
        }

        throw new SAXNotRecognizedException(featureId);
    }

    // package private
    DeclHandler getDeclHandler() {
        return declHandler;
    }

    // package private
    boolean resolveURIs() {
        return resolveAll;
    }

    /**
     * <b>SAX2</b>: Returns the specified property.
     * 
     * @exception SAXNotRecognizedException
     *                thrown if the property value is neither built in, nor yet
     *                stored.
     */
    @Override
    public Object getProperty(String propertyId)
            throws SAXNotRecognizedException {
        if ((PROPERTY + "declaration-handler").equals(propertyId)) {
            return (declHandler == base) ? null : declHandler;
        }

        if ((PROPERTY + "lexical-handler").equals(propertyId)) {
            return (lexicalHandler == base) ? null : lexicalHandler;
        }

        // unknown properties
        throw new SAXNotRecognizedException(propertyId);
    }

    /**
     * <b>SAX2</b>: Sets the state of feature flags in this parser. Some
     * built-in feature flags are mutable.
     */
    @Override
    public void setFeature(String featureId, boolean value)
            throws SAXNotRecognizedException, SAXNotSupportedException {
        boolean state;

        // Features with a defined value, we just change it if we can.
        state = getFeature(featureId);

        if (state == value) {
            return;
        }
        if (parser != null) {
            throw new SAXNotSupportedException("not while parsing");
        }

        if ((FEATURE + "namespace-prefixes").equals(featureId)) {
            // in this implementation, this only affects xmlns reporting
            xmlNames = value;
            // forcibly prevent illegal parser state
            if (!xmlNames) {
                namespaces = true;
            }
            return;
        }

        if ((FEATURE + "namespaces").equals(featureId)) {
            namespaces = value;
            // forcibly prevent illegal parser state
            if (!namespaces) {
                xmlNames = true;
            }
            return;
        }

        if ((FEATURE + "external-general-entities").equals(featureId)) {
            extGE = value;
            return;
        }
        if ((FEATURE + "external-parameter-entities").equals(featureId)) {
            extPE = value;
            return;
        }
        if ((FEATURE + "resolve-dtd-uris").equals(featureId)) {
            resolveAll = value;
            return;
        }

        if ((FEATURE + "use-entity-resolver2").equals(featureId)) {
            useResolver2 = value;
            return;
        }

        if ("http://xml.org/sax/features/unicode-normalization-checking".equals(featureId)) {
            checkNormalization = value;
            return;
        }

        throw new SAXNotRecognizedException(featureId);
    }

    /**
     * <b>SAX2</b>: Assigns the specified property. Like SAX1 handlers, these
     * may be changed at any time.
     */
    @Override
    public void setProperty(String propertyId, Object value)
            throws SAXNotRecognizedException, SAXNotSupportedException {
        // see if the property is recognized
        getProperty(propertyId);

        // Properties with a defined value, we just change it if we can.

        if ((PROPERTY + "declaration-handler").equals(propertyId)) {
            if (value == null) {
                declHandler = base;
            } else if (!(value instanceof DeclHandler)) {
                throw new SAXNotSupportedException(propertyId);
            } else {
                declHandler = (DeclHandler) value;
            }
            return;
        }

        if ((PROPERTY + "lexical-handler").equals(propertyId)) {
            if (value == null) {
                lexicalHandler = base;
            } else if (!(value instanceof LexicalHandler)) {
                throw new SAXNotSupportedException(propertyId);
            } else {
                lexicalHandler = (LexicalHandler) value;
            }
            return;
        }

        throw new SAXNotSupportedException(propertyId);
    }

    //
    // This is where the driver receives XmlParser callbacks and translates
    // them into SAX callbacks. Some more callbacks have been added for
    // SAX2 support.
    //

    void startDocument() throws SAXException {
        contentHandler.setDocumentLocator(this);
        contentHandler.startDocument();
        attributesList.clear();
    }

    void skippedEntity(String name) throws SAXException {
        contentHandler.skippedEntity(name);
    }

    InputSource getExternalSubset(String name, String baseURI)
            throws SAXException, IOException {
        if (resolver2 == null || !useResolver2 || !extPE) {
            return null;
        }
        return resolver2.getExternalSubset(name, baseURI);
    }

    InputSource resolveEntity(boolean isPE, String name, InputSource in,
            String baseURI) throws SAXException, IOException {
        InputSource source;

        // external entities might be skipped
        if (isPE && !extPE) {
            return null;
        }
        if (!isPE && !extGE) {
            return null;
        }

        // ... or not
        lexicalHandler.startEntity(name);
        if (resolver2 != null && useResolver2) {
            source = resolver2.resolveEntity(name, in.getPublicId(), baseURI,
                    in.getSystemId());
            if (source == null) {
                in.setSystemId(absolutize(baseURI, in.getSystemId(), false));
                source = in;
            }
        } else {
            in.setSystemId(absolutize(baseURI, in.getSystemId(), false));
            source = entityResolver.resolveEntity(in.getPublicId(),
                    in.getSystemId());
            if (source == null) {
                source = in;
            }
        }
        startExternalEntity(name, source.getSystemId(), true);
        return source;
    }

    // absolutize a system ID relative to the specified base URI
    // (temporarily) package-visible for external entity decls
    String absolutize(String baseURI, String systemId, boolean nice)
            throws MalformedURLException, SAXException {
        // FIXME normalize system IDs -- when?
        // - Convert to UTF-8
        // - Map reserved and non-ASCII characters to %HH

        try {
            if (baseURI == null) {
                if (XmlParser.uriWarnings) {
                    warn("No base URI; hope this SYSTEM id is absolute: "
                            + systemId);
                }
                return new URL(systemId).toString();
            } else {
                return new URL(new URL(baseURI), systemId).toString();
            }
        } catch (MalformedURLException e) {
            // Let unknown URI schemes pass through unless we need
            // the JVM to map them to i/o streams for us...
            if (!nice) {
                throw e;
            }

            // sometimes sysids for notations or unparsed entities
            // aren't really URIs...
            warn("Can't absolutize SYSTEM id: " + e.getMessage());
            return systemId;
        }
    }

    void startExternalEntity(String name, String systemId, boolean stackOnly)
            throws SAXException {
        // The following warning was deleted because the application has the
        // option of not setting systemId. Sun's JAXP or Xerces seems to
        // ignore this case.
        /*
         * if (systemId == null) warn ("URI was not reported to parser for
         * entity " + name);
         */
        if (!stackOnly) // spliced [dtd] needs startEntity
        {
            lexicalHandler.startEntity(name);
        }
        entityStack.push(systemId);
    }

    void endExternalEntity(String name) throws SAXException {
        if (!"[document]".equals(name)) {
            lexicalHandler.endEntity(name);
        }
        entityStack.pop();
    }

    void startInternalEntity(String name) throws SAXException {
        lexicalHandler.startEntity(name);
    }

    void endInternalEntity(String name) throws SAXException {
        lexicalHandler.endEntity(name);
    }

    void doctypeDecl(String name, String publicId, String systemId)
            throws SAXException {
        lexicalHandler.startDTD(name, publicId, systemId);

        // ... the "name" is a declaration and should be given
        // to the DeclHandler (but sax2 doesn't).

        // the IDs for the external subset are lexical details,
        // as are the contents of the internal subset; but sax2
        // doesn't provide the internal subset "pre-parse"
    }

    void notationDecl(String name, String publicId, String systemId,
            String baseUri) throws SAXException {
        try {
            dtdHandler.notationDecl(name, publicId,
                    (resolveAll && systemId != null) ? absolutize(baseUri,
                            systemId, true) : systemId);
        } catch (IOException e) {
            // "can't happen"
            throw new SAXParseException(e.getMessage(), this, e);
        }
    }

    void unparsedEntityDecl(String name, String publicId, String systemId,
            String baseUri, String notation) throws SAXException {
        try {
            dtdHandler.unparsedEntityDecl(
                    name,
                    publicId,
                    resolveAll ? absolutize(baseUri, systemId, true) : systemId,
                    notation);
        } catch (IOException e) {
            // "can't happen"
            throw new SAXParseException(e.getMessage(), this, e);
        }
    }

    void endDoctype() throws SAXException {
        lexicalHandler.endDTD();
    }

    private void declarePrefix(String prefix, String uri) throws SAXException {
        // NOTE: uri may be the empty string

        int index = uri.indexOf(':');

        // many versions of nwalsh docbook stylesheets
        // have bogus URLs; so this can't be an error...
        if (index < 1 && uri.length() != 0) {
            warn("relative URI for namespace: " + uri);
        }

        // char [0] must be ascii alpha [RFC 2396]
        if (uri.length() != 0 && !isAlpha(uri.charAt(0))) {
            fatal("First character of the URI must be ascii alpha");
        }

        // chars [1..index] must be ascii alphanumeric or in "+-." [RFC 2396]
        for (int i = 1; i < index; i++) {
            if (!isAlphanumericOrPlusMinusPoint(uri.charAt(i))) {
                fatal("Character " + i
                        + " of the URI must be ascii alpha or in \"+-.\"");
            }
        }

        // Namespace Constraints
        // name for xml prefix must be http://www.w3.org/XML/1998/namespace
        boolean prefixEquality = prefix.equals("xml");
        boolean uriEquality = uri.equals("http://www.w3.org/XML/1998/namespace");
        if ((prefixEquality || uriEquality) && !(prefixEquality && uriEquality)) {
            fatal("xml is by definition bound to the namespace name "
                    + "http://www.w3.org/XML/1998/namespace");
        }

        // xmlns prefix declaration is illegal but xml prefix declaration is
        // llegal...
        if (prefixEquality && uriEquality) {
            return;
        }

        // name for xmlns prefix must be http://www.w3.org/2000/xmlns/
        prefixEquality = prefix.equals("xmlns");
        uriEquality = uri.equals("http://www.w3.org/2000/xmlns/");
        if ((prefixEquality || uriEquality) && !(prefixEquality && uriEquality)) {
            fatal("http://www.w3.org/2000/xmlns/ is by definition bound"
                    + " to prefix xmlns");
        }

        // even if the uri is http://www.w3.org/2000/xmlns/
        // it is illegal to declare it
        if (prefixEquality && uriEquality) {
            fatal("declaring the xmlns prefix is illegal");
        }

        uri = uri.intern();
        prefixStack.declarePrefix(prefix, uri);
        contentHandler.startPrefixMapping(prefix, uri);
    }

    void attribute(String qname, String value, boolean isSpecified)
            throws SAXException {
        if (!attributes) {
            attributes = true;
            if (namespaces) {
                prefixStack.pushContext();
            }
        }

        // process namespace decls immediately;
        // then maybe forget this as an attribute
        if (namespaces) {
            @SuppressWarnings("unused") int index;

            // default NS declaration?
            if (stringInterning) {
                if ("xmlns" == qname) {
                    declarePrefix("", value);
                    if (!xmlNames) {
                        return;
                    }
                }
                // NS prefix declaration?
                else if ((index = qname.indexOf(':')) == 5
                        && qname.startsWith("xmlns")) {
                    String prefix = qname.substring(6);

                    if (prefix.equals("")) {
                        fatal("missing prefix "
                                + "in namespace declaration attribute");
                    }
                    if (value.length() == 0) {
                        verror("missing URI in namespace declaration attribute: "
                                + qname);
                    } else {
                        declarePrefix(prefix, value);
                    }
                    if (!xmlNames) {
                        return;
                    }
                }
            } else {
                if ("xmlns".equals(qname)) {
                    declarePrefix("", value);
                    if (!xmlNames) {
                        return;
                    }
                }
                // NS prefix declaration?
                else if ((index = qname.indexOf(':')) == 5
                        && qname.startsWith("xmlns")) {
                    String prefix = qname.substring(6);

                    if (value.length() == 0) {
                        verror("missing URI in namespace decl attribute: "
                                + qname);
                    } else {
                        declarePrefix(prefix, value);
                    }
                    if (!xmlNames) {
                        return;
                    }
                }
            }
        }
        // remember this attribute ...
        attributeCount++;

        // attribute type comes from querying parser's DTD records
        attributesList.add(new Attribute(qname, value, isSpecified));

    }

    void startElement(String elname) throws SAXException {
        ContentHandler handler = contentHandler;

        //
        // NOTE: this implementation of namespace support adds something
        // like six percent to parsing CPU time, in a large (~50 MB)
        // document that doesn't use namespaces at all. (Measured by PC
        // sampling, with a bug where endElement processing was omitted.)
        // [Measurement referred to older implementation, older JVM ...]
        //
        // It ought to become notably faster in such cases. Most
        // costs are the prefix stack calling Hashtable.get() (2%),
        // String.hashCode() (1.5%) and about 1.3% each for pushing
        // the context, and two chunks of name processing.
        //

        if (!attributes) {
            if (namespaces) {
                prefixStack.pushContext();
            }
        } else if (namespaces) {

            // now we can patch up namespace refs; we saw all the
            // declarations, so now we'll do the Right Thing
            Iterator<Attribute> itt = attributesList.iterator();
            while (itt.hasNext()) {
                Attribute attribute = itt.next();
                String qname = attribute.name;
                int index;

                // default NS declaration?
                if (stringInterning) {
                    if ("xmlns" == qname) {
                        continue;
                    }
                } else {
                    if ("xmlns".equals(qname)) {
                        continue;
                    }
                }
                // Illegal in the new Namespaces Draft
                // should it be only in 1.1 docs??
                if (qname.equals(":")) {
                    fatal("namespace names consisting of a single colon "
                            + "character are invalid");
                }
                index = qname.indexOf(':');

                // NS prefix declaration?
                if (index == 5 && qname.startsWith("xmlns")) {
                    continue;
                }

                // it's not a NS decl; patch namespace info items
                if (prefixStack.processName(qname, nsTemp, true) == null) {
                    fatal("undeclared attribute prefix in: " + qname);
                } else {
                    attribute.nameSpace = nsTemp[0];
                    attribute.localName = nsTemp[1];
                }
            }
        }

        // save element name so attribute callbacks work
        elementName = elname;
        if (namespaces) {
            if (prefixStack.processName(elname, nsTemp, false) == null) {
                fatal("undeclared element prefix in: " + elname);
                nsTemp[0] = nsTemp[1] = "";
            }
            handler.startElement(nsTemp[0], nsTemp[1], elname, this);
        } else {
            handler.startElement("", "", elname, this);
        }
        // elementName = null;

        // elements with no attributes are pretty common!
        if (attributes) {
            attributesList.clear();
            attributeCount = 0;
            attributes = false;
        }
    }

    void endElement(String elname) throws SAXException {
        ContentHandler handler = contentHandler;

        if (!namespaces) {
            handler.endElement("", "", elname);
            return;
        }
        prefixStack.processName(elname, nsTemp, false);
        handler.endElement(nsTemp[0], nsTemp[1], elname);

        @SuppressWarnings("rawtypes")
        Enumeration prefixes = prefixStack.getDeclaredPrefixes();

        while (prefixes.hasMoreElements()) {
            handler.endPrefixMapping((String) prefixes.nextElement());
        }
        prefixStack.popContext();
    }

    void startCDATA() throws SAXException {
        lexicalHandler.startCDATA();
    }

    void charData(char[] ch, int start, int length) throws SAXException {
        contentHandler.characters(ch, start, length);
    }

    void endCDATA() throws SAXException {
        lexicalHandler.endCDATA();
    }

    void ignorableWhitespace(char[] ch, int start, int length)
            throws SAXException {
        contentHandler.ignorableWhitespace(ch, start, length);
    }

    void processingInstruction(String target, String data) throws SAXException {
        contentHandler.processingInstruction(target, data);
    }

    void comment(char[] ch, int start, int length) throws SAXException {
        if (lexicalHandler != base) {
            lexicalHandler.comment(ch, start, length);
        }
    }

    void fatal(String message) throws SAXException {
        SAXParseException fatal;

        fatal = new SAXParseException(message, this);
        errorHandler.fatalError(fatal);

        // Even if the application can continue ... we can't!
        throw new FatalSAXException(message);
    }

    // We can safely report a few validity errors that
    // make layered SAX2 DTD validation more conformant
    void verror(String message) throws SAXException {
        SAXParseException err;

        err = new SAXParseException(message, this);
        errorHandler.error(err);
    }

    void warn(String message) throws SAXException {
        SAXParseException err;

        err = new SAXParseException(message, this);
        errorHandler.warning(err);
    }

    /**
     * Checks whether a given character is an ASCII alpha or not.
     * @param character The character to check.
     * @return <code>true</code> if the character is an ASCII alpha.
     */
    private boolean isAlpha(char character) {
        // Range of alpha characters in ASCII, should be the same for
        // Unicode according to "The Unicode Standard 4.0", section 5.2
        return (character >= 'A' && character <= 'Z') // Unicode A..Z
                || (character >= 'a' && character <= 'z'); // Unicode a..z
    }

    /**
     * Checks whether a given character is in "+-." or an ASCII alphanumeric.
     * This is useful for a check related to [RFC 2396]
     * 
     * @param character The character to check.
     * @return <code>true</code> if the character is in "+-." or an ASCII alphanumeric.
     */
    private boolean isAlphanumericOrPlusMinusPoint(char character) {
        return (character >= 'A' && character <= 'Z') // Unicode A..Z
                || (character >= 'a' && character <= 'z') // Unicode a..z
                || (character >= '0' && character <= '9') // Unicode 0..9
                || character == '+' || character == '-' || character == '.';
    }
    
    //
    // Implementation of org.xml.sax.Attributes.
    //

    /**
     * <b>SAX1 AttributeList, SAX2 Attributes</b> method (don't invoke on
     * parser);
     */
    @Override
    public int getLength() {
        return attributesList.size();
    }

    /**
     * <b>SAX2 Attributes</b> method (don't invoke on parser);
     */
    @Override
    public String getURI(int index) {
        if (index < 0 || index >= attributesList.size()) {
            return null;
        }
        return attributesList.get(index).nameSpace;
    }

    /**
     * <b>SAX2 Attributes</b> method (don't invoke on parser);
     */
    @Override
    public String getLocalName(int index) {
        if (index < 0 || index >= attributesList.size()) {
            return null;
        }
        Attribute attr = attributesList.get(index);
        // FIXME attr.localName is sometimes null, why?
        if (namespaces && attr.localName == null) {
            // XXX fix this here for now
            int ci = attr.name.indexOf(':');
            attr.localName = (ci == -1) ? attr.name
                    : attr.name.substring(ci + 1);
        }
        return (attr.localName == null) ? "" : attr.localName;
    }

    /**
     * <b>SAX2 Attributes</b> method (don't invoke on parser);
     */
    @Override
    public String getQName(int index) {
        if (index < 0 || index >= attributesList.size()) {
            return null;
        }
        Attribute attr = attributesList.get(index);
        return (attr.name == null) ? "" : attr.name;
    }

    /**
     * <b>SAX1 AttributeList</b> method (don't invoke on parser);
     */
    @Override
    public String getName(int index) {
        return getQName(index);
    }

    /**
     * <b>SAX1 AttributeList, SAX2 Attributes</b> method (don't invoke on
     * parser);
     */
    @Override
    public String getType(int index) {
        if (index < 0 || index >= attributesList.size()) {
            return null;
        }
        String type = parser.getAttributeType(elementName, getQName(index));
        if (type == null) {
            return "CDATA";
        }
        // ... use DeclHandler.attributeDecl to see enumerations
        if (type == "ENUMERATION") {
            return "NMTOKEN";
        }
        return type;
    }

    /**
     * <b>SAX1 AttributeList, SAX2 Attributes</b> method (don't invoke on
     * parser);
     */
    @Override
    public String getValue(int index) {
        if (index < 0 || index >= attributesList.size()) {
            return null;
        }
        return attributesList.get(index).value;
    }

    /**
     * <b>SAX2 Attributes</b> method (don't invoke on parser);
     */
    @Override
    public int getIndex(String uri, String local) {
        int length = getLength();

        for (int i = 0; i < length; i++) {
            if (!getURI(i).equals(uri)) {
                continue;
            }
            if (getLocalName(i).equals(local)) {
                return i;
            }
        }
        return -1;
    }

    /**
     * <b>SAX2 Attributes</b> method (don't invoke on parser);
     */
    @Override
    public int getIndex(String xmlName) {
        int length = getLength();

        for (int i = 0; i < length; i++) {
            if (getQName(i).equals(xmlName)) {
                return i;
            }
        }
        return -1;
    }

    /**
     * <b>SAX2 Attributes</b> method (don't invoke on parser);
     */
    @Override
    public String getType(String uri, String local) {
        int index = getIndex(uri, local);

        if (index < 0) {
            return null;
        }
        return getType(index);
    }

    /**
     * <b>SAX1 AttributeList, SAX2 Attributes</b> method (don't invoke on
     * parser);
     */
    @Override
    public String getType(String xmlName) {
        int index = getIndex(xmlName);

        if (index < 0) {
            return null;
        }
        return getType(index);
    }

    /**
     * <b>SAX Attributes</b> method (don't invoke on parser);
     */
    @Override
    public String getValue(String uri, String local) {
        int index = getIndex(uri, local);

        if (index < 0) {
            return null;
        }
        return getValue(index);
    }

    /**
     * <b>SAX1 AttributeList, SAX2 Attributes</b> method (don't invoke on
     * parser);
     */
    @Override
    public String getValue(String xmlName) {
        int index = getIndex(xmlName);

        if (index < 0) {
            return null;
        }
        return getValue(index);
    }

    //
    // Implementation of org.xml.sax.ext.Attributes2
    //

    /**
     * @return false unless the attribute was declared in the DTD.
     * @throws java.lang.ArrayIndexOutOfBoundsException
     *             When the supplied index does not identify an attribute.
     */
    @Override
    public boolean isDeclared(int index) {
        if (index < 0 || index >= attributeCount) {
            throw new ArrayIndexOutOfBoundsException();
        }
        String type = parser.getAttributeType(elementName, getQName(index));
        return (type != null);
    }

    /**
     * @return false unless the attribute was declared in the DTD.
     * @throws java.lang.IllegalArgumentException
     *             When the supplied names do not identify an attribute.
     */
    @Override
    public boolean isDeclared(String qName) {
        int index = getIndex(qName);
        if (index < 0) {
            throw new IllegalArgumentException();
        }
        String type = parser.getAttributeType(elementName, qName);
        return (type != null);
    }

    /**
     * @return false unless the attribute was declared in the DTD.
     * @throws java.lang.IllegalArgumentException
     *             When the supplied names do not identify an attribute.
     */
    @Override
    public boolean isDeclared(String uri, String localName) {
        int index = getIndex(uri, localName);
        return isDeclared(index);
    }

    /**
     * <b>SAX-ext Attributes2</b> method (don't invoke on parser);
     */
    @Override
    public boolean isSpecified(int index) {
        return attributesList.get(index).specified;
    }

    /**
     * <b>SAX-ext Attributes2</b> method (don't invoke on parser);
     */
    @Override
    public boolean isSpecified(String uri, String local) {
        int index = getIndex(uri, local);
        return isSpecified(index);
    }

    /**
     * <b>SAX-ext Attributes2</b> method (don't invoke on parser);
     */
    @Override
    public boolean isSpecified(String xmlName) {
        int index = getIndex(xmlName);
        return isSpecified(index);
    }

    //
    // Implementation of org.xml.sax.Locator.
    //

    /**
     * <b>SAX Locator</b> method (don't invoke on parser);
     */
    @Override
    public String getPublicId() {
        return null; // FIXME track public IDs too
    }

    /**
     * <b>SAX Locator</b> method (don't invoke on parser);
     */
    @Override
    public String getSystemId() {
        if (entityStack.empty()) {
            return null;
        } else {
            return entityStack.peek();
        }
    }

    /**
     * <b>SAX Locator</b> method (don't invoke on parser);
     */
    @Override
    public int getLineNumber() {
        return parser.getLineNumber();
    }

    /**
     * <b>SAX Locator</b> method (don't invoke on parser);
     */
    @Override
    public int getColumnNumber() {
        return parser.getColumnNumber();
    }

    // adapter between SAX2 content handler and SAX1 document handler callbacks
    private static class Adapter implements ContentHandler {

        private DocumentHandler docHandler;

        Adapter(DocumentHandler dh) {
            docHandler = dh;
        }

        @Override
        public void setDocumentLocator(Locator l) {
            docHandler.setDocumentLocator(l);
        }

        @Override
        public void startDocument() throws SAXException {
            docHandler.startDocument();
        }

        @Override
        public void processingInstruction(String target, String data)
                throws SAXException {
            docHandler.processingInstruction(target, data);
        }

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

        @Override
        public void startElement(String namespace, String local, String name,
                Attributes attrs) throws SAXException {
            docHandler.startElement(name, (AttributeList) attrs);
        }

        @Override
        public void characters(char[] buf, int offset, int len)
                throws SAXException {
            docHandler.characters(buf, offset, len);
        }

        @Override
        public void ignorableWhitespace(char[] buf, int offset, int len)
                throws SAXException {
            docHandler.ignorableWhitespace(buf, offset, len);
        }

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

        @Override
        public void endElement(String u, String l, String name)
                throws SAXException {
            docHandler.endElement(name);
        }

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

        @Override
        public void endDocument() throws SAXException {
            docHandler.endDocument();
        }
    }

    private static class Attribute {

        String name;

        String value;

        String nameSpace;

        String localName;

        boolean specified;

        Attribute(String name, String value, boolean specified) {
            this.name = name;
            this.value = value;
            this.nameSpace = "";
            this.specified = specified;
        }

    }

    /**
     * Sets the characterHandler.
     * 
     * @param characterHandler the characterHandler to set
     */
    public void setCharacterHandler(CharacterHandler characterHandler) {
        this.characterHandler = characterHandler;
    }

    @Override
    public String getEncoding() {
        return parser.getEncoding();
    }

    @Override
    public String getXMLVersion() {
        return "1.0";
    }

}
