O'Reilly logo

Java Web Services: Up and Running by Martin Kalin

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Chapter 4. RESTful Web Services

What Is REST?

Roy Fielding (http://roy.gbiv.com) coined the acronym REST in his Ph.D. dissertation. Chapter 5 of his dissertation lays out the guiding principles for what have come to be known as REST-style or RESTful web services. Fielding has an impressive resume. He is, among other things, a principal author of the HTTP specification and a cofounder of the Apache Software Foundation.

REST and SOAP are quite different. SOAP is a messaging protocol, whereas REST is a style of software architecture for distributed hypermedia systems; that is, systems in which text, graphics, audio, and other media are stored across a network and interconnected through hyperlinks. The World Wide Web is the obvious example of such a system. As our focus is web services, the World Wide Web is the distributed hypermedia system of interest. In the Web, HTTP is both a transport protocol and a messaging system because HTTP requests and responses are messages. The payloads of HTTP messages can be typed using the MIME type system, and HTTP provides response status codes to inform the requester about whether a request succeeded and, if not, why.

REST stands for REpresentation State Transfer, which requires clarification because the central abstraction in REST—the resource—does not occur in the acronym. A resource in the RESTful sense is anything that has an URI; that is, an identifier that satisfies formatting requirements. The formatting requirements are what make URIs uniform. Recall, too, that URI stands for Uniform Resource Identifier; hence, the notions of URI and resource are intertwined.

In practice, a resource is an informational item that has hyperlinks to it. Hyperlinks use URIs to do the linking. Examples of resources are plentiful but likewise misleading in suggesting that resources must have something in common other than identifiability through URIs. The gross national product of Lithuania in 2001 is a resource, as is the Modern Jazz Quartet. Ernie Bank’s baseball accomplishments count as a resource, as does the maximum flow algorithm. The concept of a resource is remarkably broad but, at the same time, impressively simple and precise.

As Web-based informational items, resources are pointless unless they have at least one representation. In the Web, representations are MIME-typed. The most common type of resource representation is probably still text/html, but nowadays resources tend to have multiple representations. For example, there are various interlinked HTML pages that represent the Modern Jazz Quartet, but there are also audio and audiovisual representations of this resource.

Resources have state. For example, Ernie Bank’s baseball accomplishments changed during his career with the Chicago Cubs from 1953 through 1971 and culminated in his 1977 induction into the Baseball Hall of Fame. A useful representation must capture a resource’s state. For example, the current HTML pages on Ernie at the Baseball Reference website need to represent all of his major league accomplishments, from his rookie year in 1953 through his induction into the Hall of Fame.

In a RESTful request targeted at a resource, the resource itself remains on the service machine. The requester typically receives a representation of the resource if the request succeeds. It is the representation that transfers from the service machine to the requester machine. In different terms, a RESTful client issues a request that involves a resource, for instance, a request to read the resource. If this read request succeeds, a typed representation (for instance, text/html) of the resource is transferred from the server that hosts the resource to the client that issued the request. The representation is a good one only if it captures the resource’s state in some appropriate way.

In summary, RESTful web services require not just resources to represent but also client-invoked operations on such resources. At the core of the RESTful approach is the insight that HTTP, despite the occurrence of Transport in its name, is an API and not simply a transport protocol. HTTP has its well-known verbs, officially known as methods. Table 4-1 shows the HTTP verbs that correspond to the CRUD (Create, Read, Update, Delete) operations so familiar throughout computing.

Table 4-1. HTTP verbs and CRUD operations
HTTP verbMeaning in CRUD terms
POSTCreate a new resource from the request data
GETRead a resource
PUTUpdate a resource from the request data
DELETEDelete a resource

Although HTTP is not case-sensitive, the HTTP verbs are traditionally written in uppercase. There are additional verbs. For example, the verb HEAD is a variation on GET that requests only the HTTP headers that would be sent to fulfill a GET request. There are also TRACE and INFO verbs.

Figure 4-1 is a whimsical depiction of a resource with its identifying URI, together with a RESTful client and some typed representations sent as responses to HTTP requests for the resource. Each HTTP request includes a verb to indicate which CRUD operation should be performed on the resource. A good representation is precisely one that matches the requested operation and captures the resource’s state in some appropriate way. For example, in this depiction a GET request could return my biography as a hacker as either an HTML document or a short video summary. The video would fail to capture the state of the resource if it depicted, say, only the major disasters in my brother’s career rather than those in my own. A typical HTML representation of the resource would include hyperlinks to other resources, which in turn could be the target of HTTP requests with the appropriate CRUD verbs.

A small slice of a RESTful system
Figure 4-1. A small slice of a RESTful system

HTTP also has standard response codes, such as 404 to signal that the requested resource could not be found, and 200 to signal that the request was handled successfully. In short, HTTP provides request verbs and MIME types for client requests and status codes (and MIME types) for service responses.

Modern browsers generate only GET and POST requests. Moreover, many applications treat these two types of requests interchangeably. For example, Java HttpServlets have callback methods such as doGet and doPost that handle GET and POST requests, respectively. Each callback has the same parameter types, HttpServletRequest (the key/value pairs from the requester) and HttpServletResponse (a typed response to the requester). It is common to have the two callbacks execute the same code (for instance, by having one invoke the other), thereby conflating the original HTTP distinction between read and create. A key guiding principle of the RESTful style is to respect the original meanings of the HTTP verbs. In particular, any GET request should be side effect-free (or, in jargon, idempotent) because a GET is a read rather than a create, update, or delete operation. A GET as a read with no side effects is called a safe GET.

The REST approach does not imply that either resources or the processing needed to generate adequate representations of them are simple. A REST-style web service might be every bit as subtle and complicated as a SOAP-based service. The RESTful approach tries to simplify matters by taking what HTTP, with its MIME type system, already offers: built-in CRUD operations, uniformly identifiable resources, and typed representations that can capture a resource’s state. REST as a design philosophy tries to isolate application complexity at the endpoints, that is, at the client and at the service. A service may require lots of logic and computation to maintain resources and to generate adequate representation of resources—for instance, large and subtly formatted XML documents—and a client may require significant XML processing to extract the desired information from the XML representations transferred from the service to the client. Yet the RESTful approach keeps the complexity out of the transport level, as a resource representation is transferred to the client as the body of an HTTP response message. By contrast, a SOAP-based service inevitably complicates the transport level because a SOAP message is encapsulated as the body of a transport message; for instance, an HTTP or SMTP message. SOAP requires messages within messages, whereas REST does not.[1]

Verbs and Opaque Nouns

A URI is meant to be opaque, which means that the URI:

http://bedrock/citizens/fred

has no inherent connection to the URI:

http://bedrock/citizens

although Fred happens to be a citizen of Bedrock. These are simply two different, independent identifiers. Of course, a good URI designer will come up with URIs that are suggestive about what they are meant to identify. The point is that URIs have no intrinsic hierarchical structure. URIs can and should be interpreted, but these interpretations are imposed on URIs, not inherent in them. Although URI syntax looks like the syntax used to navigate a hierarchical file system, this resemblance is misleading. A URI is an opaque identifier, a logically proper name that denotes exactly one resource.

In RESTful services, then, URIs act as identifying nouns and HTTP methods act as verbs that specify operations on the resources identified by these nouns. For reference, here is the HTTP start line from a client’s request against the TimeServer service in Chapter 1:

POST http://127.0.0.1:9876/ts HTTP/ 1.1      

The HTTP verb comes first, then the URI, and finally the requester’s version of HTTP. This URI is, of course, a URL that locates the web service. Table 4-2 uses simplified URIs to summarize the intended meanings of HTTP/URI combinations.

Table 4-2. Sample HTTP verb/URI pairs
HTTP verb/URIIntended CRUD meaning
POST empsCreate a new employee from the request data
GET empsRead a list of all employees
GET emps?id=27Read a singleton list of employee 27
PUT empsUpdate the employee list with the request data
DELETE empsDelete the employee list
DELETE emps?id=27Delete employee 27

These verb/URI pairs are terse, precise, and uniform in style. The pairs illustrate that RESTful conventions can yield simple, clear expressions about which operation should be performed on which resource. The POST and PUT verbs are used in requests that have an HTTP body; hence, the request data are housed in the HTTP message body. The GET and DELETE verbs are used in requests that have no body; hence, the request data are sent as query string entries.

For the record, RESTful web services are Turing complete; that is, these services are equal in power to any computational system, including a system that consists of SOAP-based web services. Yet the decision about whether to be RESTful in a particular application depends, as always, on practical matters. This first section has looked at REST from on high; it is now time to descend into details through examples.

From @WebService to @WebServiceProvider

The @WebService annotation signals that the messages exchanged between the service and its clients will be SOAP envelopes. The @WebServiceProvider signals that the exchanged messages will be XML documents of some type, a notion captured in the phrase raw XML. Of course, a @WebServiceProvider could process and generate SOAP on its own, but this approach is not recommended. (A later example illustrates, however.) The obvious way to provide a SOAP-based web service is to use the annotation @WebService.

In a RESTful request/response service, the service response is raw XML but the incoming request might not be XML at all. A GET request does not have a body; hence, arguments sent as part of the request occur as attributes in the query string, a collection of key/value pairs. Here is a sample:

http://www.onlineparlor.com/bets?horse=bigbrown&jockey=kent&amount=25

The question mark (?) begins the query string, and the attributes are key/value pairs separated by ampersands (&). The order of attributes in the query string is arbitrary; for instance, the jockey attribute could occur first in the query string without changing the meaning of the request. By contrast, a POST request does have a body, which can be an arbitrary XML document instead of a SOAP envelope.

A service annotated with @WebServiceProvider implements the Provider interface, which requires that the invoke method:

public Source invoke(Source request)

be defined. This method expects a Source of bytes (for instance, the bytes in an XML document that represents the service request) and returns a Source of bytes (the bytes in the XML response). When a request arrives, the infrastructure dispatches the request to the invoke method, which handles the request in some service-appropriate way. These points can be illustrated with an example.

A RESTful Version of the Teams Service

The first RESTful service revises the Teams SOAP-based service from Chapter 1. The teams in question are comedy groups such as the Marx Brothers. To begin, the RESTful service honors only GET requests, but the service will be expanded to support the other HTTP verbs associated with the standard CRUD operations.

The WebServiceProvider Annotation

Example 4-1 is the source code for the initial version of the RestfulTeams service.

Example 4-1. The RestfulTeams web service
package ch04.team;

import javax.xml.ws.Provider;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import javax.annotation.Resource;
import javax.xml.ws.BindingType;
import javax.xml.ws.WebServiceContext;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.http.HTTPException;
import javax.xml.ws.WebServiceProvider;
import javax.xml.ws.ServiceMode;
import javax.xml.ws.http.HTTPBinding;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.Collections;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.io.IOException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.beans.XMLEncoder;
import java.beans.XMLDecoder;

// The class below is a WebServiceProvider rather than the more usual
// SOAP-based WebService. The service implements the generic Provider 
// interface rather than a customized SEI with designated @WebMethods.
@WebServiceProvider

// There are two ServiceModes: PAYLOAD, the default, signals that the service
// wants access only to the underlying message payload (e.g., the
// body of an HTTP POST request); MESSAGE signals that the service wants
// access to entire message (e.g., the HTTP headers and body). 
@ServiceMode(value = javax.xml.ws.Service.Mode.MESSAGE)

// The HTTP_BINDING as opposed, for instance, to a SOAP binding.
@BindingType(value = HTTPBinding.HTTP_BINDING)
public class RestfulTeams implements Provider<Source> {
    @Resource
    protected WebServiceContext ws_ctx;

    private Map<String, Team> team_map; // for easy lookups
    private List<Team> teams;           // serialized/deserialized
    private byte[ ] team_bytes;         // from the persistence file

    private static final String file_name = "teams.ser";

    public RestfulTeams() {
        read_teams_from_file(); // read the raw bytes from teams.ser
        deserialize();          // deserialize to a List<Team>
    }

    // This method handles incoming requests and generates the response.
    public Source invoke(Source request) {
        if (ws_ctx == null) throw new RuntimeException("DI failed on ws_ctx.");

        // Grab the message context and extract the request verb.
        MessageContext msg_ctx = ws_ctx.getMessageContext();
        String http_verb = (String)
           msg_ctx.get(MessageContext.HTTP_REQUEST_METHOD);
        http_verb = http_verb.trim().toUpperCase();

        // Act on the verb. To begin, only GET requests accepted.
        if (http_verb.equals("GET")) return doGet(msg_ctx);
        else throw new HTTPException(405); // method not allowed
    }

    private Source doGet(MessageContext msg_ctx) {
        // Parse the query string.
        String query_string = (String) msg_ctx.get(MessageContext.QUERY_STRING);

        // Get all teams.
        if (query_string == null)
            return new StreamSource(new ByteArrayInputStream(team_bytes));
        // Get a named team.
        else {
            String name = get_value_from_qs("name", query_string);

            // Check if named team exists.
            Team team = team_map.get(name);
            if (team == null) throw new HTTPException(404); // not found
            // Otherwise, generate XML and return.
            ByteArrayInputStream stream = encode_to_stream(team);
            return new StreamSource(stream);
        }
    }

    private ByteArrayInputStream encode_to_stream(Object obj) {
        // Serialize object to XML and return
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        XMLEncoder enc = new XMLEncoder(stream);
        enc.writeObject(obj);
        enc.close();
        return new ByteArrayInputStream(stream.toByteArray());
    }

    private String get_value_from_qs(String key, String qs) {
        String[ ] parts = qs.split("=");
        // Check if query string has form: name=<team name>
        if (!parts[0].equalsIgnoreCase(key))
            throw new HTTPException(400); // bad request
        return parts[1].trim();
    }

    private void read_teams_from_file() {
        try {
            String cwd = System.getProperty ("user.dir");
            String sep = System.getProperty ("file.separator");
            String path = get_file_path();
            int len = (int) new File(path).length();
            team_bytes = new byte[len];
            new FileInputStream(path).read(team_bytes);
        }
        catch(IOException e) { System.err.println(e); }
    }

    private void deserialize() {
        // Deserialize the bytes into a list of teams
        XMLDecoder dec = new XMLDecoder(new ByteArrayInputStream(team_bytes));
        teams = (List<Team>) dec.readObject();

        // Create a map for quick lookups of teams.
        team_map = Collections.synchronizedMap(new HashMap<String, Team>());
        for (Team team : teams) team_map.put(team.getName(), team);
    }

    private String get_file_path() {
        String cwd = System.getProperty ("user.dir");
        String sep = System.getProperty ("file.separator");
        return cwd + sep + "ch04" + sep + "team" + sep + file_name;
    }
}

The JWS annotations indicate the shift from a SOAP-based to a REST-style service. The main annotation is now @WebServiceProvider instead of @WebService. In the next two annotations:

@ServiceMode(value = javax.xml.ws.Service.Mode.MESSAGE)
@BindingType(value = HTTPBinding.HTTP_BINDING)      

the @ServiceMode annotation overrides the default value of PAYLOAD in favor of the value MESSAGE. This annotation is included only to highlight it, as the RestfulTeams service would work just as well with the default value. The second annotation announces that the service deals with raw XML over HTTP instead of SOAP over HTTP.

The RESTful revision deals with raw XML rather than with SOAP. The comedy teams are now stored on the local disk, in a file named teams.ser, as an XML document generated using the XMLEncoder class. Here is a segment of the file:

<?xml version="1.0" encoding="UTF-8"?>
<java version="1.6.0_06" class="java.beans.XMLDecoder">
 <object class="java.util.ArrayList">
  <void method="add">
   <object class="ch04.team.Team">
    <void property="name">
     <string>BurnsAndAllen</string>
    </void>
    <void property="players">
     <object class="java.util.ArrayList">
      <void method="add">
       <object class="ch04.team.Player">
        <void property="name">
         <string>George Burns</string>
        </void>
        <void property="nickname">
         <string>George</string>
        </void>
       </object>
      </void>
      ...
</java>

An XMLDecoder is used to deserialize this stored XML document into a List<Team>. For convenience, the service also has a Map<String, Team> so that individual teams can be accessed by name. Here is the code segment:

private void deserialize() {
     // Deserialize the bytes into a list of teams
     XMLDecoder dec = new XMLDecoder(new ByteArrayInputStream(team_bytes));
     teams = (List<Team>) dec.readObject();

     // Create a map for quick lookups of teams.
     team_map = Collections.synchronizedMap(new HashMap<String, Team>());
     for (Team team : teams) team_map.put(team.getName(), team);
}

The RestfulTeams service is published using the by-now-familiar Endpoint publisher, the same publisher used for SOAP-based services under JWS:

package ch04.team;

import javax.xml.ws.Endpoint;

class TeamsPublisher {
    public static void main(String[ ] args) {
        int port = 8888;
        String url = "http://localhost:" + port + "/teams";
        System.out.println("Publishing Teams restfully on port " + port);
        Endpoint.publish(url, new RestfulTeams());
    }
}     

Of the four HTTP verbs that correspond to CRUD operations, only GET has no side effects on the resource, which is the list of classic comedy teams. For now, then, there is no need to serialize a changed List<Team> to the file teams.ser.

The JWS runtime dispatches client requests against the RestfulTeams service to the invoke method:

public Source invoke(Source request) {
    if (ws_ctx == null) throw new RuntimeException("Injection failed on ws_ctx.");

    // Grab the message context and extract the request verb.
    MessageContext msg_ctx = ws_ctx.getMessageContext();
    String http_verb = (String) msg_ctx.get(MessageContext.HTTP_REQUEST_METHOD);
    http_verb = http_verb.trim().toUpperCase();

    // Act on the verb. For now, only GET requests accepted.
    if (http_verb.equals("GET")) return doGet(msg_ctx); 
    else throw new HTTPException(405); // method not allowed
}       

This method extracts the HTTP request verb from the MessageContext and then invokes a verb-appropriate method such as doGet to handle the request. If the request verb is not GET, then an HTTPException is thrown with the status code 405 to signal method not allowed. Table 4-3 shows some of the many HTTP status codes.

Table 4-3. Sample HTTP status codes
HTTP status codeOfficial reasonMeaning
200OKRequest OK.
400Bad requestRequest malformed.
403ForbiddenRequest refused.
404Not foundResource not found.
405Method not allowedMethod not supported.
415Unsupported media typeContent type not recognized.
500Internal server errorRequest processing failed.

In general, status codes in the range of 100–199 are informational; those in the range of 200–299 are success codes; codes in the range of 300–399 are for redirection; those in the range of 400–499 signal client errors; and codes in the range of 500–599 indicate server errors.

There are two types of GET (and, later, DELETE) requests handled in the service. If the GET request comes without a query string, the RestfulTeams service treats this as a request for the entire list of teams and responds with a copy of the XML document in the file teams.ser. If the GET request has a query string, this should be in the form ?name=<team name>, for instance, ?name=MarxBrothers. In this case, the doGet method gets the named team and encodes this team as an XML document using the XMLEncoder in the method encode_to_stream. Here is the body of the doGet method:

if (query_string == null) // get all teams
     // Respond with list of all teams
     return new StreamSource(new ByteArrayInputStream(team_bytes));
else { // get the named team
     String name = get_name_from_qs(query_string);

     // Check if named team exists.
     Team team = team_map.get(name);
     if (team == null) throw new HTTPException(404); // not found

     // Respond with named team.
     ByteArrayInputStream stream = encode_to_stream(team);
     return new StreamSource(stream);
}      

The StreamSource is a source of bytes that come from the XML document and are made available to the requesting client. On a request for the Marx Brothers, the doGet method returns, as a byte stream, an XML document that begins:

<java version="1.6.0_06" class="java.beans.XMLDecoder">
 <object class="ch04.team.Team">
  <void property="name">
   <string>MarxBrothers</string>
  </void>
  <void property="players">
   <object class="java.util.ArrayList">
    <void method="add">
     <object class="ch04.team.Player">
      <void property="name">
       <string>Leonard Marx</string>
      </void>
      <void property="nickname">
       <string>Chico</string>
      ...

Language Transparency and RESTful Services

As evidence of language transparency, the first client against the RestfulTeams service is not in Java but rather in Perl. The client sends two GET requests and performs elementary processing on the responses. Here is the initial Perl client:

#!/usr/bin/perl

use strict;
use LWP;
use XML::XPath;

# Create the user agent.
my $ua = LWP::UserAgent->new;

my $base_uri = 'http://localhost:8888/teams';

# GET teams?name=MarxBrothers
my $request = $base_uri . '?name=MarxBrothers';
send_GET($request);

sub send_GET {
    my ($uri, $qs_flag) = @_;

    # Send the request and get the response.
    my $req = HTTP::Request->new(GET => $uri);
    my $res = $ua->request($req);

    # Check for errors.
    if ($res->is_success) {
        parse_GET($res->content, $qs_flag); # Process raw XML on success
    }
    else {
        print $res->status_line, "\n";      # Print error code on failure
    }
}

# Print raw XML and the elements of interest.
sub parse_GET {
    my ($raw_xml) = @_;
    print "\nThe raw XML response is:\n$raw_xml\n;;;\n";

    # For all teams, extract and print out their names and members
    my $xp = XML::XPath->new(xml => $raw_xml);
    foreach my $node ($xp->find('//object/void/string')->get_nodelist) {
        print $node->string_value, "\n";
    }
}

The Perl client issues a GET request against the URI http://localhost:8888/teams, which is the endpoint location for the Endpoint-published service. If the request succeeds, the service returns an XML representation of the teams, in this case the XML generated from a call to the XMLEncoder method writeObject. The Perl client prints the raw XML and performs a very simple parse, using an XPath package to get the team names together with the member names and nicknames. In a production environment the XML processing would be more elaborate, but the basic logic of the client would be the same: issue an appropriate request against the service and process the response in some appropriate way. On a sample client run, the output was:

The GET request is: http://localhost:8888/teams
The raw XML response is:
<java version="1.6.0_06" class="java.beans.XMLDecoder">
 <object class="java.util.ArrayList">
  <void method="add">
   <object class="ch04.team.Team">
    <void property="name">
     <string>BurnsAndAllen</string>
    </void>
    <void property="players">
     <object class="java.util.ArrayList">
      <void method="add">
       <object class="ch04.team.Player">
        <void property="name">
         <string>George Burns</string>
        </void>
        <void property="nickname">
         <string>George</string>
        </void>
       </object>
      </void>
      <void method="add">
       <object class="ch04.team.Player">
        <void property="name">
         <string>Gracie Allen</string>
        </void>
        <void property="nickname">
         <string>Gracie</string>
        </void>
       </object>
      </void>
     </object>
    </void>
   </object>
  </void>
  ...
</java>
;;;

BurnsAndAllen
George Burns
George
Gracie Allen
Gracie
AbbottAndCostello
William Abbott
Bud
Louis Cristillo
Lou
MarxBrothers
Leonard Marx
Chico
Julius Marx
Groucho
Adolph Marx
Harpo

The output below the semicolons consists of the extracted team names, together with the member names and nicknames.

Here is a Java client against the RestfulTeams service:

import java.util.Arrays;
import java.net.URL;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URLEncoder;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.ByteArrayInputStream;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.ParserConfigurationException;

class TeamsClient {
    private static final String endpoint = "http://localhost:8888/teams";

    public static void main(String[ ] args) {
        new TeamsClient().send_requests();
    }

    private void send_requests() {
        try {
            // GET requests
            HttpURLConnection conn = get_connection(endpoint, "GET");
            conn.connect();
            print_and_parse(conn, true);

            conn = get_connection(endpoint + "?name=MarxBrothers", "GET");
            conn.connect();
            print_and_parse(conn, false);
        }
        catch(IOException e) { System.err.println(e); }
        catch(NullPointerException e) { System.err.println(e); }
    }

    private HttpURLConnection get_connection(String url_string,
                                             String verb) {
        HttpURLConnection conn = null;
        try {
            URL url = new URL(url_string);
            conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod(verb);
        }
        catch(MalformedURLException e) { System.err.println(e); }
        catch(IOException e) { System.err.println(e); }
        return conn;
    }

    private void print_and_parse(HttpURLConnection conn, boolean parse) {
        try {
            String xml = "";
            BufferedReader reader =
                new BufferedReader(new InputStreamReader(conn.getInputStream()));
            String next = null;
            while ((next = reader.readLine()) != null)
                xml += next;
            System.out.println("The raw XML:\n" + xml);

            if (parse) {
                SAXParser parser =SAXParserFactory.newInstance().newSAXParser();
                parser.parse(new ByteArrayInputStream(xml.getBytes()),
                             new SaxParserHandler());
            }
        }
        catch(IOException e) { System.err.println(e); }
        catch(ParserConfigurationException e) { System.err.println(e); }
        catch(SAXException e) { System.err.println(e); }
    }

    static class SaxParserHandler extends DefaultHandler {
        char[ ] buffer = new char[1024];
        int n = 0;

        public void startElement(String uri, String lname,
                                 String qname, Attributes attributes) {
            clear_buffer();
        }

        public void characters(char[ ] data, int start, int length) {
            System.arraycopy(data, start, buffer, 0, length);
            n += length;
        }

        public void endElement(String uri, String lname, String qname) {
            if (Character.isUpperCase(buffer[0]))
                System.out.println(new String(buffer));
            clear_buffer();
        }

        private void clear_buffer() {
            Arrays.fill(buffer, '\0');
            n = 0;
        }
    }
}

The Java client issues two GET requests and uses a SAX (Simple API for XML) parser to process the returned XML. Java offers an assortment of XML-processing tools and the code examples illustrate several. A SAX parser is stream-based and event-driven—the parser receives a stream of bytes, invoking callbacks (such as the methods named startElement and characters shown above) to handle specific events, in this case the occurrence of XML start tags and character data in between start and end tags, respectively.

Summary of the RESTful Features

This first restricted example covers some key features of RESTful services but also ignores one such feature. Following is a summary of the example so far:

  • In a request, the pairing of an HTTP verb such as GET with a URI such as http://.../teams specifies a CRUD operation against a resource; in this example, a request to read available information about comedy teams.

  • The service uses HTTP status codes such as 404 (resource not found) and 405 (method not allowed) to respond to bad requests.

  • If the request is a good one, the service responds with an XML representation that captures the state of the requested resource. So far, the service honors only GET requests, but the other CRUD verbs will be added in the forthcoming revision.

  • The service does not take advantage of MIME types. A client issues a request for either a named team or a list of all teams but does not indicate a preference for the type of representation returned (for instance, text/plain as opposed to text/xml or text/html). A later example does illustrate typed requests and responses.

  • The RESTful service implementation is not constrained in the same way as a SOAP-based service precisely because there is no formal service contract. The implementation is flexible but, of course, likewise ad hoc. This issue will be raised often.

The next section extends the service to handle requests issued with the POST, PUT, and DELETE verbs.

Implementing the Remaining CRUD Operations

The remaining CRUD operations—create (POST), update (PUT), and delete (DELETE)—have side effects, which requires that the RestfulTeams service update the in-memory data structures (in this case, the list and the map of teams) and the persistence store (in this case, the local file teams.ser). The service follows an eager rather than a lazy strategy for updating teams.ser—this file is updated on every successful POST, PUT, and DELETE request. A lazier and more efficient strategy might be followed in a production environment.

The RestfulTeams implementation of the invoke method changes only slightly to accommodate the new request possibilities. Here is the change:

MessageContext msg_ctx = ws_ctx.getMessageContext();
String http_verb = (String) msg_ctx.get(MessageContext.HTTP_REQUEST_METHOD);
http_verb = http_verb.trim().toUpperCase();

// Act on the verb.
if      (http_verb.equals("GET"))    return doGet(msg_ctx);
else if (http_verb.equals("DELETE")) return doDelete(msg_ctx);
else if (http_verb.equals("POST"))   return doPost(msg_ctx);
else if (http_verb.equals("PUT"))    return doPut(msg_ctx);
else throw new HTTPException(405);   // method not allowed      

The doPost method expects that the request contains an XML document with information about the new team to be created. Following is a sample:

<create_team>
   <name>SmothersBrothers</name>
   <player>
     <name>Thomas</name>
     <nickname>Tom</nickname>
   </player>
   <player>
     <name>Richard</name>
     <nickname>Dickie</nickname>
   </player>
</create_team>      

Of course, an XML Schema that describes precisely this layout could be distributed to clients. In this example, the doPost does not validate the request document against a schema but rather parses the document to find required information such as the team’s name and the players’ names. If required information is missing, an HTTP status code of 500 (internal error) or 400 (bad request) is sent back to the client. Here is the added doPost method:

private Source doPost(MessageContext msg_ctx) {
    Map<String, List> request = (Map<String, List>)
      msg_ctx.get(MessageContext.HTTP_REQUEST_HEADERS);

    List<String> cargo = request.get(post_put_key);
    if (cargo == null) throw new HTTPException(400); // bad request

    String xml = "";
    for (String next : cargo) xml += next.trim();
    ByteArrayInputStream xml_stream = new ByteArrayInputStream(xml.getBytes());
    String team_name = null;

    try {
        // Set up the XPath object to search for the XML elements.
        DOMResult dom = new DOMResult();
        Transformer trans = TransformerFactory.newInstance().newTransformer();
        trans.transform(new StreamSource(xml_stream), dom);
        URI ns_URI = new URI("create_team");

        XPathFactory xpf = XPathFactory.newInstance();
        XPath xp = xpf.newXPath();
        xp.setNamespaceContext(new NSResolver("", ns_URI.toString()));

        team_name = xp.evaluate("/create_team/name", dom.getNode());
        List<Player> team_players = new ArrayList<Player>();
        NodeList players = (NodeList) xp.evaluate("player", dom.getNode(),
                                                  XPathConstants.NODESET);

        for (int i = 1; i <= players.getLength(); i++) {
            String name = xp.evaluate("name", dom.getNode());
            String nickname = xp.evaluate("nickname", dom.getNode());
            Player player = new Player(name, nickname);
            team_players.add(player);
        }

        // Add new team to the in-memory map and save List to file.
        Team t = new Team(team_name, team_players);
        team_map.put(team_name, t);
        teams.add(t);
        serialize();
    }
    catch(URISyntaxException e) { throw new HTTPException(500); }
    catch(TransformerConfigurationException e) { throw new HTTPException(500); }
    catch(TransformerException e) { throw new HTTPException(500); }
    catch(XPathExpressionException e) { throw new HTTPException(400); }
    // Send a confirmation to requester.
    return response_to_client("Team " + team_name + " created.");
}     

Java API for XML Processing

In parsing the request XML document, the doPost method in this example uses interfaces and classes from the javax.xml.transform package, which are part of JAX-P (Java API for XML-Processing). The JAX-P tools were designed to facilitate XML processing, which addresses the needs of a RESTful service. In this example, the two key pieces are the DOMResult and the XPath object. In the Java TeamsClient shown earlier, a SAX parser is used to process the list of comedy teams returned from the RestfulTeams service on a successful GET request with no query string. A SAX parser is stream-based and invokes programmer-supplied callbacks to process various parsing events such as the occurrence of an XML start tag. By contrast, a DOM (Document Object Model) parser is tree-based in that the parser constructs a tree representation of a well-formed XML document. The programmer then can use a standard API, for example, to search the tree for desired elements. JAX-P uses the XSLT (eXtensible Stylesheet Language Transformations) verb transform to describe the process of transforming an XML source (for instance, the request bytes from a client) into an XML result (for instance, a DOM tree). Here is the statement in doPost that does just this:

trans.transform(new StreamSource(xml_stream), dom);

The xml_stream refers to the bytes from the client in a ByteArrayInputStream, and dom refers to a DOMResult. A DOM tree can be processed in various ways. In this case, an XPath object is used to search for relatively simple patterns. For instance, the statement:

NodeList players = (NodeList) xp.evaluate("player", dom.getNode(),
                                          XPathConstants.NODESET);

gets a list of elements tagged with player from the DOM tree. The statements:

String name = xp.evaluate("name", dom.getNode());
String nickname = xp.evaluate("nickname", dom.getNode());                                         

then extract the player’s name and nickname from the DOM tree.

The doPost method respects the HTTP verb from which the method gets its name. After the name of the new team has been extracted from the request XML document, a check is made:

team_name = xp.evaluate("/create_team/name", dom.getNode());
if (team_map.containsKey(team_name)) throw new HTTPException(400); // bad request

to determine whether a team with that name already exists. Because a POST request signals a create operation, an already existing team cannot be created but instead must be updated through a PUT request.

Once the needed information about the new team has been extracted from the request XML document, the data structures Map<String, Team> and List<Team> are updated to reflect a successful create operation. The list of teams is serialized to the persistence file.

The two remaining CRUD operations, update and delete, are implemented as the methods doPut and doDelete, respectively. The RestfulTeams service requires that a DELETE request have a query string to identify a particular team; the deletion of all teams at once is not allowed. For now, a PUT request can update only a team’s name, although this easily could be expanded to allow updates to the team’s members and their names or nicknames. Here are the implementations of doPut and doDelete:

private Source doDelete(MessageContext msg_ctx) {
    String query_string = (String) msg_ctx.get(MessageContext.QUERY_STRING);

    // Disallow the deletion of all teams at once.
    if (query_string == null) throw new HTTPException(403); // illegal operation
    else {
        String name = get_value_from_qs("name", query_string);
        if (!team_map.containsKey(name)) throw new HTTPException(404);

        // Remove team from Map and List, serialize to file.
        Team team = team_map.get(name);
        teams.remove(team);
        team_map.remove(name);
        serialize();

        // Send response.
        return response_to_client(name + " deleted.");
    }
}      

private Source doPut(MessageContext msg_ctx) {
    // Parse the query string.
    String query_string = (String) msg_ctx.get(MessageContext.QUERY_STRING);
    String name = null;
    String new_name = null;

    // Get all teams.
    if (query_string == null) throw new HTTPException(403); // illegal operation
    // Get a named team.
    else {
        // Split query string into name= and new_name= sections
        String[ ] parts = query_string.split("&");
        if (parts[0] == null || parts[1] == null) throw new HTTPException(403);

        name = get_value_from_qs("name", parts[0]);
        new_name = get_value_from_qs("new_name", parts[1]);
        if (name == null || new_name == null) throw new HTTPException(403);

        Team team = team_map.get(name);
        if (team == null) throw new HTTPException(404);
        team.setName(new_name);
        team_map.put(new_name, team);
        serialize();
    }

    // Send a confirmation to requester.
    return response_to_client("Team " + name + " changed to " + new_name);
}

Each of the do methods has a similar style, and the application logic has been kept as simple as possible to focus attention on RESTful character of the service. Here, for reference, is the all of the source code for the service:

package ch04.team;

import javax.xml.ws.Provider;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import javax.annotation.Resource;
import javax.xml.ws.BindingType;
import javax.xml.ws.WebServiceContext;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.http.HTTPException;
import javax.xml.ws.WebServiceProvider;
import javax.xml.ws.ServiceMode;
import javax.xml.ws.http.HTTPBinding;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.Collections;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.io.IOException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.BufferedOutputStream;
import java.beans.XMLEncoder;
import java.beans.XMLDecoder;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import java.net.URI;
import java.net.URISyntaxException;
import org.w3c.dom.NodeList;

// The class below is a WebServiceProvider rather than
// the more usual SOAP-based WebService. As a result, the
// service implements the generic Provider interface rather
// than a customized SEI with designated @WebMethods.
@WebServiceProvider

// There are two ServiceModes: PAYLOAD, the default, signals that the service
// wants access only to the underlying message payload (e.g., the
// body of an HTTP POST request); MESSAGE signals that the service wants
// access to entire message (e.g., the HTTP headers and body). In this
// case, the MESSAGE mode lets us check on the request verb.
@ServiceMode(value = javax.xml.ws.Service.Mode.MESSAGE)

// The HTTP_BINDING as opposed, for instance, to a SOAP binding.
@BindingType(value = HTTPBinding.HTTP_BINDING)

// The generic, low-level Provider interface is an alternative
// to the SEI (service endpoint interface) of a SOAP-based
// web service. A Source is a source of the bytes. The invoke
// method expects a source and returns one.
public class RestfulTeams implements Provider<Source> {
    @Resource
    protected WebServiceContext ws_ctx;

    private Map<String, Team> team_map; // for easy lookups
    private List<Team> teams;           // serialized/deserialized
    private byte[ ] team_bytes;         // from the persistence file

    private static final String file_name = "teams.ser";
    private static final String post_put_key = "Cargo";

    public RestfulTeams() {
        read_teams_from_file();
        deserialize();
    }

    // Implementation of the Provider interface method: this
    // method handles incoming requests and generates the
    // outgoing response.
    public Source invoke(Source request) {
        if (ws_ctx == null)
            throw new RuntimeException("Injection failed on ws_ctx.");

        if (request == null) System.out.println("null request");
        else System.out.println("non-null request");

        // Grab the message context and extract the request verb.
        MessageContext msg_ctx = ws_ctx.getMessageContext();
        String http_verb = (String)
            msg_ctx.get(MessageContext.HTTP_REQUEST_METHOD);
        http_verb = http_verb.trim().toUpperCase();

        // Act on the verb.
        if      (http_verb.equals("GET"))    return doGet(msg_ctx);
        else if (http_verb.equals("DELETE")) return doDelete(msg_ctx);
        else if (http_verb.equals("POST"))   return doPost(msg_ctx);
        else if (http_verb.equals("PUT"))    return doPut(msg_ctx);
        else throw new HTTPException(405);   // bad verb exception
    }

    private Source doGet(MessageContext msg_ctx) {
        // Parse the query string.
        String query_string = (String)
            msg_ctx.get(MessageContext.QUERY_STRING);

        // Get all teams.
        if (query_string == null)
            return new StreamSource(new ByteArrayInputStream(team_bytes));
        // Get a named team.
        else {
            String name = get_value_from_qs("name", query_string);

            // Check if named team exists.
            Team team = team_map.get(name);
            if (team == null) throw new HTTPException(404); // not found

            // Otherwise, generate XML and return.
            ByteArrayInputStream stream = encode_to_stream(team);
            return new StreamSource(stream);
        }
    }

    private Source doPost(MessageContext msg_ctx) {
        Map<String, List> request = (Map<String, List>)
            msg_ctx.get(MessageContext.HTTP_REQUEST_HEADERS);

        List<String> cargo = request.get(post_put_key);
        if (cargo == null) throw new HTTPException(400); // bad request

        String xml = "";
        for (String next : cargo) xml += next.trim();
        ByteArrayInputStream xml_stream = new ByteArrayInputStream(xml.getBytes());
        String team_name = null;

        try {
            // Set up the XPath object to search for the XML elements.
            DOMResult dom = new DOMResult();
            Transformer trans =
                TransformerFactory.newInstance().newTransformer();
            trans.transform(new StreamSource(xml_stream), dom);
            URI ns_URI = new URI("create_team");

            XPathFactory xpf = XPathFactory.newInstance();
            XPath xp = xpf.newXPath();
            xp.setNamespaceContext(new NSResolver("", ns_URI.toString()));

            team_name = xp.evaluate("/create_team/name", dom.getNode());

            if (team_map.containsKey(team_name))
                throw new HTTPException(400); // bad request

            List<Player> team_players = new ArrayList<Player>();

            NodeList players = (NodeList)
                xp.evaluate("player",
                            dom.getNode(),
                            XPathConstants.NODESET);

            for (int i = 1; i <= players.getLength(); i++) {
                String name = xp.evaluate("name", dom.getNode());
                String nickname = xp.evaluate("nickname", dom.getNode());
                Player player = new Player(name, nickname);
                team_players.add(player);
            }
            // Add new team to the in-memory map and save List to file.
            Team t = new Team(team_name, team_players);
            team_map.put(team_name, t);
            teams.add(t);
            serialize();
        }
        catch(URISyntaxException e) {
            throw new HTTPException(500);   // internal server error
        }
        catch(TransformerConfigurationException e) {
            throw new HTTPException(500);   // internal server error
        }
        catch(TransformerException e) {
            throw new HTTPException(500);   // internal server error
        }
        catch(XPathExpressionException e) {
            throw new HTTPException(400);   // bad request
        }

        // Send a confirmation to requester.
        return response_to_client("Team " + team_name + " created.");
    }

    private Source doPut(MessageContext msg_ctx) {
        // Parse the query string.
        String query_string = (String) msg_ctx.get(MessageContext.QUERY_STRING);
        String name = null;
        String new_name = null;

        // Get all teams.
        if (query_string == null)
            throw new HTTPException(403); // illegal operation
        // Get a named team.
        else {
            // Split query string into name= and new_name= sections
            String[ ] parts = query_string.split("&");
            if (parts[0] == null || parts[1] == null)
                throw new HTTPException(403);

            name = get_value_from_qs("name", parts[0]);
            new_name = get_value_from_qs("new_name", parts[1]);
            if (name == null || new_name == null)
                throw new HTTPException(403);

            Team team = team_map.get(name);
            if (team == null) throw new HTTPException(404);
            team.setName(new_name);
            team_map.put(new_name, team);
            serialize();
        }

        // Send a confirmation to requester.
        return response_to_client("Team " + name + " changed to " + new_name);
    }

    private Source doDelete(MessageContext msg_ctx) {
        String query_string = (String)
            msg_ctx.get(MessageContext.QUERY_STRING);

        // Disallow the deletion of all teams at once.
        if (query_string == null)
            throw new HTTPException(403);     // illegal operation
        else {
            String name = get_value_from_qs("name", query_string);
            if (!team_map.containsKey(name))
                throw new HTTPException(404); // not found

            // Remove team from Map and List, serialize to file.
            Team team = team_map.get(name);
            teams.remove(team);
            team_map.remove(name);
            serialize();

            // Send response.
            return response_to_client(name + " deleted.");
        }
    }

    private StreamSource response_to_client(String msg) {
        HttpResponse response = new HttpResponse();
        response.setResponse(msg);
        ByteArrayInputStream stream = encode_to_stream(response);
        return new StreamSource(stream);
    }

    private ByteArrayInputStream encode_to_stream(Object obj) {
        // Serialize object to XML and return
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        XMLEncoder enc = new XMLEncoder(stream);
        enc.writeObject(obj);
        enc.close();
        return new ByteArrayInputStream(stream.toByteArray());
    }

    private String get_value_from_qs(String key, String qs) {
        String[ ] parts = qs.split("=");

        // Check if query string has form: name=<team name>
        if (!parts[0].equalsIgnoreCase(key))
            throw new HTTPException(400); // bad request
        return parts[1].trim();
    }

    private void read_teams_from_file() {
        try {
            String cwd = System.getProperty ("user.dir");
            String sep = System.getProperty ("file.separator");
            String path = get_file_path();
            int len = (int) new File(path).length();
            team_bytes = new byte[len];
            new FileInputStream(path).read(team_bytes);
        }
        catch(IOException e) { System.err.println(e); }
    }

    private void deserialize() {
        // Deserialize the bytes into a list of teams
        XMLDecoder dec =
            new XMLDecoder(new ByteArrayInputStream(team_bytes));
        teams = (List<Team>) dec.readObject();

        // Create a map for quick lookups of teams.
        team_map = Collections.synchronizedMap(new HashMap<String, Team>());
        for (Team team : teams)
            team_map.put(team.getName(), team);
    }

    private void serialize() {
        try {
            String path = get_file_path();
            BufferedOutputStream out =
                new BufferedOutputStream(new FileOutputStream(path));
            XMLEncoder enc = new XMLEncoder(out);
            enc.writeObject(teams);
            enc.close();
            out.close();
        }
        catch(IOException e) { System.err.println(e); }
    }

    private String get_file_path() {
        String cwd = System.getProperty ("user.dir");
        String sep = System.getProperty ("file.separator");
        return cwd + sep + "ch04" + sep + "team" + sep + file_name;
    }
}

The revised Perl client shown below tests the service by generating a series of requests. Here is the complete Perl client:

#!/usr/bin/perl

use strict;
use LWP;
use XML::XPath;
use Encode;
use constant true   =>  1;
use constant false  =>  0;

# Create the user agent.
my $ua = LWP::UserAgent->new;

my $base_uri = 'http://localhost:8888/teams';

# GET teams
send_GET($base_uri, false); # false means no query string

# GET teams?name=MarxBrothers
send_GET($base_uri . '?name=MarxBrothers', true);

$base_uri = $base_uri;
send_POST($base_uri);

# Check that POST worked
send_GET($base_uri . '?name=SmothersBrothers', true);
send_DELETE($base_uri . '?name=SmothersBrothers');

# Recreate the Smothers Brothers as a check.
send_POST($base_uri);

# Change name and check.
send_PUT($base_uri . '?name=SmothersBrothers&new_name=SmuthersBrothers');
send_GET($base_uri . '?name=SmuthersBrothers', true);

sub send_GET {
    my ($uri, $qs_flag) = @_;

    # Send the request and get the response.
    my $req = HTTP::Request->new(GET => $uri);
    my $res = $ua->request($req);

    # Check for errors.
    if ($res->is_success) {
        parse_GET($res->content, $qs_flag); # Process raw XML on success
    }
    else {
        print $res->status_line, "\n";      # Print error code on failure
    }
}

sub send_POST {
    my ($uri) = @_;

    my $xml = <<EOS;
      <create_team>
         <name>SmothersBrothers</name>
         <player>
           <name>Thomas</name>
           <nickname>Tom</nickname>
         </player>
         <player>
           <name>Richard</name>
           <nickname>Dickie</nickname>
         </player>
      </create_team>
EOS
    # Send request and capture response.
    my $bytes = encode('iso-8859-1', $xml); # encoding is Latin-1
    my $req = HTTP::Request->new(POST => $uri, ['Cargo' => $bytes]);
    my $res = $ua->request($req);

    # Check for errors.
    if ($res->is_success) {
        parse_SIMPLE("POST", $res->content); # Process raw XML on success
    }
    else {
        print $res->status_line, "\n";        # Print error code on failure
    }
}

sub send_DELETE {
    my $uri = shift;

    # Send the request and get the response.
    my $req = HTTP::Request->new(DELETE => $uri);
    my $res = $ua->request($req);

    # Check for errors.
    if ($res->is_success) {
        parse_SIMPLE("DELETE", $res->content);   # Process raw XML on success
    }
    else {
        print $res->status_line, "\n"; # Print error code on failure
    }
}

sub send_PUT {
    my $uri = shift;

    # Send the request and get the response.
    my $req = HTTP::Request->new(PUT => $uri);
    my $res = $ua->request($req);

    # Check for errors.
    if ($res->is_success) {
        parse_SIMPLE("PUT", $res->content);   # Process raw XML on success
    }
    else {
        print $res->status_line, "\n"; # Print error code on failure
    }
}

sub parse_SIMPLE {
    my $verb = shift;
    my $raw_xml = shift;
    print "\nResponse on $verb: \n$raw_xml;;;\n";
}     

sub parse_GET {
    my ($raw_xml) = @_;
    print "\nThe raw XML response is:\n$raw_xml\n;;;\n";

    # For all teams, extract and print out their names and members
    my $xp = XML::XPath->new(xml => $raw_xml);
    foreach my $node ($xp->find('//object/void/string')->get_nodelist) {
        print $node->string_value, "\n";
    }
}

The Provider and Dispatch Twins

In the RestfulTeams service, the clients send request information to the service through the HTTP start line (for instance, in a GET request) and optionally through an inserted HTTP header (for instance, in a POST request). Recall that GET and DELETE requests result in HTTP messages that have no body, whereas POST and PUT requests result in HTTP messages with bodies. Clients of the RestfulTeams service do not use the HTTP body at all. Even in a POST or PUT request, information about the new team to create or the existing team to update is contained in the HTTP header rather than in the body.

The approach in the RestfulTeams service illustrates the flexibility of REST-style services. The revision in this section shows how the HTTP body can be used in a POST request by introducing the Dispatch interface, which is the client-side twin of the server-side Provider interface. The RestfulTeams service already illustrates that a Provider on the service side can be used without a Dispatch on the client side; and a later example shows how a Dispatch can be used on the client side regardless of how the RESTful service is implemented. Nonetheless, the Provider and Dispatch interfaces are a natural pair.

A RESTful Provider implements the method:

public Source invoke(Source request)

and a Dispatch object, sometimes described as a dynamic service proxy, provides an implementation of this method on the client side. Recall that a Source is a source of an XML document suitable as input to a Transform, which then generates a Result that is typically an XML document as well. The Dispatch to Provider relationship supports a natural exchange of XML documents between client and service:

  • The client invokes the Dispatch method invoke, with an XML document as the Source argument. If the request does not require an XML document, then the Source argument can be null.

  • The service-side runtime dispatches the client request to the Provider method invoke whose Source argument corresponds to the client-side Source.

  • The service transforms the Source into some appropriate Result (for instance, a DOM tree), processes this Result in an application-appropriate way, and returns an XML source to the client. If no response is needed, null can be returned.

  • The Dispatch method invoke returns a Source, sent from the service, that the client then transforms into an appropriate Result and processes as needed.

The fact that the Provider method invoke and the Dispatch method invoke have the same signature underscores the natural fit between them.

A Provider/Dispatch Example

The RabbitCounterProvider is a RESTful service that revises the SOAP-based version of Chapter 3. The RESTful revision honors POST, GET, and DELETE requests from clients. A POST request, as a CRUD create operation, creates a list of Fibonacci numbers that the service caches for subsequent read or delete operations. The doPost method responds to a POST request and the method expects a Source argument, which is the source of an XML document such as:

<fib:request xmlns:fib = 'urn:fib'>[1, 2, 3, 4]</fib:request>

The XML document is thus a list of integers whose Fibonacci values are to be computed. The doGet and doDelete methods handle GET and PUT requests, respectively, neither of which has an HTTP body; hence, the doGet and doDelete methods do not have a Source parameter. All three methods return a Source value, which is the source of an XML confirmation. For example, doPost returns a confirmation XML document such as:

<fib:response xmlns:fib = 'urn:fib'>POSTed[1, 1, 2, 3]</fib:response>

The other two methods return operation-specific confirmations.

Here is the source code for the RabbitCounterProvider:

package ch04.dispatch;

import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import java.util.Collection;
import javax.xml.ws.Provider;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import javax.annotation.Resource;
import javax.xml.ws.BindingType;
import javax.xml.ws.WebServiceContext;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.http.HTTPException;
import javax.xml.ws.WebServiceProvider;
import javax.xml.ws.http.HTTPBinding;
import java.io.ByteArrayInputStream;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;

// The RabbitCounter service implemented as REST style rather than SOAP based.
@WebServiceProvider
@BindingType(value = HTTPBinding.HTTP_BINDING)

public class RabbitCounterProvider implements Provider<Source> {
    @Resource
    protected WebServiceContext ws_ctx;

    // stores previously computed values
    private Map<Integer, Integer> cache = 
       Collections.synchronizedMap(new HashMap<Integer, Integer>());

    private final String xml_start = "<fib:response xmlns:fib = 'urn:fib'>";
    private final String xml_stop = "</fib:response>";
    private final String uri = "urn:fib";

    public Source invoke(Source request) {
        // Filter on the HTTP request verb
        if (ws_ctx == null) throw new RuntimeException("DI failed on ws_ctx.");

        // Grab the message context and extract the request verb.
        MessageContext msg_ctx = ws_ctx.getMessageContext();
        String http_verb = (String) msg_ctx.get(MessageContext.HTTP_REQUEST_METHOD);
        http_verb = http_verb.trim().toUpperCase();

        // Act on the verb.
        if      (http_verb.equals("GET"))    return doGet();
        else if (http_verb.equals("DELETE")) return doDelete();
        else if (http_verb.equals("POST"))   return doPost(request);
        else throw new HTTPException(405);   // bad verb exception
    }

    private Source doPost(Source request) {
        if (request == null) throw new HTTPException(400); // bad request

        String nums = extract_request(request);
        // Extract the integers from a string such as: "[1, 2, 3]"
        nums = nums.replace('[', '\0');
        nums = nums.replace(']', '\0');
        String[ ] parts = nums.split(",");
        List<Integer> list = new ArrayList<Integer>();
        for (String next : parts) {
            int n = Integer.parseInt(next.trim());
            cache.put(n, countRabbits(n));
            list.add(cache.get(n));
        }
        String xml = xml_start + "POSTed: " + list.toString() + xml_stop;
        return make_stream_source(xml);
    }

    private Source doGet() {
        Collection<Integer> list = cache.values();
        String xml = xml_start + "GET: " + list.toString() + xml_stop;
        return make_stream_source(xml);
    }

    private Source doDelete() {
        cache.clear();
        String xml = xml_start + "DELETE: Map cleared." + xml_stop;
        return make_stream_source(xml);
    }

    private String extract_request(Source request) {
        String request_string = null;
        try {
            DOMResult dom_result = new DOMResult();
            Transformer trans = TransformerFactory.newInstance().newTransformer();
            trans.transform(request, dom_result);

            XPathFactory xpf = XPathFactory.newInstance();
            XPath xp = xpf.newXPath();
            xp.setNamespaceContext(new NSResolver("fib", uri));
            request_string = xp.evaluate("/fib:request", dom_result.getNode());
        }
        catch(TransformerConfigurationException e) { System.err.println(e); }
        catch(TransformerException e) { System.err.println(e); }
        catch(XPathExpressionException e) { System.err.println(e); }

        return request_string;
    }

    private StreamSource make_stream_source(String msg) {
        System.out.println(msg);
        ByteArrayInputStream stream = new ByteArrayInputStream(msg.getBytes());
        return new StreamSource(stream);
    }

    private int countRabbits(int n) {
        if (n < 0) throw new HTTPException(403); // forbidden

        // Easy cases.
        if (n < 2) return n;

        // Return cached values if present.
        if (cache.containsKey(n)) return cache.get(n);
        if (cache.containsKey(n - 1) && cache.containsKey(n - 2)) {
          cache.put(n, cache.get(n - 1) + cache.get(n - 2));
          return cache.get(n);
        }

        // Otherwise, compute from scratch, cache, and return.
        int fib = 1, prev = 0;
        for (int i = 2; i <= n; i++) {
            int temp = fib;
            fib += prev;
            prev = temp;
        }
        cache.put(n, fib);
        return fib;
    }
}

The code segment:

XPathFactory xpf = XPathFactory.newInstance();
XPath xp = xpf.newXPath();
xp.setNamespaceContext(new NSResolver("fib", uri));
request_string = xp.evaluate("/fib:request", dom_result.getNode());      

deserves a closer look because the NSResolver also is used in the RestfulTeams service. The call to xp.evaluate, shown in bold above, takes two arguments: an XPath pattern, in this case /fib:request, and the DOMResult node that contains the desired string data between the start tag <fib:request> and the corresponding end tag </fib:request>. The fib in fib:request is a proxy or alias for a namespace URI, in this case urn:fib. The entire start tag in the request XML document is:

<fib:request xmlns:fib = 'urn:fib'>      

The NSResolver class (NS is short for namespace) provides mappings from fib to urn:fib and vice-versa. Here is the code:

package ch04.dispatch;

import java.util.Collections;
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
import javax.xml.namespace.NamespaceContext;

public class NSResolver implements NamespaceContext {
    private Map<String, String> prefix2uri;
    private Map<String, String> uri2prefix;
    public NSResolver() {
        prefix2uri =
          Collections.synchronizedMap(new HashMap<String, String>());
        uri2prefix =
          Collections.synchronizedMap(new HashMap<String, String>());
    }


    public NSResolver(String prefix, String uri) {
        this();
        prefix2uri.put(prefix, uri);
        uri2prefix.put(uri, prefix);
    }

    public String getNamespaceURI(String prefix) { return prefix2uri.get(prefix); }
    public String getPrefix(String uri) { return uri2prefix.get(uri); }
    public Iterator getPrefixes(String uri) { return uri2prefix.keySet().iterator(); }
}

The NSResolver provides the namespace context for the XPath searches; that is, the resolver binds together a namespace URI and its proxies or aliases. For the application to work correctly, a client and the service must use the same namespace URI; in this case the structurally simple URI urn:fib.

More on the Dispatch Interface

The Dispatch-based client of the RESTful RabbitCounterProvider service has features reminiscent of a client for a SOAP-based service. The client creates identifying QName instances for a service and a port, creates a service object and adds a port, and then creates a Dispatch proxy associated with the port. Here is the code segment:

QName service_name = new QName("rcService", ns_URI.toString()); // uri is urn:fib
QName port = new QName("rcPort", ns_URI.toString());
String endpoint = "http://localhost:9876/fib";
// Now create a service proxy or dispatcher.
Service service = Service.create(service_name);
service.addPort(port, HTTPBinding.HTTP_BINDING, endpoint);
Dispatch<Source> dispatch = 
   service.createDispatch(port, Source.class, Service.Mode.PAYLOAD);   

This client-side dispatch object can dispatch XML documents as requests to the service as XML Source instances. A document is sent to the service through an invocation of the invoke method. Here are two code segments. In the first, an XML document is prepared as the body of a POST request:

String xml_start = "<fib:request xmlns:fib = 'urn:fib'>";
String xml_end = "</fib:request>";
List<Integer> nums = new ArrayList<Integer>();
for (int i = 0; i < 12; i++) nums.add(i + 1);
String xml = xml_start + nums.toString() + xml_end;

In the second, the request XML document is wrapped in Source and then sent to the service through an invocation of invoke:

StreamSource source = null;
if (data != null) source = make_stream_source(data.toString()); // data = XML doc
Source result = dispatch.invoke(source);
display_result(result, uri); // do an XPath search of the resturned XML

The GET and DELETE operations do not require XML documents; hence, the Source argument to invoke is null in both cases. Here is a client-side trace of the requests sent to the service and the responses received in return:

Request: <fib:request xmlns:fib = 'urn:fib'>
           [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
         </fib:request>
POSTed: [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144]

Request: null
GET: [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144]

Request: null
DELETE: Map cleared.

Request: null
GET: [ ]

Request: <fib:request xmlns:fib = 'urn:fib'>
            [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,...,20, 21, 22, 23, 24]
         </fib:request>
POSTed: [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89,..., 10946, 17711, 28657, 46368]

Request: null
GET: [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89,..., 10946, 6765, 28657, 17711, 46368]

Finally, here is the source code for the entire DispatchClient:

import java.net.URI;
import java.net.URISyntaxException;
import java.io.ByteArrayInputStream;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import javax.xml.namespace.QName;
import javax.xml.ws.Service;
import javax.xml.ws.Dispatch;
import javax.xml.ws.http.HTTPBinding;
import javax.xml.transform.stream.StreamSource;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.ws.handler.MessageContext;
import org.w3c.dom.NodeList;
import ch04.dispatch.NSResolver;

class DispatchClient {
    public static void main(String[ ] args) throws Exception {
        new DispatchClient().setup_and_test();
    }

    private void setup_and_test() {
        // Create identifying names for service and port.
        URI ns_URI = null;
        try {
            ns_URI = new URI("urn:fib");
        }
        catch(URISyntaxException e) { System.err.println(e); }

        QName service_name = new QName("rcService", ns_URI.toString());
        QName port = new QName("rcPort", ns_URI.toString());
        String endpoint = "http://localhost:9876/fib";

        // Now create a service proxy or dispatcher.
        Service service = Service.create(service_name);
        service.addPort(port, HTTPBinding.HTTP_BINDING, endpoint);
        Dispatch<Source> dispatch =
            service.createDispatch(port, Source.class, Service.Mode.PAYLOAD);

        // Send some requests.
        String xml_start = "<fib:request xmlns:fib = 'urn:fib'>";
        String xml_end = "</fib:request>";

        // To begin, a POST to create some Fibonacci numbers.
        List<Integer> nums = new ArrayList<Integer>();
        for (int i = 0; i < 12; i++) nums.add(i + 1);
        String xml = xml_start + nums.toString() + xml_end;
        invoke(dispatch, "POST", ns_URI.toString(), xml);

        // GET request to test whether the POST worked.
        invoke(dispatch, "GET", ns_URI.toString(), null);

        // DELETE request to remove the list
        invoke(dispatch, "DELETE", ns_URI.toString(), null);

        // GET to test whether the DELETE worked.
        invoke(dispatch, "GET", ns_URI.toString(), null);

        // POST to repopulate and a final GET to confirm
        nums = new ArrayList<Integer>();
        for (int i = 0; i < 24; i++) nums.add(i + 1);
        xml = xml_start + nums.toString() + xml_end;
        invoke(dispatch, "POST", ns_URI.toString(), xml);
        invoke(dispatch, "GET", ns_URI.toString(), null);
    }

    private void invoke(Dispatch<Source> dispatch,
                        String verb,
                        String uri,
                        Object data) {
        Map<String, Object> request_context = dispatch.getRequestContext();
        request_context.put(MessageContext.HTTP_REQUEST_METHOD, verb);

        System.out.println("Request: " + data);

        // Invoke
        StreamSource source = null;
        if (data != null) source = make_stream_source(data.toString());
        Source result = dispatch.invoke(source);
        display_result(result, uri);
    }

   private void display_result(Source result, String uri) {
        DOMResult dom_result = new DOMResult();
        try {
            Transformer trans = TransformerFactory.newInstance().newTransformer();
            trans.transform(result, dom_result);

            XPathFactory xpf = XPathFactory.newInstance();
            XPath xp = xpf.newXPath();
            xp.setNamespaceContext(new NSResolver("fib", uri));
            String result_string = 
               xp.evaluate("/fib:response", dom_result.getNode());
            System.out.println(result_string);
        }
        catch(TransformerConfigurationException e) { System.err.println(e); }
        catch(TransformerException e) { System.err.println(e); }
        catch(XPathExpressionException e) { System.err.println(e); }
    }

    private StreamSource make_stream_source(String msg) {
        ByteArrayInputStream stream = new ByteArrayInputStream(msg.getBytes());
        return new StreamSource(stream);
    }
}

A Dispatch Client Against a SOAP-based Service

The Dispatch client is flexible in that it may be used to issue requests against any service, REST-style or SOAP-based. This section illustrates how a SOAP-based service can be treated as if it were REST style. This use of Dispatch underscores that SOAP-based web services delivered over HTTP, as most are, represent a special case of REST-style services. What the SOAP libraries spare the programmer is the need to process XML directly on either the service or the client side, with handlers as the exception to this rule.

The DispatchClientTS application uses a Dispatch proxy to submit a request against the SOAP-based TimeServer service of Chapter 1. The TimeServer supports two operations: one supplies the current time as a human-readable string, whereas the other supplies the time as the elapsed milliseconds from the Unix epoch. Here is the source code for DispatchClientTS:

import java.util.Map;
import java.net.URI;
import java.net.URISyntaxException;
import java.io.ByteArrayInputStream;
import javax.xml.namespace.QName;
import javax.xml.ws.Service;
import javax.xml.ws.Dispatch;
import javax.xml.ws.http.HTTPBinding;
import javax.xml.transform.stream.StreamSource;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.ws.handler.MessageContext;
import ch04.dispatch.NSResolver;

// Dispatch client against the SOAP-based TimeServer service
class DispatchClientTS {
    public static void main(String[ ] args) throws Exception {
        new DispatchClientTS().send_and_receive_SOAP();
    }

    private void send_and_receive_SOAP() {
        // Create identifying names for service and port.
        URI ns_URI = null;
        try {
            ns_URI = new URI("http://ts.ch01/");      // from WSDL
        }
        catch(URISyntaxException e) { System.err.println(e); }

        QName service_name = new QName("tns", ns_URI.toString());
        QName port = new QName("tsPort", ns_URI.toString());
        String endpoint = "http://localhost:9876/ts"; // from WSDL
        // Now create a service proxy or dispatcher.
        Service service = Service.create(service_name);
        service.addPort(port, HTTPBinding.HTTP_BINDING, endpoint);
        Dispatch<Source> dispatch =
            service.createDispatch(port, Source.class, Service.Mode.PAYLOAD);
        // Send a request.
        String soap_request =
            "<?xml version='1.0' encoding='UTF-8'?> " +
            "<soap:Envelope " +
               "soap:encodingStyle='http://schemas.xmlsoap.org/soap/encoding/' " +
               "xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/' " +
               "xmlns:soapenc='http://schemas.xmlsoap.org/soap/encoding/' " +
               "xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' " +
               "xmlns:tns='http://ts.ch01/' " +
               "xmlns:xsd='http://www.w3.org/2001/XMLSchema'> " +
            "<soap:Body>" +
            "<tns:getTimeAsElapsed xsi:nil='true'/>" +
            "</soap:Body>" +
            "</soap:Envelope>";

        Map<String, Object> request_context = dispatch.getRequestContext();
        request_context.put(MessageContext.HTTP_REQUEST_METHOD, "POST");
        StreamSource source = make_stream_source(soap_request);
        Source result = dispatch.invoke(source);
        display_result(result, ns_URI.toString());
    }

    private void display_result(Source result, String uri) {
        DOMResult dom_result = new DOMResult();
        try {
            Transformer trans = TransformerFactory.newInstance().newTransformer();
            trans.transform(result, dom_result);

            XPathFactory xpf = XPathFactory.newInstance();
            XPath xp = xpf.newXPath();
            xp.setNamespaceContext(new NSResolver("tns", uri));
            // In original version, "//time_result" instead
            String result_string = xp.evaluate("//return", dom_result.getNode());
            System.out.println(result_string);
        }
        catch(TransformerConfigurationException e) { System.err.println(e); }
        catch(TransformerException e) { System.err.println(e); }
        catch(XPathExpressionException e) { System.err.println(e); }
    }

    private StreamSource make_stream_source(String msg) {
        ByteArrayInputStream stream = new ByteArrayInputStream(msg.getBytes());
        return new StreamSource(stream);
    }
}

The SOAP request document is hardcoded as a string. The rest of the setup is straightforward. After a service object is created and a port added with the TimeServer’s endpoint, a Dispatch proxy is created with a Service.Mode.PAYLOAD so that the SOAP request document becomes an XML Source transported to the service in the body of the HTTP request. The SOAP-based service responds with a SOAP envelope, which an XPath object then searches for the integer value that gives the elapsed milliseconds. On a sample run, the output was 1,214,514,573,623 (with commas added for readability) on a RESTful call to getTimeAsElapsed.

Implementing RESTful Web Services As HttpServlets

Here is a short review of servlets with emphasis on their use to deliver RESTful services. The class HttpServlet extends the class GenericServlet, which in turn implements the Servlet interface. All three are in the package javax.servlet, which is not included in core Java. The Servlet interface declares five methods, the most important of which is the service method that a web container invokes on every request to a servlet. The service method has a ServletRequest and a ServletResponse parameter. The request is a map that contains the request information from a client, and the response provides a network connection back to the client. The GenericServlet class implements the Service methods in a transport-neutral fashion, whereas its HttpServlet subclass implements these methods in an HTTP-specific way. Accordingly, the service parameters in the HttpServlet have the types HttpServletRequest and HttpServletResponse. The HttpServlet also provides request filtering: the service method dispatches an incoming GET request to the method doGet, an incoming POST request to the method doPost, and so on. Figure 4-2 depicts a servlet container with several servlets.

A servlet container with instances of various servlets
Figure 4-2. A servlet container with instances of various servlets

In the HttpServlet class, the do methods are no-ops (that is, methods with empty bodies) that can be overridden as needed in a programmer-derived subclass. For example, if the class MyServlet extends HttpServlet and overrides doGet but not doPost, then doPost remains a no-op in MyServlet instances.

HttpServlets are a natural, convenient way to implement RESTful web services for two reasons. First, such servlets provide methods such as doGet and doDelete that match up with HTTP verbs, and these methods execute as callbacks that the web container invokes as needed. Second, the HttpServletRequest and HttpServletResponse are the same two arguments to every do method, which encourages a uniform pattern of request processing: client-supplied data are read from the HttpServletRequest map and processed as required; then a response is sent back to the client through the output stream associated with the HttpServletResponse.

The RabbitCounterServlet

The RabbitCounterServlet that follows is a RESTful, servlet-based version of the SOAP-based RabbitCounter service of Chapter 3. The service has a deliberately simple logic to keep focus on what makes the servlet such an attractive implementation of a RESTful service. Here is the source code:

package ch04.rc;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.ws.http.HTTPException;
import java.util.Collections;
import java.util.Map;
import java.util.HashMap;
import java.util.Collection;
import java.util.List;
import java.util.ArrayList;
import java.io.IOException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.beans.XMLEncoder;

public class RabbitCounterServlet extends HttpServlet {
    private Map<Integer, Integer> cache;

    // Executed when servlet is first loaded into container.
    public void init() {
        cache = Collections.synchronizedMap(new HashMap<Integer, Integer>());
    }
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        String num = request.getParameter("num");
        
        // If no query string, assume client wants the full list
        if (num == null) {
            Collection<Integer> fibs = cache.values();
            send_typed_response(request, response, fibs);
        }
        else {
            try {
                Integer key = Integer.parseInt(num.trim());
                Integer fib = cache.get(key);
                if (fib == null) fib = -1;
                send_typed_response(request, response, fib);
            }
            catch(NumberFormatException e) {
                send_typed_response(request, response, -1);
            }
        }
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response) {
        String nums = request.getParameter("nums");
        if (nums == null)
            throw new HTTPException(HttpServletResponse.SC_BAD_REQUEST);

        // Extract the integers from a string such as: "[1, 2, 3]"
        nums = nums.replace('[', '\0');
        nums = nums.replace(']', '\0');
        String[ ] parts = nums.split(", ");
        List<Integer> list = new ArrayList<Integer>();
        for (String next : parts) {
            int n = Integer.parseInt(next.trim());
            cache.put(n, countRabbits(n));
            list.add(cache.get(n));
        }
        send_typed_response(request, response, list + " added.");
    }

    public void doDelete(HttpServletRequest request, HttpServletResponse response) {
        String key = request.getParameter("num");
        // Only one Fibonacci number may be deleted at a time.
        if (key == null)
            throw new HTTPException(HttpServletResponse.SC_BAD_REQUEST);
        try {
            int n = Integer.parseInt(key.trim());
            cache.remove(n);
            send_typed_response(request, response, n + " deleted.");
        }
        catch(NumberFormatException e) {
            throw new HTTPException(HttpServletResponse.SC_BAD_REQUEST);
        }
    }
    public void doPut(HttpServletRequest req, HttpServletResponse res) {
        throw new HTTPException(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
    }

    public void doInfo(HttpServletRequest req, HttpServletResponse res) {
        throw new HTTPException(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
    }

    public void doHead(HttpServletRequest req, HttpServletResponse res) {
        throw new HTTPException(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
    }

    public void doOptions(HttpServletRequest req, HttpServletResponse res) {
        throw new HTTPException(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
    }

    private void send_typed_response(HttpServletRequest request,
                                     HttpServletResponse response,
                                     Object data) {
        String desired_type = request.getHeader("accept");

        // If client requests plain text or HTML, send it; else XML.
        if (desired_type.contains("text/plain"))
            send_plain(response, data);
        else if (desired_type.contains("text/html"))
            send_html(response, data);
        else
            send_xml(response, data);
    }

    // For simplicity, the data are stringified and then XML encoded.
    private void send_xml(HttpServletResponse response, Object data) {
        try {
            XMLEncoder enc = new XMLEncoder(response.getOutputStream());
            enc.writeObject(data.toString());
            enc.close();
        }
        catch(IOException e) {
            throw new HTTPException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
    }
    private void send_html(HttpServletResponse response, Object data) {
        String html_start =
            "<html><head><title>send_html response</title></head><body><div>";
        String html_end = "</div></body></html>";
        String html_doc = html_start + data.toString() + html_end;
        send_plain(response, html_doc);
    }

    private void send_plain(HttpServletResponse response, Object data) {
        try {
            OutputStream out = response.getOutputStream();
            out.write(data.toString().getBytes());
            out.flush();
        }
        catch(IOException e) {
            throw new HTTPException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
    }

    private int countRabbits(int n) {
        if (n < 0) throw new HTTPException(403);

        // Easy cases.
        if (n < 2) return n;
        // Return cached value if present.
        if (cache.containsKey(n)) return cache.get(n);
        if (cache.containsKey(n - 1) && cache.containsKey(n - 2)) {
           cache.put(n, cache.get(n - 1) + cache.get(n - 2));
           return cache.get(n);
        }
        // Otherwise, compute from scratch, cache, and return.
        int fib = 1, prev = 0;
        for (int i = 2; i <= n; i++) {
            int temp = fib;
            fib += prev;
            prev = temp;
        }
        cache.put(n, fib); 
        return fib;
    }
}     

The RabbitCounterServlet overrides the init method, which the servlet container invokes when the servlet is first loaded. The method constructs the map that stores the Fibonacci numbers computed on a POST request. The other supported HTTP verbs are GET and DELETE. A GET request without a query string is treated as a request to read all of the numbers available, whereas a GET request with a query string is treated as a request for a specific Fibonacci number. The service allows the deletion of only one Fibonacci number at a time; hence, a DELETE request must have a query string that specifies which number to delete. The service does not implement the remaining CRUD operation, update; hence, the doPut method, like the remaining do methods, throws an HTTP 405 exception using the constant:

HttpServletResponse.SC_METHOD_NOT_ALLOWED

for clarity. There are similar constants for the other HTTP status codes.

Requests for MIME-Typed Responses

The RabbitCounterServlet differs from the first RESTful example in being implemented as a servlet instead of as a @WebServiceProvider. The RESTful servlet differs in a second way as well; that is, by honoring the request that a response be of a specified MIME type. Here is a client against the servlet:

import java.util.List;
import java.util.ArrayList;
import java.net.URL;
import java.net.HttpURLConnection;
import java.net.URLEncoder;
import java.net.MalformedURLException;
import java.net.URLEncoder;
import java.io.IOException;
import java.io.DataOutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;

class ClientRC {
    private static final String url = "http://localhost:8080/rc/fib";

    public static void main(String[ ] args) {
        new ClientRC().send_requests();
    }

    private void send_requests() {
        try {
            HttpURLConnection conn = null;

            // POST request to create some Fibonacci numbers.
            List<Integer> nums = new ArrayList<Integer>();
            for (int i = 1; i < 15; i++) nums.add(i);
            String payload = URLEncoder.encode("nums", "UTF-8") + "=" +
                URLEncoder.encode(nums.toString(), "UTF-8");

            // Send the request
            conn = get_connection(url, "POST");
            conn.setRequestProperty("accept", "text/xml");
            DataOutputStream out = new DataOutputStream(conn.getOutputStream());
            out.writeBytes(payload);
            out.flush();
            get_response(conn);

            // GET to test whether POST worked
            conn = get_connection(url, "GET");
            conn.addRequestProperty("accept", "text/xml");
            conn.connect();
            get_response(conn);

            conn = get_connection(url + "?num=12", "GET");
            conn.addRequestProperty("accept", "text/plain");
            conn.connect();
            get_response(conn);

            // DELETE request
            conn = get_connection(url + "?num=12", "DELETE");
            conn.addRequestProperty("accept", "text/xml");
            conn.connect();
            get_response(conn);

            // GET request to test whether DELETE worked
            conn = get_connection(url + "?num=12", "GET");
            conn.addRequestProperty("accept", "text/html");
            conn.connect();
            get_response(conn);
        }
        catch(IOException e) { System.err.println(e); }
        catch(NullPointerException e) { System.err.println(e); }
    }

    private HttpURLConnection get_connection(String url_string, String verb) {
        HttpURLConnection conn = null;
        try {
            URL url = new URL(url_string);
            conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod(verb);
            conn.setDoInput(true);
            conn.setDoOutput(true);
        }
        catch(MalformedURLException e) { System.err.println(e); }
        catch(IOException e) { System.err.println(e); }
        return conn;
    }
    private void get_response(HttpURLConnection conn) {
        try {
            String xml = "";
            BufferedReader reader =
                new BufferedReader(new InputStreamReader(conn.getInputStream()));
            String next = null;
            while ((next = reader.readLine()) != null)
                xml += next;
            System.out.println("The response:\n" + xml);
        }
        catch(IOException e) { System.err.println(e); }
    }
}     

The client sends POST, GET, and DELETE requests, each of which specifies the desired MIME type of the response. For example, in the POST request the statement:

conn.setRequestProperty("accept", "text/xml");

requests that the response be an XML document. The value of the accept key need not be a single MIME type. For instance, the request statement could be:

conn.setRequestProperty("accept", "text/xml, text/xml, application/soap");

Indeed, the listed types can be prioritized and weighted for the service’s consideration. This example sticks with single MIME types such as text/html.

The RabbitCounterServlet can send responses of MIME types text/xml (the default), text/html, and text/plain. Here is the client’s output, slightly formatted and documented for readability:

The response: // from the initial POST request to create some Fibonacci numbers
<?xml version="1.0" encoding="UTF-8"?> 
<java version="1.6.0_06" class="java.beans.XMLDecoder"> 
<string>
   [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377] added.
</string> 
</java>

The response: // from a GET request to confirm that the POST worked
<?xml version="1.0" encoding="UTF-8"?> 
<java version="1.6.0_06" class="java.beans.XMLDecoder"> 
<string>[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377]</string> 
</java>

The response: // from a GET request with text/plain as the desired type
144

The response: // from a DELETE request
<?xml version="1.0" encoding="UTF-8"?> 
<java version="1.6.0_06" class="java.beans.XMLDecoder"> 
<string>12 deleted.</string> 
</java>

The response: // from a GET to confirm the DELETE with HTML as the desired type
<html><head><title>send_html response</title></head>
<body><div>-1</div></body>
</html>

In the last response, the returned value of –1 signals that the Fibonacci number for 12 is not available. The XML and HTML formats are simple, of course, but they illustrate how RESTful services can generate typed responses that satisfy requests.

Java Clients Against Real-World RESTful Services

There are many RESTful services available from well-known players such as Yahoo!, Amazon, and eBay, although controversy continues around the issue of what counts as a truly RESTful service. This section provides sample clients against some of these commercial REST-style services.

The Yahoo! News Service

Here HTTP verb is a client against Yahoo!’s RESTful service that summarizes the current news on a specified topic. The request is an HTTP GET with a query string:

import java.net.URI;
import java.util.Map;
import javax.xml.namespace.QName;
import javax.xml.ws.Service;
import javax.xml.ws.Dispatch;
import javax.xml.ws.http.HTTPBinding;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.dom.DOMResult;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.ws.handler.MessageContext;
import org.w3c.dom.NodeList;
import yahoo.NSResolver;

// A client against the Yahoo! RESTful news summary service.
class YahooClient {
    public static void main(String[ ] args) throws Exception {
        if (args.length < 1) {
            System.err.println("YahooClient <your AppID>");
            return;
        }
        String app_id = "appid=" + args[0];
        
        // Create a name for a service port.
        URI ns_URI = new URI("urn:yahoo:yn");
        QName serviceName = new QName("yahoo", ns_URI.toString());
        QName portName = new QName("yahoo_port", ns_URI.toString());

        // Now create a service proxy
        Service s = Service.create(serviceName);

        String qs = app_id + "&type=all&results=10&" +
                    "sort=date&language=en&query=quantum mechanics";

        // Endpoint address
        URI address = new URI("http",                  // HTTP scheme
                              null,                    // user info
                              "api.search.yahoo.com",  // host
                              80,                      // port
                              "/NewsSearchService/V1/newsSearch", // path
                              qs,                      // query string
                              null);                   // fragment

        // Add the appropriate port
        s.addPort(portName, HTTPBinding.HTTP_BINDING, address.toString());

        // From the service, generate a Dispatcher
        Dispatch<Source> d =
            s.createDispatch(portName, Source.class, Service.Mode.PAYLOAD);
        Map<String, Object> request_context = d.getRequestContext();
        request_context.put(MessageContext.HTTP_REQUEST_METHOD, "GET");

        // Invoke
        Source result = d.invoke(null);
        DOMResult dom_result = new DOMResult();
        Transformer trans = TransformerFactory.newInstance().newTransformer();
        trans.transform(result, dom_result);

        XPathFactory xpf = XPathFactory.newInstance();
        XPath xp = xpf.newXPath();
        xp.setNamespaceContext(new NSResolver("yn", ns_URI.toString()));
        NodeList resultList = (NodeList)
            xp.evaluate("/yn:ResultSet/yn:Result",
                        dom_result.getNode(),
                        XPathConstants.NODESET);

        int len = resultList.getLength();
        for (int i = 1; i <= len; i++) {
            String title = 
                xp.evaluate("/yn:ResultSet/yn:Result(" + i + ")/yn:Title",
                            dom_result.getNode());
            String click =
                xp.evaluate("/yn:ResultSet/yn:Result(" + i + ")/yn:ClickUrl",
                            dom_result.getNode());
            System.out.printf("(%d) %s (%s)\n", i, title, click);
        }
    }
}   

This client application expects, as a command-line argument, the user’s application identifier. The news service is free but requires this identifier. (Signup is available at http://www.yahoo.com.) In this example, the client requests a maximum of 10 results on the topic of quantum gravity. Here is a segment of the raw XML that the Yahoo! service returns:

<?xml version="1.0" encoding="UTF-8"?>
<ResultSet xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns="urn:yahoo:yn"
   xsi:schemaLocation="urn:yahoo:yn
    http://api.search.yahoo.com/NewsSearchService/V1/NewsSearchResponse.xsd"
   totalResultsAvailable="9" totalResultsReturned="9"
   firstResultPosition="1">
<Result>
  <Title>Cosmic Log: Private space age turns 4</Title>
   <Summary>Science editor Alan Boyle's Weblog: Four years after the first 
            private-sector spaceship crossed the 62-mile mark, some space-age 
            dreams have been slow to rise while others have paid off.
   </Summary>
   <Url>
     http://cosmiclog.msnbc.msn.com/archive/2008/06/20/1158681.aspx
   </Url>
   <ClickUrl>
     http://cosmiclog.msnbc.msn.com/archive/2008/06/20/1158681.aspx
   </ClickUrl>
   <NewsSource>MSNBC</NewsSource>
   <NewsSourceUrl>http://www.msnbc.msn.com/</NewsSourceUrl>
   <Language>en</Language>
    <PublishDate>1213998337</PublishDate>
    <ModificationDate>1213998338</ModificationDate>
</Result>
...
</ResultSet>

Here is the parsed output that the YahooClient produces:

(1) Cosmic Log: Private space age turns 4 (http://cosmiclog.msnbc.msn.com/...
(2) Neutrino experiment shortcuts from novel to real world...
(3) There Will Be No Armageddon (http://www.spacedaily.com/reports/...
(4) TSX Venture Exchange Closing Summary for June 19, 2008 (http://biz.yahoo.com...
(5) Silver Shorts Reported (http://news.goldseek.com/GoldSeeker/1213848000.php)
(6) There will be no Armageddon (http://en.rian.ru/analysis/20080618/...
(7) New Lunar Prototype Vehicles Tested (Gallery)...
(8) World's Largest Quantum Bell Test Spans Three Swiss Towns...
(9) Creating science (http://www.michigandaily.com/news/2008/06/16/...

The client uses a Dispatch object to issue the request and an XPath object to search the DOM result for selected elements. The output above includes the Summary and the ClickURL elements from the raw XML. As quantum gravity is not a hot news topic, there were only 9 results from a request for 10.

The Yahoo! example underscores that clients of RESTful services assume the burden of processing the response document, which is typically XML, in some way that is appropriate to the application. Although there generally is an XML Schema that specifies how the raw XML is formatted, there is no service contract comparable to the WSDL used in SOAP-based services.

The Amazon E-Commerce Service: REST Style

Yahoo! exposes only RESTful web services, but Amazon provides its web services in two ways, as SOAP-based and as REST-style. The AmazonClientREST application that follows issues a read request against the Amazon E-Commerce service for books about the Fibonacci numbers. The client uses a Dispatch object and an HTTP GET request with a query string that specifies the details of the request:

import java.util.Map;
import javax.xml.namespace.QName;
import javax.xml.ws.Service;
import javax.xml.ws.Dispatch;
import javax.xml.ws.http.HTTPBinding;
import javax.xml.transform.stream.StreamSource;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.ws.handler.MessageContext;
import org.w3c.dom.NodeList;
import org.w3c.dom.Node;
import ch04.dispatch.NSResolver;

class AmazonClientREST {
    private final static String uri =
        "http://webservices.amazon.com/AWSECommerceService/2005-03-23";

    public static void main(String[ ] args) throws Exception {
        if (args.length < 1) {
            System.err.println("Usage: AmazonClientREST <access key>");
            return;
        }
        new AmazonClientREST().item_search(args[0].trim());
    }

    private void item_search(String access_key) {
        QName service_name = new QName("awsREST", uri);
        QName port = new QName("awsPort", uri);

        String base_url = "http://ecs.amazonaws.com/onca/xml";
        String qs = "?Service=AWSECommerceService&" +
            "Version=2005-03-23&" +
            "Operation=ItemSearch&" +
            "ContentType=text%2Fxml&" +
            "AWSAccessKeyId=" +  access_key + "&" +
            "SearchIndex=Books&" +
            "Keywords=Fibonacci";
        String endpoint = base_url + qs;

        // Now create a service proxy dispatcher.
        Service service = Service.create(service_name);
        service.addPort(port, HTTPBinding.HTTP_BINDING, endpoint);
        Dispatch<Source> dispatch =
            service.createDispatch(port, Source.class, Service.Mode.PAYLOAD);

        // Set HTTP verb.
        Map<String, Object> request_context = dispatch.getRequestContext();
        request_context.put(MessageContext.HTTP_REQUEST_METHOD, "GET");

        Source result = dispatch.invoke(null); // null payload: GET request
        display_result(result);
    }

    private void display_result(Source result) {
        DOMResult dom_result = new DOMResult();
        try {
            Transformer trans = TransformerFactory.newInstance().newTransformer();
            trans.transform(result, dom_result);
            XPathFactory xpf = XPathFactory.newInstance();
            XPath xp = xpf.newXPath();
            xp.setNamespaceContext(new NSResolver("aws", uri));

            NodeList authors = (NodeList)
                xp.evaluate("//aws:ItemAttributes/aws:Author",
                            dom_result.getNode(),
                            XPathConstants.NODESET);

            NodeList titles = (NodeList)
                xp.evaluate("//aws:ItemAttributes/aws:Title",
                            dom_result.getNode(),
                            XPathConstants.NODESET);

            int len = authors.getLength();
            for (int i = 0; i < len; i++) {
                Node author = authors.item(i);
                Node title  = titles.item(i);
                if (author != null && title != null) {
                    String a_name = author.getFirstChild().getNodeValue();
                    String t_name = title.getFirstChild().getNodeValue();
                    System.out.printf("%s: %s\n", a_name, t_name);
                }
            }
        }
        catch(TransformerConfigurationException e) { System.err.println(e); }
        catch(TransformerException e) { System.err.println(e); }
        catch(XPathExpressionException e) { System.err.println(e); }
    }
}      

The response document is now raw XML rather than a SOAP envelope. However, the raw XML conforms to the very same XML Schema document that is used in the E-Commerce WSDL contract for the SOAP-based version of the service. In effect, then, the only difference is that the raw XML is wrapped in a SOAP envelope in the case of the SOAP-based service, but is simply the payload of the HTTP response in the case of the REST-style service. Here is a segment of the raw XML:

<?xml version="1.0" encoding="UTF-8"?>
<ItemSearchResponse 
    xmlns="http://webservices.amazon.com/AWSECommerceService/2005-03-23">
   ...
  <ItemSearchRequest>
    <Keywords>Fibonacci</Keywords>
    <SearchIndex>Books</SearchIndex>
  </ItemSearchRequest>
  ...
  <TotalResults>177</TotalResults>
  <TotalPages>18</TotalPages>
  ...
  <Items>
    <Item>
      <ItemAttributes>
        <Author>Carolyn Boroden</Author>
        <Manufacturer>McGraw-Hill</Manufacturer>
        <ProductGroup>Book</ProductGroup>
        <Title>Fibonacci Trading: How to Master Time and Price Advantage</Title>
      </ItemAttributes>
    </Item>
  ...
  </Items>   
</ItemSearchResponse>      

For variety, the AmazonClientREST parses the raw XML in a slightly different way than in earlier examples. In particular, the client uses XPath to get separate lists of authors and book titles:

NodeList authors = (NodeList) xp.evaluate("//aws:ItemAttributes/aws:Author",
                                dom_result.getNode(), XPathConstants.NODESET);
NodeList titles = (NodeList)  xp.evaluate("//aws:ItemAttributes/aws:Title",
                                dom_result.getNode(), XPathConstants.NODESET);

and then loops through the lists to extract the author and the title using the DOM API. Here is the loop:

int len = authors.getLength();
for (int i = 0; i < len; i++) {
   Node author = authors.item(i);
   Node title  = titles.item(i);
   if (author != null && title != null) {
     String a_name = author.getFirstChild().getNodeValue();
     String t_name = title.getFirstChild().getNodeValue();
     System.out.printf("%s: %s\n", a_name, t_name);
   }
}

that produced, on a sample run, the following output:

Carolyn Boroden: Fibonacci Trading: How to Master Time and Price Advantage
Kimberly Elam: Geometry of Design: Studies in Proportion and Composition
Alfred S. Posamentier: The Fabulous Fibonacci Numbers
Ingmar Lehmann: Math for Mystics: From the Fibonacci sequence to Luna's Labyrinth...
Renna Shesso: Breakthrough Strategies for Predicting any Market...
Jeff Greenblatt: Wild Fibonacci: Nature's Secret Code Revealed
Joy N. Hulme: Fibonacci Analysis (Bloomberg Market Essentials: Technical Analysis)
Constance Brown: Fibonacci Fun: Fascinating Activities With Intriguing Numbers
Trudi Hammel Garland: Fibonacci Applications and Strategies for Traders
Robert Fischer: New Frontiers in Fibonacci Trading: Charting Techniques,...

The Amazon Simple Storage Service, known as Amazon S3, is a pay-for service also accessible through a SOAP-based or a RESTful client. As the name indicates, the service allows users to store and retrieve individual data objects, each of up to 5G in size. S3 is often cited as a fine example of a useful web service with a very simple interface.

The RESTful Tumblr Service

Perhaps the Tumblr service is best known for the associated term tumblelog or tlog, a variation on the traditional blog that emphasizes short text entries with associated multimedia such as photos, music, and film. It is common for tumblelogs to be artistic endeavors. The service is free but the full set of RESTful operations requires a user account, which can be set up at http://www.tumblr.com.

The TumblrClient that follows uses an HttpURLConnection to send a GET and a POST request against the Tumblr RESTful service. In this case, the HttpURLConnection is a better choice than Dispatch because a POST request to Tumblr does not contain an XML document but rather the standard key/value pairs. Here is the client code:

import java.net.URL;
import java.net.HttpURLConnection;
import java.net.URLEncoder;
import java.net.MalformedURLException;
import java.net.URLEncoder;
import java.io.IOException;
import java.io.DataOutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import javax.xml.transform.stream.StreamSource;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.dom.DOMResult;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import java.io.ByteArrayInputStream;
import org.w3c.dom.NodeList;
import org.w3c.dom.Node;

class TumblrClient {
    public static void main(String[ ] args) {
        if (args.length < 2) {
            System.err.println("Usage: TumblrClient <email> <passwd>");
            return;
        }
        new TumblrClient().tumble(args[0], args[1]);
    }

    private void tumble(String email, String password) {
        try {
            HttpURLConnection conn = null;

            // GET request.
            String url = "http://mgk-cdm.tumblr.com/api/read";
            conn = get_connection(url, "GET");
            conn.setRequestProperty("accept", "text/xml");
            conn.connect();
            String xml = get_response(conn);
            if (xml.length() > 0) {
                System.out.println("Raw XML:\n" + xml);
                parse(xml, "\nSki photo captions:", "//photo-caption");
            }

            // POST request
            url = "http://www.tumblr.com/api/write";
            conn = get_connection(url, "POST");
            String title = "Summer thoughts up north";
            String body = "Craigieburn Ski Area, NZ";
            String payload =
                URLEncoder.encode("email", "UTF-8") + "=" +
                URLEncoder.encode(email, "UTF-8") + "&" +
                URLEncoder.encode("password", "UTF-8") + "=" +
                URLEncoder.encode(password, "UTF-8") + "&" +
                URLEncoder.encode("type", "UTF-8") + "=" +
                URLEncoder.encode("regular", "UTF-8") + "&" +
                URLEncoder.encode("title", "UTF-8") + "=" +
                URLEncoder.encode(title, "UTF-8") + "&" +
                URLEncoder.encode("body", "UTF-8") + "=" +
                URLEncoder.encode(body, "UTF-8");
            DataOutputStream out = new DataOutputStream(conn.getOutputStream());
            out.writeBytes(payload);
            out.flush();
            String response = get_response(conn);
            System.out.println("Confirmation code: " + response);
        }
        catch(IOException e) { System.err.println(e); }
        catch(NullPointerException e) { System.err.println(e); }
    }

    private HttpURLConnection get_connection(String url_s, String verb) {
        HttpURLConnection conn = null;
        try {
            URL url = new URL(url_s);
            conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod(verb);
            conn.setDoInput(true);
            conn.setDoOutput(true);
        }
        catch(MalformedURLException e) { System.err.println(e); }
        catch(IOException e) { System.err.println(e); }
        return conn;
    }

    private String get_response(HttpURLConnection conn) {
        String xml = "";
        try {
            BufferedReader reader =
                new BufferedReader(new InputStreamReader(conn.getInputStream()));
            String next = null;
            while ((next = reader.readLine()) != null) xml += next;
        }
        catch(IOException e) { System.err.println(e); }
        return xml;
    }

    private void parse(String xml, String msg, String pattern) {
        StreamSource source =
            new StreamSource(new ByteArrayInputStream(xml.getBytes()));
        DOMResult dom_result = new DOMResult();
        System.out.println(msg);
        try {
            Transformer trans = TransformerFactory.newInstance().newTransformer();
            trans.transform(source, dom_result);
            XPathFactory xpf = XPathFactory.newInstance();
            XPath xp = xpf.newXPath();
            NodeList list = (NodeList)
                xp.evaluate(pattern, dom_result.getNode(), XPathConstants.NODESET);
            int len = list.getLength();
            for (int i = 0; i < len; i++) {
                Node node = list.item(i);
                if (node != null) 
                  System.out.println(node.getFirstChild().getNodeValue());
            }
        }
        catch(TransformerConfigurationException e) { System.err.println(e); }
        catch(TransformerException e) { System.err.println(e); }
        catch(XPathExpressionException e) { System.err.println(e); }
    }
}      

The URL for the GET request is:

http://mgk-cdm.tumblr.com/api/read

which is the URL for my Tumblr account’s site with api/read appended. The request returns all of my public (that is, unsecured) postings. Here is part of the raw XML returned as the response:

<?xml version="1.0" encoding="UTF-8"?>
<tumblr version="1.0">
  <tumblelog name="mgk-cdm" timezone="US/Eastern" title="Untitled"/>
  <posts start="0" total="5">
    <post id="40130991" url="http://mgk-cdm.tumblr.com/post/40130991" type="photo"
         date-gmt="2008-06-28 03:09:29 GMT" date="Fri, 27 Jun 2008 23:09:29"
         unix-timestamp="1214622569">
     <photo-caption>Trying the new skis, working better than I am.</photo-caption>
     <photo-url max-width="500">
       http://media.tumblr.com/awK1GiaTRar6p46p6Xy13mBH_500.jpg
     </photo-url>
    </post>
    ...
    <post id="40006745" url="http://mgk-cdm.tumblr.com/post/40006745" type="regular"
          date-gmt="2008-06-27 04:12:53 GMT" date="Fri, 27 Jun 2008 00:12:53"
          unix-timestamp="1214539973">
      <regular-title>Weathering the weather</regular-title>
      <regular-body>miserable, need to get fully wet or not at all</regular-body>
    </post>
    ...
    <post id="40006638" url="http://mgk-cdm.tumblr.com/post/40006638" type="regular"
          date-gmt="2008-06-27 04:11:34 GMT" date="Fri, 27 Jun 2008 00:11:34"
          unix-timestamp="1214539894">
      <regular-title>tumblr. API</regular-title>
      <regular-body>Very restful</regular-body>
    </post>
  </posts>
</tumblr>      

The raw XML has a very simple structure, dispensing even with namespaces. The TumblrClient uses XPath to extract a list of the photo captions:

Ski photo captions:
Trying the new skis, working better than I am.
Very tough day on the trails; deep snow, too deep for skating.
Long haul up, fun going down.

The client then sends a POST request, which adds a new entry in my Tumblr posts. The URL now changes to the main Tumblr site, http://www.tumblr.com, with /api/write appended. My email and password must be included in the POST request’s payload, which the following code segment handles:

String payload =
   URLEncoder.encode("email", "UTF-8") + "=" +
   URLEncoder.encode(email, "UTF-8") + "&" +
   URLEncoder.encode("password", "UTF-8") + "=" +
   URLEncoder.encode(password, "UTF-8") + "&" +
   URLEncoder.encode("type", "UTF-8") + "=" +
   URLEncoder.encode("regular", "UTF-8") + "&" +
   URLEncoder.encode("title", "UTF-8") + "=" +
   URLEncoder.encode(title, "UTF-8") + "&" +
   URLEncoder.encode("body", "UTF-8") + "=" +
   URLEncoder.encode(body, "UTF-8");
DataOutputStream out = new DataOutputStream(conn.getOutputStream());
out.writeBytes(payload);
out.flush();

The documentation for the Tumblr API is a single page. The API supports the CRUD read and create operations through the /api/read and the /api/write suffixes. As usual, a read operation is done through a GET request, and a create operation is done through a POST request. Tumblr does support some variation. For example, the suffix /api/read/json causes the response to be JSON (JavaScript Object Notation) instead of XML. An HTTP POST to the Tumblr site can be used to upload images, audio, and film in addition to text postings, and multimedia may be uploaded as either unencoded bytes or as standard URL-encoded payloads in the POST request’s body.

The simplicity of the Tumblr API encourages the building of graphical interfaces and plugins that, in turn, allow Tumblr to interact easily with other sites such as Facebook. The Tumblr API is a fine example of how much can be done with so little.

WADLing with Java-Based RESTful Services

In SOAP-based web services, the WSDL document is a blessing to programmers because this service contract can be used to generate client-side artifacts and, indeed, even a service-side interface. RESTful services do not have an official or even widely accepted counterpart to the WSDL, although there are efforts in that direction. Among them is the WADL initiative. WADL stands for Web Application Description Language.

The WADL download includes the wadl2java utility, a library of required JAR files, and a sample WADL file named YahooSearch.wadl. The download also has Ant, Maven, and command-line scripts for convenience. To begin, here is a Yahoo! client that uses wadl2java-generated artifacts:

import com.yahoo.search.ResultSet;
import com.yahoo.search.ObjectFactory;
import com.yahoo.search.Endpoint;
import com.yahoo.search.Endpoint.NewsSearch;
import com.yahoo.search.Type;
import com.yahoo.search.Result;
import com.yahoo.search.Sort;
import com.yahoo.search.ImageType;
import com.yahoo.search.Output;
import com.yahoo.search.Error;
import com.yahoo.search.SearchErrorException;
import javax.xml.bind.JAXBException;
import java.io.IOException;
import java.util.List;

class YahooWADL {
    public static void main(String[ ] args) {
        if (args.length < 1) {
            System.err.println("Usage: YahooWADL <app id>");
            return;
        }
        String app_id = args[0];
        try {
            NewsSearch service = new NewsSearch();
            String query = "neutrino";

            ResultSet result_set = service.getAsResultSet(app_id, query);
            List<Result> list = result_set.getResultList();
            int i = 1;
            for (Result next : list) {
                String title = next.getTitle();
                String click = next.getClickUrl();
                System.out.printf("(%d) %s %s\n", i++, title, click);
            }
        }
        catch(JAXBException e) { System.err.println(e); }
        catch(SearchErrorException e) { System.err.println(e); }
        catch(IOException e) { System.err.println(e); }
    }
}    

The code is cleaner than my original YahooClient. The wadl2java-generated code hides the XML processing and other grimy details such as the formatting of an appropriate query string for a GET request against the Yahoo! News Service. The client-side artifacts also include utility classes for getting images from the Yahoo! service. On a sample run, the YahooWADL client produced this output on a request for articles that include the keyword neutrino:

(1) Congress to the rescue for Fermi jobs http://www.dailyherald.com/story/...
(2) AIP FYI #69: Senate FY 2009 National Science Foundation Funding Bill...
(3) Linked by Thom Holwerda on Wed 12th Sep 2007 11:51 UTC...
(4) The World's Nine Largest Science Projects http://science.slashdot.org/...
(5) Funding bill may block Fermi layoffs http://www.suntimes.com/business/...
(6) In print http://www.sciencenews.org/view/generic/id/33654/title/For_Kids...
(7) Recent Original Stories http://www.osnews.com/thread?284017
(8) Antares : un télescope pointé vers le sol qui utilise la terre comme filtre...
(9) Software addresses quality of hands-free car phone audio...
(10) Planetary science: Tunguska at 100 http://www.nature.com/news/2008/...

Here is the WADL document used to generate the client-side artifacts:

<?xml version="1.0"?>
<!--
The contents of this file are subject to the terms of the Common Development and 
Distribution License (the "License").  You may not use this file except in  
compliance with the License. You can obtain a copy of the license at 
     http://www.opensource.org/licenses/cddl1.php
See the License for the specific language governing
permissions and limitations under the License.
-->
<application xmlns:xsd="http://www.w3.org/2001/XMLSchema"
             xmlns:yn="urn:yahoo:yn"
             xmlns:ya="urn:yahoo:api"
             xmlns:html="http://www.w3.org/1999/xhtml"
             xmlns="http://research.sun.com/wadl/2006/10">
  <grammars>
    <include href="NewsSearchResponse.xsd"/>
    <include href="NewsSearchError.xsd"/>
  </grammars>

  <resources base="http://api.search.yahoo.com/NewsSearchService/V1/">
    <resource path="newsSearch">
      <doc xml:lang="en" title="Yahoo News Search Service">
        The <html:i>Yahoo News Search</html:i> service provides online 
                    searching of news stories from around the world.
      </doc>
      <param name="appid" type="xsd:string" required="true" style="query">
        <doc>The application ID. See 
         <html:a href="http://developer.yahoo.com/faq/index.html#appid">
           Application IDs
         </html:a> for more information.
        </doc>
      </param>
      <method href="#search"/>
    </resource>
  </resources>

  <method name="GET" id="search">
    <doc xml:lang="en" title="Search news stories by keyword"/>
    <request>
      <param name="query" type="xsd:string" required="true" style="query">
        <doc xml:lang="en" title="Space separated keywords to search for"/>
      </param>
      <param name="type" type="xsd:string" default="all" style="query">
        <doc xml:lang="en" title="Keyword matching"/>
        <option value="all">
          <doc>All query terms.</doc>
        </option>
        <option value="any">
          <doc>Any query terms.</doc>
        </option>
        <option value="phrase">
          <doc>Query terms as a phrase.</doc>
        </option>
      </param>
      <param name="results" type="xsd:int" default="10" style="query">
        <doc xml:lang="en" title="Number of results"/>
      </param>
      <param name="start" type="xsd:int" default="1" style="query">
        <doc xml:lang="en" title="Index of first result"/>
      </param>
      <param name="sort" type="xsd:string" default="rank" style="query">
        <doc xml:lang="en" title="Sort by date or rank"/>
        <option value="rank"/>
        <option value="date"/>
      </param>
      <param name="language" type="xsd:string" style="query">
        <doc xml:lang="en" title="Language filter, omit for any language"/>
      </param>
      <param name="output" type="xsd:string" default="xml" style="query">
        <doc>The format for the output. If <html:em>json</html:em> is requested, 
             the results will be returned in 
             <html:a href="http://developer.yahoo.com/common/json.html">
                  JSON
             </html:a> format. If <html:em>php</html:em> is requested, the 
             results will be returned in 
              <html:a href="http://developer.yahoo.com/common/phpserial.html">
                 Serialized PHP
              </html:a> format.
        </doc>
        <option value="xml"/>
        <option value="json"/>
        <option value="php"/>
      </param>
      <param name="callback" type="xsd:string" style="query">
        <doc>The name of the callback function to wrap around the JSON data. 
             The following characters are allowed: A-Z a-z 0-9 . 
             [ ] and _. If output=json has not been requested, this 
             parameter has no effect. More information on the callback can be 
             found in the 
             <html:a href="http://developer.yahoo.com/common/json.html
             #callbackparam">Yahoo! Developer Network JSON 
             Documentation</html:a>.
      </doc>
      </param>
    </request>
    <response>
      <representation mediaType="application/xml" element="yn:ResultSet">
        <doc xml:lang="en" title="A list of news items matching the query"/>
      </representation>
      <fault id="SearchError" status="400" mediaType="application/xml"
             element="ya:Error"/>
    </response>
  </method>
</application>  

The WADL document begins with references to two XSD documents, one of which is the grammar for error documents and the other of which is the grammar for normal response documents from the service. Next comes a list of available resources, in this case only the news search service. The methods section describes the HTTP verbs and, by implication, the CRUD operations that can be used against the service. In the case of the Yahoo! news service, only GET requests are allowed. The remaining sections provide details about the parameters that can accompany requests and responses. XSD type information of the sort found in WSDL documents occurs throughout the WADL as well.

Executing the wadl2java utility on the YahooSearch.wadl file generates 11 Java source files, the most important of which for the client-side programmer is Endpoint.java. The Endpoint class encapsulates the static class NewsSearch, an instance of which has utility methods such as getAsResultSet. For reference, here is the main segment of the client YahooWADL shown earlier. The WADL artifacts allow the code to be short and clear:

NewsSearch service = new NewsSearch();
String query = "neutrino";

ResultSet result_set = service.getAsResultSet(app_id, query);
List<Result> list = result_set.getResultList();
int i = 1;
for (Result next : list) {
   String title = next.getTitle();
   String click = next.getClickUrl();
   System.out.printf("(%d) %s %s\n", i++, title, click);
}    

The search string, given in this case with the object reference query, is just a list of keywords separated by blanks. The NewsSearch object has properties for specifying sort order, the maximum number of items to return, and the like. The generated artifacts do lighten the coding burden.

WADL has stirred interest and discussion but remains, for now, a Java-centric initiative. The critical issue is whether REST-style services can be standardized to the extent that utilities such as wadl2java and perhaps java2wadl can measure up to the utilities now available for SOAP-based services. If it is fair to criticize SOAP-based services for being over-engineered, it is also fair to fault REST-style services for being under-engineered.

Problems in the wadl2java-Generated Code

Among the files that the wadl2java utility generates are Error.java and ObjectFactory.java. In each file, any occurrences of urn:yahoo:api should be changed to urn:yahoo:yn. In the 1.0 distribution, there were two occurrences in each source file. Without these changes, a JAX-B exception is thrown when the results from the Yahoo! search are unmarshaled into Java objects. The changes could be made to the XSD document and the WADL document that the wadl2java utility uses.

JAX-RS: WADLing Through Jersey

Jersey is the centerpiece project for the recent JAX-RS (Java API for XML-RESTful Web Services). Jersey applications can be deployed through familiar commercial-grade containers such as standalone Tomcat and GlassFish, but Jersey also provides the lightweight Grizzly container that is well suited for learning the framework. Jersey works well with Maven. A deployed Jersey service automatically generates a WADL, which is then available through a standard GET request. A good place to start is https://jersey.dev.java.net.

A Jersey service adheres to REST principles. A service accepts the usual RESTful requests for CRUD operations specified with the standard HTTP verbs GET, POST, DELETE, and PUT. A request is targeted at a Jersey resource, which is a POJO. Here is the MsgResource class to illustrate:

package msg.resources; 

import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.FormParam;
import javax.ws.rs.Produces;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.DELETE;
import java.beans.XMLEncoder;
import java.io.ByteArrayOutputStream;

// This is the base path, which can be extended at the method level.
@Path("/")
public class MsgResource {
    private static String msg = "Hello, world!";

    @GET
    @Produces("text/plain")
    public String read() {
        return msg + "\n";
    }

    @GET
    @Produces("text/plain")
    @Path("{extra}")
    public String personalized_read(@PathParam("extra") String cus) {
        return this.msg + ": " + cus + "\n";
    }

    @POST
    @Produces("text/xml")
    public String create(@FormParam("msg") String new_msg ) {
        this.msg = new_msg;

        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        XMLEncoder enc = new XMLEncoder(stream);
        enc.writeObject(new_msg);
        enc.close();
        return new String(stream.toByteArray()) + "\n";
    }

    @DELETE
    @Produces("text/plain")
    public String delete() {
        this.msg = null;
        return "Message deleted.\n";
    }
}

The class has intuitive annotations, including the ones for the HTTP verbs and the response MIME types. The @Path annotation right above the MsgResource class declaration is used to decouple the resource from any particular base URL. For example, the MsgResource might be available at the base URL http://foo.bar.org:1234, at the base URL http://localhost:9876, and so on. The @GET, @POST, and @DELETE annotations specify the appropriate HTTP verb for a particular service operation. The @Produces annotation specifies the MIME type of the response, in this case either text/plain for the GET and DELETE operations or text/xml for the POST operation. Each method annotated as a MsgResource is responsible for generating the declared response type.

The MsgResource class could be put in a WAR file along with the supporting Jersey JAR files and then deployed in a servlet container such as Tomcat. There is, however, a quick way to publish a Jersey service during development. Here is the publisher class to illustrate:

import com.sun.jersey.api.container.grizzly.GrizzlyWebContainerFactory;
import java.util.Map;
import java.util.HashMap;

class JerseyPublisher {
    public static void main(String[ ] args) {
        final String base_url = "http://localhost:9876/";
        final Map<String, String> config = new HashMap<String, String>();

        config.put("com.sun.jersey.config.property.packages",
                   "msg.resources"); // package with resource classes

        System.out.println("Grizzly starting on port 9876.\n" +
                           "Kill with Control-C.\n");
        try {
            GrizzlyWebContainerFactory.create(base_url, config);
        }
        catch(Exception e) { System.err.println(e); }
    }
}

Grizzly requires configuration information about the package, in this case named msg.resources, that contains the resources available to clients. In this example, the package houses only the single class MsgResource but could house several resources. On each incoming request, the Grizzly container surveys the available resources to determine which method should handle the request. RESTful routing is thus in effect. For example, a POSTed request is delegated only to a method annotated with @POST.

Compiling and executing the resource and the publisher requires that several Jersey JAR files be on the classpath. Here is the list of five under the current release:

asm-3.1.jar  
grizzly-servlet-webserver-1.8.3.jar  
jersey-core-0.9-ea.jar  
jersey-server-0.9-ea.jar  
jsr311-api-0.9.jar

All of these JAR files, together with others for a Maven-centric version of Jersey, are available at the Jersey home page cited earlier.

Once the JerseyPublisher has been started, a browser or a utility such as curl can be used to access the resource. For example, the curl command:

% curl http://localhost:9876/

issues a GET request against the service, which causes the @GET-annotated read method to execute. The response is the default message:

Hello, world! 

By contrast, the curl command:

% curl -d msg='Goodbye, cruel world!' http://localhost:9876/echo/fred

issues a POST request against the service, which in turn causes the @POST-annotated create method to execute. (A REST purist might argue that a PUT operation would be more appropriate here, as the create method arguably updates an existing message rather than creates a message.) The create method uses the @FormParam annotation so that the POSTed data are available as the method’s argument. The @FormParam parameter, in this case the string msg, need not be the same as the method parameter, in this case new_msg. The output is:

<?xml version="1.0" encoding="UTF-8"?>
<java version="1.6.0_06" class="java.beans.XMLDecoder">
 <string>Goodbye, cruel world!</string>
</java>      

because the @Produces annotation on the create method specifies text/xml as the response type. The method generates this response type with the XMLEncoder.

In addition to the read method, there is a second method, personalized_read, annotated with @GET. The method also has the annotation @Path("{extra}"). For example, the request:

% curl http://localhost:9876/bye

causes this method to be invoked with bye as the argument. The braces surrounding extra signal that extra is simply a placeholder rather than a literal. A method-level @Path is appended to the class-level @Path. In this example, the class-level @Path is simply /.

The Grizzly publisher automatically generates a WADL document, which is available at:

http://localhost:9876/application.wadl

Here is the automatically generated WADL for the MsgResource:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<application xmlns="http://research.sun.com/wadl/2006/10">
    <doc xmlns:jersey="http://jersey.dev.java.net/" 
         jersey:generatedBy="Jersey: 0.9-ea 08/22/2008 04:48 PM"/>
    <resources base="http://localhost:9876/">
        <resource path="/">
            <method name="DELETE" id="delete">
                <response>
                    <representation mediaType="text/plain"/>
                </response>
            </method>
            <method name="GET" id="read">
                <response>
                    <representation mediaType="text/plain"/>
                </response>
            </method>
            <method name="POST" id="create">
                <request>
                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" 
                           type="xs:string" name="msg"/>
                </request>
                <response>
                    <representation mediaType="text/xml"/>
                </response>
            </method>
            <resource path="{extra}">
                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" 
                       type="xs:string" style="template" name="extra"/>
                <method name="GET" id="personalized_read">
                    <response>
                        <representation mediaType="text/plain"/>
                    </response>
                </method>
            </resource>
        </resource>
    </resources>
</application>

The WADL captures that the MsgResource supports two GET operations, a POST operation, and a DELETE operation. The WADL also describes the MIME type of the response representation for each operation. Of course, this WADL document could be used as input to the wadl2java utility.

Jersey is an appropriately lightweight framework that honors the spirit of RESTful services. The Grizzly publisher is attractive for development, automatically generating a WADL document to describe the published services. For production, the move to a web container, standalone or embedded in an application server, is straightforward because Jersey resources are simply annotated POJOs. The entire JSR-311 API, the Jersey core, comprises only 3 packages and roughly 50 interfaces and classes.

For now, JAX-WS and JAX-RS are separate frameworks. It would not be surprising if, in the future, the two frameworks merged.

The Restlet Framework

Several web frameworks have embraced REST, perhaps none more decisively than Rails with its ActiveResource type, which implements a resource in the RESTful sense. Rails also emphasizes a RESTful style in routing, with CRUD request operations specified as standard HTTP verbs. Grails is a Rails knockoff implemented in Groovy, which in turn is a Ruby knockoff with access to the standard Java packages. Apache Sling is a Java-based web framework with a RESTful orientation.

The restlet framework adheres to the REST architectural style and draws inspiration from other lightweight but powerful frameworks such as NetKernel and Rails. As the name indicates, a restlet is a RESTful alternative to the traditional Java servlet. The restlet framework has a client and a service API. The framework is well designed, relatively straightforward, professionally implemented, and well documented. It plays well with existing technologies. For example, a restlet can be deployed in a servlet container such as Tomcat or Jetty. The restlet distribution includes integration support for the Spring framework and also comes with the Simple HTTP engine, which can be embedded in Java applications. The sample restlet in this section is published with the Simple HTTP engine.

The FibRestlet application reprises the Fibonacci example yet again. The service, published with the Simple HTTP engine, illustrates key constructs in a restlet. Here is the source code:

package ch04.restlet;

import java.util.Collections;
import java.util.Map;
import java.util.HashMap;
import java.util.Collection;
import java.util.List;
import java.util.ArrayList;
import org.restlet.Component;
import org.restlet.Restlet;
import org.restlet.data.Form;
import org.restlet.data.MediaType;
import org.restlet.data.Method;
import org.restlet.data.Parameter;
import org.restlet.data.Protocol;
import org.restlet.data.Request;
import org.restlet.data.Response;
import org.restlet.data.Status;

public class FibRestlet {
    private Map<Integer, Integer> cache = 
       Collections.synchronizedMap(new HashMap<Integer, Integer>());
    private final String xml_start = "<fib:response xmlns:fib = 'urn:fib'>";
    private final String xml_stop = "</fib:response>";

    public static void main(String[ ] args) {
        new FibRestlet().publish_service();
    }

    private void publish_service() {
        try {
            // Create a component to deploy as a service.
            Component component = new Component();

            // Add an HTTP server to connect clients to the component.
            // In this case, the Simple HTTP engine is the server.
            component.getServers().add(Protocol.HTTP, 7777);

            // Attach a handler to handle client requests. (Note the
            // similarity of the handle method to an HttpServlet
            // method such as doGet or doPost.)
            Restlet handler = new Restlet(component.getContext()) {
                @Override
                public void handle(Request req, Response res) {
                    Method http_verb = req.getMethod();

                    if (http_verb.equals(Method.GET)) {
                        String xml = to_xml();
                        res.setStatus(Status.SUCCESS_OK);
                        res.setEntity(xml, MediaType.APPLICATION_XML);
                    }
                    else if (http_verb.equals(Method.POST)) {
                        // The HTTP form contains key/value pairs.
                        Form form = req.getEntityAsForm();
                        String nums = form.getFirstValue("nums");
                        if (nums != null) {
                            // nums should be a list in the form: "[1, 2, 3]"
                            nums = nums.replace('[', '\0');
                            nums = nums.replace(']', '\0');
                            String[ ] parts = nums.split(",");
                            List<Integer> list = new ArrayList<Integer>();
                            for (String next : parts) {
                                int n = Integer.parseInt(next.trim());
                                cache.put(n, countRabbits(n));
                                list.add(cache.get(n));
                            }
                            String xml =
                              xml_start + "POSTed: " + list.toString() + xml_stop;
                            res.setStatus(Status.SUCCESS_OK);
                            res.setEntity(xml, MediaType.APPLICATION_XML);
                        }
                    }
                    else if (http_verb.equals(Method.DELETE)) {
                        cache.clear(); // remove the resource
                        String xml =
                            xml_start + "Resource deleted" + xml_stop;
                        res.setStatus(Status.SUCCESS_OK);
                        res.setEntity(xml, MediaType.APPLICATION_XML);
                    }
                    else // only GET, POST, and DELETE supported
                        res.setStatus(Status.SERVER_ERROR_NOT_IMPLEMENTED);
                }};

            // Publish the component as a service and start the service.
            System.out.println("FibRestlet at: http://localhost:7777/fib");
            component.getDefaultHost().attach("/fib", handler);
            component.start();
        }
        catch (Exception e) { System.err.println(e); }
    }

    private String to_xml() {
        Collection<Integer> list = cache.values();
        return xml_start + "GET: " + list.toString() + xml_stop;
    }

    private int countRabbits(int n) {
        n = Math.abs(n); // eliminate possibility of a negative argument

        // Easy cases.
        if (n < 2) return n;

        // Return cached values if present.
        if (cache.containsKey(n)) return cache.get(n);
        if (cache.containsKey(n - 1) &&
            cache.containsKey(n - 2)) {
           cache.put(n, cache.get(n - 1) + cache.get(n - 2));
           return cache.get(n);
        }

        // Otherwise, compute from scratch, cache, and return.
        int fib = 1, prev = 0;
        for (int i = 2; i <= n; i++) {
            int temp = fib;
            fib += prev;
            prev = temp;
        }
        cache.put(n, fib);
        return fib;
    }
}    

As the source code shows, the restlet framework provides easy-to-use Java wrappers such as Method, Request, Response, Form, Status, and MediaType for HTTP and MIME constructs. The framework supports virtual hosts for commercial-grade applications.

The restlet download includes a subdirectory RESTLET_HOME/lib that houses the various JAR files for the restlet framework itself and for interoperability with Tomcat, Jetty, Spring, Simple, and so forth. For the sample restlet service in this section, the JAR files com.noelios.restlet.jar, org.restlet.jar, and org.simpleframework.jar must be on the classpath.

A restlet client could be written using a standard class such as HttpURLConnection, of course. The following client illustrates the restlet API on the client side, an API that could be used independently of the service API:

import org.restlet.Client;
import org.restlet.data.Form;
import org.restlet.data.Method;
import org.restlet.data.Protocol;
import org.restlet.data.Request;
import org.restlet.data.Response;
import java.util.List;
import java.util.ArrayList;
import java.io.IOException;

class RestletClient {
    public static void main(String[ ] args) {
        new RestletClient().send_requests();
    }

    private void send_requests() {
        try {
            // Setup the request.
            Request request = new Request();
            request.setResourceRef("http://localhost:7777/fib");

            // To begin, a POST to create some service data.
            List<Integer> nums = new ArrayList<Integer>();
            for (int i = 0; i < 12; i++) nums.add(i);
            
            Form http_form = new Form();
            http_form.add("nums", nums.toString());
            request.setMethod(Method.POST);
            request.setEntity(http_form.getWebRepresentation());

            // Generate a client and make the call.
            Client client = new Client(Protocol.HTTP);

            // POST request
            Response response = get_response(client, request);
            dump(response);

            // GET request to confirm POST
            request.setMethod(Method.GET);
            request.setEntity(null);
            response = get_response(client, request);
            dump(response);

            // DELETE request
            request.setMethod(Method.DELETE);
            request.setEntity(null);
            response = get_response(client, request);
            dump(response);

            // GET request to confirm DELETE
            request.setMethod(Method.GET);
            request.setEntity(null);
            response = get_response(client, request);
            dump(response);
        }
        catch(Exception e) { System.err.println(e); }
    }

    private Response get_response(Client client, Request request) {
        return client.handle(request);
    }

    private void dump(Response response) {
        try {
            if (response.getStatus().isSuccess())
                response.getEntity().write(System.out);
            else
                System.err.println(response.getStatus().getDescription());
        }
        catch(IOException e) { System.err.println(e); }
    }
}    

The client API is remarkably clean. In this example, the client issues POST and DELETE requests with GET requests to confirm that the create and delete operations against the service were successful.

The restlet framework is a quick study. Its chief appeal is its RESTful orientation, which results in a lightweight but powerful software environment for developing and consuming RESTful services. The chief issue is whether the restlet framework can gain the market and mind share to become the standard environment for RESTful services in Java. The Jersey framework has the JSR seal of approval, which gives this framework a clear advantage.

What’s Next?

Web services, whether SOAP based or REST style, likely require security. The term security is broad and vague. The next chapter clarifies the notion and explores the technologies available for securing web services. The emphasis is on user authentication and authorization, mutual challenge, and message encryption and decryption.



[1] For a thorough coverage of REST-style web services, see Leonard Richardson and Sam Ruby’s book RESTful Web Services (O’Reilly).

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required