kat Online Bookmark Manager - Development Guide

By Mitch Stuart
Copyright © 2005 FullSpan Software  -  Usage subject to license
Software Version: 1.0  -  Document Version: $Revision: 1.3 $, $Date: 2005/08/14 02:38:48 $

Introduction

kat is an online bookmark manager that enables you to organize internet links and other brief snippets of information in a hierarchy of categories.

This document is the Development Guide for kat. See the Overview for general information and a list of other available kat documentation.

Architecture Overview

kat is a Java-based J2EE web application. It has been tested on Tomcat (versions 4.1.30 and 5.0.28), although it should run on any J2EE-compliant servlet/JSP engine.

kat uses the following architectural and infrastructure elements:

Framework Separation

In working with Struts (or any web app framework), it is important to separate the web-specific layer(s) from the business logic for the following reasons: In my usage of Struts, I found two main areas where I had to work to achieve separation between Struts and the non-Struts-specific logic in the application: the Action class and the ActionForm class.

Action Separation

To process a user request in Struts (such as a hyperlink click or a form submission), you write a subclass of the Struts Action class.

Problem: When your execute method is invoked, it is passed several parameters, including the Struts-specific ActionMapping and ActionForm, and the Servlet-specific [Http]ServletRequest and [Http]ServletResponse. If you write your business logic in the Action class, you will be tied to Struts and to the Servlet environment.

Solution: Introduce a Service layer that is not tied to the Struts or Servlet infrastructure. Your Action code becomes a thin layer that does any Struts/Servlet specific things (e.g., parameter management), and then invokes your Service layer. To support the Service approach, you will need to introduce context interfaces for Application, Session, and Request. These interfaces will have one concrete implementation for the Servlet environment, and one for the standalone (non-Servlet) environment.

The Service layer is similar to using stateless session beans in the EJB environment, but without the unnecessary baggage of EJB.

ActionForm Separation

To hold HTML form parameters, you write a subclass of the Struts ActionForm class.

Problem: If you pass the ActionForm to your Service layer, you end up contaminating that layer with Struts and Servlet related dependencies.

Solution A: Use DynaActionForm. DynaActionForm is a subclass of ActionForm that uses a Map to hold property names and values (as opposed to the individual property member variables found in a typical ActionForm). DynaActionForm is a subclass of ActionForm, so it would seem to have the same problem as mentioned above. However, DynaActionForm also implements the DynaBean interface (from Apache Jakarta Commons BeanUtils). So, you can write your Service layer to use DynaBeans, and you will be decoupled from Struts/Servlet dependencies. You will still have a coupling to the Apache Jakarta Commons library, but that is much less of a problem: it does not dictate the environment (Servlet vs. standalone) in which your code runs. In fact, the Commons library offers many nice utilities, so you might want to include it in your project anyway.

Solution B: Use FormData objects. You can create your own "POJOs" (Plain Old Java Objects) that hold the form data; I call these "FormData" classes. Then embed an instance of your FormData class in the ActionForm class (i.e., containment instead of inheritance). Since your FormData class has no dependencies on Struts or Servlets, you can freely pass it down to your Service layer. This does introduce one additional layer of indirection/complexity. For example, you always have to write two classes now (ActionForm and FormData) instead of just one (ActionForm). But I think the tradeoff in separation of concerns is worth it.

For the kat application, I implemented Solution B, FormData objects. At first, Solution A (DynaActionForms) seemed clean to me. But there is one major drawback: the DynaActionForms are not self-documenting. An ActionForm/FormData combination is self-documenting because of the property getter/setters and the JavaDoc that is generated for them (plus whatever additional JavaDoc that you write). For DynaActionForms, there is no automatic knowledge of what properties are in the form: you must write this manually in JavaDoc or some other format.

Both Solutions A and B require an indirection in JSTL expressions:

Application Code Layers

In this section we will briefly describe the layers, packages, and classes that make up the kat web application Java code base. The purpose of this section is to provide enough context to understand the overall organization of the code. Further details of these layers will be described later in the document.

The following diagram gives an overview of the layers and packages:

Application Layers

The following table provides more detail on the layers and packages. The columns in the table are:

Package Class Comments
app App Anchor for application-wide configuration, resource bundles, etc.
IAppRequest
AppRequestBase
AppRequestStandalone
Representation of a single request.

IAppRequest is the interface.

AppRequestBase is an abstract base class.

AppRequestStandalone is a concrete class for standalone clients, i.e., it represents requests that are not hosted inside the Servlet container.  Examples of standalone clients are JUnit tests or command line utilities.
IAppSession
AppSessionBase
AppSessionStandalone
Representation of an application session.

Follows the same pattern as the request classes described above.
AppRequestStatus A readonly object (its only public methods are getXXX) that provides request and session status.  For example, it tells whether the user is logged in, their loginid and userid, their role, etc.

The AppServletFilter stores an instance of this object as a request attribute so that (e.g.) JSP pages can alter their display based on the request status.  For example, certain links should be displayed only if the user has the Admin role.
web AppServletContextListener Does application initialization in the contextInitialized method.
AppServletFilter Does access control checking, login / redirect handling, dispatching, error handling, and transaction management.
AppRequestWeb A concrete implementation of IAppRequest for Servlet container requests.  Extends AppRequestBase.
AppSessionWeb A concrete implementation of IAppSession for Servlet container sessions.  Extends AppSessionBase.
AppHttpReqAttribKeys
AppHttpReqParamKeys
AppHttpSessAttribKeys
Constants used to store and retrieve values from various scopes.

Not all values stored in these scopes require defined constants.  In particular, the Struts form names, and the properties in the forms (and corresponding HTML form parameters) have their own management and are not included with these constants.
web-access-control.xml A mapping between URL regex and the rolemask allowed to access that URL.
web.action Various Subclasses of Struts org.apache.struts.action.Action.
web.form Various Subclasses of Struts org.apache.struts.action.ActionForm.
web.jsptag Various Java code supporting our custom tags.
service Various There is one service class for each operation that a client can perform.
service-access-control.xml A mapping between service class name regex and the rolemask allowed to access that service.
service.form Various This is the non-Struts-specific form layer discussed in ActionForm Separation.
service.validate Various Validation utilities available to the service layer.
domain Various Domain and application specific objects.  The classes in this package are generally composites or utilities that interact with the domain.entity objects.  For example, for the entry tree listing, there is an Entry object in domain.entity, and an EntryTree object in domain.
domain.entity Various Core entities in the application domain.  Typically each of these map to a database table.
db Various Objects and utilities for managing database connection and transaction context.
db.dao Various The DAO object provides the glue to transfer the domain.entity objects to and from the database.
security AccessChecker Loads access control files (e.g., web-access-control.xml and service-access-control.xml).

Determines whether a request for a resource is allowed based on the rolemask in the access control file and the current user's role.
resource messages.properties, others Message and other resources.
exception ConcurrentModificationException
DataAccessException
DuplicateKeyException
LoginRequiredException
ServiceException
plus others as required
Exceptions.

Control Flow Sequence

The following diagram shows the high-level flow of control sequence from the web browser to Tomcat to Struts to our application code. The "AppLogic" box is a placeholder for the application logic that will be expanded in a later diagram. This example shows a typical case where some business logic is executed and then control is forwarded to a JSP to display the results. By "forward" we mean a server-side forward (not a redirect). In this example, imagine a user is viewing her account settings and clicks "Save".

High Level Control Sequence

The following diagram shows the lower-level application logic flow of control sequence. As stated above, this example envisions a user viewing her account preferences and clicking "Save". In this lower-level diagram, we see the application logic to load the user record, apply the user edits, and save the record.

Action Control Sequence

Validation

Validation is done at several levels.

JavaScript

JavaScript validation checks the contents of the form before submitting it to the server. It can make the app feel more responsive if the user gets immediate feedback after pressing the Submit button, instead of having to wait for the server to emit the error message.

Having said that, it is essential that JavaScript validation only be used in addition to server-side validation, not in place of it. We always have to protect the server against data that is posted from non-JavaScript-enabled browsers, or from unusual or malicious user agents.

Action

Many web apps do their validation at the Struts Action layer. We don't do this because we have a separate Service layer. The Service layer serves all client technologies, while the Action layer serves only the Struts web app client.

Although the bulk of our validation is not in the Action layer, we have some validation there. There are some types of "hard" errors that need to be trapped in the Action layer and reported gracefully. For example, say there is a web page parameter that determines how the Action behaves, and it needs to be parsed as an integer. If it is not a valid integer, the Action must raise this as an error. Basically, anything that prevents the Action from instantiating or executing a Service, or computing the ActionForward to return from the execute method, must be reported by the Action.

Service

The bulk of our validation is done in the Service layer. The validation falls into three broad categories:

Authentication

Our login page asks the user for a loginid and password. The password is hashed using a salt stored in the database along with the loginid. The hashed value of the password is compared with the hashed value stored in the database. This hashing is "one way". The passwords are not stored in plain text, and there is no way to recover the plain text of a password. In the case of a forgotten password, we have a "reset password" function that the user can request.

Authorization

Each user will have a set of one or more roles. Authorization (access control) is applied in the following layers: Web, Service, and Database.

Web Authorization

The web-access-control.xml file contains a list of URL regular expression patterns. Each pattern has a role mask associated with it. A role mask of -1 means no login is required. A positive role mask indicates that login is required, and contains the bits for the roles that are allowed to access the resource. A role mask of 0 blocks all access to the resource, i.e. it states that no roles are allowed to access it.

Checking web authorization is done in our ServletFilter, so it is done automatically for every request.

Strictly speaking, from an application point of view, there is not a lot of web authorization required. Requests will be checked at the service layer so there is not a strong requirement to check them at the web layer. However, there are a couple of reasons to do so. First, just the principle of protecting at the earliest possible point. Why allow a normal user to access an admin Action if that user will simply be rejected at the Service layer. Second, web access control allows us to protect static content such as images or HTML files, or in fact to protect entire directories or subsections of the application.

Service Authorization

The service-access-control.xml file contains a list of class name regular expression patterns. Each pattern has a role mask associated with it; these work in the same way as the role masks described in Web Authorization.

Checking service authorization is done in the abstract base class ServiceBase. The execute method invokes the checkAccessToService method as part of processing the service request. All concrete service implementations automatically inherit this checking.

Database Authorization

The Web and Service authorization are task-based: is the user with this role allowed to do this operation? Database authorization is lower level: it ensures that the task for which the user is authorized is only performed on the data set the user is authorized to work with.

For example, imagine a user editing a user profile. There might be a URL pattern containing editUserProfile.do that allows any standard or admin user to access it. And there might be a service class pattern containing MyAppEditUserProfile that allows any standard or admin user to access it.

But when we actually go to retrieve or store the user record, a standard user should only be able to work with his or her own user record, whereas an admin user should be able to work with any user record.

This can be achieved with a predicate in the SQL statement like this:

   user.id = :identityUserid or :isAdmin = 1

Data Access

Hibernate

kat uses the Hibernate object/relational persistence library. The usage of Hibernate is very simple, because the tables and mappings are simple. See the mapping files (src/java/com/fullspan/kat/domain/entity/*.hbm.xml) for details.

kat uses the Hibernate Interceptor to initialize and update standard entity fields, like the create and update timestamps. See: KatEntityInterceptor.

Hibernate automatically updates each object's sequential version number for optimistic locking. When an object is displayed for editing, we store that version number in a hidden field in the HTML form, which is submitted with the data that the user has edited. The version number is checked in each entity's DAO implementation when the object is being updated. For example, KatEntryDao.get when the forUpdate flag is true. If the object has been edited between the time it was retrieved for display and the time it is being saved, a KatConcurrentModificationException is thrown.

MySQL

kat was developed and tested with MySQL version 4.0.18 and 4.0.20. Some of the MySQL limitations discussed in this section have been improved in later versions. If you want to host the kat database on a different database vendor/brand, this section should give you a good idea about where changes will need to be made in the kat source code to support the new database.

The table creation and initialization scripts are in src/db.

The kat code needs to do extra application-level value checking because MySQL does not strictly enforce all constraints. For example, if you try to store NULL into a column that doesn't take NULL values, MySQL Server instead stores a default value, such as 0 for numeric columns or an empty string for text columns. See the MySQL constraints documentation for more details.

There are uniqueness constraints on some of the kat database columns. For example, values in the t_user.loginid and t_user.email must be unique. To detect uniqueness violations and display the appropriate error message to the user, the SQLException text is parsed in each entity DAO class. For example, see KatUserDao.getPossibleDuplicateKeyException.

The relationship between parent categories and child entries is modeled in t_entry.parent_catid (and in several other columns, described in detail later). MySQL cascading operations (for example, cascade delete to delete all child entries recursively when the parent category is deleted) cannot be nested more than 15 levels. Therefore, kat does not allow entries to be created any deeper than 15 levels of nesting.

Entry Tree Data Model

The only complex part of the kat data model is the modeling of the entry trees. For example, say we have the following entries:
 1)   Top
 2)      Computing
 3)         Java
 4)            Sun
 5)               J2EE
 6)               J2SE
 7)               Tutorial
 8)            XML
 9)               JDOM
10)               Xalan
11)               Xerces
12)         Python
13)            Python Cookbook
14)            wxPython
15)      News and Views
16)         Salon
17)         Slate
In this section, we will discuss how the relationships between these entries are modeled in the kata database (and the corresponding Java objects).

Tree Model Background

Terminology

Domain Objects
kat entries (categories and items) are stored in the t_entry database table. The KatEntryShrub and KatEntryTree domain objects represent the tree and shrub objects described above. Both KatEntryShrub and KatEntryTree also include the breadcrumbs for the entry, which are displayed in the user interface.

References
A lot has been written about representing trees in relational databases. Here are some references that I consulted:

After considering various options, I decided to use the "path notation" approach to represent the tree relationships (this is described in detail below).

Compared to parent/child modeling, path notation seems better because it is simpler and more efficient to answer queries like: get an entry and all its parents; and get an entry and all its children.

Compared to "edge notation", path notation seems easier to understand and maintain.

Tree Model Details

In the path notation model, each entry has a column with the "path" to that entry: the list of all of the entry's parent categories, in order. There are various ways to represent the path to an entry. Let's consider the following sample entries:

Name Level ID Parent Cat ID
Top08null
Computing1238
Java247923
Sun3862479

If we look at the Sun entry for example, there are several ways to represent its path:

kat uses the third option: fixed length path segments with delimiter. Because the path segments are fixed length, the delimiter is not strictly necessary. However, the delimiters make the SQL and other code a bit easier to write, and they make the paths more human readable for troubleshooting.

For entry IDs, we use an integer sequence, starting with 1. We use a fixed-length, 12-digit ID, therefore we have a maximum value of 999,999,999,999. This is one trillion (minus one) IDs, it is enough to handle the scale of the system we are building. If we add 1 million rows a day, it would take 1 million days (over 2,700 years) to max out the sequence. Note that the commas are used here for ease of display, they are not present in the acutal data in the database, so they don't count towards its length.

To store the entry path, we use a varchar(255) column. Limiting the path length to 255 characters increases the portability of our software, since this column length is typically supported and fully indexable by most relational database engines (such as MySQL).

If we take our 12-character ID, and we use a 1-character separator, we have a total of 13 characters. 255 divided by 13 is 19.6, so in theory we can support a maximum of 19 levels of hierarchy.

However, we use referential integrity to maintain the relationships:

In other words, when we delete a user, all of that user's entries will be deleted. When we delete a category, all of that category's child entries will be deleted. These deletes will cascade so that all entries under the top-most deleted entry will be recursively deleted.

As discussed earlier, there is a limitation in MySQL of 15 levels of nesting for the cascade of referential integrity. Therefore, although our data model is set up to handle 19 levels of nesting in theory, in practice we only support 15 levels.

Example Query

To see the power of path notation, let's look at an example query from KatEntry.hbm.xml:
   <query name="GetEntryShrub">
      select shrub
      from KatEntry self, KatEntry shrub, KatUser user
      where user.loginid = :ownerLoginid
      and self.ownerUserid = user.id
      and self.id = :entryId
      and (   (left(shrub.entryPath, length(self.entryPath)) = self.entryPath)
           or (left(self.parentPath, length(shrub.entryPath)) = shrub.entryPath) )
      and shrub.entryLevel &lt;= self.entryLevel + 1
      and ((shrub.ownerUserid = :identityUserid)
           or (:isAdmin = 1)
          or (shrub.computedVisibility = 1))
      order by shrub.parentPath, shrub.name
   </query>
The input bind variables are the ownerLoginid, the entryId of the entry for which the shrub is being fetched, the identityUserid of the currently logged in user, and the isAdmin flag indicating whether the logged in user is an administrator. This query returns the following entries, in order: Note that the child entries are intermixed between categories and items. The caller must examine the entry_type column and put the child entries in the proper list. This is done in KatEntryDao.getEntryShrub.

Notice the line:

         and shrub.entryLevel &lt;= self.entryLevel + 1
This ensures that we get only the direct children of the entry. If you compare the GetEntryShrub query to the GetEntryTree query, you will note that this condition is only present in the GetEntryShrub query. Also note that because the Hibernate queries are stored in XML files, reserved XML characters must be escaped. So instead of writing: shrub.entryLevel <= self.entryLevel + 1 we have to write: shrub.entryLevel &lt;= self.entryLevel + 1.

Known Issues

License and External Libraries

kat is released under the FullSpan license. kat uses the following FullSpan libraries, which are released under the same license: FSJUtil, JMailSend, and PropKeyConst.

In addition, kat uses several external libraries, which are used and distributed under license from their copyright holders:

Component Description License
Apache libraries kat uses several Apache libraries including: Struts, Commons Codec, and others. This product includes software developed by the Apache Software Foundation (http://www.apache.org/).

apache-license.txt
Hibernate Object/relational persistence library hibernate-license.txt
c3p0 Connection pooling library c3p0-license.txt
JavaMail and JavaBeans Activation Framework Email and Activation APIs mail-license.txt, activation-license.txt
JDBC 2.0 Optional Package API (formerly known as the JDBC 2.0 Standard Extension API) JDBC extensions jdbc2_0-stdext-license.txt
JDOM A Java library for XML: "We intend to provide a solution for using XML from Java that is as simple as Java itself." This product includes software developed by the JDOM Project (http://www.jdom.org/).

jdom-license.txt
JTA Java Transaction API jta-license.txt
JUnit Unit testing framework junit-license.html
log4j Logging framework log4j-license.txt
MySQL Connector/J JDBC driver for MySQL mysql-connector-java-license.txt

Note that Connector/J is distributed under the GPL, and kat is not distributed under the GPL. Normally, per the MySQL licensing terms, this would disallow the kat project from distributing the Connector/J library. However, because kat is distributed under the BSD license, distribution of Connector/J is allowed under MySQL's FLOSS License Exception.
Servlet Library Servlet and JSP API servlet-license.txt
Eclipse Icon image files This product includes software developed by the Eclipse Project (http://www.eclipse.org/).

License text: eclipse-license.html