blob: 58f1fd2ce07413e732d9b02edad3740694473c3e [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.struts2.oval.interceptor;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.ActionProxy;
import com.opensymphony.xwork2.ModelDriven;
import com.opensymphony.xwork2.TextProviderFactory;
import com.opensymphony.xwork2.Validateable;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.interceptor.MethodFilterInterceptor;
import com.opensymphony.xwork2.interceptor.PrefixMethodInvocationUtil;
import com.opensymphony.xwork2.util.ValueStack;
import net.sf.oval.exception.ExpressionEvaluationException;
import net.sf.oval.expression.ExpressionLanguage;
import net.sf.oval.expression.ExpressionLanguageOGNLImpl;
import ognl.Ognl;
import ognl.OgnlException;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import com.opensymphony.xwork2.validator.DelegatingValidatorContext;
import com.opensymphony.xwork2.validator.ValidatorContext;
import net.sf.oval.ConstraintViolation;
import net.sf.oval.Validator;
import net.sf.oval.configuration.Configurer;
import net.sf.oval.context.FieldContext;
import net.sf.oval.context.MethodReturnValueContext;
import net.sf.oval.context.OValContext;
import org.apache.commons.lang3.StringUtils;
import org.apache.struts2.oval.annotation.Profiles;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
/*
This interceptor provides validation using the OVal validation framework
*/
public class OValValidationInterceptor extends MethodFilterInterceptor {
public static final String STRUTS_OVAL_VALIDATE_JPAANNOTATIONS = "struts.oval.validateJPAAnnotations";
private static final Logger LOG = LogManager.getLogger(OValValidationInterceptor.class);
protected final static String VALIDATE_PREFIX = "validate";
protected final static String ALT_VALIDATE_PREFIX = "validateDo";
protected boolean alwaysInvokeValidate = true;
protected boolean programmatic = true;
protected OValValidationManager validationManager;
protected boolean validateJPAAnnotations;
protected TextProviderFactory textProviderFactory;
private ExpressionLanguage ognlExpressionLanguage;
public OValValidationInterceptor() {
ognlExpressionLanguage = new ExpressionLanguageOGNL();
}
@Inject
public void setValidationManager(OValValidationManager validationManager) {
this.validationManager = validationManager;
}
@Inject
public void setTextProviderFactory(TextProviderFactory textProviderFactory) {
this.textProviderFactory = textProviderFactory;
}
/**
* Enable OVal support for JPA
*/
@Inject(value = STRUTS_OVAL_VALIDATE_JPAANNOTATIONS)
public void setValidateJPAAnnotations(String validateJPAAnnotations) {
this.validateJPAAnnotations = Boolean.parseBoolean(validateJPAAnnotations);
}
/**
* Determines if {@link com.opensymphony.xwork2.Validateable}'s <code>validate()</code> should be called,
* as well as methods whose name that start with "validate". Defaults to "true".
*
* @param programmatic <tt>true</tt> then <code>validate()</code> is invoked.
*/
public void setProgrammatic(boolean programmatic) {
this.programmatic = programmatic;
}
/**
* Determines if {@link com.opensymphony.xwork2.Validateable}'s <code>validate()</code> should always
* be invoked. Default to "true".
*
* @param alwaysInvokeValidate <tt>true</tt> then <code>validate()</code> is always invoked.
*/
public void setAlwaysInvokeValidate(String alwaysInvokeValidate) {
this.alwaysInvokeValidate = Boolean.parseBoolean(alwaysInvokeValidate);
}
protected String doIntercept(ActionInvocation invocation) throws Exception {
Object action = invocation.getAction();
ActionProxy proxy = invocation.getProxy();
ValueStack valueStack = invocation.getStack();
String methodName = proxy.getMethod();
String context = proxy.getConfig().getName();
if (LOG.isDebugEnabled()) {
LOG.debug("Validating [{}/{}] with method [{}]", invocation.getProxy().getNamespace(), invocation.getProxy().getActionName(), methodName);
}
//OVal vallidatio (no XML yet)
performOValValidation(action, valueStack, methodName, context);
//Validatable.valiedate() and validateX()
performProgrammaticValidation(invocation, action);
return invocation.invoke();
}
private void performProgrammaticValidation(ActionInvocation invocation, Object action) throws Exception {
if (action instanceof Validateable && programmatic) {
// keep exception that might occured in validateXXX or validateDoXXX
Exception exception = null;
Validateable validateable = (Validateable) action;
LOG.debug("Invoking validate() on action [{}]", validateable);
try {
PrefixMethodInvocationUtil.invokePrefixMethod(
invocation,
new String[]{VALIDATE_PREFIX, ALT_VALIDATE_PREFIX});
} catch (Exception e) {
// If any exception occurred while doing reflection, we want
// validate() to be executed
LOG.warn("An exception occurred while executing the prefix method", e);
exception = e;
}
if (alwaysInvokeValidate) {
validateable.validate();
}
if (exception != null) {
// rethrow if something is wrong while doing validateXXX / validateDoXXX
throw exception;
}
}
}
protected void performOValValidation(Object action, ValueStack valueStack, String methodName, String context) throws NoSuchMethodException {
Class clazz = action.getClass();
//read validation from xmls
List<Configurer> configurers = validationManager.getConfigurers(clazz, context, validateJPAAnnotations);
Validator validator = configurers.isEmpty() ? new Validator() : new Validator(configurers);
// Note: For Oval <= 1.70, API requires "validator.addExpressionLanguage("ognl", ognlExpressionLanguage)".
validator.getExpressionLanguageRegistry().registerExpressionLanguage("ognl", ognlExpressionLanguage); // Usage for Oval >= 1.80 due to API changes
//if the method is annotated with a @Profiles annotation, use those profiles
Method method = clazz.getMethod(methodName, new Class[0]);
if (method != null) {
Profiles profiles = method.getAnnotation(Profiles.class);
if (profiles != null) {
String[] profileNames = profiles.value();
if (profileNames != null && profileNames.length > 0) {
validator.disableAllProfiles();
LOG.debug("Enabling profiles [{}]", StringUtils.join(profileNames, ","));
for (String profileName : profileNames)
validator.enableProfile(profileName);
}
}
}
//perform validation
List<ConstraintViolation> violations = validator.validate(action);
addValidationErrors(violations.toArray(new ConstraintViolation[violations.size()]), action, valueStack, null);
}
private void addValidationErrors(ConstraintViolation[] violations, Object action, ValueStack valueStack, String parentFieldname) {
if (violations != null) {
ValidatorContext validatorContext = new DelegatingValidatorContext(action, textProviderFactory);
for (ConstraintViolation violation : violations) {
//translate message
String key = violation.getMessage();
String message = key;
// push context variable into stack, to allow use ${max}, ${min} etc in error messages
valueStack.push(violation.getMessageVariables());
//push the validator into the stack
valueStack.push(violation.getContext());
try {
message = validatorContext.getText(key);
} finally {
valueStack.pop();
valueStack.pop();
}
if (isActionError(violation)) {
LOG.debug("Adding action error '{}'", message);
validatorContext.addActionError(message);
} else {
ValidationError validationError = buildValidationError(violation, message);
// build field name
String fieldName = validationError.getFieldName();
if (parentFieldname != null) {
fieldName = parentFieldname + "." + fieldName;
}
LOG.debug("Adding field error [{}] with message '{}'", fieldName, validationError.getMessage());
validatorContext.addFieldError(fieldName, validationError.getMessage());
// don't add "model." prefix to fields of model in model driven action
if ((action instanceof ModelDriven) && "model".equals(fieldName)) {
fieldName = null;
}
// add violations of member object fields
addValidationErrors(violation.getCauses(), action, valueStack, fieldName);
}
}
}
}
/**
* Get field name and message, used to add the validation error to fieldErrors
*/
protected ValidationError buildValidationError(ConstraintViolation violation, String message) {
OValContext context = violation.getContext();
if (context instanceof FieldContext) {
Field field = ((FieldContext) context).getField();
String className = field.getDeclaringClass().getName();
//the default OVal message shows the field name as ActionClass.fieldName
String finalMessage = StringUtils.removeStart(message, className + ".");
return new ValidationError(field.getName(), finalMessage);
} else if (context instanceof MethodReturnValueContext) {
Method method = ((MethodReturnValueContext) context).getMethod();
String className = method.getDeclaringClass().getName();
String methodName = method.getName();
//the default OVal message shows the field name as ActionClass.fieldName
String finalMessage = StringUtils.removeStart(message, className + ".");
String fieldName = null;
if (methodName.startsWith("get")) {
fieldName = StringUtils.uncapitalize(StringUtils.removeStart(methodName, "get"));
} else if (methodName.startsWith("is")) {
fieldName = StringUtils.uncapitalize(StringUtils.removeStart(methodName, "is"));
}
//the result will have the full method name, like "getName()", replace it by "name" (obnly if it is a field)
if (fieldName != null)
finalMessage = finalMessage.replaceAll(methodName + "\\(.*?\\)", fieldName);
return new ValidationError(StringUtils.defaultString(fieldName, methodName), finalMessage);
}
return new ValidationError(violation.getCheckName(), message);
}
/**
* Decide if a violation should be added to the fieldErrors or actionErrors
*/
protected boolean isActionError(ConstraintViolation violation) {
return false;
}
class ValidationError {
private String fieldName;
private String message;
ValidationError(String fieldName, String message) {
this.fieldName = fieldName;
this.message = message;
}
public String getFieldName() {
return fieldName;
}
public String getMessage() {
return message;
}
}
}
class ExpressionLanguageOGNL extends ExpressionLanguageOGNLImpl {
private static final Logger LOG = LogManager.getLogger(ExpressionLanguageOGNL.class);
public Object evaluate(final String expression, final Map<String, ? > values) throws ExpressionEvaluationException {
try {
LOG.debug("Evaluating OGNL expression: {1}", expression);
return Ognl.getValue(expression, ActionContext.getContext().getContextMap(), values);
} catch (final OgnlException ex) {
throw new ExpressionEvaluationException("Evaluating script with OGNL failed.", ex);
}
}
}