Guestbook Sample Application Guide

Version 1.0.0

Overview

The Guestbook sample demonstrates some scenarios for the handling of bad conditions, exceptions, and errors.

The sample is based on early Internet guestbooks where visitors leave new comments, update or delete old comments, or retrieve a list of comments. The comments are stored in an in-memory data store.

This sample is only for illustrative purposes and is not intended to be a starting point for a real guestbook.

URI Overview

Table 1. URI Overview
HTTP Method URI Input Type(s) Output Type(s) Note(s)
GET /guestbook/{id} n/a text/xml Sets HTTP status code
POST /guestbook text/xml text/xml Throws javax.ws.rs.WebApplicationException
DELETE /guestbook/{id} n/a n/a Throws RuntimeException
PUT /guestbook/{id} text/xml text/xml Throws checked exception

HTTP Status Codes

Most RESTful web services use standard HTTP status codes to some degree. You can set the status code in a javax.ws.rs.core.Response by calling its status method.

A simple HTTP GET method for retrieving a new message is displayed below. In the code example below, an infamous HTTP 404 status code is set if the comment with a given message ID does not exist. A normal HTTP 200 OK status and response entity are set if the message is found using one of Response's convienence methods.

@GET
@Path("{id}")
@Produces("text/xml")
public Response readComment(@PathParam("id") String commentId) {
    Comment msg = GuestbookDatabase.getGuestbook().getComment(Integer.valueOf(commentId));
    if (msg == null) {
        return Response.status(404).build();
    }

    return Response.ok(msg).build();
}

javax.ws.rs.WebApplicationException

Before talking about regular Java exceptions and errors, JAX-RS introduces the javax.ws.rs.WebApplicationException. It provides an easy way to throw an exception with a status code or a whole javax.ws.rs.core.Response. The status code or Response can be used by the runtime to construct a more usable end-user response. WebApplicationException is useful when you want to give a custom response but do not want to add a new checked exception to your method signature.

A HTTP POST method for creating a new message is given below.

A HTTP 400 status code is set if the POST requester did not send a comment to store via a WebApplicationException.

If an ID, message, or an author are not set in the comment request data, then a custom HTTP 400 status Response with an entity is constructed. The Response is then passed to a WebApplicationException which itself is thrown. By default, the JAX-RS runtime will use the Response to return the client response.

@POST
@Produces("text/xml")
@Consumes("text/xml")
public Response createComment(Comment aComment, @Context UriInfo uriInfo) {
    /*  
     * If no comment data was sent, then return a bad request.
     */
    if (aComment == null) {
        throw new WebApplicationException(Response.Status.BAD_REQUEST);
    }

    if (aComment.getId() == null || aComment.getMessage() == null
        || aComment.getAuthor() == null) {
        CommentError commentError = new CommentError();
        commentError.setErrorMessage("Please include a comment ID, a message, and your name.");
        Response resp = Response.status(Response.Status.BAD_REQUEST).entity(commentError)
            .build();
        throw new WebApplicationException(resp);
    }

    GuestbookDatabase.getGuestbook().storeComment(aComment);
    try {
        return Response.created(new URI(uriInfo.getAbsolutePath() + "/" + aComment.getId()))
            .entity(aComment).build();
    } catch (URISyntaxException e) {
        e.printStackTrace();
        throw new WebApplicationException(e);
    }
}

Runtime Exceptions, Errors, Checked Exceptions, and other Throwables

In a real application, you cannot wrap every exception in a WebApplicationException. In JAX-RS, runtime exceptions and errors propagate to the container. Checked exceptions and other throwables are wrapped in a container specific exception, and then the container exception is thrown. By allowing the exceptions to be thrown to the container, then the container's regular exception handling is used (e.g. servlet error pages).

Here is a HTTP DELETE resource method that shows a possible runtime exception:

@DELETE
@Path("{id}")
@Produces("text/xml")
public Response deleteComment(@PathParam("id") String commentId) {
    GuestbookDatabase.getGuestbook().deleteComment(Integer.valueOf(commentId));
    return Response.noContent().build();
}

In the above snippet, if the path parameter "commentId" is not an integer, then a NumberFormatException is thrown from Integer.valueOf(). The runtime exception would be thrown to the container.

Here is a HTTP PUT method that shows a possible checked exception:

@PUT
@Path("{id}")
@Produces("text/xml")
@Consumes("text/xml")
public Response updateComment(Comment aComment, @PathParam("id") String commentId)
    throws GuestbookException {
    if (aComment.getId() == null || !Integer.valueOf(commentId).equals(aComment.getId())) {
        throw new GuestbookException("Unexpected ID.");
    }

    Comment existingComment = GuestbookDatabase.getGuestbook().getComment(
        Integer.valueOf(commentId));
    if (existingComment == null) {
        throw new GuestbookException("Cannot find existing comment to update.");
    }
    GuestbookDatabase.getGuestbook().storeComment(aComment);
    return Response.ok(aComment).build();
}

GuestbookException is an application defined checked Java exception. There are many conditions where it is thrown. If the checked exception is thrown, then it is wrapped in a container-specific exception (such as a ServletException) and then re-thrown to the container.

ExceptionMapper

Allowing exceptions to be thrown to the container may be all you need but sometimes you may want to give a custom response. In the above HTTP PUT method, the request data could not include the Comment. The aComment.getId() call would result in a NullPointerException if "aComment" is null.

An javax.ws.rs.ext.ExceptionMapper<T> allows the JAX-RS runtime to catch any java.lang.Throwable and map it to a JAX-RS Response. You could do additional tasks as well such as logging the exception in the ExceptionMapper.

Here is an example of an ExceptionMapper for NullPointerException:

package com.ibm.rest.sample.guestbook;

import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

@Provider
public class NullPointerExceptionMapper implements ExceptionMapper<NullPointerException> {

    public Response toResponse(NullPointerException arg0) {
        CommentError commentError = new CommentError();
        commentError.setErrorMessage("Someone forgot a null check.");
        return Response.serverError().entity(commentError).build();
    }

}

Whenever a NullPointerException is thrown, the JAX-RS runtime will use the above ExceptionMapper to create a Response. The Response would then be used in normal processing.

To create an ExceptionMapper:

  1. Implement the ExceptionMapper<T> interface where T is the type of exception to map.
  2. Add the @javax.ws.rs.ext.Provider annotation on the provider class.
  3. Add the provider class to the set of classes returned in your javax.ws.rs.core.Application's getClasses() or getSingletons() method.

ExceptionMappers are chosen based on the generic type T. The closest matching ExceptionMapper type to the thrown exception type is used.

For instance, suppose there were separate ExceptionMapper<RuntimeException> and an ExceptionMapper<NullPointerException> providers. If a NullPointerException or any of its subclasses was thrown in the application, the ExceptionMapper<NullPointerException> would be used. If any other RuntimeException is thrown, the ExceptionMapper<RuntimeException> would be used.