XML and Modern CGI Applications
December 12, 2001
Introduction
Perl owes a fair amount of its widespread adoption to the Web. Early in the Web's history it was discovered that serving static HTML documents was simply not a very interactive environment, and, in fairly short order. the Common Gateway Interface (CGI) was introduced. Perl's raw power and forgiving nature made it a natural for this new environment, and it very quickly became the lingua franca of CGI scripting.
CGI is not without its weaknesses, and despite well-funded campaigns from a number
of
software vendors, CGI is still widely used and shows no signs of going away anytime
soon.
This month we will be looking at a module that offers a new take on CGI coding, Christian
Glahn's CGI::XMLApplication
.
Borrowing heavily from the
Model-View-Controller pattern, CGI::XMLApplication
provides a modular,
XML-based alternative to traditional CGI scripting. The typical
CGI::XMLApplication
project consists of three parts: a small executable
script that provides access to the application, a logic module that implements various
handler methods that are called in response to the current state of the application,
and one
or more XSLT stylesheets that are used, based on application-state, to transform the
data
returned by the module into something a browser can display to its user.
Example One -- A CGI XSLT Gateway
CGI::XMLApplication
presumes that the designers and developers involved in a
given project have chosen to use XSLT stylesheets to separate application logic from
presentation, and it seeks to make that separation as painless and straightforward
as
possible. Developers need only ensure that the setStylesheet
callback returns
the location of the XSLT stylesheet that is appropriate for the current application
state.
The transformation of the DOM tree which the application builds, the optional passing
of
XSLT parameters to the transformation engine, and the delivery of the transformed
content to
the browser (along with the appropriate HTTP headers) are completely invisible to
the user.
To underscore this separation, our first example will not be a Web application in the traditional (HTML forms-based data entry) sense, but rather a generic XSLT gateway that can be added to the server's cgi-bin to allow an entire directory tree of XML content to be transformed for requesting browsers, while making the transformation invisible to users as well as stylesheet and document authors.
The first step is to create the CGI script that connects the client request with
our
application. We want the XML documents being served to be easily navigable by URL,
and we
want to make creating hyperlinks between those documents straightforward. Thus we
will
create our CGI script without a file extension, so that it's just another node in a
URL path -- everything to the right of that node is interpreted in the context of
a virtual
document root that contains the XML content. In this case, we will name the CGI script
stylechooser
.
use strict; use lib '/path/to/secure/webapp/libs'; use XSLGateway; use CGI qw(:standard);my $q = CGI->new(); my %context = (); my $gateway_name = 'stylechooser';
After loading the appropriate modules and setting a few script-wide variables we
begin by
adding fields to the %context
hash that will be passed along to the class that
handles the bulk of the application's logic. For this application we pass through
only the
portion of the requested URL that is to the right of the path to the script itself
(the
REQUEST
entry), and an optional STYLE
key that contains the data
stored in the querystring parameter "style"
$context{REQUEST} = $q->url(-path => 1); $context{REQUEST} =~ s/^$gateway_name\/?//; $context{REQUEST} ||= 'index.xml'; $context{STYLE} = $q->param('style') if $q->param('style');
Finally we create an instance of the XSLGateway
logic class and process the
request by calling it's run
method, passing in the %context
hash
as the sole argument.
my $app = XSLGateway->new(); $app->run(%context);
That's all there is to the CGI script. Now we create the XSLGateway
module
that does most of the work:
package XSLGateway; use strict; use vars qw(@ISA); use CGI::XMLApplication; use XML::LibXML; @ISA = qw(CGI::XMLApplication);
As I mentioned in the introduction, CGI::XMLApplication
works via event
callbacks: a given method in the application class is executed based on the value
of a
particular input
field (usually the name of the button used to submit the
form). There are two other callbacks that must be implemented: the
selectStylesheet
and requestDOM
methods.
The selectStylesheet
method is expected to return the full filesystem path to
the relevant XSLT stylesheet. To keep things simple, we are presuming that the stylesheets
will only reside in a single directory. We are allowing some added flexibility by
providing
for alternate styles via the $context->{STYLE}
field (which, you will recall,
contains the data passed through by the "style" query param).
sub selectStylesheet { my $self = shift; my $context = shift; my $style = $context->{STYLE} || 'default'; my $style_path = '/opt/www/htdocs/stylesheets/'; return $style_path . $style . '.xsl'; }
Next we need to create the requestDOM
method, which is expected to return an
XML::LibXML
DOM representation of the XML document being transformed for
delivery. Since our gateway is only serving static files, we need only to parse the
appropriate document using XML::LibXML
and return the resulting tree.
sub requestDOM { my $self = shift; my $context = shift; my $xml_file = $context->{REQUEST} || 'index.xml'; my $doc_path = '/opt/www/htdocs/xmldocs/'; my $requested_doc = $doc_path . $xml_file; my $parser = XML::LibXML->new; my $doc = $parser->parse_file($requested_doc); return $doc; }
Now we make sure our CGI script is safely executable in the server's cgi-bin and
upload a
few XML documents and an XSLT stylesheet or two to the appropriate directories. And
we're
ready to go. A request to
http://localhost/cgi-bin/stylechooser/mydocs/somefile.xml
will select the
file mydocs/somefile.xml
from the /opt/www/htdocs/xmldocs/
directory, transform it with the stylesheet default.xsl
in the
/opt/www/htdocs/stylesheets/
folder and deliver the transformed data to the
client.
You can obviously extend this basic framework as you need. You might, for example,
allow
for some sort of lookup table in the stylechooser
CGI script that maps certain
files or directories to particular XSLT stylesheets, or you might set and read HTTP
cookies
that configure the users' preferred style to allow for personalized, skinnable Web
sites.
But for beginning basic XML/XSLT Web publishing with a minimum of hassle and setup,
it's
hard to beat the kind of simplicity that this tiny CGI application provides.
Example Two -- A Simple Shopping Cart
For our final example we will use CGI::XMLApplication
to create a simplified
version of the workhorse of demo Web applications, the shopping cart.
As with the previous example, the part of the application that's exposed via CGI-BIN
is
extremely small. All we do is initialize our CustomerOrder
application class
and call its run()
method. This time, though, we pass through the contents of
CGI.pm
's Vars
hashref as the PARAMS
field of the
%context
hash.
use strict; use CGI qw(:standard); use lib '/path/to/secure/webapp/libs'; use CustomerOrder; my $q = CGI->new(); my %context = (); $context{PARAMS} = $q->Vars; my $app = CustomerOrder->new(); $app->run(%context);
For this example we will assume that the product information for our order application is stored in a relational database. The product list is modest so we can get away with having three screens in our application: the main data entry screen where users enter the quantities for one or more products they wish to order, a confirmation screen that shows the contents of the cart and totals the cost of the items selected, and a thank you screen that indicates that the order has been processed. In the interest of simplicity we do not discuss necessities like a shipping and billing data entry screen.
package CustomerOrder; use strict; use vars qw(@ISA); use CGI::XMLApplication; use XML::LibXML::SAX::Builder; use XML::Generator::DBI; use DBI; @ISA = qw(CGI::XMLApplication);
After loading the necessary modules and declaring our inheritance from
CGI::XMLAplication
we begin by creating the various event callbacks that are
associated with the various states (or screens, if you prefer) of our application.
First we
must register those events by setting the registerEvents()
callback to return a
list of the handlers that our application will implement over and above the handlers
that
are called by default. In this case we will register the order_confirm
and
order_send
callbacks which set the SCREENSTYLE
field in the
%context
hash. Later we will use this property to define which of the three
XSLT stylesheet will be used to render the order data to the client.
Also in Perl and XML |
OSCON 2002 Perl and XML Review PDF Presentations Using AxPoint |
Note that events are mapped to the actual subroutines that implement them using the
convention event_<eventname>
; so, for example, the "order_confim" event
is implemented by the event_order_confim
subroutine. Also be aware that the
various events are selected by CGI::XMLApplication
based on its ability to find
a form parameter that has the same name as one of the registered events. To fire the
order_confirm
handler, a form widget in the previous screen must
contain a form field named "order_confirm" that submits a non-null value. In our example
we
have taken the easy route named each form's submit
buttons appropriately to
achieve the desired results.
# event registration and event callbacks sub registerEvents { return qw( order_confirm order_send ); } sub event_order_confirm { my ($self, $context) = @_; $context->{SCREENSTYLE} = 'order_confirm.xsl'; } sub event_order_send { my ($self, $context) = @_; $context->{SCREENSTYLE} = 'order_send.xsl'; }
The event_default
callback is executed if no other handlers were requested.
Here we use it only to set the SCREENSTYLE
field to an appropriate value.
sub event_default { my ($self, $context) = @_; $context->{SCREENSTYLE} = 'order_default.xsl'; }
The event_init
callback is called for every request and before any other
handlers are called. This makes it quite useful for initializing the parts of the
application that will be used by the other handlers. In this case we use it to return
the
initial DOM tree that contains the product information from the database using the
fetch_recordset()
method, storing that tree in the %context
hash
for later use.
sub event_init { my ($self, $context) = @_; $context->{DOMTREE} = $self->fetch_recordset(); }
With the state-handler methods complete we need to implement the required
selectStylesheet
and requestDOM
methods.
As in the first example, we will assume that all the application's stylesheets are
in the
same directory on the server; all we will do here is return that path with the value
from
$context->{SCREENSTYLE}
(that was set by the state handler), appended to the
end.
# app config and helpers sub selectStylesheet { my ($self, $context) = @_; my $style = $context->{SCREENSTYLE}; my $style_path = '/opt/www/htdocs/stylesheets/cart/'; return $style_path . $style; }
Before we look at the requestDOM
handler, let's step through our
fetch_recordset
helper method first to provide a better context.
Recall that we are selecting the information about the products in our cart from
a
relational database, but the data that we are passing to the XSLT processor must be
a DOM
tree. Rather than building the DOM programmatically from scratch we make life easier
by
using Matt Sergeant's fine XML::Generator::DBI
, which generates SAX events from
data returned from an SQL SELECT
executed via Tim Bunce's legendary
DBI
module. Creating the required DOM tree is a matter of setting an instance
of XML::LibXML::SAX::Builder
(which creates an XML::LibXML
DOM
tree from SAX events) as the handler and for that driver.
sub fetch_recordset { my $self = shift; my $sql = 'select id, name, price from products'; my $dbh = DBI->connect('dbi:Oracle:webclients', 'chico', 'swordfish') || die "database connection couldn't be initialized: $DBI::errstr \n"; my $builder = XML::LibXML::SAX::Builder->new(); my $gen = XML::Generator::DBI->new(Handler => $builder, dbh => $dbh, RootElement => 'document', QueryElement => 'productlist', RowElement => 'product'); my $dom = $gen->execute($sql) || die "Error Building DOM Tree\n"; return $dom; }
The fetch_recordset
method makes an otherwise complex task trivial; but the
DOM tree it returns contains only part of the information that we want to send to
the
client. We also need to capture the quantities entered from any previous submissions
within
this session, and we need to provide a running total of the products ordered.
sub requestDOM { my ($self, $context) = @_; my $root = $context->{DOMTREE}->getDocumentElement(); my $grand_total = '0';
To include the current order quantities as part of the larger document we will loop
over
the product elements and append <quantity>
and
<item-total>
child elements to each "row". The quantity values are
available from the $context->{PARAMS}
field that contains all the data that may
have been posted to the form. See the associated stylesheets in this month's
sample code for details about how the forms fields are created and how the data is
passed around.
foreach my $row ($root->findnodes('/document/productlist/product')) { my $id = $row->findvalue('id'); my $cost = $row->findvalue('price'); my $quantity = $context->{PARAMS}->{$id} || '0'; my $item_total = $quantity * $cost; $grand_total += $item_total; # add the order quantity and item totals to the tree. $row->appendTextChild('quantity', $quantity); $row->appendTextChild('item-total', $item_total); }
Finally we add a bit of additional meta information about the order by appending
an
<instance-info>
element to the root element with an
<order-total>
child element which contains the total cost of the
currently selected items.
$grand_total ||= '0.00'; my $info = XML::LibXML::Element->new('instance-info'); $info->appendTextChild('order-total', $grand_total); $root->appendChild($info); return $context->{DOMTREE}; } 1;
Careful readers will have noticed that our simplified cart does not actually do
anything with the submitted data during the order_send
event. The truth is that
deciding where the data goes is often the most site-specific part of any cart application.
The complete application, including stylesheets, is available in this month's
sample code.
Conclusions
I was initially skeptical of CGI::XMLApplication
. As a card-carrying AxKit user I've grown accustomed to its speedy mod_perl
foundation, and I've gotten quite comfortable generating my dynamic database-driven
XML
content using AxKit's eXtensible Server Pages implementation. The reality is, though,
that
the luxury of a dedicated XML publishing/application server like AxKit is beyond the
reach
and need of many developers. There is a large gap between the "just print it" of traditional
CGI scripts and the high-octane XML-centric goodness of tools like AxKit.
CGI::XMLApplication
fills that gap nicely.
CGI::XMLApplication
offers a clean, modular approach to CGI scripting that
encourages a clear division between content and presentation, and that alone makes
it worth
a look. Perhaps more importantly, though, I found that it just got out of my way while
handing enough of the low-level details to let me focus on the task at hand. And that's
a
sure sign of a good tool.