JspDocumentParser.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.jasper.compiler;

import java.io.CharArrayWriter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.AccessController;
import java.util.Collection;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import jakarta.servlet.jsp.tagext.TagFileInfo;
import jakarta.servlet.jsp.tagext.TagInfo;
import jakarta.servlet.jsp.tagext.TagLibraryInfo;

import org.apache.jasper.Constants;
import org.apache.jasper.JasperException;
import org.apache.jasper.JspCompilationContext;
import org.apache.tomcat.Jar;
import org.apache.tomcat.util.descriptor.DigesterFactory;
import org.apache.tomcat.util.descriptor.LocalResolver;
import org.apache.tomcat.util.descriptor.tld.TldResourcePath;
import org.apache.tomcat.util.security.PrivilegedGetTccl;
import org.apache.tomcat.util.security.PrivilegedSetTccl;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.ext.DefaultHandler2;
import org.xml.sax.ext.EntityResolver2;
import org.xml.sax.helpers.AttributesImpl;

/**
 * Class implementing a parser for a JSP document, that is, a JSP page in XML
 * syntax.
 *
 * @author Jan Luehe
 * @author Kin-man Chung
 */

class JspDocumentParser
    extends DefaultHandler2
    implements TagConstants {

    private static final String LEXICAL_HANDLER_PROPERTY =
        "http://xml.org/sax/properties/lexical-handler";
    private static final String JSP_URI = "http://java.sun.com/JSP/Page";

    private final ParserController parserController;
    private final JspCompilationContext ctxt;
    private final PageInfo pageInfo;
    private final String path;
    private StringBuilder charBuffer;

    // Node representing the XML element currently being parsed
    private Node current;

    /*
     * Outermost (in the nesting hierarchy) node whose body is declared to be
     * scriptless. If a node's body is declared to be scriptless, all its
     * nested nodes must be scriptless, too.
     */
    private Node scriptlessBodyNode;

    private Locator locator;

    //Mark representing the start of the current element.  Note
    //that locator.getLineNumber() and locator.getColumnNumber()
    //return the line and column numbers for the character
    //immediately _following_ the current element.  The underlying
    //XMl parser eats white space that is not part of character
    //data, so for Nodes that are not created from character data,
    //this is the best we can do.  But when we parse character data,
    //we get an accurate starting location by starting with startMark
    //as set by the previous element, and updating it as we advance
    //through the characters.
    private Mark startMark;

    // Flag indicating whether we are inside DTD declarations
    private boolean inDTD;

    private boolean isValidating;
    private final EntityResolver2 entityResolver;

    private final ErrorDispatcher err;
    private final boolean isTagFile;
    private final boolean directivesOnly;
    private boolean isTop;

    // Nesting level of Tag dependent bodies
    private int tagDependentNesting = 0;
    // Flag set to delay incrementing tagDependentNesting until jsp:body
    // is first encountered
    private boolean tagDependentPending = false;

    /*
     * Constructor
     */
    JspDocumentParser(
        ParserController pc,
        String path,
        boolean isTagFile,
        boolean directivesOnly) {
        this.parserController = pc;
        this.ctxt = pc.getJspCompilationContext();
        this.pageInfo = pc.getCompiler().getPageInfo();
        this.err = pc.getCompiler().getErrorDispatcher();
        this.path = path;
        this.isTagFile = isTagFile;
        this.directivesOnly = directivesOnly;
        this.isTop = true;

        String blockExternalString = ctxt.getServletContext().getInitParameter(
                Constants.XML_BLOCK_EXTERNAL_INIT_PARAM);
        boolean blockExternal;
        if (blockExternalString == null) {
            blockExternal = true;
        } else {
            blockExternal = Boolean.parseBoolean(blockExternalString);
        }

        this.entityResolver = new LocalResolver(
                DigesterFactory.SERVLET_API_PUBLIC_IDS,
                DigesterFactory.SERVLET_API_SYSTEM_IDS,
                blockExternal);
    }

    /*
     * Parses a JSP document by responding to SAX events.
     *
     * @throws JasperException
     */
    public static Node.Nodes parse(
        ParserController pc,
        String path,
        Jar jar,
        Node parent,
        boolean isTagFile,
        boolean directivesOnly,
        String pageEnc,
        String jspConfigPageEnc,
        boolean isEncodingSpecifiedInProlog,
        boolean isBomPresent)
        throws JasperException {

        JspDocumentParser jspDocParser =
            new JspDocumentParser(pc, path, isTagFile, directivesOnly);
        Node.Nodes pageNodes = null;

        try {

            // Create dummy root and initialize it with given page encodings
            Node.Root dummyRoot = new Node.Root(null, parent, true,
                    pc.getJspCompilationContext().getOptions().getTempVariableNamePrefix());
            dummyRoot.setPageEncoding(pageEnc);
            dummyRoot.setJspConfigPageEncoding(jspConfigPageEnc);
            dummyRoot.setIsEncodingSpecifiedInProlog(
                isEncodingSpecifiedInProlog);
            dummyRoot.setIsBomPresent(isBomPresent);
            jspDocParser.current = dummyRoot;
            if (parent == null) {
                jspDocParser.addInclude(
                    dummyRoot,
                    jspDocParser.pageInfo.getIncludePrelude());
            } else {
                jspDocParser.isTop = false;
            }

            jspDocParser.isValidating = false;

            // Parse the input
            SAXParser saxParser = getSAXParser(false, jspDocParser);
            InputSource source = JspUtil.getInputSource(path, jar, jspDocParser.ctxt);
            try {
                saxParser.parse(source, jspDocParser);
            } catch (EnableDTDValidationException e) {
                saxParser = getSAXParser(true, jspDocParser);
                jspDocParser.isValidating = true;
                try {
                    source.getByteStream().close();
                } catch (IOException e2) {
                    // ignore
                }
                source = JspUtil.getInputSource(path, jar, jspDocParser.ctxt);
                saxParser.parse(source, jspDocParser);
            } finally {
                try {
                    source.getByteStream().close();
                } catch (IOException e) {
                    // ignore
                }
            }

            if (parent == null) {
                jspDocParser.addInclude(
                    dummyRoot,
                    jspDocParser.pageInfo.getIncludeCoda());
            }

            // Create Node.Nodes from dummy root
            pageNodes = new Node.Nodes(dummyRoot);

        } catch (IOException ioe) {
            jspDocParser.err.jspError(ioe, "jsp.error.data.file.read", path);
        } catch (SAXParseException e) {
            jspDocParser.err.jspError
                (new Mark(jspDocParser.ctxt, path, e.getLineNumber(),
                          e.getColumnNumber()),
                e, e.getMessage());
        } catch (Exception e) {
            jspDocParser.err.jspError(e, "jsp.error.data.file.processing", path);
        }

        return pageNodes;
    }

    /*
     * Processes the given list of included files.
     *
     * This is used to implement the include-prelude and include-coda
     * subelements of the jsp-config element in web.xml
     */
    private void addInclude(Node parent, Collection<String> files) throws SAXException {
        if (files != null) {
            for (String file : files) {
                AttributesImpl attrs = new AttributesImpl();
                attrs.addAttribute("", "file", "file", "CDATA", file);

                // Create a dummy Include directive node
                Node includeDir =
                        new Node.IncludeDirective(attrs, null, // XXX
    parent);
                processIncludeDirective(file, includeDir);
            }
        }
    }


    @Override
    public InputSource getExternalSubset(String name, String baseURI)
            throws SAXException, IOException {
        return entityResolver.getExternalSubset(name, baseURI);
    }

    @Override
    public InputSource resolveEntity(String publicId, String systemId)
            throws SAXException, IOException {
        return entityResolver.resolveEntity(publicId, systemId);
    }

    @Override
    public InputSource resolveEntity(String name, String publicId,
            String baseURI, String systemId) throws SAXException, IOException {
        // TODO URLs returned by the Jar abstraction may be of the form jar:jar:
        //      which is not a URL that can be resolved by the JRE. This should
        //      use the JarFactory to construct and return a valid InputSource.
        return entityResolver.resolveEntity(name, publicId, baseURI, systemId);
    }

    /*
     * Receives notification of the start of an element.
     *
     * This method assigns the given tag attributes to one of 3 buckets:
     *
     * - "xmlns" attributes that represent (standard or custom) tag libraries.
     * - "xmlns" attributes that do not represent tag libraries.
     * - all remaining attributes.
     *
     * For each "xmlns" attribute that represents a custom tag library, the
     * corresponding TagLibraryInfo object is added to the set of custom
     * tag libraries.
     */
    @Override
    public void startElement(
        String uri,
        String localName,
        String qName,
        Attributes attrs)
        throws SAXException {

        AttributesImpl taglibAttrs = null;
        AttributesImpl nonTaglibAttrs = null;
        AttributesImpl nonTaglibXmlnsAttrs = null;

        processChars();

        checkPrefixes(uri, qName, attrs);

        if (directivesOnly &&
            !(JSP_URI.equals(uri) && localName.startsWith(DIRECTIVE_ACTION))) {
            return;
        }

        // jsp:text must not have any subelements
        if (current instanceof Node.JspText) {
            throw new SAXParseException(
                Localizer.getMessage("jsp.error.text.has_subelement"),
                locator);
        }

        startMark = new Mark(ctxt, path, locator.getLineNumber(),
                             locator.getColumnNumber());

        /*
         * Notice that due to a bug in the underlying SAX parser, the
         * attributes must be enumerated in descending order.
         */
        boolean isTaglib = false;
        for (int i = attrs.getLength() - 1; i >= 0; i--) {
            isTaglib = false;
            String attrQName = attrs.getQName(i);
            if (!attrQName.startsWith("xmlns")) {
                if (nonTaglibAttrs == null) {
                    nonTaglibAttrs = new AttributesImpl();
                }
                nonTaglibAttrs.addAttribute(
                    attrs.getURI(i),
                    attrs.getLocalName(i),
                    attrs.getQName(i),
                    attrs.getType(i),
                    attrs.getValue(i));
            } else {
                if (attrQName.startsWith("xmlns:jsp")) {
                    isTaglib = true;
                } else {
                    String attrUri = attrs.getValue(i);
                    // TaglibInfo for this uri already established in
                    // startPrefixMapping
                    isTaglib = pageInfo.hasTaglib(attrUri);
                }
                if (isTaglib) {
                    if (taglibAttrs == null) {
                        taglibAttrs = new AttributesImpl();
                    }
                    taglibAttrs.addAttribute(
                        attrs.getURI(i),
                        attrs.getLocalName(i),
                        attrs.getQName(i),
                        attrs.getType(i),
                        attrs.getValue(i));
                } else {
                    if (nonTaglibXmlnsAttrs == null) {
                        nonTaglibXmlnsAttrs = new AttributesImpl();
                    }
                    nonTaglibXmlnsAttrs.addAttribute(
                        attrs.getURI(i),
                        attrs.getLocalName(i),
                        attrs.getQName(i),
                        attrs.getType(i),
                        attrs.getValue(i));
                }
            }
        }

        Node node = null;

        if (tagDependentPending && JSP_URI.equals(uri) &&
                     localName.equals(BODY_ACTION)) {
            tagDependentPending = false;
            tagDependentNesting++;
            current =
                parseStandardAction(
                    qName,
                    localName,
                    nonTaglibAttrs,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    startMark);
            return;
        }

        if (tagDependentPending && JSP_URI.equals(uri) &&
                     localName.equals(ATTRIBUTE_ACTION)) {
            current =
                parseStandardAction(
                    qName,
                    localName,
                    nonTaglibAttrs,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    startMark);
            return;
        }

        if (tagDependentPending) {
            tagDependentPending = false;
            tagDependentNesting++;
        }

        if (tagDependentNesting > 0) {
            node =
                new Node.UninterpretedTag(
                    qName,
                    localName,
                    nonTaglibAttrs,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    startMark,
                    current);
        } else if (JSP_URI.equals(uri)) {
            node =
                parseStandardAction(
                    qName,
                    localName,
                    nonTaglibAttrs,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    startMark);
        } else {
            node =
                parseCustomAction(
                    qName,
                    localName,
                    uri,
                    nonTaglibAttrs,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    startMark,
                    current);
            if (node == null) {
                node =
                    new Node.UninterpretedTag(
                        qName,
                        localName,
                        nonTaglibAttrs,
                        nonTaglibXmlnsAttrs,
                        taglibAttrs,
                        startMark,
                        current);
            } else {
                // custom action
                String bodyType = getBodyType((Node.CustomTag) node);

                if (scriptlessBodyNode == null
                        && bodyType.equalsIgnoreCase(TagInfo.BODY_CONTENT_SCRIPTLESS)) {
                    scriptlessBodyNode = node;
                }
                else if (TagInfo.BODY_CONTENT_TAG_DEPENDENT.equalsIgnoreCase(bodyType)) {
                    tagDependentPending = true;
                }
            }
        }

        current = node;
    }

    /*
     * Receives notification of character data inside an element.
     *
     * The SAX does not call this method with all of the template text, but may
     * invoke this method with chunks of it.  This is a problem when we try
     * to determine if the text contains only whitespaces, or when we are
     * looking for an EL expression string.  Therefore it is necessary to
     * buffer and concatenate the chunks and process the concatenated text
     * later (at beginTag and endTag)
     *
     * @param buf The characters
     * @param offset The start position in the character array
     * @param len The number of characters to use from the character array
     *
     * @throws SAXException
     */
    @Override
    public void characters(char[] buf, int offset, int len) {

        if (charBuffer == null) {
            charBuffer = new StringBuilder();
        }
        charBuffer.append(buf, offset, len);
    }

    private void processChars() throws SAXException {

        if (charBuffer == null || directivesOnly) {
            return;
        }

        /*
         * JSP.6.1.1: All textual nodes that have only white space are to be
         * dropped from the document, except for nodes in a jsp:text element,
         * and any leading and trailing white-space-only textual nodes in a
         * jsp:attribute whose 'trim' attribute is set to FALSE, which are to
         * be kept verbatim.
         * JSP.6.2.3 defines white space characters.
         */
        boolean isAllSpace = true;
        if (!(current instanceof Node.JspText)
            && !(current instanceof Node.NamedAttribute)) {
            for (int i = 0; i < charBuffer.length(); i++) {
                char ch = charBuffer.charAt(i);
                if (!(ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t')) {
                    isAllSpace = false;
                    break;
                }
            }
        }

        if (!isAllSpace && tagDependentPending) {
            tagDependentPending = false;
            tagDependentNesting++;
        }

        if (tagDependentNesting > 0 || pageInfo.isELIgnored() ||
                current instanceof Node.ScriptingElement) {
            if (charBuffer.length() > 0) {
                @SuppressWarnings("unused")
                Node unused = new Node.TemplateText(
                        charBuffer.toString(), startMark, current);
            }
            startMark = new Mark(ctxt, path, locator.getLineNumber(),
                                 locator.getColumnNumber());
            charBuffer = null;
            return;
        }

        if ((current instanceof Node.JspText)
            || (current instanceof Node.NamedAttribute)
            || !isAllSpace) {

            int line = startMark.getLineNumber();
            int column = startMark.getColumnNumber();

            CharArrayWriter ttext = new CharArrayWriter();
            int lastCh = 0, elType = 0;
            for (int i = 0; i < charBuffer.length(); i++) {

                int ch = charBuffer.charAt(i);
                if (ch == '\n') {
                    column = 1;
                    line++;
                } else {
                    column++;
                }
                if ((lastCh == '$' || lastCh == '#') && ch == '{') {
                    elType = lastCh;
                    if (ttext.size() > 0) {
                        @SuppressWarnings("unused")
                        Node unused = new Node.TemplateText(
                                ttext.toString(), startMark, current);
                        ttext.reset();
                        //We subtract two from the column number to
                        //account for the '[$,#]{' that we've already parsed
                        startMark = new Mark(ctxt, path, line, column - 2);
                    }
                    // following "${" || "#{" to first unquoted "}"
                    i++;
                    boolean singleQ = false;
                    boolean doubleQ = false;
                    lastCh = 0;
                    for (;; i++) {
                        if (i >= charBuffer.length()) {
                            throw new SAXParseException(
                                Localizer.getMessage(
                                    "jsp.error.unterminated",
                                    (char) elType + "{"),
                                locator);

                        }
                        ch = charBuffer.charAt(i);
                        if (ch == '\n') {
                            column = 1;
                            line++;
                        } else {
                            column++;
                        }
                        if (lastCh == '\\' && (singleQ || doubleQ)) {
                            ttext.write(ch);
                            lastCh = 0;
                            continue;
                        }
                        if (ch == '}') {
                            @SuppressWarnings("unused")
                            Node unused = new Node.ELExpression(
                                    (char) elType, ttext.toString(),
                                    startMark, current);
                            ttext.reset();
                            startMark = new Mark(ctxt, path, line, column);
                            break;
                        }
                        if (ch == '"') {
                            doubleQ = !doubleQ;
                        } else if (ch == '\'') {
                            singleQ = !singleQ;
                        }

                        ttext.write(ch);
                        lastCh = ch;
                    }
                } else if (lastCh == '\\' && (ch == '$' || ch == '#')) {
                    if (pageInfo.isELIgnored()) {
                        ttext.write('\\');
                    }
                    ttext.write(ch);
                    ch = 0;  // Not start of EL anymore
                } else {
                    if (lastCh == '$' || lastCh == '#' || lastCh == '\\') {
                        ttext.write(lastCh);
                    }
                    if (ch != '$' && ch != '#' && ch != '\\') {
                        ttext.write(ch);
                    }
                }
                lastCh = ch;
            }
            if (lastCh == '$' || lastCh == '#' || lastCh == '\\') {
                ttext.write(lastCh);
            }
            if (ttext.size() > 0) {
                @SuppressWarnings("unused")
                Node unused = new Node.TemplateText(
                        ttext.toString(), startMark, current);
            }
        }
        startMark = new Mark(ctxt, path, locator.getLineNumber(),
                             locator.getColumnNumber());

        charBuffer = null;
    }

    /*
     * Receives notification of the end of an element.
     */
    @Override
    public void endElement(String uri, String localName, String qName)
        throws SAXException {

        processChars();

        if (directivesOnly &&
            !(JSP_URI.equals(uri) && localName.startsWith(DIRECTIVE_ACTION))) {
            return;
        }

        if (current instanceof Node.NamedAttribute) {
            boolean isTrim = ((Node.NamedAttribute)current).isTrim();
            Node.Nodes subElems = current.getBody();
            for (int i = 0; subElems != null && i < subElems.size(); i++) {
                Node subElem = subElems.getNode(i);
                if (!(subElem instanceof Node.TemplateText)) {
                    continue;
                }
                // Ignore any whitespace (including spaces, carriage returns,
                // line feeds, and tabs, that appear at the beginning and at
                // the end of the body of the <jsp:attribute> action, if the
                // action's 'trim' attribute is set to TRUE (default).
                // In addition, any textual nodes in the <jsp:attribute> that
                // have only white space are dropped from the document, with
                // the exception of leading and trailing white-space-only
                // textual nodes in a <jsp:attribute> whose 'trim' attribute
                // is set to FALSE, which must be kept verbatim.
                if (i == 0) {
                    if (isTrim) {
                        ((Node.TemplateText)subElem).ltrim();
                    }
                } else if (i == subElems.size() - 1) {
                    if (isTrim) {
                        ((Node.TemplateText)subElem).rtrim();
                    }
                } else {
                    if (((Node.TemplateText)subElem).isAllSpace()) {
                        subElems.remove(subElem);
                    }
                }
            }
        } else if (current instanceof Node.ScriptingElement) {
            checkScriptingBody((Node.ScriptingElement)current);
        }

        if ( isTagDependent(current)) {
            tagDependentNesting--;
        }

        if (scriptlessBodyNode != null
                && current.equals(scriptlessBodyNode)) {
            scriptlessBodyNode = null;
        }

        if (current instanceof Node.CustomTag) {
            String bodyType = getBodyType((Node.CustomTag) current);
            if (TagInfo.BODY_CONTENT_EMPTY.equalsIgnoreCase(bodyType)) {
                // Children - if any - must be JSP attributes
                Node.Nodes children = current.getBody();
                if (children != null && children.size() > 0) {
                    for (int i = 0; i < children.size(); i++) {
                        Node child = children.getNode(i);
                        if (!(child instanceof Node.NamedAttribute)) {
                            throw new SAXParseException(Localizer.getMessage(
                                    "jasper.error.emptybodycontent.nonempty",
                                    current.qName), locator);
                        }
                    }
                }
            }
        }
        if (current.getParent() != null) {
            current = current.getParent();
        }
    }

    /*
     * Receives the document locator.
     *
     * @param locator the document locator
     */
    @Override
    public void setDocumentLocator(Locator locator) {
        this.locator = locator;
    }

    /*
     * See org.xml.sax.ext.LexicalHandler.
     */
    @Override
    public void comment(char[] buf, int offset, int len) throws SAXException {

        processChars();  // Flush char buffer and remove white spaces

        // ignore comments in the DTD
        if (!inDTD) {
            startMark = new Mark(ctxt, path, locator.getLineNumber(),
                    locator.getColumnNumber());
            @SuppressWarnings("unused")
            Node unused = new Node.Comment(new String(buf, offset, len), startMark, current);
        }
    }

    /*
     * See org.xml.sax.ext.LexicalHandler.
     */
    @Override
    public void startCDATA() throws SAXException {

        processChars();  // Flush char buffer and remove white spaces
        startMark = new Mark(ctxt, path, locator.getLineNumber(),
                             locator.getColumnNumber());
    }

    /*
     * See org.xml.sax.ext.LexicalHandler.
     */
    @Override
    public void endCDATA() throws SAXException {
        processChars();  // Flush char buffer and remove white spaces
    }

    /*
     * See org.xml.sax.ext.LexicalHandler.
     */
    @Override
    public void startEntity(String name) throws SAXException {
        // do nothing
    }

    /*
     * See org.xml.sax.ext.LexicalHandler.
     */
    @Override
    public void endEntity(String name) throws SAXException {
        // do nothing
    }

    /*
     * See org.xml.sax.ext.LexicalHandler.
     */
    @Override
    public void startDTD(String name, String publicId, String systemId)
        throws SAXException {
        if (!isValidating) {
            fatalError(new EnableDTDValidationException(
                    "jsp.error.enable_dtd_validation", null));
        }

        inDTD = true;
    }

    /*
     * See org.xml.sax.ext.LexicalHandler.
     */
    @Override
    public void endDTD() throws SAXException {
        inDTD = false;
    }

    /*
     * Receives notification of a non-recoverable error.
     */
    @Override
    public void fatalError(SAXParseException e) throws SAXException {
        throw e;
    }

    /*
     * Receives notification of a recoverable error.
     */
    @Override
    public void error(SAXParseException e) throws SAXException {
        throw e;
    }

    /*
     * Receives notification of the start of a Namespace mapping.
     */
    @Override
    public void startPrefixMapping(String prefix, String uri)
        throws SAXException {
        TagLibraryInfo taglibInfo;

        if (directivesOnly && !(JSP_URI.equals(uri))) {
            return;
        }

        try {
            taglibInfo = getTaglibInfo(prefix, uri);
        } catch (JasperException je) {
            throw new SAXParseException(
                Localizer.getMessage("jsp.error.could.not.add.taglibraries"),
                locator,
                je);
        }

        if (taglibInfo != null) {
            if (pageInfo.getTaglib(uri) == null) {
                pageInfo.addTaglib(uri, taglibInfo);
            }
            pageInfo.pushPrefixMapping(prefix, uri);
        } else {
            pageInfo.pushPrefixMapping(prefix, null);
        }
    }

    /*
     * Receives notification of the end of a Namespace mapping.
     */
    @Override
    public void endPrefixMapping(String prefix) throws SAXException {

        if (directivesOnly) {
            String uri = pageInfo.getURI(prefix);
            if (!JSP_URI.equals(uri)) {
                return;
            }
        }

        pageInfo.popPrefixMapping(prefix);
    }

    //*********************************************************************
    // Private utility methods

    private Node parseStandardAction(
        String qName,
        String localName,
        Attributes nonTaglibAttrs,
        Attributes nonTaglibXmlnsAttrs,
        Attributes taglibAttrs,
        Mark start)
        throws SAXException {

        Node node = null;

        if (localName.equals(ROOT_ACTION)) {
            if (!(current instanceof Node.Root)) {
                throw new SAXParseException(
                    Localizer.getMessage("jsp.error.nested_jsproot"),
                    locator);
            }
            node =
                new Node.JspRoot(
                    qName,
                    nonTaglibAttrs,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    start,
                    current);
            if (isTop) {
                pageInfo.setHasJspRoot(true);
            }
        } else if (localName.equals(PAGE_DIRECTIVE_ACTION)) {
            if (isTagFile) {
                throw new SAXParseException(
                    Localizer.getMessage(
                        "jsp.error.action.istagfile",
                        localName),
                    locator);
            }
            node =
                new Node.PageDirective(
                    qName,
                    nonTaglibAttrs,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    start,
                    current);
            String imports = nonTaglibAttrs.getValue("import");
            // There can only be one 'import' attribute per page directive
            if (imports != null) {
                ((Node.PageDirective)node).addImport(imports);
            }
        } else if (localName.equals(INCLUDE_DIRECTIVE_ACTION)) {
            node =
                new Node.IncludeDirective(
                    qName,
                    nonTaglibAttrs,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    start,
                    current);
            processIncludeDirective(nonTaglibAttrs.getValue("file"), node);
        } else if (localName.equals(DECLARATION_ACTION)) {
            if (scriptlessBodyNode != null) {
                // We're nested inside a node whose body is
                // declared to be scriptless
                throw new SAXParseException(
                    Localizer.getMessage(
                        "jsp.error.no.scriptlets",
                        localName),
                    locator);
            }
            node =
                new Node.Declaration(
                    qName,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    start,
                    current);
        } else if (localName.equals(SCRIPTLET_ACTION)) {
            if (scriptlessBodyNode != null) {
                // We're nested inside a node whose body is
                // declared to be scriptless
                throw new SAXParseException(
                    Localizer.getMessage(
                        "jsp.error.no.scriptlets",
                        localName),
                    locator);
            }
            node =
                new Node.Scriptlet(
                    qName,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    start,
                    current);
        } else if (localName.equals(EXPRESSION_ACTION)) {
            if (scriptlessBodyNode != null) {
                // We're nested inside a node whose body is
                // declared to be scriptless
                throw new SAXParseException(
                    Localizer.getMessage(
                        "jsp.error.no.scriptlets",
                        localName),
                    locator);
            }
            node =
                new Node.Expression(
                    qName,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    start,
                    current);
        } else if (localName.equals(USE_BEAN_ACTION)) {
            node =
                new Node.UseBean(
                    qName,
                    nonTaglibAttrs,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    start,
                    current);
        } else if (localName.equals(SET_PROPERTY_ACTION)) {
            node =
                new Node.SetProperty(
                    qName,
                    nonTaglibAttrs,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    start,
                    current);
        } else if (localName.equals(GET_PROPERTY_ACTION)) {
            node =
                new Node.GetProperty(
                    qName,
                    nonTaglibAttrs,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    start,
                    current);
        } else if (localName.equals(INCLUDE_ACTION)) {
            node =
                new Node.IncludeAction(
                    qName,
                    nonTaglibAttrs,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    start,
                    current);
        } else if (localName.equals(FORWARD_ACTION)) {
            node =
                new Node.ForwardAction(
                    qName,
                    nonTaglibAttrs,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    start,
                    current);
        } else if (localName.equals(PARAM_ACTION)) {
            node =
                new Node.ParamAction(
                    qName,
                    nonTaglibAttrs,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    start,
                    current);
        } else if (localName.equals(PARAMS_ACTION)) {
            node =
                new Node.ParamsAction(
                    qName,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    start,
                    current);
        } else if (localName.equals(PLUGIN_ACTION)) {
            node =
                new Node.PlugIn(
                    qName,
                    nonTaglibAttrs,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    start,
                    current);
        } else if (localName.equals(TEXT_ACTION)) {
            node =
                new Node.JspText(
                    qName,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    start,
                    current);
        } else if (localName.equals(BODY_ACTION)) {
            node =
                new Node.JspBody(
                    qName,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    start,
                    current);
        } else if (localName.equals(ATTRIBUTE_ACTION)) {
            node =
                new Node.NamedAttribute(
                    qName,
                    nonTaglibAttrs,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    start,
                    current);
        } else if (localName.equals(OUTPUT_ACTION)) {
            node =
                new Node.JspOutput(
                    qName,
                    nonTaglibAttrs,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    start,
                    current);
        } else if (localName.equals(TAG_DIRECTIVE_ACTION)) {
            if (!isTagFile) {
                throw new SAXParseException(
                    Localizer.getMessage(
                        "jsp.error.action.isnottagfile",
                        localName),
                    locator);
            }
            node =
                new Node.TagDirective(
                    qName,
                    nonTaglibAttrs,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    start,
                    current);
            String imports = nonTaglibAttrs.getValue("import");
            // There can only be one 'import' attribute per tag directive
            if (imports != null) {
                ((Node.TagDirective)node).addImport(imports);
            }
        } else if (localName.equals(ATTRIBUTE_DIRECTIVE_ACTION)) {
            if (!isTagFile) {
                throw new SAXParseException(
                    Localizer.getMessage(
                        "jsp.error.action.isnottagfile",
                        localName),
                    locator);
            }
            node =
                new Node.AttributeDirective(
                    qName,
                    nonTaglibAttrs,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    start,
                    current);
        } else if (localName.equals(VARIABLE_DIRECTIVE_ACTION)) {
            if (!isTagFile) {
                throw new SAXParseException(
                    Localizer.getMessage(
                        "jsp.error.action.isnottagfile",
                        localName),
                    locator);
            }
            node =
                new Node.VariableDirective(
                    qName,
                    nonTaglibAttrs,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    start,
                    current);
        } else if (localName.equals(INVOKE_ACTION)) {
            if (!isTagFile) {
                throw new SAXParseException(
                    Localizer.getMessage(
                        "jsp.error.action.isnottagfile",
                        localName),
                    locator);
            }
            node =
                new Node.InvokeAction(
                    qName,
                    nonTaglibAttrs,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    start,
                    current);
        } else if (localName.equals(DOBODY_ACTION)) {
            if (!isTagFile) {
                throw new SAXParseException(
                    Localizer.getMessage(
                        "jsp.error.action.isnottagfile",
                        localName),
                    locator);
            }
            node =
                new Node.DoBodyAction(
                    qName,
                    nonTaglibAttrs,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    start,
                    current);
        } else if (localName.equals(ELEMENT_ACTION)) {
            node =
                new Node.JspElement(
                    qName,
                    nonTaglibAttrs,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    start,
                    current);
        } else if (localName.equals(FALLBACK_ACTION)) {
            node =
                new Node.FallBackAction(
                    qName,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    start,
                    current);
        } else {
            throw new SAXParseException(
                Localizer.getMessage(
                    "jsp.error.xml.badStandardAction",
                    localName),
                locator);
        }

        return node;
    }

    /*
     * Checks if the XML element with the given tag name is a custom action,
     * and returns the corresponding Node object.
     */
    private Node parseCustomAction(
        String qName,
        String localName,
        String uri,
        Attributes nonTaglibAttrs,
        Attributes nonTaglibXmlnsAttrs,
        Attributes taglibAttrs,
        Mark start,
        Node parent)
        throws SAXException {

        // Check if this is a user-defined (custom) tag
        TagLibraryInfo tagLibInfo = pageInfo.getTaglib(uri);
        if (tagLibInfo == null) {
            return null;
        }

        TagInfo tagInfo = tagLibInfo.getTag(localName);
        TagFileInfo tagFileInfo = tagLibInfo.getTagFile(localName);
        if (tagInfo == null && tagFileInfo == null) {
            throw new SAXParseException(
                Localizer.getMessage("jsp.error.xml.bad_tag", localName, uri),
                locator);
        }
        Class<?> tagHandlerClass = null;
        if (tagInfo != null) {
            String handlerClassName = tagInfo.getTagClassName();
            try {
                tagHandlerClass =
                    ctxt.getClassLoader().loadClass(handlerClassName);
            } catch (Exception e) {
                throw new SAXParseException(
                    Localizer.getMessage("jsp.error.loadclass.taghandler",
                                         handlerClassName,
                                         qName),
                    locator, e);
            }
        }

        String prefix = getPrefix(qName);

        Node.CustomTag ret = null;
        if (tagInfo != null) {
            ret =
                new Node.CustomTag(
                    qName,
                    prefix,
                    localName,
                    uri,
                    nonTaglibAttrs,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    start,
                    parent,
                    tagInfo,
                    tagHandlerClass);
        } else {
            ret =
                new Node.CustomTag(
                    qName,
                    prefix,
                    localName,
                    uri,
                    nonTaglibAttrs,
                    nonTaglibXmlnsAttrs,
                    taglibAttrs,
                    start,
                    parent,
                    tagFileInfo);
        }

        return ret;
    }

    /*
     * Creates the tag library associated with the given uri namespace, and
     * returns it.
     *
     * @param prefix The prefix of the xmlns attribute
     * @param uri The uri namespace (value of the xmlns attribute)
     *
     * @return The tag library associated with the given uri namespace
     */
    private TagLibraryInfo getTaglibInfo(String prefix, String uri)
        throws JasperException {

        TagLibraryInfo result = null;

        if (uri.startsWith(URN_JSPTAGDIR)) {
            // uri (of the form "urn:jsptagdir:path") references tag file dir
            String tagdir = uri.substring(URN_JSPTAGDIR.length());
            result =
                new ImplicitTagLibraryInfo(
                    ctxt,
                    parserController,
                    pageInfo,
                    prefix,
                    tagdir,
                    err);
        } else {
            // uri references TLD file
            boolean isPlainUri = false;
            if (uri.startsWith(URN_JSPTLD)) {
                // uri is of the form "urn:jsptld:path"
                uri = uri.substring(URN_JSPTLD.length());
            } else {
                isPlainUri = true;
            }

            TldResourcePath tldResourcePath = ctxt.getTldResourcePath(uri);
            if (tldResourcePath != null || !isPlainUri) {
                if (ctxt.getOptions().isCaching()) {
                    result = ctxt.getOptions().getCache().get(uri);
                }
                if (result == null) {
                    /*
                     * If the uri value is a plain uri, a translation error must
                     * not be generated if the uri is not found in the taglib map.
                     * Instead, any actions in the namespace defined by the uri
                     * value must be treated as uninterpreted.
                     */
                    result =
                        new TagLibraryInfoImpl(
                            ctxt,
                            parserController,
                            pageInfo,
                            prefix,
                            uri,
                            tldResourcePath,
                            err);
                    if (ctxt.getOptions().isCaching()) {
                        ctxt.getOptions().getCache().put(uri, result);
                    }
                }
            }
        }

        return result;
    }

    /*
     * Ensures that the given body only contains nodes that are instances of
     * TemplateText.
     *
     * This check is performed only for the body of a scripting (that is:
     * declaration, scriptlet, or expression) element, after the end tag of a
     * scripting element has been reached.
     */
    private void checkScriptingBody(Node.ScriptingElement scriptingElem)
        throws SAXException {
        Node.Nodes body = scriptingElem.getBody();
        if (body != null) {
            int size = body.size();
            for (int i = 0; i < size; i++) {
                Node n = body.getNode(i);
                if (!(n instanceof Node.TemplateText)) {
                    String elemType = SCRIPTLET_ACTION;
                    if (scriptingElem instanceof Node.Declaration) {
                        elemType = DECLARATION_ACTION;
                    }
                    if (scriptingElem instanceof Node.Expression) {
                        elemType = EXPRESSION_ACTION;
                    }
                    String msg =
                        Localizer.getMessage(
                            "jsp.error.parse.xml.scripting.invalid.body",
                            elemType);
                    throw new SAXParseException(msg, locator);
                }
            }
        }
    }

    /*
     * Parses the given file included via an include directive.
     *
     * @param fname The path to the included resource, as specified by the
     * 'file' attribute of the include directive
     * @param parent The Node representing the include directive
     */
    private void processIncludeDirective(String fname, Node parent)
        throws SAXException {

        if (fname == null) {
            return;
        }

        try {
            parserController.parse(fname, parent, null);
        } catch (FileNotFoundException fnfe) {
            throw new SAXParseException(
                Localizer.getMessage("jsp.error.file.not.found", fname),
                locator,
                fnfe);
        } catch (Exception e) {
            throw new SAXParseException(e.getMessage(), locator, e);
        }
    }

    /*
     * Checks an element's given URI, qname, and attributes to see if any
     * of them hijack the 'jsp' prefix, that is, bind it to a namespace other
     * than http://java.sun.com/JSP/Page.
     *
     * @param uri The element's URI
     * @param qName The element's qname
     * @param attrs The element's attributes
     */
    private void checkPrefixes(String uri, String qName, Attributes attrs) {

        checkPrefix(uri, qName);

        int len = attrs.getLength();
        for (int i = 0; i < len; i++) {
            checkPrefix(attrs.getURI(i), attrs.getQName(i));
        }
    }

    /*
     * Checks the given URI and qname to see if they hijack the 'jsp' prefix,
     * which would be the case if qName contained the 'jsp' prefix and
     * uri was different from http://java.sun.com/JSP/Page.
     *
     * @param uri The URI to check
     * @param qName The qname to check
     */
    private void checkPrefix(String uri, String qName) {

        String prefix = getPrefix(qName);
        if (prefix.length() > 0) {
            pageInfo.addPrefix(prefix);
            if ("jsp".equals(prefix) && !JSP_URI.equals(uri)) {
                pageInfo.setIsJspPrefixHijacked(true);
            }
        }
    }

    private String getPrefix(String qName) {
        int index = qName.indexOf(':');
        if (index != -1) {
            return qName.substring(0, index);
        }
        return "";
    }

    /*
     * Gets SAXParser.
     *
     * @param validating Indicates whether the requested SAXParser should
     * be validating
     * @param jspDocParser The JSP document parser
     *
     * @return The SAXParser
     */
    private static SAXParser getSAXParser(
        boolean validating,
        JspDocumentParser jspDocParser)
        throws Exception {

        ClassLoader original;
        Thread currentThread = Thread.currentThread();
        if (Constants.IS_SECURITY_ENABLED) {
            PrivilegedGetTccl pa = new PrivilegedGetTccl(currentThread);
            original = AccessController.doPrivileged(pa);
        } else {
            original = currentThread.getContextClassLoader();
        }
        try {
            if (Constants.IS_SECURITY_ENABLED) {
                PrivilegedSetTccl pa = new PrivilegedSetTccl(currentThread, JspDocumentParser.class.getClassLoader());
                AccessController.doPrivileged(pa);
            } else {
                currentThread.setContextClassLoader(JspDocumentParser.class.getClassLoader());
            }

            SAXParserFactory factory = SAXParserFactory.newInstance();

            factory.setNamespaceAware(true);
            // Preserve xmlns attributes
            factory.setFeature(
                "http://xml.org/sax/features/namespace-prefixes",
                true);

            factory.setValidating(validating);
            if (validating) {
                // Enable DTD validation
                factory.setFeature(
                        "http://xml.org/sax/features/validation",
                        true);
                // Enable schema validation
                factory.setFeature(
                        "http://apache.org/xml/features/validation/schema",
                        true);
            }

            // Configure the parser
            SAXParser saxParser = factory.newSAXParser();
            XMLReader xmlReader = saxParser.getXMLReader();
            xmlReader.setProperty(LEXICAL_HANDLER_PROPERTY, jspDocParser);
            xmlReader.setErrorHandler(jspDocParser);

            return saxParser;
        } finally {
            if (Constants.IS_SECURITY_ENABLED) {
                PrivilegedSetTccl pa = new PrivilegedSetTccl(currentThread, original);
                AccessController.doPrivileged(pa);
            } else {
                currentThread.setContextClassLoader(original);
            }
        }
    }

    /*
     * Exception indicating that a DOCTYPE declaration is present, but
     * validation is turned off.
     */
    private static class EnableDTDValidationException
            extends SAXParseException {

        private static final long serialVersionUID = 1L;

        EnableDTDValidationException(String message, Locator loc) {
            super(message, loc);
        }

        @Override
        public synchronized Throwable fillInStackTrace() {
            // This class does not provide a stack trace
            return this;
        }
    }

    private static String getBodyType(Node.CustomTag custom) {

        if (custom.getTagInfo() != null) {
            return custom.getTagInfo().getBodyContent();
        }

        return custom.getTagFileInfo().getTagInfo().getBodyContent();
    }

    private boolean isTagDependent(Node n) {

        if (n instanceof Node.CustomTag) {
            String bodyType = getBodyType((Node.CustomTag) n);
            return
                TagInfo.BODY_CONTENT_TAG_DEPENDENT.equalsIgnoreCase(bodyType);
        }
        return false;
    }
}