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  
18  package org.apache.struts2.jasper.compiler;
19  
20  import com.opensymphony.xwork2.util.logging.Logger;
21  import com.opensymphony.xwork2.util.logging.LoggerFactory;
22  import org.apache.struts2.jasper.JasperException;
23  import org.apache.struts2.jasper.JspCompilationContext;
24  
25  import java.io.CharArrayWriter;
26  import java.io.FileNotFoundException;
27  import java.io.IOException;
28  import java.io.InputStreamReader;
29  import java.net.MalformedURLException;
30  import java.net.URL;
31  import java.util.List;
32  import java.util.Vector;
33  import java.util.jar.JarFile;
34  
35  /***
36   * JspReader is an input buffer for the JSP parser. It should allow
37   * unlimited lookahead and pushback. It also has a bunch of parsing
38   * utility methods for understanding htmlesque thingies.
39   *
40   * @author Anil K. Vijendran
41   * @author Anselm Baird-Smith
42   * @author Harish Prabandham
43   * @author Rajiv Mordani
44   * @author Mandar Raje
45   * @author Danno Ferrin
46   * @author Kin-man Chung
47   * @author Shawn Bayern
48   * @author Mark Roth
49   */
50  
51  class JspReader {
52  
53      /***
54       * Logger.
55       */
56      private Logger log = LoggerFactory.getLogger(JspReader.class);
57  
58      /***
59       * The current spot in the file.
60       */
61      private Mark current;
62  
63      /***
64       * What is this?
65       */
66      private String master;
67  
68      /***
69       * The list of source files.
70       */
71      private List sourceFiles;
72  
73      /***
74       * The current file ID (-1 indicates an error or no file).
75       */
76      private int currFileId;
77  
78      /***
79       * Seems redundant.
80       */
81      private int size;
82  
83      /***
84       * The compilation context.
85       */
86      private JspCompilationContext context;
87  
88      /***
89       * The Jasper error dispatcher.
90       */
91      private ErrorDispatcher err;
92  
93      /***
94       * Set to true when using the JspReader on a single file where we read up
95       * to the end and reset to the beginning many times.
96       * (as in ParserController.figureOutJspDocument()).
97       */
98      private boolean singleFile;
99  
100     /***
101      * Constructor.
102      *
103      * @param ctxt     The compilation context
104      * @param fname    The file name
105      * @param encoding The file encoding
106      * @param jarFile  ?
107      * @param err      The error dispatcher
108      * @throws JasperException       If a Jasper-internal error occurs
109      * @throws FileNotFoundException If the JSP file is not found (or is unreadable)
110      * @throws IOException           If an IO-level error occurs, e.g. reading the file
111      */
112     public JspReader(JspCompilationContext ctxt,
113                      String fname,
114                      String encoding,
115                      JarFile jarFile,
116                      ErrorDispatcher err)
117             throws JasperException, FileNotFoundException, IOException {
118 
119         this(ctxt, fname, encoding,
120                 JspUtil.getReader(fname, encoding, jarFile, ctxt, err),
121                 err);
122     }
123 
124     /***
125      * Constructor: same as above constructor but with initialized reader
126      * to the file given.
127      */
128     public JspReader(JspCompilationContext ctxt,
129                      String fname,
130                      String encoding,
131                      InputStreamReader reader,
132                      ErrorDispatcher err)
133             throws JasperException, FileNotFoundException {
134 
135         this.context = ctxt;
136         this.err = err;
137         sourceFiles = new Vector();
138         currFileId = 0;
139         size = 0;
140         singleFile = false;
141         pushFile(fname, encoding, reader);
142     }
143 
144     /***
145      * @return JSP compilation context with which this JspReader is
146      *         associated
147      */
148     JspCompilationContext getJspCompilationContext() {
149         return context;
150     }
151 
152     /***
153      * Returns the file at the given position in the list.
154      *
155      * @param fileid The file position in the list
156      * @return The file at that position, if found, null otherwise
157      */
158     String getFile(final int fileid) {
159         return (String) sourceFiles.get(fileid);
160     }
161 
162     /***
163      * Checks if the current file has more input.
164      *
165      * @return True if more reading is possible
166      * @throws JasperException if an error occurs
167      */
168     boolean hasMoreInput() throws JasperException {
169         if (current.cursor >= current.stream.length) {
170             if (singleFile) return false;
171             while (popFile()) {
172                 if (current.cursor < current.stream.length) return true;
173             }
174             return false;
175         }
176         return true;
177     }
178 
179     int nextChar() throws JasperException {
180         if (!hasMoreInput())
181             return -1;
182 
183         int ch = current.stream[current.cursor];
184 
185         current.cursor++;
186 
187         if (ch == '\n') {
188             current.line++;
189             current.col = 0;
190         } else {
191             current.col++;
192         }
193         return ch;
194     }
195 
196     /***
197      * Back up the current cursor by one char, assumes current.cursor > 0,
198      * and that the char to be pushed back is not '\n'.
199      */
200     void pushChar() {
201         current.cursor--;
202         current.col--;
203     }
204 
205     String getText(Mark start, Mark stop) throws JasperException {
206         Mark oldstart = mark();
207         reset(start);
208         CharArrayWriter caw = new CharArrayWriter();
209         while (!stop.equals(mark()))
210             caw.write(nextChar());
211         caw.close();
212         reset(oldstart);
213         return caw.toString();
214     }
215 
216     int peekChar() throws JasperException {
217         if (!hasMoreInput())
218             return -1;
219         return current.stream[current.cursor];
220     }
221 
222     Mark mark() {
223         return new Mark(current);
224     }
225 
226     void reset(Mark mark) {
227         current = new Mark(mark);
228     }
229 
230     boolean matchesIgnoreCase(String string) throws JasperException {
231         Mark mark = mark();
232         int ch = 0;
233         int i = 0;
234         do {
235             ch = nextChar();
236             if (Character.toLowerCase((char) ch) != string.charAt(i++)) {
237                 reset(mark);
238                 return false;
239             }
240         } while (i < string.length());
241         reset(mark);
242         return true;
243     }
244 
245     /***
246      * search the stream for a match to a string
247      *
248      * @param string The string to match
249      * @return <strong>true</strong> is one is found, the current position
250      *         in stream is positioned after the search string, <strong>
251      *         false</strong> otherwise, position in stream unchanged.
252      */
253     boolean matches(String string) throws JasperException {
254         Mark mark = mark();
255         int ch = 0;
256         int i = 0;
257         do {
258             ch = nextChar();
259             if (((char) ch) != string.charAt(i++)) {
260                 reset(mark);
261                 return false;
262             }
263         } while (i < string.length());
264         return true;
265     }
266 
267     boolean matchesETag(String tagName) throws JasperException {
268         Mark mark = mark();
269 
270         if (!matches("</" + tagName))
271             return false;
272         skipSpaces();
273         if (nextChar() == '>')
274             return true;
275 
276         reset(mark);
277         return false;
278     }
279 
280     boolean matchesETagWithoutLessThan(String tagName)
281             throws JasperException {
282         Mark mark = mark();
283 
284         if (!matches("/" + tagName))
285             return false;
286         skipSpaces();
287         if (nextChar() == '>')
288             return true;
289 
290         reset(mark);
291         return false;
292     }
293 
294 
295     /***
296      * Looks ahead to see if there are optional spaces followed by
297      * the given String.  If so, true is returned and those spaces and
298      * characters are skipped.  If not, false is returned and the
299      * position is restored to where we were before.
300      */
301     boolean matchesOptionalSpacesFollowedBy(String s)
302             throws JasperException {
303         Mark mark = mark();
304 
305         skipSpaces();
306         boolean result = matches(s);
307         if (!result) {
308             reset(mark);
309         }
310 
311         return result;
312     }
313 
314     int skipSpaces() throws JasperException {
315         int i = 0;
316         while (hasMoreInput() && isSpace()) {
317             i++;
318             nextChar();
319         }
320         return i;
321     }
322 
323     /***
324      * Skip until the given string is matched in the stream.
325      * When returned, the context is positioned past the end of the match.
326      *
327      * @param s The String to match.
328      * @return A non-null <code>Mark</code> instance (positioned immediately
329      *         before the search string) if found, <strong>null</strong>
330      *         otherwise.
331      */
332     Mark skipUntil(String limit) throws JasperException {
333         Mark ret = null;
334         int limlen = limit.length();
335         int ch;
336 
337         skip:
338         for (ret = mark(), ch = nextChar(); ch != -1;
339              ret = mark(), ch = nextChar()) {
340             if (ch == limit.charAt(0)) {
341                 Mark restart = mark();
342                 for (int i = 1; i < limlen; i++) {
343                     if (peekChar() == limit.charAt(i))
344                         nextChar();
345                     else {
346                         reset(restart);
347                         continue skip;
348                     }
349                 }
350                 return ret;
351             }
352         }
353         return null;
354     }
355 
356     /***
357      * Skip until the given string is matched in the stream, but ignoring
358      * chars initially escaped by a '\'.
359      * When returned, the context is positioned past the end of the match.
360      *
361      * @param s The String to match.
362      * @return A non-null <code>Mark</code> instance (positioned immediately
363      *         before the search string) if found, <strong>null</strong>
364      *         otherwise.
365      */
366     Mark skipUntilIgnoreEsc(String limit) throws JasperException {
367         Mark ret = null;
368         int limlen = limit.length();
369         int ch;
370         int prev = 'x';        // Doesn't matter
371 
372         skip:
373         for (ret = mark(), ch = nextChar(); ch != -1;
374              ret = mark(), prev = ch, ch = nextChar()) {
375             if (ch == '//' && prev == '//') {
376                 ch = 0;                // Double \ is not an escape char anymore
377             } else if (ch == limit.charAt(0) && prev != '//') {
378                 for (int i = 1; i < limlen; i++) {
379                     if (peekChar() == limit.charAt(i))
380                         nextChar();
381                     else
382                         continue skip;
383                 }
384                 return ret;
385             }
386         }
387         return null;
388     }
389 
390     /***
391      * Skip until the given end tag is matched in the stream.
392      * When returned, the context is positioned past the end of the tag.
393      *
394      * @param tag The name of the tag whose ETag (</tag>) to match.
395      * @return A non-null <code>Mark</code> instance (positioned immediately
396      *         before the ETag) if found, <strong>null</strong> otherwise.
397      */
398     Mark skipUntilETag(String tag) throws JasperException {
399         Mark ret = skipUntil("</" + tag);
400         if (ret != null) {
401             skipSpaces();
402             if (nextChar() != '>')
403                 ret = null;
404         }
405         return ret;
406     }
407 
408     final boolean isSpace() throws JasperException {
409         // Note: If this logic changes, also update Node.TemplateText.rtrim()
410         return peekChar() <= ' ';
411     }
412 
413     /***
414      * Parse a space delimited token.
415      * If quoted the token will consume all characters up to a matching quote,
416      * otherwise, it consumes up to the first delimiter character.
417      *
418      * @param quoted If <strong>true</strong> accept quoted strings.
419      */
420     String parseToken(boolean quoted) throws JasperException {
421         StringBuffer stringBuffer = new StringBuffer();
422         skipSpaces();
423         stringBuffer.setLength(0);
424 
425         if (!hasMoreInput()) {
426             return "";
427         }
428 
429         int ch = peekChar();
430 
431         if (quoted) {
432             if (ch == '"' || ch == '\'') {
433 
434                 char endQuote = ch == '"' ? '"' : '\'';
435                 // Consume the open quote: 
436                 ch = nextChar();
437                 for (ch = nextChar(); ch != -1 && ch != endQuote;
438                      ch = nextChar()) {
439                     if (ch == '//')
440                         ch = nextChar();
441                     stringBuffer.append((char) ch);
442                 }
443                 // Check end of quote, skip closing quote:
444                 if (ch == -1) {
445                     err.jspError(mark(), "jsp.error.quotes.unterminated");
446                 }
447             } else {
448                 err.jspError(mark(), "jsp.error.attr.quoted");
449             }
450         } else {
451             if (!isDelimiter()) {
452                 // Read value until delimiter is found:
453                 do {
454                     ch = nextChar();
455                     // Take care of the quoting here.
456                     if (ch == '//') {
457                         if (peekChar() == '"' || peekChar() == '\'' ||
458                                 peekChar() == '>' || peekChar() == '%')
459                             ch = nextChar();
460                     }
461                     stringBuffer.append((char) ch);
462                 } while (!isDelimiter());
463             }
464         }
465 
466         return stringBuffer.toString();
467     }
468 
469     void setSingleFile(boolean val) {
470         singleFile = val;
471     }
472 
473 
474     /***
475      * Gets the URL for the given path name.
476      *
477      * @param path Path name
478      * @return URL for the given path name.
479      * @throws MalformedURLException if the path name is not given in
480      *                               the correct form
481      */
482     URL getResource(String path) throws MalformedURLException {
483         return context.getResource(path);
484     }
485 
486 
487     /***
488      * Parse utils - Is current character a token delimiter ?
489      * Delimiters are currently defined to be =, &gt;, &lt;, ", and ' or any
490      * any space character as defined by <code>isSpace</code>.
491      *
492      * @return A boolean.
493      */
494     private boolean isDelimiter() throws JasperException {
495         if (!isSpace()) {
496             int ch = peekChar();
497             // Look for a single-char work delimiter:
498             if (ch == '=' || ch == '>' || ch == '"' || ch == '\''
499                     || ch == '/') {
500                 return true;
501             }
502             // Look for an end-of-comment or end-of-tag:                
503             if (ch == '-') {
504                 Mark mark = mark();
505                 if (((ch = nextChar()) == '>')
506                         || ((ch == '-') && (nextChar() == '>'))) {
507                     reset(mark);
508                     return true;
509                 } else {
510                     reset(mark);
511                     return false;
512                 }
513             }
514             return false;
515         } else {
516             return true;
517         }
518     }
519 
520     /***
521      * Register a new source file.
522      * This method is used to implement file inclusion. Each included file
523      * gets a unique identifier (which is the index in the array of source
524      * files).
525      *
526      * @return The index of the now registered file.
527      */
528     private int registerSourceFile(final String file) {
529         if (sourceFiles.contains(file)) {
530             return -1;
531         }
532 
533         sourceFiles.add(file);
534         this.size++;
535 
536         return sourceFiles.size() - 1;
537     }
538 
539 
540     /***
541      * Unregister the source file.
542      * This method is used to implement file inclusion. Each included file
543      * gets a uniq identifier (which is the index in the array of source
544      * files).
545      *
546      * @return The index of the now registered file.
547      */
548     private int unregisterSourceFile(final String file) {
549         if (!sourceFiles.contains(file)) {
550             return -1;
551         }
552 
553         sourceFiles.remove(file);
554         this.size--;
555         return sourceFiles.size() - 1;
556     }
557 
558     /***
559      * Push a file (and its associated Stream) on the file stack.  THe
560      * current position in the current file is remembered.
561      */
562     private void pushFile(String file, String encoding,
563                           InputStreamReader reader)
564             throws JasperException, FileNotFoundException {
565 
566         // Register the file
567         String longName = file;
568 
569         int fileid = registerSourceFile(longName);
570 
571         if (fileid == -1) {
572             // Bugzilla 37407: http://issues.apache.org/bugzilla/show_bug.cgi?id=37407
573             if (reader != null) {
574                 try {
575                     reader.close();
576                 } catch (Exception any) {
577                     if (log.isDebugEnabled()) {
578                         log.debug("Exception closing reader: ", any);
579                     }
580                 }
581             }
582 
583             err.jspError("jsp.error.file.already.registered", file);
584         }
585 
586         currFileId = fileid;
587 
588         try {
589             CharArrayWriter caw = new CharArrayWriter();
590             char buf[] = new char[1024];
591             for (int i = 0; (i = reader.read(buf)) != -1;)
592                 caw.write(buf, 0, i);
593             caw.close();
594             if (current == null) {
595                 current = new Mark(this, caw.toCharArray(), fileid,
596                         getFile(fileid), master, encoding);
597             } else {
598                 current.pushStream(caw.toCharArray(), fileid, getFile(fileid),
599                         longName, encoding);
600             }
601         } catch (Throwable ex) {
602             log.error("Exception parsing file ", ex);
603             // Pop state being constructed:
604             popFile();
605             err.jspError("jsp.error.file.cannot.read", file);
606         } finally {
607             if (reader != null) {
608                 try {
609                     reader.close();
610                 } catch (Exception any) {
611                     if (log.isDebugEnabled()) {
612                         log.debug("Exception closing reader: ", any);
613                     }
614                 }
615             }
616         }
617     }
618 
619     /***
620      * Pop a file from the file stack.  The field "current" is retored
621      * to the value to point to the previous files, if any, and is set
622      * to null otherwise.
623      *
624      * @return true is there is a previous file on the stack.
625      *         false otherwise.
626      */
627     private boolean popFile() throws JasperException {
628 
629         // Is stack created ? (will happen if the Jsp file we're looking at is
630         // missing.
631         if (current == null || currFileId < 0) {
632             return false;
633         }
634 
635         // Restore parser state:
636         String fName = getFile(currFileId);
637         currFileId = unregisterSourceFile(fName);
638         if (currFileId < -1) {
639             err.jspError("jsp.error.file.not.registered", fName);
640         }
641 
642         Mark previous = current.popStream();
643         if (previous != null) {
644             master = current.baseDir;
645             current = previous;
646             return true;
647         }
648         // Note that although the current file is undefined here, "current"
649         // is not set to null just for convience, for it maybe used to
650         // set the current (undefined) position.
651         return false;
652     }
653 }
654