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

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";
    }
}

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