001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017:
018: package org.apache.jasper.compiler;
019:
020: import java.io.CharArrayWriter;
021: import java.io.FileNotFoundException;
022: import java.io.IOException;
023: import java.io.InputStreamReader;
024: import java.util.List;
025: import java.util.Vector;
026: import java.util.jar.JarFile;
027: import java.net.URL;
028: import java.net.MalformedURLException;
029:
030: import org.apache.jasper.JasperException;
031: import org.apache.jasper.JspCompilationContext;
032: import org.apache.juli.logging.Log;
033: import org.apache.juli.logging.LogFactory;
034:
035: /**
036: * JspReader is an input buffer for the JSP parser. It should allow
037: * unlimited lookahead and pushback. It also has a bunch of parsing
038: * utility methods for understanding htmlesque thingies.
039: *
040: * @author Anil K. Vijendran
041: * @author Anselm Baird-Smith
042: * @author Harish Prabandham
043: * @author Rajiv Mordani
044: * @author Mandar Raje
045: * @author Danno Ferrin
046: * @author Kin-man Chung
047: * @author Shawn Bayern
048: * @author Mark Roth
049: */
050:
051: class JspReader {
052:
053: /**
054: * Logger.
055: */
056: private Log log = LogFactory.getLog(JspReader.class);
057:
058: /**
059: * The current spot in the file.
060: */
061: private Mark current;
062:
063: /**
064: * What is this?
065: */
066: private String master;
067:
068: /**
069: * The list of source files.
070: */
071: private List sourceFiles;
072:
073: /**
074: * The current file ID (-1 indicates an error or no file).
075: */
076: private int currFileId;
077:
078: /**
079: * Seems redundant.
080: */
081: private int size;
082:
083: /**
084: * The compilation context.
085: */
086: private JspCompilationContext context;
087:
088: /**
089: * The Jasper error dispatcher.
090: */
091: private ErrorDispatcher err;
092:
093: /**
094: * Set to true when using the JspReader on a single file where we read up
095: * to the end and reset to the beginning many times.
096: * (as in ParserController.figureOutJspDocument()).
097: */
098: private boolean singleFile;
099:
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, String fname,
113: String encoding, JarFile jarFile, ErrorDispatcher err)
114: throws JasperException, FileNotFoundException, IOException {
115:
116: this (ctxt, fname, encoding, JspUtil.getReader(fname, encoding,
117: jarFile, ctxt, err), err);
118: }
119:
120: /**
121: * Constructor: same as above constructor but with initialized reader
122: * to the file given.
123: */
124: public JspReader(JspCompilationContext ctxt, String fname,
125: String encoding, InputStreamReader reader,
126: ErrorDispatcher err) throws JasperException,
127: FileNotFoundException {
128:
129: this .context = ctxt;
130: this .err = err;
131: sourceFiles = new Vector();
132: currFileId = 0;
133: size = 0;
134: singleFile = false;
135: pushFile(fname, encoding, reader);
136: }
137:
138: /**
139: * @return JSP compilation context with which this JspReader is
140: * associated
141: */
142: JspCompilationContext getJspCompilationContext() {
143: return context;
144: }
145:
146: /**
147: * Returns the file at the given position in the list.
148: *
149: * @param fileid The file position in the list
150: * @return The file at that position, if found, null otherwise
151: */
152: String getFile(final int fileid) {
153: return (String) sourceFiles.get(fileid);
154: }
155:
156: /**
157: * Checks if the current file has more input.
158: *
159: * @return True if more reading is possible
160: * @throws JasperException if an error occurs
161: */
162: boolean hasMoreInput() throws JasperException {
163: if (current.cursor >= current.stream.length) {
164: if (singleFile)
165: return false;
166: while (popFile()) {
167: if (current.cursor < current.stream.length)
168: return true;
169: }
170: return false;
171: }
172: return true;
173: }
174:
175: int nextChar() throws JasperException {
176: if (!hasMoreInput())
177: return -1;
178:
179: int ch = current.stream[current.cursor];
180:
181: current.cursor++;
182:
183: if (ch == '\n') {
184: current.line++;
185: current.col = 0;
186: } else {
187: current.col++;
188: }
189: return ch;
190: }
191:
192: /**
193: * Back up the current cursor by one char, assumes current.cursor > 0,
194: * and that the char to be pushed back is not '\n'.
195: */
196: void pushChar() {
197: current.cursor--;
198: current.col--;
199: }
200:
201: String getText(Mark start, Mark stop) throws JasperException {
202: Mark oldstart = mark();
203: reset(start);
204: CharArrayWriter caw = new CharArrayWriter();
205: while (!stop.equals(mark()))
206: caw.write(nextChar());
207: caw.close();
208: reset(oldstart);
209: return caw.toString();
210: }
211:
212: int peekChar() throws JasperException {
213: if (!hasMoreInput())
214: return -1;
215: return current.stream[current.cursor];
216: }
217:
218: Mark mark() {
219: return new Mark(current);
220: }
221:
222: void reset(Mark mark) {
223: current = new Mark(mark);
224: }
225:
226: boolean matchesIgnoreCase(String string) throws JasperException {
227: Mark mark = mark();
228: int ch = 0;
229: int i = 0;
230: do {
231: ch = nextChar();
232: if (Character.toLowerCase((char) ch) != string.charAt(i++)) {
233: reset(mark);
234: return false;
235: }
236: } while (i < string.length());
237: reset(mark);
238: return true;
239: }
240:
241: /**
242: * search the stream for a match to a string
243: * @param string The string to match
244: * @return <strong>true</strong> is one is found, the current position
245: * in stream is positioned after the search string, <strong>
246: * false</strong> otherwise, position in stream unchanged.
247: */
248: boolean matches(String string) throws JasperException {
249: Mark mark = mark();
250: int ch = 0;
251: int i = 0;
252: do {
253: ch = nextChar();
254: if (((char) ch) != string.charAt(i++)) {
255: reset(mark);
256: return false;
257: }
258: } while (i < string.length());
259: return true;
260: }
261:
262: boolean matchesETag(String tagName) throws JasperException {
263: Mark mark = mark();
264:
265: if (!matches("</" + tagName))
266: return false;
267: skipSpaces();
268: if (nextChar() == '>')
269: return true;
270:
271: reset(mark);
272: return false;
273: }
274:
275: boolean matchesETagWithoutLessThan(String tagName)
276: throws JasperException {
277: Mark mark = mark();
278:
279: if (!matches("/" + tagName))
280: return false;
281: skipSpaces();
282: if (nextChar() == '>')
283: return true;
284:
285: reset(mark);
286: return false;
287: }
288:
289: /**
290: * Looks ahead to see if there are optional spaces followed by
291: * the given String. If so, true is returned and those spaces and
292: * characters are skipped. If not, false is returned and the
293: * position is restored to where we were before.
294: */
295: boolean matchesOptionalSpacesFollowedBy(String s)
296: throws JasperException {
297: Mark mark = mark();
298:
299: skipSpaces();
300: boolean result = matches(s);
301: if (!result) {
302: reset(mark);
303: }
304:
305: return result;
306: }
307:
308: int skipSpaces() throws JasperException {
309: int i = 0;
310: while (hasMoreInput() && isSpace()) {
311: i++;
312: nextChar();
313: }
314: return i;
315: }
316:
317: /**
318: * Skip until the given string is matched in the stream.
319: * When returned, the context is positioned past the end of the match.
320: *
321: * @param s The String to match.
322: * @return A non-null <code>Mark</code> instance (positioned immediately
323: * before the search string) if found, <strong>null</strong>
324: * otherwise.
325: */
326: Mark skipUntil(String limit) throws JasperException {
327: Mark ret = null;
328: int limlen = limit.length();
329: int ch;
330:
331: skip: for (ret = mark(), ch = nextChar(); ch != -1; ret = mark(), ch = nextChar()) {
332: if (ch == limit.charAt(0)) {
333: Mark restart = mark();
334: for (int i = 1; i < limlen; i++) {
335: if (peekChar() == limit.charAt(i))
336: nextChar();
337: else {
338: reset(restart);
339: continue skip;
340: }
341: }
342: return ret;
343: }
344: }
345: return null;
346: }
347:
348: /**
349: * Skip until the given string is matched in the stream, but ignoring
350: * chars initially escaped by a '\'.
351: * When returned, the context is positioned past the end of the match.
352: *
353: * @param s The String to match.
354: * @return A non-null <code>Mark</code> instance (positioned immediately
355: * before the search string) if found, <strong>null</strong>
356: * otherwise.
357: */
358: Mark skipUntilIgnoreEsc(String limit) throws JasperException {
359: Mark ret = null;
360: int limlen = limit.length();
361: int ch;
362: int prev = 'x'; // Doesn't matter
363:
364: skip: for (ret = mark(), ch = nextChar(); ch != -1; ret = mark(), prev = ch, ch = nextChar()) {
365: if (ch == '\\' && prev == '\\') {
366: ch = 0; // Double \ is not an escape char anymore
367: } else if (ch == limit.charAt(0) && prev != '\\') {
368: for (int i = 1; i < limlen; i++) {
369: if (peekChar() == limit.charAt(i))
370: nextChar();
371: else
372: continue skip;
373: }
374: return ret;
375: }
376: }
377: return null;
378: }
379:
380: /**
381: * Skip until the given end tag is matched in the stream.
382: * When returned, the context is positioned past the end of the tag.
383: *
384: * @param tag The name of the tag whose ETag (</tag>) to match.
385: * @return A non-null <code>Mark</code> instance (positioned immediately
386: * before the ETag) if found, <strong>null</strong> otherwise.
387: */
388: Mark skipUntilETag(String tag) throws JasperException {
389: Mark ret = skipUntil("</" + tag);
390: if (ret != null) {
391: skipSpaces();
392: if (nextChar() != '>')
393: ret = null;
394: }
395: return ret;
396: }
397:
398: final boolean isSpace() throws JasperException {
399: // Note: If this logic changes, also update Node.TemplateText.rtrim()
400: return peekChar() <= ' ';
401: }
402:
403: /**
404: * Parse a space delimited token.
405: * If quoted the token will consume all characters up to a matching quote,
406: * otherwise, it consumes up to the first delimiter character.
407: *
408: * @param quoted If <strong>true</strong> accept quoted strings.
409: */
410: String parseToken(boolean quoted) throws JasperException {
411: StringBuffer stringBuffer = new StringBuffer();
412: skipSpaces();
413: stringBuffer.setLength(0);
414:
415: if (!hasMoreInput()) {
416: return "";
417: }
418:
419: int ch = peekChar();
420:
421: if (quoted) {
422: if (ch == '"' || ch == '\'') {
423:
424: char endQuote = ch == '"' ? '"' : '\'';
425: // Consume the open quote:
426: ch = nextChar();
427: for (ch = nextChar(); ch != -1 && ch != endQuote; ch = nextChar()) {
428: if (ch == '\\')
429: ch = nextChar();
430: stringBuffer.append((char) ch);
431: }
432: // Check end of quote, skip closing quote:
433: if (ch == -1) {
434: err.jspError(mark(),
435: "jsp.error.quotes.unterminated");
436: }
437: } else {
438: err.jspError(mark(), "jsp.error.attr.quoted");
439: }
440: } else {
441: if (!isDelimiter()) {
442: // Read value until delimiter is found:
443: do {
444: ch = nextChar();
445: // Take care of the quoting here.
446: if (ch == '\\') {
447: if (peekChar() == '"' || peekChar() == '\''
448: || peekChar() == '>'
449: || peekChar() == '%')
450: ch = nextChar();
451: }
452: stringBuffer.append((char) ch);
453: } while (!isDelimiter());
454: }
455: }
456:
457: return stringBuffer.toString();
458: }
459:
460: void setSingleFile(boolean val) {
461: singleFile = val;
462: }
463:
464: /**
465: * Gets the URL for the given path name.
466: *
467: * @param path Path name
468: *
469: * @return URL for the given path name.
470: *
471: * @exception MalformedURLException if the path name is not given in
472: * the correct form
473: */
474: URL getResource(String path) throws MalformedURLException {
475: return context.getResource(path);
476: }
477:
478: /**
479: * Parse utils - Is current character a token delimiter ?
480: * Delimiters are currently defined to be =, >, <, ", and ' or any
481: * any space character as defined by <code>isSpace</code>.
482: *
483: * @return A boolean.
484: */
485: private boolean isDelimiter() throws JasperException {
486: if (!isSpace()) {
487: int ch = peekChar();
488: // Look for a single-char work delimiter:
489: if (ch == '=' || ch == '>' || ch == '"' || ch == '\''
490: || ch == '/') {
491: return true;
492: }
493: // Look for an end-of-comment or end-of-tag:
494: if (ch == '-') {
495: Mark mark = mark();
496: if (((ch = nextChar()) == '>')
497: || ((ch == '-') && (nextChar() == '>'))) {
498: reset(mark);
499: return true;
500: } else {
501: reset(mark);
502: return false;
503: }
504: }
505: return false;
506: } else {
507: return true;
508: }
509: }
510:
511: /**
512: * Register a new source file.
513: * This method is used to implement file inclusion. Each included file
514: * gets a unique identifier (which is the index in the array of source
515: * files).
516: *
517: * @return The index of the now registered file.
518: */
519: private int registerSourceFile(final String file) {
520: if (sourceFiles.contains(file)) {
521: return -1;
522: }
523:
524: sourceFiles.add(file);
525: this .size++;
526:
527: return sourceFiles.size() - 1;
528: }
529:
530: /**
531: * Unregister the source file.
532: * This method is used to implement file inclusion. Each included file
533: * gets a uniq identifier (which is the index in the array of source
534: * files).
535: *
536: * @return The index of the now registered file.
537: */
538: private int unregisterSourceFile(final String file) {
539: if (!sourceFiles.contains(file)) {
540: return -1;
541: }
542:
543: sourceFiles.remove(file);
544: this .size--;
545: return sourceFiles.size() - 1;
546: }
547:
548: /**
549: * Push a file (and its associated Stream) on the file stack. THe
550: * current position in the current file is remembered.
551: */
552: private void pushFile(String file, String encoding,
553: InputStreamReader reader) throws JasperException,
554: FileNotFoundException {
555:
556: // Register the file
557: String longName = file;
558:
559: int fileid = registerSourceFile(longName);
560:
561: if (fileid == -1) {
562: // Bugzilla 37407: http://issues.apache.org/bugzilla/show_bug.cgi?id=37407
563: if (reader != null) {
564: try {
565: reader.close();
566: } catch (Exception any) {
567: if (log.isDebugEnabled()) {
568: log.debug("Exception closing reader: ", any);
569: }
570: }
571: }
572:
573: err.jspError("jsp.error.file.already.registered", file);
574: }
575:
576: currFileId = fileid;
577:
578: try {
579: CharArrayWriter caw = new CharArrayWriter();
580: char buf[] = new char[1024];
581: for (int i = 0; (i = reader.read(buf)) != -1;)
582: caw.write(buf, 0, i);
583: caw.close();
584: if (current == null) {
585: current = new Mark(this , caw.toCharArray(), fileid,
586: getFile(fileid), master, encoding);
587: } else {
588: current.pushStream(caw.toCharArray(), fileid,
589: getFile(fileid), longName, encoding);
590: }
591: } catch (Throwable ex) {
592: log.error("Exception parsing file ", ex);
593: // Pop state being constructed:
594: popFile();
595: err.jspError("jsp.error.file.cannot.read", file);
596: } finally {
597: if (reader != null) {
598: try {
599: reader.close();
600: } catch (Exception any) {
601: if (log.isDebugEnabled()) {
602: log.debug("Exception closing reader: ", any);
603: }
604: }
605: }
606: }
607: }
608:
609: /**
610: * Pop a file from the file stack. The field "current" is retored
611: * to the value to point to the previous files, if any, and is set
612: * to null otherwise.
613: * @return true is there is a previous file on the stack.
614: * false otherwise.
615: */
616: private boolean popFile() throws JasperException {
617:
618: // Is stack created ? (will happen if the Jsp file we're looking at is
619: // missing.
620: if (current == null || currFileId < 0) {
621: return false;
622: }
623:
624: // Restore parser state:
625: String fName = getFile(currFileId);
626: currFileId = unregisterSourceFile(fName);
627: if (currFileId < -1) {
628: err.jspError("jsp.error.file.not.registered", fName);
629: }
630:
631: Mark previous = current.popStream();
632: if (previous != null) {
633: master = current.baseDir;
634: current = previous;
635: return true;
636: }
637: // Note that although the current file is undefined here, "current"
638: // is not set to null just for convience, for it maybe used to
639: // set the current (undefined) position.
640: return false;
641: }
642: }
|