Return-Path: Delivered-To: apmail-struts-commits-archive@minotaur.apache.org Received: (qmail 88491 invoked from network); 28 Sep 2009 00:44:41 -0000 Received: from hermes.apache.org (HELO mail.apache.org) (140.211.11.3) by minotaur.apache.org with SMTP; 28 Sep 2009 00:44:41 -0000 Received: (qmail 43077 invoked by uid 500); 28 Sep 2009 00:44:40 -0000 Delivered-To: apmail-struts-commits-archive@struts.apache.org Received: (qmail 43039 invoked by uid 500); 28 Sep 2009 00:44:40 -0000 Mailing-List: contact commits-help@struts.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@struts.apache.org Delivered-To: mailing list commits@struts.apache.org Received: (qmail 43030 invoked by uid 99); 28 Sep 2009 00:44:40 -0000 Received: from nike.apache.org (HELO nike.apache.org) (192.87.106.230) by apache.org (qpsmtpd/0.29) with ESMTP; Mon, 28 Sep 2009 00:44:40 +0000 X-ASF-Spam-Status: No, hits=-2000.0 required=10.0 tests=ALL_TRUSTED X-Spam-Check-By: apache.org Received: from [140.211.11.4] (HELO eris.apache.org) (140.211.11.4) by apache.org (qpsmtpd/0.29) with ESMTP; Mon, 28 Sep 2009 00:44:34 +0000 Received: by eris.apache.org (Postfix, from userid 65534) id 36C7823889E9; Mon, 28 Sep 2009 00:43:43 +0000 (UTC) Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: svn commit: r819435 [11/23] - in /struts/struts2/trunk/plugins/embeddedjsp/src/main/java/org/apache/struts2/jasper: ./ compiler/ compiler/tagplugin/ el/ runtime/ security/ servlet/ tagplugins/ tagplugins/jstl/ tagplugins/jstl/core/ util/ xmlparser/ Date: Mon, 28 Sep 2009 00:43:39 -0000 To: commits@struts.apache.org From: musachy@apache.org X-Mailer: svnmailer-1.0.8 Message-Id: <20090928004343.36C7823889E9@eris.apache.org> X-Virus-Checked: Checked by ClamAV on apache.org Added: struts/struts2/trunk/plugins/embeddedjsp/src/main/java/org/apache/struts2/jasper/compiler/Parser.java URL: http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/embeddedjsp/src/main/java/org/apache/struts2/jasper/compiler/Parser.java?rev=819435&view=auto ============================================================================== --- struts/struts2/trunk/plugins/embeddedjsp/src/main/java/org/apache/struts2/jasper/compiler/Parser.java (added) +++ struts/struts2/trunk/plugins/embeddedjsp/src/main/java/org/apache/struts2/jasper/compiler/Parser.java Mon Sep 28 00:43:34 2009 @@ -0,0 +1,1791 @@ +/* + * 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.net.URL; +import java.util.Iterator; +import java.util.List; + +import javax.servlet.jsp.tagext.TagAttributeInfo; +import javax.servlet.jsp.tagext.TagFileInfo; +import javax.servlet.jsp.tagext.TagInfo; +import javax.servlet.jsp.tagext.TagLibraryInfo; + +import org.apache.jasper.JasperException; +import org.apache.jasper.JspCompilationContext; +import org.xml.sax.Attributes; +import org.xml.sax.helpers.AttributesImpl; + +/** + * This class implements a parser for a JSP page (non-xml view). JSP page + * grammar is included here for reference. The token '#' that appears in the + * production indicates the current input token location in the production. + * + * @author Kin-man Chung + * @author Shawn Bayern + * @author Mark Roth + */ + +class Parser implements TagConstants { + + private ParserController parserController; + + private JspCompilationContext ctxt; + + private JspReader reader; + + private String currentFile; + + private Mark start; + + private ErrorDispatcher err; + + private int scriptlessCount; + + private boolean isTagFile; + + private boolean directivesOnly; + + private URL jarFileUrl; + + private PageInfo pageInfo; + + // Virtual body content types, to make parsing a little easier. + // These are not accessible from outside the parser. + private static final String JAVAX_BODY_CONTENT_PARAM = "JAVAX_BODY_CONTENT_PARAM"; + + private static final String JAVAX_BODY_CONTENT_PLUGIN = "JAVAX_BODY_CONTENT_PLUGIN"; + + private static final String JAVAX_BODY_CONTENT_TEMPLATE_TEXT = "JAVAX_BODY_CONTENT_TEMPLATE_TEXT"; + + private static final boolean STRICT_QUOTE_ESCAPING = Boolean.valueOf( + System.getProperty( + "org.apache.jasper.compiler.Parser.STRICT_QUOTE_ESCAPING", + "true")).booleanValue(); + + /** + * The constructor + */ + private Parser(ParserController pc, JspReader reader, boolean isTagFile, + boolean directivesOnly, URL jarFileUrl) { + this.parserController = pc; + this.ctxt = pc.getJspCompilationContext(); + this.pageInfo = pc.getCompiler().getPageInfo(); + this.err = pc.getCompiler().getErrorDispatcher(); + this.reader = reader; + this.currentFile = reader.mark().getFile(); + this.scriptlessCount = 0; + this.isTagFile = isTagFile; + this.directivesOnly = directivesOnly; + this.jarFileUrl = jarFileUrl; + start = reader.mark(); + } + + /** + * The main entry for Parser + * + * @param pc + * The ParseController, use for getting other objects in compiler + * and for parsing included pages + * @param reader + * To read the page + * @param parent + * The parent node to this page, null for top level page + * @return list of nodes representing the parsed page + */ + public static Node.Nodes parse(ParserController pc, JspReader reader, + Node parent, boolean isTagFile, boolean directivesOnly, + URL jarFileUrl, String pageEnc, String jspConfigPageEnc, + boolean isDefaultPageEncoding, boolean isBomPresent) throws JasperException { + + Parser parser = new Parser(pc, reader, isTagFile, directivesOnly, + jarFileUrl); + + Node.Root root = new Node.Root(reader.mark(), parent, false); + root.setPageEncoding(pageEnc); + root.setJspConfigPageEncoding(jspConfigPageEnc); + root.setIsDefaultPageEncoding(isDefaultPageEncoding); + root.setIsBomPresent(isBomPresent); + + if (directivesOnly) { + parser.parseTagFileDirectives(root); + return new Node.Nodes(root); + } + + // For the Top level page, add inlcude-prelude and include-coda + PageInfo pageInfo = pc.getCompiler().getPageInfo(); + if (parent == null) { + parser.addInclude(root, pageInfo.getIncludePrelude()); + } + while (reader.hasMoreInput()) { + parser.parseElements(root); + } + if (parent == null) { + parser.addInclude(root, pageInfo.getIncludeCoda()); + } + + Node.Nodes page = new Node.Nodes(root); + return page; + } + + /** + * Attributes ::= (S Attribute)* S? + */ + Attributes parseAttributes() throws JasperException { + AttributesImpl attrs = new AttributesImpl(); + + reader.skipSpaces(); + while (parseAttribute(attrs)) + reader.skipSpaces(); + + return attrs; + } + + /** + * Parse Attributes for a reader, provided for external use + */ + public static Attributes parseAttributes(ParserController pc, + JspReader reader) throws JasperException { + Parser tmpParser = new Parser(pc, reader, false, false, null); + return tmpParser.parseAttributes(); + } + + /** + * Attribute ::= Name S? Eq S? ( '"<%=' RTAttributeValueDouble | '"' + * AttributeValueDouble | "'<%=" RTAttributeValueSingle | "'" + * AttributeValueSingle } Note: JSP and XML spec does not allow while spaces + * around Eq. It is added to be backward compatible with Tomcat, and with + * other xml parsers. + */ + private boolean parseAttribute(AttributesImpl attrs) throws JasperException { + + // Get the qualified name + String qName = parseName(); + if (qName == null) + return false; + + // Determine prefix and local name components + String localName = qName; + String uri = ""; + int index = qName.indexOf(':'); + if (index != -1) { + String prefix = qName.substring(0, index); + uri = pageInfo.getURI(prefix); + if (uri == null) { + err.jspError(reader.mark(), + "jsp.error.attribute.invalidPrefix", prefix); + } + localName = qName.substring(index + 1); + } + + reader.skipSpaces(); + if (!reader.matches("=")) + err.jspError(reader.mark(), "jsp.error.attribute.noequal"); + + reader.skipSpaces(); + char quote = (char) reader.nextChar(); + if (quote != '\'' && quote != '"') + err.jspError(reader.mark(), "jsp.error.attribute.noquote"); + + String watchString = ""; + if (reader.matches("<%=")) + watchString = "%>"; + watchString = watchString + quote; + + String attrValue = parseAttributeValue(watchString); + attrs.addAttribute(uri, localName, qName, "CDATA", attrValue); + return true; + } + + /** + * Name ::= (Letter | '_' | ':') (Letter | Digit | '.' | '_' | '-' | ':')* + */ + private String parseName() throws JasperException { + char ch = (char) reader.peekChar(); + if (Character.isLetter(ch) || ch == '_' || ch == ':') { + StringBuffer buf = new StringBuffer(); + buf.append(ch); + reader.nextChar(); + ch = (char) reader.peekChar(); + while (Character.isLetter(ch) || Character.isDigit(ch) || ch == '.' + || ch == '_' || ch == '-' || ch == ':') { + buf.append(ch); + reader.nextChar(); + ch = (char) reader.peekChar(); + } + return buf.toString(); + } + return null; + } + + /** + * AttributeValueDouble ::= (QuotedChar - '"')* ('"' | ) + * RTAttributeValueDouble ::= ((QuotedChar - '"')* - ((QuotedChar-'"')'%>"') + * ('%>"' | TRANSLATION_ERROR) + */ + private String parseAttributeValue(String watch) throws JasperException { + Mark start = reader.mark(); + Mark stop = reader.skipUntilIgnoreEsc(watch); + if (stop == null) { + err.jspError(start, "jsp.error.attribute.unterminated", watch); + } + + String ret = parseQuoted(start, reader.getText(start, stop), + watch.charAt(watch.length() - 1)); + if (watch.length() == 1) // quote + return ret; + + // putback delimiter '<%=' and '%>', since they are needed if the + // attribute does not allow RTexpression. + return "<%=" + ret + "%>"; + } + + /** + * QuotedChar ::= ''' | '"' | '\\' | '\"' | "\'" | '\>' | '\$' | + * Char + */ + private String parseQuoted(Mark start, String tx, char quote) + throws JasperException { + StringBuffer buf = new StringBuffer(); + int size = tx.length(); + int i = 0; + while (i < size) { + char ch = tx.charAt(i); + if (ch == '&') { + if (i + 5 < size && tx.charAt(i + 1) == 'a' + && tx.charAt(i + 2) == 'p' && tx.charAt(i + 3) == 'o' + && tx.charAt(i + 4) == 's' && tx.charAt(i + 5) == ';') { + buf.append('\''); + i += 6; + } else if (i + 5 < size && tx.charAt(i + 1) == 'q' + && tx.charAt(i + 2) == 'u' && tx.charAt(i + 3) == 'o' + && tx.charAt(i + 4) == 't' && tx.charAt(i + 5) == ';') { + buf.append('"'); + i += 6; + } else { + buf.append(ch); + ++i; + } + } else if (ch == '\\' && i + 1 < size) { + ch = tx.charAt(i + 1); + if (ch == '\\' || ch == '\"' || ch == '\'' || ch == '>') { + // \ " and ' are always unescaped regardless of if they are + // inside or outside of an EL expression. JSP.1.6 takes + // precedence over JSP.1.3.10 (confirmed with EG). + buf.append(ch); + i += 2; + } else { + buf.append('\\'); + ++i; + } + } else if (ch == quote && STRICT_QUOTE_ESCAPING) { + // Unescaped quote character + err.jspError(start, "jsp.error.attribute.noescape", tx, + "" + quote); + } else { + buf.append(ch); + ++i; + } + } + return buf.toString(); + } + + private String parseScriptText(String tx) { + CharArrayWriter cw = new CharArrayWriter(); + int size = tx.length(); + int i = 0; + while (i < size) { + char ch = tx.charAt(i); + if (i + 2 < size && ch == '%' && tx.charAt(i + 1) == '\\' + && tx.charAt(i + 2) == '>') { + cw.write('%'); + cw.write('>'); + i += 3; + } else { + cw.write(ch); + ++i; + } + } + cw.close(); + return cw.toString(); + } + + /* + * Invokes parserController to parse the included page + */ + private void processIncludeDirective(String file, Node parent) + throws JasperException { + if (file == null) { + return; + } + + try { + parserController.parse(file, parent, jarFileUrl); + } catch (FileNotFoundException ex) { + err.jspError(start, "jsp.error.file.not.found", file); + } catch (Exception ex) { + err.jspError(start, ex.getMessage()); + } + } + + /* + * Parses a page directive with the following syntax: PageDirective ::= ( S + * Attribute)* + */ + private void parsePageDirective(Node parent) throws JasperException { + Attributes attrs = parseAttributes(); + Node.PageDirective n = new Node.PageDirective(attrs, start, parent); + + /* + * A page directive may contain multiple 'import' attributes, each of + * which consists of a comma-separated list of package names. Store each + * list with the node, where it is parsed. + */ + for (int i = 0; i < attrs.getLength(); i++) { + if ("import".equals(attrs.getQName(i))) { + n.addImport(attrs.getValue(i)); + } + } + } + + /* + * Parses an include directive with the following syntax: IncludeDirective + * ::= ( S Attribute)* + */ + private void parseIncludeDirective(Node parent) throws JasperException { + Attributes attrs = parseAttributes(); + + // Included file expanded here + Node includeNode = new Node.IncludeDirective(attrs, start, parent); + processIncludeDirective(attrs.getValue("file"), includeNode); + } + + /** + * Add a list of files. This is used for implementing include-prelude and + * include-coda of jsp-config element in web.xml + */ + private void addInclude(Node parent, List files) throws JasperException { + if (files != null) { + Iterator iter = files.iterator(); + while (iter.hasNext()) { + String file = (String) iter.next(); + AttributesImpl attrs = new AttributesImpl(); + attrs.addAttribute("", "file", "file", "CDATA", file); + + // Create a dummy Include directive node + Node includeNode = new Node.IncludeDirective(attrs, reader + .mark(), parent); + processIncludeDirective(file, includeNode); + } + } + } + + /* + * Parses a taglib directive with the following syntax: Directive ::= ( S + * Attribute)* + */ + private void parseTaglibDirective(Node parent) throws JasperException { + + Attributes attrs = parseAttributes(); + String uri = attrs.getValue("uri"); + String prefix = attrs.getValue("prefix"); + if (prefix != null) { + Mark prevMark = pageInfo.getNonCustomTagPrefix(prefix); + if (prevMark != null) { + err.jspError(reader.mark(), "jsp.error.prefix.use_before_dcl", + prefix, prevMark.getFile(), "" + + prevMark.getLineNumber()); + } + if (uri != null) { + String uriPrev = pageInfo.getURI(prefix); + if (uriPrev != null && !uriPrev.equals(uri)) { + err.jspError(reader.mark(), "jsp.error.prefix.refined", + prefix, uri, uriPrev); + } + if (pageInfo.getTaglib(uri) == null) { + TagLibraryInfoImpl impl = null; + if (ctxt.getOptions().isCaching()) { + impl = (TagLibraryInfoImpl) ctxt.getOptions() + .getCache().get(uri); + } + if (impl == null) { + String[] location = ctxt.getTldLocation(uri); + impl = new TagLibraryInfoImpl(ctxt, parserController, pageInfo, + prefix, uri, location, err); + if (ctxt.getOptions().isCaching()) { + ctxt.getOptions().getCache().put(uri, impl); + } + } else { + // Current compilation context needs location of cached + // tag files + for (TagFileInfo info : impl.getTagFiles()) { + ctxt.setTagFileJarUrl(info.getPath(), + ctxt.getTagFileJarUrl()); + } + } + pageInfo.addTaglib(uri, impl); + } + pageInfo.addPrefixMapping(prefix, uri); + } else { + String tagdir = attrs.getValue("tagdir"); + if (tagdir != null) { + String urnTagdir = URN_JSPTAGDIR + tagdir; + if (pageInfo.getTaglib(urnTagdir) == null) { + pageInfo.addTaglib(urnTagdir, + new ImplicitTagLibraryInfo(ctxt, + parserController, pageInfo, prefix, tagdir, err)); + } + pageInfo.addPrefixMapping(prefix, urnTagdir); + } + } + } + + new Node.TaglibDirective(attrs, start, parent); + } + + /* + * Parses a directive with the following syntax: Directive ::= S? ( 'page' + * PageDirective | 'include' IncludeDirective | 'taglib' TagLibDirective) S? + * '%>' + * + * TagDirective ::= S? ('tag' PageDirective | 'include' IncludeDirective | + * 'taglib' TagLibDirective) | 'attribute AttributeDirective | 'variable + * VariableDirective S? '%>' + */ + private void parseDirective(Node parent) throws JasperException { + reader.skipSpaces(); + + String directive = null; + if (reader.matches("page")) { + directive = "<%@ page"; + if (isTagFile) { + err.jspError(reader.mark(), "jsp.error.directive.istagfile", + directive); + } + parsePageDirective(parent); + } else if (reader.matches("include")) { + directive = "<%@ include"; + parseIncludeDirective(parent); + } else if (reader.matches("taglib")) { + if (directivesOnly) { + // No need to get the tagLibInfo objects. This alos suppresses + // parsing of any tag files used in this tag file. + return; + } + directive = "<%@ taglib"; + parseTaglibDirective(parent); + } else if (reader.matches("tag")) { + directive = "<%@ tag"; + if (!isTagFile) { + err.jspError(reader.mark(), "jsp.error.directive.isnottagfile", + directive); + } + parseTagDirective(parent); + } else if (reader.matches("attribute")) { + directive = "<%@ attribute"; + if (!isTagFile) { + err.jspError(reader.mark(), "jsp.error.directive.isnottagfile", + directive); + } + parseAttributeDirective(parent); + } else if (reader.matches("variable")) { + directive = "<%@ variable"; + if (!isTagFile) { + err.jspError(reader.mark(), "jsp.error.directive.isnottagfile", + directive); + } + parseVariableDirective(parent); + } else { + err.jspError(reader.mark(), "jsp.error.invalid.directive"); + } + + reader.skipSpaces(); + if (!reader.matches("%>")) { + err.jspError(start, "jsp.error.unterminated", directive); + } + } + + /* + * Parses a directive with the following syntax: + * + * XMLJSPDirectiveBody ::= S? ( ( 'page' PageDirectiveAttrList S? ( '/>' | ( + * '>' S? ETag ) ) | ( 'include' IncludeDirectiveAttrList S? ( '/>' | ( '>' + * S? ETag ) ) | + * + * XMLTagDefDirectiveBody ::= ( ( 'tag' TagDirectiveAttrList S? ( '/>' | ( + * '>' S? ETag ) ) | ( 'include' IncludeDirectiveAttrList S? ( '/>' | ( '>' + * S? ETag ) ) | ( 'attribute' AttributeDirectiveAttrList S? ( '/>' | ( '>' + * S? ETag ) ) | ( 'variable' VariableDirectiveAttrList S? ( '/>' | ( '>' S? + * ETag ) ) ) | + */ + private void parseXMLDirective(Node parent) throws JasperException { + reader.skipSpaces(); + + String eTag = null; + if (reader.matches("page")) { + eTag = "jsp:directive.page"; + if (isTagFile) { + err.jspError(reader.mark(), "jsp.error.directive.istagfile", + "<" + eTag); + } + parsePageDirective(parent); + } else if (reader.matches("include")) { + eTag = "jsp:directive.include"; + parseIncludeDirective(parent); + } else if (reader.matches("tag")) { + eTag = "jsp:directive.tag"; + if (!isTagFile) { + err.jspError(reader.mark(), "jsp.error.directive.isnottagfile", + "<" + eTag); + } + parseTagDirective(parent); + } else if (reader.matches("attribute")) { + eTag = "jsp:directive.attribute"; + if (!isTagFile) { + err.jspError(reader.mark(), "jsp.error.directive.isnottagfile", + "<" + eTag); + } + parseAttributeDirective(parent); + } else if (reader.matches("variable")) { + eTag = "jsp:directive.variable"; + if (!isTagFile) { + err.jspError(reader.mark(), "jsp.error.directive.isnottagfile", + "<" + eTag); + } + parseVariableDirective(parent); + } else { + err.jspError(reader.mark(), "jsp.error.invalid.directive"); + } + + reader.skipSpaces(); + if (reader.matches(">")) { + reader.skipSpaces(); + if (!reader.matchesETag(eTag)) { + err.jspError(start, "jsp.error.unterminated", "<" + eTag); + } + } else if (!reader.matches("/>")) { + err.jspError(start, "jsp.error.unterminated", "<" + eTag); + } + } + + /* + * Parses a tag directive with the following syntax: PageDirective ::= ( S + * Attribute)* + */ + private void parseTagDirective(Node parent) throws JasperException { + Attributes attrs = parseAttributes(); + Node.TagDirective n = new Node.TagDirective(attrs, start, parent); + + /* + * A page directive may contain multiple 'import' attributes, each of + * which consists of a comma-separated list of package names. Store each + * list with the node, where it is parsed. + */ + for (int i = 0; i < attrs.getLength(); i++) { + if ("import".equals(attrs.getQName(i))) { + n.addImport(attrs.getValue(i)); + } + } + } + + /* + * Parses a attribute directive with the following syntax: + * AttributeDirective ::= ( S Attribute)* + */ + private void parseAttributeDirective(Node parent) throws JasperException { + Attributes attrs = parseAttributes(); + Node.AttributeDirective n = new Node.AttributeDirective(attrs, start, + parent); + } + + /* + * Parses a variable directive with the following syntax: PageDirective ::= ( + * S Attribute)* + */ + private void parseVariableDirective(Node parent) throws JasperException { + Attributes attrs = parseAttributes(); + Node.VariableDirective n = new Node.VariableDirective(attrs, start, + parent); + } + + /* + * JSPCommentBody ::= (Char* - (Char* '--%>')) '--%>' + */ + private void parseComment(Node parent) throws JasperException { + start = reader.mark(); + Mark stop = reader.skipUntil("--%>"); + if (stop == null) { + err.jspError(start, "jsp.error.unterminated", "<%--"); + } + + new Node.Comment(reader.getText(start, stop), start, parent); + } + + /* + * DeclarationBody ::= (Char* - (char* '%>')) '%>' + */ + private void parseDeclaration(Node parent) throws JasperException { + start = reader.mark(); + Mark stop = reader.skipUntil("%>"); + if (stop == null) { + err.jspError(start, "jsp.error.unterminated", "<%!"); + } + + new Node.Declaration(parseScriptText(reader.getText(start, stop)), + start, parent); + } + + /* + * XMLDeclarationBody ::= ( S? '/>' ) | ( S? '>' (Char* - (char* '<')) + * CDSect?)* ETag | CDSect ::= CDStart CData CDEnd + * CDStart ::= '' Char*)) CDEnd + * ::= ']]>' + */ + private void parseXMLDeclaration(Node parent) throws JasperException { + reader.skipSpaces(); + if (!reader.matches("/>")) { + if (!reader.matches(">")) { + err.jspError(start, "jsp.error.unterminated", + "<jsp:declaration>"); + } + Mark stop; + String text; + while (true) { + start = reader.mark(); + stop = reader.skipUntil("<"); + if (stop == null) { + err.jspError(start, "jsp.error.unterminated", + "<jsp:declaration>"); + } + text = parseScriptText(reader.getText(start, stop)); + new Node.Declaration(text, start, parent); + if (reader.matches("![CDATA[")) { + start = reader.mark(); + stop = reader.skipUntil("]]>"); + if (stop == null) { + err.jspError(start, "jsp.error.unterminated", "CDATA"); + } + text = parseScriptText(reader.getText(start, stop)); + new Node.Declaration(text, start, parent); + } else { + break; + } + } + + if (!reader.matchesETagWithoutLessThan("jsp:declaration")) { + err.jspError(start, "jsp.error.unterminated", + "<jsp:declaration>"); + } + } + } + + /* + * ExpressionBody ::= (Char* - (char* '%>')) '%>' + */ + private void parseExpression(Node parent) throws JasperException { + start = reader.mark(); + Mark stop = reader.skipUntil("%>"); + if (stop == null) { + err.jspError(start, "jsp.error.unterminated", "<%="); + } + + new Node.Expression(parseScriptText(reader.getText(start, stop)), + start, parent); + } + + /* + * XMLExpressionBody ::= ( S? '/>' ) | ( S? '>' (Char* - (char* '<')) + * CDSect?)* ETag ) | + */ + private void parseXMLExpression(Node parent) throws JasperException { + reader.skipSpaces(); + if (!reader.matches("/>")) { + if (!reader.matches(">")) { + err.jspError(start, "jsp.error.unterminated", + "<jsp:expression>"); + } + Mark stop; + String text; + while (true) { + start = reader.mark(); + stop = reader.skipUntil("<"); + if (stop == null) { + err.jspError(start, "jsp.error.unterminated", + "<jsp:expression>"); + } + text = parseScriptText(reader.getText(start, stop)); + new Node.Expression(text, start, parent); + if (reader.matches("![CDATA[")) { + start = reader.mark(); + stop = reader.skipUntil("]]>"); + if (stop == null) { + err.jspError(start, "jsp.error.unterminated", "CDATA"); + } + text = parseScriptText(reader.getText(start, stop)); + new Node.Expression(text, start, parent); + } else { + break; + } + } + if (!reader.matchesETagWithoutLessThan("jsp:expression")) { + err.jspError(start, "jsp.error.unterminated", + "<jsp:expression>"); + } + } + } + + /* + * ELExpressionBody (following "${" to first unquoted "}") // XXX add formal + * production and confirm implementation against it, // once it's decided + */ + private void parseELExpression(Node parent, char type) throws JasperException { + start = reader.mark(); + Mark last = null; + boolean singleQuoted = false, doubleQuoted = false; + int currentChar; + do { + // XXX could move this logic to JspReader + last = reader.mark(); // XXX somewhat wasteful + currentChar = reader.nextChar(); + if (currentChar == '\\' && (singleQuoted || doubleQuoted)) { + // skip character following '\' within quotes + reader.nextChar(); + currentChar = reader.nextChar(); + } + if (currentChar == -1) + err.jspError(start, "jsp.error.unterminated", type + "{"); + if (currentChar == '"' && !singleQuoted) + doubleQuoted = !doubleQuoted; + if (currentChar == '\'' && !doubleQuoted) + singleQuoted = !singleQuoted; + } while (currentChar != '}' || (singleQuoted || doubleQuoted)); + + new Node.ELExpression(type, reader.getText(start, last), start, parent); + } + + /* + * ScriptletBody ::= (Char* - (char* '%>')) '%>' + */ + private void parseScriptlet(Node parent) throws JasperException { + start = reader.mark(); + Mark stop = reader.skipUntil("%>"); + if (stop == null) { + err.jspError(start, "jsp.error.unterminated", "<%"); + } + + new Node.Scriptlet(parseScriptText(reader.getText(start, stop)), start, + parent); + } + + /* + * XMLScriptletBody ::= ( S? '/>' ) | ( S? '>' (Char* - (char* '<')) + * CDSect?)* ETag ) | + */ + private void parseXMLScriptlet(Node parent) throws JasperException { + reader.skipSpaces(); + if (!reader.matches("/>")) { + if (!reader.matches(">")) { + err.jspError(start, "jsp.error.unterminated", + "<jsp:scriptlet>"); + } + Mark stop; + String text; + while (true) { + start = reader.mark(); + stop = reader.skipUntil("<"); + if (stop == null) { + err.jspError(start, "jsp.error.unterminated", + "<jsp:scriptlet>"); + } + text = parseScriptText(reader.getText(start, stop)); + new Node.Scriptlet(text, start, parent); + if (reader.matches("![CDATA[")) { + start = reader.mark(); + stop = reader.skipUntil("]]>"); + if (stop == null) { + err.jspError(start, "jsp.error.unterminated", "CDATA"); + } + text = parseScriptText(reader.getText(start, stop)); + new Node.Scriptlet(text, start, parent); + } else { + break; + } + } + + if (!reader.matchesETagWithoutLessThan("jsp:scriptlet")) { + err.jspError(start, "jsp.error.unterminated", + "<jsp:scriptlet>"); + } + } + } + + /** + * Param ::= '' S? ( ' ) S? ETag ) | ( '>' S? Param* ETag ) + * + * EmptyBody ::= '/>' | ( '>' ETag ) | ( '>' S? '' Param* '' + */ + private void parseInclude(Node parent) throws JasperException { + Attributes attrs = parseAttributes(); + reader.skipSpaces(); + + Node includeNode = new Node.IncludeAction(attrs, start, parent); + + parseOptionalBody(includeNode, "jsp:include", JAVAX_BODY_CONTENT_PARAM); + } + + /* + * For Forward: StdActionContent ::= Attributes ParamBody + */ + private void parseForward(Node parent) throws JasperException { + Attributes attrs = parseAttributes(); + reader.skipSpaces(); + + Node forwardNode = new Node.ForwardAction(attrs, start, parent); + + parseOptionalBody(forwardNode, "jsp:forward", JAVAX_BODY_CONTENT_PARAM); + } + + private void parseInvoke(Node parent) throws JasperException { + Attributes attrs = parseAttributes(); + reader.skipSpaces(); + + Node invokeNode = new Node.InvokeAction(attrs, start, parent); + + parseEmptyBody(invokeNode, "jsp:invoke"); + } + + private void parseDoBody(Node parent) throws JasperException { + Attributes attrs = parseAttributes(); + reader.skipSpaces(); + + Node doBodyNode = new Node.DoBodyAction(attrs, start, parent); + + parseEmptyBody(doBodyNode, "jsp:doBody"); + } + + private void parseElement(Node parent) throws JasperException { + Attributes attrs = parseAttributes(); + reader.skipSpaces(); + + Node elementNode = new Node.JspElement(attrs, start, parent); + + parseOptionalBody(elementNode, "jsp:element", TagInfo.BODY_CONTENT_JSP); + } + + /* + * For GetProperty: StdActionContent ::= Attributes EmptyBody + */ + private void parseGetProperty(Node parent) throws JasperException { + Attributes attrs = parseAttributes(); + reader.skipSpaces(); + + Node getPropertyNode = new Node.GetProperty(attrs, start, parent); + + parseOptionalBody(getPropertyNode, "jsp:getProperty", + TagInfo.BODY_CONTENT_EMPTY); + } + + /* + * For SetProperty: StdActionContent ::= Attributes EmptyBody + */ + private void parseSetProperty(Node parent) throws JasperException { + Attributes attrs = parseAttributes(); + reader.skipSpaces(); + + Node setPropertyNode = new Node.SetProperty(attrs, start, parent); + + parseOptionalBody(setPropertyNode, "jsp:setProperty", + TagInfo.BODY_CONTENT_EMPTY); + } + + /* + * EmptyBody ::= '/>' | ( '>' ETag ) | ( '>' S? '")) { + // Done + } else if (reader.matches(">")) { + if (reader.matchesETag(tag)) { + // Done + } else if (reader.matchesOptionalSpacesFollowedBy("' ETag ) | ( '>' S? '' Body ETag ) + * + * ScriptlessActionBody ::= JspAttributeAndBody | ( '>' ScriptlessBody ETag ) + * + * TagDependentActionBody ::= JspAttributeAndBody | ( '>' TagDependentBody + * ETag ) + * + */ + private void parseOptionalBody(Node parent, String tag, String bodyType) + throws JasperException { + if (reader.matches("/>")) { + // EmptyBody + return; + } + + if (!reader.matches(">")) { + err.jspError(reader.mark(), "jsp.error.unterminated", "<" + tag); + } + + if (reader.matchesETag(tag)) { + // EmptyBody + return; + } + + if (!parseJspAttributeAndBody(parent, tag, bodyType)) { + // Must be ( '>' # Body ETag ) + parseBody(parent, tag, bodyType); + } + } + + /** + * Attempts to parse 'JspAttributeAndBody' production. Returns true if it + * matched, or false if not. Assumes EmptyBody is okay as well. + * + * JspAttributeAndBody ::= ( '>' # S? ( ' ) S? ETag ) + */ + private boolean parseJspAttributeAndBody(Node parent, String tag, + String bodyType) throws JasperException { + boolean result = false; + + if (reader.matchesOptionalSpacesFollowedBy(" elements: + parseNamedAttributes(parent); + + result = true; + } + + if (reader.matchesOptionalSpacesFollowedBy(" but something other than + // or the end tag, translation error. + err.jspError(reader.mark(), "jsp.error.jspbody.required", "<" + + tag); + } + + return result; + } + + /* + * Params ::= `>' S? ( ( `' ( ( S? Param+ S? `' ) | + * ) ) | Param+ ) '' + */ + private void parseJspParams(Node parent) throws JasperException { + Node jspParamsNode = new Node.ParamsAction(start, parent); + parseOptionalBody(jspParamsNode, "jsp:params", JAVAX_BODY_CONTENT_PARAM); + } + + /* + * Fallback ::= '/>' | ( `>' S? `' ( ( S? ( Char* - ( Char* `' ) ) `' + * S? ) | ) `' ) | ( '>' ( Char* - ( + * Char* '' ) ) '' ) + */ + private void parseFallBack(Node parent) throws JasperException { + Node fallBackNode = new Node.FallBackAction(start, parent); + parseOptionalBody(fallBackNode, "jsp:fallback", + JAVAX_BODY_CONTENT_TEMPLATE_TEXT); + } + + /* + * For Plugin: StdActionContent ::= Attributes PluginBody + * + * PluginBody ::= EmptyBody | ( '>' S? ( ' ) S? ETag ) | ( '>' S? PluginTags + * ETag ) + * + * EmptyBody ::= '/>' | ( '>' ETag ) | ( '>' S? ' + * + * Attributes ::= ( S Attribute )* S? + * + * CustomActionEnd ::= CustomActionTagDependent | CustomActionJSPContent | + * CustomActionScriptlessContent + * + * CustomActionTagDependent ::= TagDependentOptionalBody + * + * CustomActionJSPContent ::= OptionalBody + * + * CustomActionScriptlessContent ::= ScriptlessOptionalBody + */ + private boolean parseCustomTag(Node parent) throws JasperException { + + if (reader.peekChar() != '<') { + return false; + } + + // Parse 'CustomAction' production (tag prefix and custom action name) + reader.nextChar(); // skip '<' + String tagName = reader.parseToken(false); + int i = tagName.indexOf(':'); + if (i == -1) { + reader.reset(start); + return false; + } + + String prefix = tagName.substring(0, i); + String shortTagName = tagName.substring(i + 1); + + // Check if this is a user-defined tag. + String uri = pageInfo.getURI(prefix); + if (uri == null) { + reader.reset(start); + // Remember the prefix for later error checking + pageInfo.putNonCustomTagPrefix(prefix, reader.mark()); + return false; + } + + TagLibraryInfo tagLibInfo = pageInfo.getTaglib(uri); + TagInfo tagInfo = tagLibInfo.getTag(shortTagName); + TagFileInfo tagFileInfo = tagLibInfo.getTagFile(shortTagName); + if (tagInfo == null && tagFileInfo == null) { + err.jspError(start, "jsp.error.bad_tag", shortTagName, prefix); + } + Class tagHandlerClass = null; + if (tagInfo != null) { + // Must be a classic tag, load it here. + // tag files will be loaded later, in TagFileProcessor + String handlerClassName = tagInfo.getTagClassName(); + try { + tagHandlerClass = ctxt.getClassLoader().loadClass( + handlerClassName); + } catch (Exception e) { + err.jspError(start, "jsp.error.loadclass.taghandler", + handlerClassName, tagName); + } + } + + // Parse 'CustomActionBody' production: + // At this point we are committed - if anything fails, we produce + // a translation error. + + // Parse 'Attributes' production: + Attributes attrs = parseAttributes(); + reader.skipSpaces(); + + // Parse 'CustomActionEnd' production: + if (reader.matches("/>")) { + if (tagInfo != null) { + new Node.CustomTag(tagName, prefix, shortTagName, uri, attrs, + start, parent, tagInfo, tagHandlerClass); + } else { + new Node.CustomTag(tagName, prefix, shortTagName, uri, attrs, + start, parent, tagFileInfo); + } + return true; + } + + // Now we parse one of 'CustomActionTagDependent', + // 'CustomActionJSPContent', or 'CustomActionScriptlessContent'. + // depending on body-content in TLD. + + // Looking for a body, it still can be empty; but if there is a + // a tag body, its syntax would be dependent on the type of + // body content declared in the TLD. + String bc; + if (tagInfo != null) { + bc = tagInfo.getBodyContent(); + } else { + bc = tagFileInfo.getTagInfo().getBodyContent(); + } + + Node tagNode = null; + if (tagInfo != null) { + tagNode = new Node.CustomTag(tagName, prefix, shortTagName, uri, + attrs, start, parent, tagInfo, tagHandlerClass); + } else { + tagNode = new Node.CustomTag(tagName, prefix, shortTagName, uri, + attrs, start, parent, tagFileInfo); + } + + parseOptionalBody(tagNode, tagName, bc); + + return true; + } + + /* + * Parse for a template text string until '<' or "${" or "#{" is encountered, + * recognizing escape sequences "\%", "\$", and "\#". + */ + private void parseTemplateText(Node parent) throws JasperException { + + if (!reader.hasMoreInput()) + return; + + CharArrayWriter ttext = new CharArrayWriter(); + // Output the first character + int ch = reader.nextChar(); + if (ch == '\\') { + reader.pushChar(); + } else { + ttext.write(ch); + } + + while (reader.hasMoreInput()) { + ch = reader.nextChar(); + if (ch == '<') { + reader.pushChar(); + break; + } else if ((ch == '$' || ch == '#') && !pageInfo.isELIgnored()) { + if (!reader.hasMoreInput()) { + ttext.write(ch); + break; + } + if (reader.nextChar() == '{') { + reader.pushChar(); + reader.pushChar(); + break; + } + ttext.write(ch); + reader.pushChar(); + continue; + } else if (ch == '\\') { + if (!reader.hasMoreInput()) { + ttext.write('\\'); + break; + } + char next = (char) reader.peekChar(); + // Looking for \% or \$ or \# + if (next == '%' || ((next == '$' || next == '#') && + !pageInfo.isELIgnored())) { + ch = reader.nextChar(); + } + } + ttext.write(ch); + } + new Node.TemplateText(ttext.toString(), start, parent); + } + + /* + * XMLTemplateText ::= ( S? '/>' ) | ( S? '>' ( ( Char* - ( Char* ( '<' | + * '${' ) ) ) ( '${' ELExpressionBody )? CDSect? )* ETag ) | + * + */ + private void parseXMLTemplateText(Node parent) throws JasperException { + reader.skipSpaces(); + if (!reader.matches("/>")) { + if (!reader.matches(">")) { + err.jspError(start, "jsp.error.unterminated", + "<jsp:text>"); + } + CharArrayWriter ttext = new CharArrayWriter(); + while (reader.hasMoreInput()) { + int ch = reader.nextChar(); + if (ch == '<') { + // Check for "); + if (stop == null) { + err.jspError(start, "jsp.error.unterminated", "CDATA"); + } + String text = reader.getText(start, stop); + ttext.write(text, 0, text.length()); + } else if (ch == '\\') { + if (!reader.hasMoreInput()) { + ttext.write('\\'); + break; + } + ch = reader.nextChar(); + if (ch != '$' && ch != '#') { + ttext.write('\\'); + } + ttext.write(ch); + } else if (ch == '$' || ch == '#') { + if (!reader.hasMoreInput()) { + ttext.write(ch); + break; + } + if (reader.nextChar() != '{') { + ttext.write(ch); + reader.pushChar(); + continue; + } + // Create a template text node + new Node.TemplateText(ttext.toString(), start, parent); + + // Mark and parse the EL expression and create its node: + start = reader.mark(); + parseELExpression(parent, (char) ch); + + start = reader.mark(); + ttext = new CharArrayWriter(); + } else { + ttext.write(ch); + } + } + + new Node.TemplateText(ttext.toString(), start, parent); + + if (!reader.hasMoreInput()) { + err.jspError(start, "jsp.error.unterminated", + "<jsp:text>"); + } else if (!reader.matchesETagWithoutLessThan("jsp:text")) { + err.jspError(start, "jsp.error.jsptext.badcontent"); + } + } + } + + /* + * AllBody ::= ( '<%--' JSPCommentBody ) | ( '<%@' DirectiveBody ) | ( ' 0) { + // vc: ScriptlessBody + // We must follow the ScriptlessBody production if one of + // our parents is ScriptlessBody. + parseElementsScriptless(parent); + return; + } + + start = reader.mark(); + if (reader.matches("<%--")) { + parseComment(parent); + } else if (reader.matches("<%@")) { + parseDirective(parent); + } else if (reader.matches(" ) | ( ' ) | ( '<%=' ) | ( ' ) | ( '<%' ) | ( ' ) | ( ' ) | ( ' ) | ( '<%=' ) | ( ' ) | ( '<%' ) | ( ' ) | ( ' ) | ( '${' + * ) | ( ' ) | TemplateText + */ + private void parseElementsTemplateText(Node parent) throws JasperException { + start = reader.mark(); + if (reader.matches("<%--")) { + parseComment(parent); + } else if (reader.matches("<%@")) { + parseDirective(parent); + } else if (reader.matches("")) { + if (!reader.matches(">")) { + err.jspError(start, "jsp.error.unterminated", "<jsp:body"); + } + parseBody(bodyNode, "jsp:body", bodyType); + } + } + + /* + * Parse the body as JSP content. @param tag The name of the tag whose end + * tag would terminate the body @param bodyType One of the TagInfo body + * types + */ + private void parseBody(Node parent, String tag, String bodyType) + throws JasperException { + if (bodyType.equalsIgnoreCase(TagInfo.BODY_CONTENT_TAG_DEPENDENT)) { + parseTagDependentBody(parent, tag); + } else if (bodyType.equalsIgnoreCase(TagInfo.BODY_CONTENT_EMPTY)) { + if (!reader.matchesETag(tag)) { + err.jspError(start, "jasper.error.emptybodycontent.nonempty", + tag); + } + } else if (bodyType == JAVAX_BODY_CONTENT_PLUGIN) { + // (note the == since we won't recognize JAVAX_* + // from outside this module). + parsePluginTags(parent); + if (!reader.matchesETag(tag)) { + err.jspError(reader.mark(), "jsp.error.unterminated", "<" + + tag); + } + } else if (bodyType.equalsIgnoreCase(TagInfo.BODY_CONTENT_JSP) + || bodyType.equalsIgnoreCase(TagInfo.BODY_CONTENT_SCRIPTLESS) + || (bodyType == JAVAX_BODY_CONTENT_PARAM) + || (bodyType == JAVAX_BODY_CONTENT_TEMPLATE_TEXT)) { + while (reader.hasMoreInput()) { + if (reader.matchesETag(tag)) { + return; + } + + // Check for nested jsp:body or jsp:attribute + if (tag.equals("jsp:body") || tag.equals("jsp:attribute")) { + if (reader.matches("")) { + if (!reader.matches(">")) { + err.jspError(start, "jsp.error.unterminated", + "<jsp:attribute"); + } + if (namedAttributeNode.isTrim()) { + reader.skipSpaces(); + } + parseBody(namedAttributeNode, "jsp:attribute", + getAttributeBodyType(parent, attrs.getValue("name"))); + if (namedAttributeNode.isTrim()) { + Node.Nodes subElems = namedAttributeNode.getBody(); + if (subElems != null) { + Node lastNode = subElems.getNode(subElems.size() - 1); + if (lastNode instanceof Node.TemplateText) { + ((Node.TemplateText) lastNode).rtrim(); + } + } + } + } + reader.skipSpaces(); + } while (reader.matches(" from the enclosing node + */ + private String getAttributeBodyType(Node n, String name) { + + if (n instanceof Node.CustomTag) { + TagInfo tagInfo = ((Node.CustomTag) n).getTagInfo(); + TagAttributeInfo[] tldAttrs = tagInfo.getAttributes(); + for (int i = 0; i < tldAttrs.length; i++) { + if (name.equals(tldAttrs[i].getName())) { + if (tldAttrs[i].isFragment()) { + return TagInfo.BODY_CONTENT_SCRIPTLESS; + } + if (tldAttrs[i].canBeRequestTime()) { + return TagInfo.BODY_CONTENT_JSP; + } + } + } + if (tagInfo.hasDynamicAttributes()) { + return TagInfo.BODY_CONTENT_JSP; + } + } else if (n instanceof Node.IncludeAction) { + if ("page".equals(name)) { + return TagInfo.BODY_CONTENT_JSP; + } + } else if (n instanceof Node.ForwardAction) { + if ("page".equals(name)) { + return TagInfo.BODY_CONTENT_JSP; + } + } else if (n instanceof Node.SetProperty) { + if ("value".equals(name)) { + return TagInfo.BODY_CONTENT_JSP; + } + } else if (n instanceof Node.UseBean) { + if ("beanName".equals(name)) { + return TagInfo.BODY_CONTENT_JSP; + } + } else if (n instanceof Node.PlugIn) { + if ("width".equals(name) || "height".equals(name)) { + return TagInfo.BODY_CONTENT_JSP; + } + } else if (n instanceof Node.ParamAction) { + if ("value".equals(name)) { + return TagInfo.BODY_CONTENT_JSP; + } + } else if (n instanceof Node.JspElement) { + return TagInfo.BODY_CONTENT_JSP; + } + + return JAVAX_BODY_CONTENT_TEMPLATE_TEXT; + } + + private void parseTagFileDirectives(Node parent) throws JasperException { + reader.setSingleFile(true); + reader.skipUntil("<"); + while (reader.hasMoreInput()) { + start = reader.mark(); + if (reader.matches("%--")) { + parseComment(parent); + } else if (reader.matches("%@")) { + parseDirective(parent); + } else if (reader.matches("jsp:directive.")) { + parseXMLDirective(parent); + } + reader.skipUntil("<"); + } + } +}