Platinum Partner
java,ria,frameworks,junit,rest,client-side,apache pivot

Implementing REST Services with Apache Pivot

Apache Pivot is a platform for building rich Internet applications (RIAs) in Java. While it is geared primarily towards user interface construction, Pivot contains a number of features that make it suitable for non-UI applications as well.

For example, Pivot's Web Query libraries make it easy to write both consumers and producers of REST-based web services. In this article, I'll walk through the implementation of a very basic REST service and a JUnit-based client application for testing the service. The example is based on Pivot 1.5.1, available here.

The REST Service

The following listing contains the source code for RESTDemoServlet, which provides the implementation for the REST service. It doesn't do much - it simply allows a caller to create, read, update, and delete arbitrary JSON objects - but it serves as a reasonably good example of how such services can be implemented in Pivot:

package org.apache.pivot.demos.rest.server;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;

import org.apache.pivot.json.JSONSerializer;
import org.apache.pivot.serialization.SerializationException;
import org.apache.pivot.serialization.Serializer;
import org.apache.pivot.web.Query;
import org.apache.pivot.web.QueryException;
import org.apache.pivot.web.server.QueryServlet;

public class RESTDemoServlet extends QueryServlet {
private static final long serialVersionUID = 0;

@Override
protected Object doGet(Path path) throws QueryException {
if (path.getLength() != 1) {
throw new QueryException(Query.Status.BAD_REQUEST);
}

// Read the value from the temp file
File directory = new File(System.getProperty("java.io.tmpdir"));
File file = new File(directory, path.get(0));
if (!file.exists()) {
throw new QueryException(Query.Status.NOT_FOUND);
}

Object value;
try {
JSONSerializer jsonSerializer = new JSONSerializer();
value = jsonSerializer.readObject(new FileInputStream(file));
} catch (IOException exception) {
throw new QueryException(Query.Status.INTERNAL_SERVER_ERROR);
} catch (SerializationException exception) {
throw new QueryException(Query.Status.INTERNAL_SERVER_ERROR);
}

return value;
}

@Override
protected URL doPost(Path path, Object value) throws QueryException {
if (path.getLength() > 0
|| value == null) {
throw new QueryException(Query.Status.BAD_REQUEST);
}

// Write the value to a temp file
File directory = new File(System.getProperty("java.io.tmpdir"));
File file;
try {
file = File.createTempFile(getClass().getName(), null, directory);

JSONSerializer jsonSerializer = new JSONSerializer();
jsonSerializer.writeObject(value, new FileOutputStream(file));
} catch (IOException exception) {
throw new QueryException(Query.Status.INTERNAL_SERVER_ERROR);
} catch (SerializationException exception) {
throw new QueryException(Query.Status.INTERNAL_SERVER_ERROR);
}

// Return the location of the resource
URL location;
try {
location = new URL(getLocation(), file.getName());
} catch (MalformedURLException exception) {
throw new QueryException(Query.Status.INTERNAL_SERVER_ERROR);
}

return location;
}

@Override
protected boolean doPut(Path path, Object value) throws QueryException {
if (path.getLength() != 1
|| value == null) {
throw new QueryException(Query.Status.BAD_REQUEST);
}

// Write the value to the temp file
File directory = new File(System.getProperty("java.io.tmpdir"));
File file = new File(directory, path.get(0));
if (!file.exists()) {
throw new QueryException(Query.Status.NOT_FOUND);
}

try {
JSONSerializer jsonSerializer = new JSONSerializer();
jsonSerializer.writeObject(value, new FileOutputStream(file));
} catch (IOException exception) {
throw new QueryException(Query.Status.INTERNAL_SERVER_ERROR);
} catch (SerializationException exception) {
throw new QueryException(Query.Status.INTERNAL_SERVER_ERROR);
}

return false;
}

@Override
protected void doDelete(Path path) throws QueryException {
if (path.getLength() != 1) {
throw new QueryException(Query.Status.BAD_REQUEST);
}

// Delete the file
File directory = new File(System.getProperty("java.io.tmpdir"));
File file = new File(directory, path.get(0));
if (!file.exists()) {
throw new QueryException(Query.Status.NOT_FOUND);
}

file.delete();
}

@Override
protected Serializer<?> createSerializer(Path path) throws QueryException {
return new JSONSerializer();
}
}

RESTDemoServlet extends org.apache.pivot.web.server.QueryServlet, an abstract base class provided by the Pivot platform to help facilitate implementation of REST services. QueryServlet itself extends javax.servlet.http.HttpServlet and provides overloaded versions of the base HTTP handler methods that make them easier to work with in a REST-oriented manner:

  • Object doGet(Path path)
  • URL doPost(Path path, Object value)
  • boolean doPut(Path path, Object value)
  • void doDelete(Path path)

Each method takes an instance of QueryServlet.Path that represents the path to the resource being accessed, relative to the location of the servlet itself. Path is a sequence type that allows a caller to access the components of the path via numeric index. So if, for example, the servlet is mapped to the "/my_service/*" URL pattern, given the following URL:

http://ixnay.biz/my_service/foo/bar

the path argument would contain the values "foo" and "bar", accessible via indices 0 and 1, respectively.

Serializers

Unlike the base HttpServlet class, QueryServlet operates on arbitrary Java types rather than HTTP request and response objects. This allows developers to focus on the resources managed by the service rather than the lower-level details of the HTTP protocol.

QueryServlet uses a "serializer", an implementation of the org.apache.pivot.serialization.Serializer interface, to determine how to serialize the data sent to and returned from the servlet. The serializer is responsible for converting the input stream into an object value and vice versa. The interface is defined as follows:

 

public interface Serializer<T> {
public T readObject(InputStream inputStream)
throws IOException, SerializationException;
public void writeObject(T object, OutputStream outputStream)
throws IOException, SerializationException;

public String getMIMEType(T object);

The first two methods are responsible for reading the data and writing the data, respectively: readObject() deserializes an object from an input stream and returns it, and writeObject() serializes a given object to an output stream. The third method, getMIMEType(), returns the MIME type supported by the serializer for a given value.

The serializer used for a given HTTP request is determined by the return value of the abstract createSerializer() method. This method is called by QueryServlet prior to invoking the actual HTTP handler method. The example servlet uses an instance of org.apache.pivot.json.JSONSerializer, which supports reading and writing of JSON data. Pivot provides a number of additional serializers supporting XML, CSV, and Java serialization, among others, and service implementations are free to define their own custom serializers as well.

Exceptions

Each handler method declares that it may throw an instance of org.apache.pivot.web.QueryException. This exception encapsulates an HTTP error response. It takes an integer value representing the response code as a constructor argument (the org.apache.pivot.web.Query.Status class defines a number of constants for status codes commonly used in REST responses). The web query client API, discussed in the next section, effectively re-throws these exceptions, allowing the client to handle an error response returned by the server as if the exception was generated locally.

Query String Parameters and HTTP Headers

Though it is not shown in this example, query servlet implementations can also access the query string arguments and HTTP headers included in the HTTP request, as well as control the headers sent back with the response. Query string parameters are accessible via the getParameters() method of QueryServlet, and the request/response headers can be accessed via getRequestHeaders() and getResponseHeaders(), respectively. All three methods return an instance of org.apache.pivot.web.QueryDictionary, which allows the caller to manipulate these collections via get(), put(), and remove() methods.

doGet()

doGet() is used to handle an HTTP GET request. It returns an object representing the resource at a given path. The doGet() method in the example servlet is defined as follows:

protected Object doGet(Path path) throws QueryException {
if (path.getLength() != 1) {
throw new QueryException(Query.Status.BAD_REQUEST);
}

// Read the value from the temp file
File directory = new File(System.getProperty("java.io.tmpdir"));
File file = new File(directory, path.get(0));
if (!file.exists()) {
throw new QueryException(Query.Status.NOT_FOUND);
}

Object value;
try {
JSONSerializer jsonSerializer = new JSONSerializer();
value = jsonSerializer.readObject(new FileInputStream(file));
} catch (IOException exception) {
throw new QueryException(Query.Status.INTERNAL_SERVER_ERROR);
} catch (SerializationException exception) {
throw new QueryException(Query.Status.INTERNAL_SERVER_ERROR);
}

return value;
}

This method simply loads a file from the system's temp directory that has presumably been created by a prior POST request (doPost() is discussed in the next section). The path contains a single value representing the name of the file to retrieve. If the file does not exist, an exception representing an HTTP 404 ("Not Found") error is thrown. Otherwise, the data stored in the file is deserialized by an instance of JSONSerializer and returned. The serializer returned by createSerializer() (in this case, another instance of JSONSerializer) is used by QueryServlet to write the return value to the servlet's output stream. The MIME type returned by the serializer is used as the value of the "Content-Type" response header (which, in this case, would be "application/json").

Obviously, this is a somewhat contrived example - it isn't terribly efficient to deserialize a file only to re-serialize it in order to send it back to the client. In a real application, the data returned by doGet() would most likely be obtained by querying one or more back-end data sources such as a relational or XML database or legacy application.

doPost()

doPost() is used to handle an HTTP POST request. It is primarily used to create a new resource on the server, but can also be used to execute arbitrary server-side actions.

When a resource is created, doPost() returns a URL representing the location of the new resource. Consistent with the HTTP specification, this value is returned in the "Location" response header along with an HTTP status code of 201 ("Created"). If a POST request does not result in the creation of a resource, doPost() can return null, which is translated by QueryServlet to an HTTP response of 204 ("No Content") and no corresponding "Location" header.

The doPost() method in the example looks like this. It creates a new JSON file in a temp directory on the server:

protected URL doPost(Path path, Object value) throws QueryException {
if (path.getLength() > 0
|| value == null) {
throw new QueryException(Query.Status.BAD_REQUEST);
}

// Write the value to a temp file
File directory = new File(System.getProperty("java.io.tmpdir"));
File file;
try {
file = File.createTempFile(getClass().getName(), null, directory);

JSONSerializer jsonSerializer = new JSONSerializer();
jsonSerializer.writeObject(value, new FileOutputStream(file));
} catch (IOException exception) {
throw new QueryException(Query.Status.INTERNAL_SERVER_ERROR);
} catch (SerializationException exception) {
throw new QueryException(Query.Status.INTERNAL_SERVER_ERROR);
}

// Return the location of the resource
URL location;
try {
location = new URL(getLocation(), file.getName());
} catch (MalformedURLException exception) {
throw new QueryException(Query.Status.INTERNAL_SERVER_ERROR);
}

return location;
}

The first thing the method does is ensure that the request is valid. If the caller has specified any path components or has not provided a value in the body of the request, HTTP 400 ("Bad Request") is returned. Otherwise, the value passed to the method is stored in a temp file (again, using JSONSerializer), and the location of the new resource is returned. The location value is generated simply by appending the name of the temp file to the location of the servlet, obtained by a call to QueryServlet#getLocation().

doPut()

doPut() handles an HTTP PUT request. It is often used to update an existing resource, but can also be used to create a new resource. The return value of doPut() is a boolean flag indicating whether or not a resource was created. If true, HTTP 201 is returned to the caller; otherwise, HTTP 204 is returned.

RESTDemoServlet's implementation of doPut() looks like this. It allows a caller to update an existing JSON resource on the server:

protected boolean doPut(Path path, Object value) throws QueryException {
if (path.getLength() != 1
|| value == null) {
throw new QueryException(Query.Status.BAD_REQUEST);
}

// Write the value to the temp file
File directory = new File(System.getProperty("java.io.tmpdir"));
File file = new File(directory, path.get(0));
if (!file.exists()) {
throw new QueryException(Query.Status.NOT_FOUND);
}

try {
JSONSerializer jsonSerializer = new JSONSerializer();
jsonSerializer.writeObject(value, new FileOutputStream(file));
} catch (IOException exception) {
throw new QueryException(Query.Status.INTERNAL_SERVER_ERROR);
} catch (SerializationException exception) {
throw new QueryException(Query.Status.INTERNAL_SERVER_ERROR);
}

return false;
}

Like doPost(), it first validates the format of the request. Next, it verifies that the resource actually exists; if not, HTTP 404 is returned. Finally, it uses an instance of JSONSerializer to write the updated data to the temp file.

doDelete()

doDelete() handles an HTTP DELETE request. When successful, it simply deletes the resource specified by the path and returns HTTP 204. The source code for this method is shown below:

protected void doDelete(Path path) throws QueryException {
if (path.getLength() != 1) {
throw new QueryException(Query.Status.BAD_REQUEST);
}

// Delete the file
File directory = new File(System.getProperty("java.io.tmpdir"));
File file = new File(directory, path.get(0));
if (!file.exists()) {
throw new QueryException(Query.Status.NOT_FOUND);
}

file.delete();
}

Like the other methods, the request is first validated; then, if the file exists, it is deleted.

The JUnit Test Client

The following is the source code listing for the JUnit client application used to test the service:

package org.apache.pivot.demos.rest;

import java.io.IOException;
import java.net.URL;

import org.apache.pivot.json.JSON;
import org.apache.pivot.json.JSONSerializer;
import org.apache.pivot.serialization.SerializationException;
import org.apache.pivot.web.DeleteQuery;
import org.apache.pivot.web.GetQuery;
import org.apache.pivot.web.PostQuery;
import org.apache.pivot.web.PutQuery;
import org.apache.pivot.web.Query;
import org.apache.pivot.web.QueryException;
import org.junit.BeforeClass;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;

public class RESTDemoTest {
private static String hostname = null;
private static int port = -1;
private static boolean secure = false;

@BeforeClass
public static void oneTimeSetUp() {
hostname = System.getProperty("org.apache.pivot.demos.rest.hostname", "localhost");
port = Integer.parseInt(System.getProperty("org.apache.pivot.demos.rest.port", "-1"));
secure = Boolean.parseBoolean(System.getProperty("org.apache.pivot.demos.rest.secure", "false"));
}

@Test
public void testCRUD() throws IOException, SerializationException, QueryException {
JSONSerializer jsonSerializer = new JSONSerializer();
Object contact = jsonSerializer.readObject(getClass().getResourceAsStream("contact.json"));

// Create
PostQuery postQuery = new PostQuery(hostname, port, "/pivot-demos/rest_demo", secure);
postQuery.setValue(contact);
URL location = postQuery.execute();

assertNotNull(location);

String path = location.getPath();

// Read
GetQuery getQuery = new GetQuery(hostname, port, path, secure);

Object result = getQuery.execute();
assertEquals(JSON.get(contact, "address.street"), JSON.get(result, "address.street"));
assertEquals(contact, result);

// Update
JSON.put(contact, "name", "Joseph User");
PutQuery putQuery = new PutQuery(hostname, port, path, secure);
putQuery.setValue(contact);
boolean created = putQuery.execute();

assertFalse(created);
assertEquals(contact, getQuery.execute());

// Delete
DeleteQuery deleteQuery = new DeleteQuery(hostname, port, path, secure);
deleteQuery.execute();

assertEquals(deleteQuery.getStatus(), Query.Status.NO_CONTENT);
}

@Test(expected=QueryException.class)
public void testException() throws IOException, SerializationException, QueryException {
GetQuery getQuery = new GetQuery(hostname, port, "/pivot-demos/rest_demo/foo", secure);
getQuery.execute();
}
}

The code defines two test cases: testCRUD() and testException(). The oneTimeSetUp() method is used to obtain information about the server (host name, port, and protocol) before any of the test cases are executed.

testCRUD()

The testCRUD() method executes a basic "create/read/update/delete" test. It first reads a JSON structure from a "contact.json" file that is initially used to create the resource, and later used to compare the server's responses to the original source data. The contents of this file are as follows:

{   id: 101,
name: "Joe User",

address: {
street: "123 Main St.",
city: "Cambridge",
state: "MA",
zip: "02142"
},

phoneNumber: "(617) 555-1234",
emailAddress: "joe_user@foo.com",

imAccount: {
id: "juser1234",
type: "AIM"
}
}
Create

Next, the test creates an instance of org.apache.pivot.web.PostQuery. PostQuery is a subclass of org.apache.pivot.web.Query, the abstract base class for all of Pivot's web client classes, which also include GetQuery, PutQuery, and DeleteQuery. It creates the query using the host name, port, and secure flag that were passed to the test at startup. It also passes the value "/pivot-demos/rest_demo" to the query, since this is the path to which the demo servlet is mapped in the server's "web.xml" file:

PostQuery postQuery = new PostQuery(hostname, port, "/pivot-demos/rest_demo", secure);

Finally, the test sets the query's value to the contact object and executes the query:

postQuery.setValue(contact);
URL location = postQuery.execute();

assertNotNull(location);

String path = location.getPath();

Like QueryServlet, Query uses a serializer to send data to and from the server, which, by default is an instance of JSONSerializer. Ultimately, the doPost() method described in the previous section is executed, and the resource is created on the server. The location of the new resource is passed to the caller as the return value of the execute() method. The test asserts that this value is not null and then retains it for later use by subsequent requests.

Read

Next, the test executes a GET query to retrieve the resource at the URL returned by the server in response to the initial POST:

GetQuery getQuery = new GetQuery(hostname, port, path, secure);

Object result = getQuery.execute();
assertEquals(JSON.get(contact, "address.street"), JSON.get(result, "address.street"));
assertEquals(contact, result);

The test performs two equality checks on the return value. The first uses the get() method of the org.apache.pivot.json.JSON utility class to retrieve the values of the "address.street" field in both the original and the result data and compares them using assertEquals(). The second performs an equality comparison on the entire source and response. The latter approach is obviously useful for bulk comparisons, especially when testing creation of new resources, and the former is handy for more granular comparisons, for example to validate an update of a single field or property via PUT, which is discussed next.

Update

testCRUD() next tests the HTTP PUT method. The test case modifies the "name" field of the source object and PUTs the updated value to the server using an instance of PutQuery. It asserts that the return value is false (i.e. that the query did not result in the creation of a new resource), and then compares the locally modified structure to the value returned by another GET to the server:

JSON.put(contact, "name", "Joseph User");
PutQuery putQuery = new PutQuery(hostname, port, path, secure);
putQuery.setValue(contact);
boolean created = putQuery.execute();

assertFalse(created);
assertEquals(contact, getQuery.execute());
Delete

Finally, the resource created by the initial POST request is DELETEd. The test case verifies the success of the query by validating that the returned response code is HTTP 204:

DeleteQuery deleteQuery = new DeleteQuery(hostname, port, path, secure);
deleteQuery.execute();

assertEquals(deleteQuery.getStatus(), Query.Status.NO_CONTENT);
Asynchronous Queries

The execute() method of the Query class is actually defined by Query's base class, the abstract org.apache.pivot.util.concurrent.Task class. This class is used to help simplify the task of executing background operations in a Pivot application. Task defines two execute() overloads: one is abstract and takes no arguments. It is executed synchronously and is responsible for actually performing the task. The other takes an argument of type org.apache.pivot.util.concurrent.TaskListener. This version of the method executes asynchronously - it calls the no-arg version of execute() on a background thread, and the caller is notified via the listener interface when the task is complete (or fails).

For a unit test, the synchrnous version of the method is appropriate. However, in a GUI application, the UI would appear to hang if the task was executed synchronously. As a result, Pivot GUI applications typically use the asynchronous version of execute() so that the UI remains responsive while the task is being executed in the background.

textException()

The second test case is a simple test to verify that an exception is thrown as expected. The test case declares that it expects an instance of QueryException to be thrown and executes a GET query for a non-existent resource named "foo":

@Test(expected=QueryException.class)
public void testException() throws IOException, SerializationException, QueryException {
GetQuery getQuery = new GetQuery(hostname, port, "/pivot-demos/rest_demo/foo", secure);
getQuery.execute();
}

When the test is executed, the exception is thrown, and the test passes.

Unfortunately, it is not currently possible to write a test case that expects a query exception with a specific response code; perhaps a future update to JUnit or Pivot will add support for such a feature.

Additional Examples

Though this application was obviously written specifically to demonstrate interaction with the sample REST service described in the previous section, there is nothing to prevent an application from using web queries to interact with other web services, including those written using other back-end frameworks. For example, the Pivot Stock Tracker sample application uses web queries to retrieve CSV-formatted stock quote data from Yahoo! Finance, and the Pivot web query tutorial application uses a web query to retrieve JSON data from a Yahoo Pipes service.

Additionally, though not demonstrated in this example, web query clients can also access and manipulate other aspects of the query including query string parameters, request headers, and response headers. The methods for doing so are identical to the corresponding methods described above for QueryServlet.

Summary

Pivot's Web Query libraries provide a simple and intuitive means for both producing and consuming REST-based web services. They can be used standalone, as demonstrated in this example, to implement or unit test a REST service, or they can be used to easily integrate server-side data into a Pivot GUI application.

For more information on Pivot and web queries, visit the Pivot web site.

{{ tag }}, {{tag}},

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}
{{ parent.authors[0].realName || parent.author}}

{{ parent.authors[0].tagline || parent.tagline }}

{{ parent.views }} ViewsClicks
Tweet

{{parent.nComments}}