/* Copyright 2008 Tim Fennell
*
* 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 net.sourceforge.stripes.validation.expression;
import net.sourceforge.stripes.action.ActionBean;
import net.sourceforge.stripes.controller.ParameterName;
import net.sourceforge.stripes.controller.StripesFilter;
import net.sourceforge.stripes.controller.StripesConstants;
import net.sourceforge.stripes.validation.ValidationMetadata;
import net.sourceforge.stripes.validation.ValidationErrors;
import net.sourceforge.stripes.validation.ValidationError;
import net.sourceforge.stripes.validation.ScopedLocalizableError;
import net.sourceforge.stripes.util.Log;
import net.sourceforge.stripes.util.bean.BeanUtil;
import net.sourceforge.stripes.exception.StripesRuntimeException;
import javax.servlet.jsp.JspFactory;
import javax.servlet.jsp.JspApplicationContext;
import javax.servlet.ServletContext;
import javax.el.ExpressionFactory;
import javax.el.ValueExpression;
import javax.el.ELContext;
import javax.el.ELResolver;
import javax.el.PropertyNotWritableException;
import javax.el.FunctionMapper;
import javax.el.VariableMapper;
import javax.el.ELException;
import java.util.List;
import java.util.Iterator;
import java.beans.FeatureDescriptor;
import java.lang.reflect.Method;
/**
* An implementation of {@link ExpressionExecutor} that uses the new EL API available in Java
* EE 5 in the {@link javax.el} package. While more complicated that the JSP 2.0 API it has
* one advantage which is that it can be used without the need to allocate a PageContext
* object and without any other libraries being available.
*
* @author tfenne
* @since Stripes 1.5
*/
public class Jsp21ExpressionExecutor implements ExpressionExecutor {
private static final Log log = Log.getInstance(Jsp21ExpressionExecutor.class);
/**
* Implementation of the EL interface to resolve variables. Resolves variables by
* checking two special names ("this" and "actionBean") and then falling back to
* retrieving property values from the ActionBean passed in to the constructor.
*
* @author Tim Fennell
* @since Stripes 1.5
*/
protected static class StripesELResolver extends ELResolver {
private ActionBean bean;
private Object currentValue;
/** Constructs a resolver based on the action bean . */
StripesELResolver(ActionBean bean) {
this.bean = bean;
}
/** Sets the value that the 'this' variable will point at. */
void setCurrentValue(Object value) {
this.currentValue = value;
}
/**
* Attempts to resolve the value as described in the class level javadoc.
* @param ctx the ELContext for the expression
* @param base the object on which the property resides (null == root property)
* @param prop the name of the property being looked for
* @return the value of the property or null if one can't be found
*/
@Override
public Object getValue(ELContext ctx, Object base, Object prop) {
if (ExpressionExecutorSupport.isSelfKeyword(this.bean, prop)) {
ctx.setPropertyResolved(true);
return this.currentValue;
}
else if (StripesConstants.REQ_ATTR_ACTION_BEAN.equals(prop)) {
ctx.setPropertyResolved(true);
return this.bean;
}
else {
try {
base = base == null ? this.bean : base;
Object retval = BeanUtil.getPropertyValue(String.valueOf(prop), base);
ctx.setPropertyResolved(true);
return retval;
}
catch (Exception e) { return null; }
}
}
/** Does nothing. Always returns Object.class. */
@Override
public Class<?> getType(final ELContext ctx, final Object base, final Object prop) {
ctx.setPropertyResolved(true);
return Object.class;
}
/** Does nothing. Always throws PropertyNotWritableException. */
@Override
public void setValue(ELContext elContext, Object o, Object o1, Object o2) throws PropertyNotWritableException {
throw new PropertyNotWritableException("Unsupported Op");
}
/** Always returns true. */
@Override
public boolean isReadOnly(final ELContext elContext, final Object o, final Object o1) { return true; }
/** Always returns null. */
@Override
public Iterator<FeatureDescriptor> getFeatureDescriptors(final ELContext elContext, final Object o) { return null; }
/** Always returns Object.class. */
@Override
public Class<?> getCommonPropertyType(final ELContext elContext, final Object o) { return Object.class; }
}
/**
* Implementation of the EL interface for managing expression context. Resolves variables
* using the StripesELResolver above. Both the FunctionMapper and VariableResolver are
* essentially no-op implementations.
*
* @author Tim Fennell
* @since Stripes 1.5
*/
protected static class StripesELContext extends ELContext {
@SuppressWarnings("unused")
private ActionBean bean;
private StripesELResolver resolver;
private VariableMapper vmapper;
private static final FunctionMapper fmapper = new FunctionMapper() {
@Override
public Method resolveFunction(final String s, final String s1) { return null; }
};
/**
* Constructs a new instance using the ActionBean provided as the source for most
* property resolutions.
*
* @param bean the ActionBean to resolve properties against
*/
public StripesELContext(ActionBean bean) {
this.bean = bean;
this.resolver = new StripesELResolver(bean);
this.vmapper = new VariableMapper() {
@Override
public ValueExpression resolveVariable(final String s) {
return null;
}
@Override
public ValueExpression setVariable(final String s, final ValueExpression valueExpression) {
return null;
}
};
}
/** Sets the current value of the 'this' special variable. */
public void setCurrentValue(final Object value) {resolver.setCurrentValue(value);}
/** Returns the StripesELResovler. */
@Override
public StripesELResolver getELResolver() { return this.resolver; }
/** Returns a no-op implementation of FunctionMapper. */
@Override
public FunctionMapper getFunctionMapper() { return fmapper; }
/** Returns a no-op implementation of VariableMapper. */
@Override
public VariableMapper getVariableMapper() { return vmapper; }
}
/** Default constructor that throws an exception if the JSP2.1 APIs are not available. */
public Jsp21ExpressionExecutor() {
if (getExpressionFactory() == null) {
throw new StripesRuntimeException("Could not create a JSP2.1 ExpressionFactory.");
}
}
// See interface for javadoc.
public void evaluate(final ActionBean bean, final ParameterName name, final List<Object> values,
final ValidationMetadata validationInfo, final ValidationErrors errors) {
StripesELContext ctx = null;
String expressionString = validationInfo.expression();
ValueExpression expression = null;
try {
if (expressionString != null) {
// Make sure we can get an factory
ExpressionFactory factory = getExpressionFactory();
if (factory == null) return;
ctx = new StripesELContext(bean);
// If this turns out to be slow we could probably cache the parsed expression
expression = factory.createValueExpression(ctx, expressionString, Boolean.class);
}
}
catch (ELException ele) {
throw new StripesRuntimeException(
"Could not parse the EL expression being used to validate field " +
name.getName() + ". This is not a transient error. Please double " +
"check the following expression for errors: " +
validationInfo.expression(), ele);
}
for (Object value : values) {
// And then if we have an expression to use
if (expression != null) {
try {
ctx.setCurrentValue(value);
Boolean result = (Boolean) expression.getValue(ctx);
if (!Boolean.TRUE.equals(result)) {
ValidationError error = new ScopedLocalizableError(ERROR_DEFAULT_SCOPE, ERROR_KEY);
error.setFieldValue(String.valueOf(value));
errors.add(name.getName(), error);
}
}
catch (ELException ele) {
log.error("Error evaluating expression for property ", name.getName(),
" of class ", bean.getClass().getSimpleName(), ". Expression: ",
validationInfo.expression());
}
}
}
}
/** Creates an ExpressionFactory using the JspApplicationContext. */
protected ExpressionFactory getExpressionFactory() {
ServletContext ctx = StripesFilter.getConfiguration().getServletContext();
JspApplicationContext jspCtx = JspFactory.getDefaultFactory().getJspApplicationContext(ctx);
return jspCtx.getExpressionFactory();
}
}