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

Be Sociable, Share!