Struts Component Lifecycle In Synchronous Mode

In synchronous mode the component lifecycle is managed in a five-step process:

  1. The cycle begins with the initial load of a composite page, starting the render phase.
  2. When the JSP processor encounters a <c:import> action in a composite page, it generates an HTTP request to obtain the content of the included component.

  3. The Action class that manages the component receives an "empty" GET request. The Action class forwards to a view relevant to component state. After all components on the composite page have rendered themselves, the render phase finishes and the composite page is presented to a user.
  4. The user initiates the input phase by submitting an HTML form or by activating a command link. The browser sends input data to an Action that manages the component. The Action processes data and updates component state if needed. Component state can be stored in a session-scoped form bean, in a database or in other location.
  5. After input data has been processed, the Action automatically redirects to location of the composite page, effectively switching from input phase back to render phase. Steps 1 through 3 are repeated, and an updated page is presented to the user.

action_component_title.gif

Component Configuration

Defining configuration before creating a component seems backwards, but different configuration options affect the component, so for the sake of this example let us first define the composite page and the login component in the struts-config.xml file. Do not forget to turn automatic validation off, otherwise if an associated form bean fails validation, an event handler in the Action will not be called.

In Struts 1.4 event definitions are first-class elements of an action mapping:

{{{<struts-config>

</struts-config> }}}

Observe attributes and properties new for Struts 1.4:

In Struts 1.2.9 - 1.3.x Action class does not implement dispatching functionality, and action mapping does not allow to define events. EventDispatchAction class is the best choice to handle component events. The events are configured through parameter attribute:

Struts 1.2.9, 1.3.x: {{{<struts-config>

</struts-config> }}}

Of course, Struts 1.2.9, 1.3.x does not support new "component", "view", "form" and "event" properties. Use regular forward element to define component views. Use name attribute to define associated form bean.

Component Action

With Struts 1.4 the component action class is deceptively simple. In our example it handles two events, the corresponding handlers are called automatically by Action class. The location of component's view is defined in the action mapping, so there is no need to return an ActionForward object pointing to a JSP file. On the other hand, most non-trivial components need to process data before rendering themselves or to exchange data with other components. For such cases you can use render method. Its default implementation does nothing.

public class LoginAction extends Action {

    public ActionForward login (ActionMapping mapping,
                                ActionForm form,
                                HttpServletRequest request,
                                HttpServletResponse response) throws Exception {

        HttpSession session = request.getSession();
        LoginForm inputForm = (LoginForm) form;

        // Log out current user first
        request.getSession().removeAttribute("USER");

        // Validation is turned off in struts-config.xml,
        // so explicitly validate user input;
        ActionMessages errors = inputForm.validate(mapping, request);

        if (errors != null) {
            saveErrors(session, errors);
        } else {
            // Use this session attribute to hold user's name
            session.setAttribute("USER", inputForm.getUsername());
        }

        // Always return null.
        return null;
    }

    public ActionForward logout (ActionMapping mapping,
                                 ActionForm form,
                                 HttpServletRequest request,
                                 HttpServletResponse response) throws Exception {

        LoginForm inputForm = (LoginForm) form;

        // Clean name and password in the input/output form bean
        inputForm.setUsername(null);
        inputForm.setPassword(null);

        // Remove user name from the session, effectively logging out
        request.getSession().removeAttribute("USER");

        // Always return null.
        return null;
    }
}

In Struts 1.2.9 - 1.3.x Action class does not implement dispatching functionality, you need to extend a dispatching action. Use EventDispatchAction as the simplest and the most flexible choice. Another difference is that you need to specify an ActionForward object that points to a component view. You must do it in unspecified method or whatever method you selected as default in event definition.

Using EventDispatchAction or EventActionDispatcher to handle incoming events gets you only halfway, because the code that automatically distinguishes the address of a composite page and then redirects to it is not present in older Struts versions. You need an add-on library that contains this code. Then you need to call it from execute method of your action. TODO

Struts 1.2.9, 1.3.x:

public class LoginAction extends Action {

    // Instantiate event dispatcher
    protected ActionDispatcher dispatcher = new EventActionDispatcher (this);


    public ActionForward execute(ActionMapping mapping,
                                 ActionForm form,
                                 HttpServletRequest request,
                                 HttpServletResponse response)
        throws Exception {
        
        // Set component ID in page context, so other tags inside this
        // component knew the component name
        request.setAttribute(org.apache.struts.component.Constants.COMPONENT_ID, "Login");

        // Dispatch requst to a handler
        ActionForward dispatchedForward =
            dispatcher.execute(mapping, form, request, response);
        
        // Process this action as component action
        return processComponentRequest(form, request, response, dispatchedForward, mapping);
    }


    public ActionForward execute (...) throws Exception {
        ComponentUtils.componentReload(...); // TODO
    }

    public ActionForward login (...) throws Exception {
        ...
    }

    public ActionForward logout (...) throws Exception {
        ...
    }

    public ActionForward unspecified (ActionMapping mapping,
                                      ActionForm form,
                                      HttpServletRequest request,
                                      HttpServletResponse response) throws Exception {
        return mapping.findForward("view");
    }
}

Component Form Bean

Nothing exciting here, just a session-scoped form to hold user name and to validate credentials:

public class LoginForm extends ActionForm {

    private String username;
    public String getUsername() {return username;}
    public void setUsername(String username) {this.username = username;}

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

    // Generate the error messages in the same manner as usual,
    // but do not forget to turn "validate" property of the action mapping off.
    public ActionErrors validate(ActionMapping mapping, HttpServletRequest request) {
        if (!"guest".equalsIgnoreCase(username) ||
            !"pass".equalsIgnoreCase(password)) {
            ActionErrors errors = new ActionErrors();
            errors.add("ERROR", new ActionMessage("login.badpassword"));
            return errors;
        } else {
            return null;
        }
    }
}

A common misconception is that components must use session-scoped form beans. This is not exactly true, but you might want to save data between request somehow, because Struts uses redirection to reload the composite page. Whether you store the data in session-scoped form bean or in database or in disk file is your choice. Obviously, request-scoped data will be lost after redirect.

Login And Logout Views

The Login Component has two subviews, one for a non-logged-in user, another for a logged-in user. In this example views are defined in one JSP file. It is possible to define them in different JSP files, but in this case you will not be able to use view attribute of an action mapping, and you will have to explicitly forward to view locations in a similar manner as with Struts 1.3.x.

<%@ page contentType="text/xml;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib uri="http://struts.apache.org/tags-bean" prefix="bean" %>
<%@ taglib uri="http://struts.apache.org/tags-html" prefix="html" %>
<%@ taglib uri="http://struts.apache.org/tags-logic" prefix="logic" %>
<%@ taglib uri="http://struts.apache.org/tags-comp" prefix="comp" %>

<comp:component>

<%-- "Not Logged In" subview --%>

<c:if test='${empty USER}'>
  <h3>Please Log In</h3>

  <!-- Displaying Struts errors -->
  <logic:messagesPresent>
    <html:messages id="error">
      <li><bean:write name="error"/></li>
    </html:messages>
  </logic:messagesPresent><br/>

  <html:form action="/loginintegrated.do">
    <label for="username">Username:</label>
    <input type="text" name="username" value="${loginform.username}"/>

    <label for="password">Password:</label>
    <input type="text" name="password" value="" class="datavalue"/>

    <input type="submit" name="loginEvent" value="Log In"/>
  </html:form>
</c:if>

<%-- "Logged In" subview --%>

<c:if test='${not empty USER}'>
  <h3>User Information</h3>

  <html:form action="/loginintegrated.do">
    Current user: ${USER} <br/>
    <input type="submit" name="logoutEvent" value="Log Out"/><br/>
  </html:form>
</c:if>

</comp:component>

Composite Page

Now we need to include the Login Component into a larger page (composite page). This is done with JSTL <c:import> tag. Do not use <jsp:include>, it may not work in some containers:

{{{<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<html>

</html>}}}

Done!

This is pretty much it. Run the application and navigate to composite page. The included component will evaluate user's state and will display a login form. Try to log in. The submitted credentials are sent directly to a component; if they are not correct, the composite page is redisplayed. How does it happen? The improved Action class as well as JSP tags work together behind the scenes to distinguish the address of a composite page during first render. This address is saved automatically. Then after component finishes, it reloads the composite page using saved address.

Next: Building dual-mode Struts web component

Gotchas And TODOs

* Struts 1.2.9-1.3.x: Need a separate tag for that encodes reload address within an html:form * It is not possible to navigate to a different page from async request. Input elements that correspont to "always sync" events should not have "strutsCommand" CSS class. * Must ensure that in sync mode forwarding works too, not just redirection * Currently custom Javascript handler defined by a user is thrown away. Ensure that it is possiblt to combine Ajax engine Javascript with user's Javascript in events like "onsubmit" or "onclick".

StrutsManualActionWebComponentSync (last edited 2009-09-20 23:12:19 by localhost)