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 pints on August 17, 2010 - 8:45 am
Quote
Hi,
I just wanted to create Cstomer so i created classes
Customer & CustomerResource
but on access the url http://localhost:8080/WS-RS/customer through brower
getting the following error message any clues??
type Status report
message Method Not Allowed
description The specified HTTP method is not allowed for the requested resource (Method Not Allowed).
[Reply]
Surya Suravarapu Reply:
August 17th, 2010 at 10:00 am
@pints: If you are using that URL from browser location bar — that translates to a GET request for the resource at WS-RS/customer. If you don’t have a GET request implemented on that resource (as it seems to be, based on the error message) you are receiving Method Not Allowed.
Also, you mentioned you wanted to create a Resource, which is a POST operation on the server (or PUT as some folks argue), and not a GET.
[Hint: Look at the annotations supporting each method. If you have to support a GET on a resource make sure that you have a @Get annotation over the method which implements the operation]
[Reply]
#2 by rohit on August 18, 2010 - 1:12 am
Quote
Thanks Surya.
[Reply]
#3 by rohit on August 19, 2010 - 4:04 am
Quote
Hi Surya,
One basic question if you can brief on it.
I created a java class and from main method called the method of your example to persist the Customer. Everything went well and the XML got written in .txt as propertes.
Could you please tell what was
return Response.created(URI.create(“/” + customerId)).build();
as when the execution reahced this line it did neither gave exception nor produced anything on browser.
Thanks in advance
[Reply]
#4 by rohit on August 19, 2010 - 4:20 am
Quote
I tried accessing
http://localhost:8080/WS-RS/customer/1282204638315
But it gave no resource found error 404
[Reply]