Recently, I had to write a few services using REST style in Java. Though I have developed such services in past either using Java Servlet APIs, but I wanted to find something better. I had found writing REST style services in Ruby on Rails a breeze and I wanted something as testable and easy to configure. I found a new JSR 311 that does exactly that. Similar to EJB 3.0 and Servlet 3.0 trend, it allows you to write REST services using annotations. I found an open source project that implements JSR 311 and just released 1.0. I found most of the documentation was Maven and Glassfish driven and I wanted to simply try on Tomcat and Ant so here is how I did it:
Download and Install
First, download following jar libraries (you can see that I am using jars from maven repository because Jersey’s wiki was missing jars or pointing to old version):
Developing
My application used models to describe business object, DAOs for database access and I didn’t want to pollute my business objects with REST annotations so I created two new packages for resources and services. Here is a simple example that follows this separation of concerns:
Business Objects (Model)
The model simply consists of a contact class that stores contact information for a person.
package rest;
public class Contact {
private final String name;
private final String email;
private final String address;
public Contact(final String name, final String email, final String address) {
this.name = name;
this.email = email;
this.address = address;
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
public String getAddress() {
return address;
}
}
Data Access Layer
The data access layer simply uses a hashmap for storing and accessing these contacts, e.g.
package rest;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
public class ContactDao {
private static Map repository = new HashMap();
public boolean save(Contact c) {
boolean created = repository.get(c.getName()) != null;
repository.put(c.getName(), c);
return created;
}
public boolean delete(String name) {
return repository.remove(name) != null;
}
public Contact get(String name) {
return repository.get(name);
}
public Collection getAll() {
return repository.values();
}
}
Resources
For resources, I defined ContactResource that adds XML annotations to convert Contact into XML and ContactsResponse for returning complete XML response, e.g.
package rest;
import javax.ws.rs.core.UriInfo;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
@XmlAccessorType(XmlAccessType.PROPERTY)
@XmlRootElement(name = "Contact")
public class ContactResource {
private UriInfo uriInfo;
private Contact contact;
public ContactResource(final UriInfo uriInfo, final String name, final String email, final String address) {
this.uriInfo = uriInfo;
this.contact = new Contact(name, email, address);
}
public ContactResource(final Contact contact) {
this.contact = contact;
}
ContactResource() {
}
public JSONObject toJson() throws JSONException {
return new JSONObject()
.put("name", contact.getName())
.put("email", contact.getEmail())
.put("address",contact.getAddress());
}
@XmlElement(name = "Name")
public String getName() {
return contact.getName();
}
@XmlElement(name = "Email")
public String getEmail() {
return contact.getEmail();
}
@XmlElement(name = "Address")
public String getAddress() {
return contact.getAddress();
}
}
package rest;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
@XmlAccessorType(XmlAccessType.PROPERTY)
@XmlRootElement(name = "ContactsResponse")
public class ContactsResponse {
private String uri;
private String status;
private Collection contacts;
public ContactsResponse(String uri, String status, Collection contacts) {
this.uri = uri;
this.status = status;
this.contacts = new ArrayList();
for (Contact contact : contacts) {
this.contacts.add(new ContactResource(contact));
}
}
public ContactsResponse(String uri, String status, Contact contact) {
this(uri, status, contact == null ? new ArrayList() : Collections.singleton(contact));
}
ContactsResponse() {
}
@XmlElement(name = "Uri")
public String getUri() {
return uri;
}
public void setUri(String uri) {
this.uri = uri;
}
@XmlElement(name = "Status")
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
@XmlElement(name = "Contacts")
public Collection getContactResources() {
return contacts;
}
public void setContactResources(Collection contacts) {
this.contacts = contacts;
}
}
Service
Here is the meat of JSR 311 that defines annotations for the REST based web service, i.e.,
package rest;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.springframework.context.annotation.Scope;
import com.sun.jersey.api.spring.Autowire;
@Path("/contacts/")
@Produces( { "application/json", "application/xml" })
@Consumes( { "application/json", "application/xml" })
@Scope("singleton")
@Autowire
public class ContactService {
private final ContactDao contactDao;
@Context UriInfo uriInfo;
@Context Request request;
public ContactService(ContactDao dao) {
this.contactDao = dao;
}
public ContactService() {
this(new ContactDao()); // this will be injected in real-app
}
/**
* create contact
*/
@PUT
@Consumes("*/*")
@Produces("application/xml")
@Path("{name}")
public Response createcontact(
@PathParam("name") String name,
@FormParam("email") String email,
@FormParam("address") String address) {
Contact contact = new Contact(name, email, address);
final boolean newRecord = contactDao.save(contact);
if (newRecord) {
try {
URI uri = uriInfo != null ? uriInfo.getAbsolutePath()
: new URI("/contacts/");
return Response.created(uri).build();
} catch (URISyntaxException e) {
throw new RuntimeException("Failed to create uri", e);
}
} else {
return Response.noContent().build();
}
}
/**
* deletes contact
*
*/
@DELETE
@Consumes("*/*")
@Path("{name}")
public Response deletecontact(@PathParam("name") String name) {
boolean deleted = contactDao.delete(name);
if (deleted) {
return Response.ok().build();
} else {
return Response.status(404).build();
}
}
/**
* @return contact in XML format
*/
@GET
@Consumes({"text/xml", "application/xml"})
@Produces("application/xml")
@Path("{name}")
public ContactsResponse getcontactByXml(
@PathParam("name") String name) {
Contact contact = contactDao.get(name);
String uri = uriInfo != null ? uriInfo.getAbsolutePath().toString() : "/contacts/";
return new ContactsResponse(uri, "success", contact);
}
/**
* @return contact in JSON format
*/
@GET
@Consumes("application/json")
@Produces("application/json")
@Path("{name}")
public JSONArray getcontactByJson(@PathParam("name") String name) throws JSONException {
Contact contact = contactDao.get(name);
JSONArray arr = new JSONArray();
arr.put(new ContactResource(contact).toJson());
return arr;
}
/**
* @return all contacts in XML format
*/
@GET
@Consumes({"text/xml", "application/xml"})
@Produces("application/xml")
public ContactsResponse getAllByXml() {
Collection contacts = contactDao.getAll();
String uri = uriInfo != null ? uriInfo.getAbsolutePath().toString() : "/contacts/";
return new ContactsResponse(uri, "success", contacts);
}
/**
* @return contacts in JSON format
*/
@GET
@Consumes("application/json")
@Produces("application/json")
public JSONArray getAllByJson() throws JSONException {
Collection contacts = contactDao.getAll();
JSONArray arr = new JSONArray();
for (Contact contact : contacts) {
arr.put(new ContactResource(contact).toJson());
}
return arr;
}
}
A few things to note:
- @Path defines the URI used for accessing the service
- I am using @PUT to store contacts (as the user is creating new URI as opposed to @POST where the application uses same URI). Also, I don’t have any method for update (which uses PUT) as I already am using PUT and create method simply updates the contact if it already exist.
- @PathParam is retrieved from the URI itself, e.g. /contacts/myname
- @FormParam is retrieved from POST form submission
- I can use the same URI and return different type of data based on Content-Type, e.g. when user sets it to application/xml or text/xml I return XML content and when user sets it to application/json I return JSON format.
- To return list of contacts I skip the name and simply use /contacts/
Servlet Configuration
I added servlet to handle REST requests to web.xml, e.g.
<servlet>
<servlet-name>RestServlet</servlet-name>
<servlet-class>com.sun.jersey.spi.spring.container.servlet.SpringServlet</servlet-class>
<init-param>
<param-name>com.sun.jersey.config.feature.Redirect</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>com.sun.jersey.config.feature.ImplicitViewables</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>com.sun.jersey.config.property.packages</param-name>
<param-value>rest</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>RestServlet</servlet-name>
<url-pattern>/svc/*</url-pattern>
</servlet-mapping>
I used Spring to inject DAOs in real application but if you don’t need it then replace com.sun.jersey.spi.spring.container.servlet.SpringServlet with com.sun.jersey.spi.container.servlet.ServletContainer.
Also note that com.sun.jersey.config.property.packages defines package name of Java classes that defines service classes.
Deploying
I am assuming you already know how to package a war file and deploy it to application server such as Tomcat.
Testing it
Unit Testing it
A big advantage of JSR 311 is ease of test, e.g. here is a sample unit test:
package rest;
import javax.ws.rs.core.Response;
import junit.framework.TestCase;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONObject;
import com.sun.jersey.api.NotFoundException;
public class ContactServiceTest extends TestCase {
private ContactService service;
@Override
protected void setUp() throws Exception {
service = new ContactService();
}
public void testCreateContactWithGetContactByXml() {
final String name = "shahbhat";
final String email = "shahbhat@myhost";
final String address = "shahbhat address ";
Response response = service.createContact(name, email, address);
assertEquals(201, response.getStatus());
assertNull(response.getEntity());
// recreate the same mapping and it should return no content
response = service.createContact(name, email, address);
assertEquals(204, response.getStatus());
assertNull(response.getEntity());
ContactsResponse contactResponse = service.getContactByXml(name);
assertEquals(1, contactResponse.getContactResources().size());
ContactResource contactResource = contactResponse.getContactResources().iterator().next();
assertEquals(name, contactResource.getName());
assertEquals(email, contactResource.getEmail());
assertEquals(address, contactResource.getAddress());
}
public void testCreateContactWithGetContactByJson() throws Exception {
final String name = "shahbhat";
final String email = "shahbhat@myhost";
final String address = "shahbhat address ";
Response response = service.createContact(name, email, address);
assertEquals(201, response.getStatus());
assertNull(response.getEntity());
JSONArray jsonArray = service.getContactByJson(name);
assertEquals(1, jsonArray.length());
JSONObject json = jsonArray.getJSONObject(0);
assertEquals(name, json.getString("name"));
assertEquals(email, json.getString("email"));
assertEquals(address, json.getString("address"));
}
public void testDeleteContact() {
final String name = "shahbhat";
final String email = "shahbhat@myhost";
final String address = "shahbhat address ";
Response response = service.createContact(name, email, address);
assertEquals(201, response.getStatus());
assertNull(response.getEntity());
ContactsResponse contactResponse = service.getContactByXml(name);
assertEquals(1, contactResponse.getContactResources().size());
ContactResource contactResource = contactResponse.getContactResources().iterator().next();
assertEquals(name, contactResource.getName());
assertEquals(email, contactResource.getEmail());
assertEquals(address, contactResource.getAddress());
response = service.deleteContact(name);
assertEquals(200, response.getStatus());
contactResponse = service.getContactByXml(name);
assertEquals(0, contactResponse.getContactResources().size());
//
response = service.deleteContact(name);
assertEquals(404, response.getStatus());
}
public void testGetAllContactByXml() {
service.createContact("shahbhat", "shahbhat email", "shahbhat address");
service.createContact("bill", "bill email", "bill address");
ContactsResponse contactResponse = service.getAllByXml();
assertEquals(2, contactResponse.getContactResources().size());
for (ContactResource contactResource : contactResponse.getContactResources()) {
if ("shahbhat".equals(contactResource.getName())) {
assertEquals("shahbhat email", contactResource.getEmail());
assertEquals("shahbhat address", contactResource.getAddress());
} else if ("bill".equals(contactResource.getName())) {
assertEquals("bill email", contactResource.getEmail());
assertEquals("bill address", contactResource.getAddress());
} else {
fail("unknown contact " + contactResource);
}
}
service.deleteContact("shahbhat");
service.deleteContact("bill");
contactResponse = service.getAllByXml();
assertEquals(0, contactResponse.getContactResources().size());
}
public void testGetAllContactByJson() throws Exception {
service.createContact("shahbhat", "shahbhat email", "shahbhat address");
service.createContact("bill", "bill email", "bill address");
JSONArray jsonArray = service.getAllByJson();
assertEquals(2, jsonArray.length());
for (int i=0; i<2; i++) {
JSONObject json = jsonArray.getJSONObject(i);
if ("shahbhat".equals(json.getString("name"))) {
assertEquals("shahbhat email", json.getString("email"));
assertEquals("shahbhat address", json.getString("address"));
} else if ("bill".equals(json.getString("name"))) {
assertEquals("bill email", json.getString("email"));
assertEquals("bill address", json.getString("address"));
} else {
fail("unknown contact " + json);
}
}
service.deleteContact("shahbhat");
service.deleteContact("bill");
jsonArray = service.getAllByJson();
assertEquals(0, jsonArray.length());
}
}
Functional Testing it
Once deploy, you can use curl to functionally test it (though there are other automated tools available as well), e.g
Creating Contact
curl -X PUT -d "email=myemail&address=myaddress" http://shahbhat.desktop:8080/svc/contacts/bhatti
It will create a contact and to retrieve it, use
curl http://shahbhat.desktop:8080/svc/contacts/bhatti
It should return
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><ContactsResponse><Contacts><Address>myaddress</Address><Email>myemail</Email><Name>bhatti</Name></Contacts><Status>success</Status><Uri>http://shahbhat.desktop:8080/svc/contacts/bhatti</Uri></ContactsResponse>
To get JSON format use
curl --header "Content-Type: application/json" http://shahbhat.desktop:8080/svc/contacts/bhatti
and it should return
[{"name":"bhatti","email":"myemail","address":"myaddress"}]
You can create another contact e.g.
curl -d "email=billemail&address=billaddress" http://shahbhat.desktop:8080/svc/contacts/bill
Now to get all contacts use
curl http://shahbhat.desktop:8080/svc/contacts/
and it will return
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><ContactsResponse><Contacts><Address>myaddress</Address><Email>myemail</Email><Name>bhatti</Name></Contacts><Contacts><Address>billaddress</Address><Email>billemail</Email><Name>bill</Name></Contacts><Status>success</Status><Uri>http://shahbhat.desktop:8080/svc/contacts/</Uri></ContactsResponse>
And to get JSON format use
curl --header "Content-Type: application/json" http://shahbhat.desktop:8080/svc/contacts/
which will return
[{"name":"bhatti","email":"myemail","address":"myaddress"},{"name":"bill","email":"billemail","address":"billaddress"}]
Conclusion
I found this approach is easily testable with standard unit test and easy to develop. Though, there are few glitches, e.g. @QueryParam only works with GET and if you are POSTing to something like /uri?x=yy then you won't see query parameters. But, overall I am happy with the outcome and will continue to use this framework.