In an earlier post I’ve discussed setting up Tomcat and Eclipse environment for JAX-RS. This post is an attempt to discuss some more basics of JAX-RS, specifically performing CRUD operations.
Prerequisites
- Setup environment, discussed in this post
- Commons-HttpClient is used for the client program. Commons-codec and Commons-logging are its dependencies. All of them can be downloaded from Apache Commons website. Put those jars in the web application’s lib.
Now some fun
If you are remotely interested in REST read Roy Fielding’s dissertation on the topic. The chapter on REST is a must read, not necessary to understand this exercise but strongly recommended to gain a big picture.
As a part of the CRUD (creating, reading, updating and deleting) exercise let’s try and create a Customer resource, retrieve Customer details, update the details and finally delete the Customer. To keep it simple and to keep the focus on the topic at hand I will not use a real database, instead use a file system leveraging Java’s Properties mechanism.
Our customer POJO has got 4 properties: id, first name, last name and a zip code.
public class Customer {
private Integer customerId;
private String firstName;
private String lastName;
private String zipcode;
//setters and getters
}
Create
Let’s start with CustomerResource class. A resource class is a Java class that uses JAX-RS annotations to implement corresponding Web resource. According to the specification, resource classes are POJOs that have at least one method annotated with @Path or a request method designator.
@Path ("customer")
public class CustomerResource {
public static final String DATA_FILE = "C:\\TEMP\\customer-data.txt";
@POST
@Consumes ("application/xml")
public Response addCustomer(InputStream customerData) {
try {
Customer customer = buildCustomer(null, customerData);
long customerId = persist(customer, 0);
return Response.created(URI.create("/" + customerId)).build();
} catch (Exception e) {
throw new WebApplicationException(e, Response.Status.INTERNAL_SERVER_ERROR);
}
}
- Paths are relative. For an annotated class the base URI is the application context. For an annotated method the base URI is the effective URI of the contatining class. Base URIs are treated as if they ended in “/”. So in the above example first line indicates how the Customer URI is mapped to
/customer. - addCustomer method is annotated as a method that responds to the HTTP POST method calls. As there is no @Path annotation on the method, the URI of the class is also the URI to access the method.
- Another important annotation is Consumes. It defines the media types that the methods of a resource class can accept. If not specified, it is assumed that any media type is acceptable. We don’t have a Consumes annotation at the class level in this example, but if there is one the method level annotation takes precedence over the class level annotation.
- Response is returned to the client which contains a URI to the newly created resource. Return type Response results in an entity body mapped from the entity property of the Response with the status code specified by the status property of the response.
- WebApplicationException is a RuntimeException that is used to wrap the appropriate HTTP status codes.
- addCustomer method builds the customer from the received XML input and persists it. Code for the methods called in addMethod() is provided below and is self-explanatory. These methods are used for both create and update purposes and hence the method signatures take Customer as a parameter.
private long persist(Customer customer, long customerId) throws IOException {
Properties properties = new Properties();
properties.load(new FileInputStream(DATA_FILE));
if (customerId == 0) {
customerId = System.currentTimeMillis();
}
properties.setProperty(String.valueOf(customerId),
customer.getFirstName() + "," + customer.getLastName() +
"," + customer.getZipcode());
properties.store(new FileOutputStream(DATA_FILE), null);
return customerId;
}
private Customer buildCustomer(Customer customer, InputStream customerData)
throws ParserConfigurationException, SAXException, IOException {
if (customer == null) {
customer = new Customer();
}
DocumentBuilder documentBuilder =
DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document document = documentBuilder.parse(customerData);
document.getDocumentElement().normalize();
NodeList nodeList = document.getElementsByTagName("customer");
Node customerRoot = nodeList.item(0);
if (customerRoot.getNodeType() == Node.ELEMENT_NODE) {
Element element = (Element) customerRoot;
NodeList childNodes = element.getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
Element childElement = (Element)childNodes.item(i);
String tagName = childElement.getTagName();
String textContent = childElement.getTextContent();
if (tagName.equals("firstName")) {
customer.setFirstName(textContent);
} else if (tagName.equals("lastName")) {
customer.setLastName(textContent);
} else if (tagName.equals("zipcode")) {
customer.setZipcode(textContent);
}
}
} else {
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
return customer;
}
Test it
A test client method to test add operation looks something like the following, you may run this as a Java application via a main() method or it is easy to modify the following as a Junit test case.
private static void testAddCustomer() throws IOException, HttpException {
final String addCustomerXML =
"<customer>" +
"<firstname>Joe</firstname>" +
"<lastname>Schmo</lastname>" +
"<zipcode>98042</zipcode>" +
"</customer>";
PostMethod postMethod = new PostMethod(
"http://localhost:9000/RestExample/customer");
RequestEntity entity = new InputStreamRequestEntity(
new ByteArrayInputStream(addCustomerXML.getBytes()),
"application/xml");
postMethod.setRequestEntity(entity);
HttpClient client = new HttpClient();
try {
int result = client.executeMethod(postMethod);
System.out.println("Response status code: " + result);
System.out.println("Response headers:");
Header[] headers = postMethod.getResponseHeaders();
for (int i = 0; i < headers.length; i++) {
System.out.println(headers[i].toString());
} finally {
postMethod.releaseConnection();
}
}
Output
Response status code: 201
Response headers:
Server: Apache-Coyote/1.1
Location: http://localhost:9000/RestExample/customer/1236708444823
Content-Length: 0
Date: Thu, 05 Mar 2009 12:15:22 GMT
- Response status code 201 indicates the resource was created
- Location header displays the URI of the new resource that is created
Read
retrieveCustomer() method below illustrates the read operation:
@GET
@Path ("{id}")
@Produces ("application/xml")
public StreamingOutput retrieveCustomer(@PathParam ("id") String customerId) {
try {
String customerDetails = loadCustomer(customerId);
System.out.println("customerDetails: " + customerDetails);
if (customerDetails == null) {
throw new NotFoundException("<error>No customer with id: " +
customerId + "</error>");
}
final String[] details = customerDetails.split(",");
return new StreamingOutput() {
public void write(OutputStream outputStream) {
PrintWriter out = new PrintWriter(outputStream);
out.println("< ?xml version=\"1.0\" encoding=\"UTF-8\"?>");
out.println("<customer>");
out.println("<firstname>" + details[0] + "</firstname>");
out.println("<lastname>" + details[1] + "</lastname>");
out.println("<zipcode>" + details[2] + "</zipcode>");
out.println("</customer>");
out.close();
}
};
} catch (IOException e) {
throw new WebApplicationException(e,
Response.Status.INTERNAL_SERVER_ERROR);
}
}
- Get annotation indicates that this method is responsible for HTTP GET operations
- Path annotation indicates a dynamic id. As discussed above the paths are relative, so the URI /customer/{id} is the resultant path for this method
- Produces annotation, indicates the media type that is resulted from this operation
- PathParam annotation in the parameter reads the path id that is passed using the Path annotation
- StreamingOutput is returned by this resource method. StreamingOutput is a simpler version of MessageBodyWriter. It has a write() method that has Output stream. All that you need is to write the data into that object, which will be returned to the client program.
Test it
In a web browser you may try the URI that was resulted from our create operation above (http://localhost:9000/RestExample/customer/1236708444823 for this example).
Output
You should see response something like the followiing:
< ?xml version="1.0" encoding="UTF-8" ?> - <customer> <firstname>Joe</firstname> <lastname>Schmo</lastname> <zipcode>98042</zipcode> </customer>
Update
POST is used create a new resource, PUT method updates the state of a known resource. You often see discussions about when to use POST vs PUT. My understanding is that if you are trying to create a brand new resource use POST, no Request-URI is required for POST. State is sent as a part of the BODY and the server should return you back HTTP code 201 (resource created), just like what we discussed in the CREATE section above. On the other hand, if you’re updating the state of a resource use PUT. In this case you need the URI to the resource that you are trying to update.
Update code follows:
@PUT
@Path("{id}")
@Consumes("application/xml")
public void updateCustomer(@PathParam("id") String customerId,
InputStream input) {
try {
String customerDetails = loadCustomer(customerId);
if (customerDetails == null) {
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
String[] details = customerDetails.split(",");
Customer customer = new Customer();
customer.setFirstName(details[0]);
customer.setLastName(details[1]);
customer.setZipcode(details[2]);
buildCustomer(customer, input);
persist(customer, Long.valueOf(customerId));
} catch (Exception e) {
throw new WebApplicationException(e,
Response.Status.INTERNAL_SERVER_ERROR);
}
}
- PUT annotation marks the method as a resource method that handles HTTP PUT requests
- Path and Consumes annotations are already discussed in the earlier sections
Test it
Using HttpClient the update test can be performed as follows. In this example zip code of a customer is being updated:
private static void testUpdateCustomer() throws IOException, HttpException {
final String addCustomerXML =
"<customer>" +
"<zipcode>98043</zipcode>" +
"</customer>";
PutMethod putMethod = new PutMethod(
"http://localhost:9000/RestExample/customer/1236708444823");
RequestEntity entity = new InputStreamRequestEntity(
new ByteArrayInputStream(addCustomerXML.getBytes()),
"application/xml");
putMethod.setRequestEntity(entity);
HttpClient client = new HttpClient();
try {
int result = client.executeMethod(putMethod);
System.out.println("Response status code: " + result);
System.out.println("Response headers:");
Header[] headers = putMethod.getResponseHeaders();
for (int i = 0; i < headers.length; i++) {
System.out.println(headers[i].toString());
}
} finally {
putMethod.releaseConnection();
}
}
Delete
The HTTP DELETE method requests the server to delete the resource identified by the request-URI.
@DELETE
@Path("{id}")
public void deleteCustomer(@PathParam("id") String customerId) {
try {
Properties properties = new Properties();
properties.load(new FileInputStream(DATA_FILE));
String customerDetails = properties.getProperty(customerId);
if (customerDetails == null) {
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
properties.remove(customerId);
properties.store(new FileOutputStream(DATA_FILE), null);
} catch (Exception e) {
throw new WebApplicationException(e, Response.Status.INTERNAL_SERVER_ERROR);
}
}
- DELETE annotation is used to mark the method as a resource method that handles the resource-delete operation.
Test it
Test code for Delete. Attempting to delete the customer resource created earlier:
private static void testDeleteCustomer() throws HttpException, IOException {
DeleteMethod deleteMethod = new DeleteMethod(
"http://localhost:9000/RestExample/customer/1236708444823");
HttpClient client = new HttpClient();
try {
int result = client.executeMethod(deleteMethod);
System.out.println("Response status code: " + result);
System.out.println("Response headers:");
Header[] headers = deleteMethod.getResponseHeaders();
for (int i = 0; i < headers.length; i++) {
System.out.println(headers[i].toString());
}
} finally {
deleteMethod.releaseConnection();
}
}
output
Status code 204 is returned, stands for No Content. This code is used in cases where the request is successfully processed, but the response doesn’t have a message body
Response status code: 204
Response headers:
Server: Apache-Coyote/1.1
Date: Tue, 10 Mar 2009 18:34:46 GMT
You may also like:
Follow on Twitter
#1 by Solomon on March 11th, 2009
Quote
Out of curiosity, why aren’t you using JAXB to parse the XML?
[Reply]
Surya Suravarapu Reply:
March 11th, 2009 at 9:57 am
@Solomon I like JAXB, my favorite indeed. The schema for this example is a simple one so thought of parsing the old way. Primary focus of the post is on JAX-RS and its annotation-based approach (same reason why I haven’t used a “real” database for this example).
[Reply]
Pingback: Java Annotations - Java’s love of configuration over convention « DAVEBLOG 5000
Pingback: b a r s » Blog Archive » REST: CRUD with JAX-RS (Jersey) - Surya Suravarapu’s Blog
#2 by fogus on April 6th, 2009
Quote
Nice article. I am constantly surprised at the scarcity of articles on Jersey. We use it on a project at work and I have found it to be quite a pleasant experience so far.
-m
[Reply]
#3 by sumit on June 16th, 2009
Quote
Hey, as a beginner i have some really dumb questions. I need to write a RESTful service using the JSR 311 APIs and deploy these services on Tomcat 6.0. Could someone please help me with this ?? Give some pointers or links. Any help would be appreciated!
Thanks
[Reply]
#4 by sumit on June 16th, 2009
Quote
As in SOAP service we know how to deploy, but I find it difficult to understand how to Create and Deploy the REST service.
Some help needed
[Reply]
Surya Suravarapu Reply:
June 17th, 2009 at 6:49 am
As discussed offline — deploying a RESTful service usually is no different than deploying any other WAR/EAR on your application server.
[Reply]
#5 by pankaj on July 20th, 2009
Quote
Very helpful
[Reply]
#6 by pankaj on July 20th, 2009
Quote
Please give step wise details ,how to deploy jersey at tomcat
[Reply]
Surya Suravarapu Reply:
July 20th, 2009 at 8:13 pm
Not sure whether you looked at this post in which I detailed the setup and configuration http://www.suryasuravarapu.com/2009/02/rest-jersey-configuration-on-tomcat.html. Other than that the deployment is no different than any other WAR deployment.
[Reply]
#7 by arjun on July 21st, 2009
Quote
Hi Suravarapu
I was confused with testing the testAddCustomer()
What are the imports for the following to test the testAddCustomer().
PostMethod,HttpClient and RequestEntity
Regards
Arzun
[Reply]
Surya Suravarapu Reply:
July 21st, 2009 at 7:19 am
As mentioned in the ‘Prereequisites’ section of the article you need Apache Commons HttpClient in your build path. All 3 references you pointed out are related with HttpClient. API: http://hc.apache.org/httpclient-3.x/apidocs/
Also, if you would like to use a UI for testing I found rest-client extremely useful http://code.google.com/p/rest-client/
[Reply]
#8 by Nagesh on July 24th, 2009
Quote
Is there a way you can share the code as a downloadable
[Reply]
#9 by Michel on August 1st, 2009
Quote
Hi Surya,
Thanks a lot for your article very helpfull;
i didn’t find loadCustomer method.
Regards
Michel
[Reply]
#10 by Sumved Shami on August 4th, 2009
Quote
Hi Surya
It is a very basic and nice article on REST. It has really helped me to understand the concepts clearly. Thanks for your kind help.
Can you please also provide a link from where I can download the code?
Thanks again.
Regards,
Sumved Shami
[Reply]
#11 by Sumved Shami on August 4th, 2009
Quote
Also, I have one question.
In method: public Response addCustomer(InputStream customerData) { … } , from where InputStream object is coming.
Please bear with me little bit, as I am new to REST.
Thanks & Regards,
Sumved Shami
[Reply]
#12 by Mike on September 28th, 2009
Quote
The method loadCustomer(String) is undefined for the type CustomerResource.
Can you add it ?
Can you provide the source code in a zip file ?
Thanks.
[Reply]
#13 by Dave on October 13th, 2009
Quote
Hi Surya,
Very nice article.
Is there a code jar file so that I can download all the files, such as customer-data.txt, the file containing loadCustomer(customerId), StreamingOutput class.
Thanks alot
Dave
[Reply]
pulak Reply:
November 4th, 2009 at 1:31 am
Source code for CRUD with JAX-RS (Jersey) : http://www.4shared.com/file/146112993/3f2b6c71/src.html
[Reply]
#14 by bojan on January 29th, 2010
Quote
Any idea how to do this but using JSON instead of XML?
[Reply]
#15 by Greg on February 12th, 2010
Quote
This is a nice write up and very concise and well writen. I dont’ know why you didn’t take the next step and use jaxb for marshalling and unmarhsalling the xml.
bojan: If you annotate your model objects with jaxb annotations and include the jettison and jackson libraries you can just return the objects from the methods and they will be marshalled out as xml or json depending on the accepts header in the request. You will also need a jersey-json.jar. If you google on these you will find them easy to setup.
[Reply]