Creating a RESTful web service

duration 30 minutes

Learn how to create a REST service with JAX-RS, JSON-P, and Open Liberty.

What you’ll learn

You will learn how to build and test a simple REST service with JAX-RS and JSON-P that will expose the JVM’s system properties. The REST service will respond to GET requests made to the URL:

http://localhost:9080/LibertyProject/System/properties

The service responds to a GET request with a JSON representation of the system properties, where each property is a field in a JSON object like this:

{
  "os.name":"Mac",
  "java.version": "1.8"
}

When you create a new REST application the design of the API is important. The JAX-RS APIs could be used to create JSON-RPC, or XML-RPC APIs, but wouldn’t be a RESTful service. A good RESTful service is designed around the resources that are exposed, and how to create, read, update, and delete the resources.

The service responds to GET requests to the /System/properties path. The GET request should return a 200 OK response that contains all of the JVM’s system properties.

Getting Started

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

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

Creating a JAX-RS application

JAX-RS has two key concepts for creating REST APIs. The most obvious is the resource itself, which is modeled as a class. The second is a JAX-RS application. A JAX-RS application is a grouping of resources that are exposed in a common URL path. Having a single JAX-RS application is common, although multiple are possible.

Create the JAX-RS application class in the src/main/java/io/openliberty/guides/rest/SystemApplication.java file.

package io.openliberty.guides.rest;

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

@ApplicationPath("System")
public class SystemApplication extends Application {

}

The SystemApplication class extends the Application class which, by default, associates all JAX-RS resource classes in the WAR file with this JAX-RS application. The @ApplicationPath annotation has a value that indicates the path within the WAR that the JAX-RS application accepts requests from.

Creating the JAX-RS resource

In JAX-RS, a single class should represent a single resource, or a group of resources of the same type. In this application a resource might be a system property, or a set of system properties. It is easy to have a single class handle multiple different resources, but keeping a clean separation between types of resources helps with maintainability in the long run.

Create the JAX-RS resource class in the src/main/java/io/openliberty/guides/rest/PropertiesResource.java file.

package io.openliberty.guides.rest;

import java.util.Properties;
import java.util.Map;

import javax.ws.rs.Path;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.GET;
import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;

import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import javax.json.JsonArray;
import javax.json.Json;
import javax.json.JsonNumber;

@Path("properties")
public class PropertiesResource {

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public JsonObject getProperties() {

        JsonObjectBuilder builder = Json.createObjectBuilder();

        System.getProperties()
              .entrySet()
              .stream()
              .forEach(entry -> builder.add((String)entry.getKey(),
                                            (String)entry.getValue()));

       return builder.build();
    }
}

This resource class has quite a bit of code in it, so let’s break it down into manageable chunks.

The @Path annotation on the class indicates that this resource responds to the properties path in the JAX-RS application. The @ApplicationPath annotation in the application class together with the @Path annotation in this class indicates that the resource is be available at System/properties path.

@Path("properties")
public class PropertiesResource {

JAX-RS maps the HTTP methods on the URL to the methods on the class. The method to call is determined by the annotations specified on the methods. In the application you are building, an HTTP GET request to the System/properties path results in the system properties being returned:

@GET
@Produces(MediaType.APPLICATION_JSON)
public JsonObject getProperties() {

    JsonObjectBuilder builder = Json.createObjectBuilder();

    System.getProperties()
          .entrySet()
          .stream()
          .forEach(entry -> builder.add((String)entry.getKey(),
                                        (String)entry.getValue()));

   return builder.build();
}

The @GET annotation on the method indicates that this method it to be called for the HTTP GET method. The @Produces annotation indicates the format of the content that will be returned, the value of the @Produces annotation will be specified in the HTTP Content-Type response header. For this application a JSON structure is to be returned. The desired Content-Type for a JSON response is application/json, using MediaType.APPLICATION_JSON instead of the String literal is better because in the event of a spelling error a compile failure will occur.

JAX-RS supports a number of ways to marshal JSON. The JAX-RS 2.0 specification mandates JSON-Processing (JSON-P) and JAX-B. Most JAX-RS implementations also support a Java POJO-to-JSON conversion, which allows the Properties object to be returned instead. Although this conversion would allow for a simpler implementation, it limits code portability as POJO-to-JSON conversion is non-standard. This gap in the specification will be fixed in Java EE 8 with the inclusion of JSON-B.

The method body does the following:

  1. Creates a JsonObjectBuilder object using the Json class. The JsonObjectBuilder is then used to populate a JsonObject with values.

  2. Call the getProperties method on the System class to get a Properties object that contains all the system properties.

  3. Call the entrySet method on the Properties object to get a Set of all the entries.

  4. Convert the Set to a Stream (new in Java SE 8) by calling the stream method. Streams make working through all the entries in a list very simple.

  5. Call the forEach method on the Stream passing in a function that will be invoked for each entry in the Stream. The function passed in will call the add method on the JsonObjectBuilder for every entry in the stream. The key and value for the JsonObject will be obtained by calling the getKey and getValue methods on the Map.Entry objects in the stream.

  6. Return the a JsonObject by calling the build method on the JsonObjectBuilder.

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.

Testing the service

You could test this service manually by starting a server and pointing a web browser at the http://localhost:9080/LibertyProject/System/properties URL. Automated tests are a much better approach because they will trigger a failure if a change introduces a bug. JUnit and the JAX-RS Client API provide a very simple environment to test the application.

You can write tests for the individual units of code outside of a running application server, or they can be written to call the application server directly. In this example you will create a test that does the latter.

Create the test class in the src/test/java/it/io/openliberty/guides/rest/EndpointTest.java file.

package it.io.openliberty.guides.rest;

import static org.junit.Assert.assertEquals;

import org.junit.Test;

import javax.json.JsonObject;
import javax.json.JsonArray;
import javax.json.Json;

import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response;
import javax.ws.rs.client.Entity;

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

public class EndpointTest {

    @Test
    public void testGetProperties() {
        String port = System.getProperty("liberty.test.port");
        String war = System.getProperty("war.name");
        String url = "http://localhost:" + port + "/" + war + "/";

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

        WebTarget target = client.target(url + "System/properties");
        Response response = target.request().get();

        assertEquals("Incorrect response code from " + url, 200, response.getStatus());

        JsonObject obj = response.readEntity(JsonObject.class);

        assertEquals("The system property for the local and remote JVM should match",
                     System.getProperty("os.name"),
                     obj.getString("os.name"));
        response.close();
    }
}

This test class has more lines of code than the resource implementation. This situation is common. The test method is indicated with the @Test annotation.

The test code needs to know some information about the application in order to make requests. The server port and the application context root are key, and are dictated by the server configuration. While this information can be hardcoded, it is better to specify it in a single place like the Maven pom.xml file:

<testServerHttpPort>9080</testServerHttpPort>
<testServerHttpsPort>9443</testServerHttpsPort>
<warContext>${app.name}</warContext>

These Maven properties can then be passed to the Java test program as a series of system properties:

<systemPropertyVariables>
    <liberty.test.port>${testServerHttpPort}</liberty.test.port>
    <war.name>${warContext}</war.name>
</systemPropertyVariables>

Getting the values to then create a representation of the URL is then simple:

String port = System.getProperty("liberty.test.port");
String war = System.getProperty("war.name");
String url = "http://localhost:" + port + "/" + war + "/";

The JAX-RS client can be used to make the REST call and convert the payload to and from a JSON-P representation. To get the JAX-RS client to do the conversion the client needs to have the JsrJsonpProvider class registered with it by calling the register method and providing the Class object for the JsrJsonpProvider class:

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

To call the JAX-RS service using the JAX-RS client you first create a WebTarget object by calling the target method providing the URL. To cause the HTTP request to occur first the request method on WebTarget and then the get method on the returned object need to be called. The get method call is a synchronous call that blocks until a response is received. This call returns a Response object which can be interrogated to determine whether the request was successful:

WebTarget target = client.target(url + "System/properties");
Response response = target.request().get();

The first thing to check is that a 200 response was received. The JUnit assertEquals method can be used for this. The first parameter is the error message that indicates why the test failed. The second parameter is the expected response code, and the third is the actual response code. It is important to associate the expected response code with the second parameter and the actual response with the third parameter. Otherwise, the error messages from JUnit will claim that the actual response is the expected one, which can cause confusion:

assertEquals("Incorrect response code from " + url, 200, response.getStatus());

Check the response body to ensure it returned the right information. Since the client and the server are running on the same machine, it is reasonable to expect that the system properties for the local and remote JVM would be the same. In this case, an assertion is made that the os.name system property for both JVM’s is the same. You could write additional assertions to check for more values:

JsonObject obj = response.readEntity(JsonObject.class);

assertEquals("The system property for the local and remote JVM should match",
             System.getProperty("os.name"),
             obj.getString("os.name"));

To get the service running the Liberty server needs to be correctly configured.

Update the src/main/liberty/config/server.xml file:

<server description="Sample Liberty server">

  <featureManager>
      <feature>jaxrs-2.0</feature>
      <feature>jsonp-1.0</feature>
  </featureManager>

  <httpEndpoint httpPort="${default.http.port}" httpsPort="${default.https.port}"
      id="defaultHttpEndpoint" host="*" />

  <webApplication location="rest.war" contextRoot="${app.context.root}"/>
</server>

The configuration does the following:

  1. Configures the server to support both JAX-RS 2.0 and JSON-P 1.0. This is specified in the featureManager element.

  2. Configures the server to pick up the HTTP port numbers from variables which are then specified in the Maven pom.xml file. This is specified in the httpEndpoint element. Variables use the syntax ${variableName}.

  3. Configures the server to run the produced Web application on a context root specified in the Maven pom.xml file. This is specified in the webApplication element.

The variables being used in the server.xml file are provided by the bootstrapProperties section of the Maven pom.xml:

<bootstrapProperties>
    <default.http.port>${testServerHttpPort}</default.http.port>
    <default.https.port>${testServerHttpsPort}</default.https.port>
    <app.context.root>${warContext}</app.context.root>
</bootstrapProperties>

To rebuild, run the tests, and see that the test passes, run the command: mvn install. The Maven build takes a little longer than before the test existed, but expect to see the following information in the output:

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

Results :

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

To see whether the tests detect a failure, add an assertion that you know fails, or change the existing assertion to a constant value that doesn’t match the os.name system property.

Congratulations! You’re done!

You developed a REST service by using JAX-RS, JSON-P, and Liberty.

Contribute to this guide

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