XRX: Performing Updates

By Jeni Tennison
July 18, 2008 | Comments: 8

This is part 3 of a series in which I’m trying to implement a RESTful social bookmarking web service using the XRX (XForms, REST, XQuery) architecture. I’ve covered setting up queries in eXist and using Orbeon to expose those queries with nice URLs in the previous parts. This time I’m going to look at how to handle PUT and POST to make a read/write web service.

As before, I’m going to look at the eXist level of the application first. When I was looking at a read-only service, I used stored queries to retrieve the information I wanted from the XML database in the form I wanted it. What can I do for a read/write service? Well, it turns out that you can use stored queries for that as well.

Using XQuery to change the XML stored in a database seems a bit weird to me. I tend to think of XQuery as a simple form of XSLT with a different syntax, and one of the fundamental building blocks of XSLT’s design is that it has no side-effects. But updating data using XQuery is accepted practice — there’s even a spec for it at Candidate Recommendation — so I will shrug off my preconceptions!

I’m going to change my bookmark.xq so that it handles POSTs of <bookmark> elements. This can update or create a bookmark. If there’s an existing bookmark with the given URI, then the bookmark is updated, and the updated bookmark gets returned. If there’s no existing bookmark with that URI, then a new bookmark is created and returned. If a hash parameter is provided, then it must match the URI of the bookmark. A GET will do exactly what it always did, namely return the requested bookmarks. (I can’t support PUT or DELETE because PUTting or DELETEing to the query URI will replace or delete the query itself, quite rightly.)

So there are a couple of inputs into this query that there weren’t before: the HTTP method that’s being used to access the query, and the XML document that’s the request entity. (I’m not messing around with application/x-www-form-urlencoded or multipart/form-data. You want to update the bookmarks through a web service, you supply the XML!) To get the method and the request entity, I use a couple of specialised functions:

declare variable $method as xs:string := request:get-method();
declare variable $data as node()? := request:get-data();

There’s one other piece of preparation before launching into the query itself. What I’d like to do is return different status codes based on what happens. So if the bookmark is updated, I’d return a 200 OK response; if a new bookmark’s created, I’d return a 201 Created response; and if the hash doesn’t match the URI then I’d return a 400 Bad Request response. Unfortunately, although there’s a response:set-status-code() function in the eXist function library, it’s not supported in the version of eXist that I’ve got running, so I’m not going to bother doing that for now. Instead, I’m going to put a http:status attribute on the document element of the response. That way, Orbeon will be able to pick up the correct status further down the line.

I’ll do this with a couple of functions. Well, one function with two variants. Here it is:

declare function bk:respond($status as xs:integer, $response as element()) as element() {
  element { $response/node-name(.) } {
    attribute http:status { $status },
    $response/@*, $response/node()
  }
};
declare function bk:respond($status as xs:integer) as element() {
  let $message as xs:string :=
        if ($status eq 400) then 'Bad Request'
        else if ($status eq 405) then 'Method Not Allowed'
        else ''
  return
    bk:respond($status, 
               <html>
                 <head><title>{$message}</title></head>
                 <body><p>{$message}</p></body>
               </html>)
};

The first function declaration is for a bk:respond function that takes a status code and the actual response (an element) and adds an http:status attribute to the element. (I added the line declare namespace http = "http://www.example.org/http"; to the top of the query, by the way.) The second function declaration is for the same function, but is a one-argument version. If only one argument is used, a little HTML document is created that’s relevant to the status code. There are nicer ways to do this, but it’ll do for now.

OK, so on to handling the query. First, GET requests are handled just as they were before:

if ($method eq 'GET') then
  if ($user eq '') then
    <users xmlns="http://www.example.org/bookmarks">{
      for $u in $bookmarks/bk:user,
          $b in $u/bk:bookmarks/bk:bookmark[@hash = $hash]
      order by xs:dateTime($b/@date) descending
      return <user>{$u/bk:name, $b}</user>
    }</users>
  else 
    let $user-bookmarks as element(bk:bookmarks)? := 
          $bookmarks/bk:user[bk:name = $user]/bk:bookmarks
    return 
      if ($hash = '') then $user-bookmarks
      else $user-bookmarks/bk:bookmark[@hash = $hash]

Then POST requests are handled with this code:

else if ($method eq 'POST') then
  if ($user eq '') then
    bk:respond(405)
  else
    let $user-bookmarks as element(bk:bookmarks)? :=
          $bookmarks/bk:user[bk:name = $user]/bk:bookmarks,
        $bookmark-hash as xs:string := util:md5($data/@href)
    return
      if ($hash eq '' or $hash eq $bookmark-hash) then
        let $current-bookmark as element(bk:bookmark)? :=
              $user-bookmarks/bk:bookmark[@hash = $bookmark-hash],
            $new-bookmark as element(bk:bookmark) :=
              <bookmark xmlns="http://www.example.org/bookmarks"
                        hash="{$bookmark-hash}"
                        date="{current-dateTime()}">{
                $data/@href, $data/*
              }</bookmark>
        return
          if ($current-bookmark) then
            (update replace $current-bookmark with $new-bookmark,
             bk:respond(200, $new-bookmark))
          else
            (if ($user-bookmarks/bk:bookmark) then
               update insert $new-bookmark preceding $user-bookmarks/bk:bookmark[1]
             else
               update insert $new-bookmark into $user-bookmarks,
             bk:respond(201, $new-bookmark))
      else
        bk:respond(400)

You can only update bookmarks for a known user, so a $user has to be supplied or a 405 Method Not Allowed response is given. Then I look up the user’s bookmarks ($user-bookmarks), and calculate the hash for the bookmark that’s been passed in as the request entity ($bookmark-hash) with the very helpful eXist extension function util:md5() (the util prefix is declared with declare namespace util = "http://exist-db.org/xquery/util";).

If a hash has been passed to the query (through a request parameter), and it isn’t the same as this calculated hash, then I generate a 400 Bad Request response. Otherwise, I see if there’s already a bookmark with the hash ($current-bookmark) and create the XML for the new bookmark including the calculated hash and a date stamp ($new-bookmark). If there is a current bookmark, then I replace it and respond with a 200 OK status code; otherwise I insert the new bookmark into the user’s XML and respond with a 201 Created status code.

If you know anything about XQuery Updates you’ll notice that the update instructions don’t use the same syntax as XQuery Updates. They’re a special eXist syntax. But they do map readily on to XQuery Updates. Replacing the existing bookmark using:

update replace $current-bookmark with $new-bookmark

would be done with the XQuery syntax:

replace $current-bookmark with $new-bookmark

and inserting the new bookmark into the user’s bookmarks with:

if ($user-bookmarks/bk:bookmark) then
    update insert $new-bookmark preceding $user-bookmarks/bk:bookmark[1]
else
    update insert $new-bookmark into $user-bookmarks

would be done with:

insert node $new-bookmark as first into $user-bookmarks

which is a whole load neater.

Finally, if another method is being used, then I return a 405 (Method Not Allowed) response.

else
  bk:respond(405)

Time for some testing. I create a basic bookmark document called test.xml like this:

<bookmark xmlns="http://www.example.org/bookmarks" 
  href="http://www.w3.org/TR/xquery-update-10/">
  <title>XQuery Update Facility 1.0</title>
  <notes>New syntax for updating documents</notes>
  <tag>xquery</tag>
  <tag>update</tag>
</bookmark>

and use curl to try out the query. First, POSTing the document (which is a new bookmark):

curl -T test.xml -X POST \
http://localhost:8080/exist/rest/db/orbeon/bookmarks/queries/bookmark.xq?user=drench

and the response is

<bookmark xmlns:http="http://www.example.org/http" 
          xmlns="http://www.example.org/bookmarks" 
          http:status="201" 
          hash="83b6d11123f9291b441e55f8b59a6956" 
          date="2008-07-18T21:50:44.77+01:00" 
          href="http://www.w3.org/TR/xquery-update-10/">
    <bk:title xmlns:bk="http://www.example.org/bookmarks">XQuery Update Facility 1.0</bk:title>
    <bk:notes xmlns:bk="http://www.example.org/bookmarks">New syntax for updating documents</bk:notes>
    <bk:tag xmlns:bk="http://www.example.org/bookmarks">xquery</bk:tag>
    <bk:tag xmlns:bk="http://www.example.org/bookmarks">update</bk:tag>
</bookmark>

If I now GET the URL

http://localhost:8080/exist/rest/db/orbeon/bookmarks/queries/bookmark.xq?user=drench&hash=83b6d11123f9291b441e55f8b59a6956

I get the same XML back, minus the http:status attribute, showing that the bookmark has indeed been added to the user’s bookmark file. (In fact, I can look at the file itself through eXist’s WebDAV or administration interfaces and confirm this, but GETting the bookmark through the query proves that GET still works.)

Now I can try POSTing that file again. This time I get something slightly different back, namely an 200 OK status code rather than a 201 Created status code:

<bookmark http:status="200" ...>
  ...
</bookmark>

which shows that it’s recognised that the bookmark’s already present. Again, looking at the file I can see that the bookmark’s entry’s been replaced rather than replicated.

Now let me try POSTing it with the wrong hash:

curl -T test.xml -X POST \
"http://localhost:8080/exist/rest/db/orbeon/bookmarks/queries/bookmark.xq?user=drench&hash=18683e4948941b990b5f3d2486ecb030"

The response is:

<html xmlns:http="http://www.example.org/http" http:status="400">
    <head>
        <title>Bad Request</title>
    </head>
    <body>
        <p>Bad Request</p>
    </body>
</html>

Similarly, POSTing to a URL that lacks a username:

curl -T test.xml -X POST \
http://localhost:8080/exist/rest/db/orbeon/bookmarks/queries/bookmark.xq

gives:

<html xmlns:http="http://www.example.org/http" http:status="405">
    <head>
        <title>Method Not Allowed</title>
    </head>
    <body>
        <p>Method Not Allowed</p>
    </body>
</html>

OK, so I’ve now got a query that, in a little less than 85 lines manages all the bookmark-related fetching and updating.

It occurred to me while writing this that it would be much easier to change the way the eXist collections were structured so that the individual bookmarks were equivalent to individual files. That way, eXist’s built-in REST interface would support all the different HTTP methods (including PUT and DELETE) out-of-the-box. But I’ve stuck with this set-up because it’s not always desirable to pull apart XML documents. It doesn’t particularly apply to this application, but technical documentation, legislation or other non-data-oriented material doesn’t shred nicely. Stored queries could be used with any kind of content, so they’re a useful technique to know about.

Anyway, I’ve still got some way to go: putting a good URL interface on this using Orbeon, supporting DELETE on the bookmarks, authentication and authorisation and so on. More next time…


You might also be interested in:


8 Comments

You've made excellent use of your weekend! Thank you.

I'm trying to keep up with you (different data, simpler use case) and so far, so good.

Great article! I like the way you are faithful to the use of the correct HTTP response codes.

Nice article , I really appreciate your way of faithful use of web services in xforms concept . Keep it up

I am trying to keep up here and so far so good!

I can not really see the content is there an error or something?

Lee

I am getting some type of odd error also?

Thank you for the post guys.

Peter

Thanks this was very informative.

Heather

Popular Topics

Archives

Or, visit our complete archives.

Recommended for You

Got a Question?