# $Id: XMLServer.pm,v 1.19 2005/11/15 22:03:01 mjb47 Exp $ use strict; use warnings; use XML::LibXML; use XML::LibXSLT; package DBIx::XMLServer; our $VERSION = '0.02'; my $our_ns = 'http://boojum.org.uk/NS/XMLServer'; my $sql_ns = sub { my $node = shift; my $uri = shift || $our_ns; my $prefix; $prefix = $node->lookupNamespacePrefix($uri) and return $prefix; for($prefix = 'a'; $node->lookupNamespaceURI($prefix); ++$prefix) {} $node->setNamespace($uri, $prefix, 0); return $prefix; }; package DBIx::XMLServer::Field; use Carp; our $VERSION = sprintf '%d.%03d', (q$Revision: 1.19 $ =~ /(\d+)\.(\d+)/); sub new { my $proto = shift; my $class = ref($proto) || $proto; my $self = {}; $self->{XMLServer} = shift and ref $self->{XMLServer} and $self->{XMLServer}->isa('DBIx::XMLServer') or croak "No XMLServer object supplied"; $self->{node} = shift and ref $self->{node} and $self->{node}->isa('XML::LibXML::Element') or croak "No XML element node supplied"; $self->{node}->namespaceURI eq $our_ns and $self->{node}->localname eq 'field' or croak "The node is not an element"; my $type = $self->{node}->getAttribute('type') or croak " element has no `type' attribute"; $class = $self->{XMLServer}->{types}->{$type} or croak "Undefined field type: `$type'"; bless($self, $class); $self->init if $self->can('init'); return $self; } sub where { return '1'; } sub select { my $self = shift; my $expr = $self->{node}->getAttribute('expr') or die "A element has no `expr' attribute"; return $expr; } sub join { my $self = shift; return $self->{node}->getAttribute('join'); } sub value { my $self = shift; return shift @{shift()}; } sub result { my $self = shift; my $n = shift; my $value = $self->value(shift()); do { $value = $n->ownerDocument->createElementNS($our_ns, 'sql:null'); $value->setAttribute('type', $self->{node}->getAttribute('null') || 'empty'); } unless defined $value; do { my $x = $n->ownerDocument->createTextNode($value); $value = $x; } unless ref $value; my $attr = $self->{node}->getAttribute('attribute'); if($attr) { my $x = $n->ownerDocument->createElementNS($our_ns, 'sql:attribute'); $x->setAttribute('name', $attr); $x->appendChild($value); $value = $x; } $n->replaceNode($value); } 1; package DBIx::XMLServer::OrderSpec; our $VERSION = sprintf '%d.%03d', (q$Revision: 1.19 $ =~ /(\d+)\.(\d+)/); sub new { my $proto = shift; my $class = ref($proto) || $proto; my $self = {}; my ($xmlserver, $node, $dir) = @_; $self->{field} = new DBIx::XMLServer::Field($xmlserver, $node); $self->{dir} = $dir; bless($self, $class); return $self; } sub orderspec { my $self = shift; my $spec = $self->{field}->select; for ($self->{dir}) { defined $_ or last; /^ascending$/ && do { $spec .= ' ASC'; last; }; /^descending$/ && do { $spec .= ' DESC'; last; }; } return $spec; } 1; package DBIx::XMLServer::Request; use Carp; use Text::Balanced qw(extract_bracketed); our $VERSION = sprintf '%d.%03d', (q$Revision: 1.19 $ =~ /(\d+)\.(\d+)/); if ($ lt v5.7) { require Exporter; our @ISA = qw(Exporter); our @EXPORT = qw(IsNCNameStartChar IsNCNameChar); } # Look for an initial segment of the string which looks like an XPath # pattern sub get_xpath { my $text = shift; # Repeatedly skip XPath-like stuff and bracketed things while( (extract_bracketed($text, "[(\"'", '[-|@_./:[:alnum:][:space:]]*')) [0]) {}; # Skip any more XPath-like stuff $text =~ m'\G[-|@_./:[:alnum:][:space:]]*'g; return substr($text, 0, pos $text), substr($text, pos $text); } BEGIN { # This hack is because Perl 5.6.1 appears to be buggy and not # allow unicode character properties to be declared in a package # pther than main. our $property_package = $ lt v5.8 ? 'main' : 'DBIx::XMLServer::Request'; eval <]) | (?<=[<>!]=|\/\/|::) | ^) (?: $NCName\s+ $NCName\s+ )* | (?: (?<=[.\])"']) | [0-9]+(?:\.[0-9]+)?\s | \$$NCName(?::$NCName)?\s ) \s* $NCName\s+ (?: $NCName\s+ $NCName\s+ )* ) (?{XMLServer} = shift; $self->{template} = shift; } else { $self = { @_ }; }; ref $self->{XMLServer} and $self->{XMLServer}->isa('DBIx::XMLServer') or croak "No XMLServer object supplied"; $self->{template} or $self->{template} = $self->{XMLServer}->{template}; $self->{template}->isa('XML::LibXML::Element') or croak "Template is not a XML::LibXML::Element"; $self->{template}->localname eq 'template' && $self->{template}->namespaceURI eq $our_ns or croak "Template is not "; $self->{main_table} = $self->{template}->getAttribute('table') or croak "The element has no `table' attribute"; $self->{ns} = $self->{template}->getAttribute('default-namespace'); my $p = &$sql_ns($self->{template}); $self->{record} = $self->{template}->findnodes(".//$p:record/*[1]")->shift or croak "The element contains no element"; $self->{criteria} = []; $self->{page} = 0; $self->{pagesize} = $self->{XMLServer}->{maxpagesize} unless defined $self->{pagesize}; $self->{rowcount} = $self->{XMLServer}->{rowcount} unless defined $self->{rowcount}; bless($self, $class); return $self; } sub real_parse { my $self = shift; my $query = shift or croak "No query string supplied"; foreach(split /&/, $query) { # Un-URL-encode the string tr/+/ /; s/%([0-9A-Fa-f][0-9A-Fa-f])/chr(hex($1))/eg; # Split it into key and condition my ($key, $condition) = get_xpath($_); $key or return "Unrecognised condition: '$condition'"; for ($key) { /^fields$/ && do { $condition =~ s/^=// or return "Expected '=' after 'fields' but found '$condition'"; $self->{fields} = $condition; last; }; /^order$/ && do { $condition =~ s/^=// or return "Expected '=' after 'order' but found '$condition'"; $self->{order} = $condition; last; }; /^page$/ && do { # The page number $condition =~ /^=([1-9]\d*)$/ or return "Unrecognised page number: $condition"; $self->{page} = $1 - 1; last; }; /^pagesize$/ && do { # The page size $condition =~ /^=(\d+)$/ or return "Unrecognised page size: $condition"; $self->{pagesize} = $1; defined($self->{XMLServer}->{maxpagesize}) && $self->{XMLServer}->{maxpagesize} > 0 and ( ($1 > 0 && $1 <= $self->{XMLServer}->{maxpagesize}) or return "Invalid page size: Must be between 1 " . "and $self->{XMLServer}->{maxpagesize}"); last; }; /^format$/ && $self->{userformat} && do { $condition =~ s/^=// or return "Expected '=' after 'format' but found '$condition'"; my $root = $self->{XMLServer}->{doc}->documentElement; my $p = &$sql_ns($root); $self->{template} = $root->findnodes("/$p:spec/$p:template[@" . "id='$condition']")->shift or return "Invalid format. Must be one of " . join(', ', map("'" . $_->value . "'", $root->findnodes("/$p:spec/$p:template/@"."id"))) . "."; $self->{template}->localname eq 'template' && $self->{template}->namespaceURI eq $our_ns or croak "Template is not "; $self->{main_table} = $self->{template}->getAttribute('table') or croak "The element has no `table' attribute"; $self->{ns} = $self->{template}->getAttribute('default-namespace'); $p = &$sql_ns($self->{template}); $self->{record} = $self->{template}->findnodes(".//$p:record/*[1]") ->shift or croak "The element contains no element"; last; }; # Anything else we treat as a search criterion push @{$self->{criteria}}, [$key, $condition]; } } return undef; } sub do_criteria { my $self = shift; my $prefix = $self->{ns}; $prefix = &$sql_ns($self->{record}, $self->{ns}) if defined $self->{ns} && $self->{ns} ne '*'; my $p = &$sql_ns($self->{record}); foreach(@{$self->{criteria}}) { my $key = $_->[0]; # Fix up a default namespace $key = add_prefix($key, $prefix) if $prefix; # Find the field my @nodelist = $self->{record}->findnodes($key); my $node; if(@nodelist eq 1 && $nodelist[0]->isa('XML::LibXML::Attr')) { my $name = $nodelist[0]->nodeName; my $owner = $nodelist[0]->getOwnerElement; my $q = &$sql_ns($owner); $node = $owner->findnodes("$q:field[@"."attribute='$name']")->shift or return "Attribute '$key' isn't a field"; } else { my @nodes = $self->{record}->findnodes ($key . "//$p:field[not(@"."attribute)]") or return "Unknown field: '$key'"; @nodes eq 1 or return "Expression '$key' selects more than one field"; $node = shift @nodes; } $_->[0] = new DBIx::XMLServer::Field($self->{XMLServer}, $node); } return undef; } sub _prune { my $element = shift; if($element->getAttributeNS($our_ns, 'keepme')) { foreach my $child ($element->childNodes) { _prune($child) if $child->isa('XML::LibXML::Element'); } } else { $element->unbindNode unless ($element->namespaceURI || '') eq $our_ns # Hack to avoid pruning && $element->localname eq 'field' # attribute fields && $element->getAttribute('attribute'); } } sub build_output { my $self = shift; my $doc = shift; # Create the output structure my $new_template = $self->{template}->cloneNode(1); $doc->adoptNode($new_template); $doc->setDocumentElement($new_template); my $p = &$sql_ns($new_template); my $record = $new_template->findnodes(".//$p:record")->shift or croak "There is no element in the template"; $self->{newrecord} = $record->findnodes('*')->shift or croak "The element has no child element"; $self->{rowcount} = 'NONE' unless $new_template->findnodes(".//$p:meta[@ type='rows']")->size(); # Find the nodes to return if(defined $self->{fields}) { my $prefix = $self->{ns}; $prefix = &$sql_ns($self->{newrecord}, $self->{ns}) if defined $self->{ns} && $self->{ns} ne '*'; my ($r, $s) = get_xpath($self->{fields}); return "Unexpected text: '$s'" if $s; $r = add_prefix($r, $prefix) if $prefix; $self->{fields} = $r; } else { $self->{fields} = '.'; } my @nodeset = $self->{newrecord}->findnodes ("($self->{fields})/descendant-or-self::*"); @nodeset > 0 or return "No elements match expression $self->{fields}"; # Mark the subtree containing them $self->{newrecord}->setAttributeNS($our_ns, 'keepme', 1); foreach my $node (@nodeset) { until($node->isa('XML::LibXML::Element') && $node->getAttributeNS($our_ns, "keepme")) { $node->setAttributeNS($our_ns, "keepme", 1) if $node->isa('XML::LibXML::Element'); $node = $node->parentNode; } } # Find the nodes to order by if(defined $self->{order}) { my $prefix = $self->{ns}; $prefix = &$sql_ns($self->{newrecord}, $self->{ns}) if defined $self->{ns} && $self->{ns} ne '*'; my $order = $self->{order}; my @order; while ( $order ne '' ) { my ($xpath, $more) = get_xpath($order); $xpath = add_prefix($xpath, $prefix) if $prefix; $xpath =~ s/ +(ascending|descending) *$//; my $dir = $1; my @o = $self->{newrecord}->findnodes($xpath) or return "Invalid field in order clause: $xpath\n"; foreach (@o) { my @f = $_->findnodes( $_->nodeType == XML::LibXML::XML_ATTRIBUTE_NODE ? "../$p:field[\@attribute='".$_->nodeName."']" : ".//$p:field" ) or return "No non-static data matched by order clause: $xpath\n"; foreach (@f) { push @order, new DBIx::XMLServer::OrderSpec($self->{XMLServer}, $_, $dir); } } return "Unexpected order: '$order'" unless $more eq '' || $more =~ s/^,//; $order = $more; } $self->{order} = \@order; } # Prune away what we don't want to return _prune($self->{newrecord}); return undef; } sub build_fields { my $self = shift; my @fields; my $p = &$sql_ns($self->{newrecord}); foreach($self->{newrecord}->findnodes(".//$p:field")) { push @fields, new DBIx::XMLServer::Field($self->{XMLServer}, $_); } $self->{fields} = \@fields; return undef; } sub add_join { my ($self, $table) = @_; return unless $table; do { my $root = $self->{XMLServer}->{doc}->documentElement; my $p = &$sql_ns($root); my $tabledef = $root->find("/$p:spec/$p:table[@"."name='$table']")->shift or croak "Unknown table reference: $table"; my $jointo = $tabledef->getAttribute('jointo'); my $join = ''; do { $self->add_join($jointo); $join = uc $tabledef->getAttribute('join') || ''; $join .= ' JOIN '; } if $jointo; my $sqlname = $tabledef->getAttribute('sqlname') or croak "Table `$table' has no `sqlname' attribute"; $join .= "$sqlname AS $table"; do { if(my $using = $tabledef->getAttribute('using')) { $join .= " ON $jointo.$using = $table.$using"; } elsif(my $ref = $tabledef->getAttribute('refcolumn')) { my $key = $tabledef->getAttribute('keycolumn') or croak "Table $table has `refcolumn' without `keycolumn'"; $join .= " ON $jointo.$ref = $table.$key"; } elsif(my $on = $tabledef->getAttribute('on')) { $join .= " ON $on"; } } if $jointo; push @{$self->{jointext}}, $join; $self->{joinhash}->{$table} = 1; } unless $self->{joinhash}->{$table}; } sub parse { my ($self, $arg) = @_; my $err; $self->{doc} = new XML::LibXML::Document; $self->{arg} = $arg; $err = $self->real_parse($arg) and return $err; $err = $self->do_criteria and return $err; $err = $self->build_output($self->{doc}) and return $err; $err = $self->build_fields and return $err; $self->{jointext} = []; $self->{joinhash} = {}; $self->add_join($self->{main_table}); foreach my $x (@{$self->{criteria}}) { foreach($x->[0]->join) { $self->add_join($_); } } my $select; my $from; my $where; my $order; my $limit; eval { $where = join(' AND ', map($_->[0]->where($_->[1]), @{$self->{criteria}})) || '1'; $from = join(' ', @{$self->{jointext}}); }; return $@ if $@; $self->{count_query} = "SELECT COUNT(*) FROM $from WHERE $where"; foreach my $f (@{$self->{fields}}) { foreach ($f->join) { $self->add_join($_); } } foreach my $o (@{$self->{order}}) { foreach ($o->{field}->join) { $self->add_join($_); } } eval { $select = join(',', map($_->select, @{$self->{fields}})) || '0'; $order = (defined $self->{order} && scalar @{$self->{order}}) ? ' ORDER BY ' . join(',', map($_->orderspec, @{$self->{order}})) : ''; $limit = ($self->{pagesize} > 0) ? ' LIMIT ' . ($self->{page} * $self->{pagesize}) . ", $self->{pagesize}" : ''; $from = join(' ', @{$self->{jointext}}); }; return $@ if $@; $self->{query} = "SELECT $select FROM $from WHERE $where$order$limit"; return undef; } # Process a request # $results = $xmlout->process(); sub process { my $self = shift; my %args = @_; my $err; $self->{query} or croak "DBIx::XMLServer::Request: must call parse before process"; $args{rowcount} = $self->{rowcount} unless $args{rowcount}; # Do the query my $query = $self->{query}; $query =~ s/^SELECT/SELECT SQL_CALC_FOUND_ROWS/ if $args{rowcount} eq 'FOUND_ROWS'; my $sth = $self->{XMLServer}->{dbh}->prepare($query); $sth->execute or croak $sth->errstr; # Put the data into the result tree my $r = $self->{newrecord}->parentNode; my @row; while(@row = $sth->fetchrow_array) { # Clone the template record and insert after the previous record $r = $r->parentNode->insertAfter($self->{newrecord}->cloneNode(1), $r); # Fill in the values my $p = &$sql_ns($self->{newrecord}); my @n = $r->findnodes(".//$p:field"); foreach(@{$self->{fields}}) { eval { $_->result(shift @n, \@row); }; return $@ if $@; } } my $rows = 0; do { my @r; @r = $self->{XMLServer}->{dbh}->selectrow_array('SELECT FOUND_ROWS()') or croak $self->{XMLServer}->{dbh}->errstr; $rows = $r[0]; } if $args{rowcount} eq 'FOUND_ROWS'; do { my @r; @r = $self->{XMLServer}->{dbh}->selectrow_array($self->{count_query}) or croak $self->{XMLServer}->{dbh}->errstr; $rows = $r[0]; } if $args{rowcount} eq 'COUNT'; my %params = ( 'args' => $self->{arg}, 'page' => $self->{page}, 'pagesize' => $self->{pagesize}, 'query' => $self->{query}, 'rows' => $rows, ); # Process through XSLT to produce the result return $self->{XMLServer}->{xslt}->transform($self->{doc}, XML::LibXSLT::xpath_to_string(%params)); } 1; package DBIx::XMLServer; use Carp; sub add_type { my $self = shift; my $type = shift; my $name = $type->getAttribute('name') or croak("Field type found with no name"); my $p = &$sql_ns($type); my $package_name = $type->findnodes("$p:module"); if($package_name->size) { $package_name = "$package_name"; eval "use $package_name;"; croak "Error loading module `$package_name' for field type" . " definition `$name':\n$@" if $@; } else { $package_name = "DBIx::XMLServer::Types::$name"; my $where = $type->findnodes("$p:where"); $where = $where->size ? "sub where { $where }" : ''; my $select = $type->findnodes("$p:select"); $select = $select->size ? "sub select { $select }" : ''; my $join = $type->findnodes("$p:join"); $join = $join->size ? "sub join { $join }" : ''; my $value = $type->findnodes("$p:value"); $value = $value->size ? "sub value { $value }" : ''; my $init = $type->findnodes("$p:init"); $init = $init->size ? "sub init { $init }" : ''; my $isa = $type->findnodes("$p:isa"); $isa = $isa->size ? "$isa" : 'DBIx::XMLServer::Field'; $isa =~ s/\s+//g; eval <{types}->{$name} = $package_name; } # Object constructor # $xmlout = new DBIx::XMLServer($dbh, $doc[, $template]); sub new { my $proto = shift; my $class = ref($proto) || $proto; my $self; my $doc; # Deal with the parameters if(ref $_[0]) { # dbh, doc [, template] $self = {}; $self->{dbh} = shift or croak "No database handle supplied"; $doc = shift or croak "No template file supplied"; $self->{template} = shift; } else { # Named parameters $self = { @_ }; $self->{dbh} or croak "No database handle supplied"; $doc = $self->{doc} or croak "No template file supplied"; } bless($self, $class); my $parser = new XML::LibXML; ref $doc or $doc = $parser->parse_file($doc) or croak "Couldn't parse template file '$doc'"; $doc->isa('XML::LibXML::Document') or croak "This isn't a XML::LibXML::Document"; $self->{doc} = $doc; my $root = $doc->documentElement; $root->localname eq 'spec' && $root->namespaceURI eq $our_ns or croak "Document element is not "; my $p = &$sql_ns($root); # Find all the field type definitions and parse them $self->{types} = {}; foreach($doc->findnodes("/$p:spec/$p:type")) { $self->add_type($_); } # Find the template $self->{template} or $self->{template} = $doc->find("/$p:spec/$p:template") ->shift or croak "No element found"; $self->{template}->isa('XML::LibXML::Element') or croak "Template is not a XML::LibXML::Element"; $self->{template}->localname eq 'template' && $self->{template}->namespaceURI eq $our_ns or croak "Template is not "; # Parse our XSLT stylesheet my $xslt = new XML::LibXSLT; my $f = $INC{'DBIx/XMLServer.pm'}; $f =~ s/XMLServer\.pm/XMLServer\/xmlout\.xsl/; my $style_doc = $parser->parse_file($f) or croak "Couldn't open stylesheet '$f'"; $self->{xslt} = $xslt->parse_stylesheet($style_doc) or croak "Error parsing stylesheet '$f'"; $self->{maxpagesize} = 0 unless $self->{maxpagesize}; $self->{rowcount} = 'NONE' unless defined $self->{rowcount}; return $self; } sub process { my $self = shift; my %args; my $err; # Process arguments if($#_ <= 1 && $_[0] ne 'query') { $args{query} = shift or croak "No query string given"; } else { # Named parameters %args = @_; } $args{XMLServer} = $self; my $request = new DBIx::XMLServer::Request(%args); $err = $request->parse($args{query}) and return $err; return $request->process(); } 1; __END__ =head1 NAME DBIx::XMLServer - Serve data as XML in response to HTTP requests =head1 SYNOPSIS use XML::LibXML; use DBIx::XMLServer; my $xml_server = new DBIx::XMLServer($dbh, "template.xml"); my $doc = $xml_server->process($QUERY_STRING); die "Error: $doc" unless ref $doc; print "Content-type: application/xml\r\n\r\n"; print $doc->toString(1); =head1 DESCRIPTION This module implements the whole process of generating an XML document from a database query, in response to an HTTP request. The mapping from the DBI database to an XML structure is defined in a template file, also in XML; this template is used not only to turn the data into XML, but also to parse the query string. To the user, the format of the query string is very natural in relation to the XML data which they will receive. All the methods of this object can take a hash of named parameters instead of a list of parameters. One C object can process several queries. The following steps take place in processing a query: =over =item 1. The query string is parsed. It contains search criteria together with other options about the format of the returned data. =item 2. The search criteria from the query string are converted, using the XML template, into an SQL SELECT statement. =item 3. The results of the SQL query are translated into XML, again using the template, and returned to the caller. =back =head1 METHODS =head2 Constructor my $xml_server = new DBIx::XMLServer( $dbh, $template_doc [, $template_node] ); my $xml_server = new DBIx::XMLServer( dbh => $dbh, doc => $template_doc, template => $template_node, maxpagesize => $maxpagesize ); The constructor for C takes two mandatory arguments and two optional arguments. =over =item C<$dbh> This is a handle for the database; see L for more information. =item C<$template_doc> This is the XML document containing the template. It may be either an C object or a string, which is taken as a file name. =item C<$template_node> One template file may contain several individual templates; if so, this argument may be used to pass an C object indicating which template should be used. By default the first template in the file is used. =item C<$maxpagesize> This option may be used to limit the number of records than will be returned in a query. The user can choose a page size smaller than this by using the C option on their query (see below), but they will not be allowed to request a page size larger than this maximum. =back =head2 process() my $result = $xml_server->process( $query [, $template_node] ); my $result = $xml_server->process( query => $query, template => $template_node, rowcount => $rowcount, userformat => $userformat ); This method processes an HTTP query and returns an XML document containing the results of the query. There are one mandatory argument and two optional arguments. =over =item C<$query> This is the HTTP GET query string to be processed. =item C<$template_node> As above, this may indicate which of several templates is to be used for this query. It is an C object. =item C<$rowcount> It is possible to limit the number of rows returned in one query, either in response to a user request (by using the C option, see below) or by passing the C option when creating the C object. In these cases it may be useful to know the total number of rows that would have been returned had no limit been in place. The number of rows can be put into the output XML document using the B<< >> element in the template (see below). This argument chooses how this information should be obtained from the database. =over =item FOUND_ROWS Passing C<< rowcount => 'FOUND_ROWS' >> tells the module to use the B option and the B function. If your database supports these, use this option. =item COUNT Passing C<< rowcount => 'COUNT' >> means that a second query will be done after the main database query, of this form: SELECT COUNT(*) FROM ... WHERE ... =back =item C<$userformat> Setting this to a true value allows the user to choose between several templates in the file by specifying the C option in the query string. =back The return value of this method is either an C object containing the result, or a string containing an error message. An error string is only returned for errors caused by the HTTP query string and thus the user's fault; other errors, which are the programmer's fault, will B. =head1 EXAMPLE This example is taken from the tests included with the module. The database contains two tables. Table dbixtest1: +----+--------------+---------+------+ | id | name | manager | dept | +----+--------------+---------+------+ | 1 | John Smith | NULL | 1 | | 2 | Fred Bloggs | 3 | 1 | | 3 | Ann Other | 1 | 1 | | 4 | Minnie Mouse | NULL | 2 | | 5 | Mickey Mouse | 4 | 2 | +----+--------------+---------+------+ Table dbixtest2: +----+----------------------+ | id | name | +----+----------------------+ | 1 | Widget Manufacturing | | 2 | Widget Marketing | +----+----------------------+ The template file (in F) contains the following three table definitions: The template element is as follows: The query string B produces the following output: Ann Other John Smith Widget Manufacturing The query string B produces the following output: Minnie Mouse Mickey Mouse =head1 HOW IT WORKS: OVERVIEW The main part of the template file which controls DBIx::XMLServer is the template element. This element gives a skeleton for the output XML document. Within the template element is an element, the record element, which gives a skeleton for that part of the document which is to be repeated for each row in the SQL query result. The record element is a fragment of XML, mostly not in the B namespace, which contains some B<< >> elements. Each B<< >> element corresponds to a point in the record element where data from the database will be inserted. Often, this means that one B<< >> element corresponds to one column in a table in the database. The field has a I; this determines the mappings both between data in the database and data in the XML document, and between the user's HTTP query string and the SQL WHERE clause. The HTTP query which the user supplies consists of search criteria, together with other special options which control the format of the XML output document. Each criterion in the HTTP query selects one field in the record and gives some way of limiting data on that field, typically by some comparison operation. The selection of the field is accomplished by an XPath expression, normally very simply consisting just of the name of the field. After the field has been selected, the remainder of the criterion is processed by the Perl object corresponding to that field type. For example, the built-in text field type recognises simple string comparisons as well as regular expression comparisons; and the build-in number field type recognises numeric comparisons. All these criteria are put together to form the WHERE clause of the SQL query. The user may also use the special B option to select which fields appear in the resulting XML document; the value of this option is again an XPath expression which selects a part of the record template to be returned. Other special options control how many records are returned on each page, which page of the results should be returned, and may choose one of several templates in the file. The template to use for a query is chosen as follows: =over =item 1. If the B option is set when calling C and the user has chosen a template with the B option in the query string, that template is used. =item 2. Otherwise, if a template was specified when calling C, then that template is used. =item 3. Otherwise, if a template was specified when constructing the C object, then that template is used. =item 4. Otherwise, the first template in the file is used. =back =head1 THE TEMPLATE FILE The behaviour of DBIx::XMLServer is determined entirely by the template file, which is an XML document. This section explains the format and meaning of the various elements which can occur in the template file. =head2 Namespace All the special elements used in the template file are in the namespace associated to the URI B. In this section we will suppose that the prefix B is bound to that namespace, though of course any other prefix could be used instead. =head2 The root element The document element of the template file must be an B<< >> element. This element serves only to contain the other elements in the template file. Contained in the root element are elements of three types: =over =item * Field type definition elements; =item * Table definition elements; =item * One or more template elements. =back We now describe each of these in more detail. =head2 Field type definitions A field type definition is given by a B<< >> element. Each field in the template has a type. That type determines: how a criterion from the query string is converted to an SQL WHERE clause for that field; how the SQL SELECT clause to retrieve data for that field is created; and how the resulting SQL data is turned into XML. For example, the standard date field type can interpret simple date comparisons in the query string, and puts the date into a specific format in the XML. Each field type is represented by a Perl object class, derived from C. For information about the methods which this class must define, see L. The class may be defined in a separate Perl module file, as for the standard field types; or the methods of the class may be included verbatim in the XML file, as follows. The B<< >> element has one attribute, B, and four element which may appear as children. =over =item attribute: B The B attribute defines the name by which this type will be referred to in the templates. =item element: B<< >> If the Perl code implementing the field type is contained in a Perl module in a separate file, this element is used to give the name of the module. It should contain the Perl name of the module (for example, C). =back =head3 Example DBIx::XMLOut::NumberField Instead of the B<< >> element, the B<< >> element may have separate child elements defining the various facets of the field type. =over =item element: B<< >> This element contains the name of a Perl module from which the field type is derived. The default is C. =item element: B<< >> This element contains the body of the C method. If this method is overridden, this attribute need not necessarily still be present.) =item attribute: B This attribute determines the action when the field value is null. There are three possible values: =over =item B (default) The field is omitted from the result, but the parent element remains. =item B The parent element is omitted from the record =item B The parent element has the B attribute set. =back =back =head2 The sql:omit attribute Any element in the record may have the Boolean attribute B. If this attribute is set, then this element will be omitted from any record in which the element is empty (because child elements have been omitted). =head2 The meta element The B<< >> element is used for putting information about the query into the output document. The information is selected by the B attribute of the element. The following B attributes are recognised: =over =item type='args' This gives the original query string passed to B. =item type='page' This gives the page number within the results, as selected by the B option in the query string. =item type='pagesize' This gives the page size, as selected by the B option in the query string. =item type='query' This gives the SQL query which was executed to produce the results. =item type='rows' This gives the number of rows which the query would have returned, had it not been for the B and B options. To tell the module how to find this information, set the B option when processing the request (see above). =back The B<< >> element in the template will be replaced by the corresponding string in the output document. Alternatively, it is possible to place the string into the output document as an attribute to the parent element of the B<< >> element. To do this, include an attribute B on the B<< >> element, where B is the local name of the attribute. To add a namespace to the attribute, additionally include an attribute B on the B<< >> element, replacing B with whatever namespace should be used. =head1 SPECIAL OPTIONS IN THE QUERY STRING The HTTP query string may contain certain special options which are not interpreted as criteria on the records to be returned, but instead have some other effect. =over =item fields = This option selects which part of each record is to be returned. In the absence of this option, an entire record is returned for each row in the result of the SQL query. If this option is set, its value should be an XPath expression. The expression will be evaluated in the context of the single child of the B<< >> element and should evaluate to a set of nodes; the part of the record returned is the smallest subtree containing all the nodes selected by the expression. =item pagesize = , page = These options give control over how many records are returned in one query, and which of several pages is returned. To put a limit on the page size which can be requested, use the B option when creating the C object. By default there is no limit. =item order = This option controls how records are ordered in the output document. The B<< >> should be a comma-separated list of XPath expressions, each optionally followed by a space and the string B or B. Each of these XPath expressions is evaluated within the context of the single child of the B<< >> element and should select one or more fields; these fields are used to order the result records. Fields are used in the order that they appear in the list; if a single list element selects more than one field, they are used in document order. =back =head1 HOW IT REALLY WORKS When a C object is created, the template file is parsed. A new Perl module is compiled for each field type defined. The C method performs the following steps. =over =item 1. The HTTP query string is parsed. It is split at each `&' character, and each resulting fragment is un-URL-escaped. Each fragment is then examined, and a leading part removed which matches a grammar very similar to the B production in XSLT (see L). This leading part is assumed to be an expression referring to a field in the B<< >> element of the template, unless it is one of the special options B, B or B. If the B<< >> has a B attribute, then any unqualified name in this expression has that default namespace added to it. =item 2. Each criterion in the query string is turned into part of the WHERE clause. The leading part of each fragment of the query string is evaluated as an XPath expression in the context of the single child of the B<< >> element. The result must be either a nodeset having a unique B<< >> descendant; or an attribute on an element having a child B<< >> element whose B attribute matches. In either case, a single B<< >> element is found. That field's type is looked up and the resulting field type class's C method called, being passed the remainder of the fragment of the HTTP query string. The result of the C method is added to the WHERE clause; all criteria are combined by AND. =item 3. A new result document is created whose document element is a clone of the B<< >> element. The B<< >> in this new document is located. The value of the special B option is evaluated, as an XPath expression, within the unique child of that element, and the smallest subtree containing the resulting fields is formed. The rest of the record is pruned away. The SQL SELECT clause is now created by calling the C