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 Maprepository = 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 Collectioncontacts; 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() { Collectioncontacts = 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.