Tuesday, April 10, 2007

Tomcat and WebWork


In this chapter we will finally get rid of our ugly command-line-based test
application and integrate Tomcat, WebWork and Hibernate to create a little web-based
application.


The first step for you is to install Tomcat - you can download it here.
This example uses Tomcat 5.0.28 if you want it to work flawless, you should
use this version, too. You can find documentation and install instructions for
Tomcat here.



Restructuring the development directory


At first, we will restructure our working directory, to keep the parts of our
application nicely separated:



.
+src
+de
+gloegl
+road2hibernate
+data
User.java
Event.java
User.hbm.xml
Event.hbm.xml
+actions
hibernate.cfg.xml
log4j.properties
+config
+static-web
+views
+lib
antlr-2.7.4.jar
cglib-full-2.0.2.jar
commons-collections-2.1.1.jar
commons-logging-1.0.4.jar
hibernate3.jar
jta.jar
dom4j-1.5.2.jar
jdbc2_0-stdext.jar
log4j-1.2.9.jar
hsqldb.jar

We moved our classes and mappings to the data subdirectory of road2hibernate.
Of course we need to adjust our package declaration in the java files and the
classnames in the mapping files.


In addition, we created various new directories:



  • src/de/gloegl/road2hibernate/actions will contain the source code
    for our WebWork actions.

  • config will contain the config files which will later be placed
    in WEB-INF/config of our web app

  • static-web will contain all static content of our application,
    like html and image files

  • views will contain our view files which contain the html later
    displayed to the user


As we will use WebWork for our application and Velocity as template engine
for the views, you will need to get WebWork from here.
You will need the webwork-2.1.7.jar from the WebWork download, and all jar files
in the lib/core folder of the WebWork distribution. Place all of them into the
lib directory of the development dir.



Configuring WebWork


Now we will configure our application to use WebWork. At first we need a web.xml
which we will place in the config directory:


config/web.xml:



<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
<display-name>Eventmanager</display-name>

<servlet>
<servlet-name>webwork</servlet-name>
<servlet-class>
com.opensymphony.webwork.dispatcher.ServletDispatcher
</servlet-class>
<load-on-startup>1</load-on-startup>

</servlet>

<servlet>
<servlet-name>velocity</servlet-name>
<servlet-class>
com.opensymphony.webwork.views.velocity.WebWorkVelocityServlet
</servlet-class>
<load-on-startup>10</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>webwork</servlet-name>
<url-pattern>*.action</url-pattern>
</servlet-mapping>

<servlet-mapping>
<servlet-name>velocity</servlet-name>
<url-pattern>*.vm</url-pattern>
</servlet-mapping>

<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
</web-app>

This files contains all the servlets and servlet mappings which WebWork needs
to run.


In addition, we will need a xwork.xml file, which is placed directly in the
source dir:


src/xwork.xml:



<!DOCTYPE xwork PUBLIC "-//OpenSymphony Group//XWork 1.0//EN"
"http://www.opensymphony.com/xwork/xwork-1.0.dtd">

<xwork>
<!-- Include webwork defaults (from WebWork JAR). -->
<include file="webwork-default.xml" />


<!-- Configuration for the default package. -->
<package name="default" extends="webwork-default">
</package>
</xwork>

This file contains only the declaration of the packages where WebWork will
look for its actions.



Updating the build process


Now we will make our build process place all the files together and generate
the appropriate structure for the web application:


from build.xml:



<project name="hibernate-tutorial" default="compile">

<property name="sourcedir" value="${basedir}/src"/>

<property name="targetdir" value="${basedir}/bin"/>
<property name="librarydir" value="${basedir}/lib"/>
<property name="configdir" value="${basedir}/config"/>

<property name="wardir" value="${basedir}/war"/>
<property name="viewsdir" value="${basedir}/views"/>
<property name="static-htmldir" value="${basedir}/static-web"/>

<target name="setup-war-structure" depends="compile">
<mkdir dir="${wardir}"/>
<mkdir dir="${wardir}/WEB-INF"/>

<mkdir dir="${wardir}/WEB-INF/classes"/>
<mkdir dir="${wardir}/WEB-INF/lib"/>
<mkdir dir="${wardir}/WEB-INF/views"/>

<copy todir="${wardir}/WEB-INF/classes">

<fileset dir="${targetdir}"/>
</copy>
<copy todir="${wardir}/WEB-INF/lib">
<fileset dir="${librarydir}"/>

</copy>
<copy todir="${wardir}/WEB-INF/views">
<fileset dir="${viewsdir}"/>
</copy>
<copy todir="${wardir}/WEB-INF">

<fileset dir="${configdir}"/>
</copy>
<copy todir="${wardir}/">
<fileset dir="${static-htmldir}"/>

</copy>
</target>

...

Running this target will create a directory called war, which will
contain the web application just like it will later be structured in the war
file. After running this target, the war directory will look like this:



+war
+WEB-INF
web.xml
+classes
+de
+gloegl
+road2hibernate
+data
User.class
Event.class
User.hbm.xml
Event.hbm.xml
+actions
hibernate.cfg.xml
log4j.properties
xwork.xml
+lib
<all library files here>


Now we will add tomcat specific tasks to our build file, so we can directly
install our application into tomcat using ant. You need to adjust the example
to your environment:


from build.xml:



<project name="hibernate-tutorial" default="compile">

....

<!-- ADJUST THIS ! -->
<property name="manager.url" value="http://localhost:8080/manager"/>

<property name="tomcatdir" value="C:/Program Files/Tomcat"/>
<property name="app.path" value="/eventmanager"/>
<property name="manager.username" value="username"/>

<property name="manager.password" value="password"/>

<path id="tasks.classpath">
<fileset dir="${tomcatdir}/server/lib">

<include name="catalina-ant.jar"/>
</fileset>
</path>

<taskdef
name="install"
classname="org.apache.catalina.ant.InstallTask"
classpathref="tasks.classpath"/>

<taskdef
name="reload"
classname="org.apache.catalina.ant.ReloadTask"
classpathref="tasks.classpath"/>
<taskdef
name="remove"
classname="org.apache.catalina.ant.RemoveTask"
classpathref="tasks.classpath"/>

<target name="install" depends="setup-war-structure">
<install url="${manager.url}"
username="${manager.username}"
password="${manager.password}"

path="${app.path}"
war="file://${wardir}"/>
</target>

<target name="reload" depends="setup-war-structure">
<reload url="${manager.url}"

username="${manager.username}"
password="${manager.password}"
path="${app.path}"/>
</target>

<target name="remove">

<remove url="${manager.url}"
username="${manager.username}"
password="${manager.password}"
path="${app.path}"/>

</target>

...
</project>

So now startup tomcat and run ant install from the commandline - this
will install the application to tomcat. Now you should be able to access the
eventmanager application by pointing your browser to http://localhost:8080/eventmanager/
- however you will only get a directory listing produced by Tomcat. We will
now have to create some content for the application to display.



A first WebWork action


Our first WebWork action will be very simple - it will do nothing so far but
forwarding to a view:


/src/de/gloegl/road2hibernate/actions/EventList.java:



package de.gloegl.road2hibernate.actions;

import com.opensymphony.xwork.ActionSupport;

public class EventList extends ActionSupport {

}

The class extends ActionSupport but does not override any methods. So this
will rely on the default behavior of ActionSupport: The action will forward
to the view defined as the SUCCESS view. This is what we still have to do in
the xwork.xml file:


/src/xwork.xml:



<!DOCTYPE xwork PUBLIC "-//OpenSymphony Group//XWork 1.0//EN"
"http://www.opensymphony.com/xwork/xwork-1.0.dtd">

<xwork>
<include file="webwork-default.xml" />


<package name="default" extends="webwork-default">
<default-interceptor-ref name="defaultStack" />

<action name="eventlist" class="de.gloegl.road2hibernate.actions.EventList">
<result name="success" type="dispatcher">/WEB-INF/views/eventlist.vm</result>
</action>
</package>

</xwork>

This defines an action called eventlist which will be handled by our EventList
class. In addition, the success view gets defined. We now have to write the
eventlist.vm file, which will be placed in the views folder - ant will
copy it to the correct location.


views/eventlist.vm:



<html>
<head>
<title>Event List</title>

</head>
<body>
<h1>Successful</h1>
</body>
</html>

As you see, this is just a simple HTML file - we will modify it to include
dynamic content later on. Now run ant reload. Now you should be able
to test the action by pointing your browser to http://localhost:8080/eventmanager/eventlist.action
- you should see the "Success" headline in your browser.



The "Open Session in View" pattern


When using such features as lazy loading, Hibernate needs an open session.
To do this easily in a web application we will use a simple pattern. We will
create a servlet filter which will manage our session and close it at the end
of the web request. Also we improve our HibernateUtil class to include transaction
handling per-thread:


src/de/gloegl/road2hibernate/util/HibernateUtil.java:



...
public static final ThreadLocal threadTransaction = new ThreadLocal();

public static void beginTransaction() {
Transaction tx = (Transaction) threadTransaction.get();
if (tx == null) {
tx = getSession().beginTransaction();
threadTransaction.set(tx);
}
}

/**
* Commit the database transaction.
*/
public static void commitTransaction() {
Transaction tx = (Transaction) threadTransaction.get();
if ( tx != null && !tx.wasCommitted()
&& !tx.wasRolledBack() ) {
tx.commit();
}
threadTransaction.set(null);
}

public static void rollbackTransaction() {
Transaction tx = (Transaction) threadTransaction.get();
try {
threadTransaction.set(null);
if ( tx != null && !tx.wasCommitted() && !tx.wasRolledBack() ) {
tx.rollback();
}
} finally {
closeSession();
}
}
}


We can now use this methods to do all Session and Transaction handling in a
Servlet Filter called SessionManager:


src/de/gloegl/road2hibernate/util/SessionManager.java:



package de.gloegl.road2hibernate.util;

import java.io.*;
import javax.servlet.*;

import org.hibernate.SessionFactory;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.HibernateException;
import org.hibernate.cfg.Configuration;

public class SessionManager implements Filter
{
public void init(FilterConfig config) {
}

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException
{
try
{
chain.doFilter(request, response);
}
finally
{
HibernateUtil.commitTransaction();
HibernateUtil.closeSession();
}
}

public void destroy() {
try {
Session s = HibernateUtil.currentSession();
HibernateUtil.beginTransaction();
s.connection().createStatement().execute("SHUTDOWN");
HibernateUtil.commitTransaction();
HibernateUtil.closeSession();
}
catch (Exception ex) { ex.printStackTrace(); throw new RuntimeException(ex); }
}
}

Let's look at what is done here: The HibernateUtil class will store a Hibernate
session in a ThreadLocal variable. A ThreadLocal will contain an instance of
a variable once for every thread. So if there are two parallel Threads executing,
the ThreadLocal will contain two session. In SessionManager, the doFilter method
will be invoked on every request. It will let the request go on by calling chain.doFilter().
After the request processing finished and chain.doFilter() returns,
it retrieves the Session for the current thread from the ThreadLocal and closes
it. It also commits the transaction - if the transaction was already committed
or rolled back, HibernateUtil will simply do nothing.


Our Actions can invoke the static currentSession() method of the HibernateUtil
to get a Session - if it is invoked multiple times during one thread, the same
session will be used every time.


The HibernateUtil.rollbackTransaction() method can be invoked by our
Actions to have the transaction rolled back.


Also in the destroy() method of the filter, we once again do our hsqldb-housekeeping
work.


We have to modify the classpath in our compile target to get this to work:


from build.xml:



<target name="compile" depends="copy-resources">
<javac srcdir="${sourcedir}"
destdir="${targetdir}"
debug="on"

>

<classpath>
<fileset dir="${librarydir}">
<include name="*.jar"/>
</fileset>

<fileset dir="${tomcatdir}/common/lib">
<include name="*.jar"/>
</fileset>
</classpath>
</javac>

</target>

Finally, we have to configure the filter in the web.xml:


config/web.xml:



<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
<display-name>Eventmanager</display-name>

<filter>
<filter-name>sessionmanager</filter-name>
<filter-class>de.gloegl.road2hibernate.util.SessionManager</filter-class>
</filter>

<filter-mapping>
<filter-name>sessionmanager</filter-name>
<servlet-name>action</servlet-name>
</filter-mapping>

<servlet>
<servlet-name>action</servlet-name>
<servlet-class>webwork.dispatcher.ServletDispatcher</servlet-class>
<load-on-startup>1</load-on-startup>

</servlet>

<servlet>
<servlet-name>velocity</servlet-name>
<servlet-class>webwork.view.velocity.WebWorkVelocityServlet</servlet-class>

</servlet>

<servlet-mapping>
<servlet-name>action</servlet-name>
<url-pattern>*.action</url-pattern>

</servlet-mapping>

<servlet-mapping>
<servlet-name>velocity</servlet-name>
<url-pattern>*.vm</url-pattern>

</servlet-mapping>

<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
</web-app>



Accessing Hibernate from the action


So now we have set up our supporting framework, we can finally start to use
Hibernate for loading data in our action:


src/de/gloegl/road2hibernate/EventList.java:



package de.gloegl.road2hibernate.actions;

import java.util.List;
import com.opensymphony.xwork.ActionSupport;
import org.hibernate.Session;
import org.hibernate.HibernateException;
import de.gloegl.road2hibernate.util.HibernateUtil;

public class EventList extends ActionSupport {
private List events;

public List getEvents() {
return events;
}

public String execute() {
try {
Session s = HibernateUtil.currentSession();

events = s.createQuery("from Event").list();

return SUCCESS;
} catch (HibernateException e) {
e.printStackTrace();
return ERROR;
}
}
}

We simply retrieve the session from our HibernateUtil, and load the list of
Events using session.find(). Then we assign it to an attribute of our
action. Now we can use this attribute from our view:


views/eventlist.vm:



<html>

<head>
<title>Event List</title>
</head>
<body>
<h1>Events</h1>
<ul>

#foreach( $event in $events )
<li>$event.title</li>
#end
</ul>
</body>
</html>

Using the velocity template language, we simply iterate over the events, and
print their titles. Now that our infrastructure is in place, you see how easy
it is to create actions and views. If you now call http://localhost:8080/eventmanager/eventlist.action,
you will see most likely nothing - because we have no Events in the database.
So we will create another action to create events.


But at first we will finally create an index.html file, where we link our actions:


static-web/index.html:



<html>
<head>
<title>Hibernate Event Manager</title>
</head>
<body>


<h2>Hibernate Event Manager</h2>
<ul>
<li>
<a href="eventlist.action">Event Listing</a>

</li>
<li>
<a href="newevent!enter.action">New Event</a>
</li>
</ul>


</body>
</html>

Next, we will create the NewEvent action:


src/de/gloegl/road2hibernate/NewEvent.java:



package de.gloegl.road2hibernate.actions;

import java.util.List;

import com.opensymphony.xwork.ActionSupport;

import org.hibernate.Session;
import org.hibernate.HibernateException;

import de.gloegl.road2hibernate.util.HibernateUtil;
import de.gloegl.road2hibernate.data.Event;

public class NewEvent extends ActionSupport {
private String title;

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public String execute() {
try {
Session s = HibernateUtil.currentSession();

Event event = new Event();
event.setTitle(title);

s.save(event);
s.flush();

return SUCCESS;
} catch (HibernateException e) {
e.printStackTrace();
return ERROR;
}
}

public String enter() {
return INPUT;
}

}

As you see, this action has two doXXX() methods. The reason is we
will use WebWorks ability to invoke different methods of the action class. Lets
have a look at the xwork.xml first:


src/views.properties:




<!DOCTYPE xwork PUBLIC "-//OpenSymphony Group//XWork 1.0//EN"
"http://www.opensymphony.com/xwork/xwork-1.0.dtd">

<xwork>
<include file="webwork-default.xml" />

<package name="default" extends="webwork-default">

<default-interceptor-ref name="defaultStack" />

<action name="eventlist" class="de.gloegl.road2hibernate.actions.EventList">
<result name="success" type="dispatcher">/WEB-INF/views/eventlist.vm</result>
</action>

<action name="newevent" class="de.gloegl.road2hibernate.actions.NewEvent">
<result name="success" type="dispatcher">index.html</result>
<result name="input" type="dispatcher">/WEB-INF/views/newEventForm.vm</result>
</action>

</package>
</xwork>

So we have now a new action called newevent. This action has two results, the
success-result (which is returned by the execute() method) and the input-result
(which is returned by the doEnter() method). If you look at the index.html you
will notice the link to "newevent!enter.action" - this tells WebWork
to invoke the enter() method in the action. The submit-target of our form however
will just link to "newevent.action", invoking the default execute()
method.


Finally we need to code the view file we specified.


views/newEventForm.vm:



<html>

<head>
<title>Event List</title>
</head>
<body>
<h1>New Event</h1>
<form method="POST" action="newevent.action">

<table>
<tr>
<td>Title:</td>
<td><input type="text" name="title" value="$!title"/></td>

</tr>
<tr>
<td colspan="2"><input type="submit"/></td>
</tr>

</table>
</form>
</body>
</html>

If you now go to http://localhost:8080/eventmanager
now, you should get an index page from where you can create new Events and list
them.


So far for this chapter, in the upcoming next chapter we will examine the power
of HQL more deeply.



Code download


You can download the part four development directory here

No comments: