Jersey is a JSR 311 reference implementation for the JAX-RS spec (The Java API for RESTful Web Services). The JSR development is nearing its completion, but for most part the RI is stable enough for developers to start playing with RESTful services. JAX-RS is an elegant API built around the powerful REST architecture and modeled using resource providers. These resources can be described using WADL (Web Application Description Language). Let us build a basic movie resource which lists top box office movies. Just download the latest distribution of Jersey. I am using 0.8 release for this illustration. NetBeans 6.1 does provide support a Jersey plugin for 0.7 release. Add the jars from the distribution to the project classpath. JAX-RS requires JDK 1.5 as it uses annotations for implementing web resources.
The below XSD defines a basic movie schema.
<?xml version="1.0" encoding="UTF-8" ?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
elementFormDefault="unqualified">
<xsd:element name="movieCollection">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="movie" type="movieDetails" minOccurs="1"
maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:complexType name="movieDetails">
<xsd:sequence>
<xsd:element name="title" type="xsd:string"/>
<xsd:element name="genres" type="xsd:string"/>
<xsd:element name="directedBy" type="xsd:string"/>
</xsd:sequence>
<xsd:attribute name="rank" type="xsd:int"/>
</xsd:complexType>
</xsd:schema>
Let us use JAXB to generate the Java classes for this schema using the “xjc” tool. This will generate MovieCollection and MovieDetails types in addition to the ObjectFactory. These types will be used in building the response object for our movie collection.
We will implement the “TopBoxOffice” web resource using the JAX-RS APIs.
import com.sun.jersey.spi.resource.Singleton;
import restful.impl.jaxb.MovieCollection;
import restful.impl.jaxb.MovieDetails;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.ProduceMime;
import javax.ws.rs.core.Response;
import static java.lang.System.out;
@Singleton
@Path("/boxoffice")
public class TopBoxOffice {
private MovieCollection collection = new MovieCollection();
private final String HTTP_RESPONSE_DATE_HEADER = "EEE, dd MMM yyyy HH:mm:ss zzz";
private final String DATE_ONE = (new SimpleDateFormat(HTTP_RESPONSE_DATE_HEADER, Locale.US)).format(new Date(1));
private static int rank = 0;
public TopBoxOffice() {
init();
}
@GET
@Path("/movies")
@ProduceMime({"application/xml", "application/json"})
public Response getTopBoxOfficeMoviesThisWeek() {
out.println("Invoking getTopBoxOfficeMoviesThisWeek() ...");
//Pragma and Expires headers are set to disable caching in IE
return Response.ok(collection).header("Pragma", "no-cache").header("Expires", DATE_ONE).build();
}
final void init() {
MovieDetails movie = constructMovieObject("Indiana Jones and the Kingdom of the Crystal Skull", "Steven Spielberg", "Action/Adventure and Sequel");
collection.getMovie().add(movie);
movie = constructMovieObject("The Chronicles of Narnia: Prince Caspian", "Andrew Adamson", "Action/Adventure, Science Fiction/Fantasy and Adaptation");
collection.getMovie().add(movie);
movie = constructMovieObject("Iron Man", "Jon Favreau", "Action/Adventure, Science Fiction/Fantasy and Adaptation");
collection.getMovie().add(movie);
movie = constructMovieObject("What Happens in Vegas", "Tom Vaughan", "Comedy");
collection.getMovie().add(movie);
movie = constructMovieObject("Speed Racer", "Larry Wachowski, Andy Wachowski", "Action/Adventure, Science Fiction/Fantasy and Adaptation");
collection.getMovie().add(movie);
movie = constructMovieObject("Baby Mama", "Michael McCullers", "Comedy");
collection.getMovie().add(movie);
movie = constructMovieObject("Made of Honor", "Paul Weiland", "Comedy");
collection.getMovie().add(movie);
movie = constructMovieObject("Forgetting Sarah Marshall", "Nicholas Stoller", "Comedy");
collection.getMovie().add(movie);
movie = constructMovieObject("Harold and Kumar Escape From Guantanamo Bay", "Jon Hurwitz, Hayden Schlossberg", "Comedy and Sequel");
collection.getMovie().add(movie);
movie = constructMovieObject("The Visitor", "Tom McCarthy", "Comedy and Drama");
collection.getMovie().add(movie);
}
final MovieDetails constructMovieObject(String title, String direction, String genres) {
MovieDetails movie = new MovieDetails();
movie.setTitle(title);
movie.setRank(++rank);
movie.setDirectedBy(direction);
movie.setGenres(genres);
return movie;
}
}
The Singleton annotation ensures that only one instance of this class will be instantiated per web application. The movie collection resource will be populated when the first request to the movie list resource occurs. Remember this is RI specific feature and not part of JSR 311 API. The Path annotation defines the URI path to access the web resource. The resource is annotated using GET which populates top box office movies in the response. The response will be rendered using either XML or JSON. The default media type will be XML as it is the first one in the list provided to the ProduceMime annotation. If you need JSON response, you need to set the “Accept” header to “application/json” type. This will render JSON response. If you notice the response code, we are adding couple headers which basically disables caching in IE. You may set as many HTTP response headers on the response using the javax.ws.rs.core.Response class. This resource only implements the basic HTTP GET operation. Developing PUT, POST and DELETE operations on the resource can be quite easy too. The resource can be backed by a database where these HTTP operations can be easily translated into CRUD actions operating on the data.
The resource can now be deployed in a HTTP server. Let us use the Grizzly HTTP server to quickly deploy our resource and do some testing. Grizzly jars are bundled as part of the Jersey distribution. Add the required jars to the classpath. The below code starts the HTTP server at port 9090.
import com.sun.grizzly.http.SelectorThread;
import com.sun.jersey.api.container.grizzly.GrizzlyServerFactory;
import static java.lang.System.*;
public class Server {
public static void main(String[] args) throws Exception {
SelectorThread server = GrizzlyServerFactory.create("http://localhost:9090/");
out.println("Server running, hit return to stop...");
in.read();
out.println("Stopping server");
server.stopEndpoint();
exit(0);
out.println("Server stopped");
}
}
If you want to use this within a servlet container, you can use the standard web.xml to describe a Jersey webapp.
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<servlet>
<servlet-name>Jersey Web Application</servlet-name>
<servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Jersey Web Application</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
Now you can access the URL “http://localhost:9090/boxoffice/movies” from a browser.
You should see the XML response as shown below.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<movieCollection>
<movie rank="1">
<title>Indiana Jones and the Kingdom of the Crystal Skull</title>
<genres>Action/Adventure and Sequel</genres>
<directedBy>Steven Spielberg</directedBy>
</movie>
<movie rank="2">
<title>The Chronicles of Narnia: Prince Caspian</title>
<genres>Action/Adventure, Science Fiction/Fantasy and Adaptation</genres>
<directedBy>Andrew Adamson</directedBy>
</movie>
<movie rank="3">
<title>Iron Man</title>
<genres>Action/Adventure, Science Fiction/Fantasy and Adaptation</genres>
<directedBy>Jon Favreau</directedBy>
</movie>
<movie rank="4">
<title>What Happens in Vegas</title>
<genres>Comedy</genres>
<directedBy>Tom Vaughan</directedBy>
</movie>
<movie rank="5">
<title>Speed Racer</title>
<genres>Action/Adventure, Science Fiction/Fantasy and Adaptation</genres>
<directedBy>Larry Wachowski, Andy Wachowski</directedBy>
</movie>
<movie rank="6">
<title>Baby Mama</title>
<genres>Comedy</genres>
<directedBy>Michael McCullers</directedBy>
</movie>
<movie rank="7">
<title>Made of Honor</title>
<genres>Comedy</genres>
<directedBy>Paul Weiland</directedBy>
</movie>
<movie rank="8">
<title>Forgetting Sarah Marshall</title>
<genres>Comedy</genres>
<directedBy>Nicholas Stoller</directedBy>
</movie>
<movie rank="9">
<title>Harold and Kumar Escape From Guantanamo Bay</title>
<genres>Comedy and Sequel</genres>
<directedBy>Jon Hurwitz, Hayden Schlossberg</directedBy>
</movie>
<movie rank="10">
<title>The Visitor</title>
<genres>Comedy and Drama</genres>
<directedBy>Tom McCarthy</directedBy>
</movie>
</movieCollection>
In order to test the JSON rendering, let us write a Client developed using HTTP Client library. You need to have commons-logging and commons-codec in addition to the commons-httpclient jars in the classpath.
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.GetMethod;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.apache.commons.httpclient.Header;
import static java.lang.System.out;
public class Client {
public static void main(String args[]) throws Exception {
String url = "http://localhost:9090/boxoffice/movies";
out.println("Sent HTTP GET request to the endpoint " + url);
GetMethod get = new GetMethod(url);
get.addRequestHeader("Accept", "application/json");
HttpClient httpclient = new HttpClient();
StringBuffer buffer = null;
try {
httpclient.executeMethod(get);
if (get.getStatusCode() == HttpStatus.SC_OK) {
InputStream is = get.getResponseBodyAsStream();
Header[] headers = get.getResponseHeaders();
for (Header header : headers) {
out.println(header.getName() + " : " + header.getValue());
}
BufferedReader in = new BufferedReader(new InputStreamReader(is));
buffer = new StringBuffer();
String line = null;
while ((line = in.readLine()) != null) {
buffer.append(line);
}
} else {
out.println("GET action raised an error: " + get.getStatusLine());
}
} finally {
// Release current connection to the connection pool once you are done
get.releaseConnection();
}
out.println("JSON RESPONSE : " + buffer.toString());
}
}
The output of this program is shown below.
Sent HTTP GET request to the endpoint http://localhost:9090/boxoffice/movies
Pragma : no-cache
Expires : Wed, 31 Dec 1969 17:00:00 MST
Content-Type : application/json
Transfer-Encoding : chunked
Date : Tue, 27 May 2008 04:13:28 GMT
JSON RESPONSE :
{"movieCollection":
{"movie":[
{"@rank":"1","title":"Indiana Jones and the Kingdom of the Crystal Skull","genres":"Action/Adventure and Sequel","directedBy":"Steven Spielberg"},
{"@rank":"2","title":"The Chronicles of Narnia: Prince Caspian","genres":"Action/Adventure, Science Fiction/Fantasy and Adaptation","directedBy":"Andrew Adamson"},
{"@rank":"3","title":"Iron Man","genres":"Action/Adventure, Science Fiction/Fantasy and Adaptation","directedBy":"Jon Favreau"},
{"@rank":"4","title":"What Happens in Vegas","genres":"Comedy","directedBy":"Tom Vaughan"},
{"@rank":"5","title":"Speed Racer","genres":"Action/Adventure, Science Fiction/Fantasy and Adaptation","directedBy":"Larry Wachowski, Andy Wachowski"},
{"@rank":"6","title":"Baby Mama","genres":"Comedy","directedBy":"Michael McCullers"},
{"@rank":"7","title":"Made of Honor","genres":"Comedy","directedBy":"Paul Weiland"},
{"@rank":"8","title":"Forgetting Sarah Marshall","genres":"Comedy","directedBy":"Nicholas Stoller"},
{"@rank":"9","title":"Harold and Kumar Escape From Guantanamo Bay","genres":"Comedy and Sequel","directedBy":"Jon Hurwitz, Hayden Schlossberg"},
{"@rank":"10","title":"The Visitor","genres":"Comedy and Drama","directedBy":"Tom McCarthy"}
]}
}
You can also use curl to test the above behavior. This approach is easier than the earlier one, if you have access to curl.
curl -i -H "Accept: application/json" http://localhost:9090/boxoffice/movies
WADL can be accessed at http://localhost:9090/application.wadl. This provides basic resource description as shown below.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<application xmlns="http://research.sun.com/wadl/2006/10">
<resources base="http://localhost:9090/">
<resource path="/boxoffice">
<resource path="/movies">
<method name="GET">
<response>
<representation mediaType="application/xml"/>
<representation mediaType="application/json"/>
</response>
</method>
</resource>
</resource>
</resources>
</application>
RESTful web services has more potential use cases and Jersey provides extensions to the library so that developers can customize as per their needs.
The resource provider SPI allows pluggable custom resource providers. JAX-RS also supports pluggable type system for encoding and decoding of a Java type to/from an entity of an HTTP response/request. Some of these advanced concepts deserve more space and possibly another post.
Great article!! Just curious as to why you started with the schema and generated your response object? Is there a way to also create the response with just regular POJOs?
Thanks,
Steve
LikeLike
Thanks Steve.
You can start with a pojo too. The response is tied to the media type produced by your resource. If you want to generate application/xml or application/json response types, your pojo needs to be annotated with JAXB annotations as shown in the below code.
Let me know if this helps.
-Arul
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.api.container.httpserver.HttpServerFactory;
import com.sun.net.httpserver.HttpServer;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.ProduceMime;
import javax.ws.rs.core.Response;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
@Path(“/movies”)
public class MovieResource {
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = “movieDetails”, propOrder = {
“title”,
“genres”,
“directedBy”
})
public static class MovieDetails {
@XmlElement(required = true)
private String title;
@XmlElement(required = true)
private String genres;
@XmlElement(required = true)
private String directedBy;
@XmlAttribute
private Integer rank;
public String getDirectedBy() {
return directedBy;
}
public void setDirectedBy(String directedBy) {
this.directedBy = directedBy;
}
public String getGenres() {
return genres;
}
public void setGenres(String genres) {
this.genres = genres;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public Integer getRank() {
return rank;
}
public void setRank(Integer rank) {
this.rank = rank;
}
}
@GET
@ProduceMime({“application/xml”, “application/json”})
public Response getTopBoxOfficeMovieThisWeek() {
MovieDetails pojo = new MovieDetails();
pojo.setTitle(“WALL-E”);
pojo.setDirectedBy(“Andrew Stanton”);
pojo.setRank(1);
return Response.ok(pojo).build();
}
public static void assertEquals(Object o1, Object o2) {
if (!o1.equals(o2)) {
System.out.println(o1 + ” != ” + o2);
throw new RuntimeException();
}
}
public static void client() throws Exception {
Client client = Client.create();
WebResource resource = client.resource(“http://localhost:9999/movies”);
MovieDetails pojo = resource.accept(“application/xml”).get(MovieDetails.class);
assertEquals(“WALL-E”, pojo.getTitle());
assertEquals(1, pojo.getRank());
System.out.println(“XML Response :” + resource.accept(“application/xml”).get(String.class));
System.out.println(“JSON Response :” + resource.accept(“application/json”).get(String.class));
}
public static void main(String[] args) throws Exception {
HttpServer s = HttpServerFactory.create(“http://localhost:9999/”);
s.start();
try {
client();
} finally {
s.stop(0);
}
}
}
LikeLike