/*
* =============================================================================
*
* Copyright (c) 2011-2014, The THYMELEAF team (http://www.thymeleaf.org)
*
* Licensed 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.thymeleaf.standard.expression;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.LoaderClassPath;
import ognl.Ognl;
import ognl.OgnlException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.thymeleaf.Configuration;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.cache.ICache;
import org.thymeleaf.cache.ICacheManager;
import org.thymeleaf.context.IContext;
import org.thymeleaf.context.IContextVariableRestriction;
import org.thymeleaf.context.IProcessingContext;
import org.thymeleaf.context.VariablesMap;
import org.thymeleaf.exceptions.TemplateProcessingException;
import org.thymeleaf.expression.ExpressionEvaluatorObjects;
import org.thymeleaf.util.ClassLoaderUtils;
import org.thymeleaf.util.EvaluationUtil;
/**
*
* @author Daniel Fernández
*
* @since 2.0.9
*
*/
public class OgnlVariableExpressionEvaluator
implements IStandardVariableExpressionEvaluator {
private static final Logger logger = LoggerFactory.getLogger(OgnlVariableExpressionEvaluator.class);
public static final OgnlVariableExpressionEvaluator INSTANCE = new OgnlVariableExpressionEvaluator();
private static final String OGNL_CACHE_PREFIX = "{ognl}";
private static boolean booleanFixApplied = false;
public final Object evaluate(final Configuration configuration,
final IProcessingContext processingContext, final String expression,
final StandardExpressionExecutionContext expContext, final boolean useSelectionAsRoot) {
try {
if (logger.isTraceEnabled()) {
logger.trace("[THYMELEAF][{}] OGNL expression: evaluating expression \"{}\" on target", TemplateEngine.threadIndex(), expression);
}
Object expressionTree = null;
ICache<String, Object> cache = null;
if (configuration != null) {
final ICacheManager cacheManager = configuration.getCacheManager();
if (cacheManager != null) {
cache = cacheManager.getExpressionCache();
if (cache != null) {
expressionTree = cache.get(OGNL_CACHE_PREFIX + expression);
}
}
}
if (expressionTree == null) {
expressionTree = ognl.Ognl.parseExpression(expression);
if (cache != null && null != expressionTree) {
cache.put(OGNL_CACHE_PREFIX + expression, expressionTree);
}
}
final Map<String,Object> contextVariables = processingContext.getExpressionObjects();
final Map<String,Object> additionalContextVariables = computeAdditionalContextVariables(processingContext);
if (additionalContextVariables != null) {
contextVariables.putAll(additionalContextVariables);
}
final Object evaluationRoot =
(useSelectionAsRoot?
processingContext.getExpressionSelectionEvaluationRoot() :
processingContext.getExpressionEvaluationRoot());
setVariableRestrictions(expContext, evaluationRoot, contextVariables);
final Object result = Ognl.getValue(expressionTree, contextVariables, evaluationRoot);
if (!expContext.getPerformTypeConversion()) {
return result;
}
final IStandardConversionService conversionService =
StandardExpressions.getConversionService(configuration);
return conversionService.convert(configuration, processingContext, result, String.class);
} catch (final OgnlException e) {
throw new TemplateProcessingException(
"Exception evaluating OGNL expression: \"" + expression + "\"", e);
}
}
/*
* Meant to be overwritten
*/
protected Map<String,Object> computeAdditionalContextVariables(
@SuppressWarnings("unused") final IProcessingContext processingContext) {
return Collections.emptyMap();
}
protected void setVariableRestrictions(final StandardExpressionExecutionContext expContext,
final Object evaluationRoot, final Map<String,Object> contextVariables) {
final List<IContextVariableRestriction> restrictions =
(expContext.getForbidRequestParameters()?
StandardVariableRestrictions.REQUEST_PARAMETERS_FORBIDDEN : null);
final Object context = contextVariables.get(ExpressionEvaluatorObjects.CONTEXT_VARIABLE_NAME);
if (context != null && context instanceof IContext) {
final VariablesMap<?,?> variablesMap = ((IContext)context).getVariables();
variablesMap.setRestrictions(restrictions);
}
if (evaluationRoot != null && evaluationRoot instanceof VariablesMap<?,?>) {
((VariablesMap<?,?>)evaluationRoot).setRestrictions(restrictions);
}
}
protected OgnlVariableExpressionEvaluator() {
super();
if (!booleanFixApplied && shouldApplyOgnlBooleanFix()) {
applyOgnlBooleanFix();
booleanFixApplied = true;
}
}
@Override
public String toString() {
return "OGNL";
}
/**
* <p>
* Determines whether a fix should be applied to OGNL in order
* to evaluate Strings as booleans in the same way as
* Thymeleaf does ('false', 'off' and 'no' are actually "false"
* instead of OGNL's default "true").
* </p>
*
* @return whether the OGNL boolean fix should be applied or not.
*/
protected boolean shouldApplyOgnlBooleanFix() {
return true;
}
private static void applyOgnlBooleanFix() {
try {
final ClassLoader classLoader =
ClassLoaderUtils.getClassLoader(OgnlVariableExpressionEvaluator.class);
final ClassPool pool = new ClassPool(true);
pool.insertClassPath(new LoaderClassPath(classLoader));
final CtClass[] params = new CtClass[] { pool.get(Object.class.getName()) };
// We must load by class name here instead of "OgnlOps.class.getName()" because
// the latter would cause the class to be loaded and therefore it would not be
// possible to modify it.
final CtClass ognlClass = pool.get("ognl.OgnlOps");
final CtClass fixClass = pool.get(OgnlVariableExpressionEvaluator.class.getName());
final CtMethod ognlMethod =
ognlClass.getDeclaredMethod("booleanValue", params);
final CtMethod fixMethod =
fixClass.getDeclaredMethod("fixBooleanValue", params);
ognlMethod.setBody(fixMethod, null);
// Pushes the class to the class loader, effectively making it
// load the modified version instead of the original one.
ognlClass.toClass(classLoader, null);
} catch (final Exception e) {
// Any exceptions here will be consumed and converted into log messages.
// An exception at this point could be caused by multiple situations that
// should not suppose the stop of the framework's initialization.
// If the fix cannot not applied, an INFO message is issued and initialization
// continues normally.
if (logger.isTraceEnabled()) {
logger.trace(
"Thymeleaf was not able to apply a fix on OGNL's boolean evaluation " +
"that would have enabled OGNL to evaluate Strings as booleans (e.g. in " +
"\"th:if\") in exactly the same way as Thymeleaf itself or Spring EL ('false', " +
"'off' and 'no' should be considered \"false\"). This did not stop the " +
"initialization process.", e);
} else {
logger.info(
"Thymeleaf was not able to apply a fix on OGNL's boolean evaluation " +
"that would have enabled OGNL to evaluate Strings as booleans (e.g. in " +
"\"th:if\") in exactly the same way as Thymeleaf itself or Spring EL ('false', " +
"'off' and 'no' should be considered \"false\"). This did not stop the " +
"initialization process. Exception raised was " + e.getClass().getName() +
": " + e.getMessage() + " [Set the log to TRACE for the complete exception stack trace]");
}
}
}
static boolean fixBooleanValue(final Object value) {
// This specifies how evaluation to boolean should be done *INSIDE* OGNL expressions, so the conversion
// service does not really apply at this point (it will be applied later, on the Standard -not OGNL- expr.)
return EvaluationUtil.evaluateAsBoolean(value);
}
}