XRX: Mapping URLs with Orbeon Forms

By Jeni Tennison
July 6, 2008 | Comments: 4

This is part 2 of a series exploring implementing a RESTful social bookmarking web service using the XRX (XForms, REST, XQuery) architecture. In the last part I looked at how to set up eXist with a bunch of XML and some queries that provided flexible access to that XML. In this part, I’ll look at how to use Orbeon Forms to map good URLs onto those queries to get a reasonable read-only RESTful application.

The queries that I put together last time worked pretty well, but the URLs weren’t terribly pretty. Here’s an example that returns a given bookmark for a user:

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

The social bookmarking web service I’m using is modelled on del.icio.us as described in Chapter 7 of Leonard Richardson’s and Sam Ruby’s RESTful Web Services. There, the equivalent query is given a URL like:

http://localhost:8080/users/drench/bookmarks/d3346bda2344033cf77460a4ef7363d

I’m not going to get there in one step, but I will get there. My first, slightly easier, aim is to get to the URL:

http://localhost:8080/orbeon/bookmarks/users/drench/bookmarks/d3346bda2344033cf77460a4ef7363d8

To do this, I need to create a folder within Orbeon’s $ORBEON_HOME/WEB-INF/resources/apps folder called bookmarks, and then create a few files that will configure the bookmarks application. In this case, when Orbeon receives the URL above, I want to respond with the result of querying the eXist database with the URL that I know already works.

The central part of this process is the page flow for the bookmarks application.Page flows in Orbeon are controllers that determine a model and a view and return the results. Or, page flows define how HTTP requests get mapped on to responses.

I just want to return XML for the moment, so the view that I’m going to use is a really simple identity transformation, like this:

<xsl:stylesheet version="2.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:template match="/">
  <xsl:copy-of select="." />
</xsl:template>

</xsl:stylesheet>

This stylesheet just copies the document wholesale. You can’t get much simpler than that. It’s going in $ORBEON_HOME/WEB-INF/resources/apps/bookmarks/view/identity.xsl.

The model, on the other hand, is the result of accessing the bookmark.xq query with the appropriate parameters, based on the URL itself. So there are two parts to this: parsing the URL to extract the values of the parameters, and accessing the query that will give us the results.

Let’s look at how to access the query first. The Orbeon developers recommend using an XForms <submission> element for accessing data at another URL. I think that’s because the XForms <submission> element provides a declarative way of describing an HTTP request. But it might just be because they love XForms. Either way, to point to XML held at another URL, I need to define a pipeline as follows:

<p:config xmlns:p="http://www.orbeon.com/oxf/pipeline"
  xmlns:oxf="http://www.orbeon.com/oxf/processors"
  xmlns:xforms="http://www.w3.org/2002/xforms">

  <p:param type="input" name="instance" />
  <p:param type="output" name="data" />

  <p:processor name="oxf:xforms-submission">
    <p:input name="submission">
      <xforms:submission method="get" separator="&amp;"
        action="/exist/rest/db/orbeon/bookmarks/queries/bookmark.xq"/>
    </p:input>
    <p:input name="request" href="#instance" />
    <p:output name="response" ref="data"/>
  </p:processor>

</p:config>

This pipeline is defined in Orbeon’s pipeline language (XPL), and I’ve put it in $ORBEON_HOME/WEB-INF/resources/apps/bookmarks/model/bookmark.xpl. I’m hoping that Real Soon Orbeon will migrate to XProc so it’s a bit more standard, but they’re probably waiting until we get somewhat closer to Recommendation.

Anyway, what’s this doing? Well, the <p:config> element is just the document element. The <p:param> elements define the input and output for the pipeline: in this case there’s one input (a document that describes the parameters to be passed to the query) and one output (the data that’s the result of the query).

This pipeline contains just one step, represented by the <p:processor> element. It’s an XForms submission that GETs the URL /exist/rest/db/orbeon/bookmarks/queries/bookmark.xq, appending the parameters described in the instance document, separated by ampersands (that’s the separator="&amp;" part). The response to that GET request becomes the output of the pipeline as a whole.

This pipeline will perform a GET request based on an XML document that’s passed as the input to the pipeline. So next we need to define the input to the pipeline. The way that parameters are passed into an XForms submission is through an XML document that looks like this:

<parameters>
  <user></user>
  <hash></hash>
</parameters>

The <parameters> element is a wrapper element and the <user> and <hash> elements define individual parameters (the name of the element maps to the name of the parameter, in case you hadn’t gathered). If I used the document above, I’d be setting the user and hash parameters to an empty string; note that these names match the names of the parameters that I used in the bookmark.xq query. What I want is for the URL:

http://localhost:8080/orbeon/bookmarks/users/drench/bookmarks/d3346bda2344033cf77460a4ef7363d8

to produce an XML document that looks like:

<parameters>
  <user>drench</user>
  <hash>d3346bda2344033cf77460a4ef7363d8</hash>
</parameters>

The empty version of this parameters document will go at $ORBEON_HOME/WEB-INF/resources/apps/bookmarks/model/default-bookmark-query.xml, where it will act as a kind of template for the queries that address the bookmark.xq XQuery.

Orbeon can extract parts of a URL using a regular expression, using the substrings that match subexpressions within the regular expression to populate the elements in a template XML document, with code like this:

<config xmlns="http://www.orbeon.com/oxf/controller"
        xmlns:oxf="http://www.orbeon.com/oxf/processors">

  <page path-info="/bookmarks/users/([^/]+)/bookmarks(/([^/]+))?"
        matcher="oxf:perl5-matcher"
        default-submission="model/default-bookmark-query.xml"
        model="model/bookmark.xpl"
        view="view/identity.xsl">
    <setvalue ref="/parameters/user" matcher-group="1" />
    <setvalue ref="/parameters/hash" matcher-group="3" />
  </page>

  <page path-info="/bookmarks/uris/([^/]+)"
    matcher="oxf:perl5-matcher"
    default-submission="model/default-bookmark-query.xml"
    model="model/bookmark.xpl"
    view="view/identity.xsl">
    <setvalue ref="/parameters/hash" matcher-group="1" />
  </page>

  <epilogue url="oxf:/config/epilogue.xpl"/>

</config>

The <config> element is (again) a wrapper element, this time for a page flow (or controller). Each <page> element is used to match a particular URL pattern. The first one matches URLs like:

http://localhost:8080/orbeon/bookmarks/users/drench/bookmarks/d3346bda2344033cf77460a4ef7363d8

and

http://localhost:8080/orbeon/bookmarks/users/drench/bookmarks

while the second matches URLs like:

http://localhost:8080/orbeon/bookmarks/uris/d3346bda2344033cf77460a4ef7363d8

The matcher attribute indicates that the URLs in the path-info attributes should be interpreted as regular expressions. The default-submission attribute points to the template XML document. The model attribute points to the pipeline I defined to do the XForms submission. The view attribute just points to the identity transformation, because I want this service to just return the XML rather than HTML or Atom or XSL-FO or anything special.

The <epilogue> element tells Orbeon to use its normal epilogue pipeline to process the results. The epilogue pipeline sets the appropriate response headers (such as the Content-Type) and serialises the result in the right way. If the XML that your pipeline generates is an atom feed, for example, the default epilogue will set the content type to application/atom+xml and serialise the result as XML. If it’s XSL-FO, the default epilogue will convert that to PDF (using FOP) and return that with the content type application/pdf. Arbitrary XML is returned as arbitrary XML with an appliation/xml content type.

Now, when I access

http://localhost:8080/orbeon/bookmarks/users/drench/bookmarks/d3346bda2344033cf77460a4ef7363d8

Orbeon matches the URL and creates the XML document

<parameters>
  <user>drench</user>
  <hash>d3346bda2344033cf77460a4ef7363d8</hash>
</parameters>

which gets passed as the input into a pipeline which performs a GET on the URL:

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

and returns the XML from the database on the basis of that query, setting the Content-Type HTTP header to application/xml, among other things. The other URLs work similarly, just passing different (empty) values into the parameters to the XQuery URL.

So now we just have to get rid of the /orbeon/bookmarks part of the URL. Actually, the first part — /orbeon — is governed by Tomcat. There are a few ways of changing that, all of which is described in the Tomcat documentation. You can edit $CATALINA_HOME/conf/server.xml, which in my case is /usr/local/apache-tomcat-5.5.26/conf/server.xml, and change

<Host name="localhost" appBase="webapps"
 unpackWARs="true" autoDeploy="true"
 xmlValidation="false" xmlNamespaceAware="false">
    ...
</Host>

to

<Host name="localhost" appBase="webapps"
 unpackWARs="true" autoDeploy="false"
 xmlValidation="false" xmlNamespaceAware="false">

    <Context docBase="orbeon" path="" />
    ...
</Host>

Note that as well as adding the <Context> element, which tells Tomcat to use Orbeon at the root URI of the server space, I’ve changed the autoDeploy attribute to false, which stops it being deployed twice. You have to restart Tomcat to get this working.

Or (better, because it means you don’t have to restart Tomcat), you can create a $CATALINA_HOME/conf/[enginename]/[hostname]/ROOT.xml document which holds the context:

<Context docBase="orbeon" />

(assuming you have Orbeon installed within the webapps folder). You can also use an absolute path to the Orbeon installation, but only if it isn’t within the webapps folder.

With that done, the URL

http://localhost:8080/bookmarks/users/drench/bookmarks/d3346bda2344033cf77460a4ef7363d8

returns the bookmark XML I’m after. I’ve got rid of the /orbeon part, and no one need ever know that I’m using Orbeon rather than Ruby on Rails behind the scenes.

The rest of the URL is controlled by Orbeon itself. When you request a URL that’s dispatched to Orbeon, it uses its default page flow, defined at $ORBEON_HOME/WEB-INF/resources/page-flow.xml, to determine what to do with it. What I want to do is redirect any requests to the URLs that are relevant to the bookmark application to a page flow within the bookmarks directory.

The URLs that are covered by this application are

  1. /users/*
  2. /uris/*
  3. /posts/*
  4. /recent/*

and that’s it. All of these URIs can be redirected to the page-flow.xml in the bookmarks directory, by adding the following lines in the main page-flow.xml at $ORBEON_HOME/WEB-INF/resources/page-flow.xml:

<!-- Bookmark application -->
<page id="bookmark-users" path-info="/users/*" model="apps/bookmarks/page-flow.xml" />
<page id="bookmark-posts" path-info="/posts/*" model="apps/bookmarks/page-flow.xml" />
<page id="bookmark-recent" path-info="/recent/*" model="apps/bookmarks/page-flow.xml" />
<page id="bookmark-uris" path-info="/uris/*" model="apps/bookmarks/page-flow.xml" />

Note the position of these lines does matter: it needs to be before the generic Orbeon application matcher (id="apps"), otherwise requests to /users/* will get redirected to the page flow within the users folder and so on. And to get these revised URLs to work, I also need to change the bookmarks page flow a bit, just to adjust the paths that it’s expecting:

<page path-info="/users/([^/]+)/bookmarks(/([^/]+))?"
      matcher="oxf:perl5-matcher"
      default-submission="model/default-bookmark-query.xml"
      model="model/bookmark.xpl"
      view="view/identity.xsl">
  <setvalue ref="/parameters/user" matcher-group="1" />
  <setvalue ref="/parameters/hash" matcher-group="3" />
</page>

<page path-info="/uris/([^/]+)"
  matcher="oxf:perl5-matcher"
  default-submission="model/default-bookmark-query.xml"
  model="model/bookmark.xpl"
  view="view/identity.xsl">
  <setvalue ref="/parameters/hash" matcher-group="1" />
</page>

That’s it for the URL mapping. I now have a system in which the URIs I want to use are mapped into queries on the XML database. To do it, I’ve:

  • uploaded files into the XML database
  • constructed stored XQueries that retrieve interesting sets of data from the database
  • created pipelines that run appropriate queries based on their input documents and return some XML (model)
  • created simple template documents with default values for the query parameters
  • made a simple identity transformation to view XML as the original XML
  • created Orbeon page flows (controllers) to route URLs into the correct model and view
  • configured Tomcat to make Orbeon the default application

I still haven’t handled stuff like authentication/authorisation, or actually updating the XML database. Those will have to wait for now.


You might also be interested in:


4 Comments

Jeni,

This is incredibly useful information - and makes me realize that I need to spend more time working with Orbeon.

If you're working with eXist, you may also want to check out the Atom services. To enable them, you need to modify the eXist server.xml file:


<servlet enabled="no"
context="/atom/*"
class="org.exist.atom.http.AtomServlet"/>

needs to have the enabled attribute set to "yes".

This lets you use AtomPub services in order to publish content in feeds, something I've started working with as an alternative mechanism to using the REST interfaces. For instance,


http://myserver:8080/exist/atom/content/feeds/kurt

will retrieve an Atom feed with all posted content, in the feeds/kurt collection, while you can post an Atom feed or entry element to:


http://myserver:8080/exist/atom/edit/feeds/kurt

in order to add that entry (possibly with embedded XML content) to the feed.

I find that it can make set up a little more complex (and I'll be posting some articles myself shortly on the full details of AtomPub publishing with eXist) but the combination of the AtomPub and XRX tech is very compelling.

One final note - we're aware of the rather awkward formatted code output and are in the process of changing it.

-- Kurt

Great article that enthused me to try XRX with these tools. I knew XForms, but couldn't make any substantial progress until I saw the XPL and eXist guidance here. Thanks!

After playing with the example I made one small change. I used the new xxforms:get-request-parameter() function in Orbeon 3.7 to get parameters from the URL. This was much simpler, and allowed me to use traditional URLs.

Please, please, more on XPL.

The XProc link is not correct - it points to Apache FOP (probably a copy&paste error).
Otherwise, great stuff.

Hi,

We have added another internal option called “Auto Confirm Targets” to the advertiser portal, under the “Delivery And Website Targeting Options” section when creating new ad tasks of any group or type.

Please note that this option is not enabled by default and essentially allows you to skip the manual confirmation process when creating new tasks. When this option is enabled, the newly created task will go immidately into our approval and live publishing queue as soon as the website targets get acquired, so there is no need to wait for website targets to show up and then approve each task manually, unless you need to review your website targets first. This option has been implemented for all task groups ( Guerilla, Direct, Trusted And Other ) and individual task types.

Thank you.
evani
social bookmarking

Popular Topics

Archives

Or, visit our complete archives.

Recommended for You

Got a Question?