A Perl Client Against a Java RESTful Web Service

The Perl client (see Example 3-1) against the Java predictions2 RESTful service, implemented as an HttpServlet, makes various calls that cover the CRUD operations. (The predictions2 service is from Chapter 2.) Perl comes with most Unixy systems and is available on Windows (see, for example, ActiveState). Although Perl has a quirky syntax—it has been called line-noise interpreted and even worse—its excellent libraries, standard and contributed, more than compensate. In any case, the point of the example is to provide a first look at how a client interacts with a RESTful service. The curl clients from the two previous chapters illustrated all of the CRUD operations against several services; all of the CRUD requests received at least a minimal document in return. Yet the curl utility is meant for making HTTP requests, not processing HTTP responses. The Perl client takes the second step; that is, this client performs all of the CRUD operations and then highlights the challenge of processing response documents. In the case of response documents in XML, the Perl client parses the XML to extract usable information, in this case the id, who, and what of a specified Prediction or of the entire PredictionsList returned from the service.

Example 3-1. A Perl client making CRUD calls against the RESTful predictions2 service

#!/usr/bin/perl -w

# packages
use strict;
use LWP;
use XML::XPath;
use List::MoreUtils qw(each_array);

my $baseUrl = 'http://localhost:8080/predictions2/';
my $ua = LWP::UserAgent->new;
runTests();

## Run CRUD tests against the service.
sub runTests {
    getTest($baseUrl);               ## GET all (xml)
    getTest($baseUrl . '?id=4');     ## GET one (xml)
    getTestJson($baseUrl);           ## GET all (json)
    getTestJson($baseUrl . '?id=4'); ## GET one (json)
    postTest($baseUrl);              ## POST
    getTest($baseUrl);               ## GET all (xml)
    putTest($baseUrl);               ## PUT
    getTest($baseUrl . '?id=4');     ## GET one (xml)
    deleteTest($baseUrl . '?id=31'); ## DELETE
    getTest($baseUrl);               ## GET one (xml)
}
sub getTest {
    my ($url) = @_;
    my $request = HTTP::Request->new(GET => $url);
    my $response = $ua->request($request);
    handleResponse($response, \&parseXml); # pointer to a function
}
sub getTestJson {
    my ($url) = @_;
    my $request = HTTP::Request->new(GET => $url,
                         HTTP::Headers->new('Accept' => 'application/json'));
    my $response = $ua->request($request);
    handleResponse($response, \&parseJson);
}
sub postTest {
    my ($url) = @_;
    my $request = HTTP::Request->new(POST => $url);
    $request->content_type('application/x-www-form-urlencoded');
    $request->content('who=TS Eliot&what=This is the way the world ends.');
    my $response = $ua->request($request);
    handleResponse($response, undef);
}
sub putTest {
    my ($url) = @_;
    my $request = HTTP::Request->new(PUT => $url);
    $request->content_type('application/x-www-form-urlencoded');
    $request->content('id=4#who=FOOBAR');
    my $response = $ua->request($request);
    handleResponse($response, undef);
}
sub deleteTest {
    my ($url) = @_;
    my $request = HTTP::Request->new(DELETE => $url);
    my $response = $ua->request($request);
    handleResponse($response, undef);
}
sub parseXml {
    my ($rawXml) = @_;
    # print "Raw XML response:\n" . $rawXml . "\n";
    # Set up the XPath search.
    my $xp = XML::XPath->new(xml => trim($rawXml));
    # Extract a list apiece of ids, whos, and whats.
    my @ids = $xp->find('//object/void[@property="id"]/int')->get_nodelist;
    my @whos = $xp->find('//object/void[@property="who"]/string')->get_nodelist;
    my @whats = $xp->find('//object/void[@property="what"]/string')->get_nodelist;
    # Iterate over the lists to print the data.
    my $it = each_array(@ids, @whos, @whats);
    while (my ($id, $who, $what) = $it->()) {
        print sprintf("%2d: ", $id->string_value) .
            $who->string_value . " -> '" .
            $what->string_value . "'\n";
    }
}
sub parseJson {
    my ($json) = @_;
    print "JSON document:\n$json\n";
    # ...
}
sub trim {
    my $string = shift;
    $string =~ s/^\s+//;
    $string =~ s/\s+$//;
    return $string;
}
sub handleResponse {
    my ($response, $callback) = @_;
    if ($response->is_success) {
        if (defined $callback) {
            $callback->($response->content);
        }
        else {
            print $response->content . "\n";
        }
    }
    else {
        print STDERR $response->status_line . "\n";
    }
}

The Perl client can make a GET request for all predictions or just a specified one, and the client can express a preference for XML or JSON in either case. Here is the function getTest, which requests all predictions in XML:

sub getTest {
    my ($url) = @_;                                1
    print "\nGET request against $url\n\n";
    my $request = HTTP::Request->new(GET => $url); 2
    my $response = $ua->request($request);         3
    handleResponse($response, \&parseXml);         4
}

In line 1 the $url variable (in Perl, a scalar variable starts with a $) has, as its value, a string URL that targets the predictions2 service, whose GET operation is the doGet method in the HttpServlet. The Perl client generates, in line 2, a request object (the reference is $request) and then issues this request through a LWP::UserAgent instance, with $ua as the reference (line 3). The client then invokes, in line 4, the function named handleResponse with two arguments: the $response reference, which gives access to the entire HTTP response, including the status code, the headers, and the body; and a reference to a response-processing function, in this case parseXml, which parses the returned XML if the request succeeds.

The function parseXml illustrates the challenge of extracting usable information from the XML payload. Perl, like most general-purpose languages, has various ways to parse XML. In this example, the Perl XPath library is used. Here is the parsing function without the comments:

sub parseXml {
  my ($rawXml) = @_;
  print "Raw XML response:\n" . $rawXml . "\n"; # raw xml                     1
  my $xp = XML::XPath->new(xml => trim($rawXml));                             2
  my @ids = $xp->find('//object/void[@property="id"]/int')
      ->get_nodelist;                                                         3
  my @whos = $xp->find('//object/void[@property="who"]/string')
      ->get_nodelist;                                                         4
  my @whats = $xp->find('//object/void[@property="what"]/string')
      ->get_nodelist;                                                         5
  my $it = each_array(@ids, @whos, @whats);
  while (my ($id, $who, $what) = $it->()) {
    print sprintf("%2d: ", $id->string_value) . $who->string_value . " -> '" .
              $what->string_value . "'\n";
  }
}

The function prints the raw XML from the predictions2 service (line 1) and then uses XPath (line 2) to get three lists: @ids, @whos, and @whats (lines 3, 4, and 5). (Perl lists begin with @.) The while loop then prints the text representation of each Prediction, giving the values of its id, who, and what properties. Here is a slice of the output:

 1: Cornelius Tillman ->
    'Managed holistic contingency will grow killer action-items.'
 2: Conner Kulas ->
    'Vision-oriented zero administration timeframe will generate
     backend interfaces.'
...
23: Lottie Marks ->
    'Open-source multitasking timeframe will monetize rich partnerships.'

In a production example, the application logic might be considerably more complicated than simply printing the extracted information. The extracted Prediction instances might be inserted into a database, mined for relevant patterns, integrated with other information, sent out as email greetings, and so on. Nonetheless, the XML parsing would be the first step in support of any such additional processing.

The other requests from the Perl client are very similar to the curl commands used in Chapters 1 and 2. For example, the predictions2 service returns JSON rather than XML on a GET request if the appropriate header element is in the HTTP message. The Perl client inserts the Accept key/value pair, issues the request, and then parses the returned JSON (line 1):

my $request = HTTP::Request->new(GET => $url,
                         HTTP::Headers->new('Accept' => 'application/json')); 1
my $response = $ua->request($request);
handleResponse($response, \&parseJson);

A POST or a PUT request requires that relevant data about a Prediction be inserted into the body of the HTTP request. Here is the POST test:

sub postTest {
    my ($url) = @_;
    my $request = HTTP::Request->new(POST => $url);
    $request->content_type('application/x-www-form-urlencoded');            1
    $request->content('who=TS Eliot&what=This is the way the world ends.'); 2
    my $response = $ua->request($request);                                  3
    handleResponse($response, undef); # undef means no callback function    4
}

Line 1 sets the content type of the HTTP request for x-www-form-urlencoded, the standard type for the body of a POST request; this type is commonly abbreviated as the HTTP form. The form holds two key/value pairs, one for the who and the other for the what property of a Prediction (line 2). Line 3 sends the request and awaits the response. In line 4, the call to handleResponse, the Perl value undef (short for undefined) serves roughly the same purpose as null in Java. In the call to handleResponse, a second argument with undef as its value signals that the HTTP response should be printed rather than processed further; for example, on a successful POST, the printed response would be similar to:

<?xml version="1.0" encoding="UTF-8"?>
<java version="1.7.0" class="java.beans.XMLDecoder">
  <string>Prediction 36 created.</string>
</java>

The Perl example illustrates language neutrality in RESTful web services because the predictions2 service is written in Java. At the same time, this example focuses on a central question in this chapter: How can a RESTful client minimize or even avoid XML/JSON parsing? Even the relatively short Perl client makes clear that the tricky code involves XML parsing, although the Perl XPath library has an API that eases the task. The next section addresses the issue of response-document parsing with two examples, each a Java client against the Amazon E-Commerce service. The first client contends with the XML that the Amazon service returns, whereas the second client uses JAX-B to transform the XML into native Java objects, which are then manipulated with the familiar get/set methods.