REST is an architectural style for implementing web services over
standard HTTP. You can learn more about REST from the following
resources:
Building Web Services the REST Way
Introduction to REST Slide Deck
REST Tutorial
How to Create a REST Protocol
RESTful Web Services book
Second Generation Web Services
REST and the Real World
REST for the Rest of Us
Java API for RESTful Web Services
The JAX-RS (Java API for RESTful Web Services) specification
presented in JSR 311 defines a standard way to deploy RESTful web
services using annotated POJOs (plain old Java objects). For detailed
information, you can read the JSR here.
In this article, we'll use JAX-RS to create a simple sample application that allows
you to add, retrieve, and delete books from a virtual library.
Design the REST API
The first thing to do when creating a new RESTful web service is to
define an API that exposes service functionality to the web service
client. REST is often called a Resource Oriented Architecture (ROA)
since it is based on resources that are uniquely addressable via URIs
(Uniform Resource Identifier). A REST API exposes the ability to
perform a small set of operations on these resources. Essentially,
there are four important elements within a REST service call that must
be defined by the service author. These elements are as follows:
- HTTP method
- URI
- Request body
- Response
Let's take a closer look at each of these elements.
HTTP Method
The HTTP method indicates the type of action that should be taken on
the resource specified by the URI. RESTful services typically use four
methods that roughly equate the standard CRUD (create, read, update,
delete) operations as follows:
| HTTP Method
|
CRUD Operation
|
| GET
|
read
|
| POST
|
create a new server-identified resource (occasionally used for update)
|
| PUT
|
update an existing resource or create a new client-identified resource
|
| DELETE
|
delete
|
The GET method is used for calls that have no side effects on the
server (any calls that have no side effects should always use GET). An
example of this would be when retrieving information about a book. No
transaction is taking place so the book can be retrieved any number of
times without altering state on the server. A typical GET method call
looks like this:
GET http://{server}/MyRestService/library/books/12345
Notice that the PUT and POST operations can both be used to create
and update resources. This has been a cause of much confusion when
creating REST services. Here is a hint from the book RESTful Web Services to help you decide when to use each method:
- "The difference between PUT and POST is this: the client uses
PUT when it’s in charge of deciding which URI the new resource should
have. The client uses POST when the server is in charge of deciding
which URI the new resource should have."
Another way of stating this rule is to say that PUT should be used
when creating a new URI and POST when calling an existing URI. So, you
use PUT to create (as well as update) resources when the client
controls the URI that references the resource. For example, the client
can create a new book that is referenced by an ISBN number assigned by
the client in this manner:
PUT http://{server}/MyRestService/library/books/12345
Notice that the client passed the ID (12345) by which this book will
be referenced. This call creates a new book resource that can be
accessed as shown in the GET method above. In this case, the PUT
operation is appropriate because the client is responsible for
specifying the URI that uniquely identifies the book. On the other
hand, consider a service call where the client doesn't specify the URI:
POST http://{server}/MyRestService/library/books/12345/reviews
This call creates a review for the specified book. In a case like
this, the server will return the URI by which the review can be
referenced in the Location header of the HTTP response.
This URI allows the client to retrieve the review using a URI created
by the server. The GET call would look something like this:
GET http://{server}/MyRestService/library/books/12345/reviews/2937846292
In this case, the new review resource is known as a subordinate resource.
A subordinate resource is a resource that only exists in relation to
some parent resource. In other words, a review cannot exist on its own
without being attached to a book. POST is usually used for creating
subordinate resources. PUT and POST can both be used for updating resources but PUT replaces the entire resource while POST may update only a portion of it. For example, if the PUT request shown above is called a second time with a different book representation, the entire book with ID 12345 will be replaced with the new representation. In order to update just a portion of the resource using PUT, you would need to create a new subordinate resource. For example, if the book had a "checkedOut" property, we could use PUT to set this property by defining a new resource at this URL:
PUT http://{server}/MyRestService/library/books/12345/checkedOut
This new resource would allow you to update just one property of book 12345 using PUT (since it is replacing the entire checkedOut resource). POST could also be used to modify book 12345 without the new resource (often called an "overloaded POST") but that approach isn't as true to REST principles.
Note that PUT is idempotent while POST is not.
Idempotent means that the operation can be safely performed multiple
times. Consider how performing the same PUT operation multiple times
does not change the server state (since subsequent PUT requests would
simply replace the original resource with an identical representation).
POST, on the other hand, is not idempotent because it creates a new
resource with each call. Performing multiple identical POST operations
causes the server state to change with every execution (as a new
resource is created each time).
These are the key differences between the PUT and POST methods.
Hopefully these general rules will help you when choosing which method
to use in your REST service.
Finally, the DELETE method is used to delete resources. Like PUT, DELETE is idempotent and it operates on an entire resource. That is, the whole resource is must be deleted rather than just a part of it. A typical DELETE method call looks like this:
DELETE http://{server}/MyRestService/library/books/12345
After deleting a resource, it is no longer available. In this case,
issuing a GET request to this resource would return an HTTP status code
of 404 (Not Found).
URI
The URIs by which resources are referenced are an important part of
the REST API. One of the most fundamental REST principles is that URIs
should represent nouns, not verbs. There are two good reasons for this.
First, the actions that can be performed have already been defined by
the HTTP protocol (GET, POST, PUT, DELETE). We should not be defining
new actions through naming resources using verbs. Second, since REST
requires us to specify an action based on the standard HTTP methods
with each call, it only makes sense that this action would be performed
against a concrete resource (noun) as opposed to another action (verb).
For example, consider the following URI:
GET http://{server}/MyRestService/library/getBooks
In essence, this URI is saying "get the getBooks resource". This is
redundant and a bit confusing. A better way to express this idea is
through a URI like this:
GET http://{server}/MyRestService/library/books
This URI just says "get the books resource". The representation of
the books resource would naturally be a list of books stored in the
library.
Another common guideline is to use plural nouns for collections
of items and perform actions against this collection. For example,
consider the following service calls:
| Service Call
|
Description
|
GET http://{server}/MyRestService/library/books
|
Get a list of books
|
PUT http://{server}/MyRestService/library/books/12345
|
Create a new book with ISBN 12345
|
GET http://{server}/MyRestService/library/books/12345
|
Get a single book with ISBN 12345
|
DELETE http://{server}/MyRestService/library/books/12345
|
Delete a single book with ISBN 12345
|
Notice how the last GET operation and the DELETE operation
referenced an individual book through the books collection. This
conveys the idea that we are retrieving a single book from the
collection of all books.
The URIs for REST services will typically grow in a
hierarchical fashion. For example, imagine that our library service
allowed patrons to attach a review to each book. To accommodate this
use case, the URI for this service should first reference the book to
which the review applies and then the review itself in this manner:
GET http://{server}/MyRestService/library/books/12345/reviews/1
To get a list of all reviews pertaining to the book identified by ISBN 12345, we could use a URI like this:
GET http://{server}/MyRestService/library/books/12345/reviews
Request Body
Most REST service calls do not contain any information in the body
of the request. This is due to the fact that the HTTP header often
includes all the information needed to invoke a service. The HTTP
header includes the HTTP method, URI, and all HTTP header fields. In
fact, certain HTTP methods, such as GET and DELETE, do not support any
data in the body of the request at all. However, HTTP methods that
create or modify resources, such as POST and PUT, should include the
information necessary to perform that action. For instance, a PUT
operation by definition should always include a full representation of
the resource being created or replaced. On the other hand, a POST
operation may include just enough information required by the service
to create or update a resource.
The important thing to know about the request body is that it
is unique to each service. The service designer must define the format
of the request body and convey that to service consumers. Information
in the request body is typically encoded in XML or JSON format. Here is
a typical HTTP request that contains XML information within the body:
POST http://www.sourcestream.com/books HTTP/1.1
Content-Type: application/xml
<Book>
<Title>Inside Servlets</Title>
<URI>http://http://www.amazon.com/Inside-Servlets-Server-Side-Programming-Platform/dp/0201709066/ref=sr_1_1?ie=UTF8&qid=1321917641&sr=8-1</URI>
</Book>
The sample HTTP request above indicates that the client is creating
a new book by performing a POST to the books resource. The
Content-Type header indicates that the request body is formatted in
XML.
Response
The response to a REST service call contains various information of
interest to the client. To begin, a REST response always includes a
status code that conveys the result of the requested operation. The
available status codes are defined by the HTTP specification and are
grouped into 5 ranges that convey a general meaning. These five ranges
are as follows:
| Range
|
Description
|
| 1xx (Meta)
|
Used only in negotiations with HTTP server.
|
| 2xx (Successful)
|
The operation was successful.
|
| 3xx (Redirection)
|
The client must perform an additional operation to get what it wants.
|
| 4xx (Client-Side Error)
|
There is a problem with the client's request.
|
| 5xx (Server-Side Error)
|
An error occurred on the server that prevented it from servicing the request.
|
Here are some of the most common response status codes:
| Status Code
|
Description
|
| 200 (OK)
|
The call was serviced successfully.
|
| 201 (Created)
|
A resource was created at the location specified in the Location header.
|
| 202 (Accepted)
|
Request was accepted and is being processed. Typically returned in response to long running processes.
|
| 204 (No Content)
|
The call was serviced successfully but there is no content to return.
|
| 206 (Partial Content)
|
Only part of the requested content can be returned at this time.
|
| 301 (Moved Permanently)
|
The requested resource has permanently moved to the URI as specified in the Location header.
|
| 304 (Not Modified)
|
Client included an If-Modified-Since header in the request but resource hasn't changed since that time.
|
| 307 (Temporary Redirect)
|
The requested resource has temporarily moved to the URI specified in the Location header.
|
| 400 (Bad Request)
|
Client error indicating that the request was not properly formatted.
|
| 401 (Unauthorized)
|
The client attempted to operate on the requested resource without providing the required credentials.
|
| 404 (Not Found)
|
The requested resource does not exist.
|
| 409 (Conflict)
|
The request would have put the server into an invalid state.
|
| 500 (Internal Server Error)
|
An error occurred on the server when attempting to satisfy the request.
|
| 501 (Not Implemented)
|
The specified HTTP method is not supported for the requested resource.
|
In addition to the status code, the response may contain a
Content-Type header. This header indicates the type of content
contained in the body of the response (e.g., image, XML, JSON, etc.).
To illustrate, here is a typical HTTP response that uses the
Content-Type header to indicate that the body of the response is in XML
format:
HTTP/1.1 200 OK
Date: Thu, 5 Jun 2008 06:25:24 GMT
Content-Type: application/xml
<Books>
<Book>
<Title>Inside Servlets</Title>
<URI>http://http://www.amazon.com/Inside-Servlets-Server-Side-Programming-Platform/dp/0201709066/ref=sr_1_1?ie=UTF8&qid=1321917641&sr=8-1</URI>
</Book>
</Books>
In response to a GET request, a representation of the requested
resource is returned to the client. Similar to the request body, the
format of this representation is service-specific. The service consumer
must refer to documentation provided by the service provider in order
to determine how the body of the response is to be interpreted.
Some REST service calls may not return anything within the body
of the message. For example, in response to a POST request to create a
new resource, the service will typically just respond with a message
indicating that the resource was requested and the location of the new
resource as illustrated here:
HTTP/1.1 201 Created
Date: Thu, 5 Jun 2008 06:25:24 GMT
Location: http://www.sourcestream.com/books/12345
The response above indicates that a new book resource was
successfully created and is available at the URI indicated by the
Location header.
Document the REST API
Now that you understand the different parts of a REST service's
request and response, you're ready to define an API that will be
exposed to service consumers. It is recommended to begin by defining
the different parts (nouns) in the system and then determine which of
the HTTP actions need to be performed on each. For instance, in the
library example presented earlier, there are two objects that can be
acted on: books and reviews. We can consider these two objects as the
resources upon which REST service clients will be acting. The
operations that we implement will be determined by the use cases that
the service must support. For instance, the user should be able to
create, read, update, and delete both books and reviews. This means
that we'll implement support for GET, PUT or POST, and DELETE for each
resource. Here is a list of operations that the library service will
support:
| Operation
|
HTTP Method
|
URI
|
Request Body
|
Response Body
|
HTTP Status Codes
|
| Get a book
|
GET
|
/books/{ISBN}
|
None
|
XML representation of a book
|
200 (OK), 404 (Not Found)
|
| Get list of all books
|
GET
|
/books
|
None
|
XML representation of all books
|
200 (OK), 404 (Not Found)
|
| Create a book
|
PUT
|
/books/{ISBN}
|
XML representation of a book
|
None
|
201 (Created), 400 (Bad Request)
|
| Replace a book
|
PUT
|
/books/{ISBN}
|
XML representation of a book
|
None
|
200 (OK), 400 (Bad Request)
|
| Delete a book
|
DELETE
|
/books/{ISBN}
|
None
|
None
|
200 (OK), 404 (Not Found)
|
| Get a book review
|
GET
|
/books/{ISBN}/reviews/{id}
|
None
|
XML representation of a book review
|
200 (OK), 404 (Not Found)
|
| Get all reviews for a book
|
GET
|
/books/{ISBN}/reviews
|
None
|
XML representation of all reviews for a book
|
200 (OK), 404 (Not Found)
|
| Create a book review
|
POST
|
/books/{ISBN}/reviews
|
XML representation of a book review
|
None
|
201 (Created), 400 (Bad Request)
|
| Update a book review
|
PUT
|
/books/{ISBN}/reviews/{id}
|
XML representation of a book review
|
None
|
200 (OK), 404 (Not Found)
|
| Delete a book review
|
DELETE
|
/books/{ISBN}/reviews/{id}
|
None
|
None
|
200 (OK), 404 (Not Found)
|
Another common way to document a REST API is to use a table showing
the available REST actions across the top and the resources upon which
actions are performed on the left like this:
|
GET
|
POST
|
PUT
|
DELETE
|
| Books
|
/books
/books/{ISBN}
|
|
/books/{ISBN}
|
/books/{ISBN}
|
| Reviews
|
/books/{ISBN}/reviews
/books/{ISBN}/reviews/{id}
|
/books/{ISBN}/reviews
|
/books/{ISBN}/reviews/{id}
|
/books/{ISBN}/reviews/{id}
|
Create the REST Service
We will now create a REST service that allows the user to create,
retrieve, update, and delete books from a library. Adding support for
book reviews as shown in the REST API is an exercise that is left to
the reader.
JAX-RS makes creating a RESTful web services very simple. Start
by annotating a class with path information that maps a request's URI
to the class that should process the request. For example, consider the
following class:
@Path("/library")
public class Library
{
//implementation here...
}
The @Path annotation indicates that requests beginning with /library
should be routed to this class for processing. For instance, any of the
following requests would be passed to the Library class:
GET http://{server}/MyRestService/library/books
GET http://{server}/MyRestService/library/books/1234
POST http://{server}/MyRestService/library/books/1234
DELETE http://{server}/MyRestService/library/books/1234
Note that the path information begins immediately following the web
application's context name. In this case, the application's context
name is MyRestService.
Create the GET Methods
After creating an annotated class, we are ready to create methods to
return information to the web service client. We'll start by defining
two methods that return information about the books available to the
library service.
@GET
@Path("/books")
@ProduceMime("application/xml")
public String getBooks()
{
//implementation here...
}
@GET
@Path("/books/{isbn}")
@ProduceMime("application/xml")
public String getBook(@PathParam("isbn")String id)
{
//implementation here...
}
Now let's examine these two methods in detail. The first method,
getBooks(), indicates that it should be called in response to an HTTP
GET (note the @GET annotation) to the /library/books resource. Why /library/books and not just "/books" as stated in the @Path
annotation? Because the path information specified by each individual
method is appended to the path information supplied by its class. In
this case, the Library class specified a path of /library and the getBooks() indicated /books. Lastly, the @ProduceMime
annotation tells the JAX-RS container that this method returns XML and,
therefore, the Content-Type header in the response should be set to application/xml.
The next method, getBook(), is a little more complex. From the
annotations on this method we can see that it is called to process HTTP
GET requests to the /library/books/{isbn} URI. This URI matches any URI that includes a value after the /library/books/ path. The @Path annotation names this value "isbn". Once named, the value can be referenced from other annotations. In this case, the @PathParam("isbn") parameter annotation tells the server to parse the last value out of the URI and assign it to the method parameter id. This kind of automatic assignment is a type of inversion of control (IOC) known as dependency injection.
Rather than the code having to retrieve the isbn value from the URI,
the container "injects" the value into the code via its method
parameter.
In addition to the path portion of the URI, you can also inject values from the query string into a method parameter like this:
@GET
@Path("/users")
public String getUser(@QueryParam("id")String userId)
{
//implementation here...
}
Therefore, given a URI like /users?id=1234, the getUser() method's userId
parameter would be assigned a value of 1234. That's all for the GET
methods. For more information, you can examine their full
implementation at Library REST Sample or in the sample project file available below.
Create the PUT Method
The next method we'll create will allow the client to add or update
a book. In this case, the same method supports both add and update
because it processes PUT requests. Remember that PUT requests either
create or replace a resource. Therefore, the method implementation is
often very similar, if not identical. This is due to the fact that
either operation may simply require the implementation to create a
resource and add it to a collection. If the resource already existed,
it is overwritten. If not, the new resource is added. Here's the
definition for the create/update method:
@PUT
@Path("/books/{isbn}")
@ConsumeMime("application/xml")
public Response addUpdateBook(@PathParam("isbn")String isbn, String body)
{
//implementation here...
}
This method is very similar to getBooks() except that it processes PUT requests rather than GET. Note that the value following /books/ in the URI is named "isbn" and this value is injected into the method parameter of the same name. The @ConsumeMime
annotation indicates that this method expects to be passed some content
in XML format. So, how do we get the content out of the HTTP request?
The way to do this is to declare a single String parameter that is not
annotated for dependency injection. The body of the request will
automatically be injected in this parameter.
Notice that this method returns a javax.ws.rs.core.Response
object. Returning this object allows the method to control the HTTP
status code and headers returned in the response. In this case, the
response's status code is altered based on whether an add or update
operation took place. To illustrate, here is a small snippet of the
implementation:
if (books.containsKey(isbn))
{
response = Response.status(200).build(); //updated
}
else
{
response = Response.status(201).build(); //created
}
As you can see, if the book that was passed already existed, the
response status code is set to 200 (OK). If the book did not exist
previously, the status code is set to 201 (Created). See the full
implementation at Library REST Sample or in the sample project presented below for more information.
Create the DELETE Method
Last we'll create the method that allows the client to delete books
from the library. The definition for this method looks like this:
@DELETE
@Path("/books/{isbn}")
public Response removeBook(@PathParam("isbn")String isbn)
{
//implementation here...
}
From this definition, we can see that this method process HTTP DELETE methods (based on the @DELETE annotation) passed to the /library/books/{isbn}
path. Again, the book's ISBN value is automatically extracted from the
URI and injected in the method's isbn parameter. The complete method
implementation is available at Library REST Sample or in the sample project below.
Create a Test Application
The RestEasy implementation of JAX-RS includes a client library that
makes creating REST clients quick and easy. The client library is easy
to learn because it uses the same JAX-RS annotations as the REST
service except their meaning is reversed. For example, the @Path annotation in a service method maps requests to the method based on their URI. In contrast, the @Path annotation in a client method indicates the URI to which an HTTP request should be sent.
Client Interface
To create a RestEasy client application, start by creating an
interface that describes the remote services that you'd like to call.
Let's look at a simple example:
import javax.ws.rs.*;
import javax.ws.rs.core.Response;
import org.resteasy.spi.ClientResponse;
@Path("library")
public interface TestClient
{
@GET
@Path("books")
@ProduceMime("application/xml")
String getBooks();
@POST
@Path("books/{isbn}/reviews")
@ConsumeMime("application/xml")
@ProduceMime("application/xml")
ClientResponse<String> createReview(@PathParam("isbn") String isbn, String body);
@DELETE
@Path("books/{isbn}")
Response.Status deleteBook(@PathParam("isbn") String isbn);
}
The annotations in this example work similarly to the way they do in a service class. The @Path("library") class annotation indicates the "base URI" upon which the rest of the @Path annotations will build. Now let's examine each of the methods.
The getBooks() method is annotated to indicate that it should send an HTTP GET request to the library/books URI (the library portion of the URI comes from the class's @Path annotation). The @ProduceMime annotation denotes that this method returns an XML formatted string.
The createReview() method is slightly more complex. This method's annotations indicate that it sends an HTTP POST request to the library/books/{isbn}/reviews URI. Notice that this URI includes the {isbn}
path parameter. In a service method, the value of this parameter is
extracted from the URI and injected into the associated method
parameter. The path parameter is associated with a method parameter
using the @PathParam annotation. For client methods, this
process works in reverse. Rather than extracting the value from the URI
and injecting it into the method parameter, the value is extracted from
the method parameter and injected into the URI. It is this fully
constructed URI to which the request is then sent. So, to create a new
review for a book with ISBN number 12345, the client application would
simply call the createReview() method like this:
ClientResponse<String> response = client.createReview("12345", "<Review>Great book!</Review>");
As you would expect, this call will generate a POST request to the library/books/12345/reviews URI. Don't worry about the client object for now. We'll see how that is created in the next section.
Now let's look at the createReview() method's second parameter, body.
Remember how the non-annotated parameter in a service method represents
the body of the request? It is the same with the client. Only one
non-annotated parameter is allowed per method and that parameter
represents the body of the HTTP request. Similarly, the @ConsumeMime and @ProduceMime annotations also work the same as they do on the server-side. The @ConsumeMime annotation corresponds to the body of the request and @ProduceMime corresponds to the body of the response. In the example above, the @ConsumeMime and @ProduceMime annotations indicate that the createReview() method accepts (or consumes) XML in its body parameter and returns (or produces) an XML formatted string.
Finally, let's look at the return type. You may have noticed that the return type for createReview() isn't just a normal string that contains the response body. Rather, the return type is declared as ClientResponse<String>. Why not just use String here? The answer is that ClientResponse
is used whenever the client may be interested in more than just the
body of the response. For example, this object provides additional
information about the response including its status code and all HTTP
headers. The <String> portion of the ClientResponse<String> return type indicates that the request body (also called the entity) is stored in the ClientResponse as a string. Here's how you could use the ClientResponse object to display the status code in addition to the body of the response:
ClientResponse<String> response = client.createReview("12345", "<Review>Great book!</Review>");
System.out.println("HTTP Status Code: " + response.getStatus());
System.out.println("HTTP Status Message: " + Response.Status.fromStatusCode(response.getStatus()).toString());
System.out.println("Body: " + response.getEntity().toString());
Last, let's briefly examine the deleteBook() method. There's nothing new here accept for the Response.Status
return type. Since the response to a DELETE operation is typically
empty (does not contain any body information), we can directly return
the HTTP status code rather than require the client to retrieve it from
within a ClientResponse object. Here's how we could go about showing the results of a DELETE operation:
Response.Status status = client.deleteBook("12345");
System.out.println("HTTP Status Code: " + status.getStatusCode();
System.out.println("HTTP Status Message: " + status.toString());
Also notice that the deleteBook() method is not annotated with either @ConsumeMime or @ProduceMime because the body of both its request and response is empty.
Client Application
Now that we have a REST client interface defined, let's create an
application that uses it to test our REST service. The RestEasy client
library uses a Java mechanism known as a dynamic proxy to
construct a concrete instance of our interface according to the
annotations we defined. It seems a little like magic but, fortunately,
we don't have to worry about how the TestClient instance
is created "under the hood". We just ask the RestEasy framework to
create a REST client object based on our interface and it happens! Here
is a simple client application that uses the interface we created in
the last section:
import org.resteasy.spi.ResteasyProviderFactory;
import org.resteasy.plugins.providers.RegisterBuiltin;
import org.resteasy.plugins.client.httpclient.ProxyFactory;
public class TestApp
{
public static void main(String[] args)
{
//the following two initialization statements only need to be executed once per VM
ResteasyProviderFactory.initializeInstance();
RegisterBuiltin.register(ResteasyProviderFactory.getInstance());
//note that "MyRestService" is the context assigned to the service's web application
TestClient client = ProxyFactory.create(TestClient.class, "http://localhost:8080/MyRestService");
System.out.println(client.getBooks());
System.out.println(client.createReview("12345", "<Review>Great book!</Review>").getEntity());
}
}
The first two lines in the main() method are
boiler-plate initialization calls that must be made before attempting
to create a REST service proxy. These static calls need only be
executed once per virtual machine. The ProxyFactory.create() method is where the magic happens and a fully functional instance of our TestClient interface is created. Notice that the second parameter to the create()
method conveys the location of the REST service. Once created, we can
simply call methods on the service proxy like we would on any POJO.
Create a Test HTML Page
One advantage of REST services over SOAP is that they can be tested
from a simple HTML page within a browser. A page like this is usually
placed in the root of the REST web service application and named
something like index.html or test.html. The file can then be accessed from a browser at a URI like this:
http://localhost:<port>/<context>/index.html
Let's take a look at how this an HTML page is created to test each of the methods supported by the sample REST service.
Get Books
As documented above, the get books REST service call looks like this:
GET http://<server>/<context>/library/books
Since hyperlinks always use the HTTP GET method, this service call is easily made available by one line in an HTML page:
<a href="library/books">Get All Books</a>
For instance, if the above line was stored in a file named index.html that was stored at the root of a REST web application, the test HTML file could be invoked from a browser with this URI:
http://localhost/<context>/index.html
Where <context> is the servlet context name
under which the web application is deployed. Once this page has been
loaded by the browser, it remembers its current location and will apply
this location to any relative paths referenced from the browser. That's
what happens with the "Get All Books" hyperlink above. This hyperlink
uses a relative path of library/books. The browser will automatically make this path relative to the location of the index.html file that it just loaded. Hence, the hyperlink effectively references the URI: http://localhost/<context>/library/books
Get Book
The HTML to test the "Get Book" functionality is slightly more
complex because the user must be prompted for the ISBN of the book to
retrieve. To accomplish this, we'll need to use an HTML form, input
box, button, and a little JavaScript to collect and submit the
information to the REST service. The HTML for this feature looks like
this:
<form name="GetBook" method="GET">
<table border="0">
<tr><th colspan="2" align="left">Get Book</td></tr>
<tr>
<td>ISBN:</td><td><input type="text" name="isbn"></td>
</tr>
<tr>
<td></td>
<td>
<input type="button" value="Get" onClick="GetBook.action='library/books/' + isbn.value;GetBook.submit()">
</td>
</tr>
</table>
</form>
You might be wondering why the JavaScript is necessary in the button's onClick
attribute. The reason is that default browser behavior dictates that
all values within a form that employs the GET method should be passed
in the query string. That would result in a service call that looks
like this:
GET http://localhost/<context>/library/books?isbn=12345
Unfortunately, this is not the format defined by the "get book" REST
service. Rather, the isbn number should be part of the path. To
accomplish this, the JavaScript sets the form's action
attribute (which represents the URI to which the form should be
submitted) to include the ISBN value and then submits the form. If a
book with the given ISBN value exists, its representation will be
returned in XML format. Otherwise, an HTTP status code of 404 (Not
Found) is returned.
Add or Update Book
The "add or update book" REST service is a bit trickier to test
since it uses the HTTP PUT method. Unfortunately, PUT (as well as
DELETE) is not supported by standard HTML. Contrast that with GET and
POST which are both natively supported by HTML forms and are easily
implemented by setting the form's method attribute to
"GET" or "POST". Setting a form's method to "PUT" or "DELETE" is
ignored and will cause the browser to use the default GET method.
So, how can we test PUT and DELETE service calls from an HTML
page? In a word, the answer is JavaScript. JavaScript allows us to make
Ajax requests that support all HTTP methods. Let's see how this is done
in the HTML:
<form name="AddUpdateBook">
<table border="0">
<tr><th colspan="2" align="left">Add or Update Book</th></tr>
<tr>
<td colspan="2">(If book with given ISBN already exists, it will be updated. Otherwise it will be added.)</td>
</tr>
<tr>
<td width="10%">ISBN:</td><td width="90%"><input type="text" name="isbn"></td>
</tr>
<tr>
<td>Book Title:</td><td><input type="text" name="name"></td>
</tr>
<tr>
<td></td>
<td>
<input type="button" value="Add/Update" onClick="addUpdateBook(AddUpdateBook.isbn.value, AddUpdateBook.name.value); return false;">
</td>
</tr>
</table>
</form>
In the HTML above, we collect the information on a book to be added
or updated using standard HTML form elements. However, rather than
submitting the form as usual, we make a call to a JavaScript method
called addUpdateBook(isbn, title). This JavaScript function looks like this:
function addUpdateBook(isbn, title)
{
var xmlHttp = getXmlHttp();
xmlHttp.onreadystatechange=function()
{
if (xmlHttp.readyState==4)
{
alert("Operation complete.");
}
}
xmlHttp.open("PUT", "library/books/" + isbn, true);
xmlHttp.send("<Book isbn=\"" + isbn + "\">" + title + "</Book>");
}
We won't go into too much detail about Ajax programming but notice how this JavaScript invokes a PUT request to the library/books/<ISBN> path (see the xmlHttp.open() function call) and includes an XML representation of a book in the body of the request (see the xmlHttp.send() method call). The function assigned to the xmlHttp.onreadystatechange property simply displays a popup box that indicates when the operation has completed.
You might be wondering about that mysterious getXmlHttp() function referenced above. This function simply creates the correct Ajax object (ActiveXObject for Internet Explorer and XmlHttpRequest for all other browsers) based on the client's browser. This method is as follows:
function getXmlHttp()
{
var xmlhttp = false;
if (window.XMLHttpRequest)
{
xmlhttp = new XMLHttpRequest()
}
else if (window.ActiveXObject) //code for IE
{
try
{
xmlhttp = new ActiveXObject("Msxml2.XMLHTTP")
}
catch (e)
{
try
{
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP")
}
catch (E)
{
xmlhttp=false
}
}
}
return xmlhttp;
}
Delete Book
As mentioned in the previous section, the HTTP DELETE method is not
supported by standard HTML so we must resort to JavaScript in order to
make DELETE requests. The HTML portion of the DELETE test looks like
this:
<form name="DeleteBook">
<table border="0">
<tr><th colspan="2" align="left">Delete Book</th></tr>
<tr>
<td>ISBN:</td><td><input type="text" name="deleteIsbn"></td>
</tr>
<tr>
<td></td>
<td>
<input type="button" value="Delete" onClick="deleteBook(DeleteBook.deleteIsbn.value); return false;">
</td>
</tr>
</table>
</form>
Similar to the "Add or Update Book" function, the "Delete Book"
feature prompts the user for the ISBN of the book to delete and passes
it to the deleteBook() JavaScript method. This method makes looks like this:
function deleteBook(isbn)
{
var xmlHttp = getXmlHttp();
xmlHttp.onreadystatechange=function()
{
if (xmlHttp.readyState==4)
{
alert("Operation complete.");
}
}
xmlHttp.open("DELETE", "library/books/" + isbn, true);
xmlHttp.send(null);
}
Similar to the addUpdateBook() method, this JavaScript invokes a DELETE request to the library/books/<ISBN> path (see the xmlHttp.open() method call) and includes nothing (null) in the body of the request (see the xmlHttp.send() method call). The function assigned to the xmlHttp.onreadystatechange property simply displays a popup box that indicates when the operation has completed.
Sample Project
The sample project uses JBoss's RestEasy
implementation of JAX-RS. RestEasy is deployed as a standard JEE web
application. To define a new service, simply drop a new JAX-RS
annotated class into the RestEasy web application's WEB-INF/classes directory and redeploy.
Follow these steps to try out the sample REST service presented in this article:
- Retrieve the sample WAR file from here.
-
Copy the WAR file to your
<JBOSS_HOME>/server/default/deploy directory. - Start JBoss (execute the
run script in the <JBOSS_Home>/bin directory). -
Invoke this URI from a browser: http://localhost:8080/SampleService/index.html
-
Test the REST service functions using the HTML test page.