XML and Modern CGI Applications
by Kip Hampton
|
Pages: 1, 2
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.
Resources
- xml file here (kind of)
2001-12-21 18:03:51 M Hunter - More Errors
2001-12-23 13:13:51 M Hunter - Script Errors and No XSLT
2002-01-07 08:38:36 Kip Hampton - CGI::XMLApplication
2001-12-19 13:01:09 Jim Hobbs - versions and data for XSLGateway
2001-12-17 12:00:44 Mike Barry - versions and data for XSLGateway
2001-12-17 13:12:36 Mike Barry - versions and data for XSLGateway
2001-12-17 13:01:40 Mike Barry - Re: resource
2001-12-14 16:50:47 Kip Hampton - resource
2001-12-14 03:27:08 Henrik Binggl