Writing and Debugging XQuery Web Apps with Qexo
June 11, 2003
In this article we will show to to write and install a very simple web application written in the XQuery language. We will also show you how errors are handled, and how you can debug them. Our application uses the servlet extension of Qexo (version 1.7beta2 or later), a free software implementation of XQuery.
Our application presents to the user a form like the following:
You can edit either field, perhaps to 10 and 3. When you click Submit
, you get
an updated form showing the sum of the two fields:
The application uses HTTP parameters to "remember" the sum from one request to
the next. A more interesting application might use some kind of permanent storage
or a
datebase. We'll get to those in a later article; for now let's think of this as a
slightly
more interesting version of "Hello world!"
.
An XQuery program to generate a form
The following XQuery program (adder.xql
) handles both the "logic" and "presentation" of our web
application.
define function num-parameter($name, $default) { number(request-parameter($name, $default)) } <html> <head><title>Accumulating Adder</title></head> <body> <form> <table> <tr> <td>Result so far: </td> <td> <input name="sum1" value="{num-parameter("sum1", 0) +num-parameter("sum2", 0)}" /> </td> </tr> <tr> <td>Add to it:</td> <td> <input name="sum2" value="{num-parameter("sum2", 1)}" /> </td> </tr> </table> <input type="submit" value="Submit" /> </form> </body> </html>
The main part of our XQuery module is just a big "element constructor expression"
that
generates the HTML (or rather XHTML) of a single <form>
element. The
initial values of the <input>
fields are given by embedded XQuery
expressions inside {curly braces}
. Those use the num-parameter
function, which is defined in the Query prolog. The num-parameter
function uses request-parameter
to extract a named HTTP parameter from the URL.
(Future Qexo versions may provide alternative ways of getting request parameters,
including
likely moving request-parameter
to a non-default namespace.)
I'll explain the control flow of our "application" after we look at how to install and get it running.
Installing Qexo under Tomcat
Installing this application is very easy, assuming you have a web server that can
run Java
Servlets. I tested this application using version 4.1.24 of Tomcat, a web server written in Java and
released by the Apache Foundation's Jakarta project. I assume you or someone else
has
already installed Tomcat, and that the environment variable $CATALINA_HOME
is
where Tomcat is installed.
Qexo is part of the Kawa framework. You
will need to install a jar
file of Kawa (version 1.7 or later). You can get
kawa-1.7.jar
from the Kawa ftp
site or from a mirror site. Install
this as $CATALINA_HOME/shared/lib/kawa-1.7.jar
. (If you're using Tomcat 4.0.x,
shared/lib
doesn't exist. Install as
$CATALINA_HOME/lib/kawa-1.7.jar
instead.)
If Tomcat isn't already running, start it. For example, under Unix-like systems you
can run
the script $CATALINA_HOME/bin/bin/startup.sh
. You may need to set the
environment variable JAVA_HOME
to point to the location of Java on your
machine. (On Mac OS X 10.2 this is
/System/Library/Frameworks/JavaVM.framework/Home
.) If you haven't changed any
of the defaults, you should now be able to point your browser at
http://localhost:8080/
and get the default
Tomcat home page.
Installing our web application
A web application is a group of data, servlets, and configuration files to
handle a related set of URLs. The servlet specification specifies the directory structure
of
a web application. Let us install our adder
in a new web application called
utils
. This means that we need to create two directories:
mkdir $CATALINA_HOME/webapps/utils mkdir $CATALINA_HOME/webapps/utils/WEB-INF
Each web application has a web.xml
configuration file. Copy the following web.xml
into
$CATALINA_HOME/webapps/utils/WEB-INF/web.xml
:
<?xml version="1.0" encoding="ISO-8859-1"?> <web-app> <display-name>XQuery Utils</display-name> <servlet> <servlet-name>KawaPageServlet</servlet-name> <servlet-class>gnu.kawa.servlet.KawaPageServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>KawaPageServlet</servlet-name> <url-pattern>*.xql</url-pattern> </servlet-mapping> </web-app>
The <servlet-mapping>
clause tells Tomcat that if it sees a URL that
matches the pattern *.xql
within the current web application
utils
, then it should use the servlet named KawaPageServlet
to
handle it. The <servlet>
clause tells Tomcat that the servlet named
KawaPageServlet
is implemented by the Java class
gnu.kawa.servlet.KawaPageServlet
. (This class is included in the
kawa-1.7.jar
that we installed earlier.) In other words, any URL of the form
http://localhost:8080/utils/*.xql
or
http://localhost:8080/utils/*/*.xql
will be handled by the class
gnu.kawa.servlet.KawaPageServlet
.
So far this has all been boiler-plate. Now we need to install our XQuery program
adder.xql
into the utils
web application by copying to the file
$CATALINA_HOME/webapps/utils/adder.xql
.
Accessing our web application
To access the web application, point your favorite browser at
http://localhost:8080/utils/adder.xql
. Tomcat will receive this request,
use the utils
part to determine that it is for the utils
web
application, and use the utils/WEB-INF/web.xml
configuration file. That tells
Tomcat to forward the request to the KawaPageServlet
.
The first time the KawaPageServlet
sees a request for adder.xql
,
it will compile adder.xql
to a Java class named adder
. This class
will get added to the JVM that is running Tomcat, but by default the class is not
written to
a file. This means that adder.xql
will have to be recompiled the first time you
request it each time you restart Tomcat, but the Kawa compiler is fast enough that
there is
no real gain tp keeping the compiled class around. Kawa also gives you the option
of manually compiling to a servlet.
(If you're curious about the class that KawaPageServlet
creates, add a
qexo-save-class
parameter to the initial request, as in
http://localhost:8080/utils/adder.xql?qexo-save-class
. This will write
out the compiled class to
$CATALINA_HOME/webapps/utils/WEB-INF/classes/adder.class
.)
After KawaPageServlet
has compiled adder.xql
, it will execute the
compiled body of adder.xql
. This first time there won't be any parameters in
the request, so calls to the request-parameter
function return the specified
default values: 0, 0, and 1 respectively. So the initial values in the
<input>
fields will be 0 and 1. The result of evaluating
adder.xql
will be some XHTML, which will be sent as the HTTP response back to
your browser, which will display as in the first image above.
Use your browser to edit the input fields, changing them to 10 and 3. When you click
Submit
those values are used to set the HTTP request parameters
sum1
and sum2
, with the browser sending the URL
http://localhost:8080/utils/adder.xql?sum1=10&sum2=3
. Tomcat receives
the request, passes it to KawaPageServlet
, which forwards the request to the
previously-compiled adder
class. This time the adder
gets the
values 10 and 3 for the parameters sum1
and sum2
, so when the new
form is returned to the browser the initial values of the two fields are 13 and 3,
which
will display the second image above. If you leave the fields as-is and again click
Submit
, the updated forms will show 16 and 3. And so on.
Development and Debugging
Qexo provides help for developing and debugging your applications.
XQuery syntax errors
If you request adder.xql
after editing it, KawaPageServlet
will
automatically recompile it. This makes it easy to test changes. If you make a syntax
error,
the result sent to the browser will include error messages from the compiler. For
example,
supposed you have the file min-cats.xql
with the following semi-nonsense:
define function min-cats($x, $y) { let $z in " cats" return if (x < $y) $x else $y " cats" (: returns number of cats } min-cats(3, 4)
If you request this file, you'll see in your browser the following error log instead:
min-cats.xql:2:17: missing ':=' in 'let' clause min-cats.xql:3:9: node test when focus is undefined min-cats.xql:4:5: missing 'then' min-cats.xql:5:9: missing '}' or ',' min-cats.xql:9:1: non-terminated comment starting at line 6
This gives you the filename, line number, and column number each place Qexo found
a syntax
error. Sometimes an earlier error will cause multiple errors, but in this case each
message
results from a separate error. (They should all be easy to figure out, except perhaps
the
second one, where the $x
mistyped as x
is interpreted as a
node-test in a path expression. Qexo can catch this as it is an undefined context
for such a
node-test.)
Run-time exceptions
If you cause a run-time error, you will get a stack trace which may help you track
down the
error. Here is an application list-data.xql
that looks for a non-existant
"data.txt"
:
define function listing($url) { (: doc($url) replaced document($url) in the May'03 spec. :) <pre>{ doc($url) }</pre> } listing("data.txt"), ""
If you try to run this file, Tomcat will return a Java execution stack trace. Look
for the
root cause
, which looks like this:
java.io.FileNotFoundException: http://localhost:8080/data.txt at sun.net.www.protocol.http.HttpURLConnection .getInputStream(HttpURLConnection.java:707) at gnu.kawa.xml.XMLParser.(XMLParser.java:57) at gnu.kawa.xml.XMLParser.(XMLParser.java:49) at gnu.kawa.xml.XMLParser.(XMLParser.java:42) at gnu.kawa.xml.Document.parse(Document.java:52) at gnu.kawa.xml.Document.apply(Document.java:94) at gnu.mapping.CallContext.runUntilDone(CallContext.java:258) at gnu.mapping.CallContext.runUntilValue(CallContext.java:290) at listData.listing$T(list-data.xql:3) at listData.apply(list-data.xql) at gnu.mapping.CpsMethodProc.apply(CpsMethodProc.java:49) at gnu.mapping.CallContext.runUntilDone(CallContext.java:258) at gnu.mapping.CallContext.runUntilValue(CallContext.java:290) at listData.apply(list-data.xql:5) at gnu.kawa.servlet.KawaPageServlet.apply(KawaPageServlet.java:51) at gnu.kawa.servlet.KawaServlet.doGet(KawaServlet.java:57) at javax.servlet.http.HttpServlet.service(HttpServlet.java:740) at javax.servlet.http.HttpServlet.service(HttpServlet.java:853) at org.apache.catalina.core.ApplicationFilterChain .internalDoFilter(ApplicationFilterChain.java:247) ... lots of Tomcat internals ... at org.apache.tomcat.util.threads.ThreadPool$ControlRunnable .run(ThreadPool.java:619) at java.lang.Thread.run(Thread.java:554)
More from Practical XQuery |
The cause is a FileNotFoundException
, and the exception message names the URL
it was looking for. (The data.txt
is a relative URL that gets resolved to that
of the web application.) The first few lines are within Kawa runtime routines, but
then we
get to two lines in the listData
class, which is the Java "mangling" of the
list-data.xql
file. The first one specifies that we're at line 3, in method
listing$T
, which is the Java mangling of the listing
function.
And that is indeed where the bad call to doc
is. A little further down you will
see a call at list-data.xql
line 5, which is where listing
gets
called.
We append an empty (, ""
) after the call to listing
. This is to
suppress tail-call optimization, which is an optimization done when the last thing
in a
function is a call to another function. The optimization allows some kinds of recursion
to
execute without a stack overflow, but the disadvantage is resulting that stack traces
can be
confusing. So when debugging, it may sometimes be helpful to append some empty value
so that
it becomes the last expression in the function.
Adding trace output
Sometimes it is difficult to understand what an application is doing, in which case
it is
useful to add print statements for debugging purposes. XQuery is a language free of
side
effects, so it doesn't really have print statements. However, the May 2003 draft
specification added a trace
which takes two parameters. The first parameter can
be an arbitary value that is returned as the result of the trace
call. The
other parameter is descriptive string. Both values are written to a "trace data set"
in an
implementation-defined manner. For example you could replace the num-parameter
implementation of adder.xql
by the following:
define function num-parameter($name, $default) { trace ( number(request-parameter($name, $default)) , concat("num-parameter of '", $name, "' is:")) }
That writes the following output to Qexo's standard error output (System.err
).
Tomcat redirects the error output to the file $CATALINA_HOME/logs/catalina.out
,
where you can read it.
XQuery-trace: num-parameter of 'sum1' is: 0 XQuery-trace: num-parameter of 'sum2' is: 0 XQuery-trace: num-parameter of 'sum2' is: 1