Building Documents in ACS

part of ACS Core Architecture, by Philip Greenspun and Jon Salz on 22 May 2000

This is an API for programmers writing scripts in the ArsDigita Community System that return documents (a degenerate form of which is a Web page) to Web clients.

The Big Picture

Standard AOLserver programming, like CGI scripting before it, had the programmer directly writing bytes to a client browser connection. Thus from 1995 through 2000 an AOLserver programmer would build up a complete HTML page in a script and write the bytes of that page to the client either all at once with ns_return or incrementally with ns_write.

Problems with this standard old approach:

As of ACS 3.3, new modules and scripts can be constructed to build documents rather than pages. A document is a data structure containing a body and a series of attached properties (e.g., title). Once a document is built by a script, the request processor takes over and renders the document into something which the client can understand, e.g., an HTML page.

How To Use It, In Twenty-Five Words Or Less

Using the Document API: An Example

The Pre-3.3 Way: Writing to the Connection

Consider the following hypothetical old-style Tcl page (call it user-list) that writes out a list of names of registered users:
ReturnHeaders "text/html"

ns_write "[ad_header "User List"]
<h2>User List</h2>

[ad_context_bar_ws [list "index" "Users"] "User List"]


set user_list ""

set selection [ns_db select $db "
    select first_names, last_name from users order by upper(last_name)
while { [ns_db getrow $db $selection] } {
    append user_list "<li>$first_names $last_name\n"
ns_db releasehandle

ns_write "$user_list</ul>

This is all well and good, but what if we decided we wanted to make that title appear in a sans-serif font on every page in the site, and have the context bar appear in a right-aligned table next the body, and eliminate the <hr>s as well? We couldn't, without manually fixing every single file (or having Jin write a Perl script).

The Enlightened Way: Building a Document

For reasons like this it's a good idea to break each page into a set of separate pieces which a master template can piece together however it sees fit. For HTML pages in general, we identify three pieces of information we can split up easily:

The document API allows us to output these pieces separately. user-list becomes:
doc_set_property title "User List"
doc_set_property navbar [list [list "index" "Users"] "User List"]

doc_body_append "<ul>\n"

set selection [ns_db select $db "
    select first_names, last_name from users order by upper(last_name)
while { [ns_db getrow $db $selection] } {
    doc_body_append "<li>$first_names $last_name\n"

doc_body_append "</ul>\n"

# we can release the db handle anywhere in the script now; nothing
# gets written to the client until we return (or unless 
# doc_body_flush is called)
ns_db releasehandle

None of this actually writes to the connection. It calls a few magical APIs, doc_set_mime_type, doc_set_property, and doc_body_append, which construct a data structure. This data structure is then passed by the request processor to an ADP master template (usually called master.adp), which might look like:
    <title><%= $title %></title>
  <body bgcolor=white>
    <h2><%= $title %></h2>
    <%= [eval ad_context_bar_ws $navbar] %>
    <%= $body %>
Note that to refer to the document properties (title, navbar) we just use the usual ADP syntax for reading variables, e.g., <%= $title %> to read the title property. The same goes for the document body, which is read with <%= $body %>. The request processor locates the appropriate master template for a page as follows: For example, if user-list is really /web/arsdigita/www/users/user-list, we'll check for a master template at This allows us to provide a default master template, but to override it for documents in specific parts of the site.

ADPs as Documents

You can also build a document in an ADP file, by enclosing the body in <ad-document> and </ad-document>, and using the <ad-property> tag to set properties. The following ADP file (user-list.adp) is equivalent to user-list.tcl above:
  <ad-property name=title>User List</ad-property>
  <ad-property name=navbar>[list [list "index" "Users"] "User List"]</ad-property>



set selection [ns_db select $db "
    select first_names, last_name from users order by upper(last_name)
while { [ns_db getrow $db $selection] } {
    ns_adp_puts "<li>$first_names $last_name"
ns_db releasehandle


Complete Tcl API

doc_set_mime_type mime-type
Sets the MIME type for the current document to mime-type. This defaults to text/html;content-pane (which means that we should try to apply a master template), so you probably won't need to change it in most cases. In the rare case that you do need to write a page which shouldn't be generated through the master template (e.g., a differently formatted HTML page, or a text/plain page), you'd use this procedure, e.g.,
doc_set_mime_type "text/html"
doc_set_mime_type "text/plain"
Then you'd use doc_body_append to generate your document, and the request processor would just serve your document as is.

doc_set_property name value
Sets the document-level property named name to value.

doc_body_append string
Appends string to the document body.

doc_body_flush string
Writes out as much as possible to the client. This is a dangerous API call because the programmer runs the risk of tying up a database handle if he or she is not thoughtful. Does nothing if the mime type has not been set. Does nothing if the document being produced must be rendered via a master template.

What's the Point?

Many more interesting and important things (which we'll add for ACS 4.0) fit into this framework: This API is part of a gradual move toward this model.

Under the Hood

The doc_* API calls store stuff in a global variable named doc_properties. After the abstract URL system sources a file, it checks to see if anything's been set in doc_properties. If so, it analyzes the MIME type in the document that's been returned, and invokes a template or returns the body, as appropriate. If not, it assumes the page did its own ns_write or ns_returning (and doesn't do anything special).