Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Kenneth A. Kousen - Making Java Groovy - 2014.pdf
Скачиваний:
50
Добавлен:
19.03.2016
Размер:
15.36 Mб
Скачать

Hypermedia

243

In this case I’m running the Grizzly test server on port 1234, and for this demo the data is in JSON form. The test for the GET method produces the following:

def response = client.get(path: 'people') response.status == 200 response.contentType == 'application/json' response.data.size() == 5

The RESTClient provides a get method that takes a path parameter. The response comes back with special properties for (most of) the typical headers. Other headers can be retrieved either by requesting the allHeaders property or by calling getHeader("...") with the required header. Any returned entity in the body of the response is in the data property.

See the rest14 of the PersonResourceSpec class for examples of POST, PUT, and DELETE requests.

Lessons learned (REST clients)

1JAX-RS 2.0 includes classes for building REST clients.15

2The Groovy project HttpBuilder wraps the Apache HttpClient project and makes it easier to use.

Both the RESTClient and the JAX-RS 2.0 client are used in the test cases in the hypermedia section, which is as good a segue as any to finally discuss HATEOAS in Java.15

9.5Hypermedia

A series of resource URLs is not a RESTful web service. At best, it’s a URL-driven database. Yet applications like that, which claim to be RESTful services, are all over the web.

A true16 REST application understands that specific resource URLs may evolve, despite attempts to keep them as stable as possible. The idea therefore is to make requests that discover the subsequent URLs to follow. We’re so accustomed to having a fixed API that this can be a difficult concept to adopt. Instead of knowing exactly what you’re going to get back from any given request, you know how to make the first request and interrogate the result for whatever may come next. This is similar to the way we browse the web, which is no coincidence.

It does place a higher burden on the client and the server, though. The server needs to add some sort of metadata to explain what the subsequent resources are

14Again, sorry. At some point (and that may already have happened), when I say, “No pun intended,” you’re simply not going to believe me.

15The JAX-RS client classes are very easy to use, too, which is unfortunate when you’re trying to show how cool Groovy is, but helpful for users. Oh well.

16The word true here is defined as “at least trying to follow the principles in Roy Fielding’s thesis.”

www.it-ebooks.info

244

CHAPTER 9 RESTful web services

and how to access them, and the client needs to read those responses and interpret them correctly.

This section will illustrate the ways you can add links to the service responses. I’ll start by showing an example from a public API, then demonstrate how to add links to the HTTP response headers or to the response bodies, and finally demonstrate how to customize the output however you want.

9.5.1A simple example: Rotten Tomatoes

As a simple example, consider the API provided by the movie review website Rotten Tomatoes used in chapter 8 on Groovy with databases. The Rotten Tomatoes API only supports GET requests, so it isn’t a full RESTful service.17

Using the site’s URL-based API to query for movies including the word trek looks like this:

api.rottentomatoes.com/api/public/v1.0/movies.json?q=trek&apikey=3...

Out of the resulting 151 (!) movies,18 if I select Star Trek Into Darkness, I get a JSON object that looks like the following (with a lot of parts elided):

{

"id": "771190753",

"title": "Star Trek Into Darkness", "year": 2013,

...,

"synopsis": "The Star Trek franchise continues ...",

..., "links": {

"self": "http://api.rottentomatoes.com/.../771190753.json", "cast": "http://api.rottentomatoes.com/.../771190753/cast.json", "clips": "http://api.rottentomatoes.com/.../771190753/clips.json",

"reviews": "http://api.rottentomatoes.com/.../771190753/reviews.json", "similar": "http://api.rottentomatoes.com/.../771190753/similar.json"

}

}

The movie object (a resource using a JSON representation) includes an entry called links, which itself is a map of keys and values. The keys in the links objects all point to additional resources, such as a full cast listing or reviews.

The Rotten Tomatoes service adds links to the individual resources rather than appending them to the response headers. The site uses its own format rather than

17RESTful services that only support GET can be called GETful services. If they’re stateless, too, doesn’t that make them FORGETful services? Thank you, thank you. I’ll be here all week. Try the veal, and don’t forget to tip your waitresses.

18Including one called, I kid you not, Star Trek versus Batman. The Enterprise goes back in time to the 1960s and gets taken over by the Joker and Catwoman. Seriously.

www.it-ebooks.info

Hypermedia

245

some other standard.19 It also handles content negotiation by embedding the “.json” string in the URL itself.

The client, of course, needs to know all of that, but by including a links section in the response the server is identifying exactly what’s expected next. The client can simply present those links to the user, or it can try to place them in context, which requires additional understanding.

Generating a good client for a hypermedia-based RESTful service is not a trivial task. Notice one interesting point: the entire API uses JSON to express the objects. So far in this chapter I’ve used the term resource to represent not only the server-side object

exposed to the client, but also how it’s expressed. Formally, the term representation is used to describe the form of the resource.

REPRESENTATION A representation is an immutable, self-descriptive, stateless snapshot of a resource, which may contain links to other resources.

The most common representations are XML and JSON, with JSON becoming almost ubiquitous.

The Richardson maturity model: a rigged demo

The Richardson Maturity Model (RMM) is based on a 2008 presentation made by Leonard Richardson, who described multiple levels of REST adoption.

RMM has four levels, numbered from zero to three:

Level 0: Plain old XML (POX) over HTTP—HTTP is merely a transport protocol, and the service is essentially remote procedure calls using it. Sounds a lot like SOAP, doesn’t it? That’s no accident.

Level 1: Addressable resources—Each URI corresponds to a resource on the server side.

Level 2: Uniform interface—The API utilizes only the HTTP verbs GET, PUT, POST, and DELETE (plus maybe OPTIONS and TRACE).

Level 3: Hypermedia—The representation of the response contains links defining additional steps in the process. The server may even define custom MIME types to specify how the additional metadata is included.

Now, honestly, I have no objections to this model. It’s fundamental to Roy Fielding’s thesis to include all of it; you’re not really adopting REST unless you have hypermedia, too.

The word maturity, however, carries a lot of emotional baggage. Who wants their implementation to be less mature? It also can’t be a coincidence that SOAP is considered maturity level 0. The model is fine, but there’s no need to load it down with judgmental overtones that make it feel like a rigged demo.

19Attempts at standardizing JSON links include www.subbu.org/blog/2008/10/generalized-linking and www.mnot.net/blog/2011/11/25/linking_in_json.

www.it-ebooks.info

246

CHAPTER 9 RESTful web services

 

Transitional links

 

HTTP/1.1 200 OK

 

Link: <http://localhost:1234/people/2>; rel="prev"

 

Link: <http://localhost:1234/people/3>; rel="self"

 

Link: <http://localhost:1234/people/4>; rel="next"

 

Content-Type: application/json

 

Date: Thu, 11 Apr 2013 16:08:47 GMT

 

Content-Length: 257

{"id":3,"mrst":"James","last":"Kirk",

"prev":{"params":{"rel":"prev"},"href":"http://localhost:1234/people/2"},

"self":{"params":{"rel":"self"},"href":"http://localhost:1234/people/3"},

"next":{"params":{"rel":"next"},"href":"http://localhost:1234/people/4"}}

Structural links

Figure 9.4 Transitional links appear in the HTTP response headers, while structural links are part of the response objects. In each case the links can be used to access other resources from this one.

Hypermedia20 in JAX-RS works through links, which come in two types:

Transitional links in HTTP headers

Structural links embedded in the response

Figure 9.4 shows both in a single HTTP response.

Version 2.0 of the JAX-RS specification supports transitional links using the Link and LinkBuilder classes, and structural links using a special JAXB serializer.

To illustrate both, I’ll continue with the Person example from earlier by adding links to each instance. Each person has three possible links:

A self link, containing the URL for that person

A prev link, pointing to the person with an ID one less than the current person

A next link, pointing to the person with an ID one greater than the current person

This is a rather contrived case, but it has the advantage of simplicity.

First I’ll add the links to the HTTP headers and show how to use them. Then I’ll use structural links instead, using the JAXB serializer. Finally, I’ll take control of the output generation process and customize the output writer using Groovy’s JsonBuilder.

9.5.2Adding transitional links

To create transitional links, the JAX-RS API starts with the inner class Response

.ResponseBuilder in the javax.ws.rs.core package. ResponseBuilder has three relevant methods:

public abstract Response.ResponseBuilder link(String uri, String rel) public abstract Response.ResponseBuilder link(URI uri, String rel) public abstract Response.ResponseBuilder links(Link... link)

20Believe it or not, neither the words hypermedia nor HATEOAS appears at all in the JSR 339 specification. I have no explanation for this.

www.it-ebooks.info

Getter methods for min and max IDs
Subtract to get base URI

Hypermedia

247

The first two add a single Link header to the HTTP response. The third adds a series of headers to the response. Here’s an example from the PersonResource class:

@GET @Produces(MediaType.APPLICATION_JSON) Response findAll() {

def people = dao.findAll(); Response.ok(people).link(uriInfo.requestUri, 'self').build()

}

The link method in this case uses the request URI as the first argument and sets the rel property to self. The corresponding test accesses the link as follows:

def 'get request returns all people'() { when:

def response = client.get(path: 'people')

then:

response.status == 200 response.contentType == 'application/json' response.headers.Link ==

'<http://localhost:1234/people>; rel="self"'

}

This example returns only a single Link header. For multiple links (for example, the three transitional links prev, next, and self for each individual person), the method getHeaders('Link') retrieves them all.

In the PersonResource the links are set with a private method, shown in the next listing.

Listing 9.7 Setting prev, self, and next link headers for each person

private Link[] getLinks(long id) { long minId = dao.minId

long maxId = dao.maxId

UriBuilder builder = UriBuilder.fromUri(uriInfo.requestUri) Link self = Link.fromUri(builder.build()).rel('self').build() String uri = builder.build().toString() - "/$id" switch (id) {

case minId:

Link next = Link.fromUri("${uri}/${id + 1}").rel('next').build() return [self, next]

case maxId:

Link prev = Link.fromUri("${uri}/${id - 1}").rel('prev').build() return [prev, self]

default:

Link next = Link.fromUri("${uri}/${id + 1}").rel('next').build() Link prev = Link.fromUri("${uri}/${id - 1}").rel('prev').build() return [prev, self, next]

}

}

So-called “self” links are generated for each person. Next and previous links are generated for those elements between the first and last. The links themselves are simply generated by string manipulation.

www.it-ebooks.info

248

CHAPTER 9 RESTful web services

Adding the links to the resource is done with the links method:

Response findById(@PathParam("id") long id) { Person p = dao.findById(id) Response.ok(p)

.links(getLinks(id))

.build()

}

It turns out that converting the Link headers into something useful isn’t simple with the RESTClient. In this case the JAX-RS Client class works better. The Client class has a method called getLink, which takes a string argument, in which the string is the relation type. That method returns an instance of the javax.ws.rs

.core.Link class, which corresponds to specification RFC 5988, Web Linking, of the IETF.

I’ll demonstrate the hypermedia capability by walking through the links one by one in a client. The following listing is a JUnit test case, written in Groovy, that accesses the next links.

Listing 9.8 Walking through the data using link headers

Use rel to return link and extract URI

@Test

void testNextAndPreviousHeaders() {

JAX-RS 2.0

client

Client cl = ClientBuilder.newClient()

 

 

 

int id = 1

WebTarget target = cl.target("http://localhost:1234/people/$id") def resp = target.request().get(Response.class)

def next = resp.getLink('next').uri

 

 

 

assert next.toString()[-1] == (++id).toString()

 

 

 

 

 

 

println 'following next links...'

 

 

 

while (next) {

Following

 

Last char

println "Accessing $next"

 

target = cl.target(next)

 

the link

 

should be

resp = target.request().get(Response.class)

 

 

proper ID

next = resp.getLink('next')?.uri

 

 

 

if (next)

 

 

 

assert next.toString()[-1] == (++id).toString()

}

println 'following prev links...' def prev = resp.getLink('prev').uri

assert prev.toString()[-1] == (--id).toString() while (prev) {

println "Accessing $prev" target = cl.target(prev)

resp = target.request().get(Response.class) prev = resp.getLink('prev')?.uri

if (prev)

assert prev.toString()[-1] == (--id).toString()

}

cl.close()

}

www.it-ebooks.info

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]