Refactor large Controllers
Parse parameters into domain objects
Template method
Forward versus redirect
A Web App Framework WEB4J
Action
is an interface which:
package hirondelle.web4j.action; import hirondelle.web4j.model.AppException; /** <span class="highlight"> Process an HTTP request, and return a {@link ResponsePage}. </span> <P><b>This interface is likely the most important abstraction in WEB4J.</b> Almost every feature implemented by the programmer will need an implementation of this interface. <P>Typically, one of the <em>ActionXXX</em> abstract base classes are used to build implementations of this interface. */ public interface Action { /** Execute desired operation. <P>Typical operations include : <ul> <li>validate user input <li>interact with the database <li>place required objects in the appropriate scope <li>set the appropriate {@link ResponsePage}. </ul> <P>Returns an identifier for the resource (for example a JSP) which will display the end result of this <tt>Action</tt> (using either a forward or a redirect). */ ResponsePage execute() throws AppException; }
Action
implementations perform similar tasks. These
tasks should be placed in an Abstract Base Class (ABC), which will simplify
concrete implementations. The WEB4J tool defines several such ABC's:
ActionImpl
-
collects various protected final
utility methods needed by many Action
classes. ActionImpl
in
turn forms a base class for the ActionTemplateXXX
classes.
ActionTemplateListAndEdit
-
shows a listing, and a form to edit items in the listing
ActionTemplateShowAndApply
-
shows a form for editing a single item
ActionTemplateSearch
-
shows a form for entering search criteria
ActionTemplateXXX
classes implement the template method design pattern.
The application programmer chooses a particular template based in the desired style of user interface.
Here's an example of a concrete Action
, which subclasses the
ActionTemplateShowAndApply
class mentioned above. It provides implementations for three methods:
show
, validateUserInput
, and apply
.
package hirondelle.fish.main.discussion; import hirondelle.web4j.BuildImpl; import hirondelle.web4j.action.ActionTemplateShowAndApply; import hirondelle.web4j.action.ResponsePage; import hirondelle.web4j.database.DAOException; import hirondelle.web4j.database.Db; import hirondelle.web4j.database.SqlId; import hirondelle.web4j.model.AppException; import hirondelle.web4j.model.ModelCtorException; import hirondelle.web4j.model.ModelFromRequest; import hirondelle.web4j.request.RequestParameter; import hirondelle.web4j.request.RequestParser; import java.util.Date; import java.util.List; import java.util.regex.Pattern; /** List comments, and add new ones. <P>Comments are listed, along with a paging mechanism. @sql statements.sql @view view.jsp */ public final class CommentAction extends ActionTemplateShowAndApply { public static final SqlId FETCH_RECENT_COMMENTS = new SqlId("FETCH_RECENT_COMMENTS"); public static final SqlId ADD_COMMENT = new SqlId("ADD_COMMENT"); /** Constructor. */ public CommentAction(RequestParser aRequestParser){ super(FORWARD, REDIRECT, aRequestParser); } public static final RequestParameter COMMENT_BODY = RequestParameter.withLengthCheck( "Comment Body" ); /** Used for the paging mechanism. */ public static final RequestParameter PAGE_SIZE = RequestParameter.withRegexCheck( "PageSize", Pattern.compile("(\\d){1,4}") ); /** Used for the paging mechanism. */ public static final RequestParameter PAGE_INDEX = RequestParameter.withRegexCheck( "PageIndex", Pattern.compile("(\\d){1,4}") ); /** Show the listing of comments, and a form for adding new messages. */ protected void show() throws AppException { addToRequest(ITEMS_FOR_LISTING, fetchRecentComments()); } /** Ensure user input can build a new {@link Comment}. */ protected void validateUserInput() throws AppException { try { ModelFromRequest builder = new ModelFromRequest(getRequestParser()); /* This is an example of using a time which, for testing purposes, can be made independent of the true system time. The value of the 'now' variable depends on the implementation of TimeSource. */ long now = BuildImpl.forTimeSource().currentTimeMillis(); fComment = builder.build( Comment.class, getLoggedInUserName(), COMMENT_BODY, new Date(now) ); } catch (ModelCtorException ex){ addError(ex); } } /** Add a new {@link Comment} to the database. */ protected void apply() throws AppException { //no possibility of a duplicate error. addNew(fComment); } // PRIVATE // private Comment fComment; private static final ResponsePage FORWARD = new ResponsePage( "Discussion", "view.jsp", CommentAction.class ); private static final ResponsePage REDIRECT = new ResponsePage( "CommentAction.show?PageIndex=1&PageSize=10" ); /* Here, the DAO methods are not in a separate class, but simply regular methods of this Action. This reasonable since the methods are short, and they do not make this Action class significantly more complex. */ /** Return an immutable {@link List} of recent {@link Comment} objects. <P>The definition of what constitutes "recent" is left deliberately vague, to allow various versions of "recent" - last 5 messages, messages entered over the last N days, etc. */ private List<Comment> fetchRecentComments() throws DAOException { return Db.list(Comment.class, FETCH_RECENT_COMMENTS); } /** Add <tt>aNewComment</tt> to the database. No duplicates are possible. @param aNewComment to be added to the datastore. */ private void addNew(Comment aNewComment) throws DAOException { Object[] params = { aNewComment.getUserName(), aNewComment.getBody(), aNewComment.getDate() }; Db.edit(ADD_COMMENT, params); } }