Monday, March 31, 2008

Using Jersey (JSR311) inside OSGi with Spring-DM

Or: using REST & OSGi to create a number guessing game

Jersey is an implementation of JSR 311 also know as JAX-RS. It makes writing a REST service quite simple with Java Annotations. So I wanted to use Jersey inside my OSGi bundle with the OSGi HTTP Service. However, as I found out, Jersey is not written to run in an OSGi container...

Fortunately, the problematic code in Jersey is pluggable. In this posting I'm writing a little Jersey REST Application that runs in my OSGi container. I'm using Spring-DM (aka Spring-OSGi) to do the OSGi Service dependency injection which keeps my code really lean & clean (Spring Clean ;) .

To illustrate I wrote a little number guessing game. The very first program I ever wrote was a game like this. This was on the Commodore Pet computer and I was about 10 years old. So beware...

Development Environment Setup
The Jersey version I got is 0.7-ea, which I downloaded from the Jersey download site.

I'm using the Equinox OSGi runtime in Eclipse 3.3.2 (get it here) with Spring-DM added in.

I downloaded Spring-DM 1.0.2 from here and copied all the jars from the dist and the lib directory of the download in my eclipse/plugins directory. The jars in the Spring-DM lib directory have been OSGi-ified by the Spring guys so we can nicely drop them in as OSGi bundles.

Bundle Dependencies
The general dependency picture is like the image below. I'm writing a Jersey app in a bundle that depends on a library bundle which contains the Jersey jars. This way multiple bundles can use the Jersey functionality to implement REST services while sharing the same Jersey Library bundle.


The Jersey Library Bundle
This is a simple wrapper around the Jersey libraries. At the moment I'm only depending on jersey.jar, jsr311-api.jar and asm-3.1.jar. So I'm simply creating an OSGi Bundle Project in Eclipse (Plug-in Project - see this posting for some details on how to do this), add these three jars from the Jersey install to the root of the bundle and then put them on the Bundle-Classpath. Finally simply export all the classes defined by these bundles. I guess a subset of the classes would do too, anyone fancy figuring out what the minimum Export-Package would be?

My MANIFEST.MF looks like this:

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: JerseyLibBundle
Bundle-SymbolicName: JerseyLibBundle
Bundle-Version: 1.0.0
Bundle-ClassPath: asm-3.1.jar,
 jersey.jar,
 jsr311-api.jar,
 .
Import-Package: javax.servlet,
 javax.servlet.http
Export-Package: ...,
 ... export all the packages in these jars ...


That's the Jersey Library Bundle done. My Eclipse screen looks like this (click on it to see all the details):


The REST Bundle
This is the interesting bit. Here's where we'll have our REST Guessing Game implemented.

The REST application will be available on:

  http://localhost/fun/numbers
Lists all the numbers available in XML format.

  http://localhost/fun/numbers/{number}
Returns information about the requested number in text format. The information says what number it is (the same one you asked for) and whether or not you guessed the right number.

I have a jersey_osgi.rest.Number class:
package jersey_osgi.rest;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class Number {
  enum Location {LESS, FOUND, MORE};
  private static final int MAX_NUM = 10;

  static List<Number> NUMBERS;
  static {
    // Initialize with numbers 1 to 10,
    // and pick a random on to guess.
    NUMBERS = new ArrayList<Number>();

    Random random =
      new Random(System.currentTimeMillis());
    int theOne = random.nextInt(MAX_NUM) + 1;

    for (int i=1; i <= MAX_NUM; i++) {
      Location location;
      if (i == theOne) {
        location = Location.FOUND;
      } else if (i < theOne) {
        location = Location.LESS;
      } else {
        location = Location.MORE;
      }
      NUMBERS.add(new Number(i, location));
    }
  };

  private final int number;
  private final Location location;
  public Number(int num, Location l) {
    number = num;
    location = l;
  }

  public int getNumber() {
    return number;
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append("I'm number ");
    sb.append(number);
    sb.append(". ");
    switch (location) {
    case FOUND:
      sb.append("You guessed right!");
      break;
    case LESS:
      sb.append("You need to guess higher.");
      break;
    case MORE:
      sb.append("You need to guess lower.");
      break;
    }
    return sb.toString();
  }
}


Next thing we need is a JSR311 compliant class that binds our Number class to a REST API:
package jersey_osgi.rest;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.ProduceMime;

@Path("/numbers")
public class NumberResource {
  @GET
  @ProduceMime("text/xml")
  public String listNumbers() {
    StringBuilder sb = new StringBuilder();

    sb.append("<!-- Guess the right number! The following numbers are available. -->");
    sb.append("\n");
    sb.append("<numbers>");
    sb.append("\n");
    for (Number number : Number.NUMBERS) {
      sb.append("<number>");
      sb.append(number.getNumber());
      sb.append("</number>");
      sb.append("\n");
    }
    sb.append("</numbers>");
    return sb.toString();
  }

  @GET
  @Path("/{id}")
  @ProduceMime("text/plain")
  public String getNumber(@PathParam("id") String id) {
    try {
      int i = Integer.parseInt(id);
      Number n = Number.NUMBERS.get(i-1);
      return n.toString();
    } catch (Throwable th) {
      return "Sorry that number is out of range";
    }
  }
}

That's our REST implementation done. Only thing left to do is use the OSGi HTTPService to launch the Jersey Servlet and tell it what our REST Resource are, right? You wish! You would think that the following piece does the trick:

HttpService httpService = ...; // The OSGi HTTP Service
Hashtable initParams = new Hashtable();
initParams.put(
  // The following are standard Jersey properties
  "com.sun.ws.rest.config.property.resourceConfigClass",
  "com.sun.ws.rest.api.core.PackagesResourceConfig");
initParams.put(
  "com.sun.ws.rest.config.property.packages",
  "jersey_osgi.rest");

Servlet jerseyServlet =
  new com.sun.ws.rest.spi.container.servlet.ServletContainer();
httpService.registerServlet("/fun", jerseyServlet, initParams, null);


But hey! I'm getting an ugly exception:
com.sun.ws.rest.api.container.ContainerException: The ResourceConfig instance does not contain any root resource classes.

This is because the com.sun.ws.rest.api.core.PackagesResourceConfig class contains OSGi unfriendly classloader code (it tries to treat classes as resources, which at least does'nt work on Equinox). Luckily this class is plugged into Jersey, so I can write an OSGi-friendly version. PackagesResourceConfig searches the specified packages for any classes with the JSR311 annotations.
My alternative implementation is called jersey_osgi.common.OSGiResourceConfig. The source is slightly too big for the posting, so just click on the link if you want to see it.

For my OSGi version I have to actually specify the classes that are annotated in a property called jersey_osgi.classnames. So I change to properties like this:
HttpService httpService = ...; // The OSGi HTTP Service
Hashtable initParams = new Hashtable();
initParams.put(
  "com.sun.ws.rest.config.property.resourceConfigClass",
  "jersey_osgi.common.OSGiResourceConfig");
initParams.put(
  "jersey_osgi.classnames",
  "jersey_osgi.rest.NumberResource");

Servlet jerseyServlet =
  new com.sun.ws.rest.spi.container.servlet.ServletContainer();
httpService.registerServlet("/fun", jerseyServlet, initParams, null);

And run it. Now I'm getting:
com.sun.ws.rest.api.container.ContainerException: No WebApplication provider is present aaaargh! After some debugging I found out that Jersey heavily depends on the META-INF/services SPI architecture which is quite non-OSGi friendly. There are only two possible solutions to this that I can think of:

  1. Export-Package META-INF.services in the Jersey Library Bundle. This works but is kinda nasty because you can only do it once in a single OSGi runtime. If you have multiple bundles exporting the same package, the OSGi runtime will only link you to only one of them for this package. If multiple bundles export META-INF.services you will get unexpected behaviour.

  2. Copy the META-INF/services directory from the jersey.jar into our own bundle. Other projects with OSGi bundles seem to be taking this approach too.

So I'm taking approach 2 as well and copy all the META-INF/services files of jersey.jar into my bundle. And now it works! I can nicely build my Jersey apps as OSGi bundles that all depend on shared Jersey code.

Where's the Spring stuff?
Spring-DM makes it really easy for me to interact with the OSGi HTTP Service as it simply injects it in my JerseyServletFactory class which then registers the Jersey Servlet. To trigger this, I put a jersey_osgi_spring.xml file in the META-INF/spring directory of my bundle:

<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:osgi="http://www.springframework.org/schema/osgi"
  xsi:schemaLocation="...">

  <osgi:reference id="httpServiceRef"
    interface="org.osgi.service.http.HttpService"/>

  <bean name="jerseyServletFactory"
    class="jersey_osgi.common.JerseyServletFactory" >
    <property name="jerseyClassNames"
      value="jersey_osgi.rest.NumberResource"/>
    <property name="rootContext" value="/fun"/>
    <property name="httpService" ref="httpServiceRef"/>
  </bean>
</beans

The JerseyServletFactory registers the Jersey Servlet with the OSGi HTTP Service. For this it needs that HTTP Service in the first place. Those who had the pleasure of using the pure OSGi API's to get hold of an OSGi Service know that this is actually slightly tricky. So OSGi Declarative Services come to the rescue (which I used in the earlier article about writing a Card Game with OSGi Services). Spring-DM provides an alternative to OSGi-DS and brings with it all of the goodies that Spring has to offer. With the above Spring file, my JerseyServletFactory is nicely reusable (the Root Context and Jersey resource classes are also externalized into the Spring Configuration file) and no bigger than this:
package jersey_osgi.common;

import java.util.Hashtable;
import javax.servlet.Servlet;
import org.osgi.service.http.HttpService;
import com.sun.ws.rest.spi.container.servlet.ServletContainer;

public class JerseyServletFactory {
  private String classNames;
  private String rootContext;

  // Spring uses the setters to inject our stuff
  public void setJerseyClassNames(String names) {
    classNames = names;
  }

  public void setRootContext(String ctx) {
    rootContext = ctx;
  }

  public void setHttpService(HttpService httpService) throws Exception {
  Hashtable<String, String> initParams =
    new Hashtable<String, String>();
    initParams.put(
      "com.sun.ws.rest.config.property.resourceConfigClass",
      "jersey_osgi.common.OSGiResourceConfig");
    initParams.put(
      "jersey_osgi.classnames",
      classNames);

    Servlet jerseyServlet =
      new ServletContainer();
    httpService.registerServlet(rootContext,
      jerseyServlet, initParams, null);
  }
}


Running it all inside Eclipse
Running Spring-DM inside Equinox as shipped with Eclipse requires careful definition of the runtime environment. You set this up by going to Run | Open Run Dialog ... in Eclipse where you create a new OSGi Framework run configuration. Deselect the Target Platform tickbox and only select the following bundles:
JerseyLibBundle_1.0.0
JerseyOsgiSpringDM_1.0.0
javax.servlet_2.4.0.v200706111738
org.apache.commons.logging_1.0.4.v200706111724
org.eclipse.core.jobs_3.3.1.R33x_v20070709
org.eclipse.core.runtime.compatibility.registry_3.2.100.v20070316
org.eclipse.equinox.common_3.3.0.v20070426
org.eclipse.equinox.http.jetty_1.0.1.R33x_v20070816
org.eclipse.equinox.http.registry_1.0.1.R33x_v20071231
org.eclipse.equinox.http.servlet_1.0.1.R33x_v20070816
org.eclipse.equinox.registry_3.3.1.R33x_v20070802
org.eclipse.osgi.services_3.1.200.v20070605
org.eclipse.osgi_3.3.2.R33x_v20080105
org.mortbay.jetty_5.1.11.v200706111724
org.springframework.bundle.osgi.core_1.0.2
org.springframework.bundle.osgi.extender_1.0.2
org.springframework.bundle.osgi.io_1.0.2
org.springframework.bundle.spring.aop_2.5.1
org.springframework.bundle.spring.beans_2.5.1
org.springframework.bundle.spring.context_2.5.1
org.springframework.bundle.spring.core_2.5.1
org.springframework.osgi.aopalliance.osgi_1.0.0.SNAPSHOT

Like this (I edited the unselected bundles out of the screenshot):


I found that if I don't do it exactly like this that I'm getting problems with the correct logger being instantiated, but this configuration works. Just hit 'Run' and you're off. You can now access the REST application from http://localhost/fun/numbers


I can guess a number: http://localhost/fun/numbers/5

... and try again: http://localhost/fun/numbers/3

Yay!

Get the code
You can get all the code (Apache License) as usual from the google code site. It's at available as 2 Eclipse projects at http://coderthoughts.googlecode.com/svn/trunk/jersey_osgi/. The easiest way to get them into your Eclipse installation is by using an SVN client inside Eclipse and check them out as projects. This is described in more detail in this posting (just search for the 'subclipse' in there).