RESTful Java with JAX-RS 2.0 (2013)
Part II. JAX-RS Workbook
Chapter 25. Examples for Chapter 11
In Chapter 11, you learned about HTTP caching techniques. Servers can tell HTTP clients if and how long they can cache retrieved resources. You can revalidate expired caches to avoid resending big messages by issuing conditional GET invocations. Conditional PUT operations can be invoked for safe concurrent updates.
Example ex11_1: Caching and Concurrent Updates
The example in this chapter expands on the CustomerResource
example repeated throughout this book to support caching, conditional GETs, and conditional PUTs.
The Server Code
The first thing is to add a hashCode()
method to the Customer
class:
src/main/java/com/restfully/shop/domain/Customer.java
@XmlRootElement(name
=
"customer")
public
class
Customer
{
...
@Override
public
int
hashCode()
{
int
result
=
id;
result
=
31
*
result
+
(firstName
!=
null
?
firstName.hashCode()
:
0);
result
=
31
*
result
+
(lastName
!=
null
?
lastName.hashCode()
:
0);
result
=
31
*
result
+
(street
!=
null
?
street.hashCode()
:
0);
result
=
31
*
result
+
(city
!=
null
?
city.hashCode()
:
0);
result
=
31
*
result
+
(state
!=
null
?
state.hashCode()
:
0);
result
=
31
*
result
+
(zip
!=
null
?
zip.hashCode()
:
0);
result
=
31
*
result
+
(country
!=
null
?
country.hashCode()
:
0);
return
result;
}
}
This method is used in the CustomerResource
class to generate semi-unique ETag
header values. While a hash code calculated in this manner isn’t guaranteed to be unique, there is a high probability that it will be. A database application might use an incremented version column to calculate the ETag
value.
The CustomerResource
class is expanded to support conditional GETs and PUTs. Let’s take a look at the relevant pieces of code:
src/main/java/com/restfully/shop/services/CustomerResource.java
@Path("/customers")
public
class
CustomerResource
{
...
@GET
@Path("{id}")
@Produces("application/xml")
public
Response
getCustomer(@PathParam("id")
int
id,
@Context
Request
request)
{
Customer
cust
=
customerDB.get(id);
if
(cust
==
null)
{
throw
new
WebApplicationException(Response.Status.NOT_FOUND);
}
if
(sent
==
null)
System.out.println("No ETag sent by client");
EntityTag
tag
=
new
EntityTag(Integer.toString(cust.hashCode()));
CacheControl
cc
=
new
CacheControl();
cc.setMaxAge(5);
The getCustomer()
method first starts out by retrieving the current Customer
object identified by the id
parameter. A current ETag
value is created from the hash code of the Customer
object. A new Cache-Control
header is instantiated as well.
Response.ResponseBuilder
builder
=
request.evaluatePreconditions(tag);
if
(builder
!=
null)
{
System.out.println(
"** revalidation on the server was successful");
builder.cacheControl(cc);
return
builder.build();
}
Next, Request.evaluatePreconditions()
is called to perform a conditional GET. If the client has sent an If-None-Match
header that matches the calculated current ETag
, the method returns immediately with an empty response body. In this case, a new Cache-Control
header is sent back to refresh the max-age
the client will use.
// Preconditions not met!
cust.setLastViewed(new
Date().toString());
builder
=
Response.ok(cust,
"application/xml");
builder.cacheControl(cc);
builder.tag(tag);
return
builder.build();
}
}
If no If-None-Match
header was sent or the preconditions were not met, the Customer
is sent back to the client with an updated Cache-Control
header.
@Path("{id}")
@PUT
@Consumes("application/xml")
public
Response
updateCustomer(@PathParam("id")
int
id,
@Context
Request
request,
Customer
update
)
{
Customer
cust
=
customerDB.get(id);
if
(cust
==
null)
throw
new
WebApplicationException(Response.Status.NOT_FOUND);
EntityTag
tag
=
new
EntityTag(Integer.toString(cust.hashCode()));
The updateCustomer()
method is responsible for updating a customer. It first starts off by finding the current Customer
with the given id
. From this queried customer, it generates the up-to-date value of the ETag
header.
Response.ResponseBuilder
builder
=
request.evaluatePreconditions(tag);
if
(builder
!=
null)
{
// Preconditions not met!
return
builder.build();
}
The current ETag
header is compared against any If-Match
header sent by the client. If it does match, the update can be performed:
// Preconditions met, perform update
cust.setFirstName(update.getFirstName());
cust.setLastName(update.getLastName());
cust.setStreet(update.getStreet());
cust.setState(update.getState());
cust.setZip(update.getZip());
cust.setCountry(update.getCountry());
builder
=
Response.noContent();
return
builder.build();
}
}
Finally, the update is performed.
The Client Code
The client code first performs a conditional GET. It then tries to do a conditional PUT using a bad ETag
value.
public
class
CustomerResourceTest
{
@Test
public
void
testCustomerResource()
throws
Exception
{
WebTarget
customerTarget
=
client.target("http://localhost:8080/services/customers/1");
Response
response
=
customerTarget.request().get();
Assert.assertEquals(200,
response.getStatus());
Customer
cust
=
response.readEntity(Customer.class);
EntityTag
etag
=
response.getEntityTag();
response.close();
The testCustomerResource()
method starts off by fetching a preinitialized Customer
object. It does this so that it can obtain the current ETag
of the Customer
representation.
System.out.println("Doing a conditional GET with ETag: "
+
etag.toString());
response
=
customerTarget.request()
.header("If-None-Match",
etag).get();
Assert.assertEquals(304,
response.getStatus());
response.close();
This code is performing a conditional GET. We set the If-None-Match
header using the previously fetched ETag
value. The client is expecting that the server will return a 304, “Not Modified,” response.
// Update and send a bad etag with conditional PUT
cust.setCity("Bedford");
response
=
customerTarget.request()
.header("If-Match",
"JUNK")
.put(Entity.xml(cust));
Assert.assertEquals(412,
response.getStatus());
response.close();
}
}
Finally, the code does a conditional PUT with a bad ETag
value sent with the If-Match
header. The client is expecting this operation to fail with a 412, “Precondition Failed,” response.
Build and Run the Example Program
Perform the following steps:
1. Open a command prompt or shell terminal and change to the ex11_1 directory of the workbook example code.
2. Make sure your PATH is set up to include both the JDK and Maven, as described in Chapter 17.
3. Perform the build and run the example by typing maven install
.
Another interesting thing you might want to try is to start up and leave the application running by using maven jetty:run
. Open your browser to http://localhost:8080/customers/1. Continually refresh this URL. You will be able to see if your browser performs a conditional GET request or not by viewing the <last-viewed>
element of the returned XML. I found that Firefox 3.5.2 does a conditional GET, while Safari 4.0.1 does not.