Developer's Guide

1. Writing Web Applications using XQuery

Not only is XQuery a powerful query language, it is also a functional programming language with strong typing features, and can therefore be used to implement the entire processing logic of a web application. eXist provides preinstalled library modules for getting request parameters, getting/setting session attributes, encoding URLs and so on. The directory extensions/modules in the eXist distribution contains even more extension modules, e.g. for sending mails, generating thumbnails or retrieving data from an SQL database. There's also a complete, usable HTTP client implemented as an XQuery module.

XQuery is powerful enough to replace JSP or PHP as a server-side language. We don't say that PHP or JSP are bad in any way. But if you start writing XQueries, you may soon find it easier to write everything in XQuery (maybe with the help of some XSLT) and stay within the XML world. The XSLT functions make it easy to prepare the results of an XQuery for presentation (for transformations, XSLT is often more convenient than XQuery, though XQuery could be used as well). Version 1.4 even provides a lightweight MVC (model-view-controller) framework and allows you to properly customize and rewrite the URLs the user sees.

Instead of directly producing HTML output, "XQuerylets" can also be designed as self-contained services which deliver XML data to applications on the client. Creating AJAX applications with eXist is straight-forward. No complex framework is required on the server. No need for an additional persistence layer. XQuery integrates it all.

Beyond the simple examples, the distribution contains a number of web applications which can be used as a starting point to learn more about XQuery as a server-side language, e.g. the web-based admin interface, which doesn't use any AJAX but demonstrates various extension functions to manipulate the database

1.1. Executing XQueries on the Server

eXist provides 3 different ways (if you include Atom, there are even 4) to call an XQuery over the web:

XQueryServlet

The XQueryServlet reads an XQuery script from the file system, usually from the directory in which the web application resides, and executes it with the current HTTP context. The result of the query is returned as the content of the HTTP response.

Using the XQueryServlet is thus similar to calling a JSP page or PHP script, which resides on the file system of the server.

XQueryGenerator

The XQueryGenerator integrates eXist's XQuery capabilities with Apache's Cocoon framework. As with the servlet, the XQueryGenerator (usually) reads XQuery scripts from the file system, but instead of directly returning the query results in the HTTP response, it passes them to a Cocoon pipeline.

REST Server

The REST servlet can be used to execute stored XQueries on the server! If the target resource of a GET or POST request is a binary document with mime-type application/xquery, the REST server will try to load and execute this resource with the current HTTP context.

This is a very powerful concept as it allows you to store entire web applications into the database. Links to images or other resources will be resolved relative to the database collection in which the called XQuery is stored.

1.2. XQueryServlet and XQueryGenerator

Note

TODO: release 1.4.0 limits Cocoon to one directory within the distribution. All the Cocoon-related documentation should probably go into a separate document.

eXist generates HTML web pages from XQuery files in two ways: the XQueryServlet and XQueryGenerator. With both the XQueryServlet and XQueryGenerator the compiled XQuery script is stored in a cache for future use. For this, eXist compiles XQuery into a tree of expression objects, which can be repeatedly executed. This code will only be recompiled if the source file has changed.

Both XQueryServlet and the XQueryGenerator provide initialization parameters to set the username and password used for requests. However, the code will also check if the current HTTP session contains the session attributes user and password. If so, the session settings will overwrite any previous settings. For more information on changing user identities, see the Session Example. The user identity under which the current query executes can also be changed from within the query itself, using the xmldb:login() function.

XQueryServlet

This servlet responds to URL-patterns (e.g. *.xql and *.xqy) as defined in the web.xml configuration file of the application. The servlet will interpret this pattern as pointing to a valid XQuery file. The XQuery file is then loaded, compiled and executed with the current HTTP context. The results are then sent to the client in the content of the HTTP response.

The Content-Type header of the HTTP response will be set to the value of the media-type serialization property defined by the XQuery (see serialization parameters).

To use the servlet, you must define the URL-patterns for your web application by adding the following to the WEB-INF/web.xml configuration file:

Example: Configuration for the Servlet

<web-app>
    <display-name>eXist Server</display-name>
    <description>eXist Server Setup</description>
    
    <servlet>
        <servlet-name>org.exist.http.servlets.XQueryServlet</servlet-name>
        <servlet-class>org.exist.http.servlets.XQueryServlet</servlet-class>

        <init-param>
            <param-name>uri</param-name>
            <param-value>xmldb:exist:///db</param-value>
        </init-param>
    </servlet>
    
    <servlet-mapping>
	  <servlet-name>org.exist.http.servlets.XQueryServlet</servlet-name>
	  <url-pattern>*.xql</url-pattern>
    </servlet-mapping>
</web-app>

This will configure the servlet to respond to any URL-pattern ending with the .xql file extension as specified in <servlet-mapping> . Note that the .xq is specifically NOT used for the <url-pattern> definition so as not to interfere with Cocoon examples, which exclusively use this file extension. Also note that the uri parameter in <init-param> specifies the XML:DB root collection used by the servlet. To configure this parameter to have the servlet access a remote database, follow instructions provided in the deployment docs.

XQueryGenerator (Cocoon)

As with the servlet, the Cocoon generator reads and executes XQuery scripts. However, unlike the servlet, the generator passes the results to a Cocoon pipeline for further processing. Furthermore, the XQueryGenerator has to be configured in the Cocoon sitemap (sitemap.xmap). The sitemap registers the generator and configures a pipeline to map resources for different web applications. For more information on configuring and using sitemaps, consult the documentation provided by Cocoon.The following is a basic sitemap:

Example: Cocoon Sitemap

<map:sitemap xmlns:map="http://apache.org/cocoon/sitemap/1.0">
    <map:components>
        <map:generators default="file">
            <map:generator name="xquery" 
                logger="sitemap.generator.xquery"
                src="org.exist.cocoon.XQueryGenerator"/>
        </map:generators>
        <map:readers default="resource"/>
        <map:serializers default="html"/>
        <map:selectors default="browser"/>
        <map:matchers default="wildcard"/>
        <map:transformers default="xslt">
        </map:transformers>
	</map:components>
    <map:pipelines>
        <map:pipeline>
            <map:match pattern="*.xq">
                <map:generate src="{1}.xq" type="xquery"/>
                <map:serialize encoding="UTF-8" type="html"/>
            </map:match>
        </map:pipeline>
    </map:pipelines>
</map:sitemap>

According to the above pipeline definition, any path ending with the .xq extension is matched and processed by the pipeline. The pipeline generates results using the XQueryGenerator defined as type xquery in <map:components> .

1.3. A Simple Example

To illustrate the use of the XQueryServlet and the HTTP extension functions, let's have a look at a very basic example: a simple number guessing game running on the server. The source code for this game is found in webapp/xquery/guess.xql. As the file extension indicates, this particular script is processed by the XQueryServlet. The full script is as follows:

Example: Guess a Number

xquery version "1.0";
(: $Id: devguide_xquery.xml 10365 2009-11-05 01:48:08Z ixitar $ :)

import module namespace request="http://exist-db.org/xquery/request";
import module namespace session="http://exist-db.org/xquery/session";
import module namespace util="http://exist-db.org/xquery/util";

declare function local:random($max as xs:integer) 
as empty()
{
    let $r := ceiling(util:random() * $max) cast as xs:integer
    return (
        session:set-attribute("random", $r),
        session:set-attribute("guesses", 0)
    )
};

declare function local:guess($guess as xs:integer,
$rand as xs:integer) as element()
{
    let $count := session:get-attribute("guesses") + 1
    return (
        session:set-attribute("guesses", $count),
        if ($guess lt $rand) then
            <p>Your number is too small!</p>
        else if ($guess gt $rand) then
            <p>Your number is too large!</p>
        else
            let $newRandom := local:random(100)
            return
                <p>Congratulations! You guessed the right number with
                {$count} tries. Try again!</p>
    )
};

declare function local:main() as node()?
{
    session:create(),
    let $rand := session:get-attribute("random"),
        $guess := xs:integer(request:get-parameter("guess", ()))
    return
		if ($rand) then 
			if ($guess) then
				local:guess($guess, $rand)
			else
				<p>No input!</p>
		else 
		    local:random(100)
};

<html>
    <head><title>Number Guessing</title></head>
    <body>
        <form action="{session:encode-url(request:get-uri())}">
            <table border="0">
                <tr>
                    <th colspan="2">Guess a number</th>
                </tr>
                <tr>
                    <td>Number:</td>
                    <td><input type="text" name="guess" size="3"/></td>
                </tr>
                <tr>
                    <td colspan="2" align="left"><input type="submit"/></td>
                </tr>
            </table> 
        </form>
        { local:main() }
        <p><small>View <a href="guess.xql?_source=yes">source code</a></small></p>
    </body>
</html>

The mainline of this script simply creates an HTML document with a form. Since we need to store some information in the HTTP session, the action link of the form is encoded with the current session ID (the first time the script is called there will be no session yet, but that's not a problem). The main application logic is called in line 68:

{ local:main() }

The local:main() function first calls session:create() to create a session if it has not already been generated before. The script uses two session attributes:

random

stores the random number the user should guess

guesses

keeps track of the number of attempts

If the session attribute "random" is empty, we need to create a new random number first. This is done by calling local:random():

declare function local:random($max as xs:integer) 
as empty()
{
    let $r := ceiling(util:random() * $max) cast as xs:integer
    return (
        session:set-attribute("random", $r),
        session:set-attribute("guesses", 0)
    )
};

local:random() generates a new random and stores it into the session. The "guesses" session attribute is reset to 0.

Back to local:main(): if $rand is set, the function checks if the user entered a guess, which should have been passed in the request parameter "guess". If a guess has been made, it needs to be checked by calling local:guess():

declare function local:guess($guess as xs:integer,
$rand as xs:integer) as element()
{
    let $count := session:get-attribute("guesses") + 1
    return (
        session:set-attribute("guesses", $count),
        if ($guess lt $rand) then
            <p>Your number is too small!</p>
        else if ($guess gt $rand) then
            <p>Your number is too large!</p>
        else
            let $newRandom := local:random(100)
            return
                <p>Congratulations! You guessed the right number with
                {$count} tries. Try again!</p>
    )
};

The function checks the guess and returns an HTML paragraph to tell the user if the guess was too high or too low. If the numbers match, a new random is generated.

1.4. Using the REST Server and Stored XQueries

Instead of executing XQueries on the file system, one can also use eXist's REST-style HTTP interface to call queries which are stored in a database collection. For the document to be recognized as an XQuery resource, the following two conditions need to be met:

  1. the XQuery document should be stored as a binary resource
  2. the mime-type of the resource has to be application/xquery. The Java admin client will usually assign this mime-type automatically. If in doubt, use the Java client to check the resource properties:

As an example, you can use the above number-guessing game. Store the XQuery file into a database collection, e.g. as /db/test/guess.xql.Next, point you web browser to the REST URL:


http://localhost:8080/exist/rest/db/test/guess.xql

However, what you see is probably not what you expected: depending on your web browser, you will get the raw XML (or rather HTML) of the page. Obviously, the browser does not recognize it as HTML yet. This happens because, by default, the REST server sets the Content-Type header in the HTTP response to "text/xml". We thus need to change this to "text/html" to tell the web browser that the received content can be displayed as HTML.

The REST server copies the value for the content type header from the XQuery serialization parameter media-type. You can set this parameter in the XQuery itself. Just add a line

declare option exist:serialize "method=xhtml media-type=text/html";

after the "import module" statements and before the first function declaration. With this change in place, the XQuery output should properly display as HTML and the game should work as before.

November 2009
Wolfgang M. Meier
wolfgang at exist-db.org