Generating the Scaffolding
Ant task
Finally, we can build and test the application. Make sure you
have all the CMS requirements in your classpath. The following
build fragment is all you need to generate the scaffolding. Ours
usually have other targets that rebuild the database or copy and
compile files.
Make sure the generator doesn't complain that it can't find
a configuration. Your webapp property value should
point to the location of a webapp configured with the
WEB-INF directory containing all the elements just
covered. Remember, the deployments.xml file within
WEB-INF/config must have a matching servletPath value:
<target ...>
<taskdef name="generate" classname="com.posttool.dbo.generate.GeneratorTask" />
<generate webapp="${webapp}" scaffolding="${scaffolding}" />
... move generated classes to $webapp and compile ...
... build/update database ...
</target>
Generated: POJOs with DBO annotation
After successfully generating the scaffolding, examine the
directory src. Within the directory you should find properly nested files, Java
files matching the package configuration, and object names from the
entities configuration file. In our case, we have
com/gratefulpalateimports/*.java. Each should look pretty
much like any POJO with the addition of one type of annotation:
com.posttool.dbo.DBO.
The DBO annotation is really simple. It has two string attributes,
a field and table, and one boolean, id. The following code samples
are snippets from generated POJOs:
@DBO (table = "Vineyard")
public class Vineyard {
@DBO (id = true)
private Integer id;
@DBO (field = "name")
private String name;
@DBO (field = "press_id", table = "_PressVineyard")
private Collection<Press> PressVineyard = new ArrayList<Press>();
...
Generated: SQL create and alter
Corresponding SQL create statements are generated
as create.sql. IDs are auto-increment integers. The mapping
of field types is fairly straightforward. References with a
cardinality of "one" are included as a foreign key integer in the
table. Bidirectional joins are modeled as a table with two foreign
key columns, one to each table. You can see the relationship
between the Java class annotations and the table structure if you
open both the POJOs and the generated create
statements. If the database already exists, alter
statements will be generated as alter.sql. They are meant to
help with structural changes but must be hand-edited at this time
because the differencing engine is not complete.
The following fragment shows a small sample of the
generated create including the bidirectional join table. The order
field maintains a user-specified list order, managed by the
persistence mechanism:
CREATE TABLE Vineyard (
id INT(11) NOT NULL auto_increment,
name VARCHAR(128),
winemakers VARCHAR(128),
... more fields ...
PRIMARY KEY (id)
);
... more tables ...
CREATE TABLE _PressVineyard (
press_id INT(11) NOT NULL,
vineyard_id INT(11) NOT NULL,
_order INT(11),
FOREIGN KEY (press_id) REFERENCES Press (id),
FOREIGN KEY (vineyard_id) REFERENCES Vineyard (id),
PRIMARY KEY (press_id, vineyard_id)
);
... more join tables ...
If you are uninterested in the template formalities presented by
Current CMS, skip to "Where is the CMS?" now.
Generated: Page Data Models
Following is a sample of a TemplateView. This is a
class that intends to provide data for a template in a structure
that is highly coupled with the display of that information. The
display will be handled by the JSP template, which we will look at next. In this
case, a particular wine and all its collections are populated from the
database.
We believe a line should be drawn between the preparation
of the data and its display: The Java class is responsible for
preparing structures that can dereferenced without conditions, if
possible. For example, if the designer requires two columns or some
text, instead of requiring the template to perform a modulus
operation, we prefer to make two lists, column1 and
column2. These values are then put in a
HashMap with simple names ready to become request
attributes, dereferenced in the JSP page with EL:
public class WineView implements TemplateView {
private Map<String,Object> values;
private AbstractUser user;
private SitePage page;
public WineView(AbstractUser user, SitePage page) {
this.user = user;
this.page = page;
}
public boolean isComplete() { ... }
public String getMessage() { ... }
public Map<String, Object> getValues() {
if (user == null) {
return;
}
int wineId = Text.toInt(page.getParameterValue("wine"));
Wine wine = user.read(Wine.class, wineId);
user.fill(wine);
values = new HashMap<String, Object>();
values.put("wine", wine);
}
}
Generated: JSPs
The JSP page for this TemplateView as defined by the
templates.xml configuration file is /jsp/wine.jsp. As you
can see, it attempts to display the record retrieved by the
template view, as ${wine.name}. Each field type
is generated with its own special kind of dereferencing. The field
"description" was typed as "html" in entities.xml. This is
why the XML within that field is not escaped. Date fields produce
appropriate date formatting tags. The pt:img helps to
create and display sized, adjusted images.
Three standard includes come in every template: head, menu, and
footer. Some templates, like our example, reference "related"
includes. For each entity you have a related include. This makes it possible for a wine to list its vineyards as well as the press, with both using the same short view for these related entities:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<%@ page language="java" pageEncoding="UTF-8"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c"%>
<%@ taglib uri="/WEB-INF/str.tld" prefix="str"%>
<%@ taglib uri="/WEB-INF/dt.tld" prefix="dt"%>
<%@ taglib uri="/WEB-INF/pt.tld" prefix="pt"%>
<html>
<head>
<title>wine</title>
<meta http-equiv="keywords" content="" />
<base href="${baseUrl}" />
<link rel="stylesheet" type="text/css" href="css/common.css" />
</head>
<body>
<%@ include file="include/head.jspf" %>
<div id="wine">
<div class="item">
${wine.name}
${wine.color}
${wine.region}
${wine.price}
<c:out escapeXml="no" value="${wine.description}" />
<pt:img file="${wine.bottle}" />
<pt:img file="${wine.landscape}" />
<div class="related">
<c:set var="__vineyard" value="${wine.vineyardWine}" scope="request"/>
<jsp:include page="include/related/vineyard.jsp" flush="true"/>
<c:set var="__press" value="${wine.pressWine}" scope="request"/>
<jsp:include page="include/related/press.jsp" flush="true"/>
</div>
</div>
</div>
<%@ include file="include/menu.jspf" %>
<%@ include file="include/footer.jspf" %>
</body>
</html>
Where is the CMS?
The CMS itself is actually static code. It can be copied to the
Web application directory as cms. If you generate the
scaffolding and move it to the appropriate spots in your Web
application (or create Ant tasks to do this), the CMS will be
operational. The complete installation steps are:
- configure WEB-INF directory: web.xml,
deployments.xml, entities.xml, templates.xml
(optional), *.tld, lib/*.jar
- generate scaffolding via Ant task
- copy Java scaffolding to webapp/WEB-INF/classes
- copy cms directory to webapp/cms
- create the SQL database
You can download the application template source code (available
in the Resources section) to look at the
case application configuration, complete with the generated
scaffolding and a more elaborate Ant build. You will be able to
deploy it if you make an entry in the deployments configuration
file. Check out the project page for more information.
Current CMS API
Following is a high-level walkthrough of some of the APIs used for
Current CMS.
Persistence: com.posttool.dbo
Current CMS relies primarily on a simple persistence mechanism
for all database operations: com.posttool.dbo. The
persistence mechanism is broken into three static classes that read,
write, and count. Data selection is managed using an object-oriented
approach to building the select.
The following code sample manages a list of Log
instances. All logs will meet a condition like objectId==3
and objectName=="Car" and id != 15. The
ConditionGroup allows us to build this flat condition.
It can contain any ICondition. Most of the classes in
the package com.posttool.dbo.sql.clause implement this
interface, including ConditionGroup.
The static method SQLRead.list requires two
arguments: the type of class to return (which must have DBO
annotations); and an ICondition. This method returns
List<Log> logs matching the condition. The list
is updated with standard setter methods, and finally the list is
saved to persistence storage:
ConditionGroup matchingLogs = new ConditionGroup();
matchingLogs.add(new Equals("objectId", log.getObjectId()));
matchingLogs.addAnd(new Equals("objectName", log.getObjectName()));
matchingLogs.addAnd(new NotEq("id", log.getId()));
List<Log> logs = SQLRead.list(Log.class, matchingLogs);
for (Log l : logs) {
l.setStatusCode(status.toString());
l.setCompletionDate(new Date());
}
SQLWrite.save(logs);
In the following code sample, a Select statement is
created with a SimpleCondition. This simple condition
gives the programmer direct access to the text of the where phrase
in the final SQL statement. The code should be easily read as
something like: select all artists with last name beginning with
"L", starting at the tenth and showing up to 20, ordered by ascending
last name:
Select select = new Select();
select.setWhere(new Where(new SimpleCondition("lastName","LIKE", "\"L%\"")));
select.setTop(10);
select.setSize(20);
select.setOrder("lastName ASC");
List<Artist> artists = SQLRead.list(Artist.class, select);
The previous two examples do not take into consideration persistence of entity
references or bidirectional links. These references are
Collection fields in the POJOs and join tables in the
database. Operations on the join tables are triggered lazily by
specifying the collection in the context of the fill and save static methods. This example selects car #32,
selects its "options," removes option 4, and saves the change. In
this and all cases, I try to keep the persistence mechanism close
to SQL so that the programmer is fully aware of each
transaction:
Car car = SQLRead.byId(Car.class, 32);
SQLRead.fill(car, car.getOptions());
car.getOptions().remove(4);
SQLWrite.save(car, car.getOptions());
The Abstract User: com.posttool.dbouser
You may have noticed the line Wine wine =
user.read(Wine.class, id); in the code sample from the
scaffolding WineView implements TemplateView. The
user, an AbstractUser, is passed to the
TemplateView by the URLFilter. A set of
the AbstractUser methods moderate database access.
This access is based on simple rules that come mostly from four
tables added automatically by the scaffolding generator.
For most operations, instead of using the unmoderated access
presented by the static methods of SQLRead and
SQLWrite, we use an instance of the
AbstractUser. This ensures that all reading
transactions only show the user-appropriate records. In addition, it restricts
writing and update privileges. It also logs all activity. Current
CMS contains a Web-based interface for creating and assigning
privileges to users. This information is stored in an XML file
within the WEB-INF/config/users directory, as shown in Figure
4.

Figure 4. CMS interface for manipulating user permissions (click the image to enlarge)
The AbstractUser can read, save, publish,
unpublish, delete, and version records depending on their
permissions. Permissions are set by the creator of the user. In the
case study, the AbstractUser was called a Contributor. The
administrator Contributor is capable of creating another
Contributor. Once this second Contributor is created, it is
necessary for the administrator to assign permissions to each
entity. The permissions are yes, no, and request for the actions
listed above and shown in Figure 4. Fields and references can also be
masked. If the admin allows the new Contributor to create
Contributors, then a hierarchy can be built. This hierarchy allows
CMS users to send a chain of publication requestors. Each
Contributor can assign her permissions (or less) to other
Contributors she has created.
Four other classes are added to the generated
framework. These classes are required for the CMS and application
developer to maintain log files for transactions as well as version
status and assignment for all records. The classes (and tables) are,
by default: CMSLog, CMSMessage, CMSAssignment, and
CMSVersion. CMSLog records all transactions
that change the database (including everything but reading it).
CMSMessages are between users of the system as well as
automated notification of record updates.
CMSAssignment designates which users can edit which
records. CMSVersion lists revision history as well
as the publication status of every record. All of these tables are
moderated by using the data access method of the AbstractUser.
Many methods are related to using AbstractUsers and
accessing Log/Assignment/Version metadata on records. Here is
a list of the commonly used methods:
user.save(car);
user.save(car, car.getOptions());
user.publish(car);
user.unpublish(car);
Car carVersion = user.version(car);
user.delete(car); // by default, delete changes the version status
// to 'deleted', it does not delete the row
...
Assignment a = user.read(Assignment.class, 23);
List<T> collection = user.list(rootClass.getType(), select, VersionStatus.active);
List<LoggedObject> logged = LoggedObject.fill(collection);
...
List<Assignment> allAssignments = user.list(Assignment.class, condition);
AbstractAssignment.fillContributors(allAssignments);
...
others
The Site: com.posttool.site
In many cases, this content management is in place for
use producing a public Web site. To complete this
article, we will examine the classes related to the public site.
These classes are part of the package
com.posttool.site.
The URLFilter was mentioned earlier in the article.
The filter is set by web.xml to handle all requests with the
extension .html.
This filter tries to match the requested URL with
urls listed in the site map (pages.xml).
Each page element in the XML file is represented as an instance of
SitePage. The found page is cloned (or if no page is
found, the root is cloned). If any arguments are in the URL,
that information is added to the page clone. Often, without URL
arguments for use by the CMS interface, the page should contain all the information necessary to produce a response. This includes a view
(JSP template) and a model (the data that goes in that
template).
From the page, a Template is produced (configured in
templates.xml). The template indicates the model class that
implements TemplateView. The model class produces the
data for the template by examining the properties of the page and
making database queries. The results are placed in the request for
JSP access. Each template also indicates a JSP view file. The view
includes HTML code and expression language dereferencing,
formatting, and iteration of the data provided by the model
class.
A Tiny Tag Library: com.posttool.tag
We elected to write a few tag libs ourselves. Two key
tags are pt:drawString and pt:img. The
first, drawString, will create an image file from a text field,
based on a True Type font. We use this in almost every site we make
for headlines, artists' names, and so on. The pt:img is
useful for generating thumbnails and manipulating bitmaps in
various ways. Beside scaling, the image may turned to grayscale with
a color wash placed on top. This approach is useful for creating highlight
states for images from within JSPs. The other tags are concerned
with formatting text and dates.
Now What?
So much more detail and explanation could go into this article.
As I write about each part of the code, I think of dozens of
details that need to be documented as well as code improvements and
configuration interfaces. I hope some of you find this information useful,
and I hope those who do will have the opportunity to improve it.
Get in touch, and let's talk CMS!
Resources
David Karam is usually a programmer. Sometimes he is a designer, musician or teacher.