Tutorial: Mobile Shakespeare (Part 1)

Dec 10, 2011    

The first MarkLogic tutorial I ever followed was Clark Richey’s Shakespeare hands-on, hosted from the MarkLogic developer website back in the MarkLogic Server 4.0 days.  I think it is only fitting / nostaligic that the first tutorial I write for Front2BackDev.com be a similar app but with a new twist.  Let’s make an application that helps a user access the complete works of Shakespeare, but let’s use some new tech, in MarkLogic and otherwise, that wasn’t available back in 2009 such as:

  • The MarkLogic URL rewriter:  I’m going and Nuno Job’s rewrite library to implement a REST friendly URL scheme.
  • Information Studio: loading the complete works of Shakespeare (in XML form) into a database should take us no more than a few minutes.  Because we don’t really have to do any upfront data modeling in MarkLogic, we often take a “Load First .. Damn the Schemas full speed ahead!” mentality that should be refreshing to even the most jaded Agile developer.
  • In later installments of this tutorial I am going to use the MarkLogic Search API to build some quick but professional search functionality … we’ll get to this later.
  • And lastly rather than building an XHTML website with XQuery we’re going to take a look at JQuery Mobile, which takes a “markup only” approach to developing mobile web apps that have the same look and feel of a native iOS app.

This is what we are going to build: BROKEN LINK TO OLD DEMO

Getting started:

  • Install MarkLogic 5 ( Install Guide ).  This guide assumes you’ll be working from a developer machine that is running your MarkLogic Server.
  • From Application Services create a new database called “shake”
  • Application Services is located at http://localhost:8000/appservices/
  • At the top of the screen there is a “+ New Database” button.  Use it to create a new database called “shake”

  • Download the complete works of Shakespeare from http://www.ibiblio.org/xml/examples/shakespeare/ to a folder on your desktop called “shake”
  • Use Application Services’ Information Studio to load the Shakespeare plays into the “shake” database
  • On the Application Services screen click the “+ New Flow”  button in the “Information Studio Flows” area
  • In the new flow, type the absolute file system path to the “shake” folder on your desktop in the “Collect” section with a “Filesystem Directory” collector.
  • Upload the files by selecting the “shake” database in the “Load” section and clicking “Start Loading”
  • 37 files … should take a few seconds.

  • Check out the files you uploaded in Query Console
  • Go to http://localhost:8002/qconsole
  • Change the content source to “shake” and hit the explore button
  • Verify the shakespeare plays are there
  • Run a sample query like (/PLAY)1/TITLE/text()

  • Open your favorite code editor (I use the Eclipse IDE with the XQDT plugin).
  • Create a new project (the general “Project” in Eclipse) called shake, and create a new folder within that project called “xquery”
  • In the new project create a file located at /xquery/index.xqy with the content
 "Hello World!"
  • Back in the admin console, create a new HTTP server on port 9001.  Set it’s database to “shake”, modules to , and root to the absolute path of the project/xquery folder you made.  For me this is /Users/dave/Documents/workspace/shake/xquery
  • Open up http://localhost:9001 in a browser and confim the hello world is working
Okay now for some quick REST URL design:
  • / will be our home page.  We’ll list all the plays here.
  • /play/1 should bring up the Shakespeare play with id “1”
  • /play/1/characters should display all the characters in the play with id “1”
  • /play/1/act/1/scene/1 should bring up act 1 scene 1 of play “1”
But wait, the XML for the Shakespeare plays doesn’t have an “id”.  That’s cool, add one quickly with Query Console.  There isn’t a lot of data, so we can do it in a single transaction:
for $p at $id in /PLAY
let $at := attribute id {$id}
return
xdmp:node-insert-child($p,$at)
We’ll now be editing the shake project.  So as not to confuse anyone, here is the file layout of my shake project when this tutorial is finished:
Directory layout for shake project
Okay, now we are ready to program the REST handler.   Download the rewrite library from github.  In the Admin Console (port 8001) set the “url rewriter” setting of your HTTP server to “/rewrite.xqy” .  Now place the rewrite files in the following organization inside the project
/xquery/
/xquery/lib/
/xquery/lib/rewrite/
/xquery/lib/rewrite/helper.xqy
/xquery/lib/rewrite/routes.xqy

To implement the REST rules, I prefer using Nuno Job’s rewrite library, which has a Ruby on Rails-like routes specification for redirecting pattern based URLs to “controller” resource modules. Create the file /xquery/config.xqy with the following content:

xquery version "1.0-ml" ;

(:  config.xqy
    This library module holds configuration
    variables for the application
:)

module  namespace cfg = "http://framework/lib/config";

(:  The rewrite library route configuration
    For documentation see: https://github.com/dscape/rewrite
:)
declare variable $ROUTES :=
    <routes>
        <root>home#get</root>
        <get path="play/:id">
          <to>play#get</to></get>
        <get path="play/:id/characters">
          <to>play#characters</to></get>
        <get path="play/:id/act/:act">
          <to>play#get</to></get>
        <get path="play/:id/act/:act/scene/:scene">
          <to>play#get</to></get>
    </routes>;

declare variable $TITLE := "Mobile Shakespeare";

Next we need to create the /xquery/rewrite.xqy file which will select resource “controllers” based on the ROUTES configuration.  This is the script that is called by MarkLogic for every incoming HTTP request to determine which main module should be called.  We configured it when we set the “url rewriter” setting in the Admin Console:

xquery version "1.0-ml" ;

import module namespace shake-r =
    "routes.xqy" at "/lib/rewrite/routes.xqy";
import module namespace shake =
    "http://framework/lib/config" at "/lib/config.xqy";

    (: let rewrite library determine destination URL,
       use routes configuration in config lib :)
    let $selected-url    :=
            shake-r:selectedRoute( $shake:ROUTES )
    return
            $selected-url

Now to get fancy with some JQuery Mobile.  First in a file located at /xquery/view/v-mob.xqy inside the project create a new MVC “view” template using the JQuery Mobile framework.

xquery version "1.0-ml";

module namespace v-mob = "http://framework/view/v-mob";

(:  Stick this at the top of any module that generates HTML so that
    empty tags don't get truncated to non-empty tags :)
declare option xdmp:output "method=html";

(:
    HTML5 Template using JQuery Mobile
    @author Dave http://www.front2backdev.com
    XQuery adaptation Public Domain.
        jQuery and jQuery Mobile: MIT/GPL license
:)

(:
    JQuery Mobile Output Template
    $title -- The html head title of the page
    $html  -- HTML5 nodes to put in the body
:)
declare function v-mob:render(
    $title as xs:string,
    $html as node()*
) {

xdmp:set-response-content-type("text/html"),
'<!DOCTYPE html>',
<html>
    <head>
        <title>{$title}</title>
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="stylesheet" href="http://code.jquery.com/mobile/1.0/jquery.mobile-1.0.min.css" />
        <script type="text/javascript" src="http://code.jquery.com/jquery-1.6.4.min.js"></script>
        <script type="text/javascript" src="http://code.jquery.com/mobile/1.0/jquery.mobile-1.0.min.js"></script>
    </head>
    <body> 

        <div data-role="page" data-theme="b">

            <div data-role="header">
                <a data-rel="back"
                   data-icon="back"
                   data-iconpos="notext"
                   data-transition="slide"
                   data-direction="reverse">Back</a>
                <h1>{$title}</h1>
                <a href="/"
                   data-icon="home"
                   data-iconpos="notext"
                   data-transition="fade">Home</a>
            </div><!-- /header -->

            <div data-role="content">
                {$html}
            </div><!-- /content -->

        </div><!-- /page -->

    </body>
</html>
};

Next create a MVC “controller” for the home resource  in a file located at /resource/home.xqy with the content :

xquery version "1.0-ml";

(: Home.  List all Shakespeare plays :)

import module namespace cfg = "http://framework/lib/config" at "/lib/config.xqy";

import module namespace h = "helper.xqy" at "/lib/rewrite/helper.xqy";
import module namespace v-mob = "http://framework/view/v-mob" at "/view/v-mob.xqy";

(: Don't forget to include this so script
   tags in the VIEW are not collapsed :)
declare option xdmp:output "method=html";

declare function local:get()  {
    v-mob:render(
        $cfg:TITLE,
        <ul data-role="listview" data-inset="true" data-filter="true">
        {
            (: Note this code is inefficient at scale because we
               are effectively retrieving the entire database
               to generate these links.  It's only about 8MB of data
               and it is coming out of cache, but maybe we can speed
               it up later
            :)
            for $play in /PLAY
            return
                <li>
                  <a href="/play/{fn:string($play/@id)}">
                    {$play/TITLE/text()}
                  </a>
                </li>
        }
        </ul>
    )
};

try          { xdmp:apply( h:function() ) }
catch ( $e ) {  h:error( $e ) }

So once you are good with that resource, here is another more complex one to digest.  Lastly create a MVC “controller” for the play resource  in a file located at /resource/play.xqy with the following content (Sorry this one is kind-of long, but it is doing most of the work.  We can refactor the code into some Model and Util Library code later) :

xquery version "1.0-ml";

(: Play detail  :)

import module namespace cfg = "http://framework/lib/config" at "/lib/config.xqy";

import module namespace h =
    "helper.xqy" at "/lib/rewrite/helper.xqy";
import module namespace v-mob =
    "http://framework/view/v-mob" at "/view/v-mob.xqy";

(: Don't forget to include this so script
   tags in the VIEW are not collapsed :)
declare option xdmp:output "method=html";

declare function local:characters()  { 

    let $id := h:id()
    let $play := /PLAY[@id eq $id]
    let $title := fn:string-join((
            $play/TITLE/text(),
            "Characters"
        )," ")
    return
    v-mob:render(
        $cfg:TITLE,
            <p>
                <h2>{$title}</h2>
                <h3>DRAMATIS PERSONAE</h3>
                {
                for $node in $play/PERSONAE/node()
                return
                    typeswitch ($node)
                    case element(TITLE) return
                        ()
                    case element(PGROUP) return
                        <ul data-role="listview"  data-inset="true">
                        {
                            for $p in $node/PERSONA
                            let $name := $p/text()
                            return
                                <li data-theme="d">{$name}</li>,
                            for $d in $node/GRPDESCR
                            return
                                <li data-theme="c">{$d/text()}</li>
                        }
                        </ul>
                    case element(PERSONA) return
                    <ul data-role="listview"  data-inset="true">
                        {
                            let $p := $node
                            let $name := $p/text()
                            return
                                <li data-theme="d">{$name}</li>
                        }
                        </ul>
                    default return
                        ()
                }
            </p>
    )
};

declare function local:get()  {
    let $id := h:id()
    let $act := xdmp:get-request-field("act",())[1]
    let $scene := xdmp:get-request-field("scene",())[1]

    let $act := if($act castable as xs:int) then
                  xs:int($act)
                else ()
    let $scene := if($scene castable as xs:int) then
                    xs:int($scene)
                  else ()

    let $play := /PLAY[@id eq $id]

    return
    v-mob:render(
        $cfg:TITLE,
        (
            <p>
                <h2>{$play/TITLE/text()}</h2>
                {
                    if($act) then
                      <h3>{($play/ACT)[$act]/TITLE/text()}</h3>
                    else (),

                    if($act and $scene) then
                      <h3>
                        {(($play/ACT)[$act]/SCENE)[$scene]/TITLE/text()}
                      </h3>
                    else (),

                    if(fn:not( $act or $scene )) then
                        <ul data-role="listview" data-inset="true" >
                          <li>
                            <a href="/play/{$id}/characters">
                              Characters
                              <span class="ui-li-count">
                                {fn:count($play/PERSONAE/PERSONA)}
                              </span>
                            </a>
                          </li>
                        </ul>
                    else
                      ()
                }
            </p>,

            if($act and $scene) then (
                let $paging :=
                    <div data-role="controlgroup" data-type="horizontal">
                    {
                        if($scene eq 1 and $act eq 1) then
                            ()
                        else if( $scene eq 1 and fn:exists( ($play/ACT)[$act -1] )) then
                            <a data-icon="arrow-l" href="/play/{$id}/act/{$act -1}/scene/{ fn:count(($play/ACT)[$act -1]/SCENE) }" data-role="button">Previous Scene</a>
                        else
                            <a data-icon="arrow-l"   href="/play/{$id}/act/{$act}/scene/{$scene -1}" data-role="button">Previous Scene</a>,

                        <a  data-icon="grid" href="/play/{$id}" data-role="button">Back to Scene Selection</a>,    

                        if( fn:count(($play/ACT)[$act]/SCENE) gt $scene ) then
                            <a  data-icon="arrow-r"  href="/play/{$id}/act/{$act}/scene/{$scene +1}" data-role="button">Next Scene</a>
                        else if( fn:exists( ($play/ACT)[$act +1] )) then
                            <a  data-icon="arrow-r"  href="/play/{$id}/act/{$act +1}/scene/1" data-role="button">Next Scene</a>
                        else
                            ()
                    }
                    </div>
                return (

                    $paging,

                    for $node in (($play/ACT)[$act]/SCENE)[$scene]/node()
                    return
                        typeswitch ($node)
                        case element(TITLE) return
                            <p><strong>{$node/text()}</strong></p>
                        case element(STAGEDIR) return
                            <p><em>{$node/text()}</em></p>
                        case element(SPEECH) return
                            <div class="ui-body ui-body-b"
                                 style="margin:20px 0px;">
                            {
                                $node/SPEAKER/text(),
                                <div style="margin-left:20px;">{
                                    for $line in $node/LINE/text()
                                    return
                                        <div>{$line}</div>
                                }
                                </div>
                            }

                            </div>
                        default return
                            ()

                    ,

                    $paging
                )

            )
            else
                <ul data-role="listview"  data-inset="true" >
                {
                    for $act at $a in $play/ACT
                    return
                    (
                        <li data-role="list-divider">{$act/TITLE/text()}</li>,

                        for $scene at $s in $act/SCENE
                        return
                            <li data-theme="c">
                                <a href="/play/{$id}/act/{$a}/scene/{$s}">
                                    {$scene/TITLE/text()}
                                </a>
                            </li>
                    )
                }
                </ul>
        )
    )
};

try          { xdmp:apply( h:function() ) }
catch ( $e ) {  h:error( $e ) }

And now for our hard earned screenshots.  In the next part of the tutorial we’ll add a real search page to search within the text of the plays.

A live demo of the app so far can be found at: LINK TO OLD DEMO BROKEN

Part 2 of this tutorial can be found here.

Updates:

12/16/2011 – links to apps changed
12/18/2011 – adding reminder to set rewriter in HTTP app server configuration