Thursday 15 September 2011

Java Rest with Jersey

Rest with Jersey

In my last blog post http://credindustries.blogspot.com/2011/09/what-is-rest.html
I talked about the principals behind REST. Now we will discuss the implantation
details using the jersey platform. For more code examples visit http://code.google.com/p/java-rest-platform/

I spent a lot of time piecing together different parts of Jersey  and below I am just covering the very basics. But if you are interested in using jersey I very much recommend this book.



Your Jersey resources can be either  a singletons that will handle all requests or a class that will be instantiated per request and disposed of after the request is done. Since the object is created for each request it is stateless, where as a singleton can hold state between requests. In my project I chose to use classes that are instantiated with each request.

We will need to tell your server container where your jersey resources are located. This is done through the web.xml, which provides configuration and deployment details for the web services and resources.

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaeehttp://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
<display-name>rest</display-name>
<servlet>
  <servlet-name>Jersey REST Service</servlet-name> 
  <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
  <init-param>
    <param-name>com.sun.jersey.config.property.packages</param-name>
    <param-value>com.cred.industries.platform.resources</param-value>
  </init-param>
  <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Jersey REST Service</servlet-name>
  <url-pattern>/resource/*</url-pattern>
</servlet-mapping>
</web-app>


Here we are creating a servelet for jersey where the init-param of
    com.sun.jersey.config.property.packages 
tells us what package our jersey classes are going to be in. In our case our jersey resources will be in the 
    com.cred.industries.platform.resources 
package. All Jersey resources in that package will be accessible under the /resources/ url.
In my last blog post I talked about 5 principals of Rest, now we will look at how jersey handles those principals. If you haven't read that post I recommend reading it first as we build on those concepts here.  

Addressable resources


Each resources within the system can be addressed by a unique URI.
Jersey uses the Java annotation @Path to make it very simple to create an address for a resources. We can use the @Path to expose classes and also member functions within classes to give them a sub path.

@Path("/customer")
public class CustomerRes {
    @POST
    @Path("/friend")
    public void addFriendToCustomer()

We are exposing the CustomerRes class under the path "/customer" then the member function addFriendToCustomer under the path "/friend". So to address the resources addFriendToCustomer we need to post to /resource/customer/friend. Using Rest we can also access parameters that are part of the path and query parameters.

       @GET
       @Path("/{id: \\d+}")
       public CustomerTrans getCustomerById(@PathParam("id") int custId)

In this example we are exposing the getCustomerById member function under the path "/{id: \\d+}. the id: will map to the @PathParam("id") and the \\d+ tells jersey to match it to a regular expression \d+ which will match one or more digits.

    @GET
    @Path("/group/{id}")
    public CustomerTransList getCustomersByName(
        @PathParam("id") String custName,
        @DefaultValue("0") @QueryParam("start") int setStart,
        @DefaultValue("10") @QueryParam("size") int setSize)



We can also have a mix of path and query parameters in a request. This request would look like:
  http://localhost:8080/platform/rest/customer/group/name?start=100&size=20
Where "name" is the customer we are looking for and start and size tells us where in the list we are looking and how many to get.

A standardized interface
There is a limited set of methods and operations that we can use to access our resources.
Jersey uses Java annotations @GET @POST @PUT and @DELETE to specify the verb or action we are to take on the resource.

    @POST
    @Path("/friend")
    public void addFriendToCustomer(@QueryParam("id") int custId)
    
    @PUT
    @Path("/friend")
    public void updateFriendOfCustomer(@QueryParam("id") int custId)

    @DELETE
    @Path("/friend")
    public void removeFriendFromCustomer(@QueryParam("id") int custId)

    @GET
    @Path("/friend")
    public friendsTrans getCustomersFriends(@QueryParam("id") int custId)

All of these would be accessed under the path "/resource/customer/friend" but depending on the http method they will do very different things. That is why we consider the friend resources as the noun and the http methods as the verbs. Using different verbs with a noun will do very different things. Using the "POST" verb and the "/friend" noun we will add a new friend, but if we change the verb to "DELETE" we are now deleting friends.

Self-descriptive messages
Each request to the server specifies the media type that it produces and consumes such that a single resource can handle different formats.
Again Jersey uses Java annotations to define both the format that the server accepts and the format that it produces.


    @GET
    @Path("/group/{id}")
    @Consumes(MediaType.TEXT_PLAIN)
    @Produces(MediaType.APPLICATION_XML)
    public CustomerTransList getCustomersByName( 
        @PathParam("id") String custName,
        @DefaultValue("0") @QueryParam("start") int setStart,
        @DefaultValue("10") @QueryParam("size") int setSize)

    @GET
    @Path("/group/{id}")
    @Consumes(MediaType.TEXT_PLAIN)
    @Produces(MediaType.APPLICATION_JSON)
    public CustomerTransList getCustomersByName(
        @PathParam("id") String custName,
        @DefaultValue("0") @QueryParam("start") int setStart,
        @DefaultValue("10") @QueryParam("size") int setSize)

We have the same path producing different formats depending on what the client asks for. You have probably seen me returning an object CustomerTrans. These are what I call transfer objects and are built using JAXB. Jersey supports JAXB so you can automatically marshal and un-marshal objects without having to parse xml or json. By sending a matching XML to a resources jersey in the body of your HTTP message Jersey will automatically un-marshal it into a parameter.

       @POST
       @Path("/create")
       @Consumes(MediaType.APPLICATION_XML)
       @Produces(MediaType.APPLICATION_XML)
       public CustomerTrans createCustomer(CustomerTrans custTrans)
 
This will take a XML object representing the CustomerTrans xml that was in the HTTP body and convert it to object that java can use.


To create JAXB objects we use Java annotations @XmlRootElement.

@XmlRootElement(name = "customerTrans")
public class CustomerTrans {

    @XmlAttribute(name = "customerId")
    public int mCustomerId;

    @XmlElement(name = "personaName")
    public String mPersonaName;
 
    public CustomerTrans() { }
    public CustomerTrans(int customerId, String personaName) {

        super();
        this.mCustomerId = customerId;
        this.mPersonaName = personaName;
    }
}
 
To create the JAXB object you need to annotate a class with @XmlRootElement and give it a name, in our case customerTrans. Then we can expose member variables or functions using @XmlAttribute and @XmlElement to define something as an xml attribute or and xml element respectively.
 
This will marshal and un-marshal xml in this format.

<customerTrans customerId="3">
  <personaName>test</personaName>
</customerTrans>

Communicate stateless 

Each request must provide sufficient information, so the server can respond to it without needing client session state on the server.
Each request to jersey by default does not maintain client state from one request to the next. So your client needs to pass all information need for jersey to find and process your request.

Hypermedia as the engine of application state (HATEOAS)

Clients discover the resources and interfaces available to them from the server not from fixed hardcoded paths.

As an example of this we will look at the request
    http://localhost:8080/platform/rest/customer/group/pa?start=0&size=20
As you can see the client sends the information that we are looking for all customers matching "pa" and we want the set starting from 0 including up to 20. But when sending the result we don't want to keep state on the server about what the next or previous set of customers so we send links to the next and previous set with the results from this set.

   @GET
   @Path("/group/{id}")
   @Consumes(MediaType.TEXT_PLAIN)
   @Produces(MediaType.APPLICATION_XML)
   public CustomerTransList getCustomersByName(
           @PathParam("id") String custName,
           @DefaultValue("0") @QueryParam("start") int setStart,
           @DefaultValue("10") @QueryParam("size") int setSize) {

       List<CustomerTrans> custTrans = new ArrayList<CustomerTrans>();

       //use the customer facade to find the customer
       CustomerFacade custFacade = new CustomerFacade();

       //we get the set size plus one so we can tell if there is a 
       //next result set
       custTrans = custFacade.getCustomersByName(custName, 
                                    setStart, setSize + 1);

       //when returning the links to the next/previous sets we 
       //need to figure but where we are since we tried to grab 
       //one more than the asked for size we can tell if there
       //are more results.  If there are more results we will
       //add a link to them and remove the extra customer found
       boolean isNextSet = false;
       if(custTrans.size() > setSize) {
           isNextSet = true;
           custTrans.remove(setSize);
       }
                    
       URI next = null;
       URI prev = null;  
               

       //the next link is the start plus the number of cust found
       UriBuilder linkBuilder = mUri.getAbsolutePathBuilder();
       if(isNextSet) {

           linkBuilder.queryParam("start", setStart + custTrans.size());
           linkBuilder.queryParam("size", setSize);
           next = linkBuilder.build();
       }

       //the previous link is the min of start minus the result set size or 0
       //If 0 we dont send a link back         
       int previousStart =  Math.max(setStart - setSize, 0);
       if(setStart != 0) {
           linkBuilder.queryParam("start", previousStart);
           linkBuilder.queryParam("size", setSize);
           prev = linkBuilder.build();
       }

       //add the found customers
       CustomerTransList result = new CustomerTransList(custTrans);
       if(next != null) {
           result.setNext(new LinkTrans("next", next.toString()));
       } else {
           result.setNext(new LinkTrans("next", ""));
       }

       if(prev != null) {
           result.setPrevious(new LinkTrans("prev", prev.toString()));
       } else {
           result.setPrevious(new LinkTrans("prev", ""));
       }
       return result;