Creating a hypermedia-driven RESTful web service

duration 30 minutes

You’ll explore how to use Hypermedia as the Engine of Application State (HATEOAS) to drive your RESTful web service on Open Liberty.

What you’ll learn

You will learn how to use hypermedia to create a specific style of a response JSON, which has contents that you can use to navigate your REST service. You’ll build on top of a simple inventory REST service that you can develop with MicroProfile technologies. You can find the service at the following URL:

http://localhost:9080/inventory/hosts

The service responds with a JSON file that contains an array of registered hosts. Each host has a collection of HATEOAS links:

[
  {
    "hostname": "foo",
    "_links": [
      {
        "href": "http://localhost:9080/inventory/hosts/foo",
        "rel": "self"
      }
    ]
  },
  {
    "hostname": "bar",
    "_links": [
      {
        "href": "http://localhost:9080/inventory/hosts/bar",
        "rel": "self"
      }
    ]
  },
  {
  "hostname": "*",
  "_links": [
    {
      "href": "http://localhost:9080/inventory/hosts/*",
      "rel": "self"
    }
  ]
  }
]

What is HATEOAS?

HATEOAS is a constrained form of REST application architecture. With HATEOAS, the client receives information about the available resources from the REST application. The client does not need to be hardcoded to a fixed set of resources, and the application and client can evolve independently. In other words, the application tells the client where it can go and what it can access by providing it with a simple collection of links to other available resources.

Response JSON

When you build a RESTful web service, consider the style of your response files. Whether they are JSON files, XML files, or in some other format, a good practice is to always have them in a clean and organized form. In the context of HATEOAS, each resource must contain a link reference to itself, commonly referred to as self. Each link also needs a relationship to be associated with it, although no strict rules exist as to how you need to format this relationship. Contain the collection of such links within a _links array, which itself must be a direct property of the resource object. The underscore in the _links property is used so that the property does not collide with any existing fields that are named links. In this guide, you will use the following structure of HATEOAS links:

  "_links": [
    {
      "href": ,
      "rel":
    }
  ]

The following example shows two different links. The first link has a self relationship with the resource object and is generated whenever you register a host. The link points to that host entry in the inventory:

  {
    "href": "http://localhost:9080/inventory/hosts/<hostname>",
    "rel": "self"
  }

The second link has a properties relationship with the resource object and is generated if the host system service is running. The link points to the properties resource on the host:

  {
    "href": "http://<hostname>:9080/system/properties",
    "rel": "properties"
  }

Other formats

Although you should stick to the previous format for the purpose of this guide, another common convention has the link as the value of the relationship:

  "_links": {
      "self": "http://localhost:9080/inventory/hosts/<hostname>",
      "properties": "http://<hostname>:9080/system/properties"
  }

Getting Started

The fastest way to work through this guide is to clone the git repository and use the starting project that is provided in the start directory. To do this, run the following commands:

git clone https://github.com/openliberty/guide-rest-hateoas.git
cd guide-rest-hateoas/start

Creating the response JSON

Begin by building your response JSON, which is composed of the _links array, as well as the name of the host machine.

Linking to inventory contents

As mentioned before, your starting point is an existing simple inventory REST service. You can learn more about this service and how to build it by visiting Creating a MicroProfile application.

First, tweak the request handlers in the InventoryResource class. Since the …​/inventory/hosts/ URL will no longer respond with a JSON representation of the contents of your inventory, you can discard the listContents method and integrate it into the getPropertiesForHost method:

@GET
@Path("{hostname}")
@Produces(MediaType.APPLICATION_JSON)
public JsonObject getPropertiesForHost(@PathParam("hostname") String hostname) {
    return (hostname.equals("*")) ? manager.list() : manager.get(hostname);
}

The contents of your inventory are now under the asterisk (*) wildcard and reside at the following URL:

http://localhost:9080/inventory/hosts/*

Next, add a simple GET request handler that is responsible for handling all GET requests that are made to the target URL. This method responds with a JSON that contains HATEOAS links:

@GET
@Produces(MediaType.APPLICATION_JSON)
public JsonArray handler() {
    return manager.getSystems(uriInfo.getAbsolutePath().toString());
}

You also need a UriInfo object, which you use to build your HATEOAS links:

@Context
UriInfo uriInfo;

The @Context annotation is a part of CDI and indicates that the UriInfo will be injected when the resource is instantiated.

Your new InventoryResource class is now finished. Next, let’s implement the getSystems method and build the response JSON object.

Linking to each available resource

Let’s implement the getSystems method in your InventoryManager class. This method accepts a target URL as an argument and returns a JSON object that contains HATEOAS links.

public JsonArray getSystems(String url) {
    // inventory content
    JsonObject content = InventoryUtil.buildHostJson("*", url);

    // collecting systems jsons
    JsonArrayBuilder jsonArray = inv.keySet().stream().map(host -> {
        return InventoryUtil.buildHostJson(host, url);
    }).collect(Json::createArrayBuilder, JsonArrayBuilder::add, JsonArrayBuilder::add);

    jsonArray.add(content);

    return jsonArray.build();
}

The buildHostJson helper method builds the actual JSON object. Use this method together with a simple map function to create an array of the JSON objects for each system.

Let’s implement the buildHostJson helper in the InventoryUtil class:

public static JsonObject buildHostJson(String hostname, String url) {
    return Json.createObjectBuilder()
               .add("hostname", hostname)
               .add("_links", InventoryUtil.buildLinksForHost(hostname, url))
               .build();
}

You’re creating a JSON object that contains the name of your host system and the _links array, which is generated separately in another helper:

public static JsonArray buildLinksForHost(String hostname, String invUri) {

    JsonArrayBuilder links = Json.createArrayBuilder();

    links.add(Json.createObjectBuilder()
                  .add("href", StringUtils.appendIfMissing(invUri, "/") + hostname)
                  .add("rel", "self"));

    links.add(Json.createObjectBuilder()
            .add("href", InventoryUtil.buildUri(hostname).toString())
            .add("rel", "properties"));

    return links.build();
}

This helper accepts a host name and a target URL as arguments. The helper builds a link that points to the inventory entry with a self relationship and also builds a link that points to the system service with a properties relationship:

Linking to inactive services or unavailable resources

Consider what happens when one of the return links does not work or when a link should be available for one object but not for another. In other words, it is important that a resource or service is available and running before it is linked in the _links array.

Although this guide does not cover this case, always make sure that you receive a good response code from a service before you link that service. Similarly, make sure that it makes sense for a particular object to access a resource it is linked to. For instance, it doesn’t make sense for an account holder to be able to withdraw money from their account when their balance is 0. Hence, the account holder should not be linked to a resource that provides money withdrawal.

Building the application

To build the application, run the following command:

mvn install

This command builds the application and creates a .war file in the target directory. The command also configures and installs Liberty into the target/liberty/wlp directory. After the Maven build completes, you can use the server script in that Liberty installation. You can also start and stop the server by using the Maven goals of liberty:start-server and liberty:stop-server.

If the server is running, running the installation can cause issues because the installation attempts to start a server. You can instead run the following command:

mvn package

This command rebuilds the application, and the running Liberty server automatically picks up the changes.

If you use a tool that does incremental updates, like Eclipse, then you can bypass the application build.

Starting the application

To see the new application in action, run the Maven liberty:start-server command from the start directory:

cd start
mvn liberty:start-server

After the server runs, you can find your new hypermedia-driven inventory service at the following URL:

Testing the hypermedia-driven RESTful web service

At the following URLs, access the inventory service that is now driven by hypermedia:

The first URL returns the current contents of the inventory, and the second URL returns the system properties for the host name. If the inventory does not contain an entry for the host name that is specified in the URL, the system service that is running on the requested host name is called instead. The system properties are retrieved from that system service and then stored in the inventory and returned.

If the servers are running, you can point your browser to each of the previous URLs to test the application manually. Nevertheless, you should rely on automated tests since they are more reliable and trigger a failure if a change introduces a defect.

Setting up your tests

Create a src/test/java/it/io/openliberty/guides/hateoas/EndpointTest.java test class.

You can use the @Before and @After annotations to perform any setup and teardown tasks for each of your individual tests.

package it.io.openliberty.guides.hateoas;

import static org.junit.Assert.assertEquals;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import javax.json.JsonArray;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.Response;

import org.apache.cxf.jaxrs.provider.jsrjsonp.JsrJsonpProvider;

public class EndpointTest {
    private String port;
    private String baseUrl;

    private Client client;

    private final String SYSTEM_PROPERTIES = "system/properties";
    private final String INVENTORY_HOSTS = "inventory/hosts";

    @Before
    public void setup() {
        port = System.getProperty("liberty.test.port");
        baseUrl = "http://localhost:" + port + "/";

        client = ClientBuilder.newClient();
        client.register(JsrJsonpProvider.class);
    }

    @After
    public void teardown() {
        client.close();
    }
}

Writing the tests

Each test method must be marked with the @Test annotation. Typically, the execution order of test methods is not controlled, but if control is required, you can place methods in a single container test method. This container method is then the only one marked with the @Test annotation. The following test suite contains two test methods, which run in the order they appear:

@Test
public void testSuite() {
    this.testLinkForInventoryContents();
    this.testLinksForSystem();
}

Create a test called testLinkForInventoryContents. This test is responsible for asserting that the correct HATEOAS link is created for the inventory contents.

public void testLinkForInventoryContents() {
    Response response = this.getResponse(baseUrl + INVENTORY_HOSTS);
    this.assertResponse(baseUrl, response);

    JsonArray sysArray = response.readEntity(JsonArray.class);

    String expected, actual;

    JsonArray links = sysArray.getJsonObject(0).getJsonArray("_links");

    expected = baseUrl + INVENTORY_HOSTS + "/*";
    actual = links.getJsonObject(0).getString("href");
    assertEquals("Incorrect href", expected, actual);

    // asserting that rel was correct
    expected = "self";
    actual = links.getJsonObject(0).getString("rel");
    assertEquals("Incorrect rel", expected, actual);

    response.close();
}

Write a getResponse helper method to reuse the same line of code for retrieving a response from a specific URL. This technique helps keep your code neat and organized:

private Response getResponse(String url) {
    return client.target(url).request().get();
}

Write another helper method called assertResponse. This method ensures that the response code that you receive is valid (200):

private void assertResponse(String url, Response response) {
    assertEquals("Incorrect response code from " + url, 200, response.getStatus());;
}

Create a test called testLinksForSystem. This test is responsible for asserting that the correct HATEOAS links are created for the localhost system. This method checks for both the self link that points to the inventory service and the properties link that points to the system service that is running on the localhost system.

public void testLinksForSystem() {
    this.visitLocalhost();

    Response response = this.getResponse(baseUrl + INVENTORY_HOSTS);
    this.assertResponse(baseUrl, response);

    JsonArray sysArray = response.readEntity(JsonArray.class);

    String expected, actual;

    JsonArray links = sysArray.getJsonObject(0).getJsonArray("_links");

    // testing the 'self' link

    expected = baseUrl + INVENTORY_HOSTS + "/localhost";
    actual = links.getJsonObject(0).getString("href");
    assertEquals("Incorrect href", expected, actual);

    expected = "self";
    actual = links.getJsonObject(0).getString("rel");
    assertEquals("Incorrect rel", expected, actual);

    // testing the 'properties' link

    expected = baseUrl + SYSTEM_PROPERTIES;
    actual = links.getJsonObject(1).getString("href");
    assertEquals("Incorrect href", expected, actual);

    expected = "properties";
    actual = links.getJsonObject(1).getString("rel");
    assertEquals("Incorrect rel", expected, actual);
}

Write a helper method called visitLocalhost. This method creates a GET request to the system service, registering the localhost system:

private void visitLocalhost() {
    Response response = this.getResponse(baseUrl + SYSTEM_PROPERTIES);
    this.assertResponse(baseUrl, response);
    response.close();

    Response targetResponse = client.target(baseUrl + INVENTORY_HOSTS + "/localhost")
                                    .request()
                                    .get();
    targetResponse.close();
}

Running the tests

To rebuild and run the tests, navigate to the start directory and run the mvn clean install command from the command line.

# If the server is still running from previous steps, stop it first with the following command:
mvn liberty:stop-server

# Then run this command:
mvn clean install

Some time might elapse before the tests finish running. If the tests pass, you will see the following output:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running it.io.openliberty.guides.hateoas.EndpointTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.086 sec - in it.io.openliberty.guides.hateoas.EndpointTest

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

Great work! You’re done!

You’ve just built and tested a hypermedia-driven RESTful web service on top of Open Liberty.

Contribute to this guide

Is something missing or needs to be fixed? Raise an issue, or send us a pull request.