RESTful WebObjects

Development of a REST request handler for WebObjects applications.

As part of watershed.co.uk's ongoing development, we have a number of immediate requirements:

  • Start building a replacement for the FBTFPersistentObjects framework used to provide object persistence and network database connectivity for Communicate.
  • Vend XML and other data types using a technology with easier to implement clients than SOAP.
  • Transfer data to iPhone applications efficiently using XML or binary plists.
  • Do all the above across a number of different databases and business object frameworks.

REST web services vending XML & plist data appeared a good fit for all the above but we didn't want to have to write a whole collection of direct actions and components for each site. So we are in the process of building a REST Request Handler for new and existing applications. This is still a work in progress and is only, a day or two old, but hopefully in the near future there should be useable code alongside the explanation.

So how do we use our REST Request Handler in applications?

First we create a new instance of the handler and assign it to handle particular requests. We do this in Application's constructor in Application.java

  1. RESTHandler restRequestHandler = new RESTHandler();
  2. restRequestHandler.setSecurityDelegate(new MyRESTSecurity());
  3. registerRequestHandler(restRequestHandler, RESTHandler.REQUEST_HANDLER_KEY);

We also set a security delegate here which is explained below. The request handler key is "REST" so any request sent to www.domain.com/cgi-bin/WebObjects/App.woa/REST/... will be processed by our handler.

Unfortunately, the REST Handler is not quite the simple, drop-in unit we would like. WebObjects will only create and initialise a WORequest with a HTTP GET, HEAD & POST method. Sending a HTTP DELETE or PUT action to a WebObjects application results in an exception. The solution is quite simple, subclass WORequest and return it from a re-implemented createRequest(...) in your Application class.

Security Delegate

The REST Handler can automatically provide public access to any database so as a precaution a security delegate must be implemented and added to the request handler before any requests can be accepted. The security delegate can be any object that implements the few methods declared in the security delegate interface, RESTSecurityDelegate.

  1. public interface RESTSecurityDelegate {
  2. public int requiredHTTPAuthentication(WORequest aRequest);
  3. public boolean requireSecureTransport();
  4. public boolean shouldAllowRequest(WORequest aRequest);
  5. }

The security delegate methods are simple to implement and can be varied per request. There are some general REST authentication and authorisation practices in use and these leverage existing features of HTTP: with a secure HTTPS connection to prevent eavesdropping and use, either, HTTP Basic or Digest authentication, or, include a security token as a HTTP header key such as:

GET / HTTP/1.1
User-Agent: curl/7.16.3 (powerpc-apple-darwin9.0) libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
Host: www.watershed.co.uk
X-Watershed-Auth: U2FsdGVkX1zuHyuXFLEBoBjEHtQO.
Accept: */*

Currently only HTTP Basic support is implemented and we will add Digest at a later date. The REST Handler will first check if it is allowed receive requests over plain HTTP by calling requireSecureTransport(). If the return value is true then the handler will redirect the client to a secure connection. Next the REST Handler asks the delegate, using requiredHTTPAuthentication(), if the request will require authentication and, if the client hasn't provided any, will ask the client to provide authentication. Currently, only Basic HTTP authentication is supported. Finally, the delegate decides if the handler should process the request in the shouldAllowRequest(WORequest aRequest) method.

The listing below illustrates a possible implementation of the required methods.

  1. public class SecurityDelegate implements RESTSecurityDelegate {
  2. public int requiredHTTPAuthentication(WORequest aRequest) {
  3. // Give everyone read-only access
  4. if (aRequest.method().equals("GET")) return SecurityDelegate.AUTHENTICATE_NONE;
  5. return SecurityDelegate.AUTHENTICATE_BASIC;
  6. }
  7. public boolean requireSecureTransport() {
  8. return false;
  9. }
  10. public boolean shouldAllowRequest(WORequest aRequest) {
  11. // Give everyone read-only access
  12. if (aRequest.method().equals("GET")) return true;
  13. // A user must be authenticated to change resources
  14. if (aRequest.headerForKey("authorization") != null) {
  15. String encodedAuth = aRequest.headerForKey("authorization");
  16. String decodedAuth = null;
  17. BASE64Decoder decoder = new BASE64Decoder();
  18. //encoded string starts after "Basic "
  19. encodedAuth=encodedAuth.substring(encodedAuth.indexOf(" ")+1);
  20. try{
  21. decodedAuth=new String(decoder.decodeBuffer((new ByteArrayInputStream (encodedAuth.getBytes()))));
  22. } catch(IOException ex) {}
  23. String tokens[] = decodedAuth.split(":");
  24. if(tokens.length != 2) return false;
  25. // Check the username & password
  26. if (tokens[0].equals("benjamin") && tokens[1].equals("mypassword")) return true;
  27. }
  28. return false;
  29. }
  30. }

Data Modules

Once the security delegate has approved the request, a data module is then invoked with the task of handling of the request. Using a number of installed data modules allow the application to vend REST web services in any number of different data formats. Data modules render Enterprise Objects out in a response and update, or insert, EO's from parsed data in a request. The data module also deletes EO's from the store for some requests. Each data module implements an interface based on HTTP actions and the REST Handler invokes a method passing the original WORequest and the target EO, or array of EO's.

Currently, the REST Handler selects an appropriate data module based on the Accept HTTP header sent in the request by the client. It was assumed that clients would have fine grained control over the request and the client would not always be a browser window so the REST Handler treats each acceptable MIME Type as HTTP 1.1 would, MIME Types are listed in order of preference: most desirable first. There is one exception to the HTTP 1.1 specification; the relative quality parameter is currently ignored. If a header such as Accept: image/jpeg, video/mp4, text/xml;q=0.9, text/json, text/plist, */* were received then the request handler would look first for a module that handled JPEG images and so on.

Each available data module is mapped to one or more MIME types. Currently we are just using a particular XML data module for testing and in the process of writing a plist module for use with Cocoa desktop and iPhone clients. A module is not required to support all four actions create, read update, and delete. Extra modules, such as JSON or vCard can be written relatively quickly and registered with the REST handler before accepting requests. There currently isn't a way to request a particular data format based upon the 'file' suffix in the request.

  1. restRequestHandler.registerDataModuleForMIMEType(new DataModuleXML(), "*/*");

Model Delegate

The REST Handler will pass the request and the target Enterprise Objects to the data module but on some occasions we may not want make every entity available to the public. We may also want to make some attributes, such as private internal notes, unavailable. When a request needs to render 10,000 objects to XML we may not need to include every attribute, just an essential subset. An optional model delegate provides the REST Handler and the data modules with answers and information they need to handle the requests efficiently. If the model delegate is not provided then the handler use the default model group.

  1. public interface RESTModelDelegate {
  2. public boolean cacheModelAdjustments();
  3. public boolean includeEntity(WORequest aRequest, String entityName);
  4. public NSArray removeAttributesForEntity(String entityName);
  5. public NSArray includeExtraAttributesForEntity(String entityName);
  6. public NSArray attributesForCollection(String entityName);
  7. public NSArray attributesForObject(String entityName);
  8. public String aliasForEntity(String entityName);
  9. public String labelForEntity(String entityName);
  10. public String labelForEntities(String entityName);
  11. public String labelForProperty(String propertyName);
  12. }

To be Implemented…

The REST Handler has only had a handful of train journeys of development so there are plenty of rough edges. It's currently difficult to adjust or influence the target enterprise objects provided to the data modules which would be useful for situations where we may not want to include all the available films, just the ones screening this month. There is no HTTP Digest authentication and the ability to specify query strings, such as commence<2007-05-29 or isPublic=true, in the URL or a request header would be very useful. There is still plenty of work to be done until the REST Handler is complete but it will be a valuable tool as we make architectural, storage and communication changes in watershed.co.uk and dshed.net.

|

odds&ends…
Benjamin Miller