User Guide

Date: July 17, 2008
WEB4J Javadoc : link
Version of web4j.jar: 3.8.0

Table of Contents

Example Application
Upgrading to Full Version
Why WEB4J Was Built
How WEB4J Was Built
Tour of a Typical Feature
Directory Structure
Model Objects
Data Access Objects
Request Processing
Application Security
Fine-Grained Security Constraints
Data Ownership Constraints
Actions
Populating Forms
Configuring web.xml
Configuring Implementation Classes
Multilingual Apps
Logging
Startup Tasks and Code Tables
File Uploads
Miscellaneous Tags
Utilities
Unit Testing
Building New Apps From The Example App
Version History

Example Application

WEB4J comes with an extended example application called Fish & Chips Club. If you download, configure, and run this application, then you will gain rapid insight into how WEB4J applications work. The example application is large enough to be representative of typical database applications, but not so large as to be overwhelming. Its javadoc includes convenient links to its source code, JSPs, and SQL statements. It is likely that you can build you own applications by modifying Fish & Chips Club. Since it uses package by feature, removing items unrelated to your problem domain reduces to simply deleting directories.

Please see the Fish & Chips Club Getting Started Guide for more information.

Upgrading to Full Version

The Fish & Chips Club example application includes a time-limited trial version of web4j.jar. Once you have purchased WEB4J, you can upgrade the example application simply by replacing that jar. The version you are running is logged upon startup at CONFIG level (JDK logging).

The full version of web4j.jar behaves the same as the trial version, except it performs some extra validations for you:

  • upon startup, identifiers in .sql files are matched one-to-one with declared public static final SqlId fields. Any mismatches cause an exception, and your app will not proceed. This protects you, since it changes RuntimeExceptions into startup-time exceptions, and catches trivial typing errors as soon as possible.
  • upon startup, your Model Objects are scanned for possible Cross-Site Scripting vulnerabilities, where String is used instead of SafeText. If an issue is found, it is logged as a WARNING.
  • the PerformanceMonitor filter can be configured to issue a periodic 'ping' to a fixed URL. If a problem is detected, or if the response is too slow, a TroubleTicket email is issued.
In addition, the full version of web4j.jar includes several developer tools.

Why WEB4J Was Built

WEB4J was built for one reason: other Java tools for creating a browser front end to a database are unacceptable. They take too long to learn, and are too difficult to work with. In short, they are disturbingly unproductive. This sentiment is very widely held. The attention paid recently by many Java programmers to Ruby On Rails vividly demonstrates a widespread hunger for something better.

How WEB4J Was Built

WEB4J was built slowly over several years, and its API design has always centered on giving you feelings of elegance, beauty, and concision.

WEB4J was not built like most tools, because it was not built with a specific implementation technique in mind. Rather, it was built with specific feelings or esthetics in mind. The basic steps were :

  1. build typical applications on top of the servlet and JSP APIs
  2. find all repeated code and move it from the applications into the framework
  3. do it such that the application programmer will feel the maximum elegance in their code
  4. do not presuppose any particular style of implementation
Let's clarify why this style is different, using two counter-examples :
  • Hibernate's central idea is that database schemas should be repeated in the application layer, such that SQL statements can usually be avoided, and replaced by object graph navigation
  • Java Server Faces' central idea is that web application front ends are best implemented with the 'events-and-components' style of programming used in graphical user interfaces
Hibernate and Java Server Faces are examples of tools having, at their core, very specific implementation ideas. In both cases, the central implementation ideas are taken as the "Prime Directive". Anything incompatible with that Prime Directive is automatically placed out of consideration. This is a dangerous way of building things. It places implementation first, and treats elegance, and the experience of the programmer, as secondary, not primary.

However, it does not seem possible to produce elegance without treating it as the single most important thing. Is Hibernate beautiful? Is all that XML configuration beautiful? Are its abundant cyclic references between packages beautiful? Is needing to configure the database schema in the application layer an elegant thing? Many would say "No, this is a bit ugly". And perhaps it was destined to be ugly, since it was based on an implementation idea, instead of on esthetic principles. If you wish to make something beautiful, you need to treat beauty as your Prime Directive.

Tour of a Typical Feature

Here is a screencast tour, giving you an excellent overview of how a typical feature is implemented in a WEB4J application. More details on each aspect are provided below.

Here are some typical line counts per feature, taken from a cross section of several features in the Fish & Chips Club example application. (Documentation comments are included in the line counts.)

ItemAvg LinesRelative Size
SQL file (.sql)25Graph
Presentation (.jsp)108Graph
Total133 
Model (.java)111Graph
Action (.java)144Graph
DAO (.java)46Graph
Total301 

In general, your code will only be roughly twice the size of the non-code elements of your application.

It is often the case that the DAO class is simple, consisting only of a few single-line methods. In such cases, it is recommended that those DAO methods be refactored into the Action class. This will slightly reduce the total line count.

Directory Structure

Although it is not required, you are highly encouraged to use the package by feature style for your package and directory structure. For an illustration, see the Fish & Chips Club example app.

There are numerous advantages to this style. Although it may be unfamiliar to you, it is very likely that you once you use it, you will find it superior to other styles.

If you do select this style, then you may find it convenient to use fixed, conventional names for both the view and the .sql file (which contains SQL statements), such as view.jsp and statements.sql. Using fixed names for the java classes is not recommended, however, since that would cause problems with javadoc, and would likely make your IDE less effective.

It is useful to treat your Action classes as the public face of each feature. That is, the javadoc of the Action should be the entry point into the rest of the implementation. In particular, it should link to the actual SQL statements and JSP. This is implememented in Fish & Chips Club using two custom taglets. (Taglets are part of the javadoc tool.)

Example of their use :

/**
* Edit Resto objects.
*
* @sql statements.sql
* @view view.jsp
*/
public final class RestoAction ...
These two taglets are defined in the WEB4J Developer Tools, available only with the Full Version of WEB4J.

Model Objects

Model Objects represent items in your problem domain. They roughly correspond to rows in a database table (more precisely, the rows of a ResultSet). In WEB4J, Model Objects almost always have the following public items:
  • a constructor with arguments for all possible data
  • various getXXX methods
  • the toString, equals, and hashCode methods
Example code for some Model Objects :

The primary job of a Model Object is to carry and validate data. Validation is especially important, since data is king. Most of the code in your Model Objects will deal with validation.

In WEB4J, validation is always placed in the constructor. If a validation fails, then a checked ModelCtorException must be thrown. When a validation fails, you cannot throw any other kind of exception except ModelCtorException. For example, if a required field is found to be null, then you cannot throw a NullPointerException. You must throw a ModelCtorException.

Some may object to this policy. However, you must remember that NullPointerException is an unchecked exception. Unchecked exceptions are strictly meant for bugs. Here is the important point: faulty user input is not a bug. It is to be expected as part of the normal operation of the program.

Validator and Check can help you validate your Model Objects. You are not required to use them, but you will likely find them convenient. The Check class returns common implementations of the Validator interface. For validations specific to your problem domain, you can define them as a new Validator, and then use them with the Check class, just like any other Validator.

To always implement Model Objects safely, you need to understand that mutable object fields often need special care, in the form of a defensive copy.

The ModelUtil class is provided to help you correctly implement the toString, equals, and hashCode methods.

Your Model Objects can use the Id and SafeText classes as building blocks, in the same way as Integer, BigDecimal, and so on. An Id can hold any kind of identifer you wish, while SafeText should usually be used to model free-form user input, to avoid issues with Cross-Site Scripting attacks (see below).

It is safe to use String if the Model Object's getXXX method is package-private instead of public, since it's not possible to access such a return value in a JSP. When modeling search criteria entered into a form, it is often possible to create a Model Object with package-private getXXX methods. As a general guideline, you should minimize the scope of methods and classes whenever possible.

Data Access Objects

A Data Access Object (DAO) implements persistence. It forms a bridge between java objects and database records. Each database operation has its own method in a DAO. The Db class is often used to implement such methods.

Example code for some DAOs:

The .sql files

Each DAO interacts closely with a set of SQL statements, placed in an .sql file. These are text files containing named blocks of SQL statements (example). The detailed syntax of these files is described here. These files can be placed anywhere under /WEB-INF/. You may use a single .sql file, or many.

The problem arises of how exactly these SQL statements are referenced in your source code. Incorrect references in code should be detected as soon as possible. Detecting them at compile time would be best, and detecting them at runtime would be worst. The approach taken by WEB4J is to detect bad references at startup time. This is not quite as effective as detecting such errors at compile time, but it is much more effective than detecting them at runtime, during normal operation.

At startup, WEB4J will read in all .sql files located under /WEB-INF/. It will also scan your code for all public static final SqlId fields. Then, these two sets are compared. If any mismatch is found, then a RuntimeException is thrown, and your application will not be able to proceed. This protects you both from trivial naming errors and from 'cruft' (items that are no longer used), both in your code and in the .sql files. (This check is performed by the full version of web4j.jar, but not by the trial version.)

The SqlId fields are what you use in code to reference items in your .sql files. They form the bridge between Java-land and SQL-land.

Ordering Conventions

There are two important ordering conventions used in the data layer. They exist in order to make your job easier.

Column Order of SELECTs and Model Object Constructors - The columns in a SELECT ResultSet must match one-to-one with corresponding arguments of the target Model Object constructor. Using this convention means that you don't have to perform tedious mapping between columns and Model Object setters.

Example : note how the constructor of Resto has arguments that correspond one-to-one to the SQL statements named LIST_RESTOS and FETCH_RESTO.

Order of Parameters Passed From DAO - SQL statements usually have one or more parameters, denoted by '?' placeholders in the underlying SQL. When your DAO passes data to such SQL statements, their order is taken as matching one-to-one with the corresponding '?' placeholders.

Let's take an example. For this entry in an .sql file:

ADD_RESTO  {
 INSERT INTO Resto (Name, Location, Price, Comment) VALUES (?,?,?,?)
}

The corresponding DAO method is :

Id add(Resto aResto) throws DAOException, DuplicateException {
  Id result = Db.add(
    ADD_RESTO, 
    aResto.getName(), aResto.getLocation(), aResto.getPrice(), aResto.getComment()
  );
  return result;
}
The important point is that the order of items passed to Db.add(SqlId, Object...) matches the underlying SQL. The following code would be wrong, since Price and Comment are in the wrong order:
Id add(Resto aResto) throws DAOException, DuplicateException {
  Id result = Db.add(
    ADD_RESTO, 
    aResto.getName(), aResto.getLocation(), aResto.getComment(), aResto.getPrice()
  );
  return result;
}

Reports

A report is implemented as a single SELECT operation, possibly with some criteria. The implementation of a report is very often shorter than the average feature. Since there is only one database operation, and its implementation is usually a single line, it is reasonable to place that code directly in the Action itself, instead of in a separate DAO class. In the Fish & Chips Club example app, each report is implemented with an Action class that handles the data access without using an external Data Access Object.

Here are some examples :

If an appropriate Model Object already exists, then it should likely be used to fetch or list the required data, using the Db class.

If no Model Object exists for reporting a given set of data, then the Report class can be used. The Report class translates a ResultSet into a Map. It has several methods, corresponding to different policies for formatting the data.

Transactions

It is important to preserve the integrity of the database. If multiple database operations only make sense as a single unit, then they must be placed in a transaction. These classes help you implement transactions :
  • TxSimple - for the simplest kinds of transactions, where no looping is involved. TxSimple uses ordering conventions to make your code as compact as possible. See UserDAO.delete(String) for an example.
  • TxTemplate - an example of the Template Method design pattern. It defines all of the repeated code associated with transactions, while leaving you to implement a single abstract method, for performing the body of the transaction. When subclassing TxTemplate, you have the option of defining a private static inner class in your existing DAO, instead of creating a new top level class. This is simply because such classes are usually not very large. If you aren't comfortable with inner classes, you can certainly define an ordinary top-level class instead.
  • TxTemplate will usually use DbTx. DbTx is almost exactly like Db. The difference is that Db uses connections created internally, while DbTx uses a shared connection passed by the caller.
See RoleDAO for an example of using TxTemplate and DbTx.

Distributed Transactions

Distributed transactions refer to transactions that span not only multiple operations, but multiple database vendors as well. WEB4J has no API related specifically to distributed transactions. However, if UserTransaction is available in your environment, then it can certainly be used. It's just that WEB4J classes are not currently able to help you with their implementation. (If you think WEB4J should help with distributed transactions, please send a note of gentle encouragement.)

Connections and Multiple Databases

You need to tell WEB4J how to get database connections. You do that by providing an implementation of the ConnectionSource interface. The Fish & Chips Club has an example implementation that uses container connection pools for three separate databases. It is easy to use more than one database with your application.

The database names defined by ConnectionSource are used in your application as prefixes to identify which database a given operation is intended for. These prefixes are used in two places : in .sql files and in the corresponding SqlId fields (see above).

An entry in an .sql file versus the default database looks like this (block name has no prefix) :

ADD_COMMENT {
 INSERT INTO Discussion (Name, Body, CreationDate) VALUES (?,?,?)
}
...
public static final SqlId ADD_COMMENT =  new SqlId("ADD_COMMENT");
An entry in an .sql file versus a non-default database named ACCESS_CONTROL looks like this (block name is prefixed with "ACCESS_CONTROL"):
ACCESS_CONTROL.USER_LIST {
 SELECT Name, Password
 FROM Users
 ORDER BY Name
}
...
public static final SqlId USER_LIST =  new SqlId("ACCESS_CONTROL", "USER_LIST");
Here, the prefix "ACCESS_CONTROL" is a database name defined by the ConnectionSource implementation.

Conversions

When building Model Objects, WEB4J converts ResultSet columns into 'building block' objects such as Integer, BigDecimal, and so on. That conversion is defined by the configured implementation of ConvertColumn. WEB4J comes with a reasonable default implemention called ConvertColumnImpl, which can almost always be used. If you wish to define your own custom conversion policies, then you will need to provide an alternate implementation of ConvertColumn.

Search and Sort Operations

Search operations are implemented with a SELECT, but they differ from other SELECTs in an important way. In many cases, the user input which defines the search criteria can come in a large number of combinations. In those cases, enumeration of all possible combinations in your .sql file can rapidly become impractical. The DynamicCriteria class was created to help with this issue. It allows you to programmatically add the WHERE and ORDER BY clauses to base SQL statements (defined in the usual way in an .sql file), while still keeping you safe from SQL injection attacks.

To let end users sort ResultSets in multiple ways, DynamicCriteria can also be used to construct just the ORDER BY clause.

See the example app's search feature for an illustration of using DynamicCriteria (especially the RestoSearchAction class).

Database Configuration in web.xml

There are a number of items in web.xml related to the database. For each application, you will need to review their values, to ensure they are appropriate. Please see below for further information.

Further Information

Please see the WEB4J javadoc for more information on the database layer.

Request Processing

By default, WEB4J uses a reasonable naming convention to map each incoming request to an Action. That convention is defined by RequestParserImpl. If you wish to use some other mechanism, then you will need to provide an alternate implementation of RequestParser.

Here is an example of the default Action mapping: given the Action class with fully qualified name of

hirondelle.fish.main.member.MemberEdit
the implicit URI mapping is calculated as
/main/member/MemberEdit
That is, the '.' character is changed to '/', and the prefix 'hirondelle.fish.' is removed. The prefix is configured in web.xml, with a setting named ImplicitMappingRemoveBasePackage. This default mechanism also allows for simple explicit overrides: just add to your Action a String field of the conventional form:
public static final String EXPLICIT_URI_MAPPING = "/translate/basetext/BaseTextEdit";
All Action mappings found by RequestParserImpl are logged during startup.

Once the Action class corresponding to the underlying request is found, then WEB4J creates an object of that class, and calls its execute method. That method performs the desired operation, and returns a ResponsePage, which tells WEB4J how you want to render the final response. The ResponsePage points to a JSP, and controls the forward versus redirect behavior.

Application Security

Web applications need to protect themselves from malicious attacks by hackers. The OWASP site is an excellent reference for web app security. It is highly recommended that any programmer not familiar with web app security should closely read and understand the issues discussed on the OWASP site. Any web application not built with security in mind is very likely at risk.

It's strongly recommended that you review the various items below related to security.

WEB4J does its best to help you create secure web apps. In a few important cases it has built-in, default mechanisms which prevent certain kinds of attack:

  • any incoming request URL that doesn't match the set of URLs expected by the application is automatically rejected.
  • each action is mapped only to a specific, explicit set of request parameters. Any request parameter whose name doesn't match those on a white list, or whose value is not accepted by WEB4J's hard validation mechanism, is automatically rejected.
  • WEB4J's data layer always uses PreparedStatement, never Statement. This protects your application from SQL injection attacks.

In other cases, however, only optional mechanisms are available. For example, using SafeText to model free-form user input instead of String is optional, not mandatory. If you use these optional mechanisms correctly, then they will increase your confidence in the security of your app. It's important to realize that using WEB4J doesn't guarantee that your app is secure.

Indeed, it does not seem possible for a web app framework to address all security problems. For example, how can a framework prevent you from storing passwords in clear text, or from using http when https is needed?

Here is a listing of some important security techniques you should be aware of :

  • free-form user input should be modeled with SafeText instead of String (prevents XSS attacks).
  • CsrfFilter should be used to prevent CSRF attacks.
  • forms should use GET and POST properly.
  • pages should specify their content type and encoding (typically in a template JSP, to reduce repetition).
  • the security constraints defined in web.xml work well, but beware that the servlet spec allows items having no associated security constraint to be served to the user. (Many would argue this is a dangerous default.)
  • any data ownership constraints should be implemented using WHERE clauses in SQL statements.
  • never store passwords in clear text. Store them in a hashed form instead.
  • never show stack traces or system information to the user.
If you think WEB4J should be doing more to help you with security, just send along a note and tell us why.

SQL Injection Attacks

When using WEB4J's data layer, SQL Injection attacks are simply not possible. This is because WEB4J always uses a PreparedStatement, and PreparedStatements are not vulnerable to SQL Injection attacks.

Cross-Site Scripting (XSS) Attacks

Cross-Site Scripting attacks inject scripts into free-form user input. When such input is rendered in the view without taking special precautions, it will execute. To protect against such attacks, you need to escape special characters when presenting free-form user input in the view. (The OWASP lists 12 special HTML characters that should be escaped.)

WEB4J provides SafeText for modeling free-form user input, as a replacement for String. SafeText will automatically escape special HTML characters in its toString() method. When needed, other escaping policies are available by calling SafeText.getXmlSafe() and SafeText.getRawString().

Tools which manually escape free-form user input in the view certainly work - if you remember to use them. When using SafeText, however, the danger of forgetting to escape in the view does not exist. Hence, using SafeText will increase your confidence in the security of your application.

The Full Version of WEB4J has some additional help in this regard. Upon startup, it will perform extra checking on your Model Objects. Your source code is scanned, all Model Objects are identified, and any methods that return a String are logged as a warning.

Cross-Site Request Forgery (CSRF) Attacks

Cross-Site Request Forgery attacks hijack valid sessions to perform malicious operations.

In WEB4J, you defend against CSRF attacks by performing the following :

  • specify the correct POST/GET value for the method attribute of your forms. Any action which has a side effect should be a POST (edits to the database, logging off), while any other action should be be a GET (reports, listings, searches).
  • always explicitly specify content-type, using an HTTP header. (For an example, see the Template.jsps in the example application.)
  • configure a CsrfFilter in your web.xml. This filter requires two SqlId entries. (As with any SqlIds, you will need to provide the underlying SQL statements in an .sql file, and declare the SqlId constants in some class.)

Application Firewall

An ApplicationFirewall protects your application from malicious attacks. ApplicationFirewall makes an important distinction between hard validation and soft validation. Hard validation is for low level 'sanity' checks, and detects either attacks or gross programmer errors. Failure of a hard validation can result in an unpolished response, and should not be seen during normal operation of the program. Soft validation is for high-level business validation, and is expected during normal operation. Failure of a soft validation must show a polished response to the end user. See ApplicationFirewall for more information on this important distinction.

The default ApplicationFirewallImpl works closely with RequestParameter. If an Action uses request parameters (and they usually do), then it must state explicitly what request parameters it expects: what their names are and what basic sanity checks should be performed on their values. That is done by declaring RequestParameter fields in the Action of the form :

public static final RequestParameter COMMENT = RequestParameter.withLengthCheck("Comment");
ApplicationFirewallImpl will perform hard validation on the incoming request parameters, both their names and values. The soft validations are done in your code, usually in a Model Object constructor.

Preventing Spam

For most internal back office applications located behind a firewall, spam is usually not a problem. For applications on the public web, however, spam will usually be an issue.

The SpamDetector interface and its SpamDetectorImpl help you prevent spam from getting into your database. Once you have decided on an implementation, it can be used in two ways.

  • instruct the ApplicationFirewall to use the SpamDetector on every request parameter value. This is controlled by the SpamDetectionInFirewall setting in web.xml.
  • for more finely tuned control, use Check.forSpam() to validate each parameter that may contain spam. Data that is modeled in a Model Object in a non-textual form, such as Integer or BigDecimal, does not need to be checked for spam in this way. This is because WEB4J internally converts such data from String into the target Integer or BigDecimal (or whatever), and spam will almost always cause such conversions to already fail.

Fine-Grained Security Constraints

The Servlet API allows you to configure <security-constraint> items in web.xml. Here's an example which allows access to URLs starting with /webmaster/*, if the logged in user has the corresponding role of 'webmaster':
<!-- /webmaster/* only for 'webmaster' Role -->   
<web-app>
  <security-constraint>
   <web-resource-collection>
    <web-resource-name>Webmaster Module</web-resource-name>
    <url-pattern>/webmaster/*</url-pattern>
   </web-resource-collection>
   <auth-constraint>
    <role-name>webmaster</role-name>
   </auth-constraint>
  </security-constraint>
  ...
  <security-role>
   <role-name>webmaster</role-name>
  </security-role>
</web-app>
When thinking of security, it's often useful to think in terms of nouns and verbs:
  • noun - what is being acted on
  • verb - what is being done to it; the operation, such as add, change, or delete.
In the example <security-constraint> presented above, only the noun is defined - the items under /webmaster/*. It's not fine-grained, since all operations are allowed. The Servlet API does provide the <http-method> item, but there's a problem with it: browsers typically support only two operations, POST and GET. This is often inadequate to represent fine-grained security constraints in the real world.

An effective alternative is to use the <url-pattern> to represent both the nouns and the verbs:

  • use a path in <url-pattern> to represent nouns (/blah/*)
  • use an extension in <url-pattern> to represent verbs (*.add)
  • use both a path and an extension to represent specific operations on specific items (../blah/SomeThing.delete)
Multiple extensions such as .add, .delete, and so on, are used to represent the verb. This style allows you to mix and match the nouns and the verbs, to create any kind of security constraint you may need.

Example URLs when using this technique :

  • ../MemberAction.list
  • ../MemberAction.add
  • ../MemberAction.fetchForChange?Id=3
  • ../MemberAction.delete

Data Ownership Constraints

Many applications need to restrict access of specific data to specific users. For example, an application that stores user preferences in a database table would need to restrict each user to their own preferences. The following is a recommendation for implementing such "data ownership constraints" in your WEB4J application.

For each table having data ownership constraints, an appropriate WHERE clause must be used in all SQL statements related to the table, as in

 SELECT Language, TimeZone 
 FROM Preferences WHERE UserId=?

 DELETE FROM Preferences WHERE UserId=?
This is the only way to ensure that access to each record is always confirmed as being valid. Since data ownership constraints are, of course, fundamentally about accessing tables, it seems natural to insist that any implementation of data ownership constraints include such WHERE clauses.

The source of the user id parameter is critical. The user id can only come from data stored in the session upon login, and which cannot be tampered with by a malicious user. The user id cannot come from any other source of information - not the request, not from a cookie, and not from any other source. Session data is stored on the server, and is not directly accessible to a malicious attacker.

The Fish & Chips Club example app demonstrates the above techniques in its Preferences feature.

Actions

An Action performs some desired operation, or group of closely related operations. An Action ties together all other parts of a feature's implementation. The Action uses the other parts of the implementation (Model Object, DAO, JSP) to perform the desired operations. Your Actions will usually extend the following base classes:
  • ActionImpl - many utility methods, and one abstract execute method. Forms the base class for the ActionTemplateXXX classes. If you need to define a new ActionTemplateXXX class, you should likely use ActionImpl as a base class as well.
  • ActionTemplateListAndEdit - list, add, edit, and delete records.
  • ActionTemplateSearch - suitable for search criteria forms that are used to perform a SELECT.
  • ActionTemplateShowAndApply - show a form, possibly with data, and apply an update. An example use case is user preferences: from the point of view of each user, there is only one set of preferences. So, there is no need for a listing of numerous records.
The ActionTemplateXXX classes are all instances of the Template Method design pattern. They all use a request parameter named Operation to define which operation is to be performed - add, list, and so on. The Operation enum is defined to help an Action branch according to its value.

Action methods tend to be 'branchy'. This is natural, since Actions need to handle all the different possible errors that can occur. Here are some example Actions :

Convenient Object Key Names

The ActionImpl base class defines various commonly needed items. For instance, it defines two constants that can be used, if desired, as conventional key names for items placed in request scope: These items are referenced in JSPs as ${itemForEdit} and ${itemsForListing}. Using such conventional names is optional. If they make your life easier, then use them.

Display Messages to the User

Displaying information or error messages to the user is done with various addMessage and addError methods. Such messages come in two styles defined by the AppResponseMessage class: simple and compound. Simple messages are simply fixed Strings. Compound messages are parameterized, and use a simple syntax. Here is an example having both styles, first a compound, and then a simple message:
/** Update an existing {@link Visit}. */
protected void attemptChange() throws DAOException {
  boolean success = fVisitDAO.change(fVisit);
  if (success){
    addMessage("Visit to _1_ changed successfully.", fVisit.getRestaurant());
  }
  else {
    addError("No update occurred. Visit likely deleted by another user.");
  }
}
In the background, the addMessage and addError methods will simply place a MessageList in session scope. Session scope is used since many messages need to survive a redirect. WEB4J does not currently have a mechanism for associating a message with a specific control in the JSP.

The example application displays messages with a reusable .tag file, always placed at the top of the page. The alternative style of placing error messages near form controls is sometimes problematic:

  • in the case of longer forms that require scrolling, there is no way to ensure that the user actually sees a message placed only beside a control. Thus, it seems best to at least always include some kind of message at the top of the page, in addition to any message appearing next to the control.
  • form layout can easily degrade when large message text is inserted into the flow of a form layout.
(It is likely that a future version of WEB4J will allow for changing the CSS class of controls that have an associated error message. However, given the above issues, it is less likely that placing text beside the control will be supported.)

Building Model Objects

To build a Model Object out of request parameters, you usually use ModelFromRequest along with some RequestParameter objects.

Example

public static final RequestParameter MEMBER_ID =  RequestParameter.withLengthCheck("Id");
public static final RequestParameter IS_ACTIVE = RequestParameter.withLengthCheck("Is Active");
public static final RequestParameter NAME = RequestParameter.withLengthCheck("Name");
...
/** Ensure user input can build a {@link Member}.  */
protected void validateUserInput() {
  try {
    ModelFromRequest builder = new ModelFromRequest(getRequestParser());
    fMember = builder.build(Member.class, MEMBER_ID, NAME, IS_ACTIVE);
  }
  catch (ModelCtorException ex){
    addError(ex);
  }    
}
...
private Member fMember;
The first item passed to build is a class literal denoting the kind of Model Object to be built. The remaining arguments to build represent the data (or pointers to the data) to be passed to the Model Object's constructor. This method takes a sequence parameter, so any number of parameters can be passed. It is important to note that the build method takes either RequestParameter objects, or any Java object. If a RequestParameter is passed, however, then the underlying param is first translated into an appropriate base object such as Integer or BigDecimal before being passed to the Model Object constructor. In the above example, the Model Object constructor has the form
public Member (Id aId, String aMemberName, Boolean aIsActive) ... {
  ...
}
That is, it does not take any RequestParameter objects as params. Rather, the underlying data is translated into the desired target objects as required. In the above example, the various RequestParameter objects passed to the build method are translated internally by WEB4J into an Id, String, and Boolean, corresponding to the declared parameters of the Model Object constructor.

Populating Forms

All you have to do to populate a form with data is wrap it in a Populate tag. In other web app frameworks, you cannot implement forms using the familiar controls already defined by HTML. Instead, you need to learn and use a set of custom tags which in effect replace regular HTML controls. In WEB4J, that's not necessary.

There are two use cases for the Populate tag : when there is no editing of an existing record, as in

<w:populate>
  ..form body...
</w:populate>
and when an existing item is being edited, as in
<w:populate using='<some identifier>'>
  ..form body...
</w:populate>
This second style requires that control names must match corresponding getXXX methods, using a naming convention. Let's take this example:
<w:populate using='item'>
   ...
   <input name="Message" type="text">
   ....
</w:populate>
This implies that the item object has a public method named getMessage() whose return value can be used to populate the control named 'Message'. The return value is formatted into text using Formats.objectToText().

Forms that edit the database must have method=POST, while forms that perform searches or reports must have method=GET.

Configuring web.xml

In WEB4J applications, the sole configuration file is the standard web.xml file used with all servlets. The following is a list of all items in web.xml that affect the behavior of WEB4J.

Items Having No Default Value

MailServer
Value : (NONE | <valid mail server>). Default value: NONE. The mail server used when WEB4J needs to send an email on the application's behalf. If the special value 'NONE' is supplied, then all emails will be suppressed. Note that SMTP servers restrict services to known clients. You must supply the mail server which serves the same network as your web application. If you do not, you will receive an 'Error 550, Relaying denied' error.

Webmaster
Email address of the application admin. No default value for this item. If TroubleTickets are emailed by WEB4J, they will be sent to this address. If emails are sent by WEB4J to other parties, then they will have this as the "from" address. This email address must be in the same domain as the MailServer.

ImplicitMappingRemoveBasePackage
Example value : 'com.blah' - do not include any trailing dot. No default value for this item. By default, the RequestParserImpl class will map Action classes implicitly to a specific URI. This specific URI is constructed by taking the Action class name, and removing the 'base' package, defined by this setting.

Example : given the settings in the example application, the class:

hirondelle.fish.main.home.HomePageAction
is implicitly mapped to the URI:
/main/home/HomePageAction
by removing 'hirondelle.fish.' from the (slightly modified) result of Class.getName().

Implicit mapping can always be overridden by specifying in the Action a conventional field named EXPLICIT_URI_MAPPING, of the form:

public static final String EXPLICIT_URI_MAPPING = "/main/Blah";

Items Having A Default Value

EmailInSeparateThread
Permit the creation of a separate thread for sending email, to improve the response time of Actions that send an email. Permitted values : on/off, true/false, yes/no (case-insensitive). Default : off. Some containers will not permit a servlet to create a thread. It is recommended this be turned on, if possible.

LoggingDirectory
Location for logging file. Default value: NONE. Example value: C:\log\fish\. Used by LoggingConfigImpl. Must end with a directory separator. Special value of 'NONE' will disable LoggingConfigImpl, such that it will not perform any logging config in code.

LoggingLevels
Logging levels for various loggers. Used by LoggingConfigImpl. Comma-separated list of items, in the same format as JDK logging.properties files :

com.wildebeest.myapp.level=INFO, hirondelle.web4j.level=FINE
It is recommended to include hirondelle.web4j.level, to allow logging by WEB4J classes. The default value for this item is:
hirondelle.web4j.level=CONFIG

TroubleTicketMailingList
Comma-delimited list of one or more email addresses to be notified when a problem occurs. If TroubleTickets are emailed, they will be sent to these addresses. Default value 'NONE'.

MinimumIntervalBetweenTroubleTickets
Time interval in minutes. Suggested value: 30. Default value: 30.
TroubleTickets are expected to be rare. It is possible, however, that TroubleTickets could be generated in large numbers in a short period of time. For example, if the database connection is lost, then a trouble ticket is generated with almost every request. This parameter is used to throttle down the sending of such emails.

PoorPerformanceThreshold
Time interval in seconds. Suggested value: 20. Default value: 20.
If any request processed by the Controller takes more than this number of seconds, then an email will be sent to the configured Webmaster. The email will state the number of milliseconds taken to process the request. Often, poor performance is caused by degradation not in the web application itself, but rather in the environment in which it is running.

MaxHttpRequestSize
Suggested value : 51200. Minimum value: 1000. Default value: 51200.
Maximum size in bytes of HTTP requests carrying no uploaded files. If such a request has a total size in excess of this value, then it will be discarded by WEB4J, and will not be made available to the application. This is intended only for requests whose unusually large size identify them clearly as hack requests.

MaxFileUploadRequestSize
Suggested value : 1048576. Minimum value: 1000. Default value: 51200.
Maximum size in bytes of HTTP requests carrying uploaded files. If such a request has a total size in excess of this value, then it will be discarded by WEB4J, and will not be made available to the application. This is intended only for requests whose unusually large size identify them clearly as hack requests.

MaxRequestParamValueSize
Suggested value : 51200. Minimum value: 1000. Default: 51200.
Maximum size in bytes of HTTP request parameter values. This maximum is applied as a hard validation by ApplicationFirewallImpl, and only against items created using the RequestParameter.withLengthCheck(String) factory method. This is meant as a kind of default sanity check on parameter values, when no other (more stringent) means of performing hard validation is suitable.

SpamDetectionInFirewall
Permitted values : ON and OFF. Default value: OFF. When ON, then the default implementation of ApplicationFirewall will use the configured SpamDetector to check the values of all request parameters. If spam is detected, then an unpolished response will result.

FullyValidateFileUploads
Permitted values : ON and OFF. Default value: OFF. When ON, then the default implementation of ApplicationFirewall will perform the same validation on file upload requests as for regular requests. When OFF, then such validation will not be performed.

CharacterEncoding
Character encoding used in the application. Default value is 'UTF-8'. This setting MUST match the character encoding used by the database. (Databases often use the term 'character set' for character encoding.) Character encodings define how bytes are translated into text. For more information, see the Prefer UTF-8 in all layers topic on javapractices.com.

This setting will be used by the Controller to process requests. In addition, it will be used to create an application scope attribute named web4j_key_for_character_encoding. It is highly recommended that the value of this attribute be placed in template JSPs, so that all served pages will explicitly state the desired character encoding to be used by the browser. (This also increases security.)

DefaultLocale
Default Locale used by the application. Default value: en. This setting allows independence of the server JRE's default Locale, and of HTTP headers. See LocaleSource for information on how a Locale is derived from an underlying request. Example values : en, fr. See also: java.util.Locale for possible values of Locale identifiers, and Util.buildLocale(String).

DefaultUserTimeZone
Default TimeZone used by the application. Default value : GMT. This setting allows independence of the server JRE's default TimeZone, and of HTTP headers. See TimeZoneSource for information on how a TimeZone is derived from an underlying request. Example values : 'Canada/Montreal', 'UTC'. See also: java.util.TimeZone for possible values of TimeZone identifiers, and LocaleSource.

TimeZoneHint
Example values : 'NONE', 'Canada/Montreal', 'UTC'. Default value: 'NONE'. See java.util.TimeZone for possible values of TimeZone identifiers. This item is passed as hint to the database driver, for all date columns encountered by the WEB4J data layer. Overrides the JRE default time zone. See : PreparedStatement.setTimestamp(int, Timestamp, Calendar) and ResultSet.getTimestamp(int, Calendar). The special value 'NONE' is used when the default JRE time zone suffices, or when the database already stores time zone information in the desired manner.

DecimalSeparator
Comma-separated list of names of decimal separator characters to be accepted during user input of numbers having a fractional part. Permitted values : 'PERIOD', 'COMMA', 'PERIOD,COMMA'. Default value: 'PERIOD'. Used for conversions to BigDecimal and floating point values. This item is not affected by Locale.

BigDecimalDisplayFormat
Format for display of BigDecimal values in reports. Default value: #,##0.00. See java.text.DecimalFormat for possible values. This pattern is non-localized. It is combined with a Locale to produce a locale-sensitive format.

BooleanTrueDisplayFormat
The text for presenting boolean 'true' values in a report. Example values : Yes, True, Oui.

The HMTL spec states that form controls can appear outside of a form. One may specify a checkbox, as in:

<![CDATA[
  <input type='checkbox' name='true' value='true' checked readonly notab>
]]>

The above is in fact the default value for this item. (CDATA is needed here because of the presence of special HTML characters.) Note that JSTL's c:out tag will need escapeXml set to false, in order to render such items as desired.

Another option is to use an IMG tag containing the desired image.

BooleanFalseDisplayFormat
The text for presenting boolean 'false' values in a report. Example values : No, False, Non.

See BooleanTrueDisplayFormat for more information on this style.

The default value for this item is :

<![CDATA[
  <input type='checkbox' name='false' value='false' checked readonly notab>
]]>

EmptyOrNullDisplayFormat
The text to display when an item has no value. Default value: '-' (a single hyphen). Many browsers do not render empty TABLE cells very well. The CSS 'empty-cells' property is the recommended way of controlling this behavior. As an alternative, this setting may be used to specify text representing empty or null items in a report. Example values : '-', '...'

IntegerDisplayFormat
The format for presenting integer quantities. Default value: #,###. See java.text.DecimalFormat for possible values. This pattern is non-localized. It is combined with a Locale to produce a locale-sensitive format.

FloatDisplayFormat
The format for presenting floating point quantities. Default value: #,###.00. (It is important to remember that money should almost never be represented using float or double.) See java.text.DecimalFormat for possible values. This pattern is non-localized. It is combined with a Locale to produce a locale-sensitive format.

IgnorableParamValue
Any parameter which takes this value will be ignored, by having its value replaced by an empty string, in RequestParser. Default value is an empty String.

Intended solely for SELECT tags, which show a list of items. Even when no selection is made by the user, most browsers will simply pass the first item in the SELECT as the parameter value. If such behavior is undesired, then specify this parameter, and put the IgnorableParamValue as the first item in the SELECT tag.

Typically, one would place unusual characters in IgnorableParamValue, to greatly reduce the likelihood of conflict with regular user input. You may specify an empty value as well, which corresponds to a blank initial value in the list.

In a multilingual application, this setting must not contain translatable text. For example, '----' or an empty string would be an appropriate value for a multilingual application.

IsSQLPrecompilationAttempted
Value : (true | false). Default value: true. This item applies only to the full version of WEB4J - not to the trial version. If true, then upon startup WEB4J will attempt to precompile SQL statements appearing in the .sql file(s), by calling Connection.prepareStatement(String), and detecting if an SQLException is thrown. Any detected errors are logged as SEVERE.

It is important to note that some drivers/databases will NOT throw an SQLException even when the text is syntactically invalid. (This can be determined with a simple test.) Hence, this is always an "attempt" to precompile.

The idea here is to be defensive, and to attempt to detect errors as soon as possible. In addition, this service is very useful when porting an application to a new database : one may start with the "old" statements, and begin by seeing if they compile versus the "new" database. Recommended value is 'true', unless certain the driver/database does not support precompilation in this manner. See SqlId for more information.

MaxRows
Upper limit on the number of rows to be returned by any SELECT statement. Default value: 300. Serves as a defensive safety measure, to help avoid operations which may take an excessively long time. Set value to '0' to turn off this limit. This value is passed to Statement.setMaxRows(int).

FetchSize
Hint sent to JDBC driver regarding the number of rows to be returned when more rows are needed. Default size: 25. Applies only to SELECT statements. Set value to '0' to turn off this hint. This value is passed to Statement.setFetchSize(int).

HasAutoGeneratedKeys
Value : (true | false), ignores case. Default value: false. Indicates if the database and driver support the method Connection.prepareStatement(SqlText, Statement.RETURN_GENERATED_KEYS).

ErrorCodeForDuplicateKey
The error code returned by SQLException.getErrorCode() when trying to add a duplicate record. Default value: 1 (Oracle's value). If this error code is unknown for a given database, you may determine its value by exercising your application, and deliberately causing the error to happen. Some confirmed values for this item :

Oracle 1
MySQL 1062
JavaDB -1

Some unconfirmed values for this item :

SqlServer 2627
DB2 -803
Sybase 2601

SqlFetcherDefaultTxIsolationLevel
The default transaction isolation level used for queries. Default value : DATABASE_DEFAULT. The permitted values are :

DATABASE_DEFAULT (recommended)
SERIALIZABLE
REPEATABLE_READ
READ_COMMITTED
READ_UNCOMMITTED (not recommended !)

For more information, see TxIsolationLevel, and java.sql.Connection. It is important to confirm support for these levels with your database documentation.

SqlEditorDefaultTxIsolationLevel
The default transaction isolation level used for edit operations. Default value : DATABASE_DEFAULT. The permitted values are :

DATABASE_DEFAULT (recommended)
SERIALIZABLE
REPEATABLE_READ
READ_COMMITTED
READ_UNCOMMITTED (not recommended !)

For more information, see TxIsolationLevel, and java.sql.Connection. It is important to confirm support for these levels with your database documentation.

RedirectWelcomeFile
Simple servlet for redirecting directory requests to the home page Action. The source code underlying this item is provided with the example application. It doesn't form part of the WEB4J core. Example value appropriate for development:

http://localhost:8080/fish/main/home/HomePageAction.show

Configuring Implementation Classes

You control the behavior of WEB4J by providing implementations of various interfaces, where needed. About half of these interfaces come with reasonable default implementations. For the others, no reasonable default behavior is possible, so you will need to provide your own implementations. (Implementations in Fish & Chips Club can be used as a guide.)

The BuildImpl class controls the lookup of implementations. It makes a distinction between three kinds of implementations:

Default Implementations

When they exist, any default implementation defined by WEB4J will be used if you take no action to override it. That is, if you do nothing, then default implementations will be used, whenever they exist.

Conventional-Name Implementations

Every interface is paired with a conventional implementation name. This conventional name exists only to reduce configuration effort. If you need to provide your own implementation, you can simply use this conventional package and class name. In that case, WEB4J will find this class upon startup, and register it internally as your desired implementation. The whole idea is that you just implement the class using a given name and package, without needing to do any further configuration of the class name in some text file.

The conventional package name is always 'hirondelle.web4.config', while the conventional class name varies. Please see BuildImpl for a complete listing.

Arbitrary-Name Implementations

If you need to provide an implementation, and for some reason using the above conventional-name style is unsuitable, then you must configure the class name in web.xml. Here is an example of such a configuration:
<init-param>
  <param-name>ImplementationFor.hirondelle.web4j.ApplicationInfo</param-name>
  <param-value>com.xyz.MyAppInfo</param-value>
  <description>
    Package-qualified name of class describing simple, 
    high level information about this application. 
  </description>
</init-param> 
That is, for the interface hirondelle.web4j.ApplicationInfo, your implementation class is com.xyz.MyAppInfo.

Upon startup, the BuildImpl class searches for implementations in the following order:

  1. Arbitrary-Name implementation
  2. Conventional-Name implementation
  3. Default implementation (if it exists)

See BuildImpl for further information.

Multilingual Apps

WEB4J's translation package has a number of items to help you implement multilingual applications. The most important item is the Translator interface. This interface will usually be implemented using either ResourceBundles or a database. In addition, this interface works closely with your implementation of the LocaleSource interface, which defines the end user's preferred language.

Base Text

Any item that is to be translated is referred to by Translator as 'base text'. Base text always appears in the base language of development. For example, a team coding in English would treat English as the base language, and their base text items would always be in English. It is important to understand that this base text comes in two distinct forms:
  • ordinary, natural language (such as 'Add/Edit').
  • coder keys (such as add.edit.button).
The coder key style exists simply because it is sometimes desirable to refer to a translatable item indirectly.

Natural-language base text and coder-key base text differ slightly in their behavior. To illustrate this difference, let's take an example of an app that is developed in English and presented to the user in two languages, English and French. In this case, English is the base language. When the user's language preference is English, any natural language base text such as 'Add/Edit' can be presented to the end user as is, without further processing. However, if a coder key such as 'add.edit.button' is used instead, then such text is always going to need further processing, even when the user's preferred language is English. One might think of coder keys as being in a kind of special language known only to the programmer. Thus, coder keys always need translation, even into the base language of the application.

Minimally Invasive

The translation tools in WEB4J are minimally invasive. From the point of view of the programmer, the implementation of a multilingual app looks almost the same as that of a single language app. In particular, your Java code is exactly the same in the two cases: Model Objects, Actions, and DAOs are unchanged. The only differences are in the presentation layer, in the JSP. And even in the JSP the changes are minimal: your markup will appear much the same as regular, single language markup.

The following tags implement translation in your JSPs:

  • Text - translate a single item.
  • TextFlow - translate all free flow text in entire sections of static markup.
  • Tooltips - translate all TITLE, ALT, and submit-button VALUE attributes appearing in entire sections of markup.
  • Messages - translate and present the items in a MessageList, containing error and information messages.

Using Your App to Assist in Translation

If you store your translations in a database, then you can easily use your implementation of Translator to assist your translation efforts. During development (or even production, if desired), your implementation of Translator can simply gather and record items that are in need of translation. Thus, simply by fully exercising the application, all items that need translation can be easily collected. Once all items needing translation have been placed in the database, screens and Actions can be created to add their translations. This technique is fully illustrated in the Fish & Chips Club example application.

Storing Translations in Application Scope

In a multilingual application, it is probably best to pull in all translations upon startup, and store them in application scope. This can be done as part of your StartupTasks implementation.

Storing translations in application scope allows you to avoid the cost of repeated database lookups for data which rarely changes.

Using One JSP per Language

By default, multilingual apps will use the same JSP to render pages in multiple languages, using the custom tags mentioned above. However, some organizations will still want to instead use a different JSP for each language. (This is possible, but not recommended, since it requires so much repetition of markup.)

Such a design is implemented by subclassing the Controller. By providing an override of its swapResponsePage method, you can alter the default mechanism used by WEB4J to find the ResponsePage returned by your Actions. For example, a page referenced as 'Blah.jsp' in your Action can be changed by your custom Controller, late in processing, to actually refer to some other item such as 'Blah_en.jsp' or 'Blah_fr.jsp', according to the given Locale. (See LocaleSource for more information on Locales.)

The 'Visit' feature of the Fish & Chips Club example application can be used to demonstrate this technique. To exercise it, you simply need to change the servlet-class entry in web.xml, to point to the Visit feature's CustomController.

Logging

Your application can use any logging tool you wish. (The JDK logging tools are usually adequate.)

The WEB4J framework classes (and the example app) use JDK logging. It is recommended that you configure JDK logging to view WEB4J's logging output, by configuring the JDK logging tool. You might get away with configuring logging.properties, but in a shared environment on a server, that will likely be inadequate: the Handlers (output files) defined in logging.properties are per JRE, not per web application. That means that different web apps will output to the same logging file. To address that problem, the LoggingConfig interface and its default LoggingConfigImpl have been provided to allow a programmatic way of configuring logging (if you will permit that somewhat contradictory expression). This programmatic style is per class loader, not per JRE, so its Handlers (output files) are not shared between applications.

Startup Tasks and Code Tables

To perform tasks once upon startup, include them in your implementation of StartupTasks.

One common such task involves code tables. A code table is an enumeration or list of related items in the problem domain. For example, these kinds of items might be implemented as code tables:

  • geographical divisions (countries, provinces, states, and the like)
  • types of payment (credit card, cash, and so on)
  • the names of months (January..December)
In a web application, such items are often presented to the user in a <SELECT> control.

The Code class is provided to help you implement code tables. You are not obliged to use it.

In a multilingual application backed by a database, another common startup task is to place all translations into application scope. Thus, your Translator implementation can access in-memory data, instead of continually querying the database for these relatively unimportant items which rarely change.

What happens if one or more databases are down when your app starts? If that occurs, then upon startup the Controller cannot perform its usual initialization of WEB4J's data layer, nor can it perform your configured StartupTasks. However, for each incoming request, the Controller will repeatedly attempt to connect to all databases. When all databases have been confirmed, then the initialization tasks will finally complete as desired, and your app will subsequently function normally.

File Uploads

The Servlet API has surprisingly poor support for file uploads. (The reason is that forms that contain one or more file upload controls are fundamentally different from regular requests.) The WEB4J API has no direct support for file upload tasks. However, as demonstrated by the example application, it's easy to add file upload support to your WEB4J application, using third party tools.

In the Fish & Chips Club example app, the Apache Commons FileUpload tool is used to implement a request wrapper filter for its file upload operation.

Miscellaneous Tags

Tags not mentioned previously in other sections of this guide include:
  • AlternatingRow - provides styling of alternating rows in a table listing, to increase legibility
  • HighlightCurrentPage - alters links to indicate the current page
  • Pager - helps implement a simple paging mechanism for long listings
  • ShowDate - renders dates in various ways
  • ShowForRole - shows or hides its body according to the roles assigned to the logged in user
  • TagHelper - not a tag, but a base class for creating tags

Utilities

WEB4J includes a util package containing the following :
  • Args - checks parameters in a compact style.
  • Consts - commonly used constants. Items in this class are suitable for static imports into your classes.
  • EscapeChars - escapes special characters in various ways.
  • Stopwatch - times the execution of sections of code.
  • Util - large number of utility methods.
  • WebUtil - utility methods specific to the context of the web.

Unit Testing

Unit tests verify the behavior of individual classes. They can increase your confidence in the correctness of your code. JUnit is often used to implement unit tests.

Different organizations take different approaches to unit testing. Some aggressively unit test all classes, while others prefer to focus on areas most likely to contain bugs. (Many "casual" applications don't include any unit tests at all.)

For an illustration of unit testing, please see the following packages in the example application:

  • hirondelle.fish.test
  • hirondelle.fish.test.doubles
  • hirondelle.fish.main.Member

To run the tests in the example app, perform the following :

  • edit TESTAll.setRootDirectory() to use the root directory of the web app
  • edit FakeConnectionSrc to add your credentials for database access
  • ensure the database driver is on the classpath
  • run TESTAll.main()

WEB4J helps you implement unit tests by providing test doubles. Using the terminology of Gerard Meszaros and Martin Fowler, these test doubles are fake objects (not mock objects). Such fake objects serve to mimic the real runtime environment of a class, using implementations you can control as desired.

The example application includes source code for the following fake objects :

These test doubles implement the methods most likely needed during unit testing. If, for some reason, these fake implementations don't allow you to perform certain tests, then you can simply edit them as desired. (It's not expected that this will be commonly required.)

These fake objects make a number of simplifying assumptions. The most important is that only a single thread of execution is assumed.

Also included is the source code for some utilities you may find useful :

  • FakeConnectionSrc - a fake implementation of ConnectionSource, which allows running tests without using JNDI or an externally defined connection pool.
  • FakeDAOBehavior - controls how fake DAOs behave, allowing you to mimic various kinds of exceptions. This class stores data as simple System properties.

Where To Place Unit Tests

If you follow the package-by-feature style, it's best to place unit tests in the same directory as the class being tested. Thus, the benefits of using package-by-feature remain intact.

Initializing WEB4J Classes

When your app starts up during regular operation, the Controller.init(ServletConfig) method is always called to initialize WEB4J with items specific to your application. When running tests, the same initialization usually needs to be called, but it requires a fake ServletConfig. The fake ServletConfig will often use a fake implementation of ConnectionSource as well.

See the TESTAll.initControllerIfNeeded() method in the example app for an illustration.

Unit Testing Model Objects

Model Objects encapsulate and validate data. Most unit tests for Model Objects are directed towards their validation logic. If you follow the recommended style and implement your Model Objects as immutables, then that validation logic will be implemented in the constructor, and most of your unit tests will simply throw various data sets at the constructor.

Unit Testing DAOs

To unit test Data Access Objects (DAOs), you will need to interact with the database. Various options exist for dealing with any records created as a side effect of unit tests:
  • order your tests such that any records added during tests are simply deleted at the end
  • manually run a database script to return the database to a fixed initial state
  • perform the unit tests inside a transaction, and rollback the transaction at the end of the test
The last option of using a transaction is problematic when using WEB4J's data layer. Since WEB4J's data layer often (but not always) uses internally created connections, rolling back transactions after tests are finished is often not possible. As a workaround, you may be able to wrap operations in a distributed transaction, instead of an ordinary local transaction.

DAOs and Ordering Convention Errors

The WEB4J data layer uses ordering conventions. If those conventions aren't followed, then errors will result. In some cases, having an incorrect order will result in obvious errors that are easy to find. In other cases, however, such errors may not be obvious.

It's useful to test explicitly for ordering errors. In general, you will find more ordering errors if you test with data that clearly distinguishes between different fields/columns.

To illustrate, here's an example using two fields/columns in Member objects (taken from the example app):

 DispositionIsActive
Java TypeIdBoolean
Column TypeMediumIntTinyInt
Similar Values (bad)11
Distinct Values (good)14

If there is a permutation error involving Disposition and IsActive, and if you use similar values for these fields during testing, then the permutation will not be detected. If you use distinct values, then any permutation error will be detected.

So, the guideline is to make sure your test data distinguishes clearly between fields/columns. It's likely best to avoid common, generic values such as 0, 1, nulls, and empty Strings.

Fake DAOs

A fake DAO has the same behavior as a real DAO, but stores underlying data only in an in-memory data structure (often a HashMap), not in a database. If desired, one can define fake DAOs for use with Actions. Using fake DAOs for unit testing Actions has these main benefits :
  • Action unit tests are easily repeated, since they have no persistent side effects
  • Action unit tests might execute significantly more quickly
  • fake DAOs can be made to explicitly throw specific exceptions, perhaps more easily than the real DAO
It's likely prudent to unit test your fake DAOs in addition to the real ones.

Instead of fake DAOs, you might also consider a fake relational database.

Unit Testing Actions

Actions serve as the entry point into a feature. In addition, they use all the other parts of a feature's implementation, either directly or indirectly. There are two points to keep in mind when unit testing Actions:
  • they need a fake request and a fake response
  • it might be best for them to use fake DAOs as well (see above)

Building New Apps From The Example App

The Fish & Chips Club example application can be used not only as a guide or reference, but as an actual starting point for building your applications.

The following is an outline of the recommended steps to follow to build a new application. The general idea is that the example application coexists with your app until it is no longer needed.

Phase 1 - Get New Version Of The Example App Running Against A New Database

1 - Download the latest version of fish.zip. Get it running, using the Getting Started Guide. Make sure the application can be launched and run before proceeding further. Note that .class files are included with fish.zip, so it is not necessary (or recommended) at this stage to compile the source. After each of the remaining steps, it is a good idea to verify that the application remains unbroken for each completed step.

2 - Change the base package/directory from 'hirondelle.fish' to the appropriate value for your new app. The supplied build.xml has an ANT target to help you :

  • in build.xml, set 'replacement.base.package' to the desired value
  • from the root/base directory, run the supplied ANT script : 'ant update.base.package' This ant script changes import statements and packages names.
  • review and update directory names under WEB-INF/classes, if needed
  • make sure you can compile and run the application
  • (at this point, you might review and edit build.xml to suit your needs)

In web.xml, review the settings which depend on the base package :

  • the filter-class setting for the Preferences filter
  • LoggingLevels
  • ImplicitMappingRemoveBasePackage

3 - Create databases of the correct name, and update the ConnectionSrc class to reflect the new names.

There are two ways to update the database name :

  • change the name of the already existing 'fish' databases (see MySQL docs)
  • create brand new databases of the desired name, and run the provided scripts against them. The scripts are in WEB-INF/datastore/mysql/.
The example app uses three databases, so, in the beginning, you will need the same. You can change this later, if needed.

At this stage, databases of the correct name exist, but their tables are temporary; the 'real' tables will be added later.

If MySQL is not available, then the various scripts will need to be ported to the target database. Although that may sound onerous, it is usually not that much work.

4 - Update the server configuration as needed. The connection pool and security realm info will need to reflect the new database names.

5 - edit hirondelle.config.web4j.AppInfo to reflect the correct information.

At the end of this phase :

  • you have a new project directory, with code you can modify, with the correct base package
  • you have a running application, talking to databases having the correct names
  • the databases and application are simply the example app
  • no 'real' features have been implemented yet

Phase 2 - Start Implementing Real Features

Design and create the desired screens and database tables.

New tables should be added beside the existing ones. Do not remove the tables from the example app - not yet. In the beginning, your tables and the tables of the example application will coexist, side by side.

Modify Template.jsp, Footer.jsp, stylesheets, and so on, to create the basic look of your new application. It is often useful to create menu links at this stage as well.

New features (JSPs, code, and .sql files) should be placed beside existing features, in their own packages. Again, don't worry too much about removing existing items until later in development.

Usually, a feature will have these parts :

  • a Java Server Page (JSP), for the view
  • an .sql file, containing SQL statements
  • an Action class
  • a Model Object class
  • a Data Access Object (DAO) class
It is not necessary that all of these parts be created. Use your judgement and sense of taste. For example, a simple report can usually be created without a Model Object or a DAO. (This is demonstrated by the reports in the example application.)

Phase 3 - Remove Unnecessary Items

You should only remove items related to the Fish & Chips Club only when it becomes clear that they will not be needed. If you are unsure, leave it in. It is easy to remove things later, but harder to add them back in.

In most cases, removing a feature reduces to two simple actions : removing a single directory, and removing a menu link from some related template JSP.

Build Your Own Example App

The design of the example app is meant to be representative of a typical application. However, it is not likely that its design is completely appropriate for your needs.

For example, the example app places user access tables in a database separate from the main business domain. Your app design may call for these items to be placed in one and the same database. Or, it may call for JNDI lookup to be used instead.

Version History

The version history of WEB4J is here.

The version history of the Fish & Chips Club example application is here. The version number of the Fish & Chips Club reflects the version of web4j.jar with which it was built. For example, version 3.0.0.1 of the example app was built with version 3.0.0 of web4j.jar.