ACS 3.4 Java Design Document
by Bill Schneider
Supported Functionality: What's there?
ACS/Java 3.4 is is built on the same data model and provides many of
the same modules and functions. Use of the same data model
allows cross-compatibility with ACS 3.4/Tcl, so that functions not
provided in ACS/Java (e.g., a missing admin page) may be performed by
an ACS/Tcl install operating on the same database. The following
modules are supported in ACS/Java:
-
bboard
-
calendar
-
chat
-
general comments
-
general permissions
-
news
-
spam
-
user groups
-
and a sufficient portion of the ACS core (database API, some UI widgets,
user login management) for implementing the above modules
Location of Files and Deployment
Let $ACS_JAVA be the ACS/Java base directory; under Tomcat, this
is probably $TOMCAT_HOME/webapps/acs-java. The ACS/Java tree consists
of several different types of files:
-
JSP pages, which perform business logic and database access; located
in $ACS_JAVA/module, where module is some module
-
TPL files, which are also JSP pages with a different extension;
these are presentation templates, and located in $ACS_JAVA/templates/module.
The templates tree mirrors the root ACS/Java tree.
-
Configuration Files are in $ACS_JAVA/WEB-INF. This is the
standard path for configuration file deployment in J2EE applications. Files
of interest in this directory include web.xml, acs-parameters.xml,
and acs-parameters.dtd.
-
Java classes (both compiled and source code) are in $ACS_JAVA/WEB-INF/classes.
This directory includes both classes in package com.arsdigita.acs
and org.apache.turbine; Turbine is a GPLed product of the Java
Apache Project
-
A Tag library definition is found in $ACS_JAVA/tlds/acs-tags.tld.
-
Some docs are in $ACS_JAVA/doc.
The distribution of ACS/Java is currently set up for deployment under Tomcat
Release 3.1. Tomcat is available for free at http://jakarta.apache.org.
The ACS/Java tree should be checked out into the webapps directory.
The web.xml indicates some servlet classes to be loaded at
server start-up. The base URL of the acs-java webapp context is mapped
to the RequestProcessor servlet, so that
all HTTP requests
are filtered and processed by the RequestProcessor. The *.jsp
pattern is also explicitly mapped to the RequestProcessor, unmapping the
default mapping. TPL template files, which are really just JSPs in disguise,
are mapped directly to the JSP handler servlet.
The web.xml file contains a servlet initialization parameter
for locating the acs-parameters.xml file. This file contains the
application-level configuration information for your ACS web service and
component modules.
Third-party libraries
We use the following third-party libraries in the ACS/Java code:
-
Turbine, an open-source library from
the Java Apache project. We use Turbine for some widgets, the Log class,
and the wrapper to the JavaMail API. We may use more of it in the future.
- The
com.oroinc.text.perl
package, from the ORO subproject of Jakarta. This provides regular
expression matching and substitution methods.
-
The JavaMail and Activation APIs, used for sending mail and for MIME support,
both for e-mail and parsing multi-part forms (those with enctype="multipart/form-data").
-
Oracle JDBC driver (classes111.zip or classes12.zip)
-
Sun's Java XML parser (compiled xml.jar file included with Tomcat)
Important servlets and other low-level stuff
The Request Processor
All HTTP requests to a properly-configured ACS/Java server, with the
acs-java
context mapped to the root URL in
$TOMCAT_HOME/conf/server.xml
(or corresponding config file), go through the
RequestProcessor
servlet. The request processor handles:
-
Abstract URLs. A request for
http://path/to/page is mapped
to either page.jsp, page.html, depending on the actual
files available. We don't glob for page.*; this has not yet shown
to be serious limitation.
-
Filters. The RequestProcessor class has a static method,
registerFilter,
that registers a
Filter object on a URL pattern. The return code
from a filter indicates whether the request should continue processing
filters; continue without processing any more filters; or abort.
-
Registered Procedures. Registered procedures in ACS/Java are a special
case of filters; they perform some action on a request, write to the connection,
and abort the rest of the request processing. So there is no separate procedure
registration method. Also, in Java, you can also register procedures in
web.xml
as servlets, which might sometimes make more sense.
Initialization Parameters
The Parameters servlet runs on startup and loads the
acs-parameters.xml
file (or whatever file is listed as its initialization parameter in web.xml).
This is a structured XML file with sections for each module.
We elected to make a separate XML file for application-level configuration,
rather than listing all init parameters as servlet paramters in web.xml
because we can then better structure the parameters by module, and list
multiple values for a single parameter. We could have used a Java properties
file, but that format does not provide the desired level of data structuring.
Also, XML is a standard input-validation mechanism with DTDs.
Parameter values are obtained through static methods of the Parameters
class.
Scheduled Procedures
Procedures are scheduled with the Scheduler
class. First, your procedure should extend the ScheduledProcedure
class; then, call the appropriate scheduler method. Procedures can be
scheduled daily, weekly, or at some other fixed interval.
Logging
We handle logging using the org.apache.turbine.util.Log class.
It supports three different methods corresponding to different error levels:
note, warn, and error.
Sending e-mail to users
You can send e-mail to users with the classes in the org.apache.turbine.util.mail
package, which are wrappers to the JavaMail API. There is also a sendMail()
method in the com.arsdigita.acs.Utilities class, which mimics
the AOLserver ns_sendmail procedure.
The Database API
The Java/ACS database API wraps the JDBC API, i.e., the classes and methods
in the java.sql.* package, so that JSP pages should not have to
call these methods directly. The database API also includes a series of
JSP custom tags for performing queries in JSP pages and setting up datasources
with the query results for use in templates.
The heart of the database API is the com.arsdigita.acs.db.Database
class, which handles getting connections, performing queries and
updates, and transaction management. Methods to wrap updates and
queries on LOBs are also provided.
The DataSource interface and the
Selection class,
which implements the DataSource
interface, are
also part of the database API. These interfaces are wrappers to the ResultSet
interface, and hide some of the ugliness of ResultSet from page
authors. For example, raw dates (e.g., "SELECT sysdate FROM dual")
are converted into Java strings with the ANSI date (ResultSet.getString("sysdate")
would also print the time); and CLOB contents are transparently obtained
and returned as strings.
You should also use Selection as the base class for any
class that represents a row or set of rows from a specific database
table, to provide methods specific to that data type. For example,
com.arsdigita.acs.bboard.TopicsDataSource
represents rows from the BBOARD_TOPICS
table. The
Selection
class provides the basic functionality for
initialization, iteration over rows, and getting column values, while
the TopicsDataSource
subclass provides methods specific
to a bboard topic, like the authorization tests
adminAuthorize
and authorizeUser
.
Since we're working in an OOP language with encapsulation, polymorphism,
etc., why don't we put a stronger abstraction barrier between the database,
with a Java class corresponding with each table in our data model, instead
of using a generic datasource interface?
There are several reasons why. First, if you put a lot of thought into
building your data model in your RDBMS, why should you have to double that
effort by building it a second time in Java with a class for each table,
an accessor method for each column in each table, etc.?
Second, while the Java object system is great for structuring data,
it does not take performance into account. In any database-backed web service,
it is likely that some table columns will be accessed very frequently;
some less frequently; and some very infrequently. In a table like BBOARD,
there will be many pages that display the ONE_LINE summary column,
and MSG_ID primary key, but only a few that display the MESSAGE
body CLOB column. It doesn't make sense to request the message body unless
it's requested; and special-casing a SELECT to omit the message
body in a hypothetical Java Message class is effectively a tunnel
under the abstraction barrier anyway.
Third, if we had a rigid one-to-one mapping between database tables
and Java classes, it wouldn't make sense how to handle JOINs, which are
effectively new object types built on the fly. GROUP BY complicates things
further, since that's something you should only do when needed.
Fourth, SQL code within Java classes is ugly, because there are no multiline
string literals. In JSPs, we can build our own custom tags for defining
SQL statements that take up multiple lines.
Fifth, and finally, being close to the database so that page authors
have the full expressive power of SQL at their disposal is a great feature
of ACS/Tcl; and we want to keep it that way in ACS/Java.
Though it is good not to take all SQL out of the JSPs, it is
still a good idea to move re-usable logic that calls the database
(e.g., access tests, group and role membership tests) into Java
classes to enhance readability and maintainability. Some good examples are
the GeneralPermissions
class, and the isGroupMember and isGroupAdmin
methods in UserInfo class.
|
The JSP tag library and Templating
The ACS/Java toolkit includes a series of JSP custom tags for parsing user
input from URL and form variables; defining SQL statements and loading
datasources with results from queries; and calling templates. All
ACS/Java JSP tags start with an "ad:" prefix.
The generalized control flow of a page within ACS/Java is:
-
Parse and validate form/URL vars, and set up corresponding variables,
using the <ad:page_contract> tag
-
Get the current user id/information with <ad:get_user_info>
-
Define SQL queries, and set up datasources with <ad:db_query>,
<ad:db_1row>,
or <ad:db_0or1row>; or perform DML
-
Set up request attributes (request.setAttribute("name", value))
to pass other values to the template
-
Pass control to the corresponding template with <ad:use_template/>
-
Retrieve passed-in variables in template with <ad:template_contract>
-
Display output to the user, interpolating values from attributes and datasources
with <ad:loop>, <ad:scope>, and <ad:sub>
Getting URL/Form Variables with the ad:page_contract tag
Form and URL variables are processed with the ad:page_contract JSP tag.
This tag performs some type checking and validation. The page_contract
tag automagically declares local variables corresponding to each
declared form/URL variable in the vars parameter. The page_contract tag
also sets attributes in the pageContext object corresonding to each declared
form/URL variable.
The syntax for the ad:page_contract tag is:
<ad:page_contract [id="pc"] vars="var_name(:flags)...">
Available flags for each variable include:
-
integer -- the supplied variable must be an integer and
an error will be displayed if otherwise; the variable created
will be a java.lang.Integer.
-
optional -- the variable is optional; will be set to null
if not present.
-
multiple -- there is more than one variable with the given
name; the variable declared will be an array of the base type
-
array -- if the variable name supplied to page_contract
is "var", then all form variables var.key1, var.key2, etc. will be combined
into a single object of type com.arsdigita.acs.AdSet, named "var",
with keys key1, key2, ...; var.get("key1") will return the value of form
variable var.key1, and so forth.
-
date -- if the variable name supplied to page_contract
is "var", then the form variables var_month, var_year, and var_day are
combined to form a date. The resulting object is of type java.sql.Date.
-
file -- the variable named will be a String with the name
of the file that the client uploaded, on the client side. An additional
variable named (var)_tmpfile will be created with the path to
the contents of the uploaded file on the server.
If the id attribute is set as in the above example (id="pc"), you may call
pc.addComplaint("you must enter ...") within the page_contract tag body
for indicating problems with user input. When the page_contract tag is
closed, a message will be displayed to the user and the page will not continue
executing.
Warning: Because of a quirk of the JSP spec (Section 5.5 of the
JSP 1.1 Specification document), any variable that you set within
the body of a page_contract tag will revert back to its corresponding attribute
in the pageContext; so, if you want the new value to stick, you should
call pageContext.setAttribute(varname, value). For example:
<ad:page>
<ad:page_contract vars="v:optional"/>
<%
if (v == null) {
pageContext.setAttribute("v", "default_v_value");
}
%>
</ad:page_contract>
<%--
the translation of the closing tag includes the statement
v = pageContext.getAttribute("v");
so the following line should print out "default_v_value".
--%>
<%= v %>
Getting the currently-logged-in user
The current user is indicated by an attribute in the current HttpSession
object,
which is maintained by the servlet engine and identifies users with a browser
cookie. The Security class handles the management of the
session, and the creation/validation of persistent login cookies.
The <ad:get_user_info id="user" [redirect="true|false"] /> tag
determines the currently-logged-in user and sets up a UserInfo variable, perhaps redirecting to the registration page if the user is not
logged in. If the user is not logged in and the redirect is false,
then the variable is null.
Defining SQL statements and setting up datasources with the ad:query tag
Database queries are performed with the <ad:db_query>, <ad:db_1row>,
and <ad:db_0or1row> tags depending on whether the query is
for multiple rows, one row, or zero or one row respectively.
All of the database query tags instantiate objects of type Selection, or one of its subclasses, and make them immediately available both as local
variables in the JSP page, and as request attributes for use
in templates with the <ad:sub> and <ad:loop> tags.
All query tags allow you to specify the exact subtype of DataSource
to be created; by default, db_query creates an object of type
Selection, and db_1row/db_0or1row create an
object of type SingleSelection.
Each database query tag also creates a variable of type SqlStatement
that
can be referenced for setting bind variable values. The bind()
method
binds values to variables in the order in which they occur within the SQL
statement. You can also bind variables by name, rather than positionally.
Example:
<ad:db_query id="get_bboard_topics" obj="topics" type="com.arsdigita.acs.bboard.TopicsDataSource">
select * from bboard_topics
where group_id = ?
<%
if (topic_stub != null) {
%>
and topic like :topic_stub || '%'
<%
get_bboard_topics.bind("topic_stub", topic_stub);
}
get_bboard_topics.bind(group_id);
%>
</ad:db_query>
<ad:db_1row id="get_date" obj="date">
select sysdate from dual
</ad:db_1row>
<%--
at this point date is declared and is type SingleSelection;
topics is declared and is type TopicsDataSource, which is a
subclass of Selection with methods specific to operating on bboard
topics
--%>
<%= date.get("sysdate") %>
<ul>
<ad:loop ds="topics">
<li><ad:sub var="topic"/>
</ad:loop>
</ul>
SqlStatement objects can also be passed to the Database.dml()
and Database.plsql() methods for performing inserts/updates and
calling stored procedures/functions. Note that with the plsql()
method,
you can specify an output variable type, and the first bind variable in
your query. (You should not bind an input value to the output bind
variable.)
Templating and the template_contract tag
A JSP page may pass control to a corresponding template with the <ad:use_template/>
tag.
When invoked from a JSP page /path/to/page.jsp, the request will
be forwarded to the corresponding TPL template, /templates/path/to/page.tpl.
The TPL template should indicate the attributes it uses via the template_contract
tag. Usage of this tag is:
<ad:template_contract vars="var1[:flag1,flag2,...] var2[:flags]
..."/>
The "vars" parameter to the template_contract tag is very similar to
that for the page_contract tag; each declared variable becomes a scripting
variable available for display in the template. There are two major differences,
though:
-
The values come from request attributes set up by the main JSP page (either
explicitly, or implicitly through db_query, db_1row, etc.), not URL/form
variables;
-
Since request attributes can be structured data objects and not just strings,
the default apparent type of the created variable is an Object rather than
a String (note that the actual type of the object is still what it was
when it was created; you can manually type-cast to the actual type). This
allows non-String objects with toString() methods to be displayed without
specifing their actual type in the template. (e.g., Integer, com.arsdigita.acs.html.ContextBar,
etc.)
-
The flags for variables are different. Supported flags are:
-
class=package.subpackage.ClassName: makes the created variable
have a specific apparent type, if you happen to be calling methods on the
object directly
-
datasource: specifies the object is a DataSource; not necessary
unless you want to call methods on the datasource directly in escaped Java
code (e.g., ds.next(); ds.get("key")). This is really just syntactic sugar
for class=com.arsdigita.acs.DataSource.
-
string: syntactic sugar for class=String; specifies that
the created object will have an apparent type of java.lang.String Note:
This is only necessary if you are explicitly calling methods from escaped
Java code within your template (e.g., s.length(); s.charAt(i)). It is not
necessary for writing out strings (<%= s %>).
-
optional: same as in page_contract; allows variable to
be null if not present
-
multiple: same as in page_contract; apparent type of created
object will be an Object[], or a ClassName[] if the class=... flag is also
set
Most of the body of the template should be regular HTML, with interpolated
values from datasources or variables set up by template_contract.
Datasources can be iterated over with the <ad:loop> tag, and
individual columns from a row are interpolated into the HTML output with
the <ad:sub> tag. A one-row datasource can also be interpolated
with <ad:scope>, which also brings a datasource into scope
for <ad:sub> without iteration.
Note: The ad:sub tag will try to introspect a corresponding
method for the column name requested, before calling the generic get()
method
on the datasource. So <ad:sub var="property_name"/>
will
first try to output the value of the datasource's getPropertyName()
method,
or the result of get("property_name") if no such method exists.
Example:
<ad:template_contract vars="users:datasource daterow:datasource navbar"/>
<h2>Test</h2>
<%-- directly interpolate a single variable with JSP
syntax --%>
<%= navbar %>
<hr>
<%-- interpolate a one-row datasource using ad:scope --%>
<ad:scope sources="daterow">
Today's date is <ad:sub var="sysdate"/>.
</ad:scope>
<%-- now loop over a multi-row datasource --%>
<p>List of users:
<ul>
<ad:loop ds="users">
<li><ad:sub var="first_names"/> <ad:sub var="last_name"/>
(<ad:sub var="email"/>)
</ad:loop>
</ul>
Future Improvements
There are several areas that are open for future work.
-
The database API: A more general way to handle OUT and IN/OUT bind variables
for stored procedure and function calls is needed. Right now, we
only support the use of the first bind variable in a query as an OUT parameter
for PL/SQL calls.
-
Nested transactions are not supported.
-
Some components from core (e.g., site-wide search) are not implemented.
-
The templating system should be revised to support translation of standard
ACS 4.0 templates (aka "Karl's Templates") into JSPs, so we can use the
same templates in both ACS versions.
bschneid@arsdigita.com