XQuery/Transformation idioms

From Wikibooks, open books for an open world
Jump to: navigation, search

Motivation[edit]

Document transformation using the basic typeswitch statement applies the same transformation to an element independent of where it occurs in the document. The transformation also preserves document order since it processes elements in document order. In comparison with XSLT, XQuery lacks some mechanisms such as modes, priority and numbering. This article addresses some of these limitations.

Example[edit]

The example uses a custom XML schema to markup the contents of the book "Search: The Graphics Web Guide", Ken Coupland, a compendium of websites. This document is formatted with a site-specific schema. The document contains site elements which are tagged with a category, and also category elements which provide a commentary on the category. For comparison this dataset is used in a student case study which uses XSLT for transformations.

[Sample file]

Identity Transformation[edit]

module namespace coupland = "http://www.cems.uwe.ac.uk/xmlwiki/coupland";
(: conversion module generated from a set of tags 
 
:)
 
declare function coupland:convert($nodes as node()*) as item()* {
  for $node in $nodes
  return 
     typeswitch ($node)
       case element(websites) return coupland:websites($node)
           case element(sites) return coupland:sites($node)
           case element(site) return coupland:site($node)
           case element(uri) return coupland:uri($node)
           case element(name) return coupland:name($node)
           case element(description) return coupland:description($node)
 
       default return 
         coupland:convert-default($node)
  };
 
declare function coupland:convert-default($node as node()) as item()* {
  $node
  };
 
declare function coupland:websites($node as element(websites)) as item()* {
  element websites{
     $node/@*,
     coupland:convert($node/node()) 
     }
};
 
declare function coupland:sites($node as element(sites)) as item()* {
  element sites{
     $node/@*,
     coupland:convert($node/node()) 
     }
};
 
declare function coupland:site($node as element(site)) as item()* {
  element site{
     $node/@*,
     coupland:convert($node/node()) 
     }
};
 
declare function coupland:uri($node as element(uri)) as item()* {
  element uri{
     $node/@*,
     coupland:convert($node/node()) 
     }
};
 
declare function coupland:name($node as element(name)) as item()* {
  element name{
     $node/@*,
     coupland:convert($node/node()) 
     }
};
 
declare function coupland:description($node as element(description)) as item()* {
  element description{
     $node/@*,
     coupland:convert($node/node()) 
     }
};

Customising the identity transformation[edit]

The module code is only a basic skeleton which we would edit to customize the transformation. In this example we will transform the document to HTML. This will require editing a number of the element converters.


Default action[edit]

Change the compare-default function to provide a different default action. For example:

  declare function coupland:convert-default ($node)  {
   if ($node instance of element())
   then coupland:convert($node/node())
   else $node
};

would include the content of the node but remove the tag and its attributes.

Change element name[edit]

Site descriptions will be rendered as divs:

  declare function coupland:description($node as element(description)) as item()* {
  element div{
     $node/@*,
     coupland:convert($node/node()) 
     }
};

Ignore element[edit]

The 'class' element is not needed:

declare function coupland:class($node as element(class)) as item()* {
  ()
};

Define transformation[edit]

The image element should be transformed to an html img elment using the uri as the source:

declare function coupland:image($node as element(image)) as item()* {
  element div {
    element img {
     attribute src { $node}
    }
  }
};

Transformation depends on context[edit]

By default all elements with the same name anywhere in the document are transformed in the same way. Often this is not what is required:

declare function coupland:name($node as element(name)) as item()* {
  if ($node/parent::node() instance of element(site))
  then 
    element h3{
     $node/@*,
     coupland:convert($node/node()) 
     }
  else 
    element h1{
     $node/@*,
     coupland:convert($node/node()) 
     }
};

Reordering elements[edit]

Each site is to be rendered in the order name, uri and then the rest of the sub-elements:

declare function coupland:site($node as element(site)) as item()* {
  element div{
     element div { 
        coupland:convert($node/name),
        coupland:convert($node/uri)
       } ,
     coupland:convert($node/(node() except (uri,name)))
     }
};

Numbering categories[edit]

The xsl:number instruction provides a mechanism to generate hierarchical section numbers. This instruction is very powerful. In specific cases we can generate numbers using functions.

For example to number the categories we can use this function to create a number for a node in a sequence of siblings. Note that the number is based on the order of nodes in the original document, not the transformed document (as does xsl:number) .

declare function coupland:number($node) as xs:string {
     concat(count($node/preceding-sibling::node()[name(.) = name($node)]) + 1,". ")
};

and call this function when transforming category names:

 element h2{
     $node/@*,
     coupland:number($node/parent::node()), 
     coupland:convert($node/node())

Parameterisation[edit]

The transformation can clearly be applied to different documents, but often the same transformation is to be used in different contexts. XSLT provides parameters and variables which are global to all templates.

In XQuery we can either declare global variables in the module or pass one or more parameters around the functions ( module generation is helpful here).

....

Generating an index[edit]

XSLT uses the mode mechanism to allow the same template to be processed in multiple ways. A common use case is where the same transformation must generate both an index and the content.

Several approaches suggest themselves. We could mimic the XSLT approach by passing an additional mode parameter in the calls and choose which transformation to apply in each function. Alternatively we append the mode to the function name. It is more difficult to use context (either global or passed) because the mode will need to be updated.

The simplest approach is to use use two typeswitch transformation and combine the results at a higher level. This clearly separates the two modes of transformation. The technique of module generation is helpful here.

Complex transformation[edit]

The overall HTML document can be structured in the transformer for the root element. The page uses the blueprint stylesheets. Each category of site is rendered, with the sites which are classified in that category.

declare function coupland:websites($node as element(websites)) as item()* {
(: the root element so convert to html :)
  <html>
     <head>
        <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
        <title>Web Sites by Coupland</title>
        <link rel="stylesheet" href="../../css/blueprint/screen.css" type="text/css" media="screen, projection"/>
        <link rel="stylesheet" href="../../css/blueprint/print.css" type="text/css" media="print"/>
        <!--[if IE ]><link rel="stylesheet" href="../../css/blueprint/ie.css" type="text/css" media="screen, projection" /><![endif]-->
        <link rel="stylesheet" href="screen.css" type="text/css" media="screen"/>
     </head>
     <body>
       <div class="container">
       {
        for $category in $node/category
        order by $category/class
        return
          <div>
            <div class="span-10">
              {coupland:convert($category)}
            </div>
            <div class="span-14 last">
              {for $site in $node/sites/site[category=$category/class]
               order by ($site/sortkey,$site/name)[1]
               return
                 coupland:convert($site)
              }
            </div>
            <hr />
          </div>
        }
        </div>  
      </body>
   </html>
};

Completed transformation[edit]

The full XQuery module now looks like this:

module namespace coupland = "http://www.cems.uwe.ac.uk/xmlwiki/coupland";
(: conversion module generated from a set of tags 
 
:)
 
declare function coupland:convert($nodes as node()* as node()?) as item()* {
  for $node in $nodes
  return 
     typeswitch ($node)
       case element(category) return coupland:category($node)
           case element(class) return coupland:class($node)
           case element(description) return coupland:description($node)
           case element(em) return coupland:em($node)
           case element(hub) return coupland:hub($node)
           case element(image) return coupland:image($node)
           case element(name) return coupland:name($node)
           case element(p) return coupland:p($node)
           case element(q) return coupland:q($node)
           case element(site) return coupland:site($node)
           case element(sites) return coupland:sites($node)
           case element(sortkey) return coupland:sortkey($node)
           case element(subtitle) return coupland:subtitle($node)
           case element(uri) return coupland:uri($node)
           case element(websites) return coupland:websites($node)
 
       default return 
         coupland:convert-default($node)
  };
 
declare function coupland:convert-default($node as node() as node()?) as item()* {
  $node
  };
 
declare function coupland:category($node as element(category) as node()?) as item()* {
  if ($node/parent::node() instance of element(site))
  then ()
  else 
    element div{
     $node/@*,
     coupland:convert($node/node()) 
    }
};
 
declare function coupland:class($node as element(class) as node()?) as item()* {
  ()
};
 
declare function coupland:description($node as element(description) as node()?) as item()* {
  element div{
     $node/@*,
     coupland:convert($node/node()) 
     }
};
 
declare function coupland:em($node as element(em) as node()?) as item()* {
  element em{
     $node/@*,
     coupland:convert($node/node()) 
     }
};
 
declare function coupland:hub($node as element(hub) as node()?) as item()* {
  element hub{
     $node/@*,
     coupland:convert($node/node()) 
     }
};
 
declare function coupland:image($node as element(image) as node()?) as item()* {
  element div {
    element img {
     attribute src { $node}
    }
  }
};
 
declare function coupland:name($node as element(name) as node()?) as item()* {
  if ($node/parent::node() instance of element(site))
  then 
    element span {
     attribute style {"font-size: 16pt"},
     $node/@*,
     coupland:convert($node/node())
     }
  else 
    element h1{
     $node/@*,
     coupland:number($node/parent::node()), 
     coupland:convert($node/node()) 
     }
};
 
declare function coupland:p($node as element(p) as node()?) as item()* {
  element p{
     $node/@*,
     coupland:convert($node/node()) 
     }
};
 
declare function coupland:q($node as element(q) as node()?) as item()* {
  element q{
     $node/@*,
     coupland:convert($node/node()) 
     }
};
 
declare function coupland:site($node as element(site) as node()?) as item()* {
  element div{
     element div { 
        coupland:convert($node/name),
        coupland:convert($node/uri)
       } ,
     coupland:convert($node/(node() except (uri,name)))
     }
};
 
declare function coupland:sites($node as element(sites) as node()?) as item()* {
    for $site in $node/site
    order by $node/sortkey
    return 
       coupland:convert($node/site) 
};
 
declare function coupland:sortkey($node as element(sortkey) as node()?) as item()* {
  ()
};
 
declare function coupland:subtitle($node as element(subtitle) as node()?) as item()* {
  element div{
     $node/@*,
     coupland:convert($node/node()) 
     }
};
 
declare function coupland:uri($node as element(uri) as node()?) as item()* {
  <span>
    {element a{
     attribute href {$node },
     "Link"
     }
    }
  </span>
};
 
declare function coupland:websites($node as element(websites) as node()?) as item()* {
(: the rot element so convert to html :)
  <html>
     <head>
        <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
        <title>Web Sites by Coupland</title>
        <link rel="stylesheet" href="../../css/blueprint/screen.css" type="text/css" media="screen, projection"/>
        <link rel="stylesheet" href="../../css/blueprint/print.css" type="text/css" media="print"/>
        <!--[if IE ]><link rel="stylesheet" href="../../css/blueprint/ie.css" type="text/css" media="screen, projection" /><![endif]-->
        <link rel="stylesheet" href="screen.css" type="text/css" media="screen"/>
     </head>
     <body>
       <div class="container">
       {
        for $category in $node/category
        order by $category/class
        return
          <div>
            <div class="span-10">
              {coupland:convert($category)}
            </div>
            <div class="span-14 last">
              {for $site in $node/sites/site[category=$category/class]
               order by ($site/sortkey,$site/name)[1]
               return
                 coupland:convert($site)
              }
            </div>
            <hr />
          </div>
        }
        </div>  
      </body>
   </html>
};

Transformed to HTML.