6.1. XQuery output in HTML

6.1.1. Table

XQuery is designed to extract parts of an XML file. It is therefore quite appropriate for selecting a subset of wines and displaying it as an HTML page.

Selecting only red wines in our wine catalog (Example 2.3 (page )) and outputting an HTML table of a subset of the available information for each (Figure 5.1) can be achieved with the XQuery query given in Example 6.1. In order to be compared to Example 5.2, we deliberately organized the XQuery script with a structure similar to the one used in the XSL stylesheet.

The query in Example 6.1 first defines a function for the root node (line 13‑4). Because the wine catalog is defined in a specific namespace, its prefix must be declared (line 1‑1) and used for selection. This function outputs the overall structure of the XHTML file with direct constructors. Within an enclosed expression, it calls the wine-catalog function to create the content of the body. For comparison purposes, on line 23‑5 we show an alternate way of defining the root function using computed constructors instead of giving the HTML structure. The wine-catalog function (line 30‑6) outputs a global heading and then starts a table and defines its headers. The lines of the table will be filled by selecting (line 37‑8) wines whose color property is $color. To set up this color filter, a value must be assigned to the global color external variable (line 4‑2). This value is set in an implementation-dependent way when the query is executed by the XQuery processor.

The output for each selected wine is defined with a function that is called for each cat:wine. It outputs, on a single row of the table, the values of its attributes, its color, its year (right-aligned) and formats its price to start with a dollar sign (right-aligned). It finally renders the volume of each bottle in milliliters and in liters. Because the information in the wine catalog is not stored in milliliters, we call two local functions to transform it appropriately.

Example 6.1. [WineCatalog.xq] XQuery script to select the red wines in the catalog (Example 2.3) and to produce Example 5.1 displayed as Figure 5.1. Compare this with Example 5.2.

  1 declare namespace cat = "http://www.iro.umontreal.ca/lapalme/wine-catalog"(1);
    declare default element namespace "http://www.w3.org/1999/xhtml";
    
    declare variable $color external;                                    (2)
  5 
    (: to produce legal and validable XHTML ... :)                       (3)
    declare option saxon:output "method=xml";
    declare option saxon:output "doctype-public=-//W3C//DTD XHTML 1.0 Strict//EN";
    declare option saxon:output "doctype-system=http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd";
 10 declare option saxon:output "indent=yes";
    
    (: using a direct element constructor, with an enclosed expression :)
    declare function local:root($catalog){                               (4)
        <html>
 15         <head><title>Wine Catalog</title></head>
            <body>
                {local:wine-catalog($catalog)}
            </body>   
        </html>
 20 };
    
    (:alternate definition of the function above using computed constructors :)
    declare function local:root-bis($catalog){                           (5)
        element html {
 25         element head {element title {"Wine Catalog"}},
            element body {local:wine-catalog($catalog)}
        }
    };
    
 30 declare function local:wine-catalog($catalog){                       (6)
        element h1 {concat("Wine Catalog (",$color," only)")},
        element table {
            attribute border {1},
            element tr {                                                 (7)
 35             for $t in ('Wine Name','Code','Color','Year','Price','ml','l')
                    return element th{$t}},
            for $wine in $catalog/cat:wine                               (8)
                where $wine/cat:properties/cat:color=$color
                return local:wine($wine) 
 40     }        
    };
    
    declare function local:wine($wine){                                  (9)
        <tr>{
 45         <td>{data($wine/@name)}</td>,
            <td>{data($wine/@code)}</td>,
            <td>{data($wine/cat:properties/cat:color)}</td>,
            <td align="right">{data($wine/cat:year)}</td>,
            (: format-number is not available in XQuery 1.0 !! :)
 50         <td align="right">{concat('$',$wine/cat:price)}</td>, 
            <td align="right">{local:toML($wine/@format)}</td>,
            <td align="right">{local:toL($wine/@format)}</td>
            }
        </tr>
 55 };
    
    declare function local:toML($fmt){                                   (10)
        if ($fmt='375ml') then '375'
        else if ($fmt='750ml') then '750'
 60     else if ($fmt='1l') then '1000'
        else if ($fmt='magnum') then '1500'
        else 'big'
    };
    
 65 declare function local:toL($fmt){                                    (11)
        let $ml := local:toML($fmt)
        return 
            if ($ml castable as xs:integer) 
               then number($ml) div 1000 
 70            else $ml
    };
    
    local:root(/cat:wine-catalog)                                        (12)
    

1

Definition of the cat namespace prefix in order to access the elements of the wine catalog defined in this namespace. The default namespace is set to be the one needed for a valid XHTML file. With this declaration, HTML tags that are used in the stylesheet are in the appropriate namespace for HTML validation.

2

Global variable that should be initialized when executing the query.

3

Declarations telling the SAXON transformation engine to serialize with the appropriate headers to produce valid XHTML.

4

Function called on the root node defining the skeleton of the HTML page: a head and a body with a call to a function filling the content of the HTML skeleton.

5

Alternate definition of the previous function to illustrate the difference in style between the direct and computed constructors.

6

Function for the catalog node: a header with a title and a table.

7

Uses a for-each in a list of strings to output the headers of the table.

8

A for-where instruction selects the wines with the chosen color. The result is a sequence wine nodes. The function on line line 43‑9 is applied to each of them.

9

Outputs properties of a wine. The first four are written as they appear in the source but we must use the data function to keep only the string value. The price is formatted in dollars and cents and the bottle format is given in either milliliters or liters through local functions.

10

a multi-line XPath expression to select the appropriate case within a cascaded if-then-else expression.

11

Uses the output of the previous function to get the number of milliliters, which is then divided by 1000 if it is a number. Otherwise it is returned as is.

12

Query the document starting from the root element node.


6.1.2. Computing New Information

Here we show how XQuery can be used for more complex selections and transformations. We will explain how to create a web page presenting the content of the cellar and integrating information from the wine catalog. This is equivalent to the program shown in Example 5.4. The end result is shown in Figure 5.2 (an outline of the underlying HTML code is shown in Example 5.3). There are external links in order to retrieve more information about the wines by googling the name of the wine. There are two similar links for each wine, created for the sole purpose of comparing ways of creating them in XSL.

The root function (line 11‑2) creates the high-level structure of the XHTML file. The title of the page, also displayed at the top of the page, refers to the name of the owner. In Example 2.2, element name (line 12‑8) is subdivided in two elements: first and family. When the value of such an element is accessed in an enclosed expression, it is returned as an XML element. This would not be valid in an XHTML document, so we call the data function to get the text content of this element, including the whitespace nodes.

The content of the cellar book is obtained by the cellar-book function on line 20‑3 creating a table with the address of the owner (line 28‑4) and the cellar (line 31‑5). It then calls the cellar function (line 37‑6). The lines of the addresses are obtained by looping over all elements with a for and writing a <br/> between the text values of each element of the owner and location elements. Because we want to skip the first element (the name of the owner has already been given at the top), we only keep elements (line 28‑4) with a position number greater than 1.

The cellar (line 40‑7) function produces a table of information about wines in the cellar, sorted by their code. This is why the for is followed by an order by specification. The last line of the table contains an estimated total value of the cellar. The value of each type of wine is computed by creating a sequences of values obtained by a join (two nested expressions on a for) between the codes of wines in the cellar and the codes of the wines in the catalog. The values are then summed with the predefined sum function. To compute the total number of bottles (line 60‑10), we can use the sum function. Finally, if there are any comments in the wine elements of the cellar (line 65‑11), we add a Comments section and write each of them, also in increasing order of wine code. This way, they are in the same order as in the table of wines.

We define the wine function (line 75‑12) to create a row in the table of wines. We first define a variable $code (line 76‑13) holding the value of the code attribute. In order to retrieve some information about this wine from the wine catalog, we pass the wine node from the wine catalog as a parameter when calling the nameAndUrl function (line 80‑14). The link between the current element and the corresponding element in the catalog is made using the value of the code variable given in the XPath expression. The remaining elements of the row are the purchase date (right-aligned), a number of stars corresponding to the rating and the quantity (right-aligned). The estimated value of the cellar is computed using XPath expressions. Note the use of the data function to get the value of the elements.

nameAndUrl is a function that receives a wine element as a parameter. From this wine element, it creates an XHTML link with an a element. Its href attribute value is a string specifying the query to send to Google when searching for this wine using its name. Because the link must be created dynamically, we use a computed attribute constructor within a computed a element which uses the data function to get the value of the name attribute. Note that if only $wine/@name had been specified, then the attribute name would have been added to the a element and not its value as content of the element.

The comment function (line 97‑17) outputs the value of the code of the parent node, then a colon and the text value of the comment, followed by a line break. When getting the content of the comment, the cat:bold elements will be processed by the bold function (line 101‑18). It will transform them in HTML b tags. In XSL, this call was implicit via the application of templates but in XQuery the function must be called explicitly.

Example 6.2. [CellarBook.xq] XQuery script to produce information about the cellar (Example 2.2). The resulting HTML code (Example 5.3) is rendered as Figure 5.2. Compare this with Example 5.4.

  1 declare namespace cat = "http://www.iro.umontreal.ca/lapalme/wine-catalog";
    
    (: to produce legal and validable XHTML ... :)
    declare option saxon:output "method=xml";
  5 declare option saxon:output "doctype-public=-//W3C//DTD XHTML 1.0 Strict//EN";
    declare option saxon:output "doctype-system=http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd";
    declare option saxon:output "indent=yes";
    
    declare variable $GoogleStart := "http://www.google.com/search?q=";  (1)
 10 
    declare function local:root($cellar-book){                           (2)
        <html>
            <head><title>Cellar of {data($cellar-book/owner/name)}</title></head>
            <body>
 15             {local:cellar-book($cellar-book)}
            </body>   
        </html>
    };
    
 20 declare function local:cellar-book($cellar-book){                    (3)
        <h1>Cellar of {data($cellar-book/owner/name)}</h1>,
        <table border="1">
            <tr>
                <th>Personal address</th>
 25             <th>Cellar address</th>
            </tr>
            <tr><td>
                {for $e in $cellar-book/owner/*[position()>1]            (4)
                    return (data($e),<br/>)}
 30             </td>
                <td>{for $e in $cellar-book/location/*                   (5)
                       return (data($e),<br/>)}
                </td>
            </tr>
 35     </table>,
        <p/>,
        local:cellar($cellar-book/cellar,$cellar-book/cat:wine-catalog)  (6)
    };
    
 40 declare function local:cellar($cellar,$catalog){                     (7)
         <table border="1">
            <tr>{
                for $t in ("Code","Name","Purchase Date","Rating","Nb Bottles")
                    return <th>{$t}</th>}
 45         </tr>{
                for $w in $cellar/wine
                    order by $w/@code ascending
                       return local:wine($w,$catalog)}                   (8)
            <tr>
 50             <td colspan="3">Estimated value</td>
                <td align="right">{
                (: compute the estimated value of the cellar :)
                sum (for $w in $cellar/wine,                             (9)
                         $codeInCat in $catalog/cat:wine/@code 
 55                       where $codeInCat = $w/@code 
                             return $w/quantity * $codeInCat/../cat:price)}
                </td>
                <td align="right">{
                    (: compute the total number of bottles :)
 60                 sum($cellar/wine/quantity)}                          (10)
                </td>
            </tr>
        </table>,
        (: put comment section if at least one comment appears :)
 65     if (count($cellar/wine/comment)>0)                               (11)
            then (
                <h3>Comments</h3>,
                <p>{for $c in $cellar/wine/comment
                        order by $c/../@code ascending
 70                  return local:comment($c)}
                </p>
            ) else ()
    };
    
 75 declare function local:wine($wine,$catalog){                         (12)
        let $code := $wine/@code                                         (13)
        return 
            <tr>
                <td>{substring($code,2)}</td>
 80             <td>{local:nameAndUrl($catalog/cat:wine[@code=$code])}</td>  (14)
                <td align="right">{data($wine/purchaseDate)}</td>
                <td align="center">{substring('*****',1,                 (15)
                    if ($wine/rating/@stars) then $wine/rating/@stars else 0)}
                </td>
 85             <td align="right">{data($wine/quantity)}</td>
            </tr>
            
    };
    
 90 declare function local:nameAndUrl($wine){                            (16)
        element a {
            attribute href{concat($GoogleStart,encode-for-uri($wine/@name))},
            data($wine/@name)
        }
 95 };
    
    declare function local:comment($comment){                            (17)
        data($comment/../@code),':',local:bold($comment),<br/>
    };
100 
    declare function local:bold($mixed-content){                         (18)
        for $child in $mixed-content/node()
        return if ($child instance of element() and local-name($child)="bold")
               then element b{data($child)}
105            else $child
    };
    
    (:  change the namespace of a subtree starting at an element 
        adapted from Priscilla Walmsley, XQuery, O'Reilly, p. 258 :)
110 declare function local:change-element-ns-deep($element,$new-ns){     (19)
        element {QName($new-ns,local-name($element))} {
            $element/@*,
            for $child in $element/node()
            return if ($child instance of element())
115                then local:change-element-ns-deep($child,$new-ns)
                   else $child
        }
    };
    
120 (: put the whole tree in the XHTML namespace :)
    local:change-element-ns-deep(                                        (20)
        local:root(/cellar-book),
        "http://www.w3.org/1999/xhtml")
    

1

Parameter giving the start of the URL allowing a search for a given wine on Google.

2

Function for the root node that defines the skeleton of the HTML page: a head with a title indicating the name of the owner and a body to be filled by the call to the cellar-book function.

3

Global header with the name of the owner followed by a table giving more information about the owner and the location of the cellar.

4

Loops over all child nodes of the address, except the first one, the name of the owner. After the text content of each node is rendered using the data function, a line break is forced with an HTML br element.

5

Loops over all child nodes of the location. After the text content of each node is written using the data function, a line break is forced with an HTML br element.

6

Calls the function to output the content of the cellar.

7

The content of the cellar is given as a table with a header. It is followed by a series of lines corresponding to each wine. Finally a line holding the estimated value of the whole cellar is created.

8

Calls the wine function for each wine but in increasing order of the code attribute.

9

The total value of the cellar is computed using two nested for expressions that loop over all wines of the cellar and the catalog and return the sum of the value of each wine. The value of a wine is given by the number of bottles (quantity) multiplied by the price of the wine in the catalog having the same code attribute as this wine.

10

The total number of bottles is the sum of the values of all quantity elements.

11

Comments appear at the bottom of the table if at least one wine has a comment. They are also given in ascending order of code attribute.

12

Outputs a line of an HTML table for a given wine.

13

Defines a local variable for the code attribute. It will be useful to differentiate it from the code attribute of the catalog used in the expression on line 80‑14

14

Outputs the name of the wine and its URL using the function defined on line 90‑16.

15

Outputs a string of * corresponding to the number of stars for this wine.

16

Produces the name of the wine as the anchor text for an HTML link to Google. It features the appropriate wine name in order to get further information about it.

17

A comment is preceded with the value of the surrounding code attribute and followed by a line break.

18

A cat:bold element is transformed into an HTML b element.

19

In order to produce a legal XHTML document, all elements must be in an appropriate namespace. Because in XQuery the default global default namespace declaration also applies to the XPath expressions on the input tree, it is not possible to query a tree in the default namespace and have the output tree constructor in another. The output is first created by the transformation in the empty namespace. This result is then copied but with nodes into the XHTML namespace.

20

Calls the transformation on the cellar-book element and copies the result into the XHTML namespace.


6.1.3. Bulleted Lists

As we did in the previous chapter, we now describe a query that can be applied to any XML file to show its indentation structure by means of nested HTML unnumbered lists.

To transform the cellar book (Example 2.2) into the HTML code of Example 5.6 (rendered in Figure 5.3), we can use the code given in Example 6.3 which features four main functions:

  • for the root element (line 20‑3) the function produces the overall structure of the HTML file with its head and body elements. The enclosed expression for processing subelements (line 27‑5) is called within an unnumbered list delimited by ul tags.

  • for an attribute (line 33‑6), the function returns a space, the name of the attribute followed by an equal sign and its value within double quotes.

  • for an element (line 37‑7), the function produces the name of the element returned by the function local-name() in bold (line 39‑8) followed by its attributes. If the element is a node without any children (it is a text node in this case) then it is output with only its content, otherwise a new unnumbered list is started and template matching is applied on child nodes.

  • a text node (line 54‑11) is inserted into an li element

Example 6.3. [compactHTML.xq] XQuery script to produce a bulleted outline (Example 5.6) from the cellar book (Example 2.2). Compare this with Example 5.7.

  1 (: to produce legal and validable XHTML ... :)
    declare option saxon:output "method=xml";                            (1)
    declare option saxon:output "doctype-public=-//W3C//DTD XHTML 1.0 Strict//EN";
    declare option saxon:output "doctype-system=http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd";
  5 declare option saxon:output "indent=yes";
    
    (: remove all whitespace nodes from elements of the input tree :)
    declare function local:strip-space($element){                        (2)
        element {local-name($element)} {
 10         $element/@*,
            for $child in $element/node()
            return if ($child instance of text() and normalize-space($child)='')
                   then ()
                   else if ($child instance of element()) 
 15                     then local:strip-space($child)
                        else $child
        }
    };
    
 20 declare function local:root($root,$uri){                             (3)
        <html>
            <head>
                <title>HTML compaction of "{replace($uri,'.*/(.*)','$1')}"</title> (4)
            </head>
 25         <body>
                <ul>
                    {local:element($root)}                               (5)
                </ul>
            </body>
 30     </html>
    };
    
    declare function local:attribute($attribute){                        (6)
        concat(" ",local-name($attribute),'="',$attribute,'"')
 35 };
    
    declare function local:element($element){                            (7)
        <li>
            <b>{local-name($element)}</b>                                (8)
 40         {for $attr in $element/@*
                return local:attribute($attr),
             if (count($element/*)=0) (: single text node ?:)            (9)
             then concat(" ",data($element))
             else <ul> {                                                 (10)
 45                for $child in $element/(*|text()) (: possible mixed node :)
                      return if($child instance of element())
                             then local:element($child)
                             else local:text($child)
                } </ul>
 50          }
        </li>    
    };
    
    declare function local:text($text){                                  (11)
 55     <li>{$text}</li>
    };
    
    (:  change the namespace of a subtree starting at an element 
        adapted from Priscilla Walmsley, XQuery, O'Reilly, p. 258 :)
 60 declare function local:change-element-ns-deep($element,$new-ns){     (12)
        element {QName($new-ns,local-name($element))} {
            $element/@*,
            for $child in $element/node()
            return if ($child instance of element())
 65                then local:change-element-ns-deep($child,$new-ns)
                   else $child
        }
    };
    
 70 local:change-element-ns-deep( (: put the whole tree in the XHTML namespace :)(13) 
        local:root(local:strip-space(/*),document-uri(/)),
        "http://www.w3.org/1999/xhtml")
    

1

Declarations telling the SAXON transformation engine to serialize with the appropriate headers to produce valid XHTML.

2

Function to remove all whitespace-only nodes in an element because we are only interested in the content of the input document, not its structure.

3

Produces the XHTML skeleton for the document.

4

Outputs the title of the HTML page. Because the $uri parameter contains the full URI of the current document, the replace function uses a regular expression to keep only the part after the last slash.

5

Enclosed expression for starting the processing at the root element node.

6

An attribute is output as a space, the name of the attribute and its value between double quotes after an equal sign. This sequence of nodes is within a call to concat so that no spurious spaces are inserted in the output.

7

An element node is a list-item.

8

The name of the element in bold, followed by the attributes using the function defined on line 33‑6.

9

If there are no children elements, i.e. it is a single text node, the textual content is inserted directly without adding a new list level.

10

If there are children nodes, elements are processed recursively and text nodes (in the case of mixed content) are processed with the text function.

11

If there are no children nodes, it is then a text node that is copied to the output.

12

In order to produce a legal XHTML document, all elements must be in an appropriate namespace. Because in XQuery the default global default namespace declaration also applies to the XPath expressions on the input tree, it is not possible to query a tree in the default namespace and have the output tree constructor into another. The output is first created by the transformation in the empty namespace. This result is then copied but with nodes into the XHTML namespace.

13

Calls the transformation on the cellar-book element and copies the result into the XHTML namespace.