View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    * 
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   * 
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.struts2.jasper.compiler;
18  
19  import org.apache.struts2.jasper.Constants;
20  import org.apache.struts2.jasper.JasperException;
21  import org.apache.struts2.jasper.JspCompilationContext;
22  import org.xml.sax.Attributes;
23  import org.xml.sax.helpers.AttributesImpl;
24  
25  import javax.servlet.jsp.tagext.TagAttributeInfo;
26  import javax.servlet.jsp.tagext.TagFileInfo;
27  import javax.servlet.jsp.tagext.TagInfo;
28  import javax.servlet.jsp.tagext.TagLibraryInfo;
29  import java.io.CharArrayWriter;
30  import java.io.FileNotFoundException;
31  import java.net.URL;
32  import java.util.Iterator;
33  import java.util.List;
34  
35  /***
36   * This class implements a parser for a JSP page (non-xml view).
37   * JSP page grammar is included here for reference.  The token '#'
38   * that appears in the production indicates the current input token
39   * location in the production.
40   *
41   * @author Kin-man Chung
42   * @author Shawn Bayern
43   * @author Mark Roth
44   */
45  
46  class Parser implements TagConstants {
47  
48      private ParserController parserController;
49      private JspCompilationContext ctxt;
50      private JspReader reader;
51      private String currentFile;
52      private Mark start;
53      private ErrorDispatcher err;
54      private int scriptlessCount;
55      private boolean isTagFile;
56      private boolean directivesOnly;
57      private URL jarFileUrl;
58      private PageInfo pageInfo;
59  
60      // Virtual body content types, to make parsing a little easier.
61      // These are not accessible from outside the parser.
62      private static final String JAVAX_BODY_CONTENT_PARAM =
63              "JAVAX_BODY_CONTENT_PARAM";
64      private static final String JAVAX_BODY_CONTENT_PLUGIN =
65              "JAVAX_BODY_CONTENT_PLUGIN";
66      private static final String JAVAX_BODY_CONTENT_TEMPLATE_TEXT =
67              "JAVAX_BODY_CONTENT_TEMPLATE_TEXT";
68  
69      private static final boolean STRICT_QUOTE_ESCAPING = Boolean.valueOf(
70              System.getProperty(
71                      "org.apache.struts2.jasper.compiler.Parser.STRICT_QUOTE_ESCAPING",
72                      "true")).booleanValue();
73  
74      /***
75       * The constructor
76       */
77      private Parser(ParserController pc, JspReader reader, boolean isTagFile,
78                     boolean directivesOnly, URL jarFileUrl) {
79          this.parserController = pc;
80          this.ctxt = pc.getJspCompilationContext();
81          this.pageInfo = pc.getCompiler().getPageInfo();
82          this.err = pc.getCompiler().getErrorDispatcher();
83          this.reader = reader;
84          this.currentFile = reader.mark().getFile();
85          this.scriptlessCount = 0;
86          this.isTagFile = isTagFile;
87          this.directivesOnly = directivesOnly;
88          this.jarFileUrl = jarFileUrl;
89          start = reader.mark();
90      }
91  
92      /***
93       * The main entry for Parser
94       *
95       * @param pc     The ParseController, use for getting other objects in compiler
96       *               and for parsing included pages
97       * @param reader To read the page
98       * @param parent The parent node to this page, null for top level page
99       * @return list of nodes representing the parsed page
100      */
101     public static Node.Nodes parse(ParserController pc,
102                                    JspReader reader,
103                                    Node parent,
104                                    boolean isTagFile,
105                                    boolean directivesOnly,
106                                    URL jarFileUrl,
107                                    String pageEnc,
108                                    String jspConfigPageEnc,
109                                    boolean isDefaultPageEncoding)
110             throws JasperException {
111 
112         Parser parser = new Parser(pc, reader, isTagFile, directivesOnly,
113                 jarFileUrl);
114 
115         Node.Root root = new Node.Root(reader.mark(), parent, false);
116         root.setPageEncoding(pageEnc);
117         root.setJspConfigPageEncoding(jspConfigPageEnc);
118         root.setIsDefaultPageEncoding(isDefaultPageEncoding);
119 
120         if (directivesOnly) {
121             parser.parseTagFileDirectives(root);
122             return new Node.Nodes(root);
123         }
124 
125         // For the Top level page, add inlcude-prelude and include-coda
126         PageInfo pageInfo = pc.getCompiler().getPageInfo();
127         if (parent == null) {
128             parser.addInclude(root, pageInfo.getIncludePrelude());
129         }
130         while (reader.hasMoreInput()) {
131             parser.parseElements(root);
132         }
133         if (parent == null) {
134             parser.addInclude(root, pageInfo.getIncludeCoda());
135         }
136 
137         Node.Nodes page = new Node.Nodes(root);
138         return page;
139     }
140 
141     /***
142      * Attributes ::= (S Attribute)* S?
143      */
144     Attributes parseAttributes() throws JasperException {
145         AttributesImpl attrs = new AttributesImpl();
146 
147         reader.skipSpaces();
148         while (parseAttribute(attrs))
149             reader.skipSpaces();
150 
151         return attrs;
152     }
153 
154     /***
155      * Parse Attributes for a reader, provided for external use
156      */
157     public static Attributes parseAttributes(ParserController pc,
158                                              JspReader reader)
159             throws JasperException {
160         Parser tmpParser = new Parser(pc, reader, false, false, null);
161         return tmpParser.parseAttributes();
162     }
163 
164     /***
165      * Attribute ::= Name S? Eq S?
166      * (   '"<%=' RTAttributeValueDouble
167      * | '"' AttributeValueDouble
168      * | "'<%=" RTAttributeValueSingle
169      * | "'" AttributeValueSingle
170      * }
171      * Note: JSP and XML spec does not allow while spaces around Eq.  It is
172      * added to be backward compatible with Tomcat, and with other xml parsers.
173      */
174     private boolean parseAttribute(AttributesImpl attrs)
175             throws JasperException {
176 
177         // Get the qualified name
178         String qName = parseName();
179         if (qName == null)
180             return false;
181 
182         // Determine prefix and local name components
183         String localName = qName;
184         String uri = "";
185         int index = qName.indexOf(':');
186         if (index != -1) {
187             String prefix = qName.substring(0, index);
188             uri = pageInfo.getURI(prefix);
189             if (uri == null) {
190                 err.jspError(reader.mark(),
191                         "jsp.error.attribute.invalidPrefix", prefix);
192             }
193             localName = qName.substring(index + 1);
194         }
195 
196         reader.skipSpaces();
197         if (!reader.matches("="))
198             err.jspError(reader.mark(), "jsp.error.attribute.noequal");
199 
200         reader.skipSpaces();
201         char quote = (char) reader.nextChar();
202         if (quote != '\'' && quote != '"')
203             err.jspError(reader.mark(), "jsp.error.attribute.noquote");
204 
205         String watchString = "";
206         if (reader.matches("<%="))
207             watchString = "%>";
208         watchString = watchString + quote;
209 
210         String attrValue = parseAttributeValue(watchString);
211         attrs.addAttribute(uri, localName, qName, "CDATA", attrValue);
212         return true;
213     }
214 
215     /***
216      * Name ::= (Letter | '_' | ':') (Letter | Digit | '.' | '_' | '-' | ':')*
217      */
218     private String parseName() throws JasperException {
219         char ch = (char) reader.peekChar();
220         if (Character.isLetter(ch) || ch == '_' || ch == ':') {
221             StringBuffer buf = new StringBuffer();
222             buf.append(ch);
223             reader.nextChar();
224             ch = (char) reader.peekChar();
225             while (Character.isLetter(ch) || Character.isDigit(ch) ||
226                     ch == '.' || ch == '_' || ch == '-' || ch == ':') {
227                 buf.append(ch);
228                 reader.nextChar();
229                 ch = (char) reader.peekChar();
230             }
231             return buf.toString();
232         }
233         return null;
234     }
235 
236     /***
237      * AttributeValueDouble ::= (QuotedChar - '"')*
238      * ('"' | <TRANSLATION_ERROR>)
239      * RTAttributeValueDouble ::= ((QuotedChar - '"')* - ((QuotedChar-'"')'%>"')
240      * ('%>"' | TRANSLATION_ERROR)
241      */
242     private String parseAttributeValue(String watch) throws JasperException {
243         Mark start = reader.mark();
244         Mark stop = reader.skipUntilIgnoreEsc(watch);
245         if (stop == null) {
246             err.jspError(start, "jsp.error.attribute.unterminated", watch);
247         }
248 
249         String ret = parseQuoted(start, reader.getText(start, stop),
250                 watch.charAt(watch.length() - 1));
251         if (watch.length() == 1)        // quote
252             return ret;
253 
254         // putback delimiter '<%=' and '%>', since they are needed if the
255         // attribute does not allow RTexpression.
256         return "<%=" + ret + "%>";
257     }
258 
259     /***
260      * QuotedChar ::=   '&apos;'
261      * | '&quot;'
262      * | '//'
263      * | '\"'
264      * | "\'"
265      * | '\>'
266      * | '\$'
267      * | Char
268      */
269     private String parseQuoted(Mark start, String tx, char quote)
270             throws JasperException {
271         StringBuffer buf = new StringBuffer();
272         int size = tx.length();
273         int i = 0;
274         while (i < size) {
275             char ch = tx.charAt(i);
276             if (ch == '&') {
277                 if (i + 5 < size && tx.charAt(i + 1) == 'a'
278                         && tx.charAt(i + 2) == 'p' && tx.charAt(i + 3) == 'o'
279                         && tx.charAt(i + 4) == 's' && tx.charAt(i + 5) == ';') {
280                     buf.append('\'');
281                     i += 6;
282                 } else if (i + 5 < size && tx.charAt(i + 1) == 'q'
283                         && tx.charAt(i + 2) == 'u' && tx.charAt(i + 3) == 'o'
284                         && tx.charAt(i + 4) == 't' && tx.charAt(i + 5) == ';') {
285                     buf.append('"');
286                     i += 6;
287                 } else {
288                     buf.append(ch);
289                     ++i;
290                 }
291             } else if (ch == '//' && i + 1 < size) {
292                 ch = tx.charAt(i + 1);
293                 if (ch == '//' || ch == '\"' || ch == '\'' || ch == '>') {
294                     buf.append(ch);
295                     i += 2;
296                 } else if (ch == '$') {
297                     // Replace "\$" with some special char.  XXX hack!
298                     buf.append(Constants.HACK_CHAR);
299                     i += 2;
300                 } else {
301                     buf.append('//');
302                     ++i;
303                 }
304             } else if (ch == quote && STRICT_QUOTE_ESCAPING) {
305                 // Unescaped quote character
306                 err.jspError(start, "jsp.error.attribute.noescape", tx,
307                         "" + quote);
308             } else {
309                 buf.append(ch);
310                 ++i;
311             }
312         }
313         return buf.toString();
314     }
315 
316     private String parseScriptText(String tx) {
317         CharArrayWriter cw = new CharArrayWriter();
318         int size = tx.length();
319         int i = 0;
320         while (i < size) {
321             char ch = tx.charAt(i);
322             if (i + 2 < size && ch == '%' && tx.charAt(i + 1) == '//'
323                     && tx.charAt(i + 2) == '>') {
324                 cw.write('%');
325                 cw.write('>');
326                 i += 3;
327             } else {
328                 cw.write(ch);
329                 ++i;
330             }
331         }
332         cw.close();
333         return cw.toString();
334     }
335 
336     /*
337      * Invokes parserController to parse the included page
338      */
339     private void processIncludeDirective(String file, Node parent)
340             throws JasperException {
341         if (file == null) {
342             return;
343         }
344 
345         try {
346             parserController.parse(file, parent, jarFileUrl);
347         } catch (FileNotFoundException ex) {
348             err.jspError(start, "jsp.error.file.not.found", file);
349         } catch (Exception ex) {
350             err.jspError(start, ex.getMessage());
351         }
352     }
353 
354     /*
355      * Parses a page directive with the following syntax:
356      *   PageDirective ::= ( S Attribute)*
357      */
358     private void parsePageDirective(Node parent) throws JasperException {
359         Attributes attrs = parseAttributes();
360         Node.PageDirective n = new Node.PageDirective(attrs, start, parent);
361 
362         /*
363          * A page directive may contain multiple 'import' attributes, each of
364          * which consists of a comma-separated list of package names.
365          * Store each list with the node, where it is parsed.
366          */
367         for (int i = 0; i < attrs.getLength(); i++) {
368             if ("import".equals(attrs.getQName(i))) {
369                 n.addImport(attrs.getValue(i));
370             }
371         }
372     }
373 
374     /*
375      * Parses an include directive with the following syntax:
376      *   IncludeDirective ::= ( S Attribute)*
377      */
378     private void parseIncludeDirective(Node parent) throws JasperException {
379         Attributes attrs = parseAttributes();
380 
381         // Included file expanded here
382         Node includeNode = new Node.IncludeDirective(attrs, start, parent);
383         processIncludeDirective(attrs.getValue("file"), includeNode);
384     }
385 
386     /***
387      * Add a list of files.  This is used for implementing include-prelude
388      * and include-coda of jsp-config element in web.xml
389      */
390     private void addInclude(Node parent, List files) throws JasperException {
391         if (files != null) {
392             Iterator iter = files.iterator();
393             while (iter.hasNext()) {
394                 String file = (String) iter.next();
395                 AttributesImpl attrs = new AttributesImpl();
396                 attrs.addAttribute("", "file", "file", "CDATA", file);
397 
398                 // Create a dummy Include directive node
399                 Node includeNode = new Node.IncludeDirective(attrs,
400                         reader.mark(), parent);
401                 processIncludeDirective(file, includeNode);
402             }
403         }
404     }
405 
406     /*
407      * Parses a taglib directive with the following syntax:
408      *   Directive ::= ( S Attribute)*
409      */
410     private void parseTaglibDirective(Node parent) throws JasperException {
411 
412         Attributes attrs = parseAttributes();
413         String uri = attrs.getValue("uri");
414         String prefix = attrs.getValue("prefix");
415         if (prefix != null) {
416             Mark prevMark = pageInfo.getNonCustomTagPrefix(prefix);
417             if (prevMark != null) {
418                 err.jspError(reader.mark(), "jsp.error.prefix.use_before_dcl",
419                         prefix, prevMark.getFile(), "" + prevMark.getLineNumber());
420             }
421             if (uri != null) {
422                 String uriPrev = pageInfo.getURI(prefix);
423                 if (uriPrev != null && !uriPrev.equals(uri)) {
424                     err.jspError(reader.mark(), "jsp.error.prefix.refined",
425                             prefix, uri, uriPrev);
426                 }
427                 if (pageInfo.getTaglib(uri) == null) {
428                     TagLibraryInfoImpl impl = null;
429                     if (ctxt.getOptions().isCaching()) {
430                         impl = (TagLibraryInfoImpl) ctxt.getOptions().getCache().get(uri);
431                     }
432                     if (impl == null) {
433                         String[] location = ctxt.getTldLocation(uri);
434                         impl = new TagLibraryInfoImpl(ctxt,
435                                 parserController,
436                                 prefix,
437                                 uri,
438                                 location,
439                                 err);
440                         if (ctxt.getOptions().isCaching()) {
441                             ctxt.getOptions().getCache().put(uri, impl);
442                         }
443                     }
444                     pageInfo.addTaglib(uri, impl);
445                 }
446                 pageInfo.addPrefixMapping(prefix, uri);
447             } else {
448                 String tagdir = attrs.getValue("tagdir");
449                 if (tagdir != null) {
450                     String urnTagdir = URN_JSPTAGDIR + tagdir;
451                     if (pageInfo.getTaglib(urnTagdir) == null) {
452                         pageInfo.addTaglib(urnTagdir,
453                                 new ImplicitTagLibraryInfo(
454                                         ctxt,
455                                         parserController,
456                                         prefix,
457                                         tagdir,
458                                         err));
459                     }
460                     pageInfo.addPrefixMapping(prefix, urnTagdir);
461                 }
462             }
463         }
464 
465         new Node.TaglibDirective(attrs, start, parent);
466     }
467 
468     /*
469      * Parses a directive with the following syntax:
470      *   Directive ::= S? (   'page' PageDirective
471      *                            | 'include' IncludeDirective
472      *                            | 'taglib' TagLibDirective)
473      *                       S? '%>'
474      *
475      *   TagDirective ::= S? ('tag' PageDirective
476      *                            | 'include' IncludeDirective
477      *                            | 'taglib' TagLibDirective)
478      *                      | 'attribute AttributeDirective
479      *                      | 'variable VariableDirective
480      *                       S? '%>'
481      */
482     private void parseDirective(Node parent) throws JasperException {
483         reader.skipSpaces();
484 
485         String directive = null;
486         if (reader.matches("page")) {
487             directive = "&lt;%@ page";
488             if (isTagFile) {
489                 err.jspError(reader.mark(), "jsp.error.directive.istagfile",
490                         directive);
491             }
492             parsePageDirective(parent);
493         } else if (reader.matches("include")) {
494             directive = "&lt;%@ include";
495             parseIncludeDirective(parent);
496         } else if (reader.matches("taglib")) {
497             if (directivesOnly) {
498                 // No need to get the tagLibInfo objects.  This alos suppresses
499                 // parsing of any tag files used in this tag file.
500                 return;
501             }
502             directive = "&lt;%@ taglib";
503             parseTaglibDirective(parent);
504         } else if (reader.matches("tag")) {
505             directive = "&lt;%@ tag";
506             if (!isTagFile) {
507                 err.jspError(reader.mark(), "jsp.error.directive.isnottagfile",
508                         directive);
509             }
510             parseTagDirective(parent);
511         } else if (reader.matches("attribute")) {
512             directive = "&lt;%@ attribute";
513             if (!isTagFile) {
514                 err.jspError(reader.mark(), "jsp.error.directive.isnottagfile",
515                         directive);
516             }
517             parseAttributeDirective(parent);
518         } else if (reader.matches("variable")) {
519             directive = "&lt;%@ variable";
520             if (!isTagFile) {
521                 err.jspError(reader.mark(), "jsp.error.directive.isnottagfile",
522                         directive);
523             }
524             parseVariableDirective(parent);
525         } else {
526             err.jspError(reader.mark(), "jsp.error.invalid.directive");
527         }
528 
529         reader.skipSpaces();
530         if (!reader.matches("%>")) {
531             err.jspError(start, "jsp.error.unterminated", directive);
532         }
533     }
534 
535     /*
536     * Parses a directive with the following syntax:
537     *
538     *   XMLJSPDirectiveBody ::= S? (   ( 'page' PageDirectiveAttrList
539     *                                    S? ( '/>' | ( '>' S? ETag ) )
540     *                               | ( 'include' IncludeDirectiveAttrList
541     *                                    S? ( '/>' | ( '>' S? ETag ) )
542     *                           | <TRANSLATION_ERROR>
543     *
544     *   XMLTagDefDirectiveBody ::= (   ( 'tag' TagDirectiveAttrList
545     *                                    S? ( '/>' | ( '>' S? ETag ) )
546     *                                | ( 'include' IncludeDirectiveAttrList
547     *                                    S? ( '/>' | ( '>' S? ETag ) )
548     *                                | ( 'attribute' AttributeDirectiveAttrList
549     *                                    S? ( '/>' | ( '>' S? ETag ) )
550     *                                | ( 'variable' VariableDirectiveAttrList
551     *                                    S? ( '/>' | ( '>' S? ETag ) )
552     *                              )
553     *                            | <TRANSLATION_ERROR>
554     */
555     private void parseXMLDirective(Node parent) throws JasperException {
556         reader.skipSpaces();
557 
558         String eTag = null;
559         if (reader.matches("page")) {
560             eTag = "jsp:directive.page";
561             if (isTagFile) {
562                 err.jspError(reader.mark(), "jsp.error.directive.istagfile",
563                         "&lt;" + eTag);
564             }
565             parsePageDirective(parent);
566         } else if (reader.matches("include")) {
567             eTag = "jsp:directive.include";
568             parseIncludeDirective(parent);
569         } else if (reader.matches("tag")) {
570             eTag = "jsp:directive.tag";
571             if (!isTagFile) {
572                 err.jspError(reader.mark(), "jsp.error.directive.isnottagfile",
573                         "&lt;" + eTag);
574             }
575             parseTagDirective(parent);
576         } else if (reader.matches("attribute")) {
577             eTag = "jsp:directive.attribute";
578             if (!isTagFile) {
579                 err.jspError(reader.mark(), "jsp.error.directive.isnottagfile",
580                         "&lt;" + eTag);
581             }
582             parseAttributeDirective(parent);
583         } else if (reader.matches("variable")) {
584             eTag = "jsp:directive.variable";
585             if (!isTagFile) {
586                 err.jspError(reader.mark(), "jsp.error.directive.isnottagfile",
587                         "&lt;" + eTag);
588             }
589             parseVariableDirective(parent);
590         } else {
591             err.jspError(reader.mark(), "jsp.error.invalid.directive");
592         }
593 
594         reader.skipSpaces();
595         if (reader.matches(">")) {
596             reader.skipSpaces();
597             if (!reader.matchesETag(eTag)) {
598                 err.jspError(start, "jsp.error.unterminated", "&lt;" + eTag);
599             }
600         } else if (!reader.matches("/>")) {
601             err.jspError(start, "jsp.error.unterminated", "&lt;" + eTag);
602         }
603     }
604 
605     /*
606      * Parses a tag directive with the following syntax:
607      *   PageDirective ::= ( S Attribute)*
608      */
609     private void parseTagDirective(Node parent) throws JasperException {
610         Attributes attrs = parseAttributes();
611         Node.TagDirective n = new Node.TagDirective(attrs, start, parent);
612 
613         /*
614          * A page directive may contain multiple 'import' attributes, each of
615          * which consists of a comma-separated list of package names.
616          * Store each list with the node, where it is parsed.
617          */
618         for (int i = 0; i < attrs.getLength(); i++) {
619             if ("import".equals(attrs.getQName(i))) {
620                 n.addImport(attrs.getValue(i));
621             }
622         }
623     }
624 
625     /*
626      * Parses a attribute directive with the following syntax:
627      *   AttributeDirective ::= ( S Attribute)*
628      */
629     private void parseAttributeDirective(Node parent) throws JasperException {
630         Attributes attrs = parseAttributes();
631         Node.AttributeDirective n =
632                 new Node.AttributeDirective(attrs, start, parent);
633     }
634 
635     /*
636      * Parses a variable directive with the following syntax:
637      *   PageDirective ::= ( S Attribute)*
638      */
639     private void parseVariableDirective(Node parent) throws JasperException {
640         Attributes attrs = parseAttributes();
641         Node.VariableDirective n =
642                 new Node.VariableDirective(attrs, start, parent);
643     }
644 
645     /*
646      * JSPCommentBody ::= (Char* - (Char* '--%>')) '--%>'
647      */
648     private void parseComment(Node parent) throws JasperException {
649         start = reader.mark();
650         Mark stop = reader.skipUntil("--%>");
651         if (stop == null) {
652             err.jspError(start, "jsp.error.unterminated", "&lt;%--");
653         }
654 
655         new Node.Comment(reader.getText(start, stop), start, parent);
656     }
657 
658     /*
659      * DeclarationBody ::= (Char* - (char* '%>')) '%>'
660      */
661     private void parseDeclaration(Node parent) throws JasperException {
662         start = reader.mark();
663         Mark stop = reader.skipUntil("%>");
664         if (stop == null) {
665             err.jspError(start, "jsp.error.unterminated", "&lt;%!");
666         }
667 
668         new Node.Declaration(parseScriptText(reader.getText(start, stop)),
669                 start, parent);
670     }
671 
672     /*
673      * XMLDeclarationBody ::=   ( S? '/>' )
674      *                        | ( S? '>' (Char* - (char* '<')) CDSect?)* ETag
675      *                        | <TRANSLATION_ERROR>
676      * CDSect ::= CDStart CData CDEnd
677      * CDStart ::= '<![CDATA['
678      * CData ::= (Char* - (Char* ']]>' Char*))
679      * CDEnd ::= ']]>'
680      */
681     private void parseXMLDeclaration(Node parent) throws JasperException {
682         reader.skipSpaces();
683         if (!reader.matches("/>")) {
684             if (!reader.matches(">")) {
685                 err.jspError(start, "jsp.error.unterminated",
686                         "&lt;jsp:declaration&gt;");
687             }
688             Mark stop;
689             String text;
690             while (true) {
691                 start = reader.mark();
692                 stop = reader.skipUntil("<");
693                 if (stop == null) {
694                     err.jspError(start, "jsp.error.unterminated",
695                             "&lt;jsp:declaration&gt;");
696                 }
697                 text = parseScriptText(reader.getText(start, stop));
698                 new Node.Declaration(text, start, parent);
699                 if (reader.matches("![CDATA[")) {
700                     start = reader.mark();
701                     stop = reader.skipUntil("]]>");
702                     if (stop == null) {
703                         err.jspError(start, "jsp.error.unterminated", "CDATA");
704                     }
705                     text = parseScriptText(reader.getText(start, stop));
706                     new Node.Declaration(text, start, parent);
707                 } else {
708                     break;
709                 }
710             }
711 
712             if (!reader.matchesETagWithoutLessThan("jsp:declaration")) {
713                 err.jspError(start, "jsp.error.unterminated",
714                         "&lt;jsp:declaration&gt;");
715             }
716         }
717     }
718 
719     /*
720      * ExpressionBody ::= (Char* - (char* '%>')) '%>'
721      */
722     private void parseExpression(Node parent) throws JasperException {
723         start = reader.mark();
724         Mark stop = reader.skipUntil("%>");
725         if (stop == null) {
726             err.jspError(start, "jsp.error.unterminated", "&lt;%=");
727         }
728 
729         new Node.Expression(parseScriptText(reader.getText(start, stop)),
730                 start, parent);
731     }
732 
733     /*
734      * XMLExpressionBody ::=   ( S? '/>' )
735      *                       | ( S? '>' (Char* - (char* '<')) CDSect?)* ETag )
736      *                       | <TRANSLATION_ERROR>
737      */
738     private void parseXMLExpression(Node parent) throws JasperException {
739         reader.skipSpaces();
740         if (!reader.matches("/>")) {
741             if (!reader.matches(">")) {
742                 err.jspError(start, "jsp.error.unterminated",
743                         "&lt;jsp:expression&gt;");
744             }
745             Mark stop;
746             String text;
747             while (true) {
748                 start = reader.mark();
749                 stop = reader.skipUntil("<");
750                 if (stop == null) {
751                     err.jspError(start, "jsp.error.unterminated",
752                             "&lt;jsp:expression&gt;");
753                 }
754                 text = parseScriptText(reader.getText(start, stop));
755                 new Node.Expression(text, start, parent);
756                 if (reader.matches("![CDATA[")) {
757                     start = reader.mark();
758                     stop = reader.skipUntil("]]>");
759                     if (stop == null) {
760                         err.jspError(start, "jsp.error.unterminated", "CDATA");
761                     }
762                     text = parseScriptText(reader.getText(start, stop));
763                     new Node.Expression(text, start, parent);
764                 } else {
765                     break;
766                 }
767             }
768             if (!reader.matchesETagWithoutLessThan("jsp:expression")) {
769                 err.jspError(start, "jsp.error.unterminated",
770                         "&lt;jsp:expression&gt;");
771             }
772         }
773     }
774 
775     /*
776      * ELExpressionBody
777      * (following "${" to first unquoted "}")
778      * // XXX add formal production and confirm implementation against it,
779      * //     once it's decided
780      */
781     private void parseELExpression(Node parent) throws JasperException {
782         start = reader.mark();
783         Mark last = null;
784         boolean singleQuoted = false, doubleQuoted = false;
785         int currentChar;
786         do {
787             // XXX could move this logic to JspReader
788             last = reader.mark();               // XXX somewhat wasteful
789             currentChar = reader.nextChar();
790             if (currentChar == '//' && (singleQuoted || doubleQuoted)) {
791                 // skip character following '\' within quotes
792                 reader.nextChar();
793                 currentChar = reader.nextChar();
794             }
795             if (currentChar == -1)
796                 err.jspError(start, "jsp.error.unterminated", "${");
797             if (currentChar == '"')
798                 doubleQuoted = !doubleQuoted;
799             if (currentChar == '\'')
800                 singleQuoted = !singleQuoted;
801         } while (currentChar != '}' || (singleQuoted || doubleQuoted));
802 
803         new Node.ELExpression(reader.getText(start, last), start, parent);
804     }
805 
806     /*
807      * ScriptletBody ::= (Char* - (char* '%>')) '%>'
808      */
809     private void parseScriptlet(Node parent) throws JasperException {
810         start = reader.mark();
811         Mark stop = reader.skipUntil("%>");
812         if (stop == null) {
813             err.jspError(start, "jsp.error.unterminated", "&lt;%");
814         }
815 
816         new Node.Scriptlet(parseScriptText(reader.getText(start, stop)),
817                 start, parent);
818     }
819 
820     /*
821      * XMLScriptletBody ::=   ( S? '/>' )
822      *                      | ( S? '>' (Char* - (char* '<')) CDSect?)* ETag )
823      *                      | <TRANSLATION_ERROR>
824      */
825     private void parseXMLScriptlet(Node parent) throws JasperException {
826         reader.skipSpaces();
827         if (!reader.matches("/>")) {
828             if (!reader.matches(">")) {
829                 err.jspError(start, "jsp.error.unterminated",
830                         "&lt;jsp:scriptlet&gt;");
831             }
832             Mark stop;
833             String text;
834             while (true) {
835                 start = reader.mark();
836                 stop = reader.skipUntil("<");
837                 if (stop == null) {
838                     err.jspError(start, "jsp.error.unterminated",
839                             "&lt;jsp:scriptlet&gt;");
840                 }
841                 text = parseScriptText(reader.getText(start, stop));
842                 new Node.Scriptlet(text, start, parent);
843                 if (reader.matches("![CDATA[")) {
844                     start = reader.mark();
845                     stop = reader.skipUntil("]]>");
846                     if (stop == null) {
847                         err.jspError(start, "jsp.error.unterminated", "CDATA");
848                     }
849                     text = parseScriptText(reader.getText(start, stop));
850                     new Node.Scriptlet(text, start, parent);
851                 } else {
852                     break;
853                 }
854             }
855 
856             if (!reader.matchesETagWithoutLessThan("jsp:scriptlet")) {
857                 err.jspError(start, "jsp.error.unterminated",
858                         "&lt;jsp:scriptlet&gt;");
859             }
860         }
861     }
862 
863     /***
864      * Param ::= '<jsp:param' S Attributes S? EmptyBody S?
865      */
866     private void parseParam(Node parent) throws JasperException {
867         if (!reader.matches("<jsp:param")) {
868             err.jspError(reader.mark(), "jsp.error.paramexpected");
869         }
870         Attributes attrs = parseAttributes();
871         reader.skipSpaces();
872 
873         Node paramActionNode = new Node.ParamAction(attrs, start, parent);
874 
875         parseEmptyBody(paramActionNode, "jsp:param");
876 
877         reader.skipSpaces();
878     }
879 
880     /*
881      * For Include:
882      * StdActionContent ::= Attributes ParamBody
883      *
884      * ParamBody ::=   EmptyBody
885      *               | ( '>' S? ( '<jsp:attribute' NamedAttributes )?
886      *                   '<jsp:body'
887      *                   (JspBodyParam | <TRANSLATION_ERROR> )
888      *                   S? ETag
889      *                 )
890      *               | ( '>' S? Param* ETag )
891      *
892      * EmptyBody ::=   '/>'
893      *               | ( '>' ETag )
894      *               | ( '>' S? '<jsp:attribute' NamedAttributes ETag )
895      *
896      * JspBodyParam ::= S? '>' Param* '</jsp:body>'
897      */
898     private void parseInclude(Node parent) throws JasperException {
899         Attributes attrs = parseAttributes();
900         reader.skipSpaces();
901 
902         Node includeNode = new Node.IncludeAction(attrs, start, parent);
903 
904         parseOptionalBody(includeNode, "jsp:include",
905                 JAVAX_BODY_CONTENT_PARAM);
906     }
907 
908     /*
909      * For Forward:
910      * StdActionContent ::= Attributes ParamBody
911      */
912     private void parseForward(Node parent) throws JasperException {
913         Attributes attrs = parseAttributes();
914         reader.skipSpaces();
915 
916         Node forwardNode = new Node.ForwardAction(attrs, start, parent);
917 
918         parseOptionalBody(forwardNode, "jsp:forward",
919                 JAVAX_BODY_CONTENT_PARAM);
920     }
921 
922     private void parseInvoke(Node parent) throws JasperException {
923         Attributes attrs = parseAttributes();
924         reader.skipSpaces();
925 
926         Node invokeNode = new Node.InvokeAction(attrs, start, parent);
927 
928         parseEmptyBody(invokeNode, "jsp:invoke");
929     }
930 
931     private void parseDoBody(Node parent) throws JasperException {
932         Attributes attrs = parseAttributes();
933         reader.skipSpaces();
934 
935         Node doBodyNode = new Node.DoBodyAction(attrs, start, parent);
936 
937         parseEmptyBody(doBodyNode, "jsp:doBody");
938     }
939 
940     private void parseElement(Node parent) throws JasperException {
941         Attributes attrs = parseAttributes();
942         reader.skipSpaces();
943 
944         Node elementNode = new Node.JspElement(attrs, start, parent);
945 
946         parseOptionalBody(elementNode, "jsp:element",
947                 TagInfo.BODY_CONTENT_JSP);
948     }
949 
950     /*
951      * For GetProperty:
952      * StdActionContent ::= Attributes EmptyBody
953      */
954     private void parseGetProperty(Node parent) throws JasperException {
955         Attributes attrs = parseAttributes();
956         reader.skipSpaces();
957 
958         Node getPropertyNode = new Node.GetProperty(attrs, start, parent);
959 
960         parseOptionalBody(getPropertyNode, "jsp:getProperty",
961                 TagInfo.BODY_CONTENT_EMPTY);
962     }
963 
964     /*
965      * For SetProperty:
966      * StdActionContent ::= Attributes EmptyBody
967      */
968     private void parseSetProperty(Node parent) throws JasperException {
969         Attributes attrs = parseAttributes();
970         reader.skipSpaces();
971 
972         Node setPropertyNode = new Node.SetProperty(attrs, start, parent);
973 
974         parseOptionalBody(setPropertyNode, "jsp:setProperty",
975                 TagInfo.BODY_CONTENT_EMPTY);
976     }
977 
978     /*
979      * EmptyBody ::=   '/>'
980      *               | ( '>' ETag )
981      *               | ( '>' S? '<jsp:attribute' NamedAttributes ETag )
982      */
983     private void parseEmptyBody(Node parent, String tag)
984             throws JasperException {
985         if (reader.matches("/>")) {
986             // Done
987         } else if (reader.matches(">")) {
988             if (reader.matchesETag(tag)) {
989                 // Done
990             } else if (reader.matchesOptionalSpacesFollowedBy(
991                     "<jsp:attribute")) {
992                 // Parse the one or more named attribute nodes
993                 parseNamedAttributes(parent);
994                 if (!reader.matchesETag(tag)) {
995                     // Body not allowed
996                     err.jspError(reader.mark(),
997                             "jsp.error.jspbody.emptybody.only",
998                             "&lt;" + tag);
999                 }
1000             } else {
1001                 err.jspError(reader.mark(), "jsp.error.jspbody.emptybody.only",
1002                         "&lt;" + tag);
1003             }
1004         } else {
1005             err.jspError(reader.mark(), "jsp.error.unterminated",
1006                     "&lt;" + tag);
1007         }
1008     }
1009 
1010     /*
1011      * For UseBean:
1012      * StdActionContent ::= Attributes OptionalBody
1013      */
1014     private void parseUseBean(Node parent) throws JasperException {
1015         Attributes attrs = parseAttributes();
1016         reader.skipSpaces();
1017 
1018         Node useBeanNode = new Node.UseBean(attrs, start, parent);
1019 
1020         parseOptionalBody(useBeanNode, "jsp:useBean",
1021                 TagInfo.BODY_CONTENT_JSP);
1022     }
1023 
1024     /*
1025      * Parses OptionalBody, but also reused to parse bodies for plugin
1026      * and param since the syntax is identical (the only thing that
1027      * differs substantially is how to process the body, and thus
1028      * we accept the body type as a parameter).
1029      *
1030      * OptionalBody ::= EmptyBody | ActionBody
1031      *
1032      * ScriptlessOptionalBody ::= EmptyBody | ScriptlessActionBody
1033      *
1034      * TagDependentOptionalBody ::= EmptyBody | TagDependentActionBody
1035      *
1036      * EmptyBody ::=   '/>'
1037      *               | ( '>' ETag )
1038      *               | ( '>' S? '<jsp:attribute' NamedAttributes ETag )
1039      *
1040      * ActionBody ::=   JspAttributeAndBody
1041      *                | ( '>' Body ETag )
1042      *
1043      * ScriptlessActionBody ::=   JspAttributeAndBody 
1044      *                          | ( '>' ScriptlessBody ETag )
1045      * 
1046      * TagDependentActionBody ::=   JspAttributeAndBody
1047      *                            | ( '>' TagDependentBody ETag )
1048      *
1049      */
1050     private void parseOptionalBody(Node parent, String tag, String bodyType)
1051             throws JasperException {
1052         if (reader.matches("/>")) {
1053             // EmptyBody
1054             return;
1055         }
1056 
1057         if (!reader.matches(">")) {
1058             err.jspError(reader.mark(), "jsp.error.unterminated",
1059                     "&lt;" + tag);
1060         }
1061 
1062         if (reader.matchesETag(tag)) {
1063             // EmptyBody
1064             return;
1065         }
1066 
1067         if (!parseJspAttributeAndBody(parent, tag, bodyType)) {
1068             // Must be ( '>' # Body ETag )
1069             parseBody(parent, tag, bodyType);
1070         }
1071     }
1072 
1073     /***
1074      * Attempts to parse 'JspAttributeAndBody' production.  Returns true if
1075      * it matched, or false if not.  Assumes EmptyBody is okay as well.
1076      * <p/>
1077      * JspAttributeAndBody ::=
1078      * ( '>' # S? ( '<jsp:attribute' NamedAttributes )?
1079      * '<jsp:body'
1080      * ( JspBodyBody | <TRANSLATION_ERROR> )
1081      * S? ETag
1082      * )
1083      */
1084     private boolean parseJspAttributeAndBody(Node parent, String tag,
1085                                              String bodyType)
1086             throws JasperException {
1087         boolean result = false;
1088 
1089         if (reader.matchesOptionalSpacesFollowedBy("<jsp:attribute")) {
1090             // May be an EmptyBody, depending on whether
1091             // There's a "<jsp:body" before the ETag
1092 
1093             // First, parse <jsp:attribute> elements:
1094             parseNamedAttributes(parent);
1095 
1096             result = true;
1097         }
1098 
1099         if (reader.matchesOptionalSpacesFollowedBy("<jsp:body")) {
1100             // ActionBody
1101             parseJspBody(parent, bodyType);
1102             reader.skipSpaces();
1103             if (!reader.matchesETag(tag)) {
1104                 err.jspError(reader.mark(), "jsp.error.unterminated",
1105                         "&lt;" + tag);
1106             }
1107 
1108             result = true;
1109         } else if (result && !reader.matchesETag(tag)) {
1110             // If we have <jsp:attribute> but something other than
1111             // <jsp:body> or the end tag, translation error.
1112             err.jspError(reader.mark(), "jsp.error.jspbody.required",
1113                     "&lt;" + tag);
1114         }
1115 
1116         return result;
1117     }
1118 
1119     /*
1120      * Params ::=  `>' S?
1121      *              (   ( `<jsp:body>'
1122      *                    ( ( S? Param+ S? `</jsp:body>' )
1123      *                      | <TRANSLATION_ERROR>
1124      *                    )
1125      *                  )
1126      *                | Param+
1127      *              )
1128      *              '</jsp:params>'
1129      */
1130     private void parseJspParams(Node parent) throws JasperException {
1131         Node jspParamsNode = new Node.ParamsAction(start, parent);
1132         parseOptionalBody(jspParamsNode, "jsp:params",
1133                 JAVAX_BODY_CONTENT_PARAM);
1134     }
1135 
1136     /*
1137      * Fallback ::=   '/>'
1138      *               | ( `>' S? `<jsp:body>'
1139      *                   (   ( S?
1140      *                         ( Char* - ( Char* `</jsp:body>' ) )
1141      *                         `</jsp:body>' S?
1142      *                       )
1143      *                     | <TRANSLATION_ERROR>
1144      *                   )
1145      *                   `</jsp:fallback>'
1146      *                 )
1147      *               | ( '>'
1148      *                   ( Char* - ( Char* '</jsp:fallback>' ) )
1149      *                   '</jsp:fallback>'
1150      *                 )
1151      */
1152     private void parseFallBack(Node parent) throws JasperException {
1153         Node fallBackNode = new Node.FallBackAction(start, parent);
1154         parseOptionalBody(fallBackNode, "jsp:fallback",
1155                 JAVAX_BODY_CONTENT_TEMPLATE_TEXT);
1156     }
1157 
1158     /*
1159      * For Plugin:
1160      * StdActionContent ::= Attributes PluginBody
1161      *
1162      * PluginBody ::=   EmptyBody 
1163      *                | ( '>' S? ( '<jsp:attribute' NamedAttributes )?
1164      *                    '<jsp:body'
1165      *                    ( JspBodyPluginTags | <TRANSLATION_ERROR> )
1166      *                    S? ETag
1167      *                  )
1168      *                | ( '>' S? PluginTags ETag )
1169      *
1170      * EmptyBody ::=   '/>'
1171      *               | ( '>' ETag )
1172      *               | ( '>' S? '<jsp:attribute' NamedAttributes ETag )
1173      *
1174      */
1175     private void parsePlugin(Node parent) throws JasperException {
1176         Attributes attrs = parseAttributes();
1177         reader.skipSpaces();
1178 
1179         Node pluginNode = new Node.PlugIn(attrs, start, parent);
1180 
1181         parseOptionalBody(pluginNode, "jsp:plugin",
1182                 JAVAX_BODY_CONTENT_PLUGIN);
1183     }
1184 
1185     /*
1186      * PluginTags ::= ( '<jsp:params' Params S? )?
1187      *                ( '<jsp:fallback' Fallback? S? )?
1188      */
1189     private void parsePluginTags(Node parent) throws JasperException {
1190         reader.skipSpaces();
1191 
1192         if (reader.matches("<jsp:params")) {
1193             parseJspParams(parent);
1194             reader.skipSpaces();
1195         }
1196 
1197         if (reader.matches("<jsp:fallback")) {
1198             parseFallBack(parent);
1199             reader.skipSpaces();
1200         }
1201     }
1202 
1203     /*
1204     * StandardAction ::=   'include'       StdActionContent
1205     *                    | 'forward'       StdActionContent
1206     *                    | 'invoke'        StdActionContent
1207     *                    | 'doBody'        StdActionContent
1208     *                    | 'getProperty'   StdActionContent
1209     *                    | 'setProperty'   StdActionContent
1210     *                    | 'useBean'       StdActionContent
1211     *                    | 'plugin'        StdActionContent
1212     *                    | 'element'       StdActionContent
1213     */
1214     private void parseStandardAction(Node parent) throws JasperException {
1215         Mark start = reader.mark();
1216 
1217         if (reader.matches(INCLUDE_ACTION)) {
1218             parseInclude(parent);
1219         } else if (reader.matches(FORWARD_ACTION)) {
1220             parseForward(parent);
1221         } else if (reader.matches(INVOKE_ACTION)) {
1222             if (!isTagFile) {
1223                 err.jspError(reader.mark(), "jsp.error.action.isnottagfile",
1224                         "&lt;jsp:invoke");
1225             }
1226             parseInvoke(parent);
1227         } else if (reader.matches(DOBODY_ACTION)) {
1228             if (!isTagFile) {
1229                 err.jspError(reader.mark(), "jsp.error.action.isnottagfile",
1230                         "&lt;jsp:doBody");
1231             }
1232             parseDoBody(parent);
1233         } else if (reader.matches(GET_PROPERTY_ACTION)) {
1234             parseGetProperty(parent);
1235         } else if (reader.matches(SET_PROPERTY_ACTION)) {
1236             parseSetProperty(parent);
1237         } else if (reader.matches(USE_BEAN_ACTION)) {
1238             parseUseBean(parent);
1239         } else if (reader.matches(PLUGIN_ACTION)) {
1240             parsePlugin(parent);
1241         } else if (reader.matches(ELEMENT_ACTION)) {
1242             parseElement(parent);
1243         } else if (reader.matches(ATTRIBUTE_ACTION)) {
1244             err.jspError(start, "jsp.error.namedAttribute.invalidUse");
1245         } else if (reader.matches(BODY_ACTION)) {
1246             err.jspError(start, "jsp.error.jspbody.invalidUse");
1247         } else if (reader.matches(FALLBACK_ACTION)) {
1248             err.jspError(start, "jsp.error.fallback.invalidUse");
1249         } else if (reader.matches(PARAMS_ACTION)) {
1250             err.jspError(start, "jsp.error.params.invalidUse");
1251         } else if (reader.matches(PARAM_ACTION)) {
1252             err.jspError(start, "jsp.error.param.invalidUse");
1253         } else if (reader.matches(OUTPUT_ACTION)) {
1254             err.jspError(start, "jsp.error.jspoutput.invalidUse");
1255         } else {
1256             err.jspError(start, "jsp.error.badStandardAction");
1257         }
1258     }
1259 
1260     /*
1261      * # '<' CustomAction CustomActionBody
1262      *
1263      * CustomAction ::= TagPrefix ':' CustomActionName
1264      *
1265      * TagPrefix ::= Name
1266      *
1267      * CustomActionName ::= Name
1268      *
1269      * CustomActionBody ::=   ( Attributes CustomActionEnd )
1270      *                      | <TRANSLATION_ERROR>
1271      *
1272      * Attributes ::= ( S Attribute )* S?
1273      *
1274      * CustomActionEnd ::=   CustomActionTagDependent
1275      *                     | CustomActionJSPContent
1276      *                     | CustomActionScriptlessContent
1277      *
1278      * CustomActionTagDependent ::= TagDependentOptionalBody
1279      *
1280      * CustomActionJSPContent ::= OptionalBody
1281      *
1282      * CustomActionScriptlessContent ::= ScriptlessOptionalBody
1283      */
1284     private boolean parseCustomTag(Node parent) throws JasperException {
1285 
1286         if (reader.peekChar() != '<') {
1287             return false;
1288         }
1289 
1290         // Parse 'CustomAction' production (tag prefix and custom action name)
1291         reader.nextChar();        // skip '<'
1292         String tagName = reader.parseToken(false);
1293         int i = tagName.indexOf(':');
1294         if (i == -1) {
1295             reader.reset(start);
1296             return false;
1297         }
1298 
1299         String prefix = tagName.substring(0, i);
1300         String shortTagName = tagName.substring(i + 1);
1301 
1302         // Check if this is a user-defined tag.
1303         String uri = pageInfo.getURI(prefix);
1304         if (uri == null) {
1305             reader.reset(start);
1306             // Remember the prefix for later error checking
1307             pageInfo.putNonCustomTagPrefix(prefix, reader.mark());
1308             return false;
1309         }
1310 
1311         TagLibraryInfo tagLibInfo = pageInfo.getTaglib(uri);
1312         TagInfo tagInfo = tagLibInfo.getTag(shortTagName);
1313         TagFileInfo tagFileInfo = tagLibInfo.getTagFile(shortTagName);
1314         if (tagInfo == null && tagFileInfo == null) {
1315             err.jspError(start, "jsp.error.bad_tag", shortTagName, prefix);
1316         }
1317         Class tagHandlerClass = null;
1318         if (tagInfo != null) {
1319             // Must be a classic tag, load it here.
1320             // tag files will be loaded later, in TagFileProcessor
1321             String handlerClassName = tagInfo.getTagClassName();
1322             try {
1323                 tagHandlerClass =
1324                         ctxt.getClassLoader().loadClass(handlerClassName);
1325             } catch (Exception e) {
1326                 err.jspError(start, "jsp.error.loadclass.taghandler",
1327                         handlerClassName, tagName);
1328             }
1329         }
1330 
1331         // Parse 'CustomActionBody' production:
1332         // At this point we are committed - if anything fails, we produce
1333         // a translation error.
1334 
1335         // Parse 'Attributes' production:
1336         Attributes attrs = parseAttributes();
1337         reader.skipSpaces();
1338 
1339         // Parse 'CustomActionEnd' production:
1340         if (reader.matches("/>")) {
1341             if (tagInfo != null) {
1342                 new Node.CustomTag(tagName, prefix, shortTagName, uri, attrs,
1343                         start, parent, tagInfo, tagHandlerClass);
1344             } else {
1345                 new Node.CustomTag(tagName, prefix, shortTagName, uri, attrs,
1346                         start, parent, tagFileInfo);
1347             }
1348             return true;
1349         }
1350 
1351         // Now we parse one of 'CustomActionTagDependent', 
1352         // 'CustomActionJSPContent', or 'CustomActionScriptlessContent'.
1353         // depending on body-content in TLD.
1354 
1355         // Looking for a body, it still can be empty; but if there is a
1356         // a tag body, its syntax would be dependent on the type of
1357         // body content declared in the TLD.
1358         String bc;
1359         if (tagInfo != null) {
1360             bc = tagInfo.getBodyContent();
1361         } else {
1362             bc = tagFileInfo.getTagInfo().getBodyContent();
1363         }
1364 
1365         Node tagNode = null;
1366         if (tagInfo != null) {
1367             tagNode = new Node.CustomTag(tagName, prefix, shortTagName, uri,
1368                     attrs, start, parent, tagInfo,
1369                     tagHandlerClass);
1370         } else {
1371             tagNode = new Node.CustomTag(tagName, prefix, shortTagName, uri,
1372                     attrs, start, parent, tagFileInfo);
1373         }
1374 
1375         parseOptionalBody(tagNode, tagName, bc);
1376 
1377         return true;
1378     }
1379 
1380     /*
1381      * Parse for a template text string until '<' or "${" is encountered, 
1382      * recognizing escape sequences "\%" and "\$".
1383      */
1384     private void parseTemplateText(Node parent) throws JasperException {
1385 
1386         if (!reader.hasMoreInput())
1387             return;
1388 
1389         CharArrayWriter ttext = new CharArrayWriter();
1390         // Output the first character
1391         int ch = reader.nextChar();
1392         if (ch == '//') {
1393             reader.pushChar();
1394         } else {
1395             ttext.write(ch);
1396         }
1397 
1398         while (reader.hasMoreInput()) {
1399             ch = reader.nextChar();
1400             if (ch == '<') {
1401                 reader.pushChar();
1402                 break;
1403             } else if (ch == '$') {
1404                 if (!reader.hasMoreInput()) {
1405                     ttext.write('$');
1406                     break;
1407                 }
1408                 ch = reader.nextChar();
1409                 if (ch == '{') {
1410                     reader.pushChar();
1411                     reader.pushChar();
1412                     break;
1413                 }
1414                 ttext.write('$');
1415                 reader.pushChar();
1416                 continue;
1417             } else if (ch == '//') {
1418                 if (!reader.hasMoreInput()) {
1419                     ttext.write('//');
1420                     break;
1421                 }
1422                 // Look for \% or \$
1423                 // Only recognize \$ if isELIgnored is false, but since it can
1424                 // be set in a page directive, it cannot be determined yet.
1425                 char next = (char) reader.peekChar();
1426                 if (next == '%') {
1427                     ch = reader.nextChar();
1428                 } else if (next == '$') {
1429                     // Skip the $ and use a hack to flag this sequence
1430                     reader.nextChar();
1431                     ch = Constants.HACK_CHAR;
1432                 }
1433             }
1434             ttext.write(ch);
1435         }
1436         new Node.TemplateText(ttext.toString(), start, parent);
1437     }
1438 
1439     /*
1440     * XMLTemplateText ::=   ( S? '/>' )
1441     *                     | ( S? '>'
1442     *                         ( ( Char* - ( Char* ( '<' | '${' ) ) )
1443     *                           ( '${' ELExpressionBody )?
1444     *                           CDSect?
1445     *                         )* ETag
1446     *                       )
1447     *                     | <TRANSLATION_ERROR>
1448     */
1449     private void parseXMLTemplateText(Node parent) throws JasperException {
1450         reader.skipSpaces();
1451         if (!reader.matches("/>")) {
1452             if (!reader.matches(">")) {
1453                 err.jspError(start, "jsp.error.unterminated",
1454                         "&lt;jsp:text&gt;");
1455             }
1456             CharArrayWriter ttext = new CharArrayWriter();
1457             while (reader.hasMoreInput()) {
1458                 int ch = reader.nextChar();
1459                 if (ch == '<') {
1460                     // Check for <![CDATA[
1461                     if (!reader.matches("![CDATA[")) {
1462                         break;
1463                     }
1464                     start = reader.mark();
1465                     Mark stop = reader.skipUntil("]]>");
1466                     if (stop == null) {
1467                         err.jspError(start, "jsp.error.unterminated", "CDATA");
1468                     }
1469                     String text = reader.getText(start, stop);
1470                     ttext.write(text, 0, text.length());
1471                 } else if (ch == '//') {
1472                     if (!reader.hasMoreInput()) {
1473                         ttext.write('//');
1474                         break;
1475                     }
1476                     ch = reader.nextChar();
1477                     if (ch != '$') {
1478                         ttext.write('//');
1479                     }
1480                     ttext.write(ch);
1481                 } else if (ch == '$') {
1482                     if (!reader.hasMoreInput()) {
1483                         ttext.write('$');
1484                         break;
1485                     }
1486                     ch = reader.nextChar();
1487                     if (ch != '{') {
1488                         ttext.write('$');
1489                         reader.pushChar();
1490                         continue;
1491                     }
1492                     // Create a template text node
1493                     new Node.TemplateText(ttext.toString(), start, parent);
1494 
1495                     // Mark and parse the EL expression and create its node:
1496                     start = reader.mark();
1497                     parseELExpression(parent);
1498 
1499                     start = reader.mark();
1500                     ttext = new CharArrayWriter();
1501                 } else {
1502                     ttext.write(ch);
1503                 }
1504             }
1505 
1506             new Node.TemplateText(ttext.toString(), start, parent);
1507 
1508             if (!reader.hasMoreInput()) {
1509                 err.jspError(start, "jsp.error.unterminated",
1510                         "&lt;jsp:text&gt;");
1511             } else if (!reader.matchesETagWithoutLessThan("jsp:text")) {
1512                 err.jspError(start, "jsp.error.jsptext.badcontent");
1513             }
1514         }
1515     }
1516 
1517     /*
1518      * AllBody ::=       ( '<%--'              JSPCommentBody     )
1519      *                 | ( '<%@'               DirectiveBody      )
1520      *                 | ( '<jsp:directive.'   XMLDirectiveBody   )
1521      *                 | ( '<%!'               DeclarationBody    )
1522      *                 | ( '<jsp:declaration'  XMLDeclarationBody )
1523      *                 | ( '<%='               ExpressionBody     )
1524      *                 | ( '<jsp:expression'   XMLExpressionBody  )
1525      *                 | ( '${'                ELExpressionBody   )
1526      *                 | ( '<%'                ScriptletBody      )
1527      *                 | ( '<jsp:scriptlet'    XMLScriptletBody   )
1528      *                 | ( '<jsp:text'         XMLTemplateText    )
1529      *                 | ( '<jsp:'             StandardAction     )
1530      *                 | ( '<'                 CustomAction
1531      *                                         CustomActionBody   )
1532      *                       | TemplateText
1533      */
1534     private void parseElements(Node parent)
1535             throws JasperException {
1536         if (scriptlessCount > 0) {
1537             // vc: ScriptlessBody
1538             // We must follow the ScriptlessBody production if one of
1539             // our parents is ScriptlessBody.
1540             parseElementsScriptless(parent);
1541             return;
1542         }
1543 
1544         start = reader.mark();
1545         if (reader.matches("<%--")) {
1546             parseComment(parent);
1547         } else if (reader.matches("<%@")) {
1548             parseDirective(parent);
1549         } else if (reader.matches("<jsp:directive.")) {
1550             parseXMLDirective(parent);
1551         } else if (reader.matches("<%!")) {
1552             parseDeclaration(parent);
1553         } else if (reader.matches("<jsp:declaration")) {
1554             parseXMLDeclaration(parent);
1555         } else if (reader.matches("<%=")) {
1556             parseExpression(parent);
1557         } else if (reader.matches("<jsp:expression")) {
1558             parseXMLExpression(parent);
1559         } else if (reader.matches("<%")) {
1560             parseScriptlet(parent);
1561         } else if (reader.matches("<jsp:scriptlet")) {
1562             parseXMLScriptlet(parent);
1563         } else if (reader.matches("<jsp:text")) {
1564             parseXMLTemplateText(parent);
1565         } else if (reader.matches("${")) {
1566             parseELExpression(parent);
1567         } else if (reader.matches("<jsp:")) {
1568             parseStandardAction(parent);
1569         } else if (!parseCustomTag(parent)) {
1570             checkUnbalancedEndTag();
1571             parseTemplateText(parent);
1572         }
1573     }
1574 
1575     /*
1576      * ScriptlessBody ::=  ( '<%--'              JSPCommentBody      )
1577      *                   | ( '<%@'               DirectiveBody       )
1578      *                   | ( '<jsp:directive.'   XMLDirectiveBody    )
1579      *                   | ( '<%!'               <TRANSLATION_ERROR> )
1580      *                   | ( '<jsp:declaration'  <TRANSLATION_ERROR> )
1581      *                   | ( '<%='               <TRANSLATION_ERROR> )
1582      *                   | ( '<jsp:expression'   <TRANSLATION_ERROR> )
1583      *                   | ( '<%'                <TRANSLATION_ERROR> )
1584      *                   | ( '<jsp:scriptlet'    <TRANSLATION_ERROR> )
1585      *                   | ( '<jsp:text'         XMLTemplateText     )
1586      *                   | ( '${'                ELExpressionBody    )
1587      *                   | ( '<jsp:'             StandardAction      )
1588      *                   | ( '<'                 CustomAction
1589      *                                           CustomActionBody    )
1590      *                   | TemplateText
1591      */
1592     private void parseElementsScriptless(Node parent)
1593             throws JasperException {
1594         // Keep track of how many scriptless nodes we've encountered
1595         // so we know whether our child nodes are forced scriptless
1596         scriptlessCount++;
1597 
1598         start = reader.mark();
1599         if (reader.matches("<%--")) {
1600             parseComment(parent);
1601         } else if (reader.matches("<%@")) {
1602             parseDirective(parent);
1603         } else if (reader.matches("<jsp:directive.")) {
1604             parseXMLDirective(parent);
1605         } else if (reader.matches("<%!")) {
1606             err.jspError(reader.mark(), "jsp.error.no.scriptlets");
1607         } else if (reader.matches("<jsp:declaration")) {
1608             err.jspError(reader.mark(), "jsp.error.no.scriptlets");
1609         } else if (reader.matches("<%=")) {
1610             err.jspError(reader.mark(), "jsp.error.no.scriptlets");
1611         } else if (reader.matches("<jsp:expression")) {
1612             err.jspError(reader.mark(), "jsp.error.no.scriptlets");
1613         } else if (reader.matches("<%")) {
1614             err.jspError(reader.mark(), "jsp.error.no.scriptlets");
1615         } else if (reader.matches("<jsp:scriptlet")) {
1616             err.jspError(reader.mark(), "jsp.error.no.scriptlets");
1617         } else if (reader.matches("<jsp:text")) {
1618             parseXMLTemplateText(parent);
1619         } else if (reader.matches("${")) {
1620             parseELExpression(parent);
1621         } else if (reader.matches("<jsp:")) {
1622             parseStandardAction(parent);
1623         } else if (!parseCustomTag(parent)) {
1624             checkUnbalancedEndTag();
1625             parseTemplateText(parent);
1626         }
1627 
1628         scriptlessCount--;
1629     }
1630 
1631     /*
1632     * TemplateTextBody ::=   ( '<%--'              JSPCommentBody      )
1633     *                      | ( '<%@'               DirectiveBody       )
1634     *                      | ( '<jsp:directive.'   XMLDirectiveBody    )
1635     *                      | ( '<%!'               <TRANSLATION_ERROR> )
1636     *                      | ( '<jsp:declaration'  <TRANSLATION_ERROR> )
1637     *                      | ( '<%='               <TRANSLATION_ERROR> )
1638     *                      | ( '<jsp:expression'   <TRANSLATION_ERROR> )
1639     *                      | ( '<%'                <TRANSLATION_ERROR> )
1640     *                      | ( '<jsp:scriptlet'    <TRANSLATION_ERROR> )
1641     *                      | ( '<jsp:text'         <TRANSLATION_ERROR> )
1642     *                      | ( '${'                <TRANSLATION_ERROR> )
1643     *                      | ( '<jsp:'             <TRANSLATION_ERROR> )
1644     *                      | TemplateText
1645     */
1646     private void parseElementsTemplateText(Node parent)
1647             throws JasperException {
1648         start = reader.mark();
1649         if (reader.matches("<%--")) {
1650             parseComment(parent);
1651         } else if (reader.matches("<%@")) {
1652             parseDirective(parent);
1653         } else if (reader.matches("<jsp:directive.")) {
1654             parseXMLDirective(parent);
1655         } else if (reader.matches("<%!")) {
1656             err.jspError(reader.mark(), "jsp.error.not.in.template",
1657                     "Declarations");
1658         } else if (reader.matches("<jsp:declaration")) {
1659             err.jspError(reader.mark(), "jsp.error.not.in.template",
1660                     "Declarations");
1661         } else if (reader.matches("<%=")) {
1662             err.jspError(reader.mark(), "jsp.error.not.in.template",
1663                     "Expressions");
1664         } else if (reader.matches("<jsp:expression")) {
1665             err.jspError(reader.mark(), "jsp.error.not.in.template",
1666                     "Expressions");
1667         } else if (reader.matches("<%")) {
1668             err.jspError(reader.mark(), "jsp.error.not.in.template",
1669                     "Scriptlets");
1670         } else if (reader.matches("<jsp:scriptlet")) {
1671             err.jspError(reader.mark(), "jsp.error.not.in.template",
1672                     "Scriptlets");
1673         } else if (reader.matches("<jsp:text")) {
1674             err.jspError(reader.mark(), "jsp.error.not.in.template",
1675                     "&lt;jsp:text");
1676         } else if (reader.matches("${")) {
1677             err.jspError(reader.mark(), "jsp.error.not.in.template",
1678                     "Expression language");
1679         } else if (reader.matches("<jsp:")) {
1680             err.jspError(reader.mark(), "jsp.error.not.in.template",
1681                     "Standard actions");
1682         } else if (parseCustomTag(parent)) {
1683             err.jspError(reader.mark(), "jsp.error.not.in.template",
1684                     "Custom actions");
1685         } else {
1686             checkUnbalancedEndTag();
1687             parseTemplateText(parent);
1688         }
1689     }
1690 
1691     /*
1692      * Flag as error if an unbalanced end tag appears by itself.
1693      */
1694     private void checkUnbalancedEndTag() throws JasperException {
1695 
1696         if (!reader.matches("</")) {
1697             return;
1698         }
1699 
1700         // Check for unbalanced standard actions
1701         if (reader.matches("jsp:")) {
1702             err.jspError(start, "jsp.error.unbalanced.endtag", "jsp:");
1703         }
1704 
1705         // Check for unbalanced custom actions
1706         String tagName = reader.parseToken(false);
1707         int i = tagName.indexOf(':');
1708         if (i == -1 || pageInfo.getURI(tagName.substring(0, i)) == null) {
1709             reader.reset(start);
1710             return;
1711         }
1712 
1713         err.jspError(start, "jsp.error.unbalanced.endtag", tagName);
1714     }
1715 
1716     /***
1717      * TagDependentBody :=
1718      */
1719     private void parseTagDependentBody(Node parent, String tag)
1720             throws JasperException {
1721         Mark bodyStart = reader.mark();
1722         Mark bodyEnd = reader.skipUntilETag(tag);
1723         if (bodyEnd == null) {
1724             err.jspError(start, "jsp.error.unterminated", "&lt;" + tag);
1725         }
1726         new Node.TemplateText(reader.getText(bodyStart, bodyEnd), bodyStart,
1727                 parent);
1728     }
1729 
1730     /*
1731      * Parses jsp:body action.
1732      */
1733     private void parseJspBody(Node parent, String bodyType)
1734             throws JasperException {
1735         Mark start = reader.mark();
1736         Node bodyNode = new Node.JspBody(start, parent);
1737 
1738         reader.skipSpaces();
1739         if (!reader.matches("/>")) {
1740             if (!reader.matches(">")) {
1741                 err.jspError(start, "jsp.error.unterminated",
1742                         "&lt;jsp:body");
1743             }
1744             parseBody(bodyNode, "jsp:body", bodyType);
1745         }
1746     }
1747 
1748     /*
1749      * Parse the body as JSP content.
1750      * @param tag The name of the tag whose end tag would terminate the body
1751      * @param bodyType One of the TagInfo body types
1752      */
1753     private void parseBody(Node parent, String tag, String bodyType)
1754             throws JasperException {
1755         if (bodyType.equalsIgnoreCase(TagInfo.BODY_CONTENT_TAG_DEPENDENT)) {
1756             parseTagDependentBody(parent, tag);
1757         } else if (bodyType.equalsIgnoreCase(TagInfo.BODY_CONTENT_EMPTY)) {
1758             if (!reader.matchesETag(tag)) {
1759                 err.jspError(start, "jasper.error.emptybodycontent.nonempty",
1760                         tag);
1761             }
1762         } else if (bodyType == JAVAX_BODY_CONTENT_PLUGIN) {
1763             // (note the == since we won't recognize JAVAX_* 
1764             // from outside this module).
1765             parsePluginTags(parent);
1766             if (!reader.matchesETag(tag)) {
1767                 err.jspError(reader.mark(), "jsp.error.unterminated",
1768                         "&lt;" + tag);
1769             }
1770         } else if (bodyType.equalsIgnoreCase(TagInfo.BODY_CONTENT_JSP) ||
1771                 bodyType.equalsIgnoreCase(TagInfo.BODY_CONTENT_SCRIPTLESS) ||
1772                 (bodyType == JAVAX_BODY_CONTENT_PARAM) ||
1773                 (bodyType == JAVAX_BODY_CONTENT_TEMPLATE_TEXT)) {
1774             while (reader.hasMoreInput()) {
1775                 if (reader.matchesETag(tag)) {
1776                     return;
1777                 }
1778 
1779                 // Check for nested jsp:body or jsp:attribute
1780                 if (tag.equals("jsp:body") || tag.equals("jsp:attribute")) {
1781                     if (reader.matches("<jsp:attribute")) {
1782                         err.jspError(reader.mark(), "jsp.error.nested.jspattribute");
1783                     } else if (reader.matches("<jsp:body")) {
1784                         err.jspError(reader.mark(), "jsp.error.nested.jspbody");
1785                     }
1786                 }
1787 
1788                 if (bodyType.equalsIgnoreCase(TagInfo.BODY_CONTENT_JSP)) {
1789                     parseElements(parent);
1790                 } else if (bodyType.equalsIgnoreCase(
1791                         TagInfo.BODY_CONTENT_SCRIPTLESS)) {
1792                     parseElementsScriptless(parent);
1793                 } else if (bodyType == JAVAX_BODY_CONTENT_PARAM) {
1794                     // (note the == since we won't recognize JAVAX_* 
1795                     // from outside this module).
1796                     reader.skipSpaces();
1797                     parseParam(parent);
1798                 } else if (bodyType == JAVAX_BODY_CONTENT_TEMPLATE_TEXT) {
1799                     parseElementsTemplateText(parent);
1800                 }
1801             }
1802             err.jspError(start, "jsp.error.unterminated", "&lt;" + tag);
1803         } else {
1804             err.jspError(start, "jasper.error.bad.bodycontent.type");
1805         }
1806     }
1807 
1808     /*
1809      * Parses named attributes.
1810      */
1811     private void parseNamedAttributes(Node parent) throws JasperException {
1812         do {
1813             Mark start = reader.mark();
1814             Attributes attrs = parseAttributes();
1815             Node.NamedAttribute namedAttributeNode =
1816                     new Node.NamedAttribute(attrs, start, parent);
1817 
1818             reader.skipSpaces();
1819             if (!reader.matches("/>")) {
1820                 if (!reader.matches(">")) {
1821                     err.jspError(start, "jsp.error.unterminated",
1822                             "&lt;jsp:attribute");
1823                 }
1824                 if (namedAttributeNode.isTrim()) {
1825                     reader.skipSpaces();
1826                 }
1827                 parseBody(namedAttributeNode, "jsp:attribute",
1828                         getAttributeBodyType(parent,
1829                                 attrs.getValue("name")));
1830                 if (namedAttributeNode.isTrim()) {
1831                     Node.Nodes subElems = namedAttributeNode.getBody();
1832                     if (subElems != null) {
1833                         Node lastNode = subElems.getNode(subElems.size() - 1);
1834                         if (lastNode instanceof Node.TemplateText) {
1835                             ((Node.TemplateText) lastNode).rtrim();
1836                         }
1837                     }
1838                 }
1839             }
1840             reader.skipSpaces();
1841         } while (reader.matches("<jsp:attribute"));
1842     }
1843 
1844     /***
1845      * Determine the body type of <jsp:attribute> from the enclosing node
1846      */
1847     private String getAttributeBodyType(Node n, String name) {
1848 
1849         if (n instanceof Node.CustomTag) {
1850             TagInfo tagInfo = ((Node.CustomTag) n).getTagInfo();
1851             TagAttributeInfo[] tldAttrs = tagInfo.getAttributes();
1852             for (int i = 0; i < tldAttrs.length; i++) {
1853                 if (name.equals(tldAttrs[i].getName())) {
1854                     if (tldAttrs[i].isFragment()) {
1855                         return TagInfo.BODY_CONTENT_SCRIPTLESS;
1856                     }
1857                     if (tldAttrs[i].canBeRequestTime()) {
1858                         return TagInfo.BODY_CONTENT_JSP;
1859                     }
1860                 }
1861             }
1862             if (tagInfo.hasDynamicAttributes()) {
1863                 return TagInfo.BODY_CONTENT_JSP;
1864             }
1865         } else if (n instanceof Node.IncludeAction) {
1866             if ("page".equals(name)) {
1867                 return TagInfo.BODY_CONTENT_JSP;
1868             }
1869         } else if (n instanceof Node.ForwardAction) {
1870             if ("page".equals(name)) {
1871                 return TagInfo.BODY_CONTENT_JSP;
1872             }
1873         } else if (n instanceof Node.SetProperty) {
1874             if ("value".equals(name)) {
1875                 return TagInfo.BODY_CONTENT_JSP;
1876             }
1877         } else if (n instanceof Node.UseBean) {
1878             if ("beanName".equals(name)) {
1879                 return TagInfo.BODY_CONTENT_JSP;
1880             }
1881         } else if (n instanceof Node.PlugIn) {
1882             if ("width".equals(name) || "height".equals(name)) {
1883                 return TagInfo.BODY_CONTENT_JSP;
1884             }
1885         } else if (n instanceof Node.ParamAction) {
1886             if ("value".equals(name)) {
1887                 return TagInfo.BODY_CONTENT_JSP;
1888             }
1889         } else if (n instanceof Node.JspElement) {
1890             return TagInfo.BODY_CONTENT_JSP;
1891         }
1892 
1893         return JAVAX_BODY_CONTENT_TEMPLATE_TEXT;
1894     }
1895 
1896     private void parseTagFileDirectives(Node parent)
1897             throws JasperException {
1898         reader.setSingleFile(true);
1899         reader.skipUntil("<");
1900         while (reader.hasMoreInput()) {
1901             start = reader.mark();
1902             if (reader.matches("%--")) {
1903                 parseComment(parent);
1904             } else if (reader.matches("%@")) {
1905                 parseDirective(parent);
1906             } else if (reader.matches("jsp:directive.")) {
1907                 parseXMLDirective(parent);
1908             }
1909             reader.skipUntil("<");
1910         }
1911     }
1912 }
1913