1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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';
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;
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
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
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
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
453 do {
454 ch = nextChar();
455
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 =, >, <, ", 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
498 if (ch == '=' || ch == '>' || ch == '"' || ch == '\''
499 || ch == '/') {
500 return true;
501 }
502
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
567 String longName = file;
568
569 int fileid = registerSourceFile(longName);
570
571 if (fileid == -1) {
572
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
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
630
631 if (current == null || currFileId < 0) {
632 return false;
633 }
634
635
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
649
650
651 return false;
652 }
653 }
654