Writing pages for ACS Java 4.0

What ACS authors need to know to write a good ACS Java page

by Bill Schneider and Bryan Quinn
Crash Course in Java/JSP
Getting Started
Processing Form and URL variables
Interacting with the database
Templating
Logging
Request Processor
Getting user, session and request info
Packages and Parameters with ACSPackage
checking permissions
context bars
System-level Initialization Parameters

Overview

This document is intended for people who are already familiar with writing Tcl/ADP pages for AOLserver and the ArsDigita Community System, but are relatively new to developing web applications in Java. For detailed API level information, consult the API Documentation.

Crash Course in Java/JSP

Java Server Pages (JSPs) are the Java analog to ADPs and ASPs; they send literal HTML to the browser, and provide syntax for escaping into Java code and for interpolating the values of Java objects inside HTML blocks.

Just like AOLserver provides a series of ns_... APIs for getting and setting HTTP connection-level information in Tcl code, and writing output to the browser, JSPs and the Servlet API provide similar functionality for Java web applications through the interfaces in the javax.servlet package hierarchy. Sun provides a reference for servlet and JSP interfaces as part of the Java 2 Enterprise Edition (J2EE) API documentation at http://java.sun.com/j2ee/j2sdkee/techdocs/api/index.html.

There are some major differences between JSPs and Tcl/ADP pages, though:

All JSPs (whether part of ACS or not) declare several objects implicitly; the most important are:
Object Name Object Type
request javax.servlet.http.HttpServletRequest
response javax.servlet.http.HttpServletResponse
out javax.servlet.jsp.JspWriter
The out object is used implicitly every time you output HTML in a JSP. You can use JSPs in a since literal HTML within a JSP is translated to statements like this: out.println("<h4>hello world!</h4>");

For most connection-level operation you can do in AOLserver, there is a corresponding method in Java. Some examples:
Method Tcl/AOLserver Java/JSP
Sending a redirect. ns_returnredirect $return_url response.sendRedirect(return_url);
Returning content. ReturnHeaders 200 image/jpeg response.setStatus(200);
response.setContentType("image/jpeg");
Determining an IP Address set ip [ns_conn peeraddr] String ip = request.getRemoteAddr();

The request object also has an attribute list associated with it, which can be used for passing objects from one servlet or JSP page to another. This is done by calling request.setAttribute("name", object) and request.getAttribute("name"). The request object also has methods for retrieving URL/form variables and getting properties about the connection.

Getting started

Flow of a typical ACS Java page

Most ACS Java pages will follow a similar flow pattern, which is nearly identical to most ACS Tcl pages: Here is an example from the News application.
<%@ include file="/acs.jspi" %> <% ad_page_contract { View a news item. @author Jon Salz (jsalz@arsdigita.com) @author Christian Brechbuehler (christian@arsdigita.com) @creation-date 11 Aug 2000 @cvs-id writing-a-page.html,v 1.6 2001/03/22 01:11:26 deison Exp } { news_item_id:integer,notnull } -properties { body:onevalue release_date:onevalue title:onevalue context_bar:onevalue } db_1row news_item_select { select title, body, release_date from news_items where news_item_id = :news_item_id } ContextBar context_bar = new ContextBar(); context_bar.add(title.toString()); release_date = Utilities.toPrettyDate(release_date.toString()); ad_return_template %>
	
	Setup standard import statements.


	Start an ad_page_contract for page validation.

	Specify documentation.




	
	Identify incoming page variables.


	Identify outgoing page datasources for templating system.


	

	Retrieve one row from the database setting the declared
	property values to their values in the database.


	
	Create a context bar which the template is expecting.

	
	Set the release date.
	
	Invoke the templating system.

	

Under the hood

All ACS Java pages should be implemented as JSP pages, although they will be nearly all Java code. These JSPs should be thought of as servlets-on-the-fly, with the JSP engine handling re-compilation and reloading on a change to the JSP file. JSPs are often more convenient to work with than servlets, because you can almost always change the URL for a JSP page simply by renaming the JSP in the filesystem, while moving a servlet to a different URL is more servlet-engine specific and might require editing the web.xml deployment descriptor.

Each ACS Java page is implemented with two files: a JSP with an .ajp (ACS Java Page) file extension, that does business logic, database access, user input validation, etc.; and a corresponding ADP template, with an .adp extension, in the same template format used by ACS Tcl.

The .ajp extension on the logic page indicates that it is to be handled by the macro precompiler first, which generates a .generated.jsp JSP page and then executes the generated page.

The .adp templates are also handled by a precompiler that translates ATS templates into JSP code.

The skeleton of most ACS Java JSP pages will look like this:

<%@ include file="/acs.jspi" %>
<-- this imports several Java packages and ACS libraries for use in
the JSP page --%>

<% 

// we have escaped into Java code

ad_page_contract {
    this is a simple test page
} {
    {input_param1 ""} 
    input_param2:integer
} -properties {
    output1:multirow
    output2:onevalue
}

// ... do queries, DML, etc. here ...

String output1 = "hello world!";
db_multirow output2 { 
    select a, b, c 
      from table 
      where a > :input_param2
       and  b = :input_param1
}

// now output1 and output2 are both ready to be passed to the 
// template

ad_return_template

%>

Processing form and URL variables

Form and URL variables are processed with the ad_page_contract macro. This macro uses nearly the same syntax as the procedure with the same name in ACS Tcl; it performs some type checking and validation, and displays an error if a required parameter is missing or a parameter is the wrong type. The translation of the ad_page_contract macro declares local objects corresponding to each input form/URL variable for later use in the JSP page.

The syntax for the ad_page_contract macro is:

ad_page_contract { 
   ... comment ... 
} [{ 
    var1[:flags]
    var2[:flags] 
    ...
}] [-validate {
   .. validation code ...
}] [-errors { 
   .. error code ..
}] [-properties {
   .. datasources to export to template .. 
}]

Available flags for each input variable include:

You can issue additional complaints back to the user using the ad_complain macro within the -validate block. These complaints will all be displayed together to the user when the ad_page_contract macro is finished executing.

Here is a detailed example from acs-admin/apm/version-edit.ajp.

%@ include file="/acs.jspi" %> <%@ page import="com.arsdigita.acs.acsAdmin.*" %> <%@ page import="com.arsdigita.apm.*" %> <% ad_page_contract { Edit a package version @author Bryan Quinn (bquinn@arsdigita.com) @date 17 April 2000 @cvs-id version-edit-2.tcl,v 1.4 2000/10/13 16:55:32 bquinn Exp } { version_id:naturalnum,notnull version_name version_uri summary description {description_format ""} { owner_name:multiple} { owner_uri:multiple} vendor vendor_uri {release_date ""} { upgrade_p "0" } } -validate { version_changed_ck -requires {version_id version_name version_uri} { // The user has to update the URL if he changes the name. db_1row old_version_name_query { select version_name old_version_name, version_uri old_version_uri from apm_package_versions where version_id = :version_id } boolean version_changed_p = version_name.equals(old_version_name); if (version_changed_p && version_uri.equals(old_version_uri)) { ad_complain } } } -errors { version_changed_ck {You have changed the version number but not the version URL. When creating a package for a new version, you must select a new URL for the version.} } ... %>

Interacting with the database

A detailed reference of all of the available database macros can be found
here.

Performing database queries

Database queries are performed with the db_multirow, db_foreach, db_1row, and db_string, depending on whether the query is for multiple rows, one row, or one value. Also, you may use the db_sql macro to declare a SQL code fragment to include in another SQL statement, for building dynamic queries.

Bind variables are handled by the macro precompiler. Whenever ":foo" occurs within a SQL statement, the macro precompiler will bind the current value of the Java object foo into the designated position in the SQL statement. Named binds should always be used for in vars; ":[number]" and "?" are assumed to be out vars.

The database query macros generally declare objects for use in the JSP page. The db_1row and db_foreach macros declare an object corresponding to each column name returned by the query. They, along with db_multirow, also result in the declaration of a java.sql.ResultSet. The db_string macro just returns a string and doesn't declare any variables.

Example:

<%
ad_page_contract {
   simple example page to show db_multirow and db_1row in action
} {
   group_id:notnull
   {stub ""}
} -properties {
   topics:multirow
}

String s = [db_string getTwo "select 1 + 1 as two from dual"];
// s is "2"

SqlFragment sqlf = [db_sql clause "and topic_name like '%' || :stub || '%'"];
// interpolate this sqlf fragment later

db_multirow topics {
 select * from bboard_topics
 where group_id = :group_id
 $sqlf
}
// topics is now declared as a java.sql.ResultSet

db_1row get_date { 
 select sysdate from dual
}
// now get_date is a ResultSet, with the cursor pointing at the first
// row; and sysdate is a java.lang.Object with the current date as a string.

// now display output: we're going to be bad in this example and 
// display directly in the JSP rather than using the template.
%>

<%= get_date.getString("sysdate") %> is the same as <%= sysdate %>.
<ul>
<% while (topics.next()) { %>
 <li><%= topics.getString("topic") %>
<% } %>
</ul>

Doing DML

The db_dml macro is used to performing inserts, updates, etc. It is nearly identical to its Tcl counterpart. Example:
Java and Tcl
db_dml update_password {
 update users 
 set password = :password
 where user_id = :user_id
}

There is an analogous db_exec_plsql method as well, for calling stored functions and procedures. This is also nearly equivalent to its Tcl counterpart:
Java and Tcl
// procedure call
db_exec_plsql run_proc { 
   begin
      run_stored_proc(:param1, :param2);
   end;
}

// call stored function with return value
Object item_id =
    [db_exec_plsql create_item {
        begin
            :1 := content_item.new(
                     name => :name,
                     creation_ip => :creation_ip
                  );
        end;
    }];

ACS/Java also includes a db_call_plsql macro as a shorthand for calling stored functions, eliminating the need for the BEGIN...END block:
Java only
// procedure call
// call stored function with return value
Object item_id =
    [db_call_plsql create_item {
       content_item.new( name => :name,
                         creation_ip => :creation_ip
                       )
    }];

Handling LOBs

LOBs with Oracle require special handling. On queries, the templating system will handle CLOBs transparently; @ds.clob_column@ will display the contents of clob_column in a datasource whether it is a CLOB, CHAR, or VARCHAR.

BLOB content may be retrieved with the static getBLOB() method in com.arsdigita.db.OracleUtils; the dumpBLOB method in the same class may be used to write the BLOB content directly to an output stream.

To perform DML operations with a CLOB or BLOB, you can use the -blob_file switch to db_dml. There are three important differences between this and the analogous operation in Tcl:

Example:
Java Tcl equivalent
db_dml insert_portrait {
 update users 
  set portrait_upload_date = sysdate
      portrait = empty_clob()
  where user_id = :user_id
  returning portrait into ?
} -blob_file portrait_tmpfile

db_dml insert_portrait {
 update users 
  set portrait_upload_date = sysdate
      portrait = empty_clob()
  where user_id = :user_id
  returning portrait into :1
} -blobs [list $portrait_tmpfile]

Transactions

The database API supports transactions through the db_transaction macro. Nested transactions are supported

Example:
Java Tcl equivalent
db_transaction { 
 db_dml { 
  insert into greeble values('bork')
 } 
 db_dml {
  insert into greeble values('quaggle')
 }
 if (!Permissions.hasPermission(objectId, "admin")) {
  db_abort_transaction
 }
} 
db_transaction { 
 db_dml { 
  insert into greeble values('bork')
 } 
 db_dml {
  insert into greeble values('quaggle')
 } 
 if {![ad_permission_p $object_id admin]} {
  db_abort_transaction
 }
} 

Templating

It is generally a good idea to separate content from presentation. We do this in ACS Java 4.0 the same way we do it in ACS Tcl 4.0, by breaking up each page into two separate files: one that handles business logic and sets up datasource objects with database queries, or performs some DML on the database; and a template file that presents these datasources. In ACS Java, we use .ajp pages (which are preprocessed into .jsp pages) instead of .tcl pages; and we use the same .adp templates in both ACS and ACS Java.

To pass control to an .adp template, use the ad_return_template macro. In /path/to/foo.ajp, the ad_return_template macro will pass control to the template /path/to/foo.adp.

Note that AJP files are really just JSP files with a different extension, and the AJP extension indicates that the file is to be preprocessed and translated into a JSP by the MacroTranslator class before it is executed. Also, .adp templates are ultimately translated into JSP code as well, by the TemplateTranslator class.

How do I do ... in ACS Java?

Logging

You should log using the static methods of the com.arsdigita.acs.Log class.
Tcl/AOLserverJava/JSP
ns_log notice "Sending email..." Log.note("Sending email...");
ns_log warning "Warning: ..." Log.warn("Warning: ...");
ns_log error "Oracle down, major system failure ..." Log.error("Oracle down, major system failure ...");
ns_log debug Log.debug("just assigned new user session");

The Request Processor

ACS Java 4.0 includes a RequestProcessor servlet, which handles abstract URL resolution and resolving URLs into pages in packages or the global pageroot. All HTTP requests for URLs under ACS Java go through it first, since it is deployed at the root URL pattern "/".

The following steps take place within the RequestProcessor to serve a response to the end-user's browser:

Note that requests for abstract URLs that map to static content will go through the RequestProcessor twice: the first time, the abstract URL is resolved to a concrete package URL, and the request is forwarded to the handler for the concrete URL; but since the RequestProcessor is registered on the root URL "/", the RequestProcessor is called again, and must then dispatch the request to the static-content handler servlet explicitly by name ("default"). This is necessitated by the internals of the Servlet spec, specifically, because you can't call getNamedDispatcher with a URL parameter like you can with getRequestDispatcher.

Under the hood, a request for /path/page is handled like this:

Getting user, session, and request info

Generally, anything you use ad_conn for in Tcl is a method in either ACSSession, ACSRequest, or ACSPackage. Most of the routines for getting information about the currently logged-in user are available through the ACSSession class, and information about the URL they requested is available through the ACSRequest class.

The current ACSSession, ACSRequest, and ACSPackage objects are all in .ajp pages through fields of the acs object, declared in the acs.jspi file that all .ajp pages should include. The current ACSSession object is available through the acs.session object, or in Java library code through ACSSession.get(). The current ACSRequest is similarly available through acs.request and ACSRequest.get().
Tcl/AOLserverJava/JSP
ad_conn user_id acs.session.getUserId()
ad_conn session_id acs.session.getSessionId()
ad_secure_conn_p acs.session.isSecure()
ad_conn url acs.request.getUrl()
ad_conn extra_url acs.request.getExtraUrl()
ad_set_client_property -secure_p t $module $name $value acs.session.setClientProperty(true, module, name, value)
ad_get_client_property $name $value acs.session.getClientProperty(module, name)

Packages and Parameters with ACSPackage

You can get information about the requested package through the acs.pkg object, of type com.arsdigita.acs.ACSPackage. This supports methods for getting information about an installed package instance; package parameters that are available in Tcl through ad_parameter are available through methods of the ACSPackage class as well.

Information about the requested site node is available through ACSRequest instead.

Examples:
MethodTcl/AOLserverJava/JSP
Get current package key. ad_conn package_key acs.package.getPackageKey()
Get current package id. ad_conn package_id acs.package.getPackageId()
Get current site node id. ad_conn node_id acs.request.getSiteNode().getNodeId()
Get current node url. ad_conn node_url acs.request.getSiteNode().getNodeUrl()
Get a parameter value.
ad_parameter ParameterName "" DefaultValue
acs.package.getParameter("ParameterName", "DefaultValue")
Note there is a special ACSPackage for the kernel, available with ACS.getKernelPackage(). Also, getParameter will attempt to convert the parameter value into an int or boolean if a variable of that type is supplied as the default value.

Checking Permissions

The Permissions class has two static methods, hasPermission and requirePermission, that correspond to the Tcl procedures ad_permission_p and ad_require_permission. Both take an object ID, a role, and return whether the logged-in user has the specified role on the object.
public static boolean hasPermission(BigDecimal objectId,
					String privilige)
       throws SQLException;

public static boolean hasPermission(BigDecimal objectId, 
					BigDecimal userId,
					String privilege) 
	throws SQLException;

public static void requirePermission(BigDecimal objectId, 
					 String privilege) 
	throws SQLException, ACSException;

The requirePermission method terminates the execution of the calling page displays a message to the user when they do not have the specified permission. The user is redirected to the the registration page when not logged in.

Virtual URL handlers (VUHs)

VUHs allow a URL within an installed package to dispatch to files in different packages, or to provide the feel of a directory hierarchy where none physically exists. For example, a VUH in the the acs-core-docs package, mounted on the URL /doc, serves a file in the acs-templating package when /doc/acs-templating is requested.

In ACS Tcl, VUHs are just Tcl pages with a .vuh extension; VUHs are only served if there is no concrete matching file for a URL request and the different extension helps prevent a VUH from accidentally being served when it shouldn't be.

By placing a VUH at a particular URL, it will handle all incoming request URLs that start with their own URL, for which there is no matching concrete file. index.vuh is special because it will match any request in the directory. FFor example, /packages/foo/bar.vuh will handle a requests for /foo/bar/baz, but not for /foo/quux. But /packages/foo/index.vuh will handle any incoming URL that starts with /foo.

VUHs in ACS Java work identically to the way they work in ACS Tcl; VUHs in ACS Java are JSP pages, with an extension of .vuh.jsp (or .vuh.ajp). The acs.request.getExtraUrl() method is often useful when writing VUHs.

Context bars

To make a context bar in ACS Java, use the ContextBar class. Due to Java's lack of variable-length argument lists, each individual URL/name pair must be added with another procedure call.
Tcl/AOLserverJava/JSP
set foo [ad_context_bar [list "foo" "foo page"] "bar"]
ContextBar cb = new ContextBar();
cb.add("foo", "foo page").add("bar");
String foo = cb.toString();

Scheduled procedures

You can schedule procedures with the com.arsdigita.acs.Scheduler class and its static methods. Since you can't easily pass a naked method to execute as a parameter, you have to wrap your scheduled procedures in the ScheduledProcedure class, which implements java.lang.Runnable.

Procedures are scheduled by calling the schedule and scheduleOnce methods, in various forms. Example:
Tcl Java

proc sp {} {
  # do something!
} 

ns_schedule_proc 3600 sp 
ns_schedule_daily -once 15 45 sp
ns_schedule_weekly 4 10 30 sp
ScheduledProcedure sp = new ScheduledProcedure () { 
  public static void run() { 
    // do something!
  }
};

Scheduler s = Scheduler.getInstance();
// run sp every hour
s.schedule(sp, 3600);
// run sp only once today at 3:45 p.m.
s.scheduleDailyOnce(sp, 15, 45);
// run sp Thursday at 10:30 a.m.
s.scheduleWeekly(sp, java.util.Calendar.THURSDAY, 10, 30);

System-level Initialization Parameters

Most initialization parameters are stored in the database in ACS 4.0, but there are some system properties, like the database username and password, that can't be stored in the database. These properties are stored in a Java properties file, "acs.properties" by default. The format of this file is a series of name=value pairs; the properties filename is given as the "acs.properties.file" initialization parameter to the RequestProcessor servlet in the web.xml deployment descriptor, or as a system property.

These propreties can accessed through the static methods of the Parameters class.

Initialization: running code at server start-up

Sometimes a Java class that is part of a module may need to run some code at server start-up. This is done with static initializers in a class whose name ends in "Init", which will be executed when the class is loaded by the StartupLoader servlet:
Tcl Java
# /packages/foo/tcl/foo-init.tcl
# 
# run code here
public class FooInit {

  static { 
    // any code contained within the "static" block will 
    // be run the first time this class is loaded
  } 
}

bschneid@arsdigita.com