Chrysalis Controllers

Controllers are the heart of the Chrysalis system. Controllers manage the business logic for system updates. Chrysalis controllers resemble "regular" Java classes, in the sense that they have methods with normal parameters for updating business data. They make no direct reference to the servlet API.

public class CartController extends Controller {
  private Map items = new HashMap();
  
  public void addItem(Long itemId) throws Exception {
    items.put(itemId, Item.load(itemId));
  }

  public void removeItems(Long[] itemId) {
    for (int i=0; i<itemId.length; i++) {
      items.remove(itemId[i]);
    }
  }

  public void placeOrder() throws Exception {
    // Use ids in items collection to create an order
  }

  public Collection getItems() {
    return this.items.values();
  }
}

Controllers should be written following normal object-orientated design principles: put methods together with the data they manipulate. Most of the time, controllers can be organized around the application's use cases.

Defining Controllers and Controller Methods

A Chrysalis controller can be written using the normal rules of the Java language, with the following restrictions:

  1. It must be a subclass of Controller: Controllers must descend from org.chwf.servlet.Controller. They do not need to be direct subclasses.
  2. Its class name must be XxxController: For example, CatalogController or CartController.
  3. It must have a no-parameter constructor: Most controllers define no constructor, using the default no-parameter constructor created by the Java compiler.
  4. Its instance variables must be transient or Serializable: This is because the controller itself must be Serializable.
  5. Its public methods cannot be overloaded: The mechanism Chrysalis uses to invoke controllers will not work if the controller has more than one method with the same name.
  6. Its public method parameters must be Simple or Complex types.

Simple and Complex types are defined as follows:

  • A Simple type fits into a single form field and has a converter in the org.chwf.converter package. This includes all primitive types, primitive object wrappers, strings and dates. You can define new simple types by creating new converters.
  • A Complex type must be a JavaBean that has an initializer method (discussed below).

All public instance methods of a controller may be invoked through the Chrysalis framework, excluding only the public methods inherited from the org.chwf.servlet.Controller and java.lang.Object classes. These callable methods are referred to as Controller Methods. The controller may also have static methods or methods with access restrictions (protected, package or private), but these methods have no direct impact on the Chrysalis system.

Controller method parameters correspond to the request parameters in the Command URL used to invoke the controller:

Catalog.editItem.cmd?itemId=234&name=Shirt&stock=120
public class CatalogController extends Controller {
  public void editItem(Long itemId, String name, int stock) {
    // Logic to update and save item data ...
  }
}

Because Java Reflection cannot identify method parameter names, these names must be specified in the controller package configuration:

<!-- chrysalis.xml for the Controller package -->
<config>
  <class name="CatalogController">
    <method name="editItem" parameters="itemId, name, stock" />
  </class>
  
  <!-- Other configuration ... -->
  
</config>

Note: Because controllers resemble normal Java classes, you might be tempted to use the controllers themselves as the business objects for your application. This is bad design. Chrysalis controllers are tied to the servlet environment (albeit subtly) and will not be reusable in other contexts. You should create a business object layer that is completely independent of the web environment to facilitate testing and reuse.

JavaBean Initializers

There is one special kind of controller method referred to as initializer methods. Initializers return data values available through the controller. Initializers must follow these additional rules:

  • Initializer method parameters must be Simple types.
  • Initializer method names must begin with " get ".
  • Initializer methods must return a JavaBean.

Although initializer methods resemble JavaBean getter methods, they do not completely comply with the JavaBean standard, because they may have method parameters. Initializers are used to create bean objects that are updated with request data and passed as parameters to other controller methods.

public class CatalogController extends Controller {

  public Item getItem(Long itemId) throws Exception {
    return Item.load(itemId);
  }

  // Form data will be copied into the Item object.

  public void editItem(Item item) throws Exception {
    item.save();
    setView("/showItem.jsp", "itemId", item.getItemId());
  }
}

In the example above:

  1. The getItem() initializer method will be called to load the item JavaBean.
  2. Form data will be copied into the item object using its setter methods.
  3. The item object will be passed as a parameter to the editItem() controller method as a method parameter.

Initializers are invoked based on the parameter name, not the JavaBean's class name. These makes it possible to define more than one initializer for the same class.

public class CatalogController extends Controller {

  public Item getItem(Long itemId) throws Exception {
    return Item.load(itemId);
  }

  public void editItem(Item item) throws Exception {
    item.save();
    setView("/showItem.jsp", "itemId", item.getItemId());
  }

  public Item getNewItem() throws Exception {
    return new Item();
  }

  public void createItem(Item newItem) throws Exception {
    item.save();
    setView("/showItem.jsp", "itemId", item.getItemId());
  }
}
<!-- chrysalis.xml for the Controller package -->
<config>
  <class name="CatalogController">
    <method name="getItem" parameters="itemId" />
    <method name="editItem" parameters="item" />
    <method name="getNewItem" /> <!-- No parameters -->
    <method name="createItem" parameter="newItem" />
  </class>
  
  <!-- Other configuration ... -->
  
</config>

Initializers are also invoked by the <jutil:use> tag to load controller data into JSP pages. The tag below will call the getOrder() method of the CartController:

<jutil:use var="order" controller="Cart" />

Initializers can create new beans, load bean data from a data source or return controller instance variable values:

public class CartController extends Controller {
  private Order currentOrder;

  public Order getOrder() {
    return this.currentOrder;
  }

  public void placeOrder() throws Exception {
    Order order = new Order();
    // Update order with items in the cart
    order.save();
    this.currentOrder = order;
  }
}

In terms of OO design, normal controller methods are the mutators of the Controller class, while initializers are its accessors. Clean OO design dictates that accessors should have no side effects (do not alter system data) and mutators return no data except error messages (exceptions).

Controller methods that are not initializers normally have a return value of void. Any non-initializer return value is ignored by the Chrysalis system, but it could be used for out-of-container unit testing.

Invoking Controller Methods

Controller methods are invoked in one of two ways: through a <jutil:use> tag or by a command URL.

The <jutil:use> is used to load bean data into a JSP using an initializer method. This tag is the only way a JSP can interact directly with a controller.

<jutil:use var="order" controller="Cart" />
public class CartController extends Controller {
  public Order getOrder() {
    return this.currentOrder;
  }

  // Other code ...
}

To invoke a Controller Method via a command URL, the URL must include four things:

  1. The controller class name (omitting the "Controller" suffix).
  2. The controller method being invoked.
  3. The command extension: ".cmd".
  4. Request parameter data, either appended to the URL or sent as form data.
Catalog.editItem.cmd?itemId=234&name=Shirt&stock=120

The above command URL would invoke the editItem() method of the CatalogController:

public class CatalogController extends Controller {
  public void editItem(Long itemId, String name, int stock) {
    // Logic to update and save item data ...
  }
}

The controller method itself may have any logic necessary to the update the system.

Multi-Value Controller Method Parameters

Controllers can process multi-value request parameters using arrays of simple types. Consider the following form with multiple checkboxes for removing items:

<h1>Shopping Cart</h1>

<form action="Cart.removeItems.cmd">
<table border="1">
  <tr>
    <th>Remove</th>
    <th>Item</th>
  </tr>
  <tr>
    <td><input type="checkbox" name="itemId" value="296" /></td>
    <td>Hat</td>
  </tr>
  <tr>
    <td><input type="checkbox" name="itemId" value="689" /></td>
    <td>Shirt</td>
  </tr>
  <tr>
    <td><input type="checkbox" name="itemId" value="492" /></td>
    <td>Shoes</td>
  </tr>
  <tr>
    <td></td>
    <td><input type="submit" value="Remove Items" /></td>
  </tr>
</table>
</form>

This form can invoke a method that takes an array of item ids as a parameter. Each checked box will generate an additional value in the "itemId" array, making it easy for the controller method to loop through these ids and remove the items from the shopping cart.

public class CartController extends Controller {
  private Map items = new HashMap();
  
  public void addItem(Long itemId) throws Exception {
    items.put(itemId, Item.load(itemId));
  }
  
  public void removeItems(Long[] itemId) {
    for (int i=0; i<itemId.length; i++) {
      items.remove(itemId[i]);
    }
  }
}

Required Parameters and Defaults

Unless otherwise specified, all controller method parameters are required. This means that if any request parameter is missing or misspelled, the Chrysalis system will throw an exception indicating the missing parameter. This is a deliberate design feature of Chrysalis; one of the most common bugs in web application development are mismatches between form field names and expected request parameters.

Catalog.editItem.cmd?itemId=234&NAME=Shirt&stock=120
public class CatalogController extends Controller {
  public void editItem(Long itemId, String name, int stock) {
    // Logic to update and save item data ...
  }
}
<!-- chrysalis.xml for the Controller package -->
<config>
  <class name="CatalogController">
    <method name="editItem" parameters="itemId, name, stock" />
  </class>
  
  <!-- Other configuration ... -->
  
</config>

The above example will generate a MissingParameterException, because the "name" parameter is missing from the request (misspelled in this case).

You can define default values for Controller Method parameters. If the request parameter is missing or misspelled, the default value will be used instead:

Catalog.editItem.cmd?itemId=234&NAME=Shirt&stock=120
public class CatalogController extends Controller {
  public void editItem(Long itemId, String name, int stock) {
    // Logic to update and save item data ...
  }
}
<!-- chrysalis.xml for the Controller package -->
<config>
  <class name="CatalogController">
    <method name="editItem" parameters="itemId, name, stock"
            defaults="DEFAULT_NONE, Unknown, 0" />
  </class>
  
  <!-- Other configuration ... -->
  
</config>

In the example above:

  • The value "Unknown" will be passed to the "name" parameter.
  • The value "120" will be passed to the "stock" parameter rather than the default, because this request parameter exists.
  • If the "stock" parameter were missing, the value "0" would be converted to an int and passed to the "stock" parameter.
  • If the "itemId" parameter were missing, Chrysalis would generate a MissingParameterException, because it has no default.

There are three variations on the above rules for required parameters:

  • JavaBean parameters have special rules (see below).
  • Boolean parameters are never required, defaulting to "false" if not otherwise specified.
  • Multi-value parameters are never required, defaulting to an empty array if missing.

The default rules for Booleans interact well with HTML checkboxes, and the default rules for multi-value parameters work gracefully with "empty" lists of parameters.

Required Parameters and Beans

If a controller method has a complex (JavaBean) parameter, the rules for determining required parameters change as follows:

  • All parameters of the bean's initializer method are required.
  • All updateable JavaBean properties (those with setter methods) are required.
  • The Bean parameter name itself is not required, because its value is assembled using other parameters.
public class CatalogController extends Controller {

  public Item getItem(Long itemId) throws Exception {
    return Item.load(itemId);
  }

  public void editItem(Item item) throws Exception {
    item.save();
    setView("/showItem.jsp", "itemId", item.getItemId());
  }
}
public class Item {
  private Long itemId;
  private String name;
  private int stock;

  public Long getItemId()          { return this.itemId; }
  public String getName()          { return this.name;   }
  public void setName(String name) { this.name = name;   }
  public int getStock()            { return this.stock;  }
  public void setStock(int stock)  { this.stock = stock; }

  // Other methods: load(), save(), etc.
}

In the above example, the editItem() method has three required parameters:

  • The "itemId" parameter is required because it is a required parameter of the getItem() initiailizer method.
  • The "name" and "stock" parameters are required because the Item JavaBean has setter methods for those two properties.
  • The "item" parameter is not required in the request, because its value is derived from the other parameters.

In lists of default parameter values, bean parameters can only have two default values:

  • "DEFAULT_NONE": Use the initializer method for the JavaBean.
  • "DEFAULT_NEW_OBJECT": Create a new JavaBean using its no-parameter constructor.

If a controller method has multiple complex parameters, combine the required parameters for all the JavaBeans and their initializers.

Controllers in the Servlet Session

The first time a controller is invoked for a particular user:

  1. A new controller object is created using its no-parameter constructor.
  2. This controller object is place in the servlet session.
  3. Future invocations call the existing controller.

Each controller class is effectively a per-user singleton, maintaining data on behalf of that user in the user's session. This generally eliminates the need to communicate directly with the servlet session.

All the usual advice for state maintenance in a sessions applies to controllers. Controller state is not automatically persistent and can be lost in the event of server failure or session timeout. Important application data should always be persisted to a permanent data store. Controller data should only be used for non-critical data and data caching to eliminate extraneous round trips to the data store.

There may be circumstances where you want to flush a controller out of the session to replace it with a fresh controller. The "release()" method does just that. Invalidating the servlet session will flush all controllers.

Cross-Controller Communication

Ideally, each controller should be a self-contained unit with no references to other controllers. In practice, this may not be possible. Chrysalis provides a method to retrieve a user's controller based on its class:

Controller controller = Controller.getController([class]);

// To be useful, a cast operation may be needed:
CartController cart = (CartController) 
        Controller.getController(CartController.class);

The getController() method will only work when invoked inside the servlet engine, since it must access the servlet session. Using this method will make out-of-container testing more difficult, and should therefore be avoided.

View Redirection

Because of the role controllers play in the framework, they cannot generate any output. A controller must redirect to a view after its processing is complete. It may do so in one of three ways:

  1. Do nothing (go to the default view).
  2. Specify the view in the controller package's configuration file.
  3. Specify the view programmatically with the setView() method.

The default view is specified in the "WEB-INF/classes/ChrysalisConfig.xml" file. If no view is specified at all, the default view will be "/index.view":

<config>
  <controller>
    <package>com.domain.catalog.controllers</package>
    <default view="/index.jsp" errorpage="/error.jsp" />
  </controller>
</config>

Controller methods may specify their own view in the chrysalis.xml configuration file for the controller package. The view may be specified for the entire package, individual controllers or individual controller methods:

<!-- chrysalis.xml for the Controller package -->
<config>
  <default view="/index.jsp" />
  <class name="CartController">
    <default view="/showCart.jsp" />
    <method name="addItem" parameters="itemId" />
    <method name="removeItems" parameters="itemId" />
    <method name="placeOrder" view="/showOrder.jsp" />
  </class>
  
  <!-- Other controller configuration ... -->
  
</config>

A controller can use its setView() method to specify the view page programmatically. For example, the controller might used conditional logic to determine the view:

public class CatalogController extends Controller {
  // Other operations ...

  public void editItem(Item item) throws Exception {
    item.save();
    if (item.isDiscounted()) {
      setView("/showDiscountItem.jsp");
    } else {
      setView("/showItem.jsp");
    }
  }
}

If a view needs request parameters to render correctly, a controller can use its addViewParameter() method. This method can be called multiple times to add several view parameters.

public class CatalogController extends Controller {
  // Other operations ...

  public void editItem(Item item) throws Exception {
    item.save();
    setView("/showItem.jsp");
    addViewParameter("itemId", item.getItemId());
  }
}

If a view only requires a single parameter, you can set the view and add the parameter in a single method call:

public class CatalogController extends Controller {
  // Other operations ...

  public void editItem(Item item) throws Exception {
    item.save();
    setView("/showItem.jsp", "itemId", item.getItemId());
  }
}

The controller above will target the JSP "/showItem.jsp?itemId=##", where "##" is the value of the item's itemId property.

Note: The current version of Chrysalis always uses client-side redirection [response.sendRedirect()] to target the view page. This is a deliberate design feature. It encourages developers to define each view as a self-contained unit, independent of the controller invocations that occur before it is displayed.

Access to the Servlet API

Chrysalis controllers are designed to automate the more common interactions between application code and the servlet API:

  • Servlet Request: Request parameters are transferred into controller method parameters automatically.
  • Servlet Session: Each controller is maintained in the session, so its instance variables can hold session data.
  • Servlet Response: Views are generated by JSP, and redirect operations are handled by the framework.

It is impossible to anticipate all possibilities, though, and there may be times when a developers needs access to information not provided by the Chrysalis framework. Chrysalis provides a "back door" to the Servlet API through static methods in the org.chwf.servlet.ServletData class:

ServletData.getRequest();
ServletData.getResponse();
ServletData.getSession();
ServletData.getApplication(); // The ServletContext

You should avoid using these methods, because doing so makes it more difficult to test your application code outside the servlet engine.