Building HATEOAS Links with RESTEasy and JAX-RS

Representational State Transfer (REST) is a design idiom that employs the web’s stateless client-server architecture to represent REST webservices as resources identified by URL. REST-style architectures consist of clients and servers. Clients initiate requests to servers; servers process requests and return appropriate responses. Requests and responses are built around the transfer of “representations” of “resources”. A resource can be any coherent and meaningful concept that may be addressed. A representation of a resource is typically a document that captures the current or intended state of a resource.

(Source: http://en.wikipedia.org/wiki/Representational_State_Transfer.)

Please note that this example application should be deployed on JBOSS 7 server. If you are using any other server, then you will need to update pom.xml and web.xml files as mentioned in this post.
 Table of Contents

What is HATEOAS?
Java REST HATEOAS Example
	Creating maven blank project
	Adding required dependencies in pom.xml
	Registering a new module or service
	Defining GET,PUT,POST and DELETE methods
	Annotating model classes
	Analyze the result

1. What is HATEOAS?

HATEOAS is a constraint on REST that says that a client of a REST application need only know a single fixed URL to access it. Any and all resources should be discoverable dynamically from that URL through hyperlinks included in the representations of returned resources.

Ideally, you should provide the end user only your service root URI. From there onward, user must be able to discover all other URIs in your service. These URIs can be discovered using “link”s in current resource representation. We will see a demonstration of HATEOAS in below given sample application.

Please keep in mind that HATEOAS implementation in given sample project is for demo only. In enterprise level application, any third party API or some custom implementation (preferably using annotations) is recommended.

2. Java REST HATEOAS Example

Let’s create a Java REST application and add HATEOAS links in it’s responses.

2.1. Create maven project

Creating a maven project is as simple as executing below command in command prompt. I am assuming that you have already installed maven in your system.

mvn archetype:generate -DgroupId=com.demo.rest -DartifactId=sampleRestApp -DarchetypeArtifactId=maven-archetype-webapp -DinteractiveMode=false

If you have not installed maven, please go to maven’s home page and download latest version.

Now convert above project to eclipse supported project. Below command will generate .project file and other eclipse dependencies.

mvn eclipse:eclipse -Dwtpversion=2.0

2.2. Update maven dependencies in pom.xml

Now its time to supply the required dependencies to newly created maven project. Below are the required dependencies. Add them in pom.xml file.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

  <modelversion>4.0.0</modelversion>
  <groupid>com.demo.rest</groupid>
  <artifactid>demoResteasyApplication</artifactid>
  <packaging>war</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>demoResteasyApplication Maven Webapp</name>
  <url>http://maven.apache.org</url>

  	<repositories>
	   	<repository>
	      <id>jboss</id>
	      <url>http://repository.jboss.org/maven2</url>
	   	</repository>
	</repositories>

  <dependencies>

    <dependency>
      <groupid>junit</groupid>
      <artifactid>junit</artifactid>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>

    <!-- core library -->
	<dependency>
		<groupid>org.jboss.resteasy</groupid>
		 <artifactid>resteasy-jaxrs</artifactid>
		<version>2.3.1.GA</version>
		<scope>provided</scope>
	</dependency>

   <!-- JAXB support -->
   <dependency>
      <groupid>org.jboss.resteasy</groupid>
      	<artifactid>resteasy-jaxb-provider</artifactid>
      <version>2.3.1.GA</version>
   </dependency>

   <!-- multipart/form-data and multipart/mixed support -->
   <dependency>
      <groupid>org.jboss.resteasy</groupid>
      	<artifactid>resteasy-multipart-provider</artifactid>
      <version>2.3.1.GA</version>
   </dependency>

   <dependency>
		<groupid>net.sf.scannotation</groupid>
		<artifactid>scannotation</artifactid>
		<version>1.0.2</version>
	</dependency>

  </dependencies>

  <build>
    <finalname>demoResteasyApplication</finalname>
  </build>

</project>

2.3. Register a new module or service

With release of jax-rs 2.x, we don’t need to specify anything in web.xml. Jax-rs now scan @ApplicationPath annotation for registering new application modules.

package com.demo.rest;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

import com.demo.rest.service.UserService;

@ApplicationPath("/")
public class ApplicationConfig extends Application 
{
    @SuppressWarnings("unchecked")
	public Set<class <?>> getClasses() 
	{
        return new HashSet<class <?>>(Arrays.asList(UserService.class));
    }
}

Our module class looks like this:

@Path("/user-management")
public class UserService 
{
  //Some code
}

Above module registration code will register a new application "/user-management" and will forward all relative relative resource requests to this application / module.

2.4. Define REST methods – GET, PUT, POST and DELETE

As discussed above, REST services maps resource representations and actions which will change their internal representation. These actions shall be seen as equivalent to database SELECT, INSERT, UPDATE and DELETE operations.

If we talk about HTTP protocol, they can be mapped to GET, PUT, POST and DELETE methods. Where:

  • GET method will return a resource representation
  • PUT will modify the internal state of a resource
  • POST is usually for adding a new resource but not essentially
  • DELETE is for removing a resource

Now, lets them understand in terms of user-management module.

  1. A GET should return all users or a single user representation.
  2. A PUT method should be used to modify the representation of a single user.
  3. A POST method should be used to create a new user resource.
  4. And similarly DELETE method should be used to delete a user from system.
package com.demo.rest.service;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;

import com.demo.rest.model.User;
import com.demo.rest.model.Users;

@Path("/user-management")
public class UserService {

	@GET
	@Path("/")
	@Produces("application/vnd.com.demo.user-management+xml;charset=UTF-8;version=1")
	public UserService getServiceInfo() {
		return new UserService();
	}

	@GET
	@Path("/users")
	@Produces("application/vnd.com.demo.user-management.users+xml;charset=UTF-8;version=1")
	public Users getAllUsers() {
		User user1 = new User();
		user1.setId(1);
		user1.setFirstName("demo");
		user1.setLastName("user");
		user1.setUri("/user-management/users/1");

		User user2 = new User();
		user2.setId(2);
		user2.setFirstName("demo");
		user2.setLastName("user");
		user2.setUri("/user-management/users/2");

		Users users = new Users();
		users.setUsers(new ArrayList());
		users.getUsers().add(user1);
		users.getUsers().add(user2);

		return users;
	}

	@GET
	@Path("/users/{id}")
	@Produces("application/vnd.com.demo.user-management.user+xml;charset=UTF-8;version=1")
	public User getUserById(@PathParam("id") int id) {
		User user = new User();
		user.setId(id);
		user.setFirstName("demo");
		user.setLastName("user");
		user.setUri("/user-management/users/" + id);
		return user;
	}

	@POST
	@Path("/users")
	@Consumes("application/vnd.com.demo.user-management.user+xml;charset=UTF-8;version=1")
	public Response createUser(User user,
			@DefaultValue("false") @QueryParam("allow-admin") boolean allowAdmin)
			throws URISyntaxException {
		System.out.println(user.getFirstName());
		System.out.println(user.getLastName());
		return Response.status(201)
				.contentLocation(new URI("/user-management/users/123")).build();
	}

	@PUT
	// @Path("/users/{id: [0-9]*}")
	@Path("/users/{id}")
	@Consumes("application/vnd.com.demo.user-management.user+xml;charset=UTF-8;version=1")
	@Produces("application/vnd.com.demo.user-management.user+xml;charset=UTF-8;version=1")
	public User updateUser(@PathParam("id") int id, User user)
			throws URISyntaxException {
		user.setId(id);
		user.setFirstName(user.getFirstName() + "updated");
		return user;
	}

	@DELETE
	@Path("/users/{id}")
	public Response deleteUser(@PathParam("id") int id)
			throws URISyntaxException {
		return Response.status(200).build();
	}
}

2.5. Annotate model classes

So far we have created our service class. Now its time to create the resource representation which will be visible to user to play with.

If you remember, HATEOAS insists that your application should have an starting point and thereafter, each interaction of user with application should be an state transfer. The required information for state transfer should come from present resource representation i.e. each re-presentation should provide a mechanism for nest state transfer.

Lets annotation our service and model classes with JAXB annotations, and then we will see, to which degree we have followed HATEOAS guidelines.

//Users.java [Representation of a collection of users]

package com.demo.rest.model;

import java.util.ArrayList;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlAccessorType(XmlAccessType.NONE)
@XmlRootElement(name = "users")
public class Users {

	@XmlElement(name="user")
	private ArrayList users;

	public ArrayList getUsers() {
		return users;
	}

	public void setUsers(ArrayList users) {
		this.users = users;
	}
}

//User.java [Representation of a single user]

package com.demo.rest.model;

import java.io.Serializable;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlAccessorType(XmlAccessType.NONE)
@XmlRootElement(name = "user")
public class User implements Serializable {

	private static final long serialVersionUID = 1L;

	@XmlAttribute(name = "id")
	private int id;

	@XmlAttribute(name="uri")
	private String uri;

	@XmlElement(name = "firstName")
	private String firstName;

	@XmlElement(name = "lastName")
	private String lastName;

	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getFirstName() {
		return firstName;
	}
	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}
	public String getLastName() {
		return lastName;
	}
	public void setLastName(String lastName) {
		this.lastName = lastName;
	}
	public String getUri() {
		return uri;
	}
	public void setUri(String uri) {
		this.uri = uri;
	}
}

//UserService.java with JAXB annotation added [Representation of service root]

package com.demo.rest.service;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

import com.demo.rest.model.User;
import com.demo.rest.model.Users;

@XmlAccessorType(XmlAccessType.NONE)
@XmlRootElement(name = "user-management")
@Path("/user-management")
public class UserService {
	@XmlElement(name = "users")
	private String uri1 = "/user-management/users";

	@XmlElement(name = "report")
	private String uri2 = "/user-managemet/generate-report";

	public String getUri1() {
		return uri1;
	}

	public void setUri1(String uri1) {
		this.uri1 = uri1;
	}

	public String getUri2() {
		return uri2;
	}

	public void setUri2(String uri2) {
		this.uri2 = uri2;
	}

	@GET
	@Path("/")
	@Produces("application/vnd.com.demo.user-management+xml;charset=UTF-8;version=1")
	public UserService getServiceInfo() {
		return new UserService();
	}

	@GET
	@Path("/users")
	@Produces("application/vnd.com.demo.user-management.users+xml;charset=UTF-8;version=1")
	public Users getAllUsers() {
		User user1 = new User();
		user1.setId(1);
		user1.setFirstName("demo");
		user1.setLastName("user");
		user1.setUri("/user-management/users/1");

		User user2 = new User();
		user2.setId(2);
		user2.setFirstName("demo");
		user2.setLastName("user");
		user2.setUri("/user-management/users/2");

		Users users = new Users();
		users.setUsers(new ArrayList());
		users.getUsers().add(user1);
		users.getUsers().add(user2);

		return users;
	}

	@GET
	@Path("/users/{id}")
	@Produces("application/vnd.com.demo.user-management.user+xml;charset=UTF-8;version=1")
	public User getUserById(@PathParam("id") int id) {
		User user = new User();
		user.setId(id);
		user.setFirstName("demo");
		user.setLastName("user");
		user.setUri("/user-management/users/" + id);
		return user;
	}

	@POST
	@Path("/users")
	@Consumes("application/vnd.com.demo.user-management.user+xml;charset=UTF-8;version=1")
	public Response createUser(User user,
			@DefaultValue("false") @QueryParam("allow-admin") boolean allowAdmin)
			throws URISyntaxException {
		System.out.println(user.getFirstName());
		System.out.println(user.getLastName());
		return Response.status(201)
				.contentLocation(new URI("/user-management/users/123")).build();
	}

	@PUT
	// @Path("/users/{id: [0-9]*}")
	@Path("/users/{id}")
	@Consumes("application/vnd.com.demo.user-management.user+xml;charset=UTF-8;version=1")
	@Produces("application/vnd.com.demo.user-management.user+xml;charset=UTF-8;version=1")
	public User updateUser(@PathParam("id") int id, User user)
			throws URISyntaxException {
		user.setId(id);
		user.setFirstName(user.getFirstName() + "updated");
		return user;
	}

	@DELETE
	@Path("/users/{id}")
	public Response deleteUser(@PathParam("id") int id)
			throws URISyntaxException {
		return Response.status(200).build();
	}
}

2.6. Verify HATEOAS links

So we have written a lot of code for a simple demo. Now, its time to test our code.

I am using RESTClient for verifying the APIs output. You can choose your own way for verification.

I have deployed above application in JBOSS 7.1 server runtime environment running on eclipse juno. If you want to deploy on some standalone jboss instance, you can try that also.

Lets hit the application URLs one by one:

  1. Root service API

    This API returns a representation of service root. It have a uri for user collection and one additional link for API to generate user reports.

    user-management-4567395

  2. Get all users collection

    This representation has a snapshot of user data and uri where all other information of a particular user can be fetched.

    users-1784219

  3. Get user by id

    This representation should provide each relevant detail of user resource and other links if they are.

    user-7693476

  4. Add user without media type

    A user resource, when added, should be added in collection of users. So, implicitly a POST should be available in practically all collection type representations.

    Here, user will be added in users collection so we will post it on “/user-management/users”.

    When adding a post, we must declare the media type we are posting. If we do not specify, following error will occur.

    Response code 415 [Unsupported Media Type]

    create-unsupported-media-type-4296055

  5. Add correct media type in request header

    Lets add correct media type to request header.

    add-content-type-3789827

  6. Create with correct media type

    Now, a new user should be created and a “link’ to created resource should be returned.

    create-success-3066115

  7. Delete user

    When deleting a resource, “HTTP DELETE” is used. It shall not take any media type or request body.

    Resource should be deleted on resource link itself.

    delete-9012031

So, this is how the application state should change between successive API calls through REST HATEOAS and its next state should be directed from current state representation.

I am finishing the writing here and I will leave the task of making this example more fine on you. From below given download link, download above source code files and play with it.

Happy Learning !!

Comments

Subscribe
Notify of
guest
17 Comments
Most Voted
Newest Oldest
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.

Our Blogs

REST API Tutorial

Dark Mode

Dark Mode