User Guide
WEB4J Javadoc : link
Version of web4j.jar: 4.5.0
Table of Contents
Example Application
Why WEB4J Was Built
How WEB4J Was Built
Tour of a Typical Feature
Directory Structure
Model Objects
Simple Versus Complex Domain Models
Data Access Objects
The .sql Files
Ordering Conventions
Reports
Transactions
Distributed Transactions
Connections and Multiple Databases
Conversions
Search And Sort Operations
Database Configuration in web.xml
Request Processing
Application Security
SQL Injection Attacks
Cross-Site Scripting (XSS) Attacks
Cross-Site Request Forgery (CSRF) Attacks
Application Firewall
Controlling Sessions
Logging User Data
Preventing Spam
Fine-Grained Security Constraints
Restrict-by-User Constraints
Direct Links To User Table
Indirect Links To User Table
Using Subqueries
Using web.xml and FetchIdentifierOwner
Actions
Convenient Object Key Names
Display Messages To The User
Building Model Objects
Populating Forms
Configuring web.xml
Configuring Database Items in web.xml
Configuring Implementation Classes
Listing of Interfaces and Conventional Names
Multilingual Apps
Base Text
Minimally Invasive
Using Your App To Assist In Translation
Storing Translations in Application Scope
Using One JSP Per Language
Logging
Startup Tasks and Code Tables
File Uploads
Paging
Miscellaneous Tags
Utilities
Unit Testing
Where To Place Unit Tests
Initializing WEB4J Classes
Unit Testing Model Objects
Unit Testing DAOs
DAOs and Ordering Convention Errors
Fake DAOs
Unit Testing Actions
Fake System Clock
Building New Apps From The Example App
Phase 1 - Get Example App Running Against A New Database
Phase 2 - Start Implementing Real Features
Phase 3 - Remove Unnecessary Items
Build Your Own Example App
Version History
Example Application
WEB4J comes with some non-trivial example applications:- Fish & Chips Club - an intranet app. Its access control is based on roles.
- Predictions - a public web app. Its access control is per user, not per role.
Please see the Fish & Chips Club Getting Started Guide for more information.
For those looking for a more compact introduction to WEB4J, or for those with less experience with servlets, please see the tutorial.
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 simpler.How WEB4J Was Built
WEB4J was built slowly. It was built 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 :
- build typical applications on top of the servlet and JSP APIs
- find all repeated code and move it from the applications into the framework
- do it such that the application programmer will feel the maximum elegance in their code
- do not presuppose any particular style of implementation
- 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
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.)
| Item | Avg Lines | Relative Size |
|---|---|---|
| SQL file (.sql) | 25 | |
| Presentation (.jsp) | 108 | |
| Total | 133 | |
| Model (.java) | 111 | |
| Action (.java) | 144 | |
| DAO (.java) | 46 | |
| Total | 301 |
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.
The above diagram shows the relationships between the elements found in a typical feature. The above notation means, for example, that the DAO uses the Model (and not the reverse). The View here is usually a Java Server Page.
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 WEB4J example applications.
There are numerous advantages to this style. Although it may be unfamiliar to you, it is very likely that 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 (the JSP) 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.
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
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's the important point: faulty user input is not a bug. It's 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 SafeText, Decimal, Id, and DateTime classes as building blocks, in the same way as Integer, Boolean, and so on. An Id can hold any kind of identifer you wish. SafeText should usually be used to model free-form user input, to avoid issues with Cross-Site Scripting attacks (see below). The Decimal class is the recommended replacement for BigDecimal, since it makes calculations much easier. The DateTime class is the recommended replacement for the widely reviled java.util.Date.
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.
Simple Versus Complex Domain Models
When using WEB4J's data layer, simple domain models are usually necessary. In the context of WEB4J, simple domain models have two distinguishing characteristics :- no use of the extends keyword (recommended, but not required)
- no hard-coding of 1..N relations (and similar) in domain classes
No extends Keyword
WEB4J isn't sensitive to the use of extends in your domain model. However, it's recommended that you avoid its use, since it's more difficult to work with. For further information, please see the topics in Joshua Bloch's Effective Java related to extends :
- Favor composition over inheritance
- Design and document for inheritance or else prohibit it
- Prefer interfaces to abstract classes
"There is no way to extend an instantiable class and add a value component while preserving the equals contract."
This means that the equals method is, in a sense, broken. If you need to extend a class, and add a new significant field, then it's not possible to provide a correct implementation of equals.
No 1..N Relations
Many are in the habit of hard-coding 1..N relations into the domain model, where one domain class has a List or Set of some other domain class. However, it can be strongly argued that it's better to avoid this, since it often doesn't correspond well with how different features use different data. For instance, take an example of a medical application, with 1 Doctor having N Patients. It's easy to imagine different features/screens having different needs :
- details for a single Doctor
- a listing of N Doctors
- a single Doctor with a listing of their N patients
Each feature uses different data. In some contexts, there's no need to refer to Patients at all. That is, there are common cases in which the underlying relation is of no relevance. To insist that the Doctor class must have an explicit, hard-coded relation to a Patient class disregards this fact. In effect, such a style promotes one particular use case to the detriment of the others.
On the other hand, if the Doctor and Patient classes are independent, then each feature/screen can simply retrieve the data as needed -- no more, and no less. If a feature needs to show a single Doctor along with a list of their Patients, then they are simply retrieved by the Action and placed into request scope. In effect, the relation is implemented in the Action, instead of the domain model. No wiring together of the objects is needed.
If you implement relations in your domain model, then you pay twice: first by defining an explicit link in your domain model, and then by ensuring that items are ignored in any context where they aren't needed. That's a lot of effort, for little benefit. In effect, this seems to create more problems than it solves.
There is a split among programmers between those who take data as king, and those who take the domain model as king. WEB4J agrees with those who take data is king, and as well with those who would like to keep domain classes as simple as 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.
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. WEB4J strictly enforces a one-to-one relation between the SqlId fields defined in your code, and the named statement blocks defined in your .sql files.
Here is an example. The statement named ADD_COMMENT (appearing in an .sql file) must correspond exactly to a single SqlId object (appearing in your code). The text passed to that SqlId's constructor must exactly match the name of the SQL statement.
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.
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. For more information, please see the overview of the database package.
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 the sequence parameters 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's 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 return 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.
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 protecting you from SQL injection attacks.Caveat - the DynamicCriteria class inspects the SQL you pass to it, to ensure that '?' placeholders are used everywhere they should be used, and not literal values. However, its implementation doesn't use a full SQL parser. Rather, it uses relatively simple regular expressions, and there are some cases which are not treated correctly by this class. In such cases, you can override its validation with your own implementation, or you can turn off the validation entirely - after ensuring on your own, of course, that your SQL is properly constructed with '?' placeholders, and will not be open to SQL Injection flaws. See the javadoc for more details.
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
(Your code is almost always concerned with only the last 2 steps appearing above. You don't have to remember this structure. It's presented simply as background.)
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.MemberEditthe implicit URI mapping is calculated as
/main/member/MemberEditThat 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 protect against 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. See above for more information.
- the default implementation of ConvertParam defaults to not allowing String as a building block object. Using SafeText is encouraged instead. This protects against cross-site scripting attacks.
In other cases, however, only optional mechanisms are available. For example, using SafeText to model free-form user input instead of String is highly recommended, but not absolutely 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?)
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 SQL statements defined completely in an .sql file, SQL Injection attacks are not possible. This is because WEB4J always uses a PreparedStatement, and PreparedStatements are not vulnerable to SQL Injection attacks.It's also possible to use SQL statements that are defined partly in an .sql file, and partly in code using the DynamicCriteria class. This class allows dynamic creation of WHERE clauses, but in a way which is still safe from SQL injection attacks.
The reason it's safe is as follows. Any maliciously injected criteria must be static, without any '?' parameters. It cannot be dynamic, since any extra parameters will not be known to the programmer, and the underlying PreparedStatement will fail. Hence, it is sufficient for DynamicCriteria to strictly enforce the rule that no static criteria can be added in code. This is done by demanding that a '?' must appear after each and every SQL operator (=, >, <, and so on) appearing in dynamic criteria.
Cross-Site Scripting (XSS) Attacks
Cross-Site Scripting attacks place executable scripts into free-form user input. When such input is later rendered in the view without taking special precautions, it will execute. To protect against such attacks, you need to escape special characters when reflecting free-form user input in the view. The idea is that escaping special characters will render such scripts unexecutable.Cross-site scripting attacks are dangerous, common, and simple to perform. They are a major problem with web applications in general, and they should be taken very seriously. WEB4J takes the position that, in a web application, Strings containing free-form user input are a dangerous substance.
WEB4J provides SafeText as a replacement for String. SafeText will automatically escape special HTML characters in its toString() method. When needed, other escaping policies are available from SafeText, by calling its getXmlSafe() and getRawString() methods. The principal advantage is that JSP Expression Language can safely be used to render SafeText objects without worrying about special characters.
Tools such as <c:out> which manually escape free-form user input in the view will likely 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 likely increase your confidence in the security of your application.
SafeText has a second line of defense as well. In addition to escaping special characters, it allows you to specify a "white list" of acceptable characters. The white list is defined by your implementation of the PermittedCharacters interface. The default implementation will often be satisfactory.
The ConvertParam interface defines how building block classes are created out of request parameters, before being passed to Model Object constructors. The default implementation always allows SafeText, but it only conditionally allows Strings. This is controlled by the AllowStringAsBuildingBlock setting in web.xml. By default, that setting is turned off. That is the recommended setting. It provides maximum protection against XSS attacks. If you wish to allow your Model Objects to be built with Strings, then you must explicitly allow it.
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 (example) :
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.
Controlling Sessions
Sessions require some management since some attacks attempt to steal session identifiers. Unfortunately, servlets tend to be rather liberal in the creation of superfluous sessions, which leaves unwary programmers open to some attacks.WEB4J helps in these ways :
- WEB4J itself will not create any sessions. It will only use sessions already created by the container during login.
- the optional SuppressUnwantedSessions filter disables URL rewriting entirely. URL rewriting is dangerous since it is a common vector used in session hijacking and session fixation. That is, the security risk from stolen session ids far outweighs other issues regarding session cookies. Of course, this means that the browser must have cookies enabled.
- when possible, the @page directive with session=false should be used in JSPs to minimize creation of unnecessary sessions.
- when the user logs out, your LogoffAction should explicitly destroy the session cookie (example).
In addition, the OWASP project recommends that a new session id should be created after every successful login. However, given the behavior of form-based login (as implemented in Tomcat, in any case) this doesn't seem possible to fully implement as desired. When Tomcat serves the login form, the session id is passed back to the client. Thus, the session id is served just before the user has successfully logged in, not after.
Logging User Data
There is a trade-off involving user data appearing in log files. For developers, seeing user data in log files is often very useful for problem solving. From a security standpoint, however, it's best if sensitive user data does not appear at all in the log. (For instance, think of log files containing credit card numbers.)WEB4J usually logs at the FINE level. Log entries containing user data, however, are always logged at the FINEST level.
The following two items are strongly recommended :
- in production, the logging level should be set such that only a minimal amount of logging occurs.
- if your Actions log user data in any way, it should be logged at the FINEST level, to reduce the likelihood of sensitive data appearing in log files.
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.
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)
When using this technique, incoming URLs appear as follows :
- ../MemberAction.list
- ../MemberAction.add
- ../MemberAction.fetchForChange?Id=3
- ../MemberAction.delete
The Member feature of the example app uses this technique.
When using this technique, you must also ensure that your <servlet-mapping> entries in web.xml allows for the various suffixes.
Restrict-by-User Constraints
Many applications need to restrict access to some operations to the 'owner' of the data (the person who created it). This is commonly seen in public web apps, where the items entered by one user cannot be edited or deleted by anyone else. In general, if an attempt is made by one user to edit items owned by another user, then the system must disallow it, and failing to do so is a major security problem. (Such restrictions may even be extended to include simple 'view' operations, whereby a user's data cannot even be seen by another user.)While the Servlet Specification allows for security constraints based on role (restrict-by-role), it says nothing about security constraints based on user (restrict-by-user). Internal intranet applications usually have restrict-by-role constraints, but public web applications usually have restrict-by-user constraints.
There are a number of ways to implement restrict-by-user constraints.
Direct Links To User Table
The simplest case is for tables which link directly back to the user table (which identifies the owner). Let's take a working example from the Predictions example application that comes with WEB4J.Users <- PredictionList <- PredictionThe Users table holds the user login name, password, and user id. Predictions are arranged in lists, and lists are owned by a single user. Each prediction links back to the Users table like this:
Prediction.PredictionListFK => PredictionList.Id PredictionList.UserFK => Users.IdOperations on the PredictionList table are simpler, since both the operation and its restrict-by-user constraint can usually be expressed in a single SQL statement. Some examples:
UPDATE PredictionList SET Title=? WHERE Id=? AND UserFK=? DELETE FROM PredictionList WHERE Id=? AND UserFK=? SELECT Id, Title, CreationDate, UserFK FROM PredictionList WHERE Id=? AND UserFK=?In each case, the value of the UserFK field is always passed in, to enforce the restrict-by-user constraint. Where does the value of UserFK come from? It's stored in session upon login.
When a user logs in, the user login name (usually not the same as the id) is always placed into session scope by the servlet container. In most databases the user table will be normalized, such that the user id will be used as a foreign key in other tables (and not the user login name). However, it's easy to define a servlet filter to detect successful logins, which can then add any 'extra' data to session scope - in this case, the user id. (Such a filter is also useful for adding user preferences to session scope as well.) Thus, the user id can be easily passed to the above SQL statements.
It's important to realize that the user id is a server-side secret, and should never be displayed in a web page, or sent to the client in any way. It's critical for implementing restrict-by-user constraints, and the end user is not allowed to read or write its value. (Of course, the user's login name is not a server-side secret.)
Indirect Links To User Table
The above case is fairly simple, since the table is directly linked to the user table. If the table is indirectly linked to the user table, however, then more work may be required. Multiple techniques can be used.Using Subqueries
The most compact technique is to use subqueries; in particular, a 'scalar' subquery that returns a single value, the user's id or login name. If your database supports subqueries, then a WHERE clause can usually be added to link back explicitly to the owner. As in the simpler case of a direct link described above, both the operation and the restrict-by-user constraint can still be performed with a single SQL statement.
Here's a deletion operation that uses a subquery to ensure that the owner of the record is the same as a logged in user, named 'bob' :
DELETE FROM Prediction WHERE Id = 65498 AND ( SELECT Users.LoginName FROM Users JOIN PredictonList ON Users.Id = PredictionList.UserFK JOIN Prediction ON PredictionList.Id = Prediction.PredictionListFK WHERE Prediction.Id = 65498 ) = 'bob'Such subqueries can sometimes become difficult to read, because of the nesting. The .sql file format defined by WEB4J can help, since it gives you a simple mechanism to define items as variables, as in
-- first define the subquery
PredictionOwner {
SELECT
Users.LoginName
FROM
Users
JOIN
PredictonList ON Users.Id = PredictionList.UserFK
JOIN
Prediction ON PredictionList.Id = Prediction.PredictionListFK
WHERE
Prediction.Id = ?
}
-- then reference the subquery in the top-level operation
DELETE FROM Prediction
WHERE Id = ?
AND (${PredictionOwner}) = ?
Using web.xml and FetchIdentifierOwner
WEB4J offers a second way of implementing restrict-by-user constraints. This alternative can be used when :
- subqueries aren't supported by your database.
- you want to define in one place all URLs that must implement a restrict-by-user constraint, and simultaneously force corresponding Actions to validate the owner.
The general idea is to use validated proxies for the user id. Continuing the example used above, remember that the Prediction table does not link directly to the Users table, since it's 2 steps away, not 1:
Prediction.PredictionListFK => PredictionList.Id PredictionList.UserFK => Users.IdThe PredictionAction handles all operations on predictions. Here is a listing of its URLs :
/PredictionAction.list?ParentId=5 /PredictionAction.fetchForChange?Id=7&ParentId=5 /PredictionAction.add?ParentId=5 /PredictionAction.change?Id=7&ParentId=5 /PredictionAction.delete?Id=7&ParentId=5Here, the ParentId is really a pointer to the PredictionList table. Its value is a proxy (a substitute) for the user id, since it eventually links back to the user. The problem is that when its value is passed from the client to the server as a ParentId request parameter, it's completely unvalidated, and cannot be used as a proxy for the user id without first validating it.
Validation can be done in 2 steps :
Step 1.
First, add an entry to web.xml of the form :
<init-param> <param-name>UntrustedProxyForUserId</param-name> <param-value> PredictionAction.* </param-value> </init-param>This entry identifies all requests in your application that have a restrict-by-user constraint that uses an unvalidated id. In this example, only one such constraint is defined, but you can define as many as you want, one per line. Thus, you can define all such constraints in a single spot. (See the javadoc for more information on the syntax of this entry.) This is useful since you can see all such constraints in a single place.
Step 2.
Second, the Action
(in this example,
PredictionAction)
must now implement the
FetchIdentifierOwner
interface, which has a single method:
Id fetchOwner() throws AppException;This method is implemented with a Data Access Object in the typical way, using the following underlying SELECT:
SELECT Users.LoginName FROM PredictionList, Users WHERE PredictionList.UserFK = Users.Id AND PredictionList.Id = ?The SELECT is essentially a simple lookup, which translates PredictionList.Id (the unvalidated proxy for the user id) into the login name of the person who "owns" that id. (This corresponds to the subquery mentioned earlier.) The WEB4J Controller then validates for you the result of this SELECT versus the login name stored in the current user's session. If they are the same, then the Action can continue; if not, then the Action is treated as a malicious request, is not processed any further, and an unpolished response is sent to the browser.
The Controller processes each request roughly as follows:
if the request uses an unvalidated proxy for the user id {
cast the Action into FetchIdentifierOwner
if the cast fails {
fail: do not process the request
else {
get the result of fetchOwner()
if it matches the user login name {
success: continue processing the request
}
else {
fail: do not process the request
}
}
}
Note that if you define the request as having a restrict-by-user constraint, and then forget to change your Action
to implement FetchIdentifierOwner, then the Action will fail. This protects you in case you forget.
In summary, WEB4J defines these items for validating proxies for the user id:
- UntrustedProxyForUserId - an interface; defines which requests carry unvalidated proxies for the user id.
- UntrustedProxyForUserIdImpl - the default implementation, which uses the init-param in web.xml, as described above. (It's expected that the great majority of applications will use this default implementation.)
- FetchIdentifierOwner - implemented by Actions that need to validate a proxy for the user id.
Here is as comparison of the two techniques for implementing restrict-by-user constraints :
| Item | Subquery | FetchIdentifierOwner |
|---|---|---|
| Single unit of work | Yes | No |
| Define in one place | No | Yes |
| Forces Action to implement | No | Yes |
| Needs 'extra' work | No | Yes |
Regular DAOs
If, for some reason, you don't wish to use the mechanisms described above, you can always implement restrict-by-user constraints using a regular DAO method:- each operation 'manually' performs the check on ownership
- a SELECT for the owner is first performed
- the result is compared to the id or login name of the current user
- if there is a match, then proceed with the core operation; otherwise it fails
- a transaction might be used to ensure a consistent view
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.
- 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.
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 (see /WEB-INF/tags/displayMessages.tag), always placed at the top of a templated 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.
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.
There are numerous examples of building Model Objects in this way in the example application :
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 implement 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
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.HomePageActionis implicitly mapped to the URI:
/main/home/HomePageActionby 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
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.
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=FINEIt 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.
Controls the behavior of ApplicationFirewallImpl
for file upload requests.
When ON, then ApplicationFirewallImpl will
treat file upload requests the same as any other request, and will check request parameter values in the usual way.
When OFF, then it will not perform such validation for file upload requests.
The problem addressed here is that file upload requests are fundamentally different from regular requests. In particular, access to request parameters is possible only if the file upload request has been 'wrapped' in some way, such that request parameters can be read in the usual way. This setting should be ON only if such request wrapping has been performed. Otherwise, it should be OFF.
AllowStringAsBuildingBlock
Permitted values : YES and NO. Default value: NO.
When YES, then the default implementation of ConvertParam will
allow String as a building block object, along with Integer, Date, and so on.
When set to NO, you will not be able to pass a String to any Model Object constructor, and you will need to use SafeText instead.
The idea here is that using SafeText is preferable to String.
Since String does not escape special characters, it's vulnerable to Cross-Site Scripting attacks.
UntrustedProxyForUserId
Default value: empty String. Lists all URLs having a Data Ownership Constraint implemented with an untrusted identifier.
The syntax of this entry is described by UntrustedProxyForUserIdImpl.
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 java.util.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.
DecimalStyle
Rounding behavior for the Decimal class.
Default value : 'HALF_EVEN,2'.
Comma-separated pair of values: RoundingMode,
and the number of decimals to be used to round the results of
multiplication and division operations.
See Decimal.init() for more information.
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 Decimal values.
This item is not affected by Locale.
BigDecimalDisplayFormat
Format for display of BigDecimal and Decimal 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.
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.
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(s) returned by SQLException.getErrorCode()
when trying to add a duplicate record. Default value: 1 (Oracle's value).
If you wish to treat more than one error code as being related to
duplicate key constraints, you may specify a comma-delimited list of integers.
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.
When web4j detects these error codes, then it will throw a
DuplicateException, instead of a DAOException.
Some confirmed values for this item :
| Oracle | 1 |
| MySQL | 1062 |
| JavaDB/Derby | -1 |
Some unconfirmed values for this item :
| SqlServer | 2627 |
| DB2 | -803 |
| Sybase | 2601 |
ErrorCodeForForeignKey
The integer error code(s) returned by SQLException.getErrorCode()
when a foreign key constraint fails.
If you wish to treat more than one error code as being related to
foreign key constraints, you may specify a comma-delimited list of integers.
If this error code is unknown for a given database, you may
determine its value by exercising the example application, and
deliberately causing the error to happen.
When web4j detects these error codes, then it will throw a
ForeignKeyException, instead of a DAOException.
Some example values (please confirm using your database documentation):
| Oracle | 2291 |
| MySQL | 1217 |
| JavaDB/Derby | 23503 |
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.
DateTimeFormatForPassingParamsToDb
Formats used when passing DateTime objects as
parameters to SQL statements.
Default value: YYYY-MM-DD^hh:mm:ss^YYYY-MM-DD hh:mm:ss
Three formats must be specified here, corresponding to 3 common cases (in this order):
- date only (year-month-day are present, but no other units)
- time only (hour-minute-second are present, but no other units)
- both date and time (all units are present)
The format Strings are those defined by DateTime. The 3 format Strings are separated by a ^ character.
If a DateTime parameter to an SQL statement needs to be passed in a format which is not specified here, simply format the DateTime in your code as desired, and pass the result to the database as a fully formatted String, instead of a DateTime.
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 Database Items in web.xml
When a database setting has a single value applicable across all of your databases, you simply specify that single value, as usual. When a database setting takes different values for different databases, you define the various values with the following syntax :100~Translation=200~AccessControl=300The '~' character is used as a separator. Here, the first entry '100' represents the setting for the default database. It also represents the setting for all other databases, unless an override value is provided by one of the 'name=value' pairs. Thus, the above represents :
| Database Name | Value |
|---|---|
| [the default db] | 100 |
| Translation | 200 |
| AccessControl | 300 |
| [any other db name] | 100 |
There is a single exception - the TimeZoneHint setting can only take a single value, to be applied across all databases. (The reason for this unfortunate exception is simply that changing it would have a large number of ripple effects, and would represent too much pain for too little gain.)
This syntax is particularly useful for applications which use not only multiple databases, but also multiple database vendors (MySQL and PostgreSQL in the same application, for example).
Configuring Implementation Classes
You control the behavior of WEB4J by providing implementations of various interfaces, where needed. Most of these interfaces come with reasonable default implementations. For the remainder, no reasonable default behavior is possible, so you will need to provide your own implementations. (Implementations in the example applications can be used as a guide.)The BuildImpl class controls the lookup of implementations. BuildImpl 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 below 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:
- Arbitrary-Name implementation
- Conventional-Name implementation
- Default implementation (if it exists)
See BuildImpl for further information.
Listing of Interfaces and Conventional Names
Here is a listing of all interfaces used by WEB4J, along with conventional class names, and either a default or an example implementation. The package for conventional class names is always 'hirondelle.web4j.config'.| Question | Interface | Conventional Impl Name, in hirondelle.web4j.config | Default/Example Implementation |
|---|---|---|---|
| What is the application's name, version, build date, and so on? | ApplicationInfo | AppInfo | example |
| What tasks need to be performed during startup? | StartupTasks | Startup | example |
| What Action is related to each request? | RequestParser (an ABC) | RequestToAction | RequestParserImpl |
| Which requests should be treated as malicious attacks? | ApplicationFirewall | AppFirewall | ApplicationFirewallImpl |
| Which requests use untrusted proxies for the user id? | UntrustedProxyForUserId | OwnerFirewall | UntrustedProxyForUserIdImpl |
| How is spam distinguished from regular user input? | SpamDetector | SpamDetect | SpamDetectorImpl |
| How is a request param translated into a given target type? | ConvertParam | ConvertParams | ConvertParamImpl |
| How does the application respond when a low level conversion error takes place when parsing user input? | ConvertParamError | ConvertParamErrorImpl | example |
| What characters are permitted for text input fields? | PermittedCharacters | PermittedChars | PermittedCharactersImpl |
| How are dates and times formatted and parsed? | DateConverter | DateConverterImpl | example |
| How is a Locale derived from the request? | LocaleSource | LocaleSrc | LocaleSourceImpl |
| How is the system clock defined? | TimeSource | TimeSrc | TimeSourceImpl |
| How is a TimeZone derived from the request? | TimeZoneSource | TimeZoneSrc | TimeZoneSourceImpl |
| What is the translation of this text, for a given Locale? | Translator | TranslatorImpl | example |
| How does the application obtain a database Connection? | ConnectionSource | ConnectionSrc | example |
| How is a ResultSet column translated into a given target type? | ConvertColumn | ColToObject | ConvertColumnImpl |
| How should an email be sent when a problem occurs? | Emailer | EmailerImpl | |
| How should the logging system be configured? | LoggingConfig | LogConfig | LoggingConfigImpl |
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).
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.
The default LoggingConfigImpl has the interesting property of allowing you to log records with a time stamp derived from your application's fake system clock. This is because it uses a TimeSource.
Startup Tasks and Code Tables
To perform tasks once upon startup, include them in your implementation of StartupTasks. In the example application, the implementation loads both code tables and translations into memory.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)
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 (previous to version 3.0) 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.
Version 3.0 of the Servlet API (published January, 2010) does have support for file uploads.
Paging
When a listing contains a large number of records, it's often best to provide end users with a paging mechanism. WEB4J provides a simple way to implement such a paging mechanism, using (in part) a custom JSP tag called Pager.The Pager tag lets you emit links corresponding to the first, next, or previous page. The links don't perform the actual 'subsetting' of the data. That's done elsewhere, in one of 3 possible places :
- in the JSP itself, using <c:out>
- in a DAO method
- in a SELECT statement
Please see the Pager javadoc for further information. As well, the Fish & Chips Club example application implements paging in its Discussion feature, where users post comments to a simple message board.
Note as well that there is a MaxRows setting in web.xml which controls the maximum number of records returned by a SELECT.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. The example app's Resto feature uses this tag in its listing.
- HighlightCurrentPage - alters links to indicate the current page. In the example app, the Template.jsp files use this tag to implement menus.
- ShowDate - renders dates in various ways. In the example app, the Visit feature uses this tag to render dates in a particular format.
- 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.
There are also 2 methods in BuildImpl which were created specifically for creating a proper test environment:
- adHocImplementationAdd(Class, Class)
- adHocImplementationRemove(Class)
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
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):
| Disposition | IsActive | |
|---|---|---|
| Java Type | Id | Boolean |
| Column Type | MediumInt | TinyInt |
| Similar Values (bad) | 1 | 1 |
| Distinct Values (good) | 1 | 4 |
If there is a permutation error involving Disposition and IsActive, and if you use similar values for these fields during testing, then the permutation error 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
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)
Fake System Clock
For many applications, it's often desirable to test different "processing dates". Instead of changing the real system clock directly, however, it's usually best to use a fake system clock. The important point here is to avoid direct calls to these common items :- System.currentTimeMillis()
- the default constructor for the Date class (which in turn uses System.currentTimeMillis())
WEB4J provides the TimeSource interface to help you implement a fake system clock. When you define such a TimeSource, you are both defining a fake system clock for your application, and you are sharing your fake system clock with WEB4J framework classes. Thus, your application and the framework can use exactly the same clock, and will remain synchronized.
In addition, this same mechanism allows the default logging mechanism to timestamp records with your fake system clock time. See javapractices.com for an example.
Building New Apps From The Example App
The Fish & Chips Club and Predictions example applications can be used not only as a guide or reference, but as actual starting points 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 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, scan for settings which depend on the base package, and update them:
- the filter-class setting for the Preferences filter, and the File Upload filter
- LoggingLevels
- ImplicitMappingRemoveBasePackage
- LoggingDirectory
- 3 XXXDbConnectionString settings
3 - Create databases of the correct name.
There are two ways to update the database name :
- change the name of the already existing 'fish' databases (see MySQL docs)
- alter the script provided in WEB-INF/datastore/mysql/CreateALL.SQL, and run it.
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 database script 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
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's likely that its design is not completely appropriate for your needs.For instance, 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
Here are links to the version histories of:- WEB4J
- the Fish and Chips Club example application
- the Predictions example application
The version numbers of the example apps 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.