blob: 7876908f0a4cecfa0eb57086e76439b687c0ee60 [file] [log] [blame]
/*
* 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.taglibs.standard.lang.jstl;
import java.io.Reader;
import java.io.StringReader;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.jsp.PageContext;
import org.apache.taglibs.standard.lang.jstl.parser.ELParser;
import org.apache.taglibs.standard.lang.jstl.parser.ParseException;
import org.apache.taglibs.standard.lang.jstl.parser.Token;
import org.apache.taglibs.standard.lang.jstl.parser.TokenMgrError;
/**
* <p>This is the main class for evaluating expression Strings. An
* expression String is a String that may contain expressions of the
* form ${...}. Multiple expressions may appear in the same
* expression String. In such a case, the expression String's value
* is computed by concatenating the String values of those evaluated
* expressions and any intervening non-expression text, then
* converting the resulting String to the expected type using the
* PropertyEditor mechanism.
* <p>In the special case where the expression String is a single
* expression, the value of the expression String is determined by
* evaluating the expression, without any intervening conversion to a
* String.
* <p>The evaluator maintains a cache mapping expression Strings to
* their parsed results. For expression Strings containing no
* expression elements, it maintains a cache mapping
* ExpectedType/ExpressionString to parsed value, so that static
* expression Strings won't have to go through a conversion step every
* time they are used. All instances of the evaluator share the same
* cache. The cache may be bypassed by setting a flag on the
* evaluator's constructor.
* <p>The evaluator must be passed a VariableResolver in its
* constructor. The VariableResolver is used to resolve variable
* names encountered in expressions, and can also be used to implement
* "implicit objects" that are always present in the namespace.
* Different applications will have different policies for variable
* lookups and implicit objects - these differences can be
* encapsulated in the VariableResolver passed to the evaluator's
* constructor.
* <p>Most VariableResolvers will need to perform their resolution
* against some context. For example, a JSP environment needs a
* PageContext to resolve variables. The evaluate() method takes a
* generic Object context which is eventually passed to the
* VariableResolver - the VariableResolver is responsible for casting
* the context to the proper type.
* <p>Once an evaluator instance has been constructed, it may be used
* multiple times, and may be used by multiple simultaneous Threads.
* In other words, an evaluator instance is well-suited for use as a
* singleton.
*
* @author Nathan Abramson - Art Technology Group
* @author Shawn Bayern
*/
public class ELEvaluator {
//-------------------------------------
// Properties
//-------------------------------------
//-------------------------------------
// Member variables
//-------------------------------------
/**
* Name of configuration setting for maximum number of entries in the
* cached expression string map
*/
private static final String EXPR_CACHE_PARAM
= "org.apache.taglibs.standard.lang.jstl.exprCacheSize";
/**
* Default maximum cache size
*/
private static final int MAX_SIZE = 100;
/**
* The mapping from expression String to its parsed form (String,
* Expression, or ExpressionString)
* <p>Using LRU Map with a maximum capacity to avoid out of bound map
* growth.
* <p>NOTE: use LinkedHashmap if a dependency on J2SE 1.4+ is ok
*/
private static Map sCachedExpressionStrings = null;
/**
* The mapping from ExpectedType to Maps mapping literal String to
* parsed value *
*/
private static final Map sCachedExpectedTypes = new HashMap();
/**
* The static Logger *
*/
static Logger sLogger = new Logger(System.out);
/**
* The VariableResolver *
*/
VariableResolver mResolver;
/**
* Flag if the cache should be bypassed *
*/
private volatile boolean mBypassCache;
/**
* The PageContext *
*/
// TODO: Find a better way to override the expression cache size that does not need this field.
PageContext pageContext;
//-------------------------------------
/**
* Constructor
*
* @param pResolver the object that should be used to resolve
* variable names encountered in expressions. If null, all variable
* references will resolve to null.
*/
public ELEvaluator(VariableResolver pResolver) {
mResolver = pResolver;
}
//-------------------------------------
/**
* Enable cache bypass
*
* @param pBypassCache flag indicating cache should be bypassed
*/
public void setBypassCache(boolean pBypassCache) {
mBypassCache = pBypassCache;
}
//-------------------------------------
/**
* Evaluates the given expression String
*
* @param pExpressionString the expression String to be evaluated
* @param pContext the context passed to the VariableResolver for
* resolving variable names
* @param pExpectedType the type to which the evaluated expression
* should be coerced
* @return the expression String evaluated to the given expected
* type
*/
public Object evaluate(String pExpressionString,
Object pContext,
Class pExpectedType,
Map functions,
String defaultPrefix)
throws ELException {
return evaluate(pExpressionString,
pContext,
pExpectedType,
functions,
defaultPrefix,
sLogger);
}
//-------------------------------------
/**
* Evaluates the given expression string
*/
Object evaluate(String pExpressionString,
Object pContext,
Class pExpectedType,
Map functions,
String defaultPrefix,
Logger pLogger)
throws ELException {
// Check for null expression strings
if (pExpressionString == null) {
throw new ELException
(Constants.NULL_EXPRESSION_STRING);
}
// Set the PageContext;
pageContext = (PageContext) pContext;
// Get the parsed version of the expression string
Object parsedValue = parseExpressionString(pExpressionString);
// Evaluate differently based on the parsed type
if (parsedValue instanceof String) {
// Convert the String, and cache the conversion
String strValue = (String) parsedValue;
return convertStaticValueToExpectedType(strValue,
pExpectedType,
pLogger);
} else if (parsedValue instanceof Expression) {
// Evaluate the expression and convert
Object value =
((Expression) parsedValue).evaluate(pContext,
mResolver,
functions,
defaultPrefix,
pLogger);
return convertToExpectedType(value,
pExpectedType,
pLogger);
} else if (parsedValue instanceof ExpressionString) {
// Evaluate the expression/string list and convert
String strValue =
((ExpressionString) parsedValue).evaluate(pContext,
mResolver,
functions,
defaultPrefix,
pLogger);
return convertToExpectedType(strValue,
pExpectedType,
pLogger);
} else {
// This should never be reached
return null;
}
}
//-------------------------------------
/**
* Gets the parsed form of the given expression string. If the
* parsed form is cached (and caching is not bypassed), return the
* cached form, otherwise parse and cache the value. Returns either
* a String, Expression, or ExpressionString.
*/
public Object parseExpressionString(String pExpressionString)
throws ELException {
// See if it's an empty String
if (pExpressionString.length() == 0) {
return "";
}
if (mBypassCache) {
return parseExpressionUncached(pExpressionString);
}
Map cache = getOrCreateExpressionStringMap(pageContext);
// See if it's in the cache
Object ret = cache.get(pExpressionString);
if (ret != null) {
return ret;
}
ret = parseExpressionUncached(pExpressionString);
cache.put(pExpressionString, ret);
return ret;
}
/**
* Parse an expression string bypassing the cache.
*
* This allows expressions to be validated at translation time without polluting the cache.
*
* @param pExpressionString the text to parse
* @return the parse result
* @throws ELException if there was a problem parsing the expression text
*/
public Object parseExpressionUncached(String pExpressionString) throws ELException {
try {
Reader r = new StringReader(pExpressionString);
ELParser parser = new ELParser(r);
return parser.ExpressionString();
} catch (ParseException exc) {
throw new ELException(formatParseException(pExpressionString, exc));
} catch (TokenMgrError exc) {
// Note - this should never be reached, since the parser is
// constructed to tokenize any input (illegal inputs get
// parsed to <BADLY_ESCAPED_STRING_LITERAL> or
// <ILLEGAL_CHARACTER>
throw new ELException(exc.getMessage());
}
}
//-------------------------------------
/**
* Converts the given value to the specified expected type.
*/
Object convertToExpectedType(Object pValue,
Class pExpectedType,
Logger pLogger)
throws ELException {
return Coercions.coerce(pValue,
pExpectedType,
pLogger);
}
//-------------------------------------
/**
* Converts the given String, specified as a static expression
* string, to the given expected type. The conversion is cached.
*/
Object convertStaticValueToExpectedType(String pValue,
Class pExpectedType,
Logger pLogger)
throws ELException {
// See if the value is already of the expected type
if (pExpectedType == String.class ||
pExpectedType == Object.class) {
return pValue;
}
// Find the cached value
Map valueByString = getOrCreateExpectedTypeMap(pExpectedType);
if (!mBypassCache &&
valueByString.containsKey(pValue)) {
return valueByString.get(pValue);
} else {
// Convert from a String
Object ret = Coercions.coerce(pValue, pExpectedType, pLogger);
valueByString.put(pValue, ret);
return ret;
}
}
//-------------------------------------
/**
* Creates or returns the Map that maps string literals to parsed
* values for the specified expected type.
*/
static Map getOrCreateExpectedTypeMap(Class pExpectedType) {
synchronized (sCachedExpectedTypes) {
Map ret = (Map) sCachedExpectedTypes.get(pExpectedType);
if (ret == null) {
ret = Collections.synchronizedMap(new HashMap());
sCachedExpectedTypes.put(pExpectedType, ret);
}
return ret;
}
}
//-------------------------------------
/**
* Creates LRU map of expression strings. If context parameter
* specifying cache size is present use that as the maximum size
* of the LRU map otherwise use default.
*
* TODO: Using the context parameter means the cache is sized based on the configuration
* of the first web application that calls this. This might be a problem if this jar is
* installed in the application server's classpath rather than the application's.
*/
private static synchronized Map getOrCreateExpressionStringMap(PageContext pageContext) {
if (sCachedExpressionStrings == null) {
final int maxSize;
if ((pageContext != null) && (pageContext.getServletContext() != null)) {
String value = pageContext.getServletContext().getInitParameter(EXPR_CACHE_PARAM);
if (value != null) {
maxSize = Integer.valueOf(value);
} else {
maxSize = MAX_SIZE;
}
} else {
maxSize = MAX_SIZE;
}
// fall through if it couldn't find the parameter
sCachedExpressionStrings = Collections.synchronizedMap(new LinkedHashMap() {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > maxSize;
}
});
}
return sCachedExpressionStrings;
}
//-------------------------------------
// Formatting ParseException
//-------------------------------------
/**
* Formats a ParseException into an error message suitable for
* displaying on a web page
*/
static String formatParseException(String pExpressionString,
ParseException pExc) {
// Generate the String of expected tokens
StringBuffer expectedBuf = new StringBuffer();
int maxSize = 0;
boolean printedOne = false;
if (pExc.expectedTokenSequences == null) {
return pExc.toString();
}
for (int i = 0; i < pExc.expectedTokenSequences.length; i++) {
if (maxSize < pExc.expectedTokenSequences[i].length) {
maxSize = pExc.expectedTokenSequences[i].length;
}
for (int j = 0; j < pExc.expectedTokenSequences[i].length; j++) {
if (printedOne) {
expectedBuf.append(", ");
}
expectedBuf.append
(pExc.tokenImage[pExc.expectedTokenSequences[i][j]]);
printedOne = true;
}
}
String expected = expectedBuf.toString();
// Generate the String of encountered tokens
StringBuffer encounteredBuf = new StringBuffer();
Token tok = pExc.currentToken.next;
for (int i = 0; i < maxSize; i++) {
if (i != 0) {
encounteredBuf.append(" ");
}
if (tok.kind == 0) {
encounteredBuf.append(pExc.tokenImage[0]);
break;
}
encounteredBuf.append(addEscapes(tok.image));
tok = tok.next;
}
String encountered = encounteredBuf.toString();
// Format the error message
return MessageFormat.format
(Constants.PARSE_EXCEPTION,
new Object[]{
expected,
encountered,
});
}
//-------------------------------------
/**
* Used to convert raw characters to their escaped version when
* these raw version cannot be used as part of an ASCII string
* literal.
*/
static String addEscapes(String str) {
StringBuffer retval = new StringBuffer();
char ch;
for (int i = 0; i < str.length(); i++) {
switch (str.charAt(i)) {
case 0:
continue;
case '\b':
retval.append("\\b");
continue;
case '\t':
retval.append("\\t");
continue;
case '\n':
retval.append("\\n");
continue;
case '\f':
retval.append("\\f");
continue;
case '\r':
retval.append("\\r");
continue;
default:
if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) {
String s = "0000" + Integer.toString(ch, 16);
retval.append("\\u" + s.substring(s.length() - 4, s.length()));
} else {
retval.append(ch);
}
continue;
}
}
return retval.toString();
}
//-------------------------------------
// Testing methods
//-------------------------------------
/**
* Parses the given expression string, then converts it back to a
* String in its canonical form. This is used to test parsing.
*/
public String parseAndRender(String pExpressionString)
throws ELException {
Object val = parseExpressionString(pExpressionString);
if (val instanceof String) {
return (String) val;
} else if (val instanceof Expression) {
return "${" + ((Expression) val).getExpressionString() + "}";
} else if (val instanceof ExpressionString) {
return ((ExpressionString) val).getExpressionString();
} else {
return "";
}
}
//-------------------------------------
}