XML and Modern CGI Applications
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.
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;
}
|
|
| Post your comments |
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.
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.
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.
XML.com Copyright © 1998-2006 O'Reilly Media, Inc.