| /* |
| * 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.io.Serial; |
| 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.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. |
| */ |
| 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 ioe) { |
| // ignore |
| } |
| source = JspUtil.getInputSource(path, jar, jspDocParser.ctxt); |
| saxParser.parse(source, jspDocParser); |
| } finally { |
| try { |
| source.getByteStream().close(); |
| } catch (IOException ioe) { |
| // 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. |
| */ |
| for (int i = attrs.getLength() - 1; i >= 0; i--) { |
| 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 { |
| boolean isTaglib; |
| 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; |
| |
| 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. <p> 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.isEmpty()) { |
| @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; |
| int elType; |
| 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; |
| } |
| |
| @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, |
| // except for 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 (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(); |
| } |
| } |
| |
| @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; |
| } |
| |
| @Override |
| public void fatalError(SAXParseException e) throws SAXException { |
| throw e; |
| } |
| |
| @Override |
| public void error(SAXParseException e) throws SAXException { |
| throw e; |
| } |
| |
| @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); |
| } |
| } |
| |
| @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; |
| |
| switch (localName) { |
| case 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); |
| } |
| } |
| case 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); |
| } |
| } |
| case INCLUDE_DIRECTIVE_ACTION -> { |
| node = new Node.IncludeDirective(qName, nonTaglibAttrs, nonTaglibXmlnsAttrs, taglibAttrs, start, |
| current); |
| processIncludeDirective(nonTaglibAttrs.getValue("file"), node); |
| } |
| case 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); |
| } |
| case 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); |
| } |
| case 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); |
| } |
| case USE_BEAN_ACTION -> |
| node = new Node.UseBean(qName, nonTaglibAttrs, nonTaglibXmlnsAttrs, taglibAttrs, start, current); |
| case SET_PROPERTY_ACTION -> |
| node = new Node.SetProperty(qName, nonTaglibAttrs, nonTaglibXmlnsAttrs, taglibAttrs, start, current); |
| case GET_PROPERTY_ACTION -> |
| node = new Node.GetProperty(qName, nonTaglibAttrs, nonTaglibXmlnsAttrs, taglibAttrs, start, current); |
| case INCLUDE_ACTION -> |
| node = new Node.IncludeAction(qName, nonTaglibAttrs, nonTaglibXmlnsAttrs, taglibAttrs, start, current); |
| case FORWARD_ACTION -> |
| node = new Node.ForwardAction(qName, nonTaglibAttrs, nonTaglibXmlnsAttrs, taglibAttrs, start, current); |
| case PARAM_ACTION -> |
| node = new Node.ParamAction(qName, nonTaglibAttrs, nonTaglibXmlnsAttrs, taglibAttrs, start, current); |
| case TEXT_ACTION -> node = new Node.JspText(qName, nonTaglibXmlnsAttrs, taglibAttrs, start, current); |
| case BODY_ACTION -> node = new Node.JspBody(qName, nonTaglibXmlnsAttrs, taglibAttrs, start, current); |
| case ATTRIBUTE_ACTION -> |
| node = new Node.NamedAttribute(qName, nonTaglibAttrs, nonTaglibXmlnsAttrs, taglibAttrs, start, current); |
| case OUTPUT_ACTION -> |
| node = new Node.JspOutput(qName, nonTaglibAttrs, nonTaglibXmlnsAttrs, taglibAttrs, start, current); |
| case 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); |
| } |
| } |
| case 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); |
| } |
| case 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); |
| } |
| case 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); |
| } |
| case 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); |
| } |
| case ELEMENT_ACTION -> |
| node = new Node.JspElement(qName, nonTaglibAttrs, nonTaglibXmlnsAttrs, taglibAttrs, start, current); |
| default -> 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; |
| 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.isEmpty()) { |
| 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 { |
| |
| Thread currentThread = Thread.currentThread(); |
| ClassLoader original = currentThread.getContextClassLoader(); |
| try { |
| 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 { |
| currentThread.setContextClassLoader(original); |
| } |
| } |
| |
| /* |
| * Exception indicating that a DOCTYPE declaration is present, but validation is turned off. |
| */ |
| private static class EnableDTDValidationException extends SAXParseException { |
| |
| @Serial |
| 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; |
| } |
| } |