589 lines
17 KiB
Java
589 lines
17 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 org.apache.jasper.JasperException;
|
|
import org.apache.jasper.compiler.ELNode.ELText;
|
|
import org.apache.jasper.compiler.ELNode.Function;
|
|
import org.apache.jasper.compiler.ELNode.Root;
|
|
import org.apache.jasper.compiler.ELNode.Text;
|
|
|
|
/**
|
|
* This class implements a parser for EL expressions.
|
|
*
|
|
* It takes strings of the form xxx${..}yyy${..}zzz etc, and turn it into a
|
|
* ELNode.Nodes.
|
|
*
|
|
* Currently, it only handles text outside ${..} and functions in ${ ..}.
|
|
*
|
|
* @author Kin-man Chung
|
|
*/
|
|
|
|
public class ELParser {
|
|
|
|
private Token curToken; // current token
|
|
private Token prevToken; // previous token
|
|
private String whiteSpace = "";
|
|
|
|
private final ELNode.Nodes expr;
|
|
|
|
private ELNode.Nodes ELexpr;
|
|
|
|
private int index; // Current index of the expression
|
|
|
|
private final String expression; // The EL expression
|
|
|
|
private char type;
|
|
|
|
private final boolean isDeferredSyntaxAllowedAsLiteral;
|
|
|
|
private static final String reservedWords[] = { "and", "div", "empty",
|
|
"eq", "false", "ge", "gt", "instanceof", "le", "lt", "mod", "ne",
|
|
"not", "null", "or", "true" };
|
|
|
|
public ELParser(String expression, boolean isDeferredSyntaxAllowedAsLiteral) {
|
|
index = 0;
|
|
this.expression = expression;
|
|
this.isDeferredSyntaxAllowedAsLiteral = isDeferredSyntaxAllowedAsLiteral;
|
|
expr = new ELNode.Nodes();
|
|
}
|
|
|
|
/**
|
|
* Parse an EL expression
|
|
*
|
|
* @param expression
|
|
* The input expression string of the form Char* ('${' Char*
|
|
* '}')* Char*
|
|
* @param isDeferredSyntaxAllowedAsLiteral
|
|
* Are deferred expressions treated as literals?
|
|
* @return Parsed EL expression in ELNode.Nodes
|
|
*/
|
|
public static ELNode.Nodes parse(String expression,
|
|
boolean isDeferredSyntaxAllowedAsLiteral) {
|
|
ELParser parser = new ELParser(expression,
|
|
isDeferredSyntaxAllowedAsLiteral);
|
|
while (parser.hasNextChar()) {
|
|
String text = parser.skipUntilEL();
|
|
if (text.length() > 0) {
|
|
parser.expr.add(new ELNode.Text(text));
|
|
}
|
|
ELNode.Nodes elexpr = parser.parseEL();
|
|
if (!elexpr.isEmpty()) {
|
|
parser.expr.add(new ELNode.Root(elexpr, parser.type));
|
|
}
|
|
}
|
|
return parser.expr;
|
|
}
|
|
|
|
/**
|
|
* Parse an EL expression string '${...}'. Currently only separates the EL
|
|
* into functions and everything else.
|
|
*
|
|
* @return An ELNode.Nodes representing the EL expression
|
|
*
|
|
* Note: This cannot be refactored to use the standard EL implementation as
|
|
* the EL API does not provide the level of access required to the
|
|
* parsed expression.
|
|
*/
|
|
private ELNode.Nodes parseEL() {
|
|
|
|
StringBuilder buf = new StringBuilder();
|
|
ELexpr = new ELNode.Nodes();
|
|
curToken = null;
|
|
prevToken = null;
|
|
int openBraces = 0;
|
|
while (hasNext()) {
|
|
curToken = nextToken();
|
|
if (curToken instanceof Char) {
|
|
if (curToken.toChar() == '}') {
|
|
openBraces--;
|
|
if (openBraces < 0) {
|
|
break;
|
|
}
|
|
} else if (curToken.toChar() == '{') {
|
|
openBraces++;
|
|
}
|
|
buf.append(curToken.toString());
|
|
} else {
|
|
// Output whatever is in buffer
|
|
if (buf.length() > 0) {
|
|
ELexpr.add(new ELNode.ELText(buf.toString()));
|
|
buf.setLength(0);
|
|
}
|
|
if (!parseFunction()) {
|
|
ELexpr.add(new ELNode.ELText(curToken.toString()));
|
|
}
|
|
}
|
|
}
|
|
if (curToken != null) {
|
|
buf.append(curToken.getWhiteSpace());
|
|
}
|
|
if (buf.length() > 0) {
|
|
ELexpr.add(new ELNode.ELText(buf.toString()));
|
|
}
|
|
|
|
return ELexpr;
|
|
}
|
|
|
|
/**
|
|
* Parse for a function FunctionInvokation ::= (identifier ':')? identifier
|
|
* '(' (Expression (,Expression)*)? ')' Note: currently we don't parse
|
|
* arguments
|
|
*/
|
|
private boolean parseFunction() {
|
|
if (!(curToken instanceof Id) || isELReserved(curToken.toTrimmedString()) ||
|
|
prevToken instanceof Char && prevToken.toChar() == '.') {
|
|
return false;
|
|
}
|
|
String s1 = null; // Function prefix
|
|
String s2 = curToken.toTrimmedString(); // Function name
|
|
int start = index - curToken.toString().length();
|
|
Token original = curToken;
|
|
if (hasNext()) {
|
|
int mark = getIndex() - whiteSpace.length();
|
|
curToken = nextToken();
|
|
if (curToken.toChar() == ':') {
|
|
if (hasNext()) {
|
|
Token t2 = nextToken();
|
|
if (t2 instanceof Id) {
|
|
s1 = s2;
|
|
s2 = t2.toTrimmedString();
|
|
if (hasNext()) {
|
|
curToken = nextToken();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (curToken.toChar() == '(') {
|
|
ELexpr.add(new ELNode.Function(s1, s2, expression.substring(start, index - 1)));
|
|
return true;
|
|
}
|
|
curToken = original;
|
|
setIndex(mark);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Test if an id is a reserved word in EL
|
|
*/
|
|
private boolean isELReserved(String id) {
|
|
int i = 0;
|
|
int j = reservedWords.length;
|
|
while (i < j) {
|
|
int k = (i + j) >>> 1;
|
|
int result = reservedWords[k].compareTo(id);
|
|
if (result == 0) {
|
|
return true;
|
|
}
|
|
if (result < 0) {
|
|
i = k + 1;
|
|
} else {
|
|
j = k;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Skip until an EL expression ('${' || '#{') is reached, allowing escape
|
|
* sequences '\$' and '\#'.
|
|
*
|
|
* @return The text string up to the EL expression
|
|
*/
|
|
private String skipUntilEL() {
|
|
StringBuilder buf = new StringBuilder();
|
|
while (hasNextChar()) {
|
|
char ch = nextChar();
|
|
if (ch == '\\') {
|
|
// Is this the start of a "\$" or "\#" escape sequence?
|
|
char p0 = peek(0);
|
|
if (p0 == '$' || (p0 == '#' && !isDeferredSyntaxAllowedAsLiteral)) {
|
|
buf.append(nextChar());
|
|
} else {
|
|
buf.append(ch);
|
|
}
|
|
} else if ((ch == '$' || (ch == '#' && !isDeferredSyntaxAllowedAsLiteral)) &&
|
|
peek(0) == '{') {
|
|
this.type = ch;
|
|
nextChar();
|
|
break;
|
|
} else {
|
|
buf.append(ch);
|
|
}
|
|
}
|
|
return buf.toString();
|
|
}
|
|
|
|
|
|
/**
|
|
* Escape '$' and '#', inverting the unescaping performed in
|
|
* {@link #skipUntilEL()} but only for ${ and #{ sequences since escaping
|
|
* for $ and # is optional.
|
|
*
|
|
* @param input Non-EL input to be escaped
|
|
* @param isDeferredSyntaxAllowedAsLiteral
|
|
*
|
|
* @return The escaped version of the input
|
|
*/
|
|
static String escapeLiteralExpression(String input,
|
|
boolean isDeferredSyntaxAllowedAsLiteral) {
|
|
int len = input.length();
|
|
int lastAppend = 0;
|
|
StringBuilder output = null;
|
|
for (int i = 0; i < len; i++) {
|
|
char ch = input.charAt(i);
|
|
if (ch =='$' || (!isDeferredSyntaxAllowedAsLiteral && ch == '#')) {
|
|
if (i + 1 < len && input.charAt(i + 1) == '{') {
|
|
if (output == null) {
|
|
output = new StringBuilder(len + 20);
|
|
}
|
|
output.append(input.substring(lastAppend, i));
|
|
lastAppend = i + 1;
|
|
output.append('\\');
|
|
output.append(ch);
|
|
}
|
|
}
|
|
}
|
|
if (output == null) {
|
|
return input;
|
|
} else {
|
|
output.append(input.substring(lastAppend, len));
|
|
return output.toString();
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Escape '\\', '\'' and '\"', inverting the unescaping performed in
|
|
* {@link #skipUntilEL()}.
|
|
*
|
|
* @param input Non-EL input to be escaped
|
|
*
|
|
* @return The escaped version of the input
|
|
*/
|
|
private static String escapeELText(String input) {
|
|
int len = input.length();
|
|
char quote = 0;
|
|
int lastAppend = 0;
|
|
int start = 0;
|
|
int end = len;
|
|
|
|
// Look to see if the value is quoted
|
|
String trimmed = input.trim();
|
|
int trimmedLen = trimmed.length();
|
|
if (trimmedLen > 1) {
|
|
// Might be quoted
|
|
quote = trimmed.charAt(0);
|
|
if (quote == '\'' || quote == '\"') {
|
|
if (trimmed.charAt(trimmedLen - 1) != quote) {
|
|
throw new IllegalArgumentException(Localizer.getMessage(
|
|
"org.apache.jasper.compiler.ELParser.invalidQuotesForStringLiteral",
|
|
input));
|
|
}
|
|
start = input.indexOf(quote) + 1;
|
|
end = start + trimmedLen - 2;
|
|
} else {
|
|
quote = 0;
|
|
}
|
|
}
|
|
|
|
StringBuilder output = null;
|
|
for (int i = start; i < end; i++) {
|
|
char ch = input.charAt(i);
|
|
if (ch == '\\' || ch == quote) {
|
|
if (output == null) {
|
|
output = new StringBuilder(len + 20);
|
|
}
|
|
output.append(input.substring(lastAppend, i));
|
|
lastAppend = i + 1;
|
|
output.append('\\');
|
|
output.append(ch);
|
|
}
|
|
}
|
|
if (output == null) {
|
|
return input;
|
|
} else {
|
|
output.append(input.substring(lastAppend, len));
|
|
return output.toString();
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* @return true if there is something left in EL expression buffer other
|
|
* than white spaces.
|
|
*/
|
|
private boolean hasNext() {
|
|
skipSpaces();
|
|
return hasNextChar();
|
|
}
|
|
|
|
private String getAndResetWhiteSpace() {
|
|
String result = whiteSpace;
|
|
whiteSpace = "";
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
* Implementation note: This method assumes that it is always preceded by a
|
|
* call to hasNext() in order for whitespace handling to be correct.
|
|
*
|
|
* @return The next token in the EL expression buffer.
|
|
*/
|
|
private Token nextToken() {
|
|
prevToken = curToken;
|
|
if (hasNextChar()) {
|
|
char ch = nextChar();
|
|
if (Character.isJavaIdentifierStart(ch)) {
|
|
int start = index - 1;
|
|
while (index < expression.length() &&
|
|
Character.isJavaIdentifierPart(
|
|
ch = expression.charAt(index))) {
|
|
nextChar();
|
|
}
|
|
return new Id(getAndResetWhiteSpace(), expression.substring(start, index));
|
|
}
|
|
|
|
if (ch == '\'' || ch == '"') {
|
|
return parseQuotedChars(ch);
|
|
} else {
|
|
// For now...
|
|
return new Char(getAndResetWhiteSpace(), ch);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/*
|
|
* Parse a string in single or double quotes, allowing for escape sequences
|
|
* '\\', '\"' and "\'"
|
|
*/
|
|
private Token parseQuotedChars(char quote) {
|
|
StringBuilder buf = new StringBuilder();
|
|
buf.append(quote);
|
|
while (hasNextChar()) {
|
|
char ch = nextChar();
|
|
if (ch == '\\') {
|
|
ch = nextChar();
|
|
if (ch == '\\' || ch == '\'' || ch == '\"') {
|
|
buf.append(ch);
|
|
} else {
|
|
throw new IllegalArgumentException(Localizer.getMessage(
|
|
"org.apache.jasper.compiler.ELParser.invalidQuoting",
|
|
expression));
|
|
}
|
|
} else if (ch == quote) {
|
|
buf.append(ch);
|
|
break;
|
|
} else {
|
|
buf.append(ch);
|
|
}
|
|
}
|
|
return new QuotedString(getAndResetWhiteSpace(), buf.toString());
|
|
}
|
|
|
|
/*
|
|
* A collection of low level parse methods dealing with character in the EL
|
|
* expression buffer.
|
|
*/
|
|
|
|
private void skipSpaces() {
|
|
int start = index;
|
|
while (hasNextChar()) {
|
|
char c = expression.charAt(index);
|
|
if (c > ' ')
|
|
break;
|
|
index++;
|
|
}
|
|
whiteSpace = expression.substring(start, index);
|
|
}
|
|
|
|
private boolean hasNextChar() {
|
|
return index < expression.length();
|
|
}
|
|
|
|
private char nextChar() {
|
|
if (index >= expression.length()) {
|
|
return (char) -1;
|
|
}
|
|
return expression.charAt(index++);
|
|
}
|
|
|
|
private char peek(int advance) {
|
|
int target = index + advance;
|
|
if (target >= expression.length()) {
|
|
return (char) -1;
|
|
}
|
|
return expression.charAt(target);
|
|
}
|
|
|
|
private int getIndex() {
|
|
return index;
|
|
}
|
|
|
|
private void setIndex(int i) {
|
|
index = i;
|
|
}
|
|
|
|
/*
|
|
* Represents a token in EL expression string
|
|
*/
|
|
private static class Token {
|
|
|
|
protected final String whiteSpace;
|
|
|
|
Token(String whiteSpace) {
|
|
this.whiteSpace = whiteSpace;
|
|
}
|
|
|
|
char toChar() {
|
|
return 0;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return whiteSpace;
|
|
}
|
|
|
|
String toTrimmedString() {
|
|
return "";
|
|
}
|
|
|
|
String getWhiteSpace() {
|
|
return whiteSpace;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Represents an ID token in EL
|
|
*/
|
|
private static class Id extends Token {
|
|
String id;
|
|
|
|
Id(String whiteSpace, String id) {
|
|
super(whiteSpace);
|
|
this.id = id;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return whiteSpace + id;
|
|
}
|
|
|
|
@Override
|
|
String toTrimmedString() {
|
|
return id;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Represents a character token in EL
|
|
*/
|
|
private static class Char extends Token {
|
|
|
|
private char ch;
|
|
|
|
Char(String whiteSpace, char ch) {
|
|
super(whiteSpace);
|
|
this.ch = ch;
|
|
}
|
|
|
|
@Override
|
|
char toChar() {
|
|
return ch;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return whiteSpace + ch;
|
|
}
|
|
|
|
@Override
|
|
String toTrimmedString() {
|
|
return "" + ch;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Represents a quoted (single or double) string token in EL
|
|
*/
|
|
private static class QuotedString extends Token {
|
|
|
|
private String value;
|
|
|
|
QuotedString(String whiteSpace, String v) {
|
|
super(whiteSpace);
|
|
this.value = v;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return whiteSpace + value;
|
|
}
|
|
|
|
@Override
|
|
String toTrimmedString() {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
public char getType() {
|
|
return type;
|
|
}
|
|
|
|
|
|
static class TextBuilder extends ELNode.Visitor {
|
|
|
|
protected final boolean isDeferredSyntaxAllowedAsLiteral;
|
|
protected final StringBuilder output = new StringBuilder();
|
|
|
|
protected TextBuilder(boolean isDeferredSyntaxAllowedAsLiteral) {
|
|
this.isDeferredSyntaxAllowedAsLiteral = isDeferredSyntaxAllowedAsLiteral;
|
|
}
|
|
|
|
public String getText() {
|
|
return output.toString();
|
|
}
|
|
|
|
@Override
|
|
public void visit(Root n) throws JasperException {
|
|
output.append(n.getType());
|
|
output.append('{');
|
|
n.getExpression().visit(this);
|
|
output.append('}');
|
|
}
|
|
|
|
@Override
|
|
public void visit(Function n) throws JasperException {
|
|
output.append(escapeLiteralExpression(n.getOriginalText(), isDeferredSyntaxAllowedAsLiteral));
|
|
output.append('(');
|
|
}
|
|
|
|
@Override
|
|
public void visit(Text n) throws JasperException {
|
|
output.append(escapeLiteralExpression(n.getText(),isDeferredSyntaxAllowedAsLiteral));
|
|
}
|
|
|
|
@Override
|
|
public void visit(ELText n) throws JasperException {
|
|
output.append(escapeELText(n.getText()));
|
|
}
|
|
}
|
|
}
|