/* * Copyright (c) 1998-2011 Caucho Technology -- all rights reserved * * This file is part of Resin(R) Open Source * * Each copy or derived work must preserve the copyright notice and this * notice unmodified. * * Resin Open Source 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 of the License, or * (at your option) any later version. * * Resin Open Source 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, or any warranty * of NON-INFRINGEMENT. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with Resin Open Source; if not, write to the * * Free Software Foundation, Inc. * 59 Temple Place, Suite 330 * Boston, MA 02111-1307 USA * * @author Scott Ferguson */ package com.caucho.jsp; import com.caucho.java.LineMap; import com.caucho.jsp.java.JspNode; import com.caucho.util.CharBuffer; import com.caucho.util.L10N; import com.caucho.util.LineCompileException; import com.caucho.vfs.Path; import com.caucho.vfs.ReadStream; import com.caucho.xml.QName; import com.caucho.xml.XmlChar; import java.io.IOException; import java.util.ArrayList; import java.util.HashSet; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; /** * Parses the JSP page. Both the XML and JSP tags are understood. However, * escaping is always done using JSP rules. */ public class JspParser { static L10N L = new L10N(JspParser.class); private static final Logger log = Logger.getLogger(JspParser.class.getName()); public static final String JSP_NS = "http://java.sun.com/JSP/Page"; public static final String JSTL_CORE_URI = "http://java.sun.com/jsp/jstl/core"; public static final String JSTL_FMT_URI = "http://java.sun.com/jsp/jstl/fmt"; public static final QName PREFIX = new QName("prefix"); public static final QName TAGLIB = new QName("taglib"); public static final QName TAGDIR = new QName("tagdir"); public static final QName URI = new QName("uri"); public static final QName JSP_DECLARATION = new QName("jsp", "declaration", JSP_NS); public static final QName JSP_SCRIPTLET = new QName("jsp", "scriptlet", JSP_NS); public static final QName JSP_EXPRESSION = new QName("jsp", "expression", JSP_NS); public static final QName JSP_DIRECTIVE_PAGE = new QName("jsp", "directive.page", JSP_NS); public static final QName JSP_DIRECTIVE_INCLUDE = new QName("jsp", "directive.include", JSP_NS); public static final QName JSP_DIRECTIVE_CACHE = new QName("jsp", "directive.cache", JSP_NS); public static final QName JSP_DIRECTIVE_TAGLIB = new QName("jsp", "directive.taglib", JSP_NS); public static final QName JSP_DIRECTIVE_ATTRIBUTE = new QName("jsp", "directive.attribute", JSP_NS); public static final QName JSP_DIRECTIVE_VARIABLE = new QName("jsp", "directive.variable", JSP_NS); public static final QName JSP_DIRECTIVE_TAG = new QName("jsp", "directive.tag", JSP_NS); public static final QName JSTL_CORE_OUT = new QName("resin-c", "out", "urn:jsptld:" + JSTL_CORE_URI); public static final QName JSTL_CORE_CHOOSE = new QName("resin-c", "choose", "urn:jsptld:" + JSTL_CORE_URI); public static final QName JSTL_CORE_WHEN = new QName("resin-c", "when", "urn:jsptld:" + JSTL_CORE_URI); public static final QName JSTL_CORE_OTHERWISE = new QName("resin-c", "otherwise", "urn:jsptld:" + JSTL_CORE_URI); public static final QName JSTL_CORE_FOREACH = new QName("resin-c", "forEach", "urn:jsptld:" + JSTL_CORE_URI); private static final int TAG_UNKNOWN = 0; private static final int TAG_JSP = 1; private static final int TAG_RAW = 2; private ParseState _parseState; private JspBuilder _jspBuilder; private ParseTagManager _tagManager; private LineMap _lineMap; private ArrayList<String> _preludeList = new ArrayList<String>(); private ArrayList<String> _codaList = new ArrayList<String>(); private ArrayList<Include> _includes = new ArrayList<Include>(); private Set<String> _prefixes = new HashSet<String>(); // jsp/18cy, jsp/18cz private Set<String> _localPrefixes = new HashSet<String>(); private Path _jspPath; private ReadStream _stream; private String _uriPwd; private String _contextPath = ""; private String _filename = ""; private int _line; private int _lineStart; private int _charCount; private int _startText; private int _peek = -1; private boolean _seenCr = false; private Namespace _namespaces = new Namespace(null, "jsp", JSP_NS); private boolean _isXml; private boolean _isTop = true; private CharBuffer _tag = new CharBuffer(); private CharBuffer _value = new CharBuffer(); private CharBuffer _text = new CharBuffer(); /** * Sets the JSP builder, which receives the SAX-like events from * JSP parser. */ void setJspBuilder(JspBuilder builder) { _jspBuilder = builder; } /** * Sets the context path for error messages. */ void setContextPath(String contextPath) { _contextPath = contextPath; } /** * Sets the parse state, which stores state information for the parsing. */ void setParseState(ParseState parseState) { _parseState = parseState; } /** * Sets the parse state, which stores state information for the parsing. */ ParseState getParseState() { return _parseState; } /** * Sets the tag manager */ void setTagManager(ParseTagManager manager) { _tagManager = manager; } /** * Returns true if JSP EL expressions are enabled. */ private boolean isELIgnored() { return _parseState.isELIgnored(); } /** * Returns true if Velocity-style statements are enabled. */ private boolean isVelocity() { return _parseState.isVelocityEnabled(); } /** * Returns true if JSP EL expressions are enabled. */ private boolean isDeferredSyntaxAllowedAsLiteral() { return _parseState.isDeferredSyntaxAllowedAsLiteral(); } /** * Adds a prelude. */ public void addPrelude(String prelude) { _preludeList.add(prelude); } /** * Adds a coda. */ public void addCoda(String coda) { _codaList.add(coda); } /** * Starts parsing the JSP page. * * @param path the JSP source file * @param uri the URI for the JSP source file. */ void parse(Path path, String uri) throws Exception { _parseState.pushNamespace("jsp", JSP_NS); _isXml = _parseState.isXml(); _filename = _contextPath + uri; if (uri != null) { int p = uri.lastIndexOf('/'); _uriPwd = p <= 0 ? "/" : uri.substring(0, p + 1); } else { _uriPwd = "/"; } _parseState.setUriPwd(_uriPwd); ReadStream is = path.openRead(); path.setUserPath(uri); try { parseJsp(is); } finally { is.close(); for (int i = 0; i < _includes.size(); i++) { Include inc = _includes.get(i); inc._stream.close(); } } } /** * Starts parsing the JSP page as a tag. * * @param path the JSP source file * @param uri the URI for the JSP source file. */ void parseTag(Path path, String uri) throws Exception { _parseState.setTag(true); parse(path, uri); } /** * Top-level JSP parser. * * @param stream the read stream containing the JSP file * * @return an XML DOM containing the JSP. */ private void parseJsp(ReadStream stream) throws Exception { _text.clear(); _includes.clear(); String uriPwd = _uriPwd; for (int i = _codaList.size() - 1; i >= 0; i--) pushInclude(_codaList.get(i), true); addInclude(stream, uriPwd); for (int i = _preludeList.size() - 1; i >= 0; i--) pushInclude(_preludeList.get(i), true); setLocation(); _jspBuilder.startDocument(); String pageEncoding = _parseState.getPageEncoding(); int ch; if (pageEncoding != null) { _parseState.setPageEncoding(pageEncoding); stream.setEncoding(pageEncoding); } switch ((ch = stream.read())) { case 0xfe: if ((ch = stream.read()) != 0xff) { throw error(L.l("Expected 0xff in UTF-16 header. UTF-16 pages with the initial byte 0xfe expect 0xff immediately following. The 0xfe 0xff sequence is used by some application to suggest UTF-16 encoding without a directive.")); } else { //_parseState.setContentType("text/html; charset=UTF-16BE"); log.finer(L.l("JSP '{0}': setting page encoding using BOM 'fe ff' -> 'UTF-16BE'", _jspPath.toString())); _parseState.setBom(0xfeff); _parseState.setPageEncoding("UTF-16BE"); stream.setEncoding("UTF-16BE"); } break; case 0xff: if ((ch = stream.read()) != 0xfe) { throw error(L.l("Expected 0xfe in UTF-16 header. UTF-16 pages with the initial byte 0xff expect 0xfe immediately following. The 0xff 0xfe sequence is used by some application to suggest UTF-16 encoding without a directive.")); } else { //_parseState.setContentType("text/html; charset=UTF-16LE"); log.finer(L.l("JSP '{0}': setting page encoding using BOM 'ff fe' -> 'UTF-16LE'", _jspPath.toString())); _parseState.setBom(0xfffe); _parseState.setPageEncoding("UTF-16LE"); stream.setEncoding("UTF-16LE"); } break; case 0xef: if ((ch = stream.read()) != 0xbb) { stream.unread(); stream.unread(); } else if ((ch = stream.read()) != 0xbf) { throw error(L.l("Expected 0xbf in UTF-8 header. UTF-8 pages with the initial byte 0xbb expect 0xbf immediately following. The 0xbb 0xbf sequence is used by some application to suggest UTF-8 encoding without a directive.")); } else { // jsp/002a, #3062 // _parseState.setContentType("text/html; charset=UTF-8"); log.finer(L.l("JSP '{0}': setting page encoding using BOM 'ef bb bf' -> 'UTF-8'", _jspPath.toString())); _parseState.setBom(0xefbbbf); _parseState.setPageEncoding("UTF-8"); stream.setEncoding("UTF-8"); } break; case -1: break; default: stream.unread(); break; } ch = read(); ch = parseXmlDeclaration(ch); try { parseNode(ch); } finally { for (int i = 0; i < _includes.size(); i++) { Include inc = _includes.get(i); inc._stream.close(); } } setLocation(); _jspBuilder.endDocument(); } private int parseXmlDeclaration(int ch) throws IOException, JspParseException { if (ch != '<') return ch; else if ((ch = read()) != '?') { unread(ch); return '<'; } else if ((ch = read()) != 'x') { addText("<?"); return ch; } else if ((ch = read()) != 'm') { addText("<?x"); return ch; } else if ((ch = read()) != 'l') { addText("<?xm"); return ch; } else if (! XmlChar.isWhitespace((ch = read()))) { addText("<?xml"); return ch; } String encoding = null; addText("<?xml "); ch = skipWhitespace(ch); while (XmlChar.isNameStart(ch)) { ch = readName(ch); String name = _tag.toString(); addText(name); if (XmlChar.isWhitespace(ch)) addText(' '); ch = skipWhitespace(ch); if (ch != '=') return ch; readValue(name, ch, true); String value = _value.toString(); addText("=\""); addText(value); addText("\""); if (name.equals("encoding")) encoding = value; ch = read(); if (XmlChar.isWhitespace(ch)) addText(' '); ch = skipWhitespace(ch); } if (ch != '?') return ch; else if ((ch = read()) != '>') { addText('?'); return ch; } else { addText("?>"); if (encoding != null) { _stream.setEncoding(encoding); _parseState.setPageEncoding(encoding); } return read(); } } private void parseNode(int ch) throws IOException, JspParseException { while (ch != -1) { switch (ch) { case '<': { switch ((ch = read())) { case '%': if (_isXml) throw error(L.l("'<%' syntax is not allowed in JSP/XML syntax.")); parseScriptlet(); _startText = _charCount; // escape '\\' after scriptlet at end of line if ((ch = read()) == '\\') { if ((ch = read()) == '\n') { ch = read(); } else if (ch == '\r') { if ((ch = read()) == '\n') ch = read(); } else addText('\\'); } break; case '/': ch = parseCloseTag(); break; case '\\': if ((ch = read()) == '%') { addText("<%"); ch = read(); } else addText("<\\"); break; case '!': if (! _isXml) addText("<!"); else if ((ch = read()) == '[') parseCdata(); else if (ch == '-' && (ch = read()) == '-') parseXmlComment(); else throw error(L.l("'{0}' was not expected after '<!'. In the XML syntax, only <!-- ... --> and <![CDATA[ ... ]> are legal. You can use '&!' to escape '<!'.", badChar(ch))); ch = read(); break; default: if (! XmlChar.isNameStart(ch)) { addText('<'); break; } ch = readName(ch); String name = _tag.toString(); int tagCode = getTag(name); if (! _isXml && tagCode == TAG_UNKNOWN) { addText("<"); addText(name); break; } if (_isTop && name.equals("jsp:root")) { if (_parseState.isForbidXml()) throw error(L.l("jsp:root must be in a JSP (XML) document, not a plain JSP.")); _text.clear(); _isXml = true; _parseState.setELIgnoredDefault(false); _parseState.setXml(true); } _isTop = false; parseOpenTag(name, ch, tagCode == TAG_UNKNOWN); ch = read(); // escape '\\' after scriptlet at end of line if (! _isXml && ch == '\\') { if ((ch = read()) == '\n') { ch = read(); } else if (ch == '\r') { if ((ch = read()) == '\n') ch = read(); } } } break; } case '&': if (! _isXml) addText((char) ch); else { addText((char) parseEntity()); } ch = read(); break; case '$': ch = read(); if (ch == '{' && ! isELIgnored()) ch = parseJspExpression(); else addText('$'); break; case '#': ch = read(); if (isVelocity()) { ch = parseVelocity(ch); } else if (ch != '{' || isELIgnored()) { addText('#'); } else if (isDeferredSyntaxAllowedAsLiteral()) { addText('#'); } else throw error(L.l("Deferred syntax ('#{...}') not allowed as literal.")); break; case '\\': switch (ch = read()) { case '$': if (! isELIgnored()) { addText('$'); ch = read(); } else addText('\\'); break; case '#': if (! isELIgnored()) { addText('#'); ch = read(); } else addText('\\'); break; case '\\': addText('\\'); break; default: addText('\\'); break; } break; default: addText((char) ch); ch = read(); break; } } addText(); /* XXX: end document if (! _activeNode.getNodeName().equals("jsp:root")) throw error(L.l("'</{0}>' expected at end of file. For XML, the top-level tag must have a matching closing tag.", activeNode.getNodeName())); */ } /** * JSTL-style expressions. Currently understood: * * <code><pre> * ${a * b} - any arbitrary expression * </pre></code> */ private int parseJspExpression() throws IOException, JspParseException { addText(); Path jspPath = _jspPath; String filename = _filename; int line = _line; CharBuffer cb = CharBuffer.allocate(); int ch; cb.append("${"); for (ch = read(); ch >= 0 && ch != '}'; ch = read()) cb.append((char) ch); cb.append("}"); ch = read(); String prefix = _parseState.findPrefix(JSTL_CORE_URI); if (prefix == null) { prefix = "resin-c"; /* _jspBuilder.startElement(JSP_DIRECTIVE_TAGLIB); _jspBuilder.attribute(new QName("prefix"), prefix); _jspBuilder.attribute(new QName("uri"), JSTL_CORE_URI); _jspBuilder.endAttributes(); _jspBuilder.endElement(JSP_DIRECTIVE_TAGLIB.getName()); */ _jspBuilder.addNamespace(prefix, JSTL_CORE_URI); processTaglib(prefix, JSTL_CORE_URI); } setLocation(jspPath, filename, line); _jspBuilder.startElement(JSTL_CORE_OUT); _jspBuilder.attribute(new QName("value"), cb.close()); _jspBuilder.attribute(new QName("escapeXml"), "false"); _jspBuilder.endAttributes(); _jspBuilder.endElement(JSTL_CORE_OUT.getName()); return ch; } private int parseVelocity(int ch) throws IOException, JspParseException { if (ch == '{') { return parseVelocityScriptlet(); } else if ('a' <= ch && ch <= 'z') { ch = readName(ch); String name = _tag.toString(); if (name.equals("if")) { ch = parseVelocityIf("if"); } else if (name.equals("elseif")) { addText(); setLocation(); JspNode node = _jspBuilder.getCurrentNode(); if (! "resin-c:when".equals(node.getTagName())) throw error(L.l("#elseif is missing a corresponding #if. Velocity-style #if syntax needs matching #if ... #elseif ... #else ... #end. The #if statements must also nest properly with any tags.")); _jspBuilder.endElement("resin-c:when"); ch = parseVelocityIf("elseif"); } else if (name.equals("else")) { addText(); setLocation(); _jspBuilder.endElement("resin-c:when"); setLocation(_jspPath, _filename, _lineStart); _lineStart = _line; _jspBuilder.startElement(JSTL_CORE_OTHERWISE); _jspBuilder.endAttributes(); ch = skipWhitespaceToEndOfLine(ch); } else if (name.equals("foreach")) { ch = parseVelocityForeach("resin-c:forEach"); } else if (name.equals("end")) { addText(); JspNode node = _jspBuilder.getCurrentNode(); String nodeName = null; if (node != null) nodeName = node.getTagName(); if (nodeName.equals("resin-c:when") || nodeName.equals("resin-c:otherwise")) { _jspBuilder.endElement(nodeName); _jspBuilder.endElement(JSTL_CORE_CHOOSE.getName()); } else if (nodeName.equals("resin-c:forEach")) _jspBuilder.endElement(nodeName); else { throw error(L.l("#end is missing a corresponding #if or #foreach. Velocity-style #if syntax needs matching #if ... #elseif ... #else ... #end. The #if statements must also nest properly with any tags.")); } ch = skipWhitespaceToEndOfLine(ch); } else { addText('#'); addText(name); } } else addText('#'); return ch; } /** * This syntax isn't part of velocity. * * <code><pre> * #{ int foo = 3; }# * </pre></code> */ private int parseVelocityScriptlet() throws IOException, JspParseException { addText(); setLocation(_jspPath, _filename, _line); _lineStart = _line; _jspBuilder.startElement(JSP_SCRIPTLET); _jspBuilder.endAttributes(); int ch = read(); while (ch >= 0) { if (ch == '}') { ch = read(); if (ch == '#') break; else addText('}'); } else { addText((char) ch); ch = read(); } } createText(); _jspBuilder.endElement(JSP_SCRIPTLET.getName()); ch = read(); if (ch == '\r') { ch = read(); if (ch == '\n') return read(); else return ch; } else if (ch == '\n') return read(); else return ch; } /** * parses a #foreach statement * * <pre> * #foreach ([Type] var in expr) * ... * #end * </pre> * * <pre> * #foreach ([Type] var in [min .. max]) * ... * #end * </pre> */ private int parseVelocityForeach(String eltName) throws IOException, JspParseException { int ch; for (ch = read(); XmlChar.isWhitespace(ch); ch = read()) { } if (ch != '(') throw error(L.l("Expected `(' after #foreach at `{0}'. The velocity-style #foreach syntax needs parentheses: #foreach ($a in expr)", badChar(ch))); addText(); processTaglib("resin-c", JSTL_CORE_URI); setLocation(_jspPath, _filename, _lineStart); _lineStart = _line; _jspBuilder.startElement(JSTL_CORE_FOREACH); CharBuffer cb = CharBuffer.allocate(); parseVelocityName(cb); if (cb.length() == 0) { throw error(L.l("Expected iteration variable for #foreach at `{0}'. The velocity-style #foreach syntax is: #foreach ($a in expr)", badChar(ch))); } String name = cb.toString(); cb.clear(); parseVelocityName(cb); if (cb.length() == 0) { throw error(L.l("Expected 'in' for #foreach at `{0}'. The velocity-style #foreach syntax is: #foreach ($a in expr)", badChar(ch))); } String value = cb.toString(); if (! value.equals("in")) { throw error(L.l("Expected 'in' for #foreach at `{0}'. The velocity-style #foreach syntax is: #foreach ($a in expr)", badChar(ch))); } if (name.startsWith("$")) name = name.substring(1); _jspBuilder.attribute(new QName("var"), name); cb.clear(); parseVelocityExpr(cb, ')'); String expr = cb.close(); if (expr.indexOf("..") > 0) { int h = 0; for (; Character.isWhitespace(expr.charAt(h)); h++) { } if (expr.charAt(h) != '[') throw error(L.l("Expected '[' for #foreach `{0}'. The velocity-style #foreach syntax is: #foreach ([Type] $a in [min .. max])", badChar(expr.charAt(h)))); int t = expr.length() - 1; for (; Character.isWhitespace(expr.charAt(t)); t--) { } if (expr.charAt(t) != ']') throw error(L.l("Expected ']' for #foreach `{0}'. The velocity-style #foreach syntax is: #foreach ($a in [min .. max])", badChar(expr.charAt(t)))); int p = expr.indexOf(".."); String min = expr.substring(h + 1, p); String max = expr.substring(p + 2, t); _jspBuilder.attribute(new QName("begin"), "${" + min + "}"); _jspBuilder.attribute(new QName("end"), "${" + max + "}"); } else { _jspBuilder.attribute(new QName("items"), "${" + expr + "}"); } _jspBuilder.endAttributes(); return skipWhitespaceToEndOfLine(read()); } /** * parses an #if statement */ private int parseVelocityIf(String eltName) throws IOException, JspParseException { int ch; for (ch = read(); XmlChar.isWhitespace(ch); ch = read()) { } if (ch != '(') throw error(L.l("Expected `(' after #if at `{0}'. The velocity-style #if syntax needs parentheses: #if (...)", badChar(ch))); addText(); processTaglib("resin-c", JSTL_CORE_URI); setLocation(_jspPath, _filename, _line); if (eltName.equals("if")) { _jspBuilder.startElement(JSTL_CORE_CHOOSE); _jspBuilder.endAttributes(); } _jspBuilder.startElement(JSTL_CORE_WHEN); _lineStart = _line; CharBuffer cb = CharBuffer.allocate(); parseVelocityExpr(cb, ')'); _jspBuilder.attribute(new QName("test"), "${" + cb.close() + "}"); _jspBuilder.endAttributes(); return skipWhitespaceToEndOfLine(read()); } private int parseVelocityName(CharBuffer cb) throws IOException, JspParseException { int ch; for (ch = read(); XmlChar.isWhitespace(ch); ch = read()) { } for (; Character.isJavaIdentifierPart((char) ch); ch = read()) cb.append((char) ch); return ch; } private int parseVelocityMin(CharBuffer cb) throws IOException, JspParseException { int ch; for (ch = read(); ch >= 0; ch = read()) { if (ch != '$') cb.append((char) ch); if (ch == '(') { ch = parseVelocityExpr(cb, ')'); cb.append((char) ch); } else if (ch == '[') { ch = parseVelocityExpr(cb, ']'); cb.append((char) ch); } else if (ch == '.') { ch = read(); if (ch == '.') return ch; else { cb.append('.'); _peek = ch; } } } return ch; } private int parseVelocityExpr(CharBuffer cb, int end) throws IOException, JspParseException { int ch; for (ch = read(); ch >= 0 && ch != end; ch = read()) { if (ch != '$') cb.append((char) ch); if (ch == '(') { ch = parseVelocityExpr(cb, ')'); cb.append((char) ch); } else if (ch == '[') { ch = parseVelocityExpr(cb, ']'); cb.append((char) ch); } } return ch; } /** * Parses a <![CDATA[ block. All text in the CDATA is uninterpreted. */ private void parseCdata() throws IOException, JspParseException { int ch; ch = readName(read()); String name = _tag.toString(); if (! name.equals("CDATA")) throw error(L.l("Expected <![CDATA[ at <!['{0}'.", name, "XML only recognizes the <![CDATA directive.")); if (ch != '[') throw error(L.l("Expected '[' at '{0}'. The XML CDATA syntax is <![CDATA[...]]>.", String.valueOf(ch))); String filename = _filename; int line = _line; while ((ch = read()) >= 0) { while (ch == ']') { if ((ch = read()) != ']') addText(']'); else if ((ch = read()) != '>') addText("]]"); else return; } addText((char) ch); } throw error(L.l("Expected closing ]]> at end of file to match <![[CDATA at {0}.", filename + ":" + line)); } /** * Parses an XML name for elements and attribute names. The parsed name * is stored in the 'tag' class variable. * * @param ch the next character * * @return the character following the name */ private int readName(int ch) throws IOException, JspParseException { _tag.clear(); for (; XmlChar.isNameChar((char) ch); ch = read()) _tag.append((char) ch); return ch; } private void parsePageDirective(String name, String value) throws IOException, JspParseException { if ("isELIgnored".equals(name)) { if ("true".equals(value)) _parseState.setELIgnored(true); } } /** * Parses a special JSP syntax. */ private void parseScriptlet() throws IOException, JspParseException { addText(); _lineStart = _line; int ch = read(); // probably should be a qname QName eltName = null; switch (ch) { case '=': eltName = JSP_EXPRESSION; ch = read(); break; case '!': eltName = JSP_DECLARATION; ch = read(); break; case '@': parseDirective(); return; case '-': if ((ch = read()) == '-') { parseComment(); return; } else { eltName = JSP_SCRIPTLET; addText('-'); } break; default: eltName = JSP_SCRIPTLET; break; } setLocation(_jspPath, _filename, _lineStart); _jspBuilder.startElement(eltName); _jspBuilder.endAttributes(); while (ch >= 0) { switch (ch) { case '\\': addText('\\'); ch = read(); if (ch >= 0) addText((char) ch); ch = read(); break; case '%': ch = read(); if (ch == '>') { createText(); setLocation(); _jspBuilder.endElement(eltName.getName()); return; } else if (ch == '\\') { ch = read(); if (ch == '>') { addText("%"); } else addText("%\\"); } else addText('%'); break; default: addText((char) ch); ch = read(); break; } } createText(); setLocation(); _jspBuilder.endElement(eltName.getName()); } /** * Parses the JSP directive syntax. */ private void parseDirective() throws IOException, JspParseException { String language = null; int ch = skipWhitespace(read()); String directive = ""; if (XmlChar.isNameStart(ch)) { ch = readName(ch); directive = _tag.toString(); } else throw error(L.l("Expected jsp directive name at '{0}'. JSP directive syntax is <%@ name attr1='value1' ... %>", badChar(ch))); QName qname; if (directive.equals("page")) qname = JSP_DIRECTIVE_PAGE; else if (directive.equals("include")) qname = JSP_DIRECTIVE_INCLUDE; else if (directive.equals("taglib")) qname = JSP_DIRECTIVE_TAGLIB; else if (directive.equals("cache")) qname = JSP_DIRECTIVE_CACHE; else if (directive.equals("attribute")) qname = JSP_DIRECTIVE_ATTRIBUTE; else if (directive.equals("variable")) qname = JSP_DIRECTIVE_VARIABLE; else if (directive.equals("tag")) qname = JSP_DIRECTIVE_TAG; else throw error(L.l("'{0}' is an unknown jsp directive. Only <%@ page ... %>, <%@ include ... %>, <%@ taglib ... %>, and <%@ cache ... %> are known.", directive)); unread(ch); ArrayList<QName> keys = new ArrayList<QName>(); ArrayList<String> values = new ArrayList<String>(); ArrayList<String> prefixes = new ArrayList<String>(); ArrayList<String> uris = new ArrayList<String>(); parseAttributes(keys, values, prefixes, uris); ch = skipWhitespace(read()); if (ch != '%' || (ch = read()) != '>') { throw error(L.l("expected '%>' at {0}. JSP directive syntax is '<%@ name attr1='value1' ... %>'. (Started at line {1})", badChar(ch), _lineStart)); } setLocation(_jspPath, _filename, _lineStart); _lineStart = _line; _jspBuilder.startElement(qname); for (int i = 0; i < keys.size(); i++) { _jspBuilder.attribute(keys.get(i), values.get(i)); } _jspBuilder.endAttributes(); if (qname.equals(JSP_DIRECTIVE_TAGLIB)) processTaglibDirective(keys, values); setLocation(); _jspBuilder.endElement(qname.getName()); if (qname.equals(JSP_DIRECTIVE_PAGE) || qname.equals(JSP_DIRECTIVE_TAG)) { String contentEncoding = _parseState.getPageEncoding(); if (contentEncoding == null) contentEncoding = _parseState.getCharEncoding(); if (contentEncoding != null) { try { _stream.setEncoding(contentEncoding); } catch (Exception e) { log.log(Level.FINER, e.toString(), e); throw error(L.l("unknown content encoding '{0}'", contentEncoding), e); } } } /* if (directive.equals("include")) parseIncludeDirective(elt); else if (directive.equals("taglib")) parseTaglibDirective(elt); */ } /** * Parses an XML comment. */ private void parseComment() throws IOException, JspParseException { int ch = read(); while (ch >= 0) { if (ch == '-') { ch = read(); while (ch == '-') { if ((ch = read()) == '-') continue; else if (ch == '%' && (ch = read()) == '>') return; else if (ch == '-') ch = read(); } } else ch = read(); } } private void parseXmlComment() throws IOException, JspParseException { int ch; while ((ch = read()) >= 0) { while (ch == '-') { if ((ch = read()) == '-' && (ch = read()) == '>') return; } } } /** * Parses the open tag. */ private void parseOpenTag(String name, int ch, boolean isXml) throws IOException, JspParseException { addText(); ch = skipWhitespace(ch); ArrayList<QName> keys = new ArrayList<QName>(); ArrayList<String> values = new ArrayList<String>(); ArrayList<String> prefixes = new ArrayList<String>(); ArrayList<String> uris = new ArrayList<String>(); unread(ch); parseAttributes(keys, values, prefixes, uris); QName qname = getElementQName(name); setLocation(_jspPath, _filename, _lineStart); _lineStart = _line; _jspBuilder.startElement(qname); for (int i = 0; i < keys.size(); i++) { QName key = keys.get(i); String value = values.get(i); _jspBuilder.attribute(key, value); } _jspBuilder.endAttributes(); for (int i = 0; i < prefixes.size(); i++) { String prefix = prefixes.get(i); String uri = uris.get(i); _jspBuilder.addNamespace(prefix, uri); } if (qname.equals(JSP_DIRECTIVE_TAGLIB)) processTaglibDirective(keys, values); ch = skipWhitespace(read()); JspNode node = _jspBuilder.getCurrentNode(); if (ch == '/') { if ((ch = read()) != '>') throw error(L.l("expected '/>' at '{0}' (for tag '<{1}>' at line {2}). The XML empty tag syntax is: <tag attr1='value1'/>", badChar(ch), name, String.valueOf(_lineStart))); setLocation(); _jspBuilder.endElement(qname.getName()); } else if (ch != '>') throw error(L.l("expected '>' at '{0}' (for tag '<{1}>' at line {2}). The XML tag syntax is: <tag attr1='value1'>", badChar(ch), name, String.valueOf(_lineStart))); // If tagdependent and not XML mode, then read the raw text. else if ("tagdependent".equals(node.getBodyContent()) && ! _isXml) { String tail = "</" + name + ">"; for (ch = read(); ch >= 0; ch = read()) { _text.append((char) ch); if (_text.endsWith(tail)) { _text.setLength(_text.length() - tail.length()); addText(); _jspBuilder.endElement(qname.getName()); return; } } throw error(L.l("expected '{0}' at end of file (for tag <{1}> at line {2}). Tags with 'tagdependent' content need close tags.", tail, String.valueOf(_lineStart))); } } /** * Returns the full QName for the JSP page's name. */ private QName getElementQName(String name) { int p = name.lastIndexOf(':'); if (p > 0) { String prefix = name.substring(0, p); String url = Namespace.find(_namespaces, prefix); _prefixes.add(prefix); if (url != null) return new QName(prefix, name.substring(p + 1), url); else return new QName("", name, ""); } else { String url = Namespace.find(_namespaces, ""); if (url != null) return new QName("", name, url); else return new QName("", name, ""); } } /** * Returns the full QName for the JSP page's name. */ private QName getAttributeQName(String name) { int p = name.lastIndexOf(':'); if (p > 0) { String prefix = name.substring(0, p); String url = Namespace.find(_namespaces, prefix); if (url != null) return new QName(prefix, name.substring(p + 1), url); else return new QName("", name, ""); } else return new QName("", name, ""); } /** * Parses the attributes of an element. */ private void parseAttributes(ArrayList<QName> names, ArrayList<String> values, ArrayList<String> prefixes, ArrayList<String> uris) throws IOException, JspParseException { int ch = skipWhitespace(read()); while (XmlChar.isNameStart(ch)) { ch = readName(ch); String key = _tag.toString(); readValue(key, ch, _isXml); String value = _value.toString(); if (key.startsWith("xmlns:")) { String prefix = key.substring(6); _jspBuilder.startPrefixMapping(prefix, value); //_parseState.pushNamespace(prefix, value); prefixes.add(prefix); uris.add(value); _namespaces = new Namespace(_namespaces, prefix, value); } else if (key.equals("xmlns")) { _jspBuilder.startPrefixMapping("", value); //_parseState.pushNamespace(prefix, value); //_parseState.pushNamespace("", value); _namespaces = new Namespace(_namespaces, "", value); } else { names.add(getAttributeQName(key)); values.add(value); } ch = skipWhitespace(read()); } unread(ch); } /** * Reads an attribute value. */ private void readValue(String attribute, int ch, boolean isXml) throws IOException, JspParseException { boolean isRuntimeAttribute = false; _value.clear(); ch = skipWhitespace(ch); if (ch != '=') { unread(ch); return; } ch = skipWhitespace(read()); if (ch != '\'' && ch != '"') { if (XmlChar.isNameChar(ch)) { ch = readName(ch); throw error(L.l("'{0}' attribute value must be quoted at '{1}'. JSP attribute syntax is either attr=\"value\" or attr='value'.", attribute, _tag)); } else throw error(L.l("'{0}' attribute value must be quoted at '{1}'. JSP attribute syntax is either attr=\"value\" or attr='value'.", attribute, badChar(ch))); } int end = ch; int lastCh = 0; ch = read(); if (ch != '<') { } else if ((ch = read()) != '%') _value.append('<'); else if ((ch = read()) != '=') _value.append("<%"); else { _value.append("<%"); isRuntimeAttribute = true; } while (ch != -1 && (ch != end || isRuntimeAttribute)) { if (ch == '\\') { switch ((ch = read())) { case '\\': case '\'': case '\"': // jsp/1505 vs jsp/184s // _value.append('\\'); _value.append((char) ch); ch = read(); break; case '>': if (lastCh == '%') { _value.append('>'); ch = read(); } else _value.append('\\'); break; case '%': if (lastCh == '<') { _value.append('%'); ch = read(); } else _value.append('\\'); break; default: _value.append('\\'); break; } } else if (ch == '%' && isRuntimeAttribute) { _value.append('%'); ch = read(); if (ch == '>') isRuntimeAttribute = false; } else if (ch == '&' && isXml) { lastCh = -1; _value.append((char) parseEntity()); ch = read(); } else if (ch == '&') { if ((ch = read()) == 'a') { if ((ch = read()) != 'p') _value.append("&a"); else if ((ch = read()) != 'o') _value.append("&ap"); else if ((ch = read()) != 's') _value.append("&apo"); else if ((ch = read()) != ';') _value.append("&apos"); else { _value.append('\''); ch = read(); } } else if (ch == 'q') { if ((ch = read()) != 'u') _value.append("&q"); else if ((ch = read()) != 'o') _value.append("&qu"); else if ((ch = read()) != 't') _value.append("&quo"); else if ((ch = read()) != ';') _value.append("""); else { _value.append('"'); ch = read(); } } else _value.append('&'); } else { _value.append((char) ch); lastCh = ch; ch = read(); } } } /** * Parses an XML entity. */ int parseEntity() throws IOException, JspParseException { int ch = read(); if (_isXml && ch == '#') { int value = 0; for (ch = read(); ch >= '0' && ch <= '9'; ch = read()) value = 10 * value + ch - '0'; if (ch != ';') throw error(L.l("expected ';' at '{0}' in character entity. The XML character entities syntax is &#nn;", badChar(ch))); return (char) value; } CharBuffer cb = CharBuffer.allocate(); for (; ch >= 'a' && ch <= 'z'; ch = read()) cb.append((char) ch); if (ch != ';') { log.warning(L.l("expected ';' at '{0}' in entity '&{1}'. The XML entity syntax is &name;", badChar(ch), cb)); } String entity = cb.close(); if (entity.equals("lt")) return '<'; else if (entity.equals("gt")) return '>'; else if (entity.equals("amp")) return '&'; else if (entity.equals("apos")) return '\''; else if (entity.equals("quot")) return '"'; else throw error(L.l("unknown entity '&{0};'. XML only recognizes the special entities <, >, &, ' and "", entity)); } private int parseCloseTag() throws IOException, JspParseException { int ch; if (! XmlChar.isNameStart(ch = read())) { addText("</"); return ch; } ch = readName(ch); String name = _tag.toString(); if (! _isXml && getTag(name) == TAG_UNKNOWN) { addText("</"); addText(name); return ch; } ch = skipWhitespace(ch); if (ch != '>') throw error(L.l("expected '>' at {0}. The XML close tag syntax is </name>.", badChar(ch))); JspNode node = _jspBuilder.getCurrentNode(); String nodeName = node.getTagName(); if (nodeName.equals(name)) { } else if (nodeName.equals("resin-c:when")) { throw error(L.l("#if expects closing #end before </{0}> (#if at {1}). #if statements require #end before the enclosing tag closes.", name, String.valueOf(node.getStartLine()))); } else if (nodeName.equals("resin-c:otherwise")) { throw error(L.l("#else expects closing #end before </{0}> (#else at {1}). #if statements require #end before the enclosing tag closes.", name, String.valueOf(node.getStartLine()))); } else { throw error(L.l("expected </{0}> at </{1}>. Closing tags must match opened tags.", nodeName, name)); } addText(); setLocation(); _jspBuilder.endElement(name); return read(); } private void processTaglibDirective(ArrayList<QName> keys, ArrayList<String> values) throws IOException, JspParseException { int p = keys.indexOf(PREFIX); if (p < 0) throw error(L.l("The taglib directive requires a 'prefix' attribute. 'prefix' is the XML prefix for all tags in the taglib.")); String prefix = values.get(p); if (_prefixes.contains(prefix) && _parseState.getQName(prefix) == null) { throw error(L.l("The taglib prefix '{0}' must be defined before it is used.", prefix)); } if (_localPrefixes.contains(prefix)) throw error(L.l( "<{0}> cannot occur after an action that uses the same prefix: {1}.", JSP_DIRECTIVE_TAGLIB.getName(), prefix)); String uri = null; p = keys.indexOf(URI); if (p >= 0) uri = values.get(p); String tagdir = null; p = keys.indexOf(TAGDIR); if (p >= 0) tagdir = values.get(p); if (uri != null) processTaglib(prefix, uri); else if (tagdir != null) processTaglibDir(prefix, tagdir); } /** * Adds a new known taglib prefix to the current namespace. */ private void processTaglib(String prefix, String uri) throws JspParseException { Taglib taglib = null; int colon = uri.indexOf(':'); int slash = uri.indexOf('/'); String location = null; if (colon > 0 && colon < slash) location = uri; else if (slash == 0) location = uri; else location = _uriPwd + uri; try { taglib = _tagManager.addTaglib(prefix, uri, location); String tldURI = "urn:jsptld:" + uri; _parseState.pushNamespace(prefix, tldURI); _namespaces = new Namespace(_namespaces, prefix, tldURI); return; } catch (JspParseException e) { throw error(e); } catch (Exception e) { log.log(Level.WARNING, e.toString(), e); } if (colon > 0 && colon < slash) throw error(L.l("Unknown taglib '{0}'. Taglibs specified with an absolute URI must either be:\n1) specified in the web.xml\n2) defined in a jar's .tld in META-INF\n3) defined in a .tld in WEB-INF\n4) predefined by Resin", uri)); } /** * Adds a new known tag dir to the current namespace. */ private void processTaglibDir(String prefix, String tagDir) throws JspParseException { Taglib taglib = null; try { taglib = _tagManager.addTaglibDir(prefix, tagDir); String tagURI = "urn:jsptagdir:" + tagDir; _parseState.pushNamespace(prefix, tagURI); _namespaces = new Namespace(_namespaces, prefix, tagURI); return; } catch (JspParseException e) { throw error(e); } catch (Exception e) { log.log(Level.WARNING, e.toString(), e); } } private void processIncludeDirective(ArrayList keys, ArrayList values) throws IOException, JspParseException { int p = keys.indexOf("file"); if (p < 0) throw error(L.l("The include directive requires a 'file' attribute.")); String file = (String) values.get(p); pushInclude(file); } public void pushInclude(String value) throws IOException, JspParseException { pushInclude(value, false); } public void pushInclude(String value, boolean allowDuplicate) throws IOException, JspParseException { if (value.equals("")) throw error("include directive needs 'file' attribute. Use either <%@ include file='myfile.jsp' %> or <jsp:directive.include file='myfile.jsp'/>"); Path include; if (value.length() > 0 && value.charAt(0) == '/') include = _parseState.resolvePath(value); else include = _parseState.resolvePath(_uriPwd + value); String newUrl = _uriPwd; if (value.startsWith("/")) newUrl = value; else newUrl = _uriPwd + value; include.setUserPath(newUrl); String newUrlPwd; int p = newUrl.lastIndexOf('/'); newUrlPwd = newUrl.substring(0, p + 1); if (_jspPath != null && _jspPath.equals(include) && ! allowDuplicate) throw error(L.l("circular include of '{0}' forbidden. A JSP file may not include itself.", include)); for (int i = 0; i < _includes.size(); i++) { Include inc = _includes.get(i); if (inc._stream != null && inc._stream.getPath() != null && inc._stream.getPath().equals(include) && ! allowDuplicate) throw error(L.l("circular include of '{0}'. A JSP file may not include itself.", include)); } try { addInclude(include.openRead(), newUrlPwd); } catch (IOException e) { log.log(Level.WARNING, e.toString(), e); if (include.exists()) throw error(L.l("can't open include of '{0}'. '{1}' exists but it's not readable.", value, include.getNativePath())); else throw error(L.l("can't open include of '{0}'. '{1}' does not exist.", value, include.getNativePath())); } } private void addInclude(ReadStream stream, String newUrlPwd) throws IOException, JspParseException { addText(); readLf(); Include inc = null; if (_stream != null) { inc = new Include(_localPrefixes, _stream, _line, _uriPwd, _parseState.isLocalScriptingInvalid()); _parseState.setLocalScriptingInvalid(false); _includes.add(inc); _localPrefixes = new HashSet<String>(); } _parseState.addDepend(stream.getPath()); try { String encoding = _stream.getEncoding(); if (encoding != null) stream.setEncoding(encoding); } catch (Exception e) { } _stream = stream; _filename = stream.getUserPath(); _jspPath = stream.getPath(); _line = 1; _lineStart = _line; _uriPwd = newUrlPwd; _parseState.setUriPwd(_uriPwd); } /** * Skips whitespace characters. * * @param ch the current character * * @return the first non-whitespace character */ private int skipWhitespace(int ch) throws IOException, JspParseException { for (; XmlChar.isWhitespace(ch); ch = read()) { } return ch; } /** * Skips whitespace to end of line * * @param ch the current character * * @return the first non-whitespace character */ private int skipWhitespaceToEndOfLine(int ch) throws IOException, JspParseException { for (; XmlChar.isWhitespace(ch); ch = read()) { if (ch == '\n') return read(); else if (ch == '\r') { ch = read(); if (ch == '\n') return read(); else return ch; } } return ch; } private void addText(char ch) { _text.append(ch); } private void addText(String s) { _text.append(s); } private void addText() throws JspParseException { if (_text.length() > 0) createText(); _startText = _charCount; _lineStart = _line; } private void createText() throws JspParseException { String string = _text.toString(); setLocation(_jspPath, _filename, _lineStart); if (_parseState.isTrimWhitespace() && isWhitespace(string)) { } else _jspBuilder.text(string, _filename, _lineStart, _line); _lineStart = _line; _text.clear(); _startText = _charCount; } private boolean isWhitespace(String s) { int length = s.length(); for (int i = 0; i < length; i++) { if (! Character.isWhitespace(s.charAt(i))) return false; } return true; } /** * Checks to see if the element name represents a tag. */ private int getTag(String name) throws JspParseException { int p = name.indexOf(':'); if (p < 0) return TAG_UNKNOWN; String prefix = name.substring(0, p); String local = name.substring(p + 1); _prefixes.add(prefix); _localPrefixes.add(prefix); String url = Namespace.find(_namespaces, prefix); if (url != null) return TAG_JSP; else return TAG_UNKNOWN; /* QName qname; if (url != null) qname = new QName(prefix, local, url); else qname = new QName(prefix, local, null); TagInfo tag = _tagManager.getTag(qname); if (tag != null) return TAG_JSP; else return TAG_UNKNOWN; */ } private void unread(int ch) { _peek = ch; } /** * Reads the next character we're in the middle of cr/lf. */ private void readLf() throws IOException, JspParseException { if (_seenCr) { int ch = read(); if (ch != '\n') _peek = ch; } } /** * Reads the next character. */ private int read() throws IOException, JspParseException { int ch; if (_peek >= 0) { ch = _peek; _peek = -1; return ch; } try { if ((ch = _stream.readChar()) >= 0) { _charCount++; if (ch == '\r') { _line++; _charCount = 0; _seenCr = true; } else if (ch == '\n' && _seenCr) { _seenCr = false; _charCount = 0; } else if (ch == '\n') { _line++; _charCount = 0; } else { _seenCr = false; } return ch; } } catch (IOException e) { throw error(e.toString()); } _stream.close(); _seenCr = false; if (_includes.size() > 0) { setLocation(_jspPath, _filename, _line); Include include = _includes.get(_includes.size() - 1); _includes.remove(_includes.size() - 1); _stream = include._stream; _filename = _stream.getUserPath(); _jspPath = _stream.getPath(); _line = include._line; _lineStart = _line; _uriPwd = include._uriPwd; _localPrefixes = include._localPrefixes; _parseState.setUriPwd(_uriPwd); _parseState.setLocalScriptingInvalid(include._oldLocalScriptingDisabled); setLocation(_jspPath, _filename, _line); return read(); } return -1; } void clear(Path appDir, String errorPage) { } /** * Creates an error message adding the filename and line. * * @param e the exception */ public JspParseException error(Exception e) { String message = e.getMessage(); if (e instanceof JspParseException) { log.log(Level.FINE, e.toString(), e); } if (e instanceof JspLineParseException) return (JspLineParseException) e; else if (e instanceof LineCompileException) return new JspLineParseException(e); if (_lineMap == null) return new JspLineParseException(_filename + ":" + _line + ": " + message, e); else { LineMap.Line line = _lineMap.getLine(_line); return new JspLineParseException(line.getSourceFilename() + ":" + line.getSourceLine(_line) + ": " + message, e); } } /** * Creates an error message adding the filename and line. * * @param message the error message */ public JspParseException error(String message) { JspGenerator gen = _jspBuilder.getGenerator(); if (_lineMap == null) { if (gen != null) return new JspLineParseException(_filename + ":" + _line + ": " + message + gen.getSourceLines(_jspPath, _line)); else return new JspLineParseException(_filename + ":" + _line + ": " + message); } else { LineMap.Line line = _lineMap.getLine(_line); return new JspLineParseException(line.getSourceFilename() + ":" + line.getSourceLine(_line) + ": " + message); } } /** * Creates an error message adding the filename and line. * * @param message the error message */ public JspParseException error(String message, Throwable e) { if (_lineMap == null) return new JspLineParseException(_filename + ":" + _line + ": " + message, e); else { LineMap.Line line = _lineMap.getLine(_line); return new JspLineParseException(line.getSourceFilename() + ":" + line.getSourceLine(_line) + ": " + message, e); } } /** * Sets the current location in the original file */ private void setLocation() { setLocation(_jspPath, _filename, _line); } /** * Sets the current location in the original file * * @param filename the filename * @param line the line in the source file */ private void setLocation(Path jspPath, String filename, int line) { if (_lineMap == null) { _jspBuilder.setLocation(jspPath, filename, line); } else { LineMap.Line srcLine = _lineMap.getLine(line); if (srcLine != null) { _jspBuilder.setLocation(jspPath, srcLine.getSourceFilename(), srcLine.getSourceLine(line)); } } } private String badChar(int ch) { if (ch < 0) return "end of file"; else if (ch == '\n' || ch == '\r') return "end of line"; else if (ch >= 0x20 && ch <= 0x7f) return "'" + (char) ch + "'"; else return "'" + (char) ch + "' (\\u" + hex(ch) + ")"; } private String hex(int value) { CharBuffer cb = new CharBuffer(); for (int b = 3; b >= 0; b--) { int v = (value >> (4 * b)) & 0xf; if (v < 10) cb.append((char) (v + '0')); else cb.append((char) (v - 10 + 'a')); } return cb.toString(); } class Include { ReadStream _stream; int _line; String _uriPwd; Set<String> _localPrefixes; boolean _oldLocalScriptingDisabled; Include(Set<String> prefixes, ReadStream stream, int line, String uriPwd, boolean oldLocalScriptingDisabled ) { _stream = stream; _line = line; _uriPwd = uriPwd; _localPrefixes = prefixes; _oldLocalScriptingDisabled = oldLocalScriptingDisabled; } } }