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