A Technical Deep Dive on Liberty

duration 80 minutes
New

Prerequisites:

Liberty is a cloud-optimized Java runtime that is fast to start up with a low memory footprint and a development mode, known as dev mode, for quick iteration. With Liberty, adopting the latest open cloud-native Java APIs, like MicroProfile and Jakarta EE, is as simple as adding features to your server configuration. The Liberty zero migration architecture lets you focus on what’s important and not the APIs changing under you.

What you’ll learn

You will learn how to build a RESTful microservice on Liberty with Jakarta EE and MicroProfile. You will use Gradle throughout this exercise to build the microservice and to interact with the running Liberty instance. Then, you’ll build a container image for the microservice. You will also learn how to secure the REST endpoints and use JSON Web Tokens to communicate with the provided system secured microservice.

The microservice that you’ll work with is called inventory. The inventory microservice persists data into a PostgreSQL database.

Inventory microservice

Additional prerequisites

Before you begin, Podman needs to be installed. For installation instructions, refer to the official Podman documentation. You will build and run the microservices in containers.

If you are running Mac or Windows, make sure to start your Podman-managed VM before you proceed.

Getting started

Clone the Git repository:

git clone https://github.com/openliberty/guide-liberty-deep-dive-gradle.git
cd guide-liberty-deep-dive-gradle

The start directory is an empty directory where you will build the inventory service.

The finish directory contains the finished projects of different modules that you will build.

Before you begin, make sure you have all the necessary prerequisites.

Getting started with Liberty and REST

Liberty now offers an easier way to get started with developing your application: the Open Liberty Starter. This tool provides a simple and quick way to get the necessary files to start building an application on Liberty. Through this tool, you can specify your application and project name. You can also choose a build tool from either Maven or Gradle, and pick the Java SE, Jakarta EE, and MicroProfile versions for your application.

In this workshop, the starting project is provided for you or you can use the Open Liberty Starter to create the starting point of the application. Gradle is used as the selected build tool and the application uses of Jakarta EE 10 and MicroProfile 6.

To get started with this tool, see the Getting Started page: https://openliberty.io/start/

On that page, enter the following properties in the Create a starter application wizard.

  • Under Group specify: io.openliberty.deepdive

  • Under Artifact specify: inventory

  • Under Build Tool select: Gradle

  • Under Java SE Version select: your version

  • Under Java EE/Jakarta EE Version select: 10

  • Under MicroProfile Version select: 6.0

Then, click Generate Project, which downloads the starter project as inventory.zip file to the start directory of this project. You can replace the provided inventory.zip file.

Next, extract the inventory.zip file to the guide-liberty-deepdive-gradle/start/inventory directory.

Use Powershell for the following commands:

cd start
Expand-Archive -LiteralPath .\inventory.zip -DestinationPath inventory
cd start
unzip inventory.zip -d inventory

Building the application

This application is configured to be built with Gradle. Every Gradle-configured project contains a settings.gradle and a build.gradle file that defines the project configuration, dependencies, and plug-ins.

settings.gradle

1rootProject.name = 'inventory'

build.gradle

 1plugins {
 2    id 'war'
 3    // tag::libertyGradlePlugin[]
 4    id 'io.openliberty.tools.gradle.Liberty' version '3.5.2'
 5    // end::libertyGradlePlugin[]
 6}
 7
 8version '1.0-SNAPSHOT'
 9group 'io.openliberty.deepdive'
10
11sourceCompatibility = 11
12targetCompatibility = 11
13tasks.withType(JavaCompile) {
14    options.encoding = 'UTF-8'
15}
16
17repositories {
18    mavenCentral()
19}
20
21dependencies {
22    // provided dependencies
23    providedCompile 'jakarta.platform:jakarta.jakartaee-api:10.0.0' 
24    providedCompile 'org.eclipse.microprofile:microprofile:6.0' 
25
26}
27
28clean.dependsOn 'libertyStop'

Your settings.gradle and build.gradle files are located in the start/inventory directory and is configured to include the io.openliberty.tools.gradle.Liberty Liberty Gradle plugin. Using the plug-in, you can install applications into Liberty and manage the server instances.

To begin, open a command-line session and navigate to your application directory.

cd inventory

Build and deploy the inventory microservice to Liberty by running the Gradle libertyRun task:

gradlew libertyRun
./gradlew libertyRun

The gradlew command initiates a Gradle build, during which the target directory is created to store all build-related files.

The libertyRun argument specifies the Liberty run task, which starts a Liberty server instance in the foreground. As part of this phase, a Liberty server runtime is downloaded and installed into the build/wlp directory. Additionally, a server instance is created and configured in the build/wlp/usr/servers/defaultServer directory, and the application is installed into that server by using loose config.

For more information about the Liberty Gradle plug-in, see its GitHub repository.

While the server starts up, various messages display in your command-line session. Wait for the following message, which indicates that the server startup is complete:

[INFO] [AUDIT] CWWKF0011I: The server defaultServer is ready to run a smarter planet.

When you need to stop the server, press CTRL+C in the command-line session where you ran the server, or run the libertyStop goal from the start/inventory directory in another command-line session:

gradlew libertyStop
./gradlew libertyStop

Starting and stopping the Liberty server in the background

Although you can start and stop the server in the foreground by using the Gradle libertyRun task, you can also start and stop the server in the background with the Gradle libertyStart and libertyStop goals:

gradlew libertyStart
gradlew libertyStop
./gradlew libertyStart
./gradlew libertyStop

Updating the server configuration without restarting the server

The Liberty Gradle plug-in includes a dev task that listens for any changes in the project, including application source code or configuration. The Liberty server automatically reloads the configuration without restarting. This goal allows for quicker turnarounds and an improved developer experience.

If the Liberty server is running, stop it and restart it in dev mode by running the libertyDev goal in the start/inventory directory:

gradlew libertyDev
./gradlew libertyDev

After you see the following message, your application server in dev mode is ready:

**************************************************************
*    Liberty is running in dev mode.

Dev mode automatically picks up changes that you make to your application and allows you to run tests by pressing the enter/return key in the active command-line session. When you’re working on your application, rather than rerunning Gradle tasks, press the enter/return key to verify your change.

Developing a RESTful microservice

Now that a basic Liberty application is running, the next step is to create the additional application and resource classes that the application needs. Within these classes, you use Jakarta REST and other MicroProfile and Jakarta APIs.

Create the Inventory class.
src/main/java/io/openliberty/deepdive/rest/Inventory.java

Inventory.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2022, 2023 IBM Corporation and others.
 4 * All rights reserved. This program and the accompanying materials
 5 * are made available under the terms of the Eclipse Public License 2.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-2.0/
 8 *
 9 * SPDX-License-Identifier: EPL-2.0
10 *******************************************************************************/
11// end::copyright[]
12package io.openliberty.deepdive.rest;
13
14import java.util.ArrayList;
15import java.util.Collections;
16import java.util.List;
17
18import io.openliberty.deepdive.rest.model.SystemData;
19import jakarta.enterprise.context.ApplicationScoped;
20
21@ApplicationScoped
22public class Inventory {
23
24    private List<SystemData> systems = Collections.synchronizedList(new ArrayList<>());
25
26    public List<SystemData> getSystems() {
27        return systems;
28    }
29
30    // tag::getSystem[]
31    public SystemData getSystem(String hostname) {
32        for (SystemData s : systems) {
33            if (s.getHostname().equalsIgnoreCase(hostname)) {
34                return s;
35            }
36        }
37        return null;
38    }
39    // end::getSystem[]
40
41    // tag::add[]
42    public void add(String hostname, String osName, String javaVersion, Long heapSize) {
43        systems.add(new SystemData(hostname, osName, javaVersion, heapSize));
44    }
45    // end::add[]
46
47    // tag::update[]
48    public void update(SystemData s) {
49        for (SystemData systemData : systems) {
50            if (systemData.getHostname().equalsIgnoreCase(s.getHostname())) {
51                systemData.setOsName(s.getOsName());
52                systemData.setJavaVersion(s.getJavaVersion());
53                systemData.setHeapSize(s.getHeapSize());
54            }
55        }
56    }
57    // end::update[]
58
59    // tag::removeSystem[]
60    public boolean removeSystem(SystemData s) {
61        return systems.remove(s);
62    }
63    // end::removeSystem[]
64}

This Inventory class stores a record of all systems and their system properties. The getSystem() method within this class retrieves and returns the system data from the system. The add() method enables the addition of a system and its data to the inventory. The update() method enables a system and its data on the inventory to be updated. The removeSystem() method enables the deletion of a system from the inventory.

Create the model subdirectory, then create the SystemData class. The SystemData class is a Plain Old Java Object (POJO) that represents a single inventory entry.

mkdir src\main\java\io\openliberty\deepdive\rest\model
mkdir src/main/java/io/openliberty/deepdive/rest/model
Create the SystemData class.
src/main/java/io/openliberty/deepdive/rest/model/SystemData.java

SystemData.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2022, 2023 IBM Corporation and others.
 4 * All rights reserved. This program and the accompanying materials
 5 * are made available under the terms of the Eclipse Public License 2.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-2.0/
 8 *
 9 * SPDX-License-Identifier: EPL-2.0
10 *******************************************************************************/
11// end::copyright[]
12package io.openliberty.deepdive.rest.model;
13
14public class SystemData {
15
16    private int id;
17    private String hostname;
18    private String osName;
19    private String javaVersion;
20    private Long   heapSize;
21
22    public SystemData() {
23    }
24
25    public SystemData(String hostname, String osName, String javaVer, Long heapSize) {
26        this.hostname = hostname;
27        this.osName = osName;
28        this.javaVersion = javaVer;
29        this.heapSize = heapSize;
30    }
31
32    public int getId() {
33        return id;
34    }
35
36    public void setId(int id) {
37        this.id = id;
38    }
39
40    public String getHostname() {
41        return hostname;
42    }
43
44    public void setHostname(String hostname) {
45        this.hostname = hostname;
46    }
47
48    public String getOsName() {
49        return osName;
50    }
51
52    public void setOsName(String osName) {
53        this.osName = osName;
54    }
55
56    public String getJavaVersion() {
57        return javaVersion;
58    }
59
60    public void setJavaVersion(String javaVersion) {
61        this.javaVersion = javaVersion;
62    }
63
64    public Long getHeapSize() {
65        return heapSize;
66    }
67
68    public void setHeapSize(Long heapSize) {
69        this.heapSize = heapSize;
70    }
71
72    @Override
73    public boolean equals(Object host) {
74      if (host instanceof SystemData) {
75        return hostname.equals(((SystemData) host).getHostname());
76      }
77      return false;
78    }
79}

The SystemData class contains the hostname, operating system name, Java version, and heap size properties. The various methods within this class allow the viewing or editing the properties of each system in the inventory.

Create the SystemResource class.
src/main/java/io/openliberty/deepdive/rest/SystemResource.java

SystemResource.java

  1// tag::copyright[]
  2/*******************************************************************************
  3 * Copyright (c) 2022, 2023 IBM Corporation and others.
  4 * All rights reserved. This program and the accompanying materials
  5 * are made available under the terms of the Eclipse Public License 2.0
  6 * which accompanies this distribution, and is available at
  7 * http://www.eclipse.org/legal/epl-2.0/
  8 *
  9 * SPDX-License-Identifier: EPL-2.0
 10 *******************************************************************************/
 11// end::copyright[]
 12package io.openliberty.deepdive.rest;
 13
 14import java.util.List;
 15
 16import io.openliberty.deepdive.rest.model.SystemData;
 17import jakarta.enterprise.context.ApplicationScoped;
 18import jakarta.inject.Inject;
 19import jakarta.ws.rs.Consumes;
 20import jakarta.ws.rs.DELETE;
 21import jakarta.ws.rs.GET;
 22import jakarta.ws.rs.POST;
 23import jakarta.ws.rs.PUT;
 24import jakarta.ws.rs.Path;
 25import jakarta.ws.rs.PathParam;
 26import jakarta.ws.rs.Produces;
 27import jakarta.ws.rs.QueryParam;
 28import jakarta.ws.rs.core.MediaType;
 29import jakarta.ws.rs.core.Response;
 30
 31@ApplicationScoped
 32//tag::path[]
 33@Path("/systems")
 34//end::path[]
 35public class SystemResource {
 36
 37    @Inject
 38    Inventory inventory;
 39
 40    //tag::getListContents[]
 41    @GET
 42    //end::getListContents[]
 43    @Path("/")
 44    //tag::producesListContents[]
 45    @Produces(MediaType.APPLICATION_JSON)
 46    //end::producesListContents[]
 47    public List<SystemData> listContents() {
 48        //tag::getSystems[]
 49        return inventory.getSystems();
 50        //end::getSystems[]
 51    }
 52
 53    //tag::getGetSystem[]
 54    @GET
 55    //end::getGetSystem[]
 56    @Path("/{hostname}")
 57    //tag::producesGetSystem[]
 58    @Produces(MediaType.APPLICATION_JSON)
 59    //end::producesGetSystem[]
 60    public SystemData getSystem(@PathParam("hostname") String hostname) {
 61        return inventory.getSystem(hostname);
 62    }
 63
 64    @POST
 65    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
 66    @Produces(MediaType.APPLICATION_JSON)
 67    public Response addSystem(
 68        @QueryParam("hostname") String hostname,
 69        @QueryParam("osName") String osName,
 70        @QueryParam("javaVersion") String javaVersion,
 71        @QueryParam("heapSize") Long heapSize) {
 72
 73        SystemData s = inventory.getSystem(hostname);
 74        if (s != null) {
 75            return fail(hostname + " already exists.");
 76        }
 77        inventory.add(hostname, osName, javaVersion, heapSize);
 78        return success(hostname + " was added.");
 79    }
 80
 81    @PUT
 82    @Path("/{hostname}")
 83    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
 84    @Produces(MediaType.APPLICATION_JSON)
 85    public Response updateSystem(
 86        @PathParam("hostname") String hostname,
 87        @QueryParam("osName") String osName,
 88        @QueryParam("javaVersion") String javaVersion,
 89        @QueryParam("heapSize") Long heapSize) {
 90
 91        SystemData s = inventory.getSystem(hostname);
 92        if (s == null) {
 93            return fail(hostname + " does not exists.");
 94        }
 95        s.setOsName(osName);
 96        s.setJavaVersion(javaVersion);
 97        s.setHeapSize(heapSize);
 98        inventory.update(s);
 99        return success(hostname + " was updated.");
100    }
101
102    @DELETE
103    @Path("/{hostname}")
104    @Produces(MediaType.APPLICATION_JSON)
105    public Response removeSystem(@PathParam("hostname") String hostname) {
106        SystemData s = inventory.getSystem(hostname);
107        if (s != null) {
108            inventory.removeSystem(s);
109            return success(hostname + " was removed.");
110        } else {
111            return fail(hostname + " does not exists.");
112        }
113    }
114
115    @POST
116    @Path("/client/{hostname}")
117    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
118    @Produces(MediaType.APPLICATION_JSON)
119    public Response addSystemClient(@PathParam("hostname") String hostname) {
120        return fail("This api is not implemented yet.");
121    }
122
123    private Response success(String message) {
124        return Response.ok("{ \"ok\" : \"" + message + "\" }").build();
125    }
126
127    private Response fail(String message) {
128        return Response.status(Response.Status.BAD_REQUEST)
129                       .entity("{ \"error\" : \"" + message + "\" }")
130                       .build();
131    }
132}

RestApplication.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2022, 2023 IBM Corporation and others.
 4 * All rights reserved. This program and the accompanying materials
 5 * are made available under the terms of the Eclipse Public License 2.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-2.0/
 8 *
 9 * SPDX-License-Identifier: EPL-2.0
10 *******************************************************************************/
11// end::copyright[]
12package io.openliberty.deepdive.rest;
13
14import jakarta.ws.rs.ApplicationPath;
15import jakarta.ws.rs.core.Application;
16
17//tag::applicationPath[]
18@ApplicationPath("/api")
19//end::applicationPath[]
20public class RestApplication extends Application {
21}

In Jakarta RESTful Web Services, a single class like the SystemResource.java class must 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 efficient to have a single class handle multiple different resources, but keeping a clean separation between types of resources helps with maintainability.

The @Path annotation on this class indicates that this resource responds to the /systems path in the RESTful application. The @ApplicationPath annotation in the RestApplication class, together with the @Path annotation in the SystemResource class, indicates that this resource is available at the /api/systems path.

The Jakarta RESTful Web Services API maps the HTTP methods on the URL to the methods of the class by using annotations. This application uses the GET annotation to map an HTTP GET request to the /api/systems path.

The @GET annotation on the listContents method indicates that the method is to be called for the HTTP GET method. The @Produces annotation indicates the format of the content that is returned. The value of the @Produces annotation is specified in the HTTP Content-Type response header. For this application, a JSON structure is returned for these Get methods. The Content-Type for a JSON response is application/json with MediaType.APPLICATION_JSON instead of the String content type. Using a constant such as MediaType.APPLICATION_JSON is better as in case of a spelling error, a compile failure occurs.

The Jakarta RESTful Web Services API supports a number of ways to marshal JSON. The Jakarta RESTful Web Services specification mandates JSON-Binding (JSON-B). The method body returns the result of inventory.getSystems(). Because the method is annotated with @Produces(MediaType.APPLICATION_JSON), the Jakarta RESTful Web Services API uses JSON-B to automatically convert the returned object to JSON data in the HTTP response.

Running the application

Because you started the Liberty server in dev mode at the beginning of this exercise, all the changes were automatically picked up.

Check out the service that you created at the http://localhost:9080/inventory/api/systems URL. If successful, it returns [] to you.

Documenting APIs

Next, you will investigate how to document and filter RESTful APIs from annotations, POJOs, and static OpenAPI files by using MicroProfile OpenAPI.

The OpenAPI specification, previously known as the Swagger specification, defines a standard interface for documenting and exposing RESTful APIs. This specification allows both humans and computers to understand or process the functionalities of services without requiring direct access to underlying source code or documentation. The MicroProfile OpenAPI specification provides a set of Java interfaces and programming models that allow Java developers to natively produce OpenAPI v3 documents from their RESTful applications.

build.gradle

 1plugins {
 2    id 'war'
 3    id 'io.openliberty.tools.gradle.Liberty' version '3.5.2'
 4}
 5
 6version '1.0-SNAPSHOT'
 7group 'io.openliberty.deepdive'
 8
 9sourceCompatibility = 11
10targetCompatibility = 11
11tasks.withType(JavaCompile) {
12    options.encoding = 'UTF-8'
13}
14
15repositories {
16    mavenCentral()
17}
18
19dependencies {
20    // provided dependencies
21    providedCompile 'jakarta.platform:jakarta.jakartaee-api:10.0.0' 
22    // tag::mp5[]
23    providedCompile 'org.eclipse.microprofile:microprofile:6.0' 
24    // end::mp5[]
25
26}
27
28clean.dependsOn 'libertyStop'

server.xml

 1<?xml version="1.0" encoding="UTF-8"?>
 2<server description="inventory">
 3
 4    <!-- Enable features -->
 5    <featureManager>
 6        <feature>jakartaee-10.0</feature>
 7        <!-- tag::mp5[] -->
 8        <feature>microProfile-6.0</feature>
 9        <!-- end::mp5[] -->
10    </featureManager>
11
12    <!-- To access this server from a remote client add a host attribute to the following element, e.g. host="*" -->
13    <httpEndpoint id="defaultHttpEndpoint"
14                  httpPort="9080"
15                  httpsPort="9443" />
16
17    <!-- Automatically expand WAR files and EAR files -->
18    <applicationManager autoExpand="true"/>
19
20    <!-- Default SSL configuration enables trust for default certificates from the Java runtime -->
21    <ssl id="defaultSSLConfig" trustDefaultCerts="true" />
22</server>

The MicroProfile OpenAPI API is included in the microProfile dependency that is specified in your build.gradle file. The microProfile feature that includes the mpOpenAPI feature is also enabled in the server.xml file.

Generating the OpenAPI document

Because the Jakarta RESTful Web Services framework handles basic API generation for Jakarta RESTful Web Services annotations, a skeleton OpenAPI tree can be generated from the existing inventory service. You can use this tree as a starting point and augment it with annotations and code to produce a complete OpenAPI document.

To see the generated OpenAPI tree, you can either visit the http://localhost:9080/openapi URL or visit the http://localhost:9080/openapi/ui URL for a more interactive view of the APIs. Click the interactive UI link on the welcome page. Within this UI, you can view each of the endpoints that are available in your application and any schemas. Each endpoint is color coordinated to easily identify the type of each request (for example GET, POST, PUT, DELETE, etc.). Clicking each endpoint within this UI enables you to view further details of each endpoint’s parameters and responses. This UI is used for the remainder of this workshop to view and test the application endpoints.

Augmenting the existing Jakarta RESTful Web Services annotations with OpenAPI annotations

Because all Jakarta RESTful Web Services annotations are processed by default, you can augment the existing code with OpenAPI annotations without needing to rewrite portions of the OpenAPI document that are already covered by the Jakarta RESTful Web Services framework.

Replace the SystemResources class.
src/main/java/io/openliberty/deepdive/rest/SystemResource.java

SystemResource.java

  1// tag::copyright[]
  2/*******************************************************************************
  3 * Copyright (c) 2022, 2023 IBM Corporation and others.
  4 * All rights reserved. This program and the accompanying materials
  5 * are made available under the terms of the Eclipse Public License 2.0
  6 * which accompanies this distribution, and is available at
  7 * http://www.eclipse.org/legal/epl-2.0/
  8 *
  9 * SPDX-License-Identifier: EPL-2.0
 10 *******************************************************************************/
 11// end::copyright[]
 12package io.openliberty.deepdive.rest;
 13
 14import java.util.List;
 15
 16import org.eclipse.microprofile.openapi.annotations.Operation;
 17import org.eclipse.microprofile.openapi.annotations.enums.ParameterIn;
 18import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
 19import org.eclipse.microprofile.openapi.annotations.media.Schema;
 20import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
 21import org.eclipse.microprofile.openapi.annotations.parameters.Parameters;
 22import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
 23import org.eclipse.microprofile.openapi.annotations.responses.APIResponseSchema;
 24import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
 25
 26import io.openliberty.deepdive.rest.model.SystemData;
 27import jakarta.enterprise.context.ApplicationScoped;
 28import jakarta.inject.Inject;
 29import jakarta.ws.rs.Consumes;
 30import jakarta.ws.rs.DELETE;
 31import jakarta.ws.rs.GET;
 32import jakarta.ws.rs.POST;
 33import jakarta.ws.rs.PUT;
 34import jakarta.ws.rs.Path;
 35import jakarta.ws.rs.PathParam;
 36import jakarta.ws.rs.Produces;
 37import jakarta.ws.rs.QueryParam;
 38import jakarta.ws.rs.core.MediaType;
 39import jakarta.ws.rs.core.Response;
 40
 41@ApplicationScoped
 42@Path("/systems")
 43public class SystemResource {
 44
 45    @Inject
 46    Inventory inventory;
 47
 48    // tag::listContents[]
 49    @GET
 50    @Path("/")
 51    @Produces(MediaType.APPLICATION_JSON)
 52    // tag::listContentsAPIResponseSchema[]
 53    @APIResponseSchema(value = SystemData.class,
 54        responseDescription = "A list of system data stored within the inventory.",
 55        responseCode = "200")
 56    // end::listContentsAPIResponseSchema[]
 57    // tag::listContentsOperation[]
 58    @Operation(
 59        summary = "List contents.",
 60        description = "Returns the currently stored system data in the inventory.",
 61        operationId = "listContents")
 62    // end::listContentsOperation[]
 63    public List<SystemData> listContents() {
 64        return inventory.getSystems();
 65    }
 66    // end::listContents[]
 67
 68    // tag::getSystem[]
 69    @GET
 70    @Path("/{hostname}")
 71    @Produces(MediaType.APPLICATION_JSON)
 72    // tag::getSystemAPIResponseSchema[]
 73    @APIResponseSchema(value = SystemData.class,
 74        responseDescription = "System data of a particular host.",
 75        responseCode = "200")
 76    // end::getSystemAPIResponseSchema[]
 77    // tag::getSystemOperation[]
 78    @Operation(
 79        summary = "Get System",
 80        description = "Retrieves and returns the system data from the system "
 81                      + "service running on the particular host.",
 82        operationId = "getSystem")
 83    // end::getSystemOperation[]
 84    public SystemData getSystem(
 85        // tag::getSystemParameter[]
 86        @Parameter(
 87            name = "hostname", in = ParameterIn.PATH,
 88            description = "The hostname of the system",
 89            required = true, example = "localhost",
 90            schema = @Schema(type = SchemaType.STRING))
 91        // end::getSystemParameter[]
 92        @PathParam("hostname") String hostname) {
 93        return inventory.getSystem(hostname);
 94    }
 95    // end::getSystem[]
 96
 97    // tag::addSystem[]
 98    @POST
 99    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
100    @Produces(MediaType.APPLICATION_JSON)
101    // tag::addSystemAPIResponses[]
102    @APIResponses(value = {
103        // tag::addSystemAPIResponse[]
104        @APIResponse(responseCode = "200",
105            description = "Successfully added system to inventory"),
106        @APIResponse(responseCode = "400",
107            description = "Unable to add system to inventory")
108        // end::addSystemAPIResponse[]
109    })
110    // end::addSystemAPIResponses[]
111    // tag::addSystemParameters[]
112    @Parameters(value = {
113        // tag::addSystemParameter[]
114        @Parameter(
115            name = "hostname", in = ParameterIn.QUERY,
116            description = "The hostname of the system",
117            required = true, example = "localhost",
118            schema = @Schema(type = SchemaType.STRING)),
119        @Parameter(
120            name = "osName", in = ParameterIn.QUERY,
121            description = "The operating system of the system",
122            required = true, example = "linux",
123            schema = @Schema(type = SchemaType.STRING)),
124        @Parameter(
125            name = "javaVersion", in = ParameterIn.QUERY,
126            description = "The Java version of the system",
127            required = true, example = "11",
128            schema = @Schema(type = SchemaType.STRING)),
129        @Parameter(
130            name = "heapSize", in = ParameterIn.QUERY,
131            description = "The heap size of the system",
132            required = true, example = "1048576",
133            schema = @Schema(type = SchemaType.NUMBER)),
134        // end::addSystemParameter[]
135    })
136    // end::addSystemParameters[]
137    // tag::addSystemOperation[]
138    @Operation(
139        summary = "Add system",
140        description = "Add a system and its data to the inventory.",
141        operationId = "addSystem"
142    )
143    // end::addSystemOperation[]
144    public Response addSystem(
145        @QueryParam("hostname") String hostname,
146        @QueryParam("osName") String osName,
147        @QueryParam("javaVersion") String javaVersion,
148        @QueryParam("heapSize") Long heapSize) {
149
150        SystemData s = inventory.getSystem(hostname);
151        if (s != null) {
152            return fail(hostname + " already exists.");
153        }
154        inventory.add(hostname, osName, javaVersion, heapSize);
155        return success(hostname + " was added.");
156    }
157    // end::addSystem[]
158
159    // tag::updateSystem[]
160    @PUT
161    @Path("/{hostname}")
162    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
163    @Produces(MediaType.APPLICATION_JSON)
164    // tag::updateSystemAPIResponses[]
165    @APIResponses(value = {
166        // tag::updateSystemAPIResponse[]
167        @APIResponse(responseCode = "200",
168            description = "Successfully updated system"),
169        @APIResponse(responseCode = "400",
170            description =
171                "Unable to update because the system does not exist in the inventory.")
172        // end::updateSystemAPIResponse[]
173    })
174    // end::updateSystemAPIResponses[]
175    // tag::updateSystemParameters[]
176    @Parameters(value = {
177        // tag::updateSystemParameter[]
178        @Parameter(
179            name = "hostname", in = ParameterIn.PATH,
180            description = "The hostname of the system",
181            required = true, example = "localhost",
182            schema = @Schema(type = SchemaType.STRING)),
183        @Parameter(
184            name = "osName", in = ParameterIn.QUERY,
185            description = "The operating system of the system",
186            required = true, example = "linux",
187            schema = @Schema(type = SchemaType.STRING)),
188        @Parameter(
189            name = "javaVersion", in = ParameterIn.QUERY,
190            description = "The Java version of the system",
191            required = true, example = "11",
192            schema = @Schema(type = SchemaType.STRING)),
193        @Parameter(
194            name = "heapSize", in = ParameterIn.QUERY,
195            description = "The heap size of the system",
196            required = true, example = "1048576",
197            schema = @Schema(type = SchemaType.NUMBER)),
198        // end::updateSystemParameter[]
199    })
200    // end::updateSystemParameters[]
201    // tag::updateSystemOperation[]
202    @Operation(
203        summary = "Update system",
204        description = "Update a system and its data on the inventory.",
205        operationId = "updateSystem"
206    )
207    // end::updateSystemOperation[]
208    public Response updateSystem(
209        @PathParam("hostname") String hostname,
210        @QueryParam("osName") String osName,
211        @QueryParam("javaVersion") String javaVersion,
212        @QueryParam("heapSize") Long heapSize) {
213
214        SystemData s = inventory.getSystem(hostname);
215        if (s == null) {
216            return fail(hostname + " does not exists.");
217        }
218        s.setOsName(osName);
219        s.setJavaVersion(javaVersion);
220        s.setHeapSize(heapSize);
221        inventory.update(s);
222        return success(hostname + " was updated.");
223    }
224    // end::updateSystem[]
225
226    // tag::removeSystem[]
227    @DELETE
228    @Path("/{hostname}")
229    @Produces(MediaType.APPLICATION_JSON)
230    // tag::removeSystemAPIResponses[]
231    @APIResponses(value = {
232        // tag::removeSystemAPIResponse[]
233        @APIResponse(responseCode = "200",
234            description = "Successfully deleted the system from inventory"),
235        @APIResponse(responseCode = "400",
236            description =
237                "Unable to delete because the system does not exist in the inventory")
238        // end::removeSystemAPIResponse[]
239    })
240    // end::removeSystemAPIResponses[]
241    // tag::removeSystemParameter[]
242    @Parameter(
243        name = "hostname", in = ParameterIn.PATH,
244        description = "The hostname of the system",
245        required = true, example = "localhost",
246        schema = @Schema(type = SchemaType.STRING)
247    )
248    // end::removeSystemParameter[]
249    // tag::removeSystemOperation[]
250    @Operation(
251        summary = "Remove system",
252        description = "Removes a system from the inventory.",
253        operationId = "removeSystem"
254    )
255    // end::removeSystemOperation[]
256    public Response removeSystem(@PathParam("hostname") String hostname) {
257        SystemData s = inventory.getSystem(hostname);
258        if (s != null) {
259            inventory.removeSystem(s);
260            return success(hostname + " was removed.");
261        } else {
262            return fail(hostname + " does not exists.");
263        }
264    }
265    // end::removeSystem[]
266
267    // tag::addSystemClient[]
268    @POST
269    @Path("/client/{hostname}")
270    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
271    @Produces(MediaType.APPLICATION_JSON)
272    // tag::addSystemClientAPIResponses[]
273    @APIResponses(value = {
274        // tag::addSystemClientAPIResponse[]
275        @APIResponse(responseCode = "200",
276            description = "Successfully added system client"),
277        @APIResponse(responseCode = "400",
278            description = "Unable to add system client")
279        // end::addSystemClientAPIResponse[]
280    })
281    // end::addSystemClientAPIResponses[]
282    // tag::addSystemClientParameter[]
283    @Parameter(
284        name = "hostname", in = ParameterIn.PATH,
285        description = "The hostname of the system",
286        required = true, example = "localhost",
287        schema = @Schema(type = SchemaType.STRING)
288    )
289    // end::addSystemClientParameter[]
290    // tag::addSystemClientOperation[]
291    @Operation(
292        summary = "Add system client",
293        description = "This adds a system client.",
294        operationId = "addSystemClient"
295    )
296    // end::addSystemClientOperation[]
297    public Response addSystemClient(@PathParam("hostname") String hostname) {
298        return fail("This api is not implemented yet.");
299    }
300    // end::addSystemClient[]
301
302    private Response success(String message) {
303        return Response.ok("{ \"ok\" : \"" + message + "\" }").build();
304    }
305
306    private Response fail(String message) {
307        return Response.status(Response.Status.BAD_REQUEST)
308                       .entity("{ \"error\" : \"" + message + "\" }")
309                       .build();
310    }
311}

Add OpenAPI @APIResponseSchema, @APIResponses, @APIResponse, @Parameters, @Parameter, and @Operation annotations to the REST methods, listContents(), getSystem(), addSystem(), updateSystem(), removeSystem(), and addSystemClient().

Note, the @Parameter annotation can be placed either inline or outline. Examples of both are provided within this workshop.

Many OpenAPI annotations are available and can be used according to what’s best for your application and its classes. You can find all the annotations in the MicroProfile OpenAPI specification.

Because the Liberty server was started in dev mode at the beginning of this exercise, your changes were automatically picked up. Go to the http://localhost:9080/openapi URL to see the updated endpoint descriptions. The endpoints at which your REST methods are served now more meaningful:

---
openapi: 3.0.3
info:
  title: Generated API
  version: "1.0"
servers:
- url: http://localhost:9080/inventory
- url: https://localhost:9443/inventory
paths:
  /api/systems:
    get:
      summary: List contents.
      description: Returns the currently stored host:properties pairs in the inventory.
      operationId: listContents
      responses:
        "200":
          description: Returns the currently stored host:properties pairs in the inventory.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SystemData'
...

You can also visit the http://localhost:9080/openapi/ui URL to see each endpoint’s updated description. Click each of the icons within the UI to see the updated descriptions for each of the endpoints.

Augmenting POJOs with OpenAPI annotations

OpenAPI annotations can also be added to POJOs to describe what they represent. Currently, the OpenAPI document doesn’t have a meaningful description of the SystemData POJO so it’s difficult to tell exactly what this POJO is used for. To describe the SystemData POJO in more detail, augment the SystemData.java file with some OpenAPI annotations.

Replace the SystemData class.
src/main/java/io/openliberty/deepdive/rest/model/SystemData.java

SystemData.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2022, 2023 IBM Corporation and others.
 4 * All rights reserved. This program and the accompanying materials
 5 * are made available under the terms of the Eclipse Public License 2.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-2.0/
 8 *
 9 * SPDX-License-Identifier: EPL-2.0
10 *******************************************************************************/
11// end::copyright[]
12package io.openliberty.deepdive.rest.model;
13
14import org.eclipse.microprofile.openapi.annotations.media.Schema;
15//tag::SystemDataSchema[]
16@Schema(name = "SystemData",
17        description = "POJO that represents a single inventory entry.")
18public class SystemData {
19//end::SystemDataSchema[]
20
21    private int id;
22
23    //tag::hostnameSchema[]
24    @Schema(required = true)
25    private String hostname;
26    //end::hostnameSchema[]
27
28    private String osName;
29    private String javaVersion;
30    private Long   heapSize;
31
32    public SystemData() {
33    }
34
35    public SystemData(String hostname, String osName, String javaVer, Long heapSize) {
36        this.hostname = hostname;
37        this.osName = osName;
38        this.javaVersion = javaVer;
39        this.heapSize = heapSize;
40    }
41
42    public int getId() {
43        return id;
44    }
45
46    public void setId(int id) {
47        this.id = id;
48    }
49
50    public String getHostname() {
51        return hostname;
52    }
53
54    public void setHostname(String hostname) {
55        this.hostname = hostname;
56    }
57
58    public String getOsName() {
59        return osName;
60    }
61
62    public void setOsName(String osName) {
63        this.osName = osName;
64    }
65
66    public String getJavaVersion() {
67        return javaVersion;
68    }
69
70    public void setJavaVersion(String javaVersion) {
71        this.javaVersion = javaVersion;
72    }
73
74    public Long getHeapSize() {
75        return heapSize;
76    }
77
78    public void setHeapSize(Long heapSize) {
79        this.heapSize = heapSize;
80    }
81
82    @Override
83    public boolean equals(Object host) {
84      if (host instanceof SystemData) {
85        return hostname.equals(((SystemData) host).getHostname());
86      }
87      return false;
88    }
89}

Add OpenAPI @Schema annotations to the SystemData class and the hostname variable.

Refresh the http://localhost:9080/openapi URL to see the updated OpenAPI tree. You should see much more meaningful data for the Schema:

components:
  schemas:
    SystemData:
      description: POJO that represents a single inventory entry.
      required:
      - hostname
      - properties
      type: object
      properties:
        hostname:
          type: string
        properties:
          type: object

Again, you can also view this at the http://localhost:9080/openapi/ui URL. Scroll down in the UI to the schemas section and open up the SystemData schema icon.

You can also use this UI to try out the various endpoints. In the UI, head to the POST request /api/systems. This endpoint enables you to create a system. Once you’ve opened this icon up, click the Try it out button. Now enter appropriate values for each of the required parameters and click the Execute button.

You can verify that this system was created by testing the /api/systems GET request that returns the currently stored system data in the inventory. Execute this request in the UI, then in the response body you should see your system and its data listed.

You can follow these same steps for updating and deleting systems: visiting the corresponding endpoint in the UI, executing the endpoint, and then verifying the result by using the /api/systems GET request endpoint.

You can learn more about MicroProfile OpenAPI from the Documenting RESTful APIs guide.

Configuring the microservice

Next, you can externalize your Liberty server configuration and inject configuration for your microservice by using MicroProfile Config.

Enabling configurable ports and context root

So far, you used hardcoded values to set the HTTP and HTTPS ports and the context root for the Liberty server. These configurations can be externalized so you can easily change their values when you want to deploy your microservice by different ports and context root.

Replace the server.xml file.
src/main/liberty/config/server.xml

server.xml

 1<?xml version="1.0" encoding="UTF-8"?>
 2<server description="inventory">
 3
 4    <!-- Enable features -->
 5    <featureManager>
 6        <feature>jakartaee-10.0</feature>
 7        <feature>microProfile-6.0</feature>
 8    </featureManager>
 9
10    <!-- tag::httpPortVariable[] -->
11    <variable name="default.http.port" defaultValue="9080" />
12    <!-- end::httpPortVariable[] -->
13    <!-- tag::httpsPortVariable[] -->
14    <variable name="default.https.port" defaultValue="9443" />
15    <!-- end::httpsPortVariable[] -->
16    <!-- tag::contextRootVariable[] -->
17    <variable name="default.context.root" defaultValue="/inventory" />
18    <!-- end::contextRootVariable[] -->
19
20    <!-- To access this server from a remote client,
21         add a host attribute to the following element, e.g. host="*" -->
22    <!-- tag::editedHttpEndpoint[] -->
23    <httpEndpoint id="defaultHttpEndpoint"
24                  httpPort="${default.http.port}" 
25                  httpsPort="${default.https.port}" />
26    <!-- end::editedHttpEndpoint[] -->
27    
28    <!-- Automatically expand WAR files and EAR files -->
29    <applicationManager autoExpand="true"/>
30
31    <!-- Configures the application on a specified context root -->
32    <!-- tag::editedContextRoot[] -->
33    <webApplication contextRoot="${default.context.root}" 
34                    location="inventory.war" /> 
35    <!-- end::editedContextRoot[] -->
36
37    <!-- Default SSL configuration enables trust for default certificates from the Java runtime -->
38    <ssl id="defaultSSLConfig" trustDefaultCerts="true" />
39</server>

Add variables for the HTTP port, HTTPS port, and the context root to the server.xml file. Change the httpEndpoint element to reflect the new default.http.port and default.http.port variables and change the contextRoot to use the new default.context.root variable too.

Replace the build.gradle file.
build.gradle

build.gradle

 1plugins {
 2    id 'war'
 3    // tag::libertyGradlePlugin[]
 4    id 'io.openliberty.tools.gradle.Liberty' version '3.5.2'
 5    // end::libertyGradlePlugin[]
 6}
 7
 8version '1.0-SNAPSHOT'
 9group 'io.openliberty.deepdive'
10
11sourceCompatibility = 11
12targetCompatibility = 11
13tasks.withType(JavaCompile) {
14    options.encoding = 'UTF-8'
15}
16
17repositories {
18    mavenCentral()
19}
20
21dependencies {
22    // provided dependencies
23    providedCompile 'jakarta.platform:jakarta.jakartaee-api:10.0.0'
24    providedCompile 'org.eclipse.microprofile:microprofile:6.0'
25}
26
27// tag::war[]
28war {
29    archiveVersion = ''
30}
31// end::war[]
32
33// tag::ext[]
34ext  {
35    // tag::httpPort[]
36    liberty.server.var.'default.http.port' = '9081'
37    // end::httpPort[]
38    // tag::httpsPort[]
39    liberty.server.var.'default.https.port' = '9445'
40    // end::httpsPort[]
41    // tag::contextRoot[]
42    liberty.server.var.'default.context.root' = '/trial'
43    // end::contextRoot[]
44}
45// end::ext[]
46
47clean.dependsOn 'libertyStop'

Set the archiveVersion property to an empty string for the war task and add properties for the HTTP port, HTTPS port, and the context root to the build.gradle file.

  • liberty.server.var.'default.http.port' to 9081

  • liberty.server.var.'default.https.port' to 9445

  • liberty.server.var.'default.context.root' to /trial

Because you are using dev mode, these changes are automatically picked up by the server. After you see the following message, your application server in dev mode is ready again:

**************************************************************
*    Liberty is running in dev mode.

Now, you can access the application by the http://localhost:9081/trial/api/systems URL. Alternatively, for the updated OpenAPI UI, use the following URL http://localhost:9081/openapi/ui/.

build.gradle

 1plugins {
 2    id 'war'
 3    // tag::libertyGradlePlugin[]
 4    id 'io.openliberty.tools.gradle.Liberty' version '3.5.2'
 5    // end::libertyGradlePlugin[]
 6}
 7
 8version '1.0-SNAPSHOT'
 9group 'io.openliberty.deepdive'
10
11sourceCompatibility = 11
12targetCompatibility = 11
13tasks.withType(JavaCompile) {
14    options.encoding = 'UTF-8'
15}
16
17repositories {
18    mavenCentral()
19}
20
21dependencies {
22    // provided dependencies
23    providedCompile 'jakarta.platform:jakarta.jakartaee-api:10.0.0'
24    providedCompile 'org.eclipse.microprofile:microprofile:6.0'
25}
26
27// tag::war[]
28war {
29    archiveVersion = ''
30}
31// end::war[]
32
33// tag::ext[]
34ext  {
35    // tag::httpPort[]
36    liberty.server.var.'default.http.port' = '9080'
37    // end::httpPort[]
38    // tag::httpsPort[]
39    liberty.server.var.'default.https.port' = '9443'
40    // end::httpsPort[]
41    // tag::contextRoot[]
42    liberty.server.var.'default.context.root' = '/inventory'
43    // end::contextRoot[]
44}
45// end::ext[]
46
47clean.dependsOn 'libertyStop'

When you are finished trying out changing this configuration, change the variables back to their original values.

  • update liberty.server.var.'default.http.port' to 9080

  • update liberty.server.var.'default.https.port' to 9443

  • update liberty.server.var.'default.context.root' to /inventory

Wait until you see the following message:

**************************************************************
*    Liberty is running in dev mode.

Injecting static configuration

You can now explore how to use MicroProfile’s Config API to inject static configuration into your microservice.

The MicroProfile Config API is included in the MicroProfile dependency that is specified in your build.gradle file. Look for the dependency with the microprofile artifact ID. This dependency provides a library that allows the use of the MicroProfile Config API. The microProfile feature is also enabled in the server.xml file.

First, you need to edit the SystemResource class to inject static configuration into the CLIENT_PORT variable.

Replace the SystemResource class.
src/main/java/io/openliberty/deepdive/rest/SystemResource.java

SystemResource.java

  1// tag::copyright[]
  2/*******************************************************************************
  3 * Copyright (c) 2022, 2023 IBM Corporation and others.
  4 * All rights reserved. This program and the accompanying materials
  5 * are made available under the terms of the Eclipse Public License 2.0
  6 * which accompanies this distribution, and is available at
  7 * http://www.eclipse.org/legal/epl-2.0/
  8 *
  9 * SPDX-License-Identifier: EPL-2.0
 10 *******************************************************************************/
 11// end::copyright[]
 12package io.openliberty.deepdive.rest;
 13
 14import java.util.List;
 15
 16import org.eclipse.microprofile.openapi.annotations.Operation;
 17import org.eclipse.microprofile.openapi.annotations.enums.ParameterIn;
 18import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
 19import org.eclipse.microprofile.openapi.annotations.media.Schema;
 20import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
 21import org.eclipse.microprofile.openapi.annotations.parameters.Parameters;
 22import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
 23import org.eclipse.microprofile.openapi.annotations.responses.APIResponseSchema;
 24import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
 25
 26import org.eclipse.microprofile.config.inject.ConfigProperty;
 27
 28import io.openliberty.deepdive.rest.model.SystemData;
 29import jakarta.enterprise.context.ApplicationScoped;
 30import jakarta.inject.Inject;
 31import jakarta.ws.rs.Consumes;
 32import jakarta.ws.rs.DELETE;
 33import jakarta.ws.rs.GET;
 34import jakarta.ws.rs.POST;
 35import jakarta.ws.rs.PUT;
 36import jakarta.ws.rs.Path;
 37import jakarta.ws.rs.PathParam;
 38import jakarta.ws.rs.Produces;
 39import jakarta.ws.rs.QueryParam;
 40import jakarta.ws.rs.core.MediaType;
 41import jakarta.ws.rs.core.Response;
 42
 43@ApplicationScoped
 44@Path("/systems")
 45public class SystemResource {
 46
 47    @Inject
 48    Inventory inventory;
 49
 50    // tag::inject[]
 51    @Inject
 52    // end::inject[]
 53    // tag::configProperty[]
 54    @ConfigProperty(name = "client.https.port")
 55    // end::configProperty[]
 56    String CLIENT_PORT;
 57
 58
 59    @GET
 60    @Path("/")
 61    @Produces(MediaType.APPLICATION_JSON)
 62    @APIResponseSchema(value = SystemData.class,
 63        responseDescription = "A list of system data stored within the inventory.",
 64        responseCode = "200")
 65    @Operation(
 66        summary = "List contents.",
 67        description = "Returns the currently stored system data in the inventory.",
 68        operationId = "listContents")
 69    public List<SystemData> listContents() {
 70        return inventory.getSystems();
 71    }
 72
 73    @GET
 74    @Path("/{hostname}")
 75    @Produces(MediaType.APPLICATION_JSON)
 76    @APIResponseSchema(value = SystemData.class,
 77        responseDescription = "System data of a particular host.",
 78        responseCode = "200")
 79    @Operation(
 80        summary = "Get System",
 81        description = "Retrieves and returns the system data from the system "
 82        + "service running on the particular host.",
 83        operationId = "getSystem"
 84    )
 85    public SystemData getSystem(
 86        @Parameter(
 87            name = "hostname", in = ParameterIn.PATH,
 88            description = "The hostname of the system",
 89            required = true, example = "localhost",
 90            schema = @Schema(type = SchemaType.STRING)
 91        )
 92        @PathParam("hostname") String hostname) {
 93        return inventory.getSystem(hostname);
 94    }
 95
 96    @POST
 97    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
 98    @Produces(MediaType.APPLICATION_JSON)
 99    @APIResponses(value = {
100        @APIResponse(responseCode = "200",
101            description = "Successfully added system to inventory"),
102        @APIResponse(responseCode = "400",
103            description = "Unable to add system to inventory")
104    })
105    @Parameters(value = {
106        @Parameter(
107            name = "hostname", in = ParameterIn.QUERY,
108            description = "The hostname of the system",
109            required = true, example = "localhost",
110            schema = @Schema(type = SchemaType.STRING)),
111        @Parameter(
112            name = "osName", in = ParameterIn.QUERY,
113            description = "The operating system of the system",
114            required = true, example = "linux",
115            schema = @Schema(type = SchemaType.STRING)),
116        @Parameter(
117            name = "javaVersion", in = ParameterIn.QUERY,
118            description = "The Java version of the system",
119            required = true, example = "11",
120            schema = @Schema(type = SchemaType.STRING)),
121        @Parameter(
122            name = "heapSize", in = ParameterIn.QUERY,
123            description = "The heap size of the system",
124            required = true, example = "1048576",
125            schema = @Schema(type = SchemaType.NUMBER)),
126    })
127    @Operation(
128        summary = "Add system",
129        description = "Add a system and its data to the inventory.",
130        operationId = "addSystem"
131    )
132    public Response addSystem(
133        @QueryParam("hostname") String hostname,
134        @QueryParam("osName") String osName,
135        @QueryParam("javaVersion") String javaVersion,
136        @QueryParam("heapSize") Long heapSize) {
137
138        SystemData s = inventory.getSystem(hostname);
139        if (s != null) {
140            return fail(hostname + " already exists.");
141        }
142        inventory.add(hostname, osName, javaVersion, heapSize);
143        return success(hostname + " was added.");
144    }
145
146    @PUT
147    @Path("/{hostname}")
148    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
149    @Produces(MediaType.APPLICATION_JSON)
150    @APIResponses(value = {
151        @APIResponse(responseCode = "200",
152            description = "Successfully updated system"),
153        @APIResponse(responseCode = "400",
154           description =
155               "Unable to update because the system does not exist in the inventory.")
156    })
157    @Parameters(value = {
158        @Parameter(
159            name = "hostname", in = ParameterIn.PATH,
160            description = "The hostname of the system",
161            required = true, example = "localhost",
162            schema = @Schema(type = SchemaType.STRING)),
163        @Parameter(
164            name = "osName", in = ParameterIn.QUERY,
165            description = "The operating system of the system",
166            required = true, example = "linux",
167            schema = @Schema(type = SchemaType.STRING)),
168        @Parameter(
169            name = "javaVersion", in = ParameterIn.QUERY,
170            description = "The Java version of the system",
171            required = true, example = "11",
172            schema = @Schema(type = SchemaType.STRING)),
173        @Parameter(
174            name = "heapSize", in = ParameterIn.QUERY,
175            description = "The heap size of the system",
176            required = true, example = "1048576",
177            schema = @Schema(type = SchemaType.NUMBER)),
178    })
179    @Operation(
180        summary = "Update system",
181        description = "Update a system and its data on the inventory.",
182        operationId = "updateSystem"
183    )
184    public Response updateSystem(
185        @PathParam("hostname") String hostname,
186        @QueryParam("osName") String osName,
187        @QueryParam("javaVersion") String javaVersion,
188        @QueryParam("heapSize") Long heapSize) {
189
190        SystemData s = inventory.getSystem(hostname);
191        if (s == null) {
192            return fail(hostname + " does not exists.");
193        }
194        s.setOsName(osName);
195        s.setJavaVersion(javaVersion);
196        s.setHeapSize(heapSize);
197        inventory.update(s);
198        return success(hostname + " was updated.");
199    }
200
201    @DELETE
202    @Path("/{hostname}")
203    @Produces(MediaType.APPLICATION_JSON)
204    @APIResponses(value = {
205        @APIResponse(responseCode = "200",
206            description = "Successfully deleted the system from inventory"),
207        @APIResponse(responseCode = "400",
208            description =
209                "Unable to delete because the system does not exist in the inventory")
210    })
211    @Parameter(
212        name = "hostname", in = ParameterIn.PATH,
213        description = "The hostname of the system",
214        required = true, example = "localhost",
215        schema = @Schema(type = SchemaType.STRING)
216    )
217    @Operation(
218        summary = "Remove system",
219        description = "Removes a system from the inventory.",
220        operationId = "removeSystem"
221    )
222    public Response removeSystem(@PathParam("hostname") String hostname) {
223        SystemData s = inventory.getSystem(hostname);
224        if (s != null) {
225            inventory.removeSystem(s);
226            return success(hostname + " was removed.");
227        } else {
228            return fail(hostname + " does not exists.");
229        }
230    }
231
232    @POST
233    @Path("/client/{hostname}")
234    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
235    @Produces(MediaType.APPLICATION_JSON)
236    @APIResponses(value = {
237        @APIResponse(responseCode = "200",
238            description = "Successfully added system client"),
239        @APIResponse(responseCode = "400",
240            description = "Unable to add system client")
241    })
242    @Parameter(
243        name = "hostname", in = ParameterIn.PATH,
244        description = "The hostname of the system",
245        required = true, example = "localhost",
246        schema = @Schema(type = SchemaType.STRING)
247    )
248    @Operation(
249        summary = "Add system client",
250        description = "This adds a system client.",
251        operationId = "addSystemClient"
252    )
253    //tag::printClientPort[]
254    public Response addSystemClient(@PathParam("hostname") String hostname) {
255        System.out.println(CLIENT_PORT);
256        return success("Client Port: " + CLIENT_PORT);
257    }
258    //end::printClientPort[]
259
260    private Response success(String message) {
261        return Response.ok("{ \"ok\" : \"" + message + "\" }").build();
262    }
263
264    private Response fail(String message) {
265        return Response.status(Response.Status.BAD_REQUEST)
266                       .entity("{ \"error\" : \"" + message + "\" }")
267                       .build();
268    }
269}

The @Inject annotation injects the value from other configuration sources to the CLIENT_PORT variable. The @ConfigProperty defines the external property name as client.https.port.

Update the POST request so that the /client/{hostname} endpoint prints the CLIENT_PORT value.

Adding the microprofile-config.properties file

Define the configurable variables in the microprofile-config.properties configuration file for MicroProfile Config at the src/main/resources/META-INF directory.

mkdir src\main\resources\META-INF
mkdir -p src/main/resources/META-INF
Create the microprofile-config.properties file.
src/main/resources/META-INF/microprofile-config.properties

microprofile-config.properties

1# tag::ordinal[]
2config_ordinal=100
3# end::ordinal[]
4
5# tag::configPort[]
6client.https.port=5555
7# end::configPort[]

Using the config_ordinal variable in this properties file, you can set the ordinal of this file and thus other configuration sources.

The client.https.port variable enables the client port to be overwritten.

Revisit the OpenAPI UI http://localhost:9080/openapi/ui to view these changes. Open the /api/systems/client/{hostname} endpoint and run it within the UI to view the CLIENT_PORT value.

You can learn more about MicroProfile Config from the Configuring microservices guide.

Securing RESTful APIs

Now you can secure your RESTful APIs. Navigate to your application directory.

cd start/inventory

Begin by adding some users and user groups to your server.xml Liberty configuration file.

Replace the server.xml file.
src/main/liberty/config/server.xml

server.xml

 1<?xml version="1.0" encoding="UTF-8"?>
 2<server description="inventory">
 3
 4    <featureManager>
 5        <feature>jakartaee-10.0</feature>
 6        <feature>microProfile-6.0</feature>
 7    </featureManager>
 8
 9    <variable name="default.http.port" defaultValue="9080" />
10    <variable name="default.https.port" defaultValue="9443" />
11    <variable name="default.context.root" defaultValue="/inventory" />
12
13    <httpEndpoint id="defaultHttpEndpoint"
14                  httpPort="${default.http.port}" 
15                  httpsPort="${default.https.port}" />
16
17    <!-- Automatically expand WAR files and EAR files -->
18    <applicationManager autoExpand="true"/>
19
20    <!-- tag::basicregistry[] -->
21    <basicRegistry id="basic" realm="WebRealm">
22        <user name="bob" password="{xor}PTA9Lyg7" />
23        <user name="alice" password="{xor}PjM2PDovKDs=" />
24        <!-- tag::myadmins[] -->
25        <group name="admin">
26            <member name="bob" />
27        </group>
28        <!-- end::myadmins[] -->
29        <!-- tag::myusers[] -->
30        <group name="user">
31            <member name="bob" />
32            <member name="alice" />
33        </group>
34        <!-- end::myusers[] -->
35    </basicRegistry>
36    <!-- end::basicregistry[] -->
37
38    <!-- Configures the application on a specified context root -->
39    <webApplication contextRoot="${default.context.root}"
40                    location="inventory.war">
41        <application-bnd>
42            <!-- tag::securityrole[] -->
43            <!-- tag::adminrole[] -->
44            <security-role name="admin">
45                <group name="admin" />
46            </security-role>
47            <!-- end::adminrole[] -->
48            <!-- tag::userrole[] -->
49            <security-role name="user">
50                <group name="user" />
51            </security-role>
52            <!-- end::userrole[] -->
53            <!-- end::securityrole[] -->
54        </application-bnd>
55     </webApplication>
56
57    <!-- Default SSL configuration enables trust for default certificates from the Java runtime -->
58    <ssl id="defaultSSLConfig" trustDefaultCerts="true" />
59
60</server>

The basicRegistry element contains a list of all users for the application and their passwords, as well as all of the user groups. Note that this basicRegistry element is a very simple case for learning purposes. For more information about the different user registries, see the User registries documentation. The admin group tells the application which of the users are in the administrator group. The user group tells the application that users are in the user group.

The security-role maps the admin role to the admin group, meaning that all users in the admin group have the administrator role. Similarly, the user role is mapped to the user group, meaning all users in the user group have the user role.

Your application has the following users and passwords:

Username

Password

Role

bob

bobpwd

admin, user

alice

alicepwd

user

Now you can secure the inventory service.

Replace the SystemResource class.
src/main/java/io/openliberty/deepdive/rest/SystemResource.java

SystemResource.java

  1// tag::copyright[]
  2/*******************************************************************************
  3 * Copyright (c) 2022, 2023 IBM Corporation and others.
  4 * All rights reserved. This program and the accompanying materials
  5 * are made available under the terms of the Eclipse Public License 2.0
  6 * which accompanies this distribution, and is available at
  7 * http://www.eclipse.org/legal/epl-2.0/
  8 *
  9 * SPDX-License-Identifier: EPL-2.0
 10 *******************************************************************************/
 11// end::copyright[]
 12package io.openliberty.deepdive.rest;
 13
 14import java.util.List;
 15
 16import org.eclipse.microprofile.openapi.annotations.Operation;
 17import org.eclipse.microprofile.openapi.annotations.enums.ParameterIn;
 18import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
 19import org.eclipse.microprofile.openapi.annotations.media.Schema;
 20import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
 21import org.eclipse.microprofile.openapi.annotations.parameters.Parameters;
 22import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
 23import org.eclipse.microprofile.openapi.annotations.responses.APIResponseSchema;
 24import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
 25
 26import org.eclipse.microprofile.config.inject.ConfigProperty;
 27
 28import io.openliberty.deepdive.rest.model.SystemData;
 29import jakarta.enterprise.context.ApplicationScoped;
 30import jakarta.annotation.security.RolesAllowed;
 31import jakarta.inject.Inject;
 32import jakarta.ws.rs.Consumes;
 33import jakarta.ws.rs.DELETE;
 34import jakarta.ws.rs.GET;
 35import jakarta.ws.rs.POST;
 36import jakarta.ws.rs.PUT;
 37import jakarta.ws.rs.Path;
 38import jakarta.ws.rs.PathParam;
 39import jakarta.ws.rs.Produces;
 40import jakarta.ws.rs.QueryParam;
 41import jakarta.ws.rs.core.MediaType;
 42import jakarta.ws.rs.core.Response;
 43
 44@ApplicationScoped
 45@Path("/systems")
 46// tag::SystemResource[]
 47public class SystemResource {
 48
 49    // tag::inventory[]
 50    @Inject
 51    Inventory inventory;
 52    // end::inventory[]
 53
 54    // tag::inject[]
 55    @Inject
 56    // end::inject[]
 57    // tag::configProperty[]
 58    @ConfigProperty(name = "client.https.port")
 59    // end::configProperty[]
 60    String CLIENT_PORT;
 61
 62
 63    @GET
 64    @Path("/")
 65    @Produces(MediaType.APPLICATION_JSON)
 66    @APIResponseSchema(value = SystemData.class,
 67        responseDescription = "A list of system data stored within the inventory.",
 68        responseCode = "200")
 69    @Operation(
 70        summary = "List contents.",
 71        description = "Returns the currently stored system data in the inventory.",
 72        operationId = "listContents")
 73    public List<SystemData> listContents() {
 74        return inventory.getSystems();
 75    }
 76
 77    @GET
 78    @Path("/{hostname}")
 79    @Produces(MediaType.APPLICATION_JSON)
 80    @APIResponseSchema(value = SystemData.class,
 81        responseDescription = "System data of a particular host.",
 82        responseCode = "200")
 83    @Operation(
 84        summary = "Get System",
 85        description = "Retrieves and returns the system data from the system "
 86                      + "service running on the particular host.",
 87        operationId = "getSystem"
 88    )
 89    public SystemData getSystem(
 90        @Parameter(
 91            name = "hostname", in = ParameterIn.PATH,
 92            description = "The hostname of the system",
 93            required = true, example = "localhost",
 94            schema = @Schema(type = SchemaType.STRING)
 95        )
 96        @PathParam("hostname") String hostname) {
 97        return inventory.getSystem(hostname);
 98    }
 99
100    @POST
101    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
102    @Produces(MediaType.APPLICATION_JSON)
103    @APIResponses(value = {
104        @APIResponse(responseCode = "200",
105            description = "Successfully added system to inventory"),
106        @APIResponse(responseCode = "400",
107            description = "Unable to add system to inventory")
108    })
109    @Parameters(value = {
110        @Parameter(
111            name = "hostname", in = ParameterIn.QUERY,
112            description = "The hostname of the system",
113            required = true, example = "localhost",
114            schema = @Schema(type = SchemaType.STRING)),
115        @Parameter(
116            name = "osName", in = ParameterIn.QUERY,
117            description = "The operating system of the system",
118            required = true, example = "linux",
119            schema = @Schema(type = SchemaType.STRING)),
120        @Parameter(
121            name = "javaVersion", in = ParameterIn.QUERY,
122            description = "The Java version of the system",
123            required = true, example = "11",
124            schema = @Schema(type = SchemaType.STRING)),
125        @Parameter(
126            name = "heapSize", in = ParameterIn.QUERY,
127            description = "The heap size of the system",
128            required = true, example = "1048576",
129            schema = @Schema(type = SchemaType.NUMBER)),
130    })
131    @Operation(
132        summary = "Add system",
133        description = "Add a system and its data to the inventory.",
134        operationId = "addSystem"
135    )
136    public Response addSystem(
137        @QueryParam("hostname") String hostname,
138        @QueryParam("osName") String osName,
139        @QueryParam("javaVersion") String javaVersion,
140        @QueryParam("heapSize") Long heapSize) {
141
142        SystemData s = inventory.getSystem(hostname);
143        if (s != null) {
144            return fail(hostname + " already exists.");
145        }
146        inventory.add(hostname, osName, javaVersion, heapSize);
147        return success(hostname + " was added.");
148    }
149
150    // tag::put[]
151    @PUT
152    // end::put[]
153    // tag::putEndpoint[]
154    @Path("/{hostname}")
155    // end::putEndpoint[]
156    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
157    @Produces(MediaType.APPLICATION_JSON)
158    // tag::putRolesAllowed[]
159    @RolesAllowed({ "admin", "user" })
160    // end::putRolesAllowed[]
161    @APIResponses(value = {
162        @APIResponse(responseCode = "200",
163            description = "Successfully updated system"),
164        @APIResponse(responseCode = "400",
165           description =
166           "Unable to update because the system does not exist in the inventory.")
167    })
168    @Parameters(value = {
169        @Parameter(
170            name = "hostname", in = ParameterIn.PATH,
171            description = "The hostname of the system",
172            required = true, example = "localhost",
173            schema = @Schema(type = SchemaType.STRING)),
174        @Parameter(
175            name = "osName", in = ParameterIn.QUERY,
176            description = "The operating system of the system",
177            required = true, example = "linux",
178            schema = @Schema(type = SchemaType.STRING)),
179        @Parameter(
180            name = "javaVersion", in = ParameterIn.QUERY,
181            description = "The Java version of the system",
182            required = true, example = "11",
183            schema = @Schema(type = SchemaType.STRING)),
184        @Parameter(
185            name = "heapSize", in = ParameterIn.QUERY,
186            description = "The heap size of the system",
187            required = true, example = "1048576",
188            schema = @Schema(type = SchemaType.NUMBER)),
189    })
190    @Operation(
191        summary = "Update system",
192        description = "Update a system and its data on the inventory.",
193        operationId = "updateSystem"
194    )
195    public Response updateSystem(
196        @PathParam("hostname") String hostname,
197        @QueryParam("osName") String osName,
198        @QueryParam("javaVersion") String javaVersion,
199        @QueryParam("heapSize") Long heapSize) {
200
201        SystemData s = inventory.getSystem(hostname);
202        if (s == null) {
203            return fail(hostname + " does not exists.");
204        }
205        s.setOsName(osName);
206        s.setJavaVersion(javaVersion);
207        s.setHeapSize(heapSize);
208        inventory.update(s);
209        return success(hostname + " was updated.");
210    }
211
212    // tag::delete[]
213    @DELETE
214    // end::delete[]
215    // tag::deleteEndpoint[]
216    @Path("/{hostname}")
217    // end::deleteEndpoint[]
218    @Produces(MediaType.APPLICATION_JSON)
219    // tag::deleteRolesAllowed[]
220    @RolesAllowed({ "admin" })
221    // end::deleteRolesAllowed[]
222    @APIResponses(value = {
223        @APIResponse(responseCode = "200",
224            description = "Successfully deleted the system from inventory"),
225        @APIResponse(responseCode = "400",
226            description =
227                "Unable to delete because the system does not exist in the inventory")
228    })
229    @Parameter(
230        name = "hostname", in = ParameterIn.PATH,
231        description = "The hostname of the system",
232        required = true, example = "localhost",
233        schema = @Schema(type = SchemaType.STRING)
234    )
235    @Operation(
236        summary = "Remove system",
237        description = "Removes a system from the inventory.",
238        operationId = "removeSystem"
239    )
240    public Response removeSystem(@PathParam("hostname") String hostname) {
241        SystemData s = inventory.getSystem(hostname);
242        if (s != null) {
243            inventory.removeSystem(s);
244            return success(hostname + " was removed.");
245        } else {
246            return fail(hostname + " does not exists.");
247        }
248    }
249
250    // tag::addSystemClient[]
251    @POST
252    @Path("/client/{hostname}")
253    // end::addSystemClient[]
254    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
255    @Produces(MediaType.APPLICATION_JSON)
256    @RolesAllowed({ "admin" })
257    @APIResponses(value = {
258        @APIResponse(responseCode = "200",
259            description = "Successfully added system client"),
260        @APIResponse(responseCode = "400",
261            description = "Unable to add system client")
262    })
263    @Parameter(
264        name = "hostname", in = ParameterIn.PATH,
265        description = "The hostname of the system",
266        required = true, example = "localhost",
267        schema = @Schema(type = SchemaType.STRING)
268    )
269    @Operation(
270        summary = "Add system client",
271        description = "This adds a system client.",
272        operationId = "addSystemClient"
273    )
274    //tag::printClientPort[]
275    public Response addSystemClient(@PathParam("hostname") String hostname) {
276        System.out.println(CLIENT_PORT);
277        return success("Client Port: " + CLIENT_PORT);
278    }
279    //end::printClientPort[]
280
281    private Response success(String message) {
282        return Response.ok("{ \"ok\" : \"" + message + "\" }").build();
283    }
284
285    private Response fail(String message) {
286        return Response.status(Response.Status.BAD_REQUEST)
287                       .entity("{ \"error\" : \"" + message + "\" }")
288                       .build();
289    }
290}
291// end::SystemResource[]

This class now has role-based access control. The role names that are used in the @RolesAllowed annotations are mapped to group names in the groups claim of the JSON Web Token (JWT). This mapping results in an authorization decision wherever the security constraint is applied.

The /{hostname} endpoint that is annotated with the @PUT annotation updates a system in the inventory. This PUT endpoint is annotated with the @RolesAllowed({ "admin", "user" }) annotation. Only authenticated users with the role of admin or user can access this endpoint.

The /{hostname} endpoint that is annotated with the @DELETE annotation removes a system from the inventory. This DELETE endpoint is annotated with the @RolesAllowed({ "admin" }) annotation. Only authenticated users with the role of admin can access this endpoint.

You can manually check that the inventory microservice is secured by making requests to the PUT and DELETE endpoints.

Before making requests, you must add a system to the inventory. Try adding a system by using the POST endpoint /systems by running the following command:

curl -X POST "http://localhost:9080/inventory/api/systems?hostname=localhost&osName=mac&javaVersion=11&heapSize=1"

You can expect the following response:

{ "ok" : "localhost was added." }

This command calls the /systems endpoint and adds a system localhost to the inventory. You can validate that the command worked by calling the /systems endpoint with a GET request to retrieve all the systems in the inventory, with the following curl command:

curl -s "http://localhost:9080/inventory/api/systems"

You can now expect the following response:

[{"heapSize":1,"hostname":"localhost","javaVersion":"11","osName":"mac","id":23}]

Now try calling your secure PUT endpoint to update the system that you just added by the following curl command:

curl -k --user alice:alicepwd -X PUT "http://localhost:9080/inventory/api/systems/localhost?heapSize=2097152&javaVersion=11&osName=linux"

As this endpoint is accessible to the groups user and admin, you must log in with user credentials to update the system.

You should see the following response:

{ "ok" : "localhost was updated." }

This response means that you logged in successfully as an authenticated user, and that the endpoint works as expected.

Now try calling the DELETE endpoint. As this endpoint is only accessible to admin users, you can expect this command to fail if you attempt to access it with a user in the user group.

You can check that your application is secured against these requests with the following command:

curl -k --user alice:alicepwd -X DELETE "https://localhost:9443/inventory/api/systems/localhost"

As alice is part of the user group, this request cannot work. In your dev mode console, you can expect the following output:

jakarta.ws.rs.ForbiddenException: Unauthorized

Now attempt to call this endpoint with an authenticated admin user that can work correctly. Run the following curl command:

curl -k --user bob:bobpwd -X DELETE "https://localhost:9443/inventory/api/systems/localhost"

You can expect to see the following response:

{ "ok" : "localhost was removed." }

This response means that your endpoint is secure. Validate that it works correctly by calling the /systems endpoint with the following curl command:

curl "http://localhost:9080/inventory/api/systems"

You can expect to see the following output:

[]

This response shows that the endpoints work as expected and that the system you added was successfully deleted.

Consuming the secured RESTful APIs by JWT

You can now implement JSON Web Tokens (JWT) and configure them as Single Sign On (SSO) cookies to use the RESTful APIs. The JWT that is generated by Liberty is used to communicate securely between the inventory and system microservices. You can implement the /client/{hostname} POST endpoint to collect the properties from the system microservices and create a system in the inventory.

The system microservice is provided for you.

Writing the RESTful client interface

Create the client subdirectory. Then, create a RESTful client interface for the system microservice in the inventory microservice.

mkdir src\main\java\io\openliberty\deepdive\rest\client
mkdir src/main/java/io/openliberty/deepdive/rest/client
Create the SystemClient interface.
src/main/java/io/openliberty/deepdive/rest/client/SystemClient.java

SystemClient.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2022, 2023 IBM Corporation and others.
 4 * All rights reserved. This program and the accompanying materials
 5 * are made available under the terms of the Eclipse Public License 2.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-2.0/
 8 *
 9 * SPDX-License-Identifier: EPL-2.0
10 *******************************************************************************/
11// end::copyright[]
12package io.openliberty.deepdive.rest.client;
13
14import jakarta.ws.rs.GET;
15import jakarta.ws.rs.HeaderParam;
16import jakarta.ws.rs.Path;
17import jakarta.ws.rs.PathParam;
18import jakarta.ws.rs.Produces;
19import jakarta.ws.rs.core.MediaType;
20
21@Path("/api")
22public interface SystemClient extends AutoCloseable {
23
24    @GET
25    @Path("/property/{property}")
26    @Produces(MediaType.TEXT_PLAIN)
27    String getProperty(@HeaderParam("Authorization") String authHeader,
28                       @PathParam("property") String property);
29
30    @GET
31    @Path("/heapsize")
32    @Produces(MediaType.TEXT_PLAIN)
33    Long getHeapSize(@HeaderParam("Authorization") String authHeader);
34
35}

This interface declares methods for accessing each of the endpoints that are set up for you in the system service. The MicroProfile Rest Client feature automatically builds and generates a client implementation based on what is defined in the SystemClient interface. You don’t need to set up the client and connect with the remote service.

Now create the required exception classes that are used by the SystemClient instance.

Create the UnknownUriException class.
src/main/java/io/openliberty/deepdive/rest/client/UnknownUriException.java

UnknownUriException.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2022, 2023 IBM Corporation and others.
 4 * All rights reserved. This program and the accompanying materials
 5 * are made available under the terms of the Eclipse Public License 2.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-2.0/
 8 *
 9 * SPDX-License-Identifier: EPL-2.0
10 *******************************************************************************/
11// end::copyright[]
12package io.openliberty.deepdive.rest.client;
13
14public class UnknownUriException extends Exception {
15
16    private static final long serialVersionUID = 1L;
17
18    public UnknownUriException() {
19        super();
20    }
21
22    public UnknownUriException(String message) {
23        super(message);
24    }
25
26}

This class is an exception that is thrown when an unknown URI is passed to the SystemClient.

Create the UnknownUriExceptionMapper class.
src/main/java/io/openliberty/deepdive/rest/client/UnknownUriExceptionMapper.java

UnknownUriExceptionMapper.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2022, 2023 IBM Corporation and others.
 4 * All rights reserved. This program and the accompanying materials
 5 * are made available under the terms of the Eclipse Public License 2.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-2.0/
 8 *
 9 * SPDX-License-Identifier: EPL-2.0
10 *******************************************************************************/
11// end::copyright[]
12package io.openliberty.deepdive.rest.client;
13
14import java.util.logging.Logger;
15
16import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper;
17
18import jakarta.ws.rs.core.MultivaluedMap;
19import jakarta.ws.rs.core.Response;
20
21public class UnknownUriExceptionMapper
22    implements ResponseExceptionMapper<UnknownUriException> {
23    Logger LOG = Logger.getLogger(UnknownUriExceptionMapper.class.getName());
24
25    @Override
26    public boolean handles(int status, MultivaluedMap<String, Object> headers) {
27        LOG.info("status = " + status);
28        return status == 404;
29    }
30
31    @Override
32    public UnknownUriException toThrowable(Response response) {
33        return new UnknownUriException();
34    }
35}

This class links the UnknownUriException class with the corresponding response code through a ResponseExceptionMapper mapper class.

Implementing the /client/{hostname} endpoint

Now implement the /client/{hostname} POST endpoint of the SystemResource class to consume the secured system microservice.

Replace the SystemResource class.
src/main/java/io/openliberty/deepdive/rest/SystemResource.java

SystemResource.java

  1// tag::copyright[]
  2/*******************************************************************************
  3 * Copyright (c) 2022, 2023 IBM Corporation and others.
  4 * All rights reserved. This program and the accompanying materials
  5 * are made available under the terms of the Eclipse Public License 2.0
  6 * which accompanies this distribution, and is available at
  7 * http://www.eclipse.org/legal/epl-2.0/
  8 *
  9 * SPDX-License-Identifier: EPL-2.0
 10 *******************************************************************************/
 11// end::copyright[]
 12package io.openliberty.deepdive.rest;
 13
 14import java.net.URI;
 15import java.util.List;
 16
 17import org.eclipse.microprofile.openapi.annotations.Operation;
 18import org.eclipse.microprofile.openapi.annotations.enums.ParameterIn;
 19import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
 20import org.eclipse.microprofile.openapi.annotations.media.Schema;
 21import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
 22import org.eclipse.microprofile.openapi.annotations.parameters.Parameters;
 23import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
 24import org.eclipse.microprofile.openapi.annotations.responses.APIResponseSchema;
 25import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
 26import org.eclipse.microprofile.rest.client.RestClientBuilder;
 27import org.eclipse.microprofile.config.inject.ConfigProperty;
 28import org.eclipse.microprofile.jwt.JsonWebToken;
 29
 30import io.openliberty.deepdive.rest.client.SystemClient;
 31import io.openliberty.deepdive.rest.client.UnknownUriExceptionMapper;
 32import io.openliberty.deepdive.rest.model.SystemData;
 33import jakarta.annotation.security.RolesAllowed;
 34import jakarta.enterprise.context.ApplicationScoped;
 35import jakarta.inject.Inject;
 36import jakarta.ws.rs.Consumes;
 37import jakarta.ws.rs.DELETE;
 38import jakarta.ws.rs.GET;
 39import jakarta.ws.rs.POST;
 40import jakarta.ws.rs.PUT;
 41import jakarta.ws.rs.Path;
 42import jakarta.ws.rs.PathParam;
 43import jakarta.ws.rs.Produces;
 44import jakarta.ws.rs.QueryParam;
 45import jakarta.ws.rs.core.MediaType;
 46import jakarta.ws.rs.core.Response;
 47
 48@ApplicationScoped
 49@Path("/systems")
 50public class SystemResource {
 51
 52    @Inject
 53    Inventory inventory;
 54
 55    @Inject
 56    @ConfigProperty(name = "client.https.port")
 57    String CLIENT_PORT;
 58
 59    // tag::jwt[]
 60    @Inject
 61    JsonWebToken jwt;
 62    // end::jwt[]
 63
 64    @GET
 65    @Path("/")
 66    @Produces(MediaType.APPLICATION_JSON)
 67    @APIResponseSchema(value = SystemData.class,
 68        responseDescription = "A list of system data stored within the inventory.",
 69        responseCode = "200")
 70    @Operation(
 71        summary = "List contents.",
 72        description = "Returns the currently stored system data in the inventory.",
 73        operationId = "listContents")
 74    public List<SystemData> listContents() {
 75        return inventory.getSystems();
 76    }
 77
 78    @GET
 79    @Path("/{hostname}")
 80    @Produces(MediaType.APPLICATION_JSON)
 81    @APIResponseSchema(value = SystemData.class,
 82        responseDescription = "System data of a particular host.",
 83        responseCode = "200")
 84    @Operation(
 85        summary = "Get System",
 86        description = "Retrieves and returns the system data from the system "
 87                      + "service running on the particular host.",
 88        operationId = "getSystem"
 89    )
 90    public SystemData getSystem(
 91        @Parameter(
 92            name = "hostname", in = ParameterIn.PATH,
 93            description = "The hostname of the system",
 94            required = true, example = "localhost",
 95            schema = @Schema(type = SchemaType.STRING)
 96        )
 97        @PathParam("hostname") String hostname) {
 98        return inventory.getSystem(hostname);
 99    }
100
101    @POST
102    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
103    @Produces(MediaType.APPLICATION_JSON)
104    @APIResponses(value = {
105        @APIResponse(responseCode = "200",
106            description = "Successfully added system to inventory"),
107        @APIResponse(responseCode = "400",
108            description = "Unable to add system to inventory")
109    })
110    @Parameters(value = {
111        @Parameter(
112            name = "hostname", in = ParameterIn.QUERY,
113            description = "The hostname of the system",
114            required = true, example = "localhost",
115            schema = @Schema(type = SchemaType.STRING)),
116        @Parameter(
117            name = "osName", in = ParameterIn.QUERY,
118            description = "The operating system of the system",
119            required = true, example = "linux",
120            schema = @Schema(type = SchemaType.STRING)),
121        @Parameter(
122            name = "javaVersion", in = ParameterIn.QUERY,
123            description = "The Java version of the system",
124            required = true, example = "11",
125            schema = @Schema(type = SchemaType.STRING)),
126        @Parameter(
127            name = "heapSize", in = ParameterIn.QUERY,
128            description = "The heap size of the system",
129            required = true, example = "1048576",
130            schema = @Schema(type = SchemaType.NUMBER)),
131    })
132    @Operation(
133        summary = "Add system",
134        description = "Add a system and its data to the inventory.",
135        operationId = "addSystem"
136    )
137    public Response addSystem(
138        @QueryParam("hostname") String hostname,
139        @QueryParam("osName") String osName,
140        @QueryParam("javaVersion") String javaVersion,
141        @QueryParam("heapSize") Long heapSize) {
142
143        SystemData s = inventory.getSystem(hostname);
144        if (s != null) {
145            return fail(hostname + " already exists.");
146        }
147        inventory.add(hostname, osName, javaVersion, heapSize);
148        return success(hostname + " was added.");
149    }
150
151    // tag::put[]
152    @PUT
153    // end::put[]
154    // tag::putEndpoint[]
155    @Path("/{hostname}")
156    // end::putEndpoint[]
157    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
158    @Produces(MediaType.APPLICATION_JSON)
159    // tag::putRolesAllowed[]
160    @RolesAllowed({ "admin", "user" })
161    // end::putRolesAllowed[]
162    @APIResponses(value = {
163        @APIResponse(responseCode = "200",
164            description = "Successfully updated system"),
165        @APIResponse(responseCode = "400",
166           description =
167               "Unable to update because the system does not exist in the inventory.")
168    })
169    @Parameters(value = {
170        @Parameter(
171            name = "hostname", in = ParameterIn.PATH,
172            description = "The hostname of the system",
173            required = true, example = "localhost",
174            schema = @Schema(type = SchemaType.STRING)),
175        @Parameter(
176            name = "osName", in = ParameterIn.QUERY,
177            description = "The operating system of the system",
178            required = true, example = "linux",
179            schema = @Schema(type = SchemaType.STRING)),
180        @Parameter(
181            name = "javaVersion", in = ParameterIn.QUERY,
182            description = "The Java version of the system",
183            required = true, example = "11",
184            schema = @Schema(type = SchemaType.STRING)),
185        @Parameter(
186            name = "heapSize", in = ParameterIn.QUERY,
187            description = "The heap size of the system",
188            required = true, example = "1048576",
189            schema = @Schema(type = SchemaType.NUMBER)),
190    })
191    @Operation(
192        summary = "Update system",
193        description = "Update a system and its data on the inventory.",
194        operationId = "updateSystem"
195    )
196    public Response updateSystem(
197        @PathParam("hostname") String hostname,
198        @QueryParam("osName") String osName,
199        @QueryParam("javaVersion") String javaVersion,
200        @QueryParam("heapSize") Long heapSize) {
201
202        SystemData s = inventory.getSystem(hostname);
203        if (s == null) {
204            return fail(hostname + " does not exists.");
205        }
206        s.setOsName(osName);
207        s.setJavaVersion(javaVersion);
208        s.setHeapSize(heapSize);
209        inventory.update(s);
210        return success(hostname + " was updated.");
211    }
212
213    // tag::delete[]
214    @DELETE
215    // end::delete[]
216    // tag::deleteEndpoint[]
217    @Path("/{hostname}")
218    // end::deleteEndpoint[]
219    @Produces(MediaType.APPLICATION_JSON)
220    // tag::deleteRolesAllowed[]
221    @RolesAllowed({ "admin" })
222    // end::deleteRolesAllowed[]
223    @APIResponses(value = {
224        @APIResponse(responseCode = "200",
225            description = "Successfully deleted the system from inventory"),
226        @APIResponse(responseCode = "400",
227            description =
228                "Unable to delete because the system does not exist in the inventory")
229    })
230    @Parameter(
231        name = "hostname", in = ParameterIn.PATH,
232        description = "The hostname of the system",
233        required = true, example = "localhost",
234        schema = @Schema(type = SchemaType.STRING)
235    )
236    @Operation(
237        summary = "Remove system",
238        description = "Removes a system from the inventory.",
239        operationId = "removeSystem"
240    )
241    public Response removeSystem(@PathParam("hostname") String hostname) {
242        SystemData s = inventory.getSystem(hostname);
243        if (s != null) {
244            inventory.removeSystem(s);
245            return success(hostname + " was removed.");
246        } else {
247            return fail(hostname + " does not exists.");
248        }
249    }
250
251    // tag::addSystemClient[]
252    @POST
253    @Path("/client/{hostname}")
254    // end::addSystemClient[]
255    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
256    @Produces(MediaType.APPLICATION_JSON)
257    @RolesAllowed({ "admin" })
258    @APIResponses(value = {
259        @APIResponse(responseCode = "200",
260            description = "Successfully added system client"),
261        @APIResponse(responseCode = "400",
262            description = "Unable to add system client")
263    })
264    @Parameter(
265        name = "hostname", in = ParameterIn.PATH,
266        description = "The hostname of the system",
267        required = true, example = "localhost",
268        schema = @Schema(type = SchemaType.STRING)
269    )
270    @Operation(
271        summary = "Add system client",
272        description = "This adds a system client.",
273        operationId = "addSystemClient"
274    )
275    public Response addSystemClient(@PathParam("hostname") String hostname) {
276
277        SystemData s = inventory.getSystem(hostname);
278        if (s != null) {
279            return fail(hostname + " already exists.");
280        }
281
282        // tag::getCustomRestClient[]
283        SystemClient customRestClient = null;
284        try {
285            customRestClient = getSystemClient(hostname);
286        } catch (Exception e) {
287            return fail("Failed to create the client " + hostname + ".");
288        }
289        // end::getCustomRestClient[]
290
291        // tag::authHeader[]
292        String authHeader = "Bearer " + jwt.getRawToken();
293        // end::authHeader[]
294        try {
295            // tag::customRestClient[]
296            String osName = customRestClient.getProperty(authHeader, "os.name");
297            String javaVer = customRestClient.getProperty(authHeader, "java.version");
298            Long heapSize = customRestClient.getHeapSize(authHeader);
299            // end::customRestClient[]
300            // tag::addSystem[]
301            inventory.add(hostname, osName, javaVer, heapSize);
302            // end::addSystem[]
303        } catch (Exception e) {
304            return fail("Failed to reach the client " + hostname + ".");
305        }
306        return success(hostname + " was added.");
307    }
308
309    // tag::getSystemClient[]
310    private SystemClient getSystemClient(String hostname) throws Exception {
311        String customURIString = "https://" + hostname + ":" + CLIENT_PORT + "/system";
312        URI customURI = URI.create(customURIString);
313        return RestClientBuilder.newBuilder()
314                                .baseUri(customURI)
315                                .register(UnknownUriExceptionMapper.class)
316                                .build(SystemClient.class);
317    }
318    // end::getSystemClient[]
319
320    private Response success(String message) {
321        return Response.ok("{ \"ok\" : \"" + message + "\" }").build();
322    }
323
324    private Response fail(String message) {
325        return Response.status(Response.Status.BAD_REQUEST)
326                       .entity("{ \"error\" : \"" + message + "\" }")
327                       .build();
328    }
329}

The getSystemClient() method builds and returns a new instance of the SystemClient class for the hostname provided. The /client/{hostname} POST endpoint uses this method to create a REST client that is called customRestClient to consume the system microservice.

A JWT instance is injected to the jwt field variable by the jwtSso feature. It is used to create the authHeader authentication header. It is then passed as a parameter to the endpoints of the customRestClient to get the properties from the system microservice. A system is then added to the inventory.

Configuring the JSON Web Token

Next, add the JSON Web Token (Single Sign On) feature to the server configuration file for the inventory service.

Replace the server.xml file.
src/main/liberty/config/server.xml

server.xml

 1<?xml version="1.0" encoding="UTF-8"?>
 2<server description="inventory">
 3
 4    <featureManager>
 5        <feature>jakartaee-10.0</feature>
 6        <feature>microProfile-6.0</feature>
 7        <!-- tag::jwtSsoFeature[] -->
 8        <feature>jwtSso-1.0</feature>
 9        <!-- end::jwtSsoFeature[] -->
10    </featureManager>
11
12    <variable name="default.http.port" defaultValue="9080" />
13    <variable name="default.https.port" defaultValue="9443" />
14    <variable name="default.context.root" defaultValue="/inventory" />
15
16    <httpEndpoint id="defaultHttpEndpoint"
17                  httpPort="${default.http.port}" 
18                  httpsPort="${default.https.port}" />
19
20    <!-- Automatically expand WAR files and EAR files -->
21    <applicationManager autoExpand="true"/>
22    
23    <!-- tag::keyStore[] -->
24    <keyStore id="defaultKeyStore" password="secret" />
25    <!-- end::keyStore[] -->
26    
27    <basicRegistry id="basic" realm="WebRealm">
28        <user name="bob" password="{xor}PTA9Lyg7" />
29        <user name="alice" password="{xor}PjM2PDovKDs=" />
30
31        <group name="admin">
32            <member name="bob" />
33        </group>
34
35        <group name="user">
36            <member name="bob" />
37            <member name="alice" />
38        </group>
39    </basicRegistry>
40
41    <!-- Configures the application on a specified context root -->
42    <webApplication contextRoot="${default.context.root}"
43                    location="inventory.war">
44        <application-bnd>
45            <security-role name="admin">
46                <group name="admin" />
47            </security-role>
48            <security-role name="user">
49                <group name="user" />
50            </security-role>
51        </application-bnd>
52    </webApplication>
53
54    <!-- tag::jwtSsoConfig[] -->
55    <jwtSso jwtBuilderRef="jwtInventoryBuilder"/> 
56    <!-- end::jwtSsoConfig[] -->
57    <!-- tag::jwtBuilder[] -->
58    <jwtBuilder id="jwtInventoryBuilder" 
59                issuer="http://openliberty.io" 
60                audiences="systemService"
61                expiry="24h"/>
62    <!-- end::jwtBuilder[] -->
63    <!-- tag::mpJwt[] -->
64    <mpJwt audiences="systemService" 
65           groupNameAttribute="groups" 
66           id="myMpJwt" 
67           issuer="http://openliberty.io"/>
68    <!-- end::mpJwt[] -->
69
70    <!-- Default SSL configuration enables trust for default certificates from the Java runtime -->
71    <ssl id="defaultSSLConfig" trustDefaultCerts="true" />
72
73</server>

microprofile-config.properties

1mp.jwt.verify.issuer=http://openliberty.io
2mp.jwt.token.header=Authorization
3mp.jwt.token.cookie=Bearer
4mp.jwt.verify.audiences=systemService, adminServices
5# mp.jwt.decrypt.key.location=privatekey.pem
6mp.jwt.verify.publickey.algorithm=RS256

The jwtSso feature adds the libraries that are required for JWT SSO implementation. Configure the jwtSso feature by adding the jwtBuilder configuration to your server.xml. Also, configure the MicroProfile JWT with the audiences and issuer properties that match the microprofile-config.properties defined at the system/src/main/webapp/META-INF directory under the system project. For more information, see the JSON Web Token Single Sign-On feature, jwtSso element, and jwtBuilder element documentation.

The keyStore element is used to define the repository of security certificates used for SSL encryption. The id attribute is a unique configuration ID that is set to defaultKeyStore. The password attribute is used to load the keystore file, and its value can be stored in clear text or encoded form. To learn more about other attributes, see the keyStore attribute documentation.

Because the keystore file is not provided at the src directory, Liberty creates a Public Key Cryptography Standards #12 (PKCS12) keystore file for you by default. This file needs to be replaced, as the keyStore configuration must be the same in both system and inventory microservices. As the configured system microservice is already provided for you, copy the key.p12 keystore file from the system microservice to your inventory service.

mkdir src\main\liberty\config\resources\security
copy ..\..\finish\system\src\main\liberty\config\resources\security\key.p12 src\main\liberty\config\resources\security\key.p12
mkdir -p src/main/liberty/config/resources/security
cp ../../finish/system/src/main/liberty/config/resources/security/key.p12 src/main/liberty/config/resources/security/key.p12

Now configure the client https port in the build.gradle configuration file.

Replace the build.gradle file.
build.gradle

build.gradle

 1plugins {
 2    id 'war'
 3    id 'io.openliberty.tools.gradle.Liberty' version '3.5.2'
 4}
 5
 6version '1.0-SNAPSHOT'
 7group 'io.openliberty.deepdive'
 8
 9sourceCompatibility = 11
10targetCompatibility = 11
11tasks.withType(JavaCompile) {
12    options.encoding = 'UTF-8'
13}
14
15repositories {
16    mavenCentral()
17}
18
19dependencies {
20    // provided dependencies
21    providedCompile 'jakarta.platform:jakarta.jakartaee-api:10.0.0'
22    providedCompile 'org.eclipse.microprofile:microprofile:6.0'
23}
24
25war {
26    archiveVersion = ''
27}
28
29ext  {
30    liberty.server.var.'default.http.port' = '9080'
31    liberty.server.var.'default.https.port' = '9443'
32    liberty.server.var.'default.context.root' = '/inventory'
33    // tag::https[]
34    liberty.server.var.'client.https.port' = '9444'
35    // end::https[]
36}
37
38clean.dependsOn 'libertyStop'

Configure the client https port by setting the liberty.server.var.'client.https.port' to 9444.

In your dev mode console for the inventory microservice, press CTRL+C to stop the server. Then, restart the dev mode of the inventory microservice.

gradlew libertyDev
./gradlew libertyDev

After you see the following message, your application server in dev mode is ready again:

**************************************************************
*    Liberty is running in dev mode.

Running the /client/{hostname} endpoint

Open another command-line session and run the system microservice from the finish directory.

cd finish\system
gradlew libertyRun
cd finish/system
./gradlew libertyRun

Wait until the following message displays on the system microservice console.

CWWKF0011I: The defaultServer server is ready to run a smarter planet. ...

You can check that the system microservice is secured against unauthenticated requests at the https://localhost:9444/system/api/heapsize URL. You can expect to see the following error in the console of the system microservice:

CWWKS5522E: The MicroProfile JWT feature cannot perform authentication because a MicroProfile JWT cannot be found in the request.

You can check that the /client/{hostname} endpoint you updated can access the system microservice.

Make an authorized request to the new /client/{hostname} endpoint. As this endpoint is restricted to admin, you can use the login credentials for bob, which is in the admin group.

curl -k --user bob:bobpwd -X POST "https://localhost:9443/inventory/api/systems/client/localhost"

You can expect the following output:

{ "ok" : "localhost was added." }

You can verify that this endpoint works as expected by running the following command:

curl "http://localhost:9080/inventory/api/systems"

You can expect to see your system listed in the output.

[
  {
    "heapSize": 2999975936,
    "hostname": "localhost",
    "id": 11,
    "javaVersion": "11.0.11",
    "osName": "Linux"
  }
]

Adding health checks

Next, you’ll use MicroProfile Health to report the health status of the microservice and PostgreSQL database connection.

Navigate to your application directory

cd start/inventory

A health report is generated automatically for all health services that enable MicroProfile Health.

All health services must provide an implementation of the HealthCheck interface, which is used to verify their health. MicroProfile Health offers health checks for startup, liveness, and readiness.

A startup check allows applications to define startup probes that are used for initial verification of the application before the liveness probe takes over. For example, a startup check might check which applications require additional startup time on their first initialization.

A liveness check allows third-party services to determine whether a microservice is running. If the liveness check fails, the application can be terminated. For example, a liveness check might fail if the application runs out of memory.

A readiness check allows third-party services, such as Kubernetes, to determine whether a microservice is ready to process requests.

Create the health subdirectory before creating the health check classes.

mkdir src\main\java\io\openliberty\deepdive\rest\health
mkdir src/main/java/io/openliberty/deepdive/rest/health
Create the StartupCheck class.
src/main/java/io/openliberty/deepdive/rest/health/StartupCheck.java

StartupCheck.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2022, 2023 IBM Corporation and others.
 4 * All rights reserved. This program and the accompanying materials
 5 * are made available under the terms of the Eclipse Public License 2.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-2.0/
 8 *
 9 * SPDX-License-Identifier: EPL-2.0
10 *******************************************************************************/
11// end::copyright[]
12// tag::StartupCheck[]
13package io.openliberty.deepdive.rest.health;
14
15import java.lang.management.ManagementFactory;
16import com.sun.management.OperatingSystemMXBean;
17import jakarta.enterprise.context.ApplicationScoped;
18import org.eclipse.microprofile.health.Startup;
19import org.eclipse.microprofile.health.HealthCheck;
20import org.eclipse.microprofile.health.HealthCheckResponse;
21
22// tag::Startup[]
23@Startup
24// end::Startup[]
25@ApplicationScoped
26public class StartupCheck implements HealthCheck {
27
28    @Override
29    public HealthCheckResponse call() {
30        OperatingSystemMXBean bean = (com.sun.management.OperatingSystemMXBean)
31        ManagementFactory.getOperatingSystemMXBean();
32        double cpuUsed = bean.getSystemCpuLoad();
33        String cpuUsage = String.valueOf(cpuUsed);
34        return HealthCheckResponse.named("Startup Check")
35                                  .status(cpuUsed < 0.95).build();
36    }
37}
38// end::StartupCheck[]

The @Startup annotation indicates that this class is a startup health check procedure. Navigate to the http://localhost:9080/health/started URL to check the status of the startup health check. In this case, you are checking the cpu usage. If more than 95% of the cpu is being used, a status of DOWN is returned.

Create the LivenessCheck class.
src/main/java/io/openliberty/deepdive/rest/health/LivenessCheck.java

LivenessCheck.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2022, 2023 IBM Corporation and others.
 4 * All rights reserved. This program and the accompanying materials
 5 * are made available under the terms of the Eclipse Public License 2.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-2.0/
 8 *
 9 * SPDX-License-Identifier: EPL-2.0
10 *******************************************************************************/
11// end::copyright[]
12// tag::LivenessCheck[]
13package io.openliberty.deepdive.rest.health;
14
15import java.lang.management.ManagementFactory;
16import java.lang.management.MemoryMXBean;
17
18import jakarta.enterprise.context.ApplicationScoped;
19import org.eclipse.microprofile.health.Liveness;
20import org.eclipse.microprofile.health.HealthCheck;
21import org.eclipse.microprofile.health.HealthCheckResponse;
22
23// tag::Liveness[]
24@Liveness
25// end::Liveness[]
26@ApplicationScoped
27public class LivenessCheck implements HealthCheck {
28
29    @Override
30    public HealthCheckResponse call() {
31        MemoryMXBean memBean = ManagementFactory.getMemoryMXBean();
32        long memUsed = memBean.getHeapMemoryUsage().getUsed();
33        long memMax = memBean.getHeapMemoryUsage().getMax();
34
35        return HealthCheckResponse.named("Liveness Check")
36                                  .status(memUsed < memMax * 0.9)
37                                  .build();
38    }
39}
40// end::LivenessCheck[]

The @Liveness annotation indicates that this class is a liveness health check procedure. Navigate to the http://localhost:9080/health/live URL to check the status of the liveness health check. In this case, you are checking the heap memory usage. If more than 90% of the maximum memory is being used, a status of DOWN is returned.

Create the ReadinessCheck class.
src/main/java/io/openliberty/deepdive/rest/health/ReadinessCheck.java

ReadinessCheck.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2022, 2023 IBM Corporation and others.
 4 * All rights reserved. This program and the accompanying materials
 5 * are made available under the terms of the Eclipse Public License 2.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-2.0/
 8 *
 9 * SPDX-License-Identifier: EPL-2.0
10 *******************************************************************************/
11// end::copyright[]
12// tag::ReadinessCheck[]
13package io.openliberty.deepdive.rest.health;
14
15import java.time.LocalDateTime;
16import jakarta.enterprise.context.ApplicationScoped;
17import org.eclipse.microprofile.config.inject.ConfigProperty;
18import org.eclipse.microprofile.health.Readiness;
19import org.eclipse.microprofile.health.HealthCheck;
20import org.eclipse.microprofile.health.HealthCheckResponse;
21
22// tag::Readiness[]
23@Readiness
24// end::Readiness[]
25// tag::ApplicationScoped[]
26@ApplicationScoped
27// end::ApplicationScoped[]
28public class ReadinessCheck implements HealthCheck {
29
30    private static final int ALIVE_DELAY_SECONDS = 10;
31    private static final String READINESS_CHECK = "Readiness Check";
32    private static LocalDateTime aliveAfter = LocalDateTime.now();
33
34    @Override
35    public HealthCheckResponse call() {
36        if (isAlive()) {
37            return HealthCheckResponse.up(READINESS_CHECK);
38        }
39
40        return HealthCheckResponse.down(READINESS_CHECK);
41    }
42
43    public static void setUnhealthy() {
44        aliveAfter = LocalDateTime.now().plusSeconds(ALIVE_DELAY_SECONDS);
45    }
46
47    private static boolean isAlive() {
48        return LocalDateTime.now().isAfter(aliveAfter);
49    }
50}
51// end::ReadinessCheck[]

The @Readiness annotation indicates that this class is a readiness health check procedure. Navigate to the http://localhost:9080/health/ready URL to check the status of the readiness health check. This readiness check tests the connection to the PostgreSQL container that was created earlier in the guide. If the connection is refused, a status of DOWN is returned.

Or, you can visit the http://localhost:9080/health URL to see the overall health status of the application.

Providing metrics

Next, you can learn how to use MicroProfile Metrics to provide metrics from the inventory microservice.

Go to your application directory.

cd start/inventory

Enable the bob user to access the /metrics endpoints.

Replace the server.xml file.
src/main/liberty/config/server.xml

server.xml

 1<?xml version="1.0" encoding="UTF-8"?>
 2<server description="inventory">
 3
 4    <featureManager>
 5        <feature>jakartaee-10.0</feature>
 6        <feature>microProfile-6.0</feature>
 7        <feature>jwtSso-1.0</feature>
 8    </featureManager>
 9
10    <variable name="default.http.port" defaultValue="9080" />
11    <variable name="default.https.port" defaultValue="9443" />
12    <variable name="default.context.root" defaultValue="/inventory" />
13
14    <httpEndpoint id="defaultHttpEndpoint"
15                  httpPort="${default.http.port}" 
16                  httpsPort="${default.https.port}" />
17
18    <!-- Automatically expand WAR files and EAR files -->
19    <applicationManager autoExpand="true"/>
20    
21    <keyStore id="defaultKeyStore" password="secret" />
22    
23    <basicRegistry id="basic" realm="WebRealm">
24        <user name="bob" password="{xor}PTA9Lyg7" />
25        <user name="alice" password="{xor}PjM2PDovKDs=" />
26
27        <group name="admin">
28            <member name="bob" />
29        </group>
30
31        <group name="user">
32            <member name="bob" />
33            <member name="alice" />
34        </group>
35    </basicRegistry>
36
37    <!-- tag::administrator[] -->
38    <administrator-role>
39        <user>bob</user>
40        <group>AuthorizedGroup</group>
41    </administrator-role>
42    <!-- end::administrator[] -->
43
44    <!-- Configures the application on a specified context root -->
45    <webApplication contextRoot="${default.context.root}"
46                    location="inventory.war">
47        <application-bnd>
48            <security-role name="admin">
49                <group name="admin" />
50            </security-role>
51            <security-role name="user">
52                <group name="user" />
53            </security-role>
54        </application-bnd>
55    </webApplication>
56
57    <jwtSso jwtBuilderRef="jwtInventoryBuilder"/> 
58    <jwtBuilder id="jwtInventoryBuilder" 
59                issuer="http://openliberty.io" 
60                audiences="systemService"
61                expiry="24h"/>
62    <mpJwt audiences="systemService" 
63           groupNameAttribute="groups" 
64           id="myMpJwt" 
65           issuer="http://openliberty.io"/>
66
67    <!-- Default SSL configuration enables trust for default certificates from the Java runtime -->
68    <ssl id="defaultSSLConfig" trustDefaultCerts="true" />
69
70</server>

The administrator-role configuration authorizes the bob user as an administrator.

Use annotations that are provided by MicroProfile Metrics to instrument the inventory microservice to provide application-level metrics data.

Replace the SystemResource class.
src/main/java/io/openliberty/deepdive/rest/SystemResource.java

SystemResource.java

  1// tag::copyright[]
  2/*******************************************************************************
  3 * Copyright (c) 2022, 2023 IBM Corporation and others.
  4 * All rights reserved. This program and the accompanying materials
  5 * are made available under the terms of the Eclipse Public License 2.0
  6 * which accompanies this distribution, and is available at
  7 * http://www.eclipse.org/legal/epl-2.0/
  8 *
  9 * SPDX-License-Identifier: EPL-2.0
 10 *******************************************************************************/
 11// end::copyright[]
 12package io.openliberty.deepdive.rest;
 13
 14import java.net.URI;
 15import java.util.List;
 16
 17import org.eclipse.microprofile.openapi.annotations.Operation;
 18import org.eclipse.microprofile.openapi.annotations.enums.ParameterIn;
 19import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
 20import org.eclipse.microprofile.openapi.annotations.media.Schema;
 21import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
 22import org.eclipse.microprofile.openapi.annotations.parameters.Parameters;
 23import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
 24import org.eclipse.microprofile.openapi.annotations.responses.APIResponseSchema;
 25import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
 26import org.eclipse.microprofile.rest.client.RestClientBuilder;
 27import org.eclipse.microprofile.config.inject.ConfigProperty;
 28import org.eclipse.microprofile.jwt.JsonWebToken;
 29// tag::metricsImport[]
 30import org.eclipse.microprofile.metrics.annotation.Counted;
 31// end::metricsImport[]
 32
 33import io.openliberty.deepdive.rest.client.SystemClient;
 34import io.openliberty.deepdive.rest.client.UnknownUriExceptionMapper;
 35import io.openliberty.deepdive.rest.model.SystemData;
 36import jakarta.annotation.security.RolesAllowed;
 37import jakarta.enterprise.context.ApplicationScoped;
 38import jakarta.inject.Inject;
 39import jakarta.ws.rs.Consumes;
 40import jakarta.ws.rs.DELETE;
 41import jakarta.ws.rs.GET;
 42import jakarta.ws.rs.POST;
 43import jakarta.ws.rs.PUT;
 44import jakarta.ws.rs.Path;
 45import jakarta.ws.rs.PathParam;
 46import jakarta.ws.rs.Produces;
 47import jakarta.ws.rs.QueryParam;
 48import jakarta.ws.rs.core.MediaType;
 49import jakarta.ws.rs.core.Response;
 50
 51@ApplicationScoped
 52@Path("/systems")
 53public class SystemResource {
 54
 55    @Inject
 56    Inventory inventory;
 57
 58    @Inject
 59    @ConfigProperty(name = "client.https.port")
 60    String CLIENT_PORT;
 61
 62    @Inject
 63    JsonWebToken jwt;
 64
 65    @GET
 66    @Path("/")
 67    @Produces(MediaType.APPLICATION_JSON)
 68    @APIResponseSchema(value = SystemData.class,
 69        responseDescription = "A list of system data stored within the inventory.",
 70        responseCode = "200")
 71    @Operation(
 72        summary = "List contents.",
 73        description = "Returns the currently stored system data in the inventory.",
 74        operationId = "listContents")
 75    public List<SystemData> listContents() {
 76        return inventory.getSystems();
 77    }
 78
 79    @GET
 80    @Path("/{hostname}")
 81    @Produces(MediaType.APPLICATION_JSON)
 82    @APIResponseSchema(value = SystemData.class,
 83        responseDescription = "System data of a particular host.",
 84        responseCode = "200")
 85    @Operation(
 86        summary = "Get System",
 87        description = "Retrieves and returns the system data from the system "
 88                      + "service running on the particular host.",
 89        operationId = "getSystem"
 90    )
 91    public SystemData getSystem(
 92        @Parameter(
 93            name = "hostname", in = ParameterIn.PATH,
 94            description = "The hostname of the system",
 95            required = true, example = "localhost",
 96            schema = @Schema(type = SchemaType.STRING)
 97        )
 98        @PathParam("hostname") String hostname) {
 99        return inventory.getSystem(hostname);
100    }
101
102    @POST
103    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
104    @Produces(MediaType.APPLICATION_JSON)
105    // tag::metricsAddSystem[]
106    @Counted(name = "addSystem",
107             absolute = true,
108             description = "Number of times adding system endpoint is called")
109    // end::metricsAddSystem[]
110    @APIResponses(value = {
111        @APIResponse(responseCode = "200",
112            description = "Successfully added system to inventory"),
113        @APIResponse(responseCode = "400",
114            description = "Unable to add system to inventory")
115    })
116    @Parameters(value = {
117        @Parameter(
118            name = "hostname", in = ParameterIn.QUERY,
119            description = "The hostname of the system",
120            required = true, example = "localhost",
121            schema = @Schema(type = SchemaType.STRING)),
122        @Parameter(
123            name = "osName", in = ParameterIn.QUERY,
124            description = "The operating system of the system",
125            required = true, example = "linux",
126            schema = @Schema(type = SchemaType.STRING)),
127        @Parameter(
128            name = "javaVersion", in = ParameterIn.QUERY,
129            description = "The Java version of the system",
130            required = true, example = "11",
131            schema = @Schema(type = SchemaType.STRING)),
132        @Parameter(
133            name = "heapSize", in = ParameterIn.QUERY,
134            description = "The heap size of the system",
135            required = true, example = "1048576",
136            schema = @Schema(type = SchemaType.NUMBER)),
137    })
138    @Operation(
139        summary = "Add system",
140        description = "Add a system and its data to the inventory.",
141        operationId = "addSystem"
142    )
143    public Response addSystem(
144        @QueryParam("hostname") String hostname,
145        @QueryParam("osName") String osName,
146        @QueryParam("javaVersion") String javaVersion,
147        @QueryParam("heapSize") Long heapSize) {
148
149        SystemData s = inventory.getSystem(hostname);
150        if (s != null) {
151            return fail(hostname + " already exists.");
152        }
153        inventory.add(hostname, osName, javaVersion, heapSize);
154        return success(hostname + " was added.");
155    }
156
157    @PUT
158    @Path("/{hostname}")
159    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
160    @Produces(MediaType.APPLICATION_JSON)
161    @RolesAllowed({ "admin", "user" })
162    // tag::metricsUpdateSystem[]
163    @Counted(name = "updateSystem",
164             absolute = true,
165             description = "Number of times updating a system endpoint is called")
166    // end::metricsUpdateSystem[]
167    @APIResponses(value = {
168        @APIResponse(responseCode = "200",
169            description = "Successfully updated system"),
170        @APIResponse(responseCode = "400",
171           description =
172               "Unable to update because the system does not exist in the inventory.")
173    })
174    @Parameters(value = {
175        @Parameter(
176            name = "hostname", in = ParameterIn.PATH,
177            description = "The hostname of the system",
178            required = true, example = "localhost",
179            schema = @Schema(type = SchemaType.STRING)),
180        @Parameter(
181            name = "osName", in = ParameterIn.QUERY,
182            description = "The operating system of the system",
183            required = true, example = "linux",
184            schema = @Schema(type = SchemaType.STRING)),
185        @Parameter(
186            name = "javaVersion", in = ParameterIn.QUERY,
187            description = "The Java version of the system",
188            required = true, example = "11",
189            schema = @Schema(type = SchemaType.STRING)),
190        @Parameter(
191            name = "heapSize", in = ParameterIn.QUERY,
192            description = "The heap size of the system",
193            required = true, example = "1048576",
194            schema = @Schema(type = SchemaType.NUMBER)),
195    })
196    @Operation(
197        summary = "Update system",
198        description = "Update a system and its data on the inventory.",
199        operationId = "updateSystem"
200    )
201    public Response updateSystem(
202        @PathParam("hostname") String hostname,
203        @QueryParam("osName") String osName,
204        @QueryParam("javaVersion") String javaVersion,
205        @QueryParam("heapSize") Long heapSize) {
206
207        SystemData s = inventory.getSystem(hostname);
208        if (s == null) {
209            return fail(hostname + " does not exists.");
210        }
211        s.setOsName(osName);
212        s.setJavaVersion(javaVersion);
213        s.setHeapSize(heapSize);
214        inventory.update(s);
215        return success(hostname + " was updated.");
216    }
217
218    @DELETE
219    @Path("/{hostname}")
220    @Produces(MediaType.APPLICATION_JSON)
221    @RolesAllowed({ "admin" })
222    // tag::metricsRemoveSystem[]
223    @Counted(name = "removeSystem",
224             absolute = true,
225             description = "Number of times removing a system endpoint is called")
226    // end::metricsRemoveSystem[]
227    @APIResponses(value = {
228        @APIResponse(responseCode = "200",
229            description = "Successfully deleted the system from inventory"),
230        @APIResponse(responseCode = "400",
231            description =
232                "Unable to delete because the system does not exist in the inventory")
233    })
234    @Parameter(
235        name = "hostname", in = ParameterIn.PATH,
236        description = "The hostname of the system",
237        required = true, example = "localhost",
238        schema = @Schema(type = SchemaType.STRING)
239    )
240    @Operation(
241        summary = "Remove system",
242        description = "Removes a system from the inventory.",
243        operationId = "removeSystem"
244    )
245    public Response removeSystem(@PathParam("hostname") String hostname) {
246        SystemData s = inventory.getSystem(hostname);
247        if (s != null) {
248            inventory.removeSystem(s);
249            return success(hostname + " was removed.");
250        } else {
251            return fail(hostname + " does not exists.");
252        }
253    }
254
255    @POST
256    @Path("/client/{hostname}")
257    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
258    @Produces(MediaType.APPLICATION_JSON)
259    @RolesAllowed({ "admin" })
260    // tag::metricsAddSystemClient[]
261    @Counted(name = "addSystemClient",
262             absolute = true,
263             description = "Number of times adding a system by client is called")
264    // end::metricsAddSystemClient[]
265    @APIResponses(value = {
266        @APIResponse(responseCode = "200",
267            description = "Successfully added system client"),
268        @APIResponse(responseCode = "400",
269            description = "Unable to add system client")
270    })
271    @Parameter(
272        name = "hostname", in = ParameterIn.PATH,
273        description = "The hostname of the system",
274        required = true, example = "localhost",
275        schema = @Schema(type = SchemaType.STRING)
276    )
277    @Operation(
278        summary = "Add system client",
279        description = "This adds a system client.",
280        operationId = "addSystemClient"
281    )
282    public Response addSystemClient(@PathParam("hostname") String hostname) {
283
284        SystemData s = inventory.getSystem(hostname);
285        if (s != null) {
286            return fail(hostname + " already exists.");
287        }
288
289        SystemClient customRestClient = null;
290        try {
291            customRestClient = getSystemClient(hostname);
292        } catch (Exception e) {
293            return fail("Failed to create the client " + hostname + ".");
294        }
295
296        String authHeader = "Bearer " + jwt.getRawToken();
297        try {
298            String osName = customRestClient.getProperty(authHeader, "os.name");
299            String javaVer = customRestClient.getProperty(authHeader, "java.version");
300            Long heapSize = customRestClient.getHeapSize(authHeader);
301            inventory.add(hostname, osName, javaVer, heapSize);
302        } catch (Exception e) {
303            return fail("Failed to reach the client " + hostname + ".");
304        }
305        return success(hostname + " was added.");
306    }
307
308    private SystemClient getSystemClient(String hostname) throws Exception {
309        String customURIString = "https://" + hostname + ":" + CLIENT_PORT + "/system";
310        URI customURI = URI.create(customURIString);
311        return RestClientBuilder.newBuilder()
312                                .baseUri(customURI)
313                                .register(UnknownUriExceptionMapper.class)
314                                .build(SystemClient.class);
315    }
316
317    private Response success(String message) {
318        return Response.ok("{ \"ok\" : \"" + message + "\" }").build();
319    }
320
321    private Response fail(String message) {
322        return Response.status(Response.Status.BAD_REQUEST)
323                       .entity("{ \"error\" : \"" + message + "\" }")
324                       .build();
325    }
326}

Import the Counted annotation and apply it to the POST /api/systems, PUT /api/systems/{hostname}, DELETE /api/systems/{hostname}, and POST /api/systems/client/{hostname} endpoints to monotonically count how many times that the endpoints are accessed.

Additional information about the annotations that MicroProfile metrics provides, relevant metadata fields, and more are available at the MicroProfile Metrics Annotation Javadoc.

Point your browser to the http://localhost:9080/openapi/ui URL to try out your application and call some of the endpoints that you annotated.

MicroProfile Metrics provides 4 different REST endpoints.

  • The /metrics endpoint provides you with all the metrics in text format.

  • The /metrics?scope=application endpoint provides you with application-specific metrics.

  • The /metrics?scope=base endpoint provides you with metrics that are defined in MicroProfile specifications. Metrics in the base scope are intended to be portable between different MicroProfile-compatible runtimes.

  • The /metrics?scope=vendor endpoint provides you with metrics that are specific to the runtime.

Point your browser to the https://localhost:9443/metrics URL to review all the metrics that are enabled through MicroProfile Metrics. Log in with bob as your username and bobpwd as your password. You can see the metrics in text format.

To see only the application metrics, point your browser to https://localhost:9443/metrics?scope=application. You can expect to see your application metrics in the output.

# HELP updateSystem_total Number of times updating a system endpoint is called
# TYPE updateSystem_total counter
updateSystem_total{mp_scope="application",} 1.0
# HELP removeSystem_total Number of times removing a system endpoint is called
# TYPE removeSystem_total counter
removeSystem_total{mp_scope="application",} 1.0
# HELP addSystemClient_total Number of times adding a system by client is called
# TYPE addSystemClient_total counter
addSystemClient_total{mp_scope="application",} 0.0
# HELP addSystem_total Number of times adding system endpoint is called
# TYPE addSystem_total counter
addSystem_total{mp_scope="application",} 1.0

You can see the system metrics at the https://localhost:9443/metrics?scope=base URL. You can also see the vendor metrics at the https://localhost:9443/metrics?scope=vendor URL.

Building the container 

Press CTRL+C in the command-line session to stop the gradlew libertyDev dev mode that you started in the previous section.

Navigate to your application directory:

cd start/inventory

The first step to containerizing your application inside of a container is creating a Containerfile. A Containerfile is a collection of instructions for building a container image that can then be run as a container.

Make sure to start your podman daemon before you proceed.

Create the Containerfile in the start/inventory directory.
Containerfile

Containerfile

 1# tag::from[]
 2FROM icr.io/appcafe/open-liberty:full-java11-openj9-ubi
 3# end::from[]
 4
 5ARG VERSION=1.0
 6ARG REVISION=SNAPSHOT
 7
 8# tag::label[]
 9LABEL \
10  org.opencontainers.image.authors="My Name" \
11  org.opencontainers.image.vendor="Open Liberty" \
12  org.opencontainers.image.url="local" \
13  org.opencontainers.image.source="https://github.com/OpenLiberty/draft-guide-liberty-deepdive" \
14  org.opencontainers.image.version="$VERSION" \
15  org.opencontainers.image.revision="$REVISION" \
16  vendor="Open Liberty" \
17  name="inventory" \
18  version="$VERSION-$REVISION" \
19  summary="" \
20  description="This image contains the inventory microservice running with the Open Liberty runtime."
21# end::label[]
22
23USER root
24
25# tag::copy[]
26# tag::copy-config[]
27# tag::config-userID[]
28COPY --chown=1001:0 \
29# end::config-userID[]
30    # tag::inventory-config[]
31    src/main/liberty/config/ \
32    # end::inventory-config[]
33    # tag::config[]
34    /config/
35    # end::config[]
36# end::copy-config[]
37
38# tag::copy-war[]
39# tag::war-userID[]
40COPY --chown=1001:0 \
41# end::war-userID[]
42    # tag::inventory-war[]
43    build/libs/inventory.war \
44    # end::inventory-war[]
45    # tag::config-apps[]
46    /config/apps
47    # end::config-apps[]
48# end::copy-war[]
49# end::copy[]
50
51USER 1001
52
53RUN configure.sh

The FROM instruction initializes a new build stage and indicates the parent image from which your image is built. In this case, you’re using the icr.io/appcafe/open-liberty:full-java11-openj9-ubi image that comes with the latest Open Liberty runtime as your parent image.

To help you manage your images, you can label your container images with the LABEL command.

The COPY instructions are structured as COPY [--chown=<user>:<group>] <source> <destination>. They copy local files into the specified destination within your container image. In this case, the first COPY instruction copies the server configuration file that is at src/main/liberty/config/server.xml to the /config/ destination directory. Similarly, the second COPY instruction copies the .war file to the /config/apps destination directory.

Building the container image

Run the war task from the start/inventory directory so that the .war file resides in the build/libs directory.

gradlew war
./gradlew war

Build your container image with the following commands:

podman build -t liberty-deepdive-inventory:1.0-SNAPSHOT .

When the build finishes, run the following command to list all local container images:

podman images

Verify that the liberty-deepdive-inventory:1.0-SNAPSHOT image is listed among the container images, for example:

REPOSITORY                             TAG
localhost/liberty-deepdive-inventory   1.0-SNAPSHOT
icr.io/appcafe/open-liberty            full-java11-openj9-ubi

Running the application in container

Now that the inventory container image is built, you will run the application in container:

podman run -d --name inventory -p 9080:9080 liberty-deepdive-inventory:1.0-SNAPSHOT

The following table describes the flags in this command:

Flag Description

-d

Runs the container in the background.

--name

Specifies a name for the container.

-p

Maps the host ports to the container ports. For example: -p <HOST_PORT>:<CONTAINER_PORT>

Next, run the podman ps command to verify that your container is started:

podman ps

Make sure that your container is running and show Up as their status:

CONTAINER ID    IMAGE                                              COMMAND                CREATED          STATUS          PORTS                                        NAMES
2b584282e0f5    localhost/liberty-deepdive-inventory:1.0-SNAPSHOT  /opt/ol/wlp/bin/s...   8 seconds ago    Up 8 second     0.0.0.0:9080->9080/tcp   inventory

If a problem occurs and your container exit prematurely, the container don’t appear in the container list that the podman ps command displays. Instead, your container appear with an Exited status when you run the podman ps -a command. Run the podman logs inventory command to view the container logs for any potential problems. Run the podman stats inventory command to display a live stream of usage statistics for your container. You can also double-check that your Containerfile file is correct. When you find the cause of the issues, remove the faulty container with the podman rm inventory command. Rebuild your image, and start the container again.

Now, you can access the application by the http://localhost:9080/inventory/api/systems URL. Alternatively, for the updated OpenAPI UI, use the following URL http://localhost:9080/openapi/ui/.

When you’re finished trying out the application, run the following commands to stop the container:

podman stop inventory
podman rm inventory

To learn how to optimize the image size, check out the Containerizing microservices with Podman guide.

Support Licensing

Open Liberty is open source under the Eclipse Public License v1 so there is no fee to use it in production. Community support is available at StackOverflow, Gitter, or the mail list, and bugs can be raised in GitHub. Commercial support is available for Open Liberty from IBM. For more information, see the IBM Marketplace. The WebSphere Liberty product is built on Open Liberty. No migration is required to use WebSphere Liberty, you simply point to WebSphere Liberty in your build. WebSphere Liberty users get support for the packaged Open Liberty function.

WebSphere Liberty is also available in Maven Central.

You can use WebSphere Liberty for development even without purchasing it. However, if you have production entitlement, you can easily change to use it with the following steps.

In the build.gradle, add the liberty element as the following:

liberty {
    runtime = [ 'group':'com.ibm.websphere.appserver.runtime',
                'name':'wlp-kernel']
}

Rebuild and restart the inventory service by dev mode:

./gradlew clean
./gradlew libertyDev

In the Containerfile, replace the Liberty image at the FROM statement with websphere-liberty as shown in the following example:

FROM icr.io/appcafe/websphere-liberty:full-java11-openj9-ubi

ARG VERSION=1.0
ARG REVISION=SNAPSHOT
...

Great work! You’re done!

You just completed a hands-on deep dive on Liberty!

Guide Attribution

A Technical Deep Dive on Liberty by Open Liberty is licensed under CC BY-ND 4.0

Copy file contents

Prerequisites:

Nice work! Where to next?

What did you think of this guide?

Extreme Dislike Dislike Like Extreme Like

What could make this guide better?

Raise an issue to share feedback

Create a pull request to contribute to this guide

Need help?

Ask a question on Stack Overflow

Like Open Liberty? Star our repo on GitHub.

Star