DekGenius.com
[ Team LiB ] Previous Section Next Section

4.2 Extending the Controller

In the last chapter, we discussed how to best add features to a front controller. The main problem was that adding new features directly to the controller specializes it. A specialized controller quickly becomes unwieldy, with lots of code for each specific page. Every time we wanted new functionality, we had to rebuild, retest, and redeploy the entire controller. To solve this problem, we looked at the Decorator pattern, which allowed us to dynamically add functionality to the front controller.

With decorators in place, common functions such as security are separated from the front controller. But the controller is still responsible for managing page navigation as well as instantiating actions. Since the set of pages and the set of available actions are likely to change, a tight coupling between them and the front controller is a bad idea. Similar to decorators, we would like to separate the specialized bits of the code for actions and navigation into their own classes and leave the major objects—like the front controller—to act as frameworks.

Here's an example. Imagine a simple, multistep form, such as an online mortgage application.[3] If each page in the form contained embedded references to the next page, it would be very difficult to change the page order. Each affected page, as well as those that came before and after it, would need to be modified to reflect the change. Even worse, these pages could not be reused. If the first page of our mortgage application took basic name and address information, we might want to use the same view in a credit card application. Unfortunately, we can't, since the first mortgage page contains a reference to the second mortgage page: there's no way to direct it to the credit card application instead. This inflexibility stems from the tight coupling of views to controllers.

[3] Yes, we know that in reality these are anything but simple!

To build a flexible application, we need to decouple the pages and controllers. We need the flexibility to add, remove, and reorder pages, reusing them at will. Ideally, we want to do all of this at runtime, while preserving the view-controller separation. The Service to Worker pattern can help us.

4.2.1 The Service to Worker Pattern

The Service to Worker pattern is based on both the Model-View-Controller pattern and the Front Controller pattern. The goal of Service to Worker is to maintain separation between actions, views, and controllers. The service, in this case is the front controller, a central point for handling requests. Like in the Front Controller pattern, the service delegates updating the model to a page-specific action called the worker. So far, Service to Worker is the same as the Front Controller pattern.

In the Service to Worker pattern, an object called the dispatcher performs the task of managing workers and views. The dispatcher encapsulates page selection, and consequently, worker selection. It decouples the behavior of the application from the front controller. To change the order in which pages are shown, for example, only the dispatcher needs to be modified.

The Service to Worker pattern is shown in Figure 4-1. It looks like the Front Controller pattern, with the addition of the dispatcher.

Figure 4-1. The Service to Worker pattern
figs/j2ee_0401.gif

The controller provides an initial point of entry for every request (just like in the Front Controller pattern). It allows common functionality to be added and removed easily, and it can be enhanced using decorators.

The dispatcher encapsulates page selection. In its simplest form, the dispatcher takes some parameters from the request and uses them to select actions and a view. This type of simple dispatcher may be implemented as a method of the front controller. In addition to the requested page, the dispatcher often takes several other factors into account when choosing the next view, the current page, the user permissions, and the validity of the entered information. This type of dispatcher is implemented as a separate class, and ideally, it is configurable via a file at runtime.

The dispatcher uses a set of actions to perform model updates. Each action encapsulates a single, specific update to the model, and only that update. An action might be something as simple as adding a row to a database table, or as complex as coordinating transactions across multiple business objects. Because all navigation is encapsulated in the dispatcher, the actions are not responsible for view selection. As in the Front Controller pattern, actions are usually implemented as an instance of the GoF Command pattern.

The end result of the Service to Worker pattern is an extensible front controller. The controller itself is simply a framework: decorators perform common functions, actions actually update the model, and the dispatcher chooses the resulting view. Because these pieces are loosely coupled, each can operate independently, allowing extension and reuse.

4.2.2 Service to Worker in J2EE

As an example of Service to Worker, let's think about a workflow.[4] A workflow is a sequence of tasks that must be performed in a specific order. Many environments have workflows, such as the mortgage application we discussed earlier and the "wizards" that guide a user through installing software.

[4] This example is based loosely on a proposed workflow language for the Struts framework, part of the Apache project. More information is available from http://jakarta.apache.org/struts/proposal-workflow.html.

Our workflow is very simple, and it is based on our previous examples. It consists of three web pages: one asking the user to log in, another setting a language preference, and a third displaying a summary page. This workflow is shown in Figure 4-2.

Figure 4-2. A simple workflow
figs/j2ee_0402.gif

While it would be easy enough to build this workflow based on the Front Controller pattern, we would like the result to be extensible. We want to be able to add pages and actions without changing the front controller. Ideally, we would like to specify our workflow in XML, so we could change the order of pages without even modifying the dispatcher. Our XML might look like:

<?xml version="1.0" encoding="UTF-8"?>
<workflow>
    <state name="login" action="LoginAction" 
           viewURI="login.jsp" />
    <state name="language" action="LanguageAction" 
           viewURI="language.html" />
    <state name="display" action="RestartAction" 
           viewURI="display.jsp" />
</workflow>

The dispatcher, then, is a simple state machine. When a request is submitted, the dispatcher determines the current state of the session and runs the corresponding action. If the action succeeds, there is a transition to the next state. Once the state is determined, control is forwarded to the associated view.

The interactions in a J2EE Service to Worker implementation are shown in Figure 4-3. To explain all these pieces, we'll work backward, starting with models and views and ending with the front controller.

Figure 4-3. J2EE Service to Worker implementation
figs/j2ee_0403.gif
4.2.2.1 Models and views

The models and views in the Service to Worker pattern remain more or less unchanged from the MVC and Front Controller patterns. As in Chapter 3, our model is the UserBean. The interface has been extended slightly from earlier examples to include a "language" attribute:

package s2wexample.model;

public interface UserBean {
    // the username field
    public String getUsername(  );
    public void setUsername(String username);
  
    // the password field
    public String getPassword(  );
    public void setPassword(String password);
    
    // the language field
    public String getLanguage(  );
    public void setLanguage(String language);
    
    // business methods to perform login
    public boolean doLogin(  );
    public boolean isLoggedIn(  );
}

The views are simple JSP pages that read the model data from our UserBean. Our login page is virtually unchanged from the earlier example. Example 4-1 shows the login page.

Example 4-1. login.jsp
<%@page contentType="text/html"%>
<jsp:useBean id="userbean" scope="session" 
             class="s2wexample.model.UserBean" />
<html>
<head><title>Login</title></head>
 <body>
  <br><br>
  <form action="/pages/workflow" method="post">
   Username:<input type="text" name="username"
   value=<jsp:getProperty name="userbean" property="username"/>>
   <br>
   Password: <input type="password" name="password">
   <br>
   <input type="submit" value="Log In">
  </form>
 </body>
</html>

The only thing to notice here is that our form action is now /pages/workflow. This will be the target of all our links, such as those in the second page, language.html. Since this page does not require any dynamic data, it is stored as a plain HTML file, shown in Example 4-2.

Example 4-2. language.html
<html>
<head><title>Language Selection</title></head>
 <body>
  <br><br>
  <form action="/pages/workflow" method="get">
   Language: <select name="language">
              <option value="En">English</option>
              <option value="Fr">French</option>
             </select>
   <br>
   <input type="submit" value="Continue">
  </form>
 </body>
</html>
4.2.2.2 Actions

Actions, as we mentioned earlier, are implemented as instances of the command pattern. All of our actions will share a simple interface:

public interface Action {
    public boolean performAction(HttpServletRequest req,
                                 ServletContext context);
}

This interface gives each action full access to the request, as well as the servlet context, which includes the HTTP session. Each action reads a different set of parameters from the request. The action then updates the model data, which is accessible through either the request or the session. If the performAction( ) method returns true, the dispatcher knows to move on to the next state. If not, the same state is repeated.

Example 4-3 shows the LoginAction, which is called with input from the login page. This action is also responsible for creating the initial UserBean if it does not exist.

Example 4-3. LoginAction.java
package s2wexample.controller.actions;

import s2wexample.controller.*;
import s2wexample.model.*;
import javax.servlet.http.*;
import javax.servlet.*;

public class LoginAction implements Action {
    public static final String USERBEAN_ATTR = "userbean";
    private static final String NAME_PARAM = "username";
    private static final String PASSWORD_PARAM = "password";
    
    public boolean performAction(HttpServletRequest req, 
                                 ServletContext context) {
        // read request parameters
        String username = req.getParameter(NAME_PARAM);
        String password = req.getParameter(PASSWORD_PARAM);

        // find the UserBean, create if necessary
        HttpSession session = req.getSession(  );
        UserBean ub = (UserBean)session.getAttribute(USERBEAN_ATTR);
        if (ub == null) {
            ub = UserBeanFactory.newInstance(  );
            session.setAttribute(USERBEAN_ATTR, ub); 
        }
        
        // try to login, return the result
        ub.setUsername(username);
        ub.setPassword(password);
        return ub.doLogin(  );
    } 
}
4.2.2.3 The dispatcher

Dispatchers, like actions, come in many flavors. There may be a single master dispatcher or different dispatchers for different parts of an application. Usually, there is one default dispatcher and a few more specialized ones to handle special cases like wizards. In any case, we will define a simple Dispatcher interface to allow uniform access to all the possible dispatchers:

public interface Dispatcher {
    // called after initialization
    public void setContext(ServletContext context)
        throws IOException;
    // called for each request
    public String getNextPage(HttpServletRequest req,
                              ServletContext context);
}

The dispatcher must build and store a simple state machine for each user. As requests come in, the dispatcher retrieves the current state and uses it to determine the correct action to run. If the action succeeds, it displays the view associated with the next state.

Our dispatcher must also process the XML state data. When we see an action attribute, we will convert it to a Java class by loading the class named nameAction, where name is the value of the action attribute.

Internally, we will use simple Java objects to model the states of the workflow. The WorkflowDispatcher is shown in Example 4-4.

Example 4-4. WorkflowDispatcher
 package s2wexample.controller;

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import javax.xml.parsers.*;
import org.w3c.dom.*;

public class WorkflowDispatcher implements Dispatcher {
    // tags expected in the XML
    private static final String WORKFLOW_TAG = "workflow";
    private static final String STATE_TAG = "state";
    private static final String NAME_ATTR = "name";
    private static final String ACTION_ATTR = "action";
    private static final String VIEW_ATTR = "viewURI";
    
    // where to find action classes
    private static final String ACTION_PREFIX = 
        "s2wexample.controller.actions.";
    
    // the internal model of a workflow state
    class State {
        protected String name;
        protected Action action;
        protected String viewUri;
    }
    
    // the current state and state list
    private State[] states;
    private int currentState;
    
    // called by the controller after initialization
    public void setContext(ServletContext context)
        throws IOException {
        InputStream is = 
            context.getResourceAsStream("/LanguageWorkflow.xml");
        try {
            states = parseXML(is);
        } catch(Exception ex) {
            throw new IOException(ex.getMessage(  ));
        }
        currentState = 0;
    }
    
    // choose the next state
    public String getNextPage(HttpServletRequest req,
                              ServletContext context) {
        State s = states[currentState];
        // increment the state only if the action suceeds
        if ((s.action == null) || 
            s.action.performAction(req, context)) {
            if (currentState < states.length - 1) {
                s = states[++currentState];
            } else {
                currentState = 0;
                s = states[currentState];
            }
        }
        
        return s.viewUri;
    }
    
    // parse a state XML file
    private State[] parseXML(InputStream is) throws Exception {
        DocumentBuilderFactory factory = 
            DocumentBuilderFactory.newInstance(  );
        DocumentBuilder builder = factory.newDocumentBuilder(  );
        Document doc = builder.parse(is);
        
        // find the workflow element
        NodeList workflows = doc.getElementsByTagName(WORKFLOW_TAG);
        Element workflow = (Element)workflows.item(0);
        
        // find all the states
        NodeList states = doc.getElementsByTagName(STATE_TAG);
        State[] stateList = new State[states.getLength(  )];
        
        // read state information
        for(int i = 0; i < states.getLength(  ); i++) {
            stateList[i] = new State(  );
            
            Element curState = (Element)states.item(i);
            stateList[i].name = curState.getAttribute(NAME_ATTR);
            stateList[i].viewUri = curState.getAttribute(VIEW_ATTR);
            
            // convert actions names into class instances
            String action = curState.getAttribute(ACTION_ATTR);
            if (action != null && action.length(  ) > 0) {
                Class c = Class.forName(ACTION_PREFIX + action);
                stateList[i].action = (Action)c.newInstance(  );
            }
        }
        
        return stateList;
    }
}
4.2.2.4 The front controller

Last but not least is the front controller, which is more of a framework than a container for actual application logic at this point. The controller's job right now is to manage dispatchers, using them to choose the next view. Once the view is chosen, the front controller passes control to the view, and the front controller's job is finished. Example 4-5 shows our front controller servlet.

Example 4-5. FrontController.java
package s2wexample.controller;

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import s2wexample.model.*;

public class FrontController extends HttpServlet {
    private static final String DISPATCHER_ATTR = "Dispatcher";
    private static final String DISPATCHER_PREFIX =
        "s2wexample.controller.";
    
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
    }
    
    // process get requests
    public void doGet(HttpServletRequest request,        
                      HttpServletResponse response)
    throws ServletException, IOException {
        process(request, response);
    }
    
    // process post requests
    public void doPost(HttpServletRequest request, 
                       HttpServletResponse response)
    throws ServletException, IOException {
        process(request, response);
    }
    
    // common processing routine
    public void process(HttpServletRequest request, 
                        HttpServletResponse response)
    throws ServletException, IOException {
        HttpSession session = request.getSession(  );
        ServletContext context = getServletContext(  );
        
        // get the last element of the request in lower case
        String reqPath = request.getPathInfo(  );
        reqPath = Character.toUpperCase(reqPath.charAt(1)) +
            reqPath.substring(2).toLowerCase(  );
        
        // find the dispatcher in the session
        Dispatcher dispatcher =
            (Dispatcher)session.getAttribute(reqPath + 
                                             DISPATCHER_ATTR);
        // if no dispatcher was found, create one
        if (dispatcher == null) {
            String className = reqPath + "Dispatcher";
            try {
                Class c = Class.forName(DISPATCHER_PREFIX + 
                                        className);
                dispatcher = (Dispatcher)c.newInstance(  );
            } catch(Exception ex) {
                throw new ServletException("Can't find class " + 
                                           className, ex); 
            }
            
            // store the dispatcher in the session
            dispatcher.setContext(context);
            session.setAttribute(reqPath + DISPATCHER_ATTR, 
                                 dispatcher);
        }
        
        // use the dispatcher to find the next page
        String nextPage = dispatcher.getNextPage(request, context);

        // make sure we don't cache dynamic data
        response.setHeader("Cache-Control", "no-cache");
        response.setHeader("Pragma", "no-cache");
        
        // forward control to the view
        RequestDispatcher forwarder = 
            request.getRequestDispatcher("/" + nextPage);
        forwarder.forward(request, response);
    }
}

Notice that the front controller manages dispatchers in the same way that the dispatchers manage actions. Dispatchers are mapped based on the requested URL. Since all our views used the path /pages/workflow in their requests, the front controller maps these requests to an instance of the WorkflowDispatcher class. As a result, the same application could use many different dispatchers for different parts of the web site. In practice, the mapping between URLs and dispatchers is usually done with an XML file, just like for actions.

Using the Service to Worker pattern, the controller has been divided up into a set of reusable components: a front controller, dispatchers, and actions. Adding a new page is dynamic: you simply create the view JSP and corresponding action class and add it all to an XML file. To add, remove, or reorder pages, we only need to change the XML.

The Service to Worker pattern, as we have presented it, is quite flexible. The simple navigation model of our example, however, is only really appropriate for workflows. Even branches to the linear flow of pages are not covered. We won't cover all the possible dispatchers and actions here. Both the Jakarta Struts project and Sun's J2EE sample applications contain more advanced implementations of the Service to Worker patterns, and they are a good place to look for more information.

    [ Team LiB ] Previous Section Next Section