Saturday, January 3, 2009

Easy Java-Based REST Testing Tools

As I've been putting a paper together for my Rocky Mountain Oracle Users Group (RMOUG) Training Days 2009 presentation REST from Web Services Frustrations, I have been wanting a very simple HTTP client and a very simple HTTP server to use to demonstrate both sides of using a REST-based web service. More specifically, I want to demonstrate the various HTTP methods being invoked from both the client perspective and from the server perspective. In this blog posting, I will look quickly at the two Java-based approaches I am currently planning on using for this presentation.


RESTClient (rest-client)

RESTClient is hosted on Google Code as the rest-client project (not to be confused with the Python-based RESTClient project or even with the Google Code-hosted python-rest-client). RESTClient 2.2 is currently available and there is an article called RESTClient 2.1 - Getting Started that provides a nice introduction to RESTClient. A large set of screen snapshots of RESTClient can be found at http://flickr.com/photos/subwiz/tags/restclient/.

I like RESTClient because it is very simple (I was able to start using it quickly and without reading any instructions) and because it is Java Swing-based. The RESTClient download is an executable JAR that is less than 5 MB in size. Once downloaded, it does not need to be uncompressed, but can instead be directly executed with the command (for RESTClient 2.2):


java -jar restclient-2.2-jar-with-dependencies.jar


When RESTClient is started as shown above, the Swing-based GUI comes up as shown in the next screen snapshot.



This screen snapshot demonstrates how self-explanatory this tool is to use. One simply enters the URL of interest in the obviously named "URL" field and then selects the HTTP method to be used in the radio buttons underneath the again obviously named "HTTP Methods" header. The bottom of this simple GUI has tabs for looking at headers and at the response body and has a large field for displaying response status information. I will demonstrate some of this functionality toward the end of this blog posting.


The Sun-Provided HTTPServer

The RESTClient product described above, as its name suggests, covers my need for a simple REST client. For a simple REST server, I had thought about using a Java servlet with the appropriate methods corresponding to HTTP methods (doGet(), doPost(), doPut(), doDelete(), doHead(), doOptions(), and doTrace()) overridden for my use. However, I was really looking for the simplest solution that covered my needs and realized that the Sun-provided HTTPServer would work well here.

I have previously blogged about the HTTP Server and adapted the example I provided in that blog posting to a slightly more sophisticated (but still extremely basic) tool to tell me, from a server perspective, what a potential HTTP/REST client has requested. While I may tweak this a bit before the actual conference, I include its current state here. The first class shown, DustinHttpHandler, is the class that implements HttpHandler.

DustinHttpHandler.java


package dustin.rest;

import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.util.List;

/**
* Simple HTTP-receiving server based on the Sun-provided HTTP Server. Written
* primarily to be with clients designed for interaction with REST-based
* web services. In other words, this simple HTTP Server implementation can be
* used as a REST-based web services simulator.
*
* @author Dustin
*/
public class DustinHttpHandler implements HttpHandler
{
/**
* Only method prescribed by the HttpHandler interface.
*
* @param httpExchange Encapsulation of HTTP request and response.
* @throws java.io.IOException
*/
public void handle(HttpExchange httpExchange) throws IOException
{
final String requestMethod = httpExchange.getRequestMethod();
final String response = buildResponse(httpExchange);
System.out.println("*** HTTP Request Method: " + requestMethod + "***");
httpExchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, response.length());
final OutputStream os = httpExchange.getResponseBody();
os.write(response.getBytes());
os.close();
}

/**
* Build up the response String.
*
* @param httpExchange HTTP method associated with request.
* @return Response String.
*/
private String buildResponse(final HttpExchange httpExchange)
{
final StringBuffer buffer = new StringBuffer();
buffer.append("<html><head><title>Dustin's Simple Example</title></head>");
buffer.append("<body><table border='1'>");
appendHtmlTableRow(buffer, httpExchange.getRequestMethod(), "HTTP Method");
appendHtmlTableRow(buffer, httpExchange.getProtocol(), "Protocol");
appendApplicationState(buffer, httpExchange, "Application State");
appendRequestHeadersPortion(buffer, httpExchange.getRequestHeaders());
buffer.append("</table></body></html>");
return buffer.toString();
}

/**
* Append the application state (query parameters provided in the URL provided
* by the client) to the provided StringBuffer.
*
* @param buffer StringBuffer to which to append the client-provided
* application state.
* @param httpExchange Provides the handle to the client-provided state.
* @param label Label for the HTML table.
*/
private void appendApplicationState(
final StringBuffer buffer,
final HttpExchange httpExchange,
final String label)
{
appendHtmlTableRow(buffer, httpExchange.getRequestURI().getQuery(), label);
}

/**
* Append provided headers to the provided buffer.
*
* @param buffer Buffer to which headers information will be appended.
* @param headers Headers whose information needs to be appended to buffer.
*/
private void appendRequestHeadersPortion(
final StringBuffer buffer, final Headers headers)
{
appendHtmlTableWithHeaders(buffer, "Request Headers", headers);
}

/**
* Append provided HTTP response headers to provided StringBuffer.
*
* @param buffer Buffer to which to append the response headers information.
* @param headers Response headers to be appended to StringBuffer.
*/
private void appendResponseHeadersPortion(
final StringBuffer buffer, final Headers headers)
{
appendHtmlTableWithHeaders(buffer, "Response Headers", headers);
}

/**
* Append an HTML table row with potentially multiple headers.
*
* @param buffer Buffer to which to append the new HTML table row.
* @param headerLabel The highlighted label for the left column.
* @param headers The headers to be appended to the provided buffer.
*/
private void appendHtmlTableWithHeaders(
final StringBuffer buffer,
final String headerLabel,
final Headers headers)
{
appendSingleHeaderRow(buffer, headerLabel);
for (final Headers.Entry<String,List<String>> header : headers.entrySet())
{
appendHtmlTableRowMultipleValues(buffer, header.getValue(), header.getKey());
}
}

/**
* Build a two-column row for an HTML table with the elementString value
* being placed in the right column and the elementLabel being placed in
* the left column with heading emphasis.
*
* @param bufferToAppend Buffer to which to append HTML table row.
* @param elementString Normal table data element (right column) value.
* @param elementLabel Highlighted table header element (left column) value.
*/
private void appendHtmlTableRow(
final StringBuffer bufferToAppend,
final String elementString,
final String elementLabel)
{
bufferToAppend.append(
"<tr><th>" + elementLabel + "</th><td>" + elementString + "</td></tr>");
}

/**
* Append to buffer with an HTML table row that has potentially multiple
* values for a single value column (multiple values per header column).
*
* @param bufferToAppend Buffer to which to append HTML table row.
* @param elementStrings Multiple values to be appended in single values column.
* @param elementLabel Header label for left column of row.
*/
private void appendHtmlTableRowMultipleValues(
final StringBuffer bufferToAppend,
final List<String> elementStrings,
final String elementLabel)
{
bufferToAppend.append("<tr><th>" + elementLabel + "</th><td>");
for (final String value : elementStrings)
{
bufferToAppend.append(value + "\n");
}
bufferToAppend.append("</td></tr>");
}

/**
* Append a single-cell row for HTML table section headers.
*
* @param bufferToAppend Buffer to which to append the single-cell header row.
* @param value String to place in the single-cell header row; the section
* title.
*/
private void appendSingleHeaderRow(
final StringBuffer bufferToAppend,
final String value)
{
bufferToAppend.append("<tr><th colspan='2'>");
bufferToAppend.append(value);
bufferToAppend.append("</th></tr>");
}
}


The Main class that instantiates and uses the HttpHandler described above is called Main and its code listing is shown next:

Main.java


package dustin.rest;

import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;

/**
* Simple executable to start HttpServer for HTTP request/response interaction.
*
* Run this class with a command line this:
* java -cp RESTfulJava.jar dustin.rest.Main
*
* The URL for connecting to this HTTPServer instance from a client will be
* http://localhost:8000/dustin
*/
public class Main
{
public static final int PORT = 8000;
public static final int BACKLOG = 0; // none
public static final String DEFAULT_URL_CONTEXT = "/dustin";

/**
* Main executable to run Sun's built-in JVM HTTP server.
*
* @param arguments The command line arguments; single argument for URL
* context expected.
*/
public static void main(final String[] arguments) throws IOException
{
final String hostName = InetAddress.getLocalHost().getCanonicalHostName();
final String urlContext = obtainUrlContext(arguments);
final HttpServer server =
HttpServer.create(new InetSocketAddress(PORT), BACKLOG);
server.createContext(urlContext, new DustinHttpHandler());
server.setExecutor(null); // allow default executor to be created
System.out.println("\n\nSimple HTTP Server is running. Use URL\n");
System.out.println(" http://" + hostName + ":" + PORT + urlContext);
System.out.println("\nto access this server.");
server.start();
}

/**
* Extract the URL Context from the command-line or use a default if no
* argument was provided on the command-line.
*
* @param commandLineArguments Command-line arguments.
* @return URL context.
*/
private static String obtainUrlContext(final String[] commandLineArguments)
{
String urlContext = DEFAULT_URL_CONTEXT;
final int numCmdLineArgs = commandLineArguments.length;
if (numCmdLineArgs > 0)
{
final String cmdProvidedContext = commandLineArguments[0];
urlContext = (cmdProvidedContext.startsWith("/"))
? cmdProvidedContext
: "/" + cmdProvidedContext;
}
return urlContext;
}
}


I intentionally have the code print the URL to call to access the HttpServer to make it easier for clients to connect. Note also that a command-line argument can be specified to override the context. This is effectively the resource representation portion in REST parlance.

It is easy to run this application to take advantage of the Sun-provided HttpServer. I built the JAR to be executable as shown in the Main-Class entry in the META-INF/MANIFEST.MF file shown below:

MANIFEST.MF of JAR file

Manifest-Version: 1.0
Ant-Version: Apache Ant 1.7.0
Created-By: 11.0-b15 (Sun Microsystems Inc.)
Main-Class: dustin.rest.Main


With the JAR declared as an executable JAR (RestServer.jar), it is simple to run the application:


java -jar RestServer.jar



Leveraging the RESTClient with the Sun-Provided HTTP Server

In most cases, I plan to use the RESTClient with "real" REST-based web services and use the HTTP Server approach with "real" clients of REST-based web services. In that way, I have easy approaches to testing pieces of the REST interaction. However, it can still be interesting to use the two together and I intend to do so in my presentation to provide some background on HTTP methods and terminology.

When I run the HTTP Server-based RestServer with a command-line argument of "rest/example/name", it looks like this:


java -jar RestServer.jar rest/example/name


When running the above, I see the following expected output (with your own host of course and with a context different than "rest/example/name" if provided as a command-line argument):


Simple HTTP Server is running. Use URL

http://gandalf.marx.net:8000/rest/example/name

to access this server.


This means the simple HTTP Server is running and I can use CTRL-C to break it when I'm finished.

Running the RESTClient is really easy as discussed above:


java -jar restclient-2.2-jar-with-dependencies.jar


I can enter the URL provided by the standard output of the HTTP Server in the URL field of the RESTClient GUI, select an HTTP method with the radio button, and click on the green greater-than signs/arrows pointing right. When using POST method, and changing the tab at the bottom to "Body," the response body is shown as indicated in the following screen snapshot.



If I change the HTTP method to a different method type, the body returned looks the same, but with the different method indicated. The one exception is the HEAD method. As expected, there is no response body shown for the HEAD method because "the HEAD method is identical to GET except that the server MUST NOT return a message-body in the response" (http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4). The HEAD example is shown in the next screen snapshot.



One can also use a web browser to see the returned body (which is conveniently in HTML thanks to my simple implementation of the HttpHander). By default, the browser uses GET as shown in the following screen snapshot. I also have added an id parameter to the URL for this example to indicate how application state can be passed by the client to the server and how the server in the HTTPServer example can provide an indication of the query parameters received.




Conclusion

There are many tools that can be used in REST development and more are coming all the time. Because I often find myself preferring the simplest tool for the job, it is difficult to beat RESTClient and the HTTP Server that comes with the Sun-provided JVM for simple testing and simple demonstration of REST principles and implementations.

4 comments:

Unknown said...

Thanks for featuring WizTools.org RESTClient (I am the owner of the project). Just a FYI, RESTClient has Jetty embedded. It can be started from the menu Tools>Start Trace Server.

@DustinMarx said...

subWiz,

Thanks for the feedback and the information about the embedded Jetty server. Thanks also for a very handy tool.

Dustin

Anonymous said...

You can make the REST calls using Eclipse Http4e. It is an amazing tool for SOAP, REST or simply replaying HTTP calls.

http://http4e.roussev.org/

@DustinMarx said...

With the shutting down of Google Code, rest-client is now hosted on GitHub.