644 lines
19 KiB
Java
644 lines
19 KiB
Java
/*
|
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
|
* contributor license agreements. See the NOTICE file distributed with
|
|
* this work for additional information regarding copyright ownership.
|
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
|
* (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
package org.apache.jasper.compiler;
|
|
|
|
import java.io.CharArrayWriter;
|
|
import java.io.FileNotFoundException;
|
|
import java.io.IOException;
|
|
import java.io.InputStreamReader;
|
|
|
|
import org.apache.jasper.JasperException;
|
|
import org.apache.jasper.JspCompilationContext;
|
|
import org.apache.jasper.runtime.ExceptionUtils;
|
|
import org.apache.juli.logging.Log;
|
|
import org.apache.juli.logging.LogFactory;
|
|
import org.apache.tomcat.Jar;
|
|
|
|
/**
|
|
* JspReader is an input buffer for the JSP parser. It should allow
|
|
* unlimited lookahead and pushback. It also has a bunch of parsing
|
|
* utility methods for understanding htmlesque thingies.
|
|
*
|
|
* @author Anil K. Vijendran
|
|
* @author Anselm Baird-Smith
|
|
* @author Harish Prabandham
|
|
* @author Rajiv Mordani
|
|
* @author Mandar Raje
|
|
* @author Danno Ferrin
|
|
* @author Kin-man Chung
|
|
* @author Shawn Bayern
|
|
* @author Mark Roth
|
|
*/
|
|
|
|
class JspReader {
|
|
|
|
/**
|
|
* Logger.
|
|
*/
|
|
private final Log log = LogFactory.getLog(JspReader.class); // must not be static
|
|
|
|
/**
|
|
* The current spot in the file.
|
|
*/
|
|
private Mark current;
|
|
|
|
/**
|
|
* The compilation context.
|
|
*/
|
|
private final JspCompilationContext context;
|
|
|
|
/**
|
|
* The Jasper error dispatcher.
|
|
*/
|
|
private final ErrorDispatcher err;
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @param ctxt The compilation context
|
|
* @param fname The file name
|
|
* @param encoding The file encoding
|
|
* @param jar ?
|
|
* @param err The error dispatcher
|
|
* @throws JasperException If a Jasper-internal error occurs
|
|
* @throws FileNotFoundException If the JSP file is not found (or is unreadable)
|
|
* @throws IOException If an IO-level error occurs, e.g. reading the file
|
|
*/
|
|
public JspReader(JspCompilationContext ctxt,
|
|
String fname,
|
|
String encoding,
|
|
Jar jar,
|
|
ErrorDispatcher err)
|
|
throws JasperException, FileNotFoundException, IOException {
|
|
|
|
this(ctxt, fname, JspUtil.getReader(fname, encoding, jar, ctxt, err),
|
|
err);
|
|
}
|
|
|
|
/**
|
|
* Constructor: same as above constructor but with initialized reader
|
|
* to the file given.
|
|
*
|
|
* @param ctxt The compilation context
|
|
* @param fname The file name
|
|
* @param reader A reader for the JSP source file
|
|
* @param err The error dispatcher
|
|
*
|
|
* @throws JasperException If an error occurs parsing the JSP file
|
|
*/
|
|
public JspReader(JspCompilationContext ctxt,
|
|
String fname,
|
|
InputStreamReader reader,
|
|
ErrorDispatcher err)
|
|
throws JasperException {
|
|
|
|
this.context = ctxt;
|
|
this.err = err;
|
|
|
|
try {
|
|
CharArrayWriter caw = new CharArrayWriter();
|
|
char buf[] = new char[1024];
|
|
for (int i = 0 ; (i = reader.read(buf)) != -1 ;)
|
|
caw.write(buf, 0, i);
|
|
caw.close();
|
|
current = new Mark(this, caw.toCharArray(), fname);
|
|
} catch (Throwable ex) {
|
|
ExceptionUtils.handleThrowable(ex);
|
|
log.error("Exception parsing file ", ex);
|
|
err.jspError("jsp.error.file.cannot.read", fname);
|
|
} finally {
|
|
if (reader != null) {
|
|
try {
|
|
reader.close();
|
|
} catch (Exception any) {
|
|
if(log.isDebugEnabled()) {
|
|
log.debug("Exception closing reader: ", any);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @return JSP compilation context with which this JspReader is
|
|
* associated
|
|
*/
|
|
JspCompilationContext getJspCompilationContext() {
|
|
return context;
|
|
}
|
|
|
|
/**
|
|
* Checks if the current file has more input.
|
|
*
|
|
* @return True if more reading is possible
|
|
*/
|
|
boolean hasMoreInput() {
|
|
return current.cursor < current.stream.length;
|
|
}
|
|
|
|
int nextChar() {
|
|
if (!hasMoreInput())
|
|
return -1;
|
|
|
|
int ch = current.stream[current.cursor];
|
|
|
|
current.cursor++;
|
|
|
|
if (ch == '\n') {
|
|
current.line++;
|
|
current.col = 0;
|
|
} else {
|
|
current.col++;
|
|
}
|
|
return ch;
|
|
}
|
|
|
|
/**
|
|
* A faster approach than calling {@link #mark()} & {@link #nextChar()}.
|
|
* However, this approach is only safe if the mark is only used within the
|
|
* JspReader.
|
|
*/
|
|
private int nextChar(Mark mark) {
|
|
if (!hasMoreInput()) {
|
|
return -1;
|
|
}
|
|
|
|
int ch = current.stream[current.cursor];
|
|
|
|
mark.init(current, true);
|
|
|
|
current.cursor++;
|
|
|
|
if (ch == '\n') {
|
|
current.line++;
|
|
current.col = 0;
|
|
} else {
|
|
current.col++;
|
|
}
|
|
return ch;
|
|
}
|
|
|
|
/**
|
|
* Search the given character, If it was found, then mark the current cursor
|
|
* and the cursor point to next character.
|
|
*/
|
|
private Boolean indexOf(char c, Mark mark) {
|
|
if (!hasMoreInput())
|
|
return null;
|
|
|
|
int end = current.stream.length;
|
|
int ch;
|
|
int line = current.line;
|
|
int col = current.col;
|
|
int i = current.cursor;
|
|
for(; i < end; i ++) {
|
|
ch = current.stream[i];
|
|
|
|
if (ch == c) {
|
|
mark.update(i, line, col);
|
|
}
|
|
if (ch == '\n') {
|
|
line++;
|
|
col = 0;
|
|
} else {
|
|
col++;
|
|
}
|
|
if (ch == c) {
|
|
current.update(i+1, line, col);
|
|
return Boolean.TRUE;
|
|
}
|
|
}
|
|
current.update(i, line, col);
|
|
return Boolean.FALSE;
|
|
}
|
|
|
|
/**
|
|
* Back up the current cursor by one char, assumes current.cursor > 0,
|
|
* and that the char to be pushed back is not '\n'.
|
|
*/
|
|
void pushChar() {
|
|
current.cursor--;
|
|
current.col--;
|
|
}
|
|
|
|
String getText(Mark start, Mark stop) {
|
|
Mark oldstart = mark();
|
|
reset(start);
|
|
CharArrayWriter caw = new CharArrayWriter();
|
|
while (!markEquals(stop)) {
|
|
caw.write(nextChar());
|
|
}
|
|
caw.close();
|
|
setCurrent(oldstart);
|
|
return caw.toString();
|
|
}
|
|
|
|
/**
|
|
* Read ahead one character without moving the cursor.
|
|
*
|
|
* @return The next character or -1 if no further input is available
|
|
*/
|
|
int peekChar() {
|
|
return peekChar(0);
|
|
}
|
|
|
|
/**
|
|
* Read ahead the given number of characters without moving the cursor.
|
|
*
|
|
* @param readAhead The number of characters to read ahead. NOTE: This is
|
|
* zero based.
|
|
*
|
|
* @return The requested character or -1 if the end of the input is reached
|
|
* first
|
|
*/
|
|
int peekChar(int readAhead) {
|
|
int target = current.cursor + readAhead;
|
|
if (target < current.stream.length) {
|
|
return current.stream[target];
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
Mark mark() {
|
|
return new Mark(current);
|
|
}
|
|
|
|
|
|
/**
|
|
* This method avoids a call to {@link #mark()} when doing comparison.
|
|
*/
|
|
private boolean markEquals(Mark another) {
|
|
return another.equals(current);
|
|
}
|
|
|
|
void reset(Mark mark) {
|
|
current = new Mark(mark);
|
|
}
|
|
|
|
/**
|
|
* Similar to {@link #reset(Mark)} but no new Mark will be created.
|
|
* Therefore, the parameter mark must NOT be used in other places.
|
|
*/
|
|
private void setCurrent(Mark mark) {
|
|
current = mark;
|
|
}
|
|
|
|
/**
|
|
* search the stream for a match to a string
|
|
* @param string The string to match
|
|
* @return <strong>true</strong> is one is found, the current position
|
|
* in stream is positioned after the search string, <strong>
|
|
* false</strong> otherwise, position in stream unchanged.
|
|
*/
|
|
boolean matches(String string) {
|
|
int len = string.length();
|
|
int cursor = current.cursor;
|
|
int streamSize = current.stream.length;
|
|
if (cursor + len < streamSize) { //Try to scan in memory
|
|
int line = current.line;
|
|
int col = current.col;
|
|
int ch;
|
|
int i = 0;
|
|
for(; i < len; i ++) {
|
|
ch = current.stream[i+cursor];
|
|
if (string.charAt(i) != ch) {
|
|
return false;
|
|
}
|
|
if (ch == '\n') {
|
|
line ++;
|
|
col = 0;
|
|
} else {
|
|
col++;
|
|
}
|
|
}
|
|
current.update(i+cursor, line, col);
|
|
} else {
|
|
Mark mark = mark();
|
|
int ch = 0;
|
|
int i = 0;
|
|
do {
|
|
ch = nextChar();
|
|
if (((char) ch) != string.charAt(i++)) {
|
|
setCurrent(mark);
|
|
return false;
|
|
}
|
|
} while (i < len);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
boolean matchesETag(String tagName) {
|
|
Mark mark = mark();
|
|
|
|
if (!matches("</" + tagName))
|
|
return false;
|
|
skipSpaces();
|
|
if (nextChar() == '>')
|
|
return true;
|
|
|
|
setCurrent(mark);
|
|
return false;
|
|
}
|
|
|
|
boolean matchesETagWithoutLessThan(String tagName) {
|
|
Mark mark = mark();
|
|
|
|
if (!matches("/" + tagName))
|
|
return false;
|
|
skipSpaces();
|
|
if (nextChar() == '>')
|
|
return true;
|
|
|
|
setCurrent(mark);
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Looks ahead to see if there are optional spaces followed by
|
|
* the given String. If so, true is returned and those spaces and
|
|
* characters are skipped. If not, false is returned and the
|
|
* position is restored to where we were before.
|
|
*/
|
|
boolean matchesOptionalSpacesFollowedBy(String s) {
|
|
Mark mark = mark();
|
|
|
|
skipSpaces();
|
|
boolean result = matches( s );
|
|
if( !result ) {
|
|
setCurrent(mark);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
int skipSpaces() {
|
|
int i = 0;
|
|
while (hasMoreInput() && isSpace()) {
|
|
i++;
|
|
nextChar();
|
|
}
|
|
return i;
|
|
}
|
|
|
|
/**
|
|
* Skip until the given string is matched in the stream.
|
|
* When returned, the context is positioned past the end of the match.
|
|
*
|
|
* @param limit The String to match.
|
|
* @return A non-null <code>Mark</code> instance (positioned immediately
|
|
* before the search string) if found, <strong>null</strong>
|
|
* otherwise.
|
|
*/
|
|
Mark skipUntil(String limit) {
|
|
Mark ret = mark();
|
|
int limlen = limit.length();
|
|
char firstChar = limit.charAt(0);
|
|
Boolean result = null;
|
|
Mark restart = null;
|
|
|
|
skip:
|
|
while((result = indexOf(firstChar, ret)) != null) {
|
|
if (result.booleanValue()) {
|
|
if (restart != null) {
|
|
restart.init(current, true);
|
|
} else {
|
|
restart = mark();
|
|
}
|
|
for (int i = 1 ; i < limlen ; i++) {
|
|
if (peekChar() == limit.charAt(i)) {
|
|
nextChar();
|
|
} else {
|
|
current.init(restart, true);
|
|
continue skip;
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Skip until the given string is matched in the stream, but ignoring
|
|
* chars initially escaped by a '\' and any EL expressions.
|
|
* When returned, the context is positioned past the end of the match.
|
|
*
|
|
* @param limit The String to match.
|
|
* @param ignoreEL <code>true</code> if something that looks like EL should
|
|
* not be treated as EL.
|
|
* @return A non-null <code>Mark</code> instance (positioned immediately
|
|
* before the search string) if found, <strong>null</strong>
|
|
* otherwise.
|
|
*/
|
|
Mark skipUntilIgnoreEsc(String limit, boolean ignoreEL) {
|
|
Mark ret = mark();
|
|
int limlen = limit.length();
|
|
int ch;
|
|
int prev = 'x'; // Doesn't matter
|
|
char firstChar = limit.charAt(0);
|
|
skip:
|
|
for (ch = nextChar(ret) ; ch != -1 ; prev = ch, ch = nextChar(ret)) {
|
|
if (ch == '\\' && prev == '\\') {
|
|
ch = 0; // Double \ is not an escape char anymore
|
|
} else if (prev == '\\') {
|
|
continue;
|
|
} else if (!ignoreEL && (ch == '$' || ch == '#') && peekChar() == '{' ) {
|
|
// Move beyond the '{'
|
|
nextChar();
|
|
skipELExpression();
|
|
} else if (ch == firstChar) {
|
|
for (int i = 1 ; i < limlen ; i++) {
|
|
if (peekChar() == limit.charAt(i))
|
|
nextChar();
|
|
else
|
|
continue skip;
|
|
}
|
|
return ret;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Skip until the given end tag is matched in the stream.
|
|
* When returned, the context is positioned past the end of the tag.
|
|
*
|
|
* @param tag The name of the tag whose ETag (</tag>) to match.
|
|
* @return A non-null <code>Mark</code> instance (positioned immediately
|
|
* before the ETag) if found, <strong>null</strong> otherwise.
|
|
*/
|
|
Mark skipUntilETag(String tag) {
|
|
Mark ret = skipUntil("</" + tag);
|
|
if (ret != null) {
|
|
skipSpaces();
|
|
if (nextChar() != '>')
|
|
ret = null;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Parse ELExpressionBody that is a body of ${} or #{} expression. Initial
|
|
* reader position is expected to be just after '${' or '#{' characters.
|
|
* <p>
|
|
* In case of success, this method returns <code>Mark</code> for the last
|
|
* character before the terminating '}' and reader is positioned just after
|
|
* the '}' character. If no terminating '}' is encountered, this method
|
|
* returns <code>null</code>.
|
|
* <p>
|
|
* Starting with EL 3.0, nested paired {}s are supported.
|
|
*
|
|
* @return Mark for the last character of EL expression or <code>null</code>
|
|
*/
|
|
Mark skipELExpression() {
|
|
// ELExpressionBody.
|
|
// Starts with "#{" or "${". Ends with "}".
|
|
// May contain quoted "{", "}", '{', or '}' and nested "{...}"
|
|
Mark last = mark();
|
|
boolean singleQuoted = false;
|
|
boolean doubleQuoted = false;
|
|
int nesting = 0;
|
|
int currentChar;
|
|
do {
|
|
currentChar = nextChar(last);
|
|
while (currentChar == '\\' && (singleQuoted || doubleQuoted)) {
|
|
// skip character following '\' within quotes
|
|
// No need to update 'last', as neither of these characters
|
|
// can be the closing '}'.
|
|
nextChar();
|
|
currentChar = nextChar();
|
|
}
|
|
if (currentChar == -1) {
|
|
return null;
|
|
}
|
|
if (currentChar == '"' && !singleQuoted) {
|
|
doubleQuoted = !doubleQuoted;
|
|
} else if (currentChar == '\'' && !doubleQuoted) {
|
|
singleQuoted = !singleQuoted;
|
|
} else if (currentChar == '{' && !doubleQuoted && !singleQuoted) {
|
|
nesting++;
|
|
} else if (currentChar =='}' && !doubleQuoted && !singleQuoted) {
|
|
// Note: This also matches the terminating '}' at which point
|
|
// nesting will be set to -1 - hence the test for
|
|
// while (currentChar != '}' || nesting > -1 ||...) below
|
|
// to continue the loop until the final '}' is detected
|
|
nesting--;
|
|
}
|
|
} while (currentChar != '}' || singleQuoted || doubleQuoted || nesting > -1);
|
|
|
|
return last;
|
|
}
|
|
|
|
final boolean isSpace() {
|
|
// Note: If this logic changes, also update Node.TemplateText.rtrim()
|
|
return peekChar() <= ' ';
|
|
}
|
|
|
|
/**
|
|
* Parse a space delimited token.
|
|
* If quoted the token will consume all characters up to a matching quote,
|
|
* otherwise, it consumes up to the first delimiter character.
|
|
*
|
|
* @param quoted If <strong>true</strong> accept quoted strings.
|
|
*/
|
|
String parseToken(boolean quoted) throws JasperException {
|
|
StringBuilder StringBuilder = new StringBuilder();
|
|
skipSpaces();
|
|
StringBuilder.setLength(0);
|
|
|
|
if (!hasMoreInput()) {
|
|
return "";
|
|
}
|
|
|
|
int ch = peekChar();
|
|
|
|
if (quoted) {
|
|
if (ch == '"' || ch == '\'') {
|
|
|
|
char endQuote = ch == '"' ? '"' : '\'';
|
|
// Consume the open quote:
|
|
ch = nextChar();
|
|
for (ch = nextChar(); ch != -1 && ch != endQuote;
|
|
ch = nextChar()) {
|
|
if (ch == '\\')
|
|
ch = nextChar();
|
|
StringBuilder.append((char) ch);
|
|
}
|
|
// Check end of quote, skip closing quote:
|
|
if (ch == -1) {
|
|
err.jspError(mark(), "jsp.error.quotes.unterminated");
|
|
}
|
|
} else {
|
|
err.jspError(mark(), "jsp.error.attr.quoted");
|
|
}
|
|
} else {
|
|
if (!isDelimiter()) {
|
|
// Read value until delimiter is found:
|
|
do {
|
|
ch = nextChar();
|
|
// Take care of the quoting here.
|
|
if (ch == '\\') {
|
|
if (peekChar() == '"' || peekChar() == '\'' ||
|
|
peekChar() == '>' || peekChar() == '%')
|
|
ch = nextChar();
|
|
}
|
|
StringBuilder.append((char) ch);
|
|
} while (!isDelimiter());
|
|
}
|
|
}
|
|
|
|
return StringBuilder.toString();
|
|
}
|
|
|
|
|
|
/**
|
|
* Parse utils - Is current character a token delimiter ?
|
|
* Delimiters are currently defined to be =, >, <, ", and ' or any
|
|
* any space character as defined by <code>isSpace</code>.
|
|
*
|
|
* @return A boolean.
|
|
*/
|
|
private boolean isDelimiter() {
|
|
if (! isSpace()) {
|
|
int ch = peekChar();
|
|
// Look for a single-char work delimiter:
|
|
if (ch == '=' || ch == '>' || ch == '"' || ch == '\''
|
|
|| ch == '/') {
|
|
return true;
|
|
}
|
|
// Look for an end-of-comment or end-of-tag:
|
|
if (ch == '-') {
|
|
Mark mark = mark();
|
|
if (((ch = nextChar()) == '>')
|
|
|| ((ch == '-') && (nextChar() == '>'))) {
|
|
setCurrent(mark);
|
|
return true;
|
|
} else {
|
|
setCurrent(mark);
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|