Architecture
For our sample BookManager project, we chose a mix of well-understood, mature, and widely available open source technologies to implement a standard n-tiered, web-based application. This is a typical JEE app that has a Presentation layer made with JSPs, a Services layer made with Struts actions, and a Persistence layer that uses Hibernate as a front-end to a Derby database. This architecture is shown in the following figure:
Let's examine each of these logical tiers.
Presentation tier
The client is presented as a series of web pages, generated by JSP, using the Apache Struts 2.x framework. Struts is a very mature set of servlets and JSP tag libraries that provides a classic MVC (Model-View-Controller) pattern for web-based presentation tiers written in Java. The look and feel is enhanced through the use of basic CSS (Cascading Style Sheets). You can learn more about Struts from the project's home page at struts.apache.org.
Services tier
The services are written as Java servlets, the majority of which are implemented as Struts Actions. This provides the controller piece of the MVC implementation of Apache Struts. The work of validating input, persisting the data, and retrieving them is handled by these servlets. This approach allows us to deploy our application on any Sun-compliant servlet container; in our case, Apache Tomcat.
Many of the features such as exception handling, file uploading, lifecycle callbacks, and validation are provided by Interceptors (these are conceptually the same as servlet filters) or the JDK's Proxy
class. They provide a way to supply pre-processing and post-processing around an action.
The Struts framework is completely configurable via XML files.
Persistence tier
The data is stored in a relational database (RDBMS) that is abstracted by the open source Hibernate 3.x framework. Hibernate provides a simple object-relational (OR) mapping construct that makes it easy to persist Java beans in a relational database, without having to write Structured Query Language (SQL) or hand-coded mapping logic. The mapping is stored in easily modifiable and distributable XML files, which are read by Hibernate. In turn, Hibernate wraps the complexity of all database activity, including connecting, communicating, and performing typical Create, Read, Update, and Delete (CRUD) functions.
Hibernate is an abstraction of Persistence, which still requires an implementation of some sort. For our project, we are using the Apache Derby embedded database. Derby is a lightweight, completely pure Java database that can be bundled with an application and distributed as part of the final software package, without need for a separate installation and setup. Likewise, it is instantiated and used at run-time and thus, does not require a separate process that must be managed independent of BookManager Application. While this is suitable for our purposes, the configuration we're using does not scale, and must be replaced either with a server-based Derby install or another RDBMS system (such as MySQL, Oracle, and so on). Fortunately, Hibernate makes it easy to switch; a few changes to one of the configuration files and the inclusion of the proper JDBC library (that is, the library that facilitates communication with the database from Java) is all that's needed to migrate the BookManager Application to a different database product.
Control flow
The Struts framework is the backbone of the BookManager Application and we use it to specify the flow of control based on user actions. These are configured in a struts.xml
file that describes which JSPs work with which Actions and under what conditions. The following is the Struts configuration XML source code:
<!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN" "http://struts.apache.org/dtds/struts-2.0.dtd"> <struts> <package name="default" extends="struts-default"> <interceptors> <interceptor name="checkAuthentication" class="client.interceptor.LoginInterceptor" /> <interceptor-stack name="booklookDefaultStackNoAuth"> <interceptor-ref name="createSession"/> <interceptor-ref name="defaultStack"/> </interceptor-stack> </interceptors> <default-interceptor-ref name="booklookDefaultStack" /> <!-- This section provides a single routing point for any errors thrown by the server, as well as forcing the user back to the login page in the event he is not authenticated. --> <global-results> <result name="error">/error.jsp</result> <result name="login">/login.jsp</result> </global-results> <global-exception-mappings> <exception-mapping exception="org.apache.struts.register.exceptions.SecurityBreachException" result="securityerror" /> <exception-mapping exception="java.lang.Exception" result="error" /> </global-exception-mappings> <!-- This section maps the individual servlets to the pages that should be displayed as a result. Each servlet is an action that corresponds to an activity the user can perform. The login action is different from the others in that if an error occurs, we display the login page again, rather than an error page --> <action name="login" class="client.action.LoginAction"> <interceptor-ref name="booklookDefaultStackNoAuth"/> <result name="success">welcome.jsp</result> <result name="error">login.jsp</result> </action> <action name="addbookscreen"class="client.action.AddBookScreenAction"> <result>addbook.jsp</result> </action> <action name="addbook" class="client.action.AddBookAction"> <result name="success" type="redirectAction">listbooks</result> </action> <action name="listbooks" class="client.action.ListBooksAction"> <result>listbooks.jsp</result> </action> <action name="editbookscreen"class="client.action.EditBookScreenAction"> <result>editbook.jsp</result> </action> <action name="editbook" class="client.action.EditBookAction"> <result name="success" type="redirectAction">listbooks</result> </action> <action name="deletebook" class="client.action.DeleteBookAction"> <result name="success" type="redirectAction">listbooks</result> </action> <action name="logout" class="client.action.LogoutAction"> <interceptor-ref name="booklookDefaultStackNoAuth"/> </action> </package> </struts>
Interceptors
Struts 2 has an interceptor feature that allows a developer to process the workflow of any Struts request, prior to it being served to the user. We added a LoginInterceptor
class to the existing chain of Struts interceptors (defined by the line defaultStack
) to check if a USER
object is present in the HTTP
session prior to each Struts action being served. If it is the case, we assume the user has authenticated via the login page and continue the user onto their originally requested action. If not, we bypass the original user's action and force him to the login.jsp
page to enter his credentials. These credentials are sent to a LoginAction
in Struts, which builds a BookManagerUser
object and queries the Derby database via Hibernate to see if the user's login and password are a match. If so, it creates a USER
session variable with a BookplaneUser Javabean
object in it. This variable contains not only the user's login and SHA-encrypted password but also his role. Any user with a role of admin will be permitted to add and update book information; all other user roles can only view the book list.
In the current BookManager Application, the struts.xml
file is configured with interceptors, global results, and actions. It is typical to have several interceptors assigned per action. As you can imagine, having to configure every interceptor for each action would quickly become extremely unmanageable. For this reason, interceptors can be grouped into named stacks. In our case, we've created an interceptor stack named booklookDefaultStackNoAuth
that combines the out of the box defaultStack
and createSession
interceptors, and attaches them to the login and logout actions. The following is the code for the LoginInterceptor
:
package client.interceptor; import java.util.Map; import com.opensymphony.xwork2.Action; import com.opensymphony.xwork2.ActionContext; import com.opensymphony.xwork2.ActionInvocation; import com.opensymphony.xwork2.interceptor.AbstractInterceptor; @SuppressWarnings("serial") public class LoginInterceptor extends AbstractInterceptor { public String intercept(ActionInvocation actionInvocation) throwsException { Map<String, Object> session =ActionContext.getContext().getSession(); Object booklookUserObject = session.get("USER"); if (booklookUserObject == null) { return Action.LOGIN; } return actionInvocation.invoke(); } }
From the above LoginInterceptor.java
source, the class has a single method implementation, intercept()
. Using custom interceptors in your application is an elegant way to provide cross-cutting application features. The AbstractInterceptor
class provides a default no-op implementation of both the destroy
as well as the init
method.
To allow a user to logout, we added a LogoutAction
and links to it on each page of the application. When a user selects this, his USER
session variable is deleted, which in turn forces the LoginInterceptor
to return the user to the login.jsp
. As the entire BackplaneUser
object is present in the session, we can access it from inside each of the JSPs. We use this to add the user's login name to a welcome message at the top of each screen in the application.
The LoginInterceptor
is configured to run by default on all actions except the login and logout actions. This is because if we were to force a check on the login during a login, we'd wind up in an infinite loop! We provide this login-free path for the login and logout by defining an alternate interceptor stack that does not contain the LoginInterceptor
and assigning it as the path for LoginAction
and LogoutAction
.
Actions
Actions are a fundamental concept in most web application frameworks and they are the basic unit of work that can be associated with an HTTP request coming from a browser. The very basic usage of an action is to perform work with a single result always being returned.
package client.action; import java.util.Map; import server.beans.BooklookUser; import server.services.PasswordEncrypter; import server.services.Persistence; import com.opensymphony.xwork2.ActionContext; import com.opensymphony.xwork2.ActionSupport; @SuppressWarnings("serial") public class LoginAction extends ActionSupport { private String username; private String password; public String getUsername(){ return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password){ this.password = password; } public String execute() throws Exception { Map<String, Object> session =ActionContext.getContext().getSession(); String encryptedPassword = PasswordEncrypter.encrypt(password); BooklookUser user = Persistence.getInstance().getUser(username,encryptedPassword); if (user == null) { session.put("LOGINSUCCESS", "false"); return ERROR; } session.put("LOGINSUCCESS", "true"); session.put("USER", user); return SUCCESS; } }
From the above code listing of the LoginAction
, the execute
method gets the session, retrieves the user with that username-password, and finally sets the session. Thus, you can imagine the Action doing a piece of work from the execute method and returning a string.
You can see from the above struts.xml
listing, the action name is associated to the action class, which is responsible for the execute
method. Optionally, the interceptor reference name is also mentioned, which takes care of the additional orthogonal functionality. The interesting thing here is, if the result is a success, then welcome.jsp
is invoked; otherwise the user is served login.jsp
. Remember that the result of the execute()
method of LoginAction
is a String.
<action name="login" class="client.action.LoginAction"> <interceptor-ref name="booklookDefaultStackNoAuth"/> <result name="success">welcome.jsp</result> <result name="error">login.jsp</result> </action>
PasswordEncrypter
and Persistence are two services that are used in most of our Action classes. More importantly, the Persistence class provides several APIs for retrieving the sessions, looking up users, and maintaining the book data.
Admin
Admin is a simple utility module for administering the users and their access to the BookManager Application. It is an executable JAR with a main class that accepts command line input, and makes static calls to a UserAdmin
class. This class handles building the necessary Hibernate objects and adding or retrieving them from the Derby database via Hibernate calls.
The Admin utility needs to be run before the application war is actually deployed on to the servlet container, to create and populate the Derby database schema and add users and administrators for application access.
Flow summary
We have seen how different aspects of the Interceptors, Actions, and Admin module work together. Now, we will see how a single request to log in from the browser translates to different actions:
The browser requests the
login.action
.The Filter Dispatcher of the Struts 2 framework looks at the request and determines the appropriate Action—in this case
LoginAction
(defined in thestruts.xml
file).Next, the Interceptors are applied. In this case, the
booklookDefaultStackNoAuth
interceptor is applied to the action (defined in thestruts.xml
file).Next, the Action method is executed. In our example, the appropriate method on the action login is executed to authenticate the user from the database and a welcome page (
welcome.jsp
) is shown. If the authentication fails, the user is shown an error message and the login page (login.jsp
).
In case of other action such as addbook
, the Action is executed and redirected to the listbooks
action. You can observe that Actions and redirections are mentioned declaratively in the struts.xml
configuration file:
<action name="addbook" class="client.action.AddBookAction"> <result name="success" type="redirectAction"> listbooks </result> </action>