ValidationInterceptor.java

/*
 * 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.validator;

import org.apache.struts2.ActionInvocation;
import org.apache.struts2.ActionProxy;
import org.apache.struts2.Validateable;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.interceptor.MethodFilterInterceptor;
import org.apache.struts2.interceptor.PrefixMethodInvocationUtil;
import org.apache.struts2.interceptor.ValidationAware;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
 * <!-- START SNIPPET: description -->
 *
 * <p>
 * This interceptor runs the action through the standard validation framework, which in turn checks the action against
 * any validation rules (found in files such as <i>ActionClass-validation.xml</i>) and adds field-level and action-level
 * error messages (provided that the action implements {@link ValidationAware}). This interceptor
 * is often one of the last (or second to last) interceptors applied in a stack, as it assumes that all values have
 * already been set on the action.
 * </p>
 *
 * <p>
 * This interceptor does nothing if the name of the method being invoked is specified in the <b>excludeMethods</b>
 * parameter. <b>excludeMethods</b> accepts a comma-delimited list of method names. For example, requests to
 * <b>foo!input.action</b> and <b>foo!back.action</b> will be skipped by this interceptor if you set the
 * <b>excludeMethods</b> parameter to "input, back".
 * </p>
 *
 * <p>
 * The workflow of the action request does not change due to this interceptor. Rather,
 * this interceptor is often used in conjunction with the <b>workflow</b> interceptor.
 * </p>
 *
 * <p>
 * <b>NOTE:</b> As this method extends off MethodFilterInterceptor, it is capable of
 * deciding if it is applicable only to selective methods in the action class. See
 * <code>MethodFilterInterceptor</code> for more info.
 * </p>
 *
 * <!-- END SNIPPET: description -->
 *
 * <p><u>Interceptor parameters:</u></p>
 *
 * <!-- START SNIPPET: parameters -->
 *
 * <ul>
 *
 * <li>alwaysInvokeValidate - Defaults to true. If true validate() method will always
 * be invoked, otherwise it will not.</li>
 *
 * <li>programmatic - Defaults to true. If true and the action is Validateable call validate(),
 * and any method that starts with "validate".
 * </li>
 *
 * <li>declarative - Defaults to true. Perform validation based on xml or annotations.</li>
 *
 * </ul>
 *
 * <!-- END SNIPPET: parameters -->
 *
 * <p><u>Extending the interceptor:</u></p>
 *
 * <!-- START SNIPPET: extending -->
 *
 * There are no known extension points for this interceptor.
 *
 * <!-- END SNIPPET: extending -->
 *
 * <p><u>Example code:</u></p>
 *
 * <pre>
 * <!-- START SNIPPET: example -->
 *
 * &lt;action name=&quot;someAction&quot; class=&quot;com.examples.SomeAction&quot;&gt;
 *     &lt;interceptor-ref name=&quot;params&quot;/&gt;
 *     &lt;interceptor-ref name=&quot;validation&quot;/&gt;
 *     &lt;interceptor-ref name=&quot;workflow&quot;/&gt;
 *     &lt;result name=&quot;success&quot;&gt;good_result.ftl&lt;/result&gt;
 * &lt;/action&gt;
 *
 * &lt;-- in the following case myMethod of the action class will not
 *        get validated --&gt;
 * &lt;action name=&quot;someAction&quot; class=&quot;com.examples.SomeAction&quot;&gt;
 *     &lt;interceptor-ref name=&quot;params&quot;/&gt;
 *     &lt;interceptor-ref name=&quot;validation&quot;&gt;
 *         &lt;param name=&quot;excludeMethods&quot;&gt;myMethod&lt;/param&gt;
 *     &lt;/interceptor-ref&gt;
 *     &lt;interceptor-ref name=&quot;workflow&quot;/&gt;
 *     &lt;result name=&quot;success&quot;&gt;good_result.ftl&lt;/result&gt;
 * &lt;/action&gt;
 *
 * &lt;-- in the following case only annotated methods of the action class will
 *        be validated --&gt;
 * &lt;action name=&quot;someAction&quot; class=&quot;com.examples.SomeAction&quot;&gt;
 *     &lt;interceptor-ref name=&quot;params&quot;/&gt;
 *     &lt;interceptor-ref name=&quot;validation&quot;&gt;
 *         &lt;param name=&quot;validateAnnotatedMethodOnly&quot;&gt;true&lt;/param&gt;
 *     &lt;/interceptor-ref&gt;
 *     &lt;interceptor-ref name=&quot;workflow&quot;/&gt;
 *     &lt;result name=&quot;success&quot;&gt;good_result.ftl&lt;/result&gt;
 * &lt;/action&gt;
 *
 *
 * <!-- END SNIPPET: example -->
 * </pre>
 *
 * @author Jason Carreira
 * @author Rainer Hermanns
 * @author <a href='mailto:the_mindstorm[at]evolva[dot]ro'>Alexandru Popescu</a>
 * @see ActionValidatorManager
 * @see org.apache.struts2.interceptor.DefaultWorkflowInterceptor
 */
public class ValidationInterceptor extends MethodFilterInterceptor {

    private static final Logger LOG = LogManager.getLogger(ValidationInterceptor.class);

    private final static String VALIDATE_PREFIX = "validate";
    private final static String ALT_VALIDATE_PREFIX = "validateDo";

    private boolean validateAnnotatedMethodOnly;

    private ActionValidatorManager actionValidatorManager;

    private boolean alwaysInvokeValidate = true;
    private boolean programmatic = true;
    private boolean declarative = true;

    @Inject
    public void setActionValidatorManager(ActionValidatorManager mgr) {
        this.actionValidatorManager = mgr;
    }

    /**
     * Determines if {@link 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 validation based on annotations or xml should be performed. Defaults
     * to "true".
     *
     * @param declarative <tt>true</tt> then perform validation based on annotations or xml.
     */
    public void setDeclarative(boolean declarative) {
        this.declarative = declarative;
    }

    /**
     * Determines if {@link 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);
    }

    /**
     * Gets if <code>validate()</code> should always be called or only per annotated method.
     *
     * @return <tt>true</tt> to only validate per annotated method, otherwise <tt>false</tt> to always validate.
     */
    public boolean isValidateAnnotatedMethodOnly() {
        return validateAnnotatedMethodOnly;
    }

    /**
     * Determine if <code>validate()</code> should always be called or only per annotated method.
     * Default to <tt>false</tt>.
     *
     * @param validateAnnotatedMethodOnly  <tt>true</tt> to only validate per annotated method, otherwise <tt>false</tt> to always validate.
     */
    public void setValidateAnnotatedMethodOnly(boolean validateAnnotatedMethodOnly) {
        this.validateAnnotatedMethodOnly = validateAnnotatedMethodOnly;
    }

    /**
     * Gets the current action and its context and delegates to {@link ActionValidatorManager} proper validate method.
     *
     * @param invocation  the execution state of the Action.
     * @throws Exception if an error occurs validating the action.
     */
    protected void doBeforeInvocation(ActionInvocation invocation) throws Exception {
        Object action = invocation.getAction();
        ActionProxy proxy = invocation.getProxy();

        //the action name has to be from the url, otherwise validators that use aliases, like
        //MyActio-someaction-validator.xml will not be found, see WW-3194
        //UPDATE:  see WW-3753
        String context = this.getValidationContext(proxy);
        String method = proxy.getMethod();

        if (LOG.isDebugEnabled()) {
            LOG.debug("Validating {}/{} with method {}.", invocation.getProxy().getNamespace(), invocation.getProxy().getActionName(), method);
        }


        if (declarative) {
           if (validateAnnotatedMethodOnly) {
               actionValidatorManager.validate(action, context, method);
           } else {
               actionValidatorManager.validate(action, context);
           }
       }

        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 occured 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;
            }
        }
    }

    @Override
    protected String doIntercept(ActionInvocation invocation) throws Exception {
        doBeforeInvocation(invocation);
        return invocation.invoke();
    }

    /**
     * <p>
     * Returns the context that will be used by the
     * {@link ActionValidatorManager} to associate the action invocation with
     * the appropriate {@link ValidatorConfig ValidatorConfigs}.
     * </p>
     *
     * <p>
     * The context returned is used in the pattern
     * <i>ActionClass-context-validation.xml</i>
     * </p>
     *
     * <p>
     * The default context is the action name from the URL, but the method can
     * be overridden to implement custom contexts.
     * </p>
     *
     * <p>
     * This can be useful in cases in which a single action and a single model
     * require vastly different validation based on some condition.
     * </p>
     *
     * @param proxy action proxy
     * @return the context (action name)
     */
    protected String getValidationContext(ActionProxy proxy) {
        // This method created for WW-3753
        return proxy.getActionName();
    }

}