XML Phonebook Sample Application Guide

Version 1.0.0

Overview

The XML Phonebook sample demonstrates various ways that XML can be processed in requests and responses.

Incoming requests can have a payload referred to as the request entity. In order for JAX-RS resource methods to process a request entity, a single unannotated parameter can be in the method signature. The parameter can be any type that has a corresponding JAX-RS MessageBodyReader entity provider.

Outgoing responses can also have a payload, known as the response entity. JAX-RS resource methods can return a Java object which is then written out by a JAX-RS MessageBodyWriter entity provider.

In this sample, some of the standard included JAX-RS entity providers are used to read and write XML content.

Standard Entity Providers

Here is a table with the standard entity providers which are included with every JAX-RS 1.0 runtime:

Table 2. Standard Entity Providers
Java Type MessageBodyReader MessageBodyWriter Supported Content-Types
byte[] X X */*
java.io.InputStream X X */*
java.io.Reader X X */*
java.lang.String X X */*
java.io.File X X */*
javax.activation.DataSource X X */*
javax.xml.transform.Source X X text/xml, application/xml, application/*+xml
javax.ws.rs.core.MultivaluedMap X X application/x-www-form-urlencoded
JAXB types X X text/xml, application/xml, application/*+xml
javax.ws.rs.core.StreamingOutput X */*

In the following code example, the incoming request entity is stored in the method parameter named "requestEntity" and the response is returned. The standard bytes[] MessageBodyReader and MessageBodyWriter are used for transforming entities to and from bytes[].

@GET
public byte[] processRequest(byte[] requestEntity) {
    /* do some processing with the requestEntity parameter */

    byte[] responseEntity = new byte[100];
    /* construct a response */
    return responseEntity;
}

Among other types, java.io.InputStream, java.io.Reader, java.lang.String, java.io.File, javax.xml.transform.Source Java types, and JAXB types can be used to send and receive XML.

Custom Entity Providers

JAX-RS provides many standard MessageBodyReaders and MessageBodyWriters entity providers, but of course applications can provide their own custom entity providers. See the custom entity providers sample for more details.

Content Negotiation

A common question is how does the JAX-RS runtime know that the request content or the response content is of a particular media type. Afterall, if bytes[] is returned, the bytes[] could easily represent text/plain, application/xml, or any other media type. This is discussed in the content negotiation sample.

java.io.InputStream and java.io.Reader

A common way of receiving and returning content is via objects that implement java.io.InputStream. Java libraries that read InputStreams can be used to more easily parse the content.

A java.io.Reader can be used in a similar fashion as the InputStream.

Advantages and Disadvantages

When representing an incoming request, an InputStream allows data to be consumed on demand. It can potentially be more memory efficient depending on how the InputStream is consumed. For instance, passing the InputStream to a SaX/pull XML parser, the XML contents can be read on demand resulting in better memory usage but has the traditional tradeoffs where the XML document cannot be randomly accessed or immediately validated as well-formed. If the InputStream were fed to a DOM parser, then the entire request would be read so the memory usage is higher but random access to the document nodes is allowed. InputStreams provide access to the raw bytes of the request entity so are very versatile.

For representing response entities, InputStreams may be more difficult to construct to return. However, they are again useful for memory efficient operations where the response is constructed on demand. For example, suppose you want to return a file's contents, you can use a FileInputStream to return the contents without loading the entire file into memory. If you want to use the response OutputStream directly, look into the StreamingOutput return type described below.

@POST
@Consumes("text/xml")
@Produces("text/xml")
public Response createEntry(InputStream is) throws SAXException, IOException,
    ParserConfigurationException, URISyntaxException {
    Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(is);

    NodeList nameNodes = doc.getDocumentElement().getElementsByTagName("name");
    NodeList numberNodes = doc.getDocumentElement().getElementsByTagName("number");

    /*
     * add the phone entry
     */
    int id = PhoneBook.getPhoneBook().getNextID();

    PhoneEntry entry = new PhoneEntry();
    entry.setName(nameNodes.item(0).getTextContent());
    entry.setNumber(numberNodes.item(0).getTextContent());

    PhoneBook.getPhoneBook().getEntries().put(Integer.valueOf(id), entry);

    /*
     * return XML entity via an InputStream
     */
    StringWriter sw = new StringWriter();
    sw.append("<entry>" + "<id>" + id + "</id>" + "<name>" + entry.getName() + "</name>"
        + "<number>" + entry.getNumber() + "</number>" + "</entry>");

    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(sw.getBuffer()
        .toString().getBytes());

    return Response.created(new URI(uriInfo.getAbsolutePath() + "/" + id)).entity(byteArrayInputStream).type(MediaType.TEXT_XML_TYPE).build();
}

javax.ws.rs.core.StreamingOutput

For writing content as a response, a Java class that implements javax.ws.rs.core.StreamingOutput can be used. The only method in the StreamingOutput interface is a write method which provides a java.io.Outputstream to write to. StreamingOutput is only as a possible MessageBodyWriter entity Java type. It does not work for reading.

In this code snippet, a list of phone book entries are returned in the response. An anonymous Java class that implements StreamingOutput is returned.

Advantages and Disadvantages

With all streams, StreamingOutput could potentially lead to low memory usage. Using a StreamingOutput to write the response allows an application developer to write the raw bytes via a familiar OutputStream interface.

When using the OutputStream, if you decorate the OutputStream with other OutputStream or Writer classes, be sure to flush the contents. For instance, suppose you were to wrap the OutputStream with a java.io.OutputStreamWriter and then a java.io.BufferedWriter. Then, you use the BufferedWriter to write a few characters out. If you do not do a Writer.flush(), the characters could stay in the BufferedWriter and never be written out to the OutputStream.

@GET
@Produces("text/xml")
public StreamingOutput getDirectory() {
    final StringWriter sw = new StringWriter();
    sw.append("<phonebook>");
    sw.append("<entries>");
    for (Integer id : PhoneBook.getPhoneBook().getEntries().keySet()) {
        sw.append("<entry>" + "<href>" + id + "</href>" + "</entry>");
    }
    sw.append("</entries>");
    sw.append("</phonebook>");
    
    return new StreamingOutput() {
        public void write(OutputStream arg0) throws IOException, WebApplicationException {
            arg0.write(sw.toString().getBytes());
            arg0.flush();
        }
    };
}

java.lang.String

Another easy way to receive and send content is via a java.lang.String. You can manually construct the XML content via a String and return it. Processing is more complex but you could also use Java XML libraries to help parse strings.

To ensure that this method will only be invoked when requests have a text/xml Content-Type and are willing to accept text/xml as a response, the @Consumes and @Produces annotations are added to the method. This helps limit the expected request payload so that the String entity parameter is intended to represent text/xml content. Of course, the requesting client could send malformed XML as the payload but still specify that the Content-Type is text/xml. Your resource methods should have proper error handling.

Advantages and Disadvantages

For representing requests, a String is fairly easy to manipulate but requires the entire request entity to be read and stored in memory. Also, when using a String, it is generally assumed that incoming requests are convertable to Strings. It may be difficult to get the original raw bytes of the request entity.

For representing responses, a String again is easy to construct but generally requires that the entire String be in memory.

For complex XML in particular, it is difficult to parse and construct Strings directly and is recommend that you look into using libraries or JAXB.

@POST
@Consumes("text/xml")
@Produces("text/xml")
public Response createEntry(String requestXMLContent) throws SAXException, IOException,
    ParserConfigurationException, URISyntaxException {
    /*
     * parse the XML content
     */
    Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(requestXMLContent);

    NodeList nameNodes = doc.getDocumentElement().getElementsByTagName("name");
    NodeList numberNodes = doc.getDocumentElement().getElementsByTagName("number");

    /*
     * add the phone entry
     */
    int id = PhoneBook.getPhoneBook().getNextID();

    PhoneEntry entry = new PhoneEntry();
    entry.setName(nameNodes.item(0).getTextContent());
    entry.setNumber(numberNodes.item(0).getTextContent());

    PhoneBook.getPhoneBook().getEntries().put(Integer.valueOf(id), entry);

    /*
     * construct XML entity via a String
     */
    StringWriter sw = new StringWriter();
    sw.append("<entry>" + "<id>" + id + "</id>" + "<name>" + entry.getName() + "</name>"
        + "<number>" + entry.getNumber() + "</number>" + "</entry>");

    return Response.created(new URI(uriInfo.getAbsolutePath() + "/" + id)).entity(sw.toString()).type(MediaType.TEXT_XML_TYPE).build();
}

javax.xml.transform.Source

JAX-RS 1.0 implementations also provide a MessageBodyReader and MessageBodyWriter for javax.xml.transform.Source as well. This allows application developers to more easily use some JDK standard XML APIs.

Application developers could do various things including using XSLT stylesheets and other transformations via the Java provided Transformers.

Advantages and Disadvantages

For representing requests, a Source object is natively supported by most of the native Java Transformer APIs. It is useful to get the Source object directly in the method, but if the incoming request is not XML, JAX-RS runtimes could throw errors trying to parse the request entity and not invoke your method. It may be easier to get an InputStream and create an InputSource in the resource method so you can directly handle error conditions.

@PUT
@Path("{id}")
@Produces("text/xml")
public Source updateEntry(Source requestSource, @PathParam("id") Integer id)
    throws SAXException, IOException, ParserConfigurationException, URISyntaxException,
    TransformerConfigurationException, TransformerException,
    TransformerFactoryConfigurationError {
    DOMResult result = new DOMResult();
    TransformerFactory.newInstance().newTransformer().transform(requestSource, result);
    Node n = result.getNode();
    Document doc = (Document)n;

    NodeList nameNodes = doc.getDocumentElement().getElementsByTagName("name");
    if (nameNodes.getLength() != 1) {
        throw new WebApplicationException(Response.Status.BAD_REQUEST);
    }

    NodeList numberNodes = doc.getDocumentElement().getElementsByTagName("number");
    if (numberNodes.getLength() != 1) {
        throw new WebApplicationException(Response.Status.BAD_REQUEST);
    }

    /*
     * get the phone entry and update
     */
    PhoneEntry entry = PhoneBook.getPhoneBook().getEntries().get(id);
    entry.setName(nameNodes.item(0).getTextContent());
    entry.setNumber(numberNodes.item(0).getTextContent());

    StringWriter sw = new StringWriter();
    sw.append("<entry>" + "<href>" + uriInfo.getAbsolutePath() + "</href>" + "<name>"
        + entry.getName() + "</name>" + "<number>" + entry.getNumber() + "</number>"
        + "</entry>");
    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(sw.getBuffer()
        .toString().getBytes());
    Document responseDoc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(
        byteArrayInputStream);
    return new DOMSource(responseDoc);
}