ACS Java 4.0 Templating System Design

by Bill Schneider

The big picture

ACS 4.0 uses the ArsDigita Templating System (ATS) (aka "ACS Templating," or "Karl's Templates") for displaying dynamic content to the user with dynamic values generated from the database or other server-side computations. In a templated ACS 4.0 page, a .tcl page sets up datasources and other values, specified in ad_page_contract; the template is then loaded with the ad_return_template procedure, which translates the template into Tcl, and displays the content to the user.

We want to use the same templates from ACS 4.0 in ACS Java 4.0. For those templates that do not include Tcl code, we can do this by translating ACS templates into JSP code, following similar steps as the ATS:

Under the hood

The DataSource API

For ACS Java 3.4, we developed a DataSource interface for representing both single- and multi-row results returned from database queries. The DataSource interface contains methods for iterating over result rows and for retrieving column values.

We also developed a series of custom JSP tags (ad:db_query, ad:db_1row, ad:db_0or1row) for setting up DataSource objects from database queries, and putting the created DataSource object in the request attributes.

We use the same DataSource objects and JSP tags in ACS Java 4.0 as we did in 3.4, although we will break the DataSource interface up into two interfaces: DataSource, which implements the basic methods to operate on a single set of name/value properties; and MultiDataSource, which extends DataSource and adds methods for iterating over multiple rows and seeking to different rows.

We will, however, need to revise the Selection class that implements MultiDataSource. The JDBC interface has no method for determining how many rows were returned from a query in advance, which is necessary when either the ":rowcount" field is requested from a template, or when the "grid" tag is used. We alos need the capability to seek to any row in the result rows for the grid tag, and so the same datasource can be used by more than one multiple tag.

So, rather than just wrapping a ResultSet, as it did previously, Selection will pull in all of the rows from the database. (Note: If we were guaranteed a JDBC 2.0 ResultSet that allowed both forwards and backwards scrolling cursors, we could use these methods to figure out the number of rows returned instead.)

Template-serving mechanism

In ACS Java 3.4, we broke up most of our JSP pages into two files, a .jsp file that handled business logic and database access, and set up datasources in the request attributes; and a .tpl template that handled display logic. The .tpl files are just .jsp files with a different extension, to bypass the RequestProcessor and prevent redundant filtering.

Each main .jsp page would pass control to the template with a JSP custom tag, ad:use_template. This would look at the the request URL, and forward the request to the corresponding template file. If the request was for the URL /path/to/page.jsp, the ad:use_template tag would forward to the URL /templates/path/to/page.tpl. The .tpl templates did not require any translation, although, for convenience, the ad:template_contract tag was provided for creating local variables from passed-in request attributes.

In ACS Java 4.0, we propose to translate ATS templates into the JSP code that can then be directly served by the underlying JSP engine. The following mechanism translates standard ATS templates into JSP code:

We use Java code within JSP files for performing business logic and database access. We could also use Java servlets for the same purpose; however, it is easier to dynamically change the site layout using JSP, because all you have to do to publish a new JSP page is put the JSP file where you want it located. The JSP engine will handle re-compiling and re-loading JSP code without a server restart; publishing a servlet requires editing the web.xml deployment descriptor to register a servlet on a URL, and restarting the server for the change to take effect.

Template transforms

We will translate ATS templates to JSP code with the following series of transforms:

Argument types in conditional expressions

Comparsisons in <if> tags might be interpreted either numerically or lexically, and so we must maintain consistency with the original Tcl-based ATS interpreter, regardless of the types of the actual arguments.  The ATS performs all equals/not-equals tests lexically, even on numeric arguments (<if 1 ne 1.0> is true); odd/even tests must be performed numerically; and inequalities (lt, le, gt, ge) are performed numerically on numeric arguments, and lexically otherwise.  In Tcl, the decision to perform a comparison numerically or lexically, and the comparison itself, is handled entirely by the expr command; performing numerical comparisons on numbers taken from template text in Java, though, requires explicitly detecting that a series of characters is a numeric value, and then performing an explicit type conversion.

An argument to <if> may be either a datasource or a literal ("3.14159", "failure").  Since literal values from a template file are Strings until converted to another type, and the DataSource.get() method returns a String, it is easiest to first convert single-value arguments to Strings first and then convert all the arguments to whatever type is needed for the comparison together.  Although this may result in some superfluous conversions, avoiding this conversion would require lots of run-time type checking and would produce much more complex Java code inside the translated template.

For eq/ne, we'll compare all arguments as Strings using the String.equals() method.

For odd and even, we'll coerce all arguments to int using Integer.parseInt(); e.g.:
if (Integer.parseInt(datasource.get("val")) % 2 == 0) {
    ...
}

For inequalities, we'll have to first detect if a String argument contains a number, and then compare it numerically.  We could further refine testing numbers to see if we have an integer vs. a floating point, but it makes for a simpler conditional expression to compare all numbers as floating-point.  First, we declare a utility function isNumber():

public static boolean isNumber(String s) {
    Perl5Util re = new Perl5Util();
    // a number can start with a minus sign, and is a series of
    // digits.  It may contain a decimal point and a series of more digits.
    // after that it might contain an optional exponent.
    return (re.match("/^-?[0-9]+(\.[0-9]+)?([Ee][\+-][0-9]+)?$/", s));
}
Then, we would generate a floating point for numeric comparison with Double.parseDouble, if applicable; otherwise, we use the String.compareTo() method for lexical comparisons.

For example:


<if @datasource.value1@ lt @datasource.value2@> ... body of if ... <if> 
becomes

if (Utilities.isNumber(datasource.get("value1")) && Utilities.isNumber(datasource.get("value2"))
? (Double.parseDouble(datasource.get("value1")) < Double.parseDouble(datasource.get("value2")))
: (datasource.get("value1").compareTo(datasource.get("value2")) < 0)) {
       ... body of if ...
}

Areas for Future Work


bschneid@arsdigita.com