Search code examples
struts2bean-validationhibernate-validator

Struts2 Bean Validation


I am currently setting up a Stuts2 app that is using hibernate bean validation to validate form fields on a login page. I have added all necessary plugins and dependencies and configured the struts.xml to include a custom stack that calls the beanValidation interceptor. The /logon path is excluded from the authInterceptor. Unfortunately, even when the form fields for email and password are populated, the return message is that they are blank. I can see in the debugger that the email and password are being set in the LoginAction class but it's as if the beanValidation doesn't have this information. If I switch up the order of the interceptors to have beanValidation last, it is never called.

  <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
   "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
   "http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
    <constant name="struts.devMode" value="false" />
    <constant name="struts.action.extension" value="," />
    <constant name="struts.custom.i18n.resources" value="login,signup" />
    <constant name="struts.mapper.alwaysSelectFullNamespace"
        value="false" />
    <constant name="struts.beanValidation.providerClass" value="org.hibernate.validator.HibernateValidator" />
    <constant name="struts.beanValidation.ignoreXMLConfiguration"
        value="false" />
    <constant name="struts.beanValidation.convertMessageToUtf"
        value="false" />
    <constant name="struts.beanValidation.convertMessageFromEncoding"
        value="ISO-8859-1" />
    <constant name="struts.enable.SlashesInActionNames" value="true" />
    <package name="defaultPackage" namespace="/" extends="struts-default">
        <interceptors>
            <interceptor name="authInterceptor"
                class="com.mypackage.interceptor.AuthenticationInterceptor">
            </interceptor>
            <interceptor name="beanValidation"
                class="org.apache.struts.beanvalidation.validation.interceptor.BeanValidationInterceptor" />
            <interceptor-stack name="authStack">
                <interceptor-ref name="defaultStack" />
                <interceptor-ref name="beanValidation">
                    <param name="validateAnnotatedMethodOnly">true</param>
                    <param name="excludeMethods">input,back,cancel,browse</param>
                </interceptor-ref>
                <interceptor-ref name="authInterceptor" />
            </interceptor-stack>
        </interceptors>
        <default-interceptor-ref name="authStack"></default-interceptor-ref>
        <global-results>
            <result name="login">/jsp/login.jsp</result>
        </global-results>
        <action name="login" class="com.mypackage.action.LoginAction"
            method="login">
            <interceptor-ref name="authStack">
                <param name="excludeMethods">login</param>
            </interceptor-ref>
            <result name="login">/jsp/login.jsp</result>
            <result name="input">/jsp/login.jsp</result>
        </action>
        <action name="logon" class="com.mypackage.action.LoginAction"
            method="doLogon">
            <interceptor-ref name="authStack">
                <param name="authInterceptor.excludeMethods">doLogon</param>
            </interceptor-ref>
            <result name="input">/jsp/login.jsp</result>
            <result name="success">/jsp/login.jsp</result>
        </action>
        <action name="password/reset" class="com.mypackage.action.PasswordResetAction"
            method="resetPassword">
            <interceptor-ref name="authInterceptor">
                <param name="excludeMethods">resetPassword</param>
            </interceptor-ref>
            <result name="reset-password">/jsp/password_reset_html.jsp</result>
        </action>
        <action name="register" class="com.mypackage.action.RegisterAction"
            method="register">
            <interceptor-ref name="authInterceptor">
                <param name="excludeMethods">register</param>
            </interceptor-ref>
            <result name="register">/jsp/register_html.jsp</result>
        </action>
        <action name="doRegistration" class="com.mypackage.action.RegisterAction">
            <interceptor-ref name="authInterceptor">
                <param name="excludeMethods">resetPassword</param>
            </interceptor-ref>
        </action>
    </package>
</struts>

HTML

         <div class="fields-container">
            <div class="field-div">
                <div>
                    <span><s:text name="login.email" /></span>
                </div>
                <label class="form-label"> <input type="text" name="email"
                    value="" />
                </label>
            </div>
            <div class="field-div">
                <div>
                    <span><s:text name="login.password" /></span>
                </div>
                <label class="form-label"> <input type="text"
                    name="password" value="" />
                </label>
            </div>
        </div>

Action Class

public class LoginAction extends BaseAction {

/**
 * 
 */
private static final long serialVersionUID = 1L;

private static final Logger log = LoggerFactory.getLogger(LoginAction.class);

@NotNull(message = "{login.error.email.empty}")
private String email;

@NotNull(message = "{login.error.password.empty}")
private String password;

public String login() {
    return ActionConstants.LOGIN;
}

/**
 * Perform the authentication of the user.
 * 
 * @return The result string which maps to a jsp page.
 */
public String doLogon() {
    String result = null;
    return result;
}

public String getEmail() {
    return email;
}

public void setEmail(String email) {
    this.email = email;
}

public String getPassword() {
    return password;
}

public void setPassword(String password) {
    this.password = password;
}

EDIT 6/10/16

I changed the order of execution for the interceptors so that the defaultStack would go first as my understanding is that it's responsible for populating the beans in the ActionClass. Now, validation is occurring as it should. However, if I leave any of the fields blank, the proper error message is returned but the ActionClass method doLogon is still being called. I am trying to now figure out how to get the beanValidation to short circuit so that the ActionClass is not called.


Solution

  • So it appears that the org.apache.struts.beanvalidation.validation.interceptor. BeanValidationInterceptor is not meant to short circuit the execution of other interceptors. In version 2.5 of the struts bean validation plugin, the doIntercept method simply returns invocation.invoke() which would indicate that even if there are errors, execution will continue.

        protected String doIntercept(ActionInvocation invocation)
        throws Exception
      {
        Validator validator = this.beanValidationManager.getValidator();
        if (validator == null)
        {
          LOG.debug("There is no Bean Validator configured in class path. Skipping Bean validation..");
          return invocation.invoke();
        }
        LOG.debug("Starting bean validation using validator: {}", new Object[] { validator.getClass() });
    
        Object action = invocation.getAction();
        ActionProxy actionProxy = invocation.getProxy();
        String methodName = actionProxy.getMethod();
        if (LOG.isDebugEnabled()) {
          LOG.debug("Validating [{}/{}] with method [{}]", new Object[] { invocation.getProxy().getNamespace(), invocation.getProxy().getActionName(), methodName });
        }
        Collection<Method> annotatedMethods = AnnotationUtils.getAnnotatedMethods(action.getClass(), new Class[] { SkipValidation.class });
        if (!annotatedMethods.contains(getActionMethod(action.getClass(), methodName))) {
          performBeanValidation(action, validator);
        }
        return invocation.invoke();
      }
    

    Because I do not want the ActionClass method to be called if there are errors, I created a copy of the BeanValidationInterceptor.class and added it to the struts.xml. The new class has an instance variable for storing the ConstraintViolations and the doIntercept method returns "input" if there are errors.

    private Set<ConstraintViolation<Object>> constraintViolations;
    
    protected String doIntercept(ActionInvocation invocation) throws Exception {
        Validator validator = this.beanValidationManager.getValidator();
        if (validator == null) {
            LOG.debug("There is no Bean Validator configured in class path. Skipping Bean validation..");
            return invocation.invoke();
        }
        LOG.debug("Starting bean validation using validator: {}", new Object[] { validator.getClass() });
    
        Object action = invocation.getAction();
        ActionProxy actionProxy = invocation.getProxy();
        String methodName = actionProxy.getMethod();
        if (LOG.isDebugEnabled()) {
            LOG.debug("Validating [{}/{}] with method [{}]", new Object[] { invocation.getProxy().getNamespace(),
                    invocation.getProxy().getActionName(), methodName });
        }
        Collection<Method> annotatedMethods = AnnotationUtils.getAnnotatedMethods(action.getClass(),
                new Class[] { SkipValidation.class });
        if (!annotatedMethods.contains(getActionMethod(action.getClass(), methodName))) {
            performBeanValidation(action, validator);
        }
        return this.constraintViolations.isEmpty() ? invocation.invoke() : "input";
    }