First Steps with Jakarta Struts

Just over a year ago, I started to learn how to use the Jakarta Struts J2EE Web application framework. During this journey, I realised that I found some aspects of Struts confusing at first, and that many other developers might well suffer the same difficulties. This article is an attempt to address that problem.

Many good books on the framework are now available, and the online documentation is quite mature for an open-source project; in spite of this, I have often had to refer to a wide variety of resources in order to build a good understanding of Struts. And there are still some areas in which the documentation is sparse.

However, this article isn’t an attempt to provide a definitive source of documentation. Rather, it’s intended to allow Struts newcomers to hit the ground running, to get a feel for what the framework is like, and understand what it can and can’t do.

Of the Struts example applications I’ve seen, most have been either too trivial or too complex, the complex ones having lots of external dependencies that must be resolved before the example can even be compiled and run. In this tutorial, I’ll attempt to hit a spot between these two extremes. I’ve deliberately created an application that has the minimum of external dependencies, so you should be able to actually run the code without too much difficulty! Everything you’ll need is open-source and can be downloaded for free — one of the advantages of J2EE.

The example application that we’re going to build is an online discussion forum, which I’ve given the rather generic name of "Web Forum". I’ve chosen an application of this type because it should be familiar, and because you can create quite a lot of functionality without the application becoming too complex. Our Web Forum is fully functional and reasonably well-debugged; hopefully, it should be fun to learn how it all fits together.

It’s important to note that this code shouldn’t be considered an example of a production-quality J2EE system. In particular, the exception handling is extremely — and deliberately — simplistic. This isn’t a tutorial on persistence strategies, object-oriented design or Web application GUI design; it’s simply a vehicle for learning Struts. Undoubtedly, some aspects of what I’ve created here could be improved upon, but that’s not important right now. Let’s get started!

Ingredients

To start, let’s talk about the ingredients you’ll need to create the application. I developed the Web Forum using Oracle JDeveloper because I’m familiar with that IDE. You can use your Java tool of choice for writing the code.

Apart from the source code itself, here’s a list of the other elements you’ll need:

I also used the MySQL Control Centre to create the database and control the database server, although the MySQL command line can be used if that’s what you prefer. Note that, because this isn’t a MySQL tutorial, some familiarity with MySQL is assumed. As a minimum, you’ll need to know how to start and stop the database server, how to create a new database, and you’ll need the ability to run a provided SQL script to create tables.

Source Code Downloads

These are the files we’ll use through this tutorial.

To reduce the download sizes, I’ve split the Struts JAR files that go into WEB-INF/lib into a separate download. The pre-compiled WAR file is also available.

The Application

Let’s take a brief tour of the Web Forum’s functionality. I’d thought about some of this before I wrote a line of code, but other aspects became apparent as I was building the app. The fact that there are plenty of existing forums to serve as inspiration certainly helped!

Upon starting the application, the user is presented with a list of topics, and information about each topic, such as:

  • the date and time it was posted
  • the number of replies
  • the topic’s author

The 16 most recent topics are displayed, ordered from the newest to the oldest:

1471_topic

At this point, the user can click on a topic to view it, along with any follow-up replies:

1471_multitopics

The date and time of the post are displayed, as well as the authors’ names, the dates on which they registered with the system, and the number of posts they’ve made. The user can register if he or she is a new user, or log in if they’re an existing user. A user must be logged in before he or she can create a new topic or a new reply. The user can elect to log in automatically during registration, or at the Login screen:

1471_login

The auto-log in feature uses a cookie that expires after thirty days. During registration, the user must provide a unique user name, his or her real name (or screen alias) and a password. All of these fields are mandatory, apart from surname, and the user must confirm his or her chosen password by entering it twice:

1471_register

Upon successful registration, the user is taken back to the topics screen, on which he or she can create a new topic:

1471_newtopic

Both the subject and message fields are mandatory. Users are allowed to use the HTML <b>, <i> or <u> tags for basic text formatting (bold, italic or underline) within the message field. Once the user creates a new topic, he or she is taken back to the Topics screen.

The topic hyperlinks ensure that users can see which topics they’ve already read. The hyperlinks to topics that the user has already read are displayed using the browser’s visited link colour. However, each topic link contains an encoding of the number of replies that have been made to that topic. Therefore, new replies to a topic cause that topic’s hyperlink to be displayed using the browser’s unvisited colour. Thanks to Joel Spolsky for this brilliantly simple idea.

When viewing a topic, users can compose a new reply; again, the message field is mandatory and accepts basic formatting tags:

1471_newreply

The Persistence Layer

The persistence layer in the Web Forum application is very simple. There are two database tables: one for posts and another for users. The posts table stores topics and their replies. The users table stores the details of the registered users of the application. These tables are accessed using data access objects (DAOs), which are just Java objects that perform simple object-relational mapping. In other words, they take a Java object and persist its contents into a table, and vice-versa.

The DAOs have the appropriate SQL statements embedded into them, although this probably isn’t good practice: database administrators like such things to be externalised so they can be tuned. I have also created for all the DAOs an abstract superclass, which contains utility methods that obtain a JDBC connection from a data source and close database resources, etc.

This is the schema for the posts table:

1471_table1

The most interesting feature of this table is the ParentID column, which links replies back to their parent topic. The Subject column is null when a post is a reply and not a topic. I’m storing the number of replies to each topic in a ReplyCount column. Technically, this is redundant because it could be calculated, but I’m storing it because MySQL doesn’t currently support nested SQL SELECT statements.

The CreationDate column originally auto-updated, but I changed this because the date and time of the original topic were being updated to the current date and time whenever a reply was added to that topic.

Here’s the schema for the users table:

1471_table2

The MySQL SQL script that creates these tables is in src/sql/create_tables.sql. This script can be run from a query window in MySQL Control Centre. Note that it assumes the existence of a database named webforum. Also, because we’re not building the functionality to create new topics in this article, you’ll have to manually insert some test data so that you have some topics to display.

Business Object Layer

I didn’t have to do much thinking to come up with the business objects in the application: they’re pretty obvious. There are classes for individual posts and users, as well as collections of posts and users. The class model — excluding the Struts classes — is shown below:

1471_classmodel

Some of these classes, such as UserCookie, will be covered later in the series. As can be seen from the class model, a Posts class contains a collection of Post objects at runtime, and features methods for retrieving this collection and for adding a new post. The Post class itself has attributes that correspond to the columns in the posts table, and overloaded constructors that are invoked depending upon whether the post is a topic or a reply. Accessors and mutators (getters and setters) are not shown on the class model.

The Users class contains a collection of User objects at runtime, and features methods for retrieving this collection and for adding new users. In fact, I deprecated the getUsers method after I discovered that I’d coded it, but didn’t actually call it from anywhere! The User class has attributes that mirror the columns in the users table, as well as a convenient getDisplayName method that returns the user’s first names and surname with a space character in the middle.

Project Structure

The organisation of the source code folder tree is shown below:

1471_projectstructure

Java source code files go under src, and the Java package hierarchy is rooted at com.johntopley.webforum. The public_html folder corresponds to the root of the Web application.

I always put JSPs under pages because this allows them to be protected by the Web container using J2EE declarative security. Anything in public_html and its sub-folders really should be regarded as public.

I also like to separate the Struts configuration files into a config folder, although usually you’ll see them stored directly under WEB-INF.

The application entry point is public_html/index.jsp, which is declared as a welcome file in the web.xml Web application deployment descriptor. Let’s take a look at index.jsp:

<%@ taglib uri=http://jakarta.apache.org/struts/tags-logic  
   prefix="logic" %>  
<logic:redirect forward="ViewTopics" />

All this page does is transfer control to the Struts framework, because if we’re going to use a framework, we want to be using it as soon as possible. Struts ships with a number of JSP tag libraries (taglibs), and here we’re using the logic taglib. This handles the conditional generation of output text, looping over object collections and application flow management. In this case, the redirect tag performs an HTTP redirect to a Struts logical URL. More about that in a moment.

One thing to note about the Web Forum application is that we’re using the Servlet 2.3 specification syntax to reference the taglibs using URIs, rather than referring to TLD files in the web.xml file. This is documented in section 5.4.3 of The Struts User’s Guide.

The Heart of Struts

The heart of Struts is the config/struts-config.xml file. This file defines the flow of the application and tells Struts which classes to use for what. The Struts ActionServlet is a controller class that reads this file and receives all incoming requests for the Web application. The ActionServlet needs to be configured as a servlet in the web.xml file:

<servlet-name>action</servlet-name>  
 <servlet-class>org.apache.struts.action.ActionServlet</servlet-class>  
 <init-param>  
   <param-name>config</param-name>  
   <param-value>/WEB-INF/config/struts-config.xml</param-value>  
 </init-param>  
 .  
 .  
 .

The main part of the struts-config.xml file covered by this article is:

<struts-config>  
 <global-forwards>  
   <forward name="ViewTopics" path="/ViewTopics.do"/>  
 </global-forwards>  
 
 <action-mappings>  
   <action  
     path="/ViewTopics"  
     type="com.johntopley.webforum.controller.action.ViewTopicsAction"  
     scope="request">  
     <forward  
       name="Topics"  
       path="/WEB-INF/pages/topics.jsp"  
     />  
   </action>  
 </action-mappings>  
</struts-config>

Struts introduces a layer of indirection into Web applications because it uses logical URLs. This means that the address you see in the browser’s address bar does not correspond to the physical location of that resource on the Web server. This allows developers to move resources around easily without breaking things. The Struts name for the association of a logical name with a resource is an ActionForward, often just called a Forward. The Struts configuration file contains a global-forwards section that allows the configuration of Forwards that are available throughout a Struts application. These are effectively the application’s entry points.

Another key Struts concept is the Action class. Actions are simply Java servlets, so anything that a servlet can do, an Action class can do. Actions are used to process requests for specific URLs. Generally, they should act as a thin layer around the business objects layer, which does the real work.

Actions are referred to by ActionMappings, which, again, are logical URLs. The Struts convention is that ActionMappings end with .do. The web.xml file needs to be configured so that the Struts ActionServlet is used to process any URL matching the pattern *.do, as shown here:

<servlet-mapping>  
 <servlet-name>action</servlet-name>  
 <url-pattern>*.do</url-pattern>  
</servlet-mapping>
Flow Of Events

Let’s recap where we’ve got to so far. The index.jsp page redirects to the ViewTopics global ActionForward, and this passes control to the /ViewTopics Action. Struts knows which Action class to invoke because the type attribute in the ActionMapping gives the fully-qualified name of the associated Java class, which must inherit from org.apache.struts.action.Action, and must have an execute method with the following signature:

public ActionForward execute(ActionMapping mapping,  
                            ActionForm form,  
                            HttpServletRequest request,  
                            HttpServletResponse response)  
 throws Exception

The mapping parameter is an ActionMapping object that represents the ActionMapping that invoked this Action. The form parameter is used if an HTML form is associated with the request. The request and response parameters highlight the fact that an Action class is just a specialisation of a servlet.

The Web Forum application uses a BaseAction abstract class that inherits from the org.apache.struts.action.Action class mentioned earlier. The other Action classes within the application inherit from BaseAction, so it serves as an extension point at which functionality common to all Action classes can be added if required. This should be considered good practice.

Important: Action classes should not use instance variables, as they are not thread-safe. State should be shared by storing values in the request, session, or servlet context objects, depending on their scope.

This is the execute method in ViewTopicsAction.java:

public ActionForward execute(ActionMapping mapping,  
                            ActionForm form,  
                            HttpServletRequest request,  
                            HttpServletResponse response)  
 throws Exception  
{  
 request.setAttribute(KeyConstants.POST_LIST_KEY,  
   new PostsDAO().getTopics());  
 
 return mapping.findForward(ForwardConstants.TOPICS_PAGE);  
}

A new PostsDAO object is instantiated and its getTopics method is called. This method uses the following SQL statement to query the posts table:

SELECT p.PostID, p.Subject, p.ReplyCount, p.UserID, p.CreationDate  
FROM Posts p  
WHERE p.ParentID = 0  
ORDER BY p.CreationDate DESC

The SQL WHERE clause ensures that only topics are selected — not replies. The getTopics method returns an instance of the Posts class, i.e., an ordered collection of Post objects. This instance is stored in the HTTP request under the key referred to by the KeyConstants.POST_LIST_KEY constant. The JSP that displays the list of topics will use this Posts object stored in the request. Finally, the findForward method of the ActionMapping class is invoked. This takes, as a String parameter, the name of a Struts Forward to pass control to. The ForwardConstants class contains all of the Forward names used within the Web Forum.

As well as global ActionForwards, Struts also has local ActionForwards. These are simply Forwards whose scope is local to a single ActionMapping. In other words, they are not globally visible within the application.

Important: Struts gives precedence to local ActionForwards over global ActionForwards.

A local Forward is used within the /ViewTopics ActionMapping to hold a reference to the JSP that displays the list of topics:

<forward name="Topics" path="/WEB-INF/pages/topics.jsp"/>

We finally have a physical path to a page! topics.jsp is included with the source code downloads for this article (linked above), so you can see the list of topics.

Building the Application Using Apache Ant

If you’re not familiar with the Apache Ant build tool, spend a few moments reading Apache Ant Demystified, which should get you up to speed quickly. The build file for the Web Forum application shouldn’t look too unfamiliar, although it does use some more of Ant’s built-in tasks. Another key difference is that, because this application is rather more than a simple "Hello World" app, we have to deal with the classpath. The good news is that Ant makes this easy too!

To compile the application, we need to have the J2EE libraries on the classpath. Fortunately, the Apache Tomcat Web container already comes with these libraries, so the question becomes one of referencing. Rather than hard-code Tomcat’s installation folder into our build file, let’s set up an operating system environment variable called TOMCAT_HOME that refers to the root of the Tomcat installation folder, e.g. C:Program FilesApache Software FoundationTomcat 5.0. The procedure for doing this is exactly the same as the one we used to create the ANT_HOME and JAVA_HOME environment variables in Apache Ant Demystified.

With that done, we can add a couple of property declarations to the top of our build.xml file:

<project name="webforum" default="dist" basedir=".">   
 <description>  
   Build file for the Struts WebForum application.  
 </description>  
 
 <property environment="env" />  
 <property name="tomcat.dir" location="${env.TOMCAT_HOME}" />

The first property declaration simply says that we want to use the identifier env to refer to operating system environment variables. The second property sets the location of tomcat.dir to whatever env.TOMCAT_HOME is. Thus, from now on, when we want to refer to Tomcat’s installation folder, we can simply use ${tomcat.dir}.

Later, we set up a property named j2ee.lib.dir that points to the location at which Tomcat keeps its J2EE libraries:

<property name="j2ee.lib.dir" location="${tomcat.dir}/common/lib" />

We now need to deal with setting up the classpath. Ant provides several ways to do this; we’re going to set up a path structure which we’ll assign the unique ID of classpath:

<path id="classpath">   
 <pathelement path="${j2ee.lib.dir}/servlet-api.jar" />  
 <fileset dir="${web.lib.dir}">  
   <include name="**/*.jar" />  
 </fileset>  
</path>

The snippet above shows that there are two elements within this path structure. The first element is an individual reference to the servlet-api.jar file, which is referenced using the pathelement tag. The second element is Ant’s fileset tag, which in our case has been set up to include all the JAR files within the folder represented by the ${web.lib.dir} property. To clarify, we’re setting up a classpath that includes Tomcat’s servlet-api.jar file and all the JAR files within public_html/WEB-INF/lib.

In order to use this classpath, the javac task within our build file’s compile target contains a nested classpath tag, which has a refid attribute set to the name of the path structure that we defined above:

<javac srcdir="${src.dir}" destdir="${classes.dir}">   
 <classpath refid="classpath" />  
</javac>

That’s how we make sure that the Java compiler can find all the libraries it needs to compile our application. The javadocs target also makes use of this classpath, which we defined once and can then re-use anywhere within the build file.

The next task is to package the compiled code into a WAR file (Web ARchive) so that it can be run by Tomcat. We create a dist target that looks like this:

<target name="dist" depends="compile"   
           description="Create the binary distribution.">  
 
 <delete dir="${dist.dir}" />  
 <mkdir dir="${dist.dir}" />  
 
 <war basedir="${web.src.dir}"  
         destfile="${dist.warfile}"  
         compress="true"  
         webxml="${web.src.dir}/WEB-INF/web.xml"  
         excludes="**/web.xml">  
        <classes dir="${classes.dir}" />  
 </war>  
</target>

Note that this target depends on the compile target, which makes sure that we always compile the code before we try to create a distribution in which it’s included! After deleting and recreating the output directory, we use Ant’s war task to create the WAR file. The basedir attribute points to a property that represents the public_html folder — the root of our application. The destfile attribute simply specifies the name of the output WAR file that’s to be created. We can choose whether or not to compress the file using — you guessed it — the compress attribute; the webxml attribute tells the war task where our Web application deployment descriptor is located. The use of this last attribute automatically ensures that the web.xml file is included in the WAR file. Thus, we use the excludes attribute to tell Ant not to generate a warning because it thinks we’re trying to include web.xml twice. Finally, a nested classes element tells the war task where our compiled classes are located.

If you run the dist target and then open /dist/WebForum.war using WinZip (or download the pre-compiled version from this site), you can see that the layout within the archive precisely mirrors the source code tree mentioned earlier. The compiled code has been placed within WEB-INF/classes and the Struts and MySQL JDBC JAR files are within WEB-INF/lib.

Running The Application Using Apache Tomcat

These instructions describe how to configure the Apache Tomcat Web container to use a MySQL data source. They’ve been tested using Tomcat 5.0.28 and MySQL 4.0.18, but should work with Tomcat 4 and other versions of MySQL, too. Note that TOMCAT_HOME refers to the root of the Tomcat installation folder, e.g. C:Program FilesApache Software FoundationTomcat 5.0.

Copy the MySQL JDBC driver JAR file to TOMCAT_HOME/common/lib.
Edit the TOMCAT_HOME/conf/server.xml file so that it includes a context definition for the Web Forum application. The main elements of the server.xml file are shown below:

<Server>   
 <Service>  
   <Engine>  
     <Host>  
       :  
       <Context>  
         <!-- Existing context definitions. -->  
       </Context>  
       :  
       <!--  
        | Paste Web Forum context definition (shown below) here.  
        +-->  
     </Host>  
   </Engine>  
 </Service>  
</Server>

Add the following context definition for the Web Forum application to the appropriate place in the server.xml file, substituting your MySQL webforum database user name and password where the italics indicate:

<!--   
| Begin WebForum context definition.  
+-->  
<Context path="/WebForum" docBase="WebForum"  
   reloadable="true">  
 
 <Logger className="org.apache.catalina.logger.FileLogger"  
     prefix="localhost_WebForum." suffix=".txt" timestamp="true"/>  
 
 <Resource name="jdbc/WebForumDS" auth="Container"  
     type="javax.sql.DataSource"/>  
 
 <ResourceParams name="jdbc/WebForumDS">  
   <parameter>  
     <name>factory</name>  
     <value>org.apache.commons.dbcp.BasicDataSourceFactory</value>  
   </parameter>  
 
   <!--  
    | The JDBC connection URL for connecting to your MySQL DB.  
    | The autoReconnect=true argument to the URL makes sure that the  
    | MySQL JDBC Driver will automatically reconnect if mysqld closed  
    | the connection. mysqld by default closes idle connections after  
    | 8 hours.  
    +-->  
   <parameter>  
     <name>url</name>  
     <value>jdbc:mysql://localhost:3306/webforum?  
         autoReconnect=true</value>  
   </parameter>  
 
   <!--  
    | MySQL username and password for DB connections.  
    +-->  
   <parameter>  
     <name>username</name>  
     <value>username</value>  
   </parameter>  
   <parameter>  
     <name>password</name>  
     <value>password</value>  
   </parameter>  
 
   <!--  
     | Class name for MySQL JDBC driver.  
     +-->  
   <parameter>  
     <name>driverClassName</name>  
     <value>com.mysql.jdbc.Driver</value>  
   </parameter>  
 
   <!--  
    | Maximum number of DB connections in pool. Make sure you  
    | configure your mysqld max_connections large enough to handle  
    | all of your DB connections. Set to 0 for no limit.  
    +-->  
   <parameter>  
     <name>maxActive</name>  
     <value>100</value>  
   </parameter>  
 
   <!--  
    | Maximum number of idle DB connections to retain in pool.  
    | Set to 0 for no limit.  
    +-->  
   <parameter>  
     <name>maxIdle</name>  
     <value>30</value>  
   </parameter>  
 
   <!--  
    | Maximum time to wait for a DB connection to become available  
    | in ms, in this example 10 seconds. An exception is thrown if  
    | this timeout is exceeded.  
    | Set to -1 to wait indefinitely.  
    +-->  
   <parameter>  
     <name>maxWait</name>  
     <value>10000</value>  
   </parameter>  
 </ResourceParams>  
</Context>  
<!--  
| End WebForum context definition.  
+-->

Next, copy the dist/WebForum.war we created earlier to TOMCAT_HOME/webapps. This archive will be automatically expanded and the Web Forum deployed the next time Tomcat is started.

After starting the MySQL and Tomcat servers, the application can be accessed in a Web browser using the URL: http://localhost:8080/WebForum/

We’ve covered a lot of important ground with this instalment. Next time, we’ll take a look at how the Topics page works, and move on from there. See you then!

Win an Annual Membership to Learnable,

SitePoint's Learning Platform

No Reader comments

Comments on this post are closed.