For the past couple of weeks Paul Sandoz and I have been working on a client side framework for Jersey that encourages RESTful design on the client side. After a trial period with frequent code changes, we feel that the code base is now stable enough to go public.
The framework introduces an annotation based approach that enables developers to turn arbitrary classes of the client side code into HTTP response handlers. The framework refers to instances of such classes as ‘views’ (mostly for the lack of a better term).
Views serve three purposes:
1. Build-up state corresponding to the handled response
View classes provide specifically annotated methods that will be invoked by the framework core to handle the response. These ‘build’ methods are intended to build-up the state of the view object from the HTTP response.
2. Present or make accessible the data of the current application state
Usually, users (human or machine) intend to use the information contained in HTTP response bodies. View objects make this information accessible either through GUI elements for human users or through access methods for machine users.
3. Use the hypermedia controls embedded in the response body to turn user (human or machine) actions into HTTP requests.
Both human and machine users interact with RESTful applications through user agent components by activating the controls embedded in the representations of the current application state. View objects provide a means for the user to activate the controls and will then create the corresponding HTTP requests. The means for control activation can be GUI elements in the case of human users or regular methods in the case of machine users.
Enough theory – let’s look at some code.
Suppose we want to develop a view class that is is suitable for Atom Publishing Protocol collections. The following examples step by step show the different aspects of such a view class and how it could be used.
First, we need a build method for handling HTTP success responses to GET requests to collection resources.
public class CollectionView {
private Feed feed;
@GET
@Status(200)
@Consumes("application/atom+xml")
public void build(Feed feed) {
this.feed = feed;
}
}
The annotations on the build-method indicate to the framework core that the method applies to responses to GET requests that have a status of 200 Ok and a response body of the media type application/atom+xml. As requested by the method parameter list the framework core will inject the response body as a Feed object to enable access to the body inside the build method.
The code below shows how the CollectionView class might be used. The new view method on the Jersey Client class performs an HTTP GET on the provided URI, instantiates a new object of the provided class and then dispatches to a suitable build method of the created instances (based on the annotations).
public void main(String args[]) {
URI uri = URI.create("http://example.org/blogs/jim/entries");
Client client = new Client();
CollectionView cv = client.view(uri, CollectionView.class);
}
Next we need to add to our CollectionView class methods that provide access to information contained in the feed. To keep things simple, we’ll just provide a getter for the title of the feed.
public class CollectionView {
private Feed feed;
@GET
@Status(200)
@Consumes("application/atom+xml")
public void build(Feed feed) {
this.feed = feed;
}
public String getFeedTitle() {
return this.feed.getTitle();
}
}
Example for using the getFeedTitle() method:
public void main(String args[]) {
URI uri = URI.create("http://example.org/blogs/jim/entries");
Client client = new Client();
CollectionView cv = client.view(uri, CollectionView.class);
System.out.println("Feed title is " + cv.getFeedTitle());
}
The third purpose of view classes mentioned above is to provide users (human or machine) with a means to activate the controls embedded in the response entities. RFC 5005 defines (among others) the ‘next’ link relation that can be used to tell a consumer of the feed document where the next page of the feed representation is located. We will use this link relation to demonstrate how a view class can expose a hypermedia control to the user.
One possibility to make ‘next’ links accessible to the user is to provide some getNextPageUri() method on the view class that can be called by the user to retrieve the URI of the next page. However, we can also provide a design that matches better the usual intent of the user of a feed: to iterate over the contained entries.
The code below shows an iterateEntries() method that processes all entries in the feed document of the view and follows any ‘next’ link to subsequent feed pages. The EntryHandler interface acts as a callback class.
public interface EntryHandler {
public boolean handleEntry(Entry e);
}
public class CollectionView {
private Client client;
private Feed feed;
@GET
@Status(200)
@Consumes("application/atom+xml")
public void build(Feed feed, @Context Client client) {
this.client = client;
this.feed = feed;
}
public String getFeedTitle() {
return this.feed.getTitle();
}
public void iterateEntries(EntryHandler entryHandler) {
List<Entry> entries = this.feed.getEntries();
for (Entry entry : entries) {
boolean proceed = entryHandler.handleEntry(entry);
if (!proceed) {
return;
}
}
Link link = this.feed.getLink("next");
if (link == null) {
return;
}
URI nextUri = link.getHref().toURI();
CollectionView v = this.client.view(nextUri, CollectionView.class);
v.iterateEntries(entryHandler);
}
}
Note that we have added an annotated parameter to the build method to inject the client:
public void build(Feed feed, @Context Client client) {
This is necessary because the iterateEntries() method constructs a view for the next page if a ‘next’-link is found.
The CollectionView class can now be used to iterate over all entries in a feed as follows:
public void main(String args[]) {
URI uri = URI.create("http://example.org/blogs/jim/entries");
Client client = new Client();
CollectionView cv = client.view(uri, CollectionView.class);
cv.iterateEntries(new EntryHandler() {
private int count = 0;
public boolean handleEntry(Entry entry) {
this.count++;
System.out.println("Entry #" + this.count + ": " + entry.getTitle());
// limit the number of processed entries to 200
return (this.count <= 200);
}
});
}
(You can find a similar example in the Jersey sources at experimental/view-client/jersey-view-client-samples/atompub-simple-view-client-sample)
The CollectionView example class demonstrates the three purposes of a view:
- Handle HTTP responses
- Present or make accessible application state
- Provide a means for the user to activate hypermedia controls
Interactions with resources never occur without assumptions about the resources (for example that it is a Web page, an image or an Atom Publishing Protocol collection). View classes are manifestations of such assumptions and the choice of a specific view class for a given interaction expresses which assumptions are being made.
From a code structuring and maintenance perspective view classes nicely bundle up all the elements that pertain to such assumptions and act as a single point of documentation.
There are a number of issues we have not covered in this first blog. Among them are
- Support for several response body media types
- Error handling
- Constructing requests other than GET
- Performing a sequence of automated requests
- Using ‘existing’ client side classes as view classes
We will talk about those and other features in follow up postings.
Instructions for Obtaining the Code
Unfortunately there are currently issues obtaining the latest SNAPSHOT artifacts from the java.net maven repository. Until this is resolved it is necessary to obtain and build Jersey yourself. The new client side framework is located in experimental/view-client.
Use SE 6 with maven version 2.2.0 or 2.2.1.
Join the Jersey project as an Observer at https://jersey.dev.java.net.
Check out the trunk source:
svn checkout \
https://jersey.dev.java.net/svn/jersey/trunk/jersey \
jersey --username <username>
Clean and install:
mvn clean install
If you want to do this a bit quicker you can skip the tests
mvn -Dmaven.test.skip=true clean install
You may need to set the following maven options (which can be set using the MAVEN_OPTS environment variable):
-Xmx1048m -XX:PermSize=64M -XX:MaxPermSize=128M
Discussion
Please send comments to the jersey users list.