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.
HTTP verb | Meaning in CRUD terms |
POST | Create a new resource from the request data |
GET | Read a resource |
PUT | Update a resource from the request data |
DELETE | Delete 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.
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 HttpServlet
s 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.
HTTP verb/URI | Intended CRUD meaning |
POST emps | Create a new employee from the request data |
GET emps | Read a list of all employees |
GET emps?id=27 | Read a singleton list of employee 27 |
PUT emps | Update the employee list with the request data |
DELETE emps | Delete the employee list |
DELETE emps?id=27 | Delete 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.
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.
HTTP status code | Official reason | Meaning |
200 | OK | Request OK. |
400 | Bad request | Request malformed. |
403 | Forbidden | Request refused. |
404 | Not found | Resource not found. |
405 | Method not allowed | Method not supported. |
415 | Unsupported media type | Content type not recognized. |
500 | Internal server error | Request 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 totext/xml
ortext/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
methodinvoke
, with an XML document as theSource
argument. If the request does not require an XML document, then theSource
argument can benull
.The service-side runtime dispatches the client request to the
Provider
methodinvoke
whoseSource
argument corresponds to the client-sideSource
.The service transforms the
Source
into some appropriateResult
(for instance, a DOM tree), processes thisResult
in an application-appropriate way, and returns an XML source to the client. If no response is needed,null
can be returned.The
Dispatch
methodinvoke
returns aSource
, sent from the service, that the client then transforms into an appropriateResult
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.
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.
HttpServlet
s 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).
Get Java Web Services: Up and Running now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.