Skip to main content
Last updated November 21, 2011 21:06, by spericas
Feedicon  

Hypermedia Example: ClusterService

This example shows how to use the new Link and LinkBuilder classes in JAX-RS 2.0. A Link can be created by introspecting a resource method and, optionally, extending its meta-data (attributes like "rel", "type", etc).

Application Model

The ClusterService comprises of a single cluster that contains a set of machines. The cluster can be ONLINE or OFFLINE; each machine can be STOPPED, STARTED or SUSPENDED. The following code shows the important details of the application's model:

public class Cluster {
    public enum Status { OFFLINE, ONLINE };
    private String name;
    private Status status = Status.ONLINE;    
    private List<Machine> machines = new ArrayList<Machine>();
    ...
}

public class Machine {
    public enum Status { STOPPED, STARTED, SUSPENDED };
    private String name;
    private int nOfCpus;
    private double load;
    private Status status = Status.STOPPED;
    ...
}

Representations and Links

The following is a JSON representation for the cluster obtain via GET /cluster that include all of its link headers:

Content-Type:application/json
Link: </cluster>; produces="application/json"; method="GET"; rel="self"
</cluster/offliner>; produces="application/json"; method="POST"; rel="offliner"
</cluster/machine/%7Bname%7D>; produces="application/json"; method="GET"; rel="item"

{
"machines": [
{
"load": "1.4",
"name": "alpha",
"state": "STOPPED",
"nOfCpus": "2"
}, ... 
],
"name": "cluster1",
"status": "ONLINE"
}

There is a link to self at /cluster and a link to offliner at /cluster/offliner for taking the cluster OFFLINE (note the state indicates that it is ONLINE at the moment). Instead of including links for each of the machines in the cluster (a collection), we include a link header, denoted by item, whose URI is a template (note that %7B% and %7D% are the curly braces). This enables a client to instantiate the URI template and retrieve additional information for each machine. For example, using item it can execute GET /cluster/machine/alpha and get:

Content-Type:application/json
Link:</cluster/machine/alpha>; produces="application/json"; method="GET"; rel="self"
</cluster/machine/alpha/starter>; produces="application/json"; method="POST"; rel="starter"

{
"load": "1.4",
"name": "alpha",
"state": "STOPPED",
"nOfCpus": "2"
}

which includes transitional links for machine alpha. Note that the transitional links include additional meta-data about the type (produces) and the HTTP method. As we shall see, all this info can be generated by a single method call.

Resource Classes

There are two resource classes ClusterResource and MachineResource. Each class defines, purely by convention, a method called getTransitionalLinks and uses it in to generate responses. Let us look at ClusterResource first:

@Path("/cluster")
public class ClusterResource {
    private Cluster cluster = Model.getCluster();
    
    @GET
    @Produces({"application/json"})
    public Response self() {
        return Response.ok(cluster).links(getTransitionalLinks()).build();
    }
    
    @PUT
    @Consumes({"application/json"})
    @Rel("update")      // overrides method name as rel
    public void doPut(Cluster cluster) {
        Model.updateCluster(cluster);
    }
    
    @POST
    @Path("onliner")
    @Produces({"application/json"})
    public Response goOnline() {
        cluster.setStatus(Status.ONLINE);
        return Response.ok(cluster).links(getTransitionalLinks()).build();
    }
    
    @POST
    @Path("offliner")
    @Produces({"application/json"})
    public Response goOffline() {
        cluster.setStatus(Status.OFFLINE);
        return Response.ok(cluster).links(getTransitionalLinks()).build();
    }
    
    private Link[] getTransitionalLinks() {
        Link self = Link.fromResourceMethod(getClass(), "self").build();
        Link update = Link.fromResourceMethod(getClass(), "doPut").build();
        Link item = Link.fromResourceMethod(MachineResource.class, "self").rel("item").build();
        Link onliner = Link.fromResourceMethod(getClass(), "goOnline").build();
        Link offliner = Link.fromResourceMethod(getClass(), "goOffline").build();
        
        return cluster.getStatus() == Status.ONLINE ? 
                new Link[] { self, update, item, offliner } : 
                new Link[] { self, update, item, onliner };
    }
}

With the exception of getTransitionalLinks all the other methods should be self explanatory. The method getTransitionalLinks generates links based on the internal state of the cluster, i.e. whether it is ONLINE or OFFLINE.

The new method LinkBuilder Link.fromResourceMethod(Class<?>, String) is used to generate the transitional links. This method is akin to UriBuilder.fromResource(Class<?>) and UriBuilder.path(Class<?>, String), but semantically richer. It automatically generates the link header params rel, produces, consumes and method. In the spirit of CoC, the attribute rel is set (in order):

  1. As the value of the @Rel annotation, if any
  2. As the value of the @Path annotation, if any
  3. As the method name

Regardless, any of the these values can be changed using the builder returned --note how rel("item") is set in the example above.

The implementation of the MachineResource is very similar:

@Path("/cluster/machine/{name}")
public class MachineResource {
    private Machine machine;

    public MachineResource(@PathParam("name") String name) {
        machine = Model.getMachine(name);
    }

    @GET
    @Produces({"application/json"})
    public Response self() {
        return Response.ok(machine).links(getTransitionalLinks()).build();
    }

    @POST
    @Path("starter")
    @Produces({"application/json"})
    public Response doStart() {
        machine.setState(State.STARTED);
        return Response.ok(machine).links(getTransitionalLinks()).build();
    }

    @POST
    @Path("stopper")
    @Produces({"application/json"})
    public Response doStop() {
        machine.setState(State.STOPPED);
        return Response.ok(machine).links(getTransitionalLinks()).build();
    }

    @POST
    @Path("suspender")
    @Produces({"application/json"})
    public Response doSuspend() {
        machine.setState(State.SUSPENDED);
        return Response.ok(machine).links(getTransitionalLinks()).build();
    }

    private Link[] getTransitionalLinks() {
        String name = machine.getName();
        
        Link self = Link.fromResourceMethod(MachineResource.class, "self").build(name);
        Link starter = Link.fromResourceMethod(MachineResource.class, "doStart").build(name);
        Link stopper = Link.fromResourceMethod(MachineResource.class, "doStop").build(name);
        Link suspender = Link.fromResourceMethod(MachineResource.class, "doSuspend").build(name);

        switch (machine.getState()) {
            case STOPPED:
                return new Link[] { self, starter };
            case STARTED:
                return new Link[] { self, stopper, suspender };
            case SUSPENDED:
                return new Link[] { self, starter };
            default:
                throw new IllegalStateException();
        }
    } 
    ...
}

As expected, the set of transitional states depends on the internal state of the machine. The other resource methods in MachineResource are straightforward.

Realizing Hypermedia (Client Part)

Using the links generated above, and with a few convenience methods added to the Client API, truly hypermedia-driven clients can be written. Suppose that we want to bring a cluster online and start all of its machines. The following code accomplishes this task (using relative links for simplicity):

Client client = ClientFactory.newClient();
        
// Get cluster representation -- entry point
Response rc = client.target("/cluster").request("application/json").get();
        
// Ensure cluster is online
if (rc.hasLink("onliner")) {
    client.invocation(rc.getLink("onliner")).invoke();
}
        
// Start all machines in cluster
Cluster c = rc.getEntity(Cluster.class);
for (Machine m : c.getMachines()) {
    // Machine name is needed for URI template in link
    Link ml = rc.getLink("item").asBuilder().build(m.getName());
    Response rm = client.invocation(ml).invoke();
            
    // Start machine if not started already
    if (rm.hasLink("starter")) {
        client.invocation(rm.getLink("starter")).invoke();
    }
}

The example above uses a utility method Response.hasLink(String relation) to check if a link is defined and a new method Client.invocation(Link link) to create an invocation from a hyperlink.

Self Linking

A method annotated with @Rel(SELF) where SELF = "self" could be used for the generation of self links.

@Path("/cluster")
public class ClusterResource {
    private Cluster cluster = Model.getCluster();
    
    @GET
    @Produces({"application/json"})
    @Rel(SELF)
    public Response getSelf() {
        return Response.ok(cluster).links(getTransitionalLinks()).build();
    }
   ...
    private Link[] getTransitionalLinks() {
        // Generate "self" transition
        Link self = Link.fromResource(getClass()).build();
        // Get "self" transition for item and update it with new rel name "item"
        Link item = Link.fromResource(MachineResource.class).rel("item").build();
        ...
        return cluster.getStatus() == Status.ONLINE ? 
                new Link[] { self, update, item, offliner } : 
                new Link[] { self, update, item, onliner };
    }
}

Linking Using Global IDs

Mattias and Kalle from Jayway (https://github.com/jayway/jax-rs-hateoas) have proposed the use of an @Linkable annotation that is akin to @Rel but also uses a global ID. It is therefore possible to use one of these IDs to generate a link instead of a method name. This enables the same resource class to provide representations for multiple DTOs given that the same value for rel can be repeated.

Let us review the example from the last section, this time using @Linkable:

@Path("/cluster")
public class ClusterResource {
    private Cluster cluster = Model.getCluster();
    
    @GET
    @Produces({"application/json"})
    @Linkable(id=LinkableIds.CLUSTER_ID, rel="self")
    public Response getSelf() {
        return Response.ok(cluster).links(getTransitionalLinks()).build();
    }
   ...
    private Link[] getTransitionalLinks() {
        // Generate "self" transition
        Link self = Link.fromId(LinkableIds.CLUSTER_ID).build();
        // Get "self" transition for item and update it with new rel name "item"
        Link item = Link.fromResource(MachineResource.class).rel("item").build();
        ...
        return cluster.getStatus() == Status.ONLINE ? 
                new Link[] { self, update, item, offliner } : 
                new Link[] { self, update, item, onliner };
    }
}

In this example, we assign the ID LinkableIds.CLUSTER_ID to the resource method getSelf. By adding the new method Link.fromId() we can build the URI for that resource method. Naturally, support for this annotation will require updating an implementation's internal model to associated these IDs with resource methods and to check for uniqueness on a per application basis. The advantage of this approach is that it introduces a level of indirection where multiple rel with the same value are possible in a resource class. It's also (arguably) easier to generate links by referring directly to IDs.

If the ID is made optional in @Linkable, it could serve the dual purpose of (i) optionally supporting IDs and (ii) same purpose as @Rel as defined above. So @Linkable could be defined as follows:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Linkable {
	String id() default "";
	String rel();
}

Links in Representations

Although the proposal presented above does not provide support for links in representation, it certainly does not prevent this use. Applications can use Link instances in their model and provide serialization and deserialization logic for it. We could even provide an adapter to serialize these links using JAXB. The XML serialization should look link LINK in HTML:

<link href="..." rel="self" ... />

The following adapter would serialize a Link in this format:

public static class LinkAdapter extends XmlAdapter<JaxbLink, Link> {
    @Override
    public Link unmarshal(JaxbLink v) throws Exception { ... }
    @Override
    public JaxbLink marshal(Link v) throws Exception { ... }
    }

public static class JaxbLink {
        private URI uri;
        private Map<QName, Object> params;

        public JaxbLink() {
        }
        public JaxbLink(URI uri) {
            this.uri = uri;
        }
        @XmlAttribute(name="href")
        public URI getUri() {
            return uri;
        }
        @XmlAnyAttribute
        public Map<QName, Object> getParams() {
            if (params == null) {
                params = new HashMap<QName, Object>();
            }
            return params;
        }        
    }

Using this adapter, our model and resource could be updated as follows to generate the links ("self" in this example) as part of the representation:

public class Machine {
    ...
    @XmlJavaTypeAdapter(LinkAdapter.class)
    private Link self;
    ...
}

@Path("/cluster/machine/{name}")
public class MachineResource {
    private Machine machine;

    public MachineResource(@PathParam("name") String name) {
        machine = Model.getMachine(name);
    }
    @GET
    @Produces({"application/xml"})
    public Machine self() {
        machine.setSelf(Link.fromResourceMethod(MachineResource.class, 
                "self").build(name));
        // set other links ...
        return machine;
    }
    ...
}

Summary

This a summary of what is needed to support the example above:

  • A new annotation @Rel only needed when CoC is not sufficient
  • New method Link.fromResourceMethod(Class<?>, String) with semantics defined above
  • The Response.hasLink(String) convenience method
  • Client.invocation(Link) to create an invocation from a hyperlink
  • Link.asBuilder() to further configure a Link, e.g. to set template variables
  • Optionally, the ability to get a builder from an Invocation for additional configuration, after it's been created from a Link
 
 
Close
loading
Please Confirm
Close