Why validate() called twice on initial entry on JSP in Struts 2

I’m beginning to write a login page, and my initial tests show me that the validate method is being called twice when my login.jsp is first entered, but not when I submit the login form. I want to understand why validate() is called twice and how to stop it.

First, here is my struts.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts
     PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.1//EN"
            "http://struts.apache.org/dtds/struts-2.1.dtd">

<struts>
  <constant name="struts.devMode" value="true"/>
  <constant name="struts.i18n.reload" value="true" />
  <constant name="struts.ui.theme" value="simple"/>
  <constant name="struts.custom.i18n.resources"
            value="resources.i18n.portalLogin, resources.i18n.errors"/>
  <constant name="struts.action.extension" value="action"/>
  <constant name="struts.ognl.allowStaticMethodAccess" value="true" />

  <!-- Configuration for the default package. -->
  <package name="default" extends="json-default">

    <interceptors>
      <interceptor name="security"
                   class="login.interceptor.SecurityInterceptor"/>
      <interceptor-stack name="appDefaultStack">
        <interceptor-ref name="jsonValidationWorkflowStack"/>
        <interceptor-ref name="defaultStack">
          <param name="exception.logEnabled">true</param>
          <param name="exception.logLevel">ERROR</param>
          <!-- 
              Exclude the jQuery no-cache parameter and the jQuery Displaytag
              parameters from processing by the ParametersInterceptor 
           -->
          <param name="params.excludeParams">_,d-d+-[sop]</param>
        </interceptor-ref>
        <interceptor-ref name="security"></interceptor-ref>
      </interceptor-stack>
    </interceptors>

    <default-interceptor-ref name="appDefaultStack" />

    <global-results>
      <result name="error">/jsp/error/systemError.jsp</result>
      <result name="security">/jsp/error/accessDenied.jsp</result>
    </global-results>

    <global-exception-mappings>
      <exception-mapping result="error" exception="java.lang.Exception"/>
    </global-exception-mappings>

    <action name="Login" class="login.action.Login">
      <result name="input">/jsp/login.jsp</result>
      <result name="success" type="redirect">
        <param name="ActionName">LoginRedirect</param>
      </result>
    </action>
  </package>
</struts>

Here is my index.jsp:

<!DOCTYPE HTML>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() +
                  ":" + request.getServerPort() + path;

    // prevent caching these pages at clients.
    response.setHeader("Cache-Control", "no-cache"); // HTTP 1.1
    response.setHeader("Pragma", "no-cache"); // HTTP 1.0
    response.setDateHeader("Expires", 0); // prevents caching at proxy
%>

<html>
  <head>
    <base href="<%=basePath%>">
    <title>Login</title>
  </head>

  <script>
    location.href = '<%= basePath %>/Login.action';
  </script>

  <body>
    Please go to the <a href="<%= basePath %>/Login.action">Login</a> page.<br>
  </body>
</html>

Here is my login.jsp:

<%@ taglib prefix='s' uri='/struts-tags' %>
<%@ taglib prefix='tiles' uri='/tiles' %>

<tiles:insertDefinition name='fullLayout'>
  <tiles:putAttribute name='title' cascade='true'>
    <s:text name='layout.title'/>
  </tiles:putAttribute>
  <tiles:putAttribute name='content' cascade='true'>
    <s:text name='instructions.text'/>
    <s:form name='Login' action='Login' method='POST' validate='true' focusElement="userId"
            autocomplete='off'>
      <div class='userIdInput'>
        <label class='formLabel required' for='userId'>
          <s:text name='text.userID' />
        </label>
        <s:textfield cssClass='inputText' name='userId' id='userId' value="%{userId}" />
      </div>
      <div class='passwordInput'>
        <label class='formLabel required' for='password'>
          <s:text name='text.password' />
        </label>
        <s:textfield cssClass='inputText' name='password' id='password' type='password' />
      </div>
      <input id='Login' type='submit' title='<s:text name="login.submit" />'
             value='<s:text name="login.submit" />'>
    </s:form>
  </tiles:putAttribute>
</tiles:insertDefinition>

Here is my Login.java Action:

package login.action;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Map;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.apache.struts2.interceptor.ServletRequestAware;

import com.opensymphony.xwork2.ActionSupport;
import logging.Logger;
import common.util.StringUtils;

/**
 * The main Action class for this application
 */
public class Login extends ActionSupport implements ServletRequestAware {
    private static final long serialVersionUID = 1L;

    private HttpServletRequest httpRequest;
    private String userId;
    private String password;

    /* (non-Javadoc)
     * @see com.opensymphony.xwork2.Action#execute()
     */
    @Override
    public String execute() {
        Logger.debug(this, "execute(" + hashCode() + "): Begin");
        boolean debug = Logger.canLogDebug(this);
        HttpSession session = httpRequest.getSession();

        if ((userId == null || userId.isEmpty()) &&
            (password == null || password.isEmpty())) {
            if (debug) {
                Logger.debug(this, "    No userID and no Password.");
                Logger.debug(this, "execute End");
            }
            return INPUT;
        }

        if (debug) {
            Logger.debug(this, "userId   = " + userId);
            Logger.debug(this,
                    "    password is " +
                    ((password == null || password.isEmpty()) ? "not " : "") +
                    "set");
        }

        Logger.debug(this, "execute End");
        return SUCCESS;
    }

    /**
     * @return String
     */
    public String getUserId() {
        return userId;
    }

    /**
     * @param String userId
     */
    public void setUserId(String userId) {
        Logger.debug(this, "in setUserId(" + userId + ")");
        this.userId = userId;
    }

    /**
     * @param String password
     */
    public void setPassword(String password) {
        Logger.debug(this, "in setPassword(" + password + ")");
        this.password = password;
    }

    /* (non-Javadoc)
     * @see com.opensymphony.xwork2.ActionSupport#validate()
     */
    @Override
    public void validate() {
        Logger.debug(this, "validate(" + hashCode() + "): Begin");
        if ((userId == null || userId.isEmpty()) &&
            (password == null || password.isEmpty())) {
            if (Logger.canLogDebug(this)) {
                Logger.debug(this, "    No userID and no Password.");
                Logger.debug(this, "validateLogin: End");
            }
            return;
        }

        if ((userId == null || userId.isEmpty())) {
            Logger.debug(this, "    No userID.");
            addActionError(getText("missing.required",
                           new String[] {getText("text.userID")}));
        }
        if ((password == null || password.isEmpty())) {
            Logger.debug(this, "    No Password.");
            addActionError(getText("missing.required",
                           new String[] {getText("text.password")}));
        }
        Logger.debug(this, "validate: End");
    }

    /* (non-Javadoc)
     * @see ServletRequestAware#setServletRequest(HttpServletRequest)
     */
    @Override
    public void setServletRequest(HttpServletRequest httpRequest) {
        this.httpRequest = httpRequest;
    }
}

Here is my SecurityInterceptor.java:

package login.interceptor;

import javax.servlet.http.HttpServletRequest;

import org.apache.struts2.interceptor.ServletRequestAware;

import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
import logging.Logger;

public class SecurityInterceptor extends AbstractInterceptor
                                 implements ServletRequestAware {
    private static final long serialVersionUID = 1L;
    HttpServletRequest servletRequest = null;

    @Override
    public String intercept(ActionInvocation invocation) throws Exception {
        boolean debug = Logger.canLogDebug(this);
        if (debug) {
            Logger.debug(this, "intercept: Begin");
            Logger.debug(this, "invocation = " + 
                               invocation.getAction().getClass().getName());
        }

        //TODO: Siteminder integration

        Logger.debug(this, "intercept: End");
        return invocation.invoke();
    }

    /* (non-Javadoc)
     * @see ServletRequestAware#setServletRequest(HttpServletRequest)
     */
    @Override
    public void setServletRequest(HttpServletRequest servletRequest) {
        this.servletRequest = servletRequest;
    }
}

And, finally, here is my logged information:

Initial JSP Entry

2016/01/21 15:17:50.470| DEBUG|                      login.action.Login| validate(12565027): Begin
2016/01/21 15:17:50.473| DEBUG|                      login.action.Login|     No userID and no Password.
2016/01/21 15:17:50.475| DEBUG|                      login.action.Login| validateLogin: End
2016/01/21 15:17:50.480| DEBUG|                      login.action.Login| validate(12565027): Begin
2016/01/21 15:17:50.480| DEBUG|                      login.action.Login|     No userID and no Password.
2016/01/21 15:17:50.480| DEBUG|                      login.action.Login| validateLogin: End
2016/01/21 15:17:50.480| DEBUG|   login.interceptor.SecurityInterceptor| intercept: Begin
2016/01/21 15:17:50.480| DEBUG|   login.interceptor.SecurityInterceptor| invocation = login.action.Login
2016/01/21 15:17:50.480| DEBUG|   login.interceptor.SecurityInterceptor| intercept: End
2016/01/21 15:17:50.480| DEBUG|                      login.action.Login| execute(12565027): Begin
2016/01/21 15:17:50.480| DEBUG|                      login.action.Login|     No userID and no Password.
2016/01/21 15:17:50.480| DEBUG|                      login.action.Login| execute End

After Form Submit

2016/01/21 15:21:14.071| DEBUG|                      login.action.Login| in setPassword()
2016/01/21 15:21:14.071| DEBUG|                      login.action.Login| in setUserId(mike)
2016/01/21 15:21:14.071| DEBUG|                      login.action.Login| validate(25137882): Begin
2016/01/21 15:21:14.072| DEBUG|                      login.action.Login|     No Password.
2016/01/21 15:21:14.110| DEBUG|                      login.action.Login| validate: End

Answer

Because you reference it twice in the interceptor stack used by default. Both stacks include validation interceptor which calls validate.

<interceptor-ref name="jsonValidationWorkflowStack"/>
<interceptor-ref name="defaultStack">

Just remove the second, the first stack include basicStack that is a minimum your application should use. Other interceptors you can add in your custom stack or action.