A Technical Deep Dive on Liberty

duration 120 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 Maven 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 and deploy it to Kubernetes in a Liberty Docker container. 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

Docker must be installed before you start the Persisting Data module. For installation instructions, refer to the official Docker documentation. You’ll build and run the application in Docker containers.

Make sure to start your Docker daemon before you proceed.

Also, Kubernetes must be installed before you start the Deploying the microservice to Kubernetes.

Use Docker Desktop, where a local Kubernetes environment is preinstalled and enabled. If you do not see the Kubernetes tab, then upgrade to the latest version of Docker Desktop.

Complete the setup for your operating system:

After you complete the Docker setup instructions for your operating system, ensure that Kubernetes (not Swarm) is selected as the orchestrator in Docker Preferences.

Use Docker Desktop, where a local Kubernetes environment is preinstalled and enabled. If you do not see the Kubernetes tab, then upgrade to the latest version of Docker Desktop.

Complete the setup for your operating system:

After you complete the Docker setup instructions for your operating system, ensure that Kubernetes (not Swarm) is selected as the orchestrator in Docker Preferences.

You will use Minikube as a single-node Kubernetes cluster that runs locally in a virtual machine. Make sure you have kubectl installed. If you need to install kubectl, see the kubectl installation instructions. For Minikube installation instructions, see the Minikube documentation.

Getting started

Clone the Git repository:

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

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 Open Liberty Starter is used to create the starting point of the application. Maven is used as the selected build tool and the application uses of Jakarta EE 9.1 and MicroProfile 5.

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: Maven

  • Under Java SE Version select: your version

  • Under Java EE/Jakarta EE Version select: 9.1

  • Under MicroProfile Version select: 5

Then, click Generate Project, which downloads the starter project as inventory.zip file.

Next, extract the inventory.zip file on your system. Move the contents of this extracted inventory directory to the start directory of this project, which is located at the following path: guide-liberty-deepdive/start/inventory

Building the application

This application is configured to be built with Maven. Every Maven-configured project contains a pom.xml file that defines the project configuration, dependencies, and plug-ins.

pom.xml

 1<?xml version="1.0" encoding="UTF-8" ?>
 2<project xmlns="http://maven.apache.org/POM/4.0.0"
 3    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 5    <modelVersion>4.0.0</modelVersion>
 6
 7    <groupId>io.openliberty.deepdive</groupId>
 8    <artifactId>inventory</artifactId>
 9    <version>1.0-SNAPSHOT</version>
10    <packaging>war</packaging>
11
12    <properties>
13        <maven.compiler.source>11</maven.compiler.source>
14        <maven.compiler.target>11</maven.compiler.target>
15        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
16    </properties>
17
18    <dependencies>
19        <dependency>
20            <groupId>jakarta.platform</groupId>
21            <artifactId>jakarta.jakartaee-api</artifactId>
22            <version>9.1.0</version>
23            <scope>provided</scope>
24        </dependency>
25        <dependency>
26            <groupId>org.eclipse.microprofile</groupId>
27            <artifactId>microprofile</artifactId>
28            <version>5.0</version>
29            <type>pom</type>
30            <scope>provided</scope>
31        </dependency>
32    </dependencies>
33
34    <build>
35        <finalName>inventory</finalName>
36        <pluginManagement>
37            <plugins>
38                <plugin>
39                    <groupId>org.apache.maven.plugins</groupId>
40                    <artifactId>maven-war-plugin</artifactId>
41                    <version>3.3.2</version>
42                </plugin>
43                <!-- tag::libertyMavenPlugin[] -->
44                <plugin>
45                    <groupId>io.openliberty.tools</groupId>
46                    <artifactId>liberty-maven-plugin</artifactId>
47                    <version>3.5.1</version>
48                </plugin>
49                <!-- end::libertyMavenPlugin[] -->
50            </plugins>
51        </pluginManagement>
52        <plugins>
53            <plugin>
54                <groupId>io.openliberty.tools</groupId>
55                <artifactId>liberty-maven-plugin</artifactId>
56            </plugin>
57        </plugins>
58    </build>
59</project>

Your pom.xml file is located in the start/inventory directory and is configured to include the liberty-maven-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 start/inventory

Build and deploy the inventory microservice to Liberty by running the Maven liberty:run goal:

mvn liberty:run

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

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

For more information about the Liberty Maven 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 liberty:stop goal from the start/inventory directory in another command-line session:

mvn liberty:stop

Starting and stopping the Liberty server in the background

Although you can start and stop the server in the foreground by using the Maven liberty:run goal, you can also start and stop the server in the background with the Maven liberty:start and liberty:stop goals:

mvn liberty:start
mvn liberty:stop

Updating the server configuration without restarting the server

The Liberty Maven plug-in includes a dev goal 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 liberty:dev goal in the start/inventory directory:

mvn liberty:dev

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 Maven commands, 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 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 v1.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-v10.html
 8 *
 9 * Contributors:
10 *     IBM Corporation - Initial implementation
11 *******************************************************************************/
12// end::copyright[]
13package io.openliberty.deepdive.rest;
14
15import java.util.ArrayList;
16import java.util.Collections;
17import java.util.List;
18
19import io.openliberty.deepdive.rest.model.SystemData;
20import jakarta.enterprise.context.ApplicationScoped;
21
22@ApplicationScoped
23public class Inventory {
24
25    private List<SystemData> systems = Collections.synchronizedList(new ArrayList<>());
26
27    public List<SystemData> getSystems() {
28        return systems;
29    }
30
31    // tag::getSystem[]
32    public SystemData getSystem(String hostname) {
33        for (SystemData s : systems) {
34            if (s.getHostname().equalsIgnoreCase(hostname)) {
35                return s;
36            }
37        }
38        return null;
39    }
40    // end::getSystem[]
41
42    // tag::add[]
43    public void add(String hostname, String osName, String javaVersion, Long heapSize) {
44        systems.add(new SystemData(hostname, osName, javaVersion, heapSize));
45    }
46    // end::add[]
47
48    // tag::update[]
49    public void update(SystemData s) {
50        for (SystemData systemData : systems) {
51            if (systemData.getHostname().equalsIgnoreCase(s.getHostname())) {
52                systemData.setOsName(s.getOsName());
53                systemData.setJavaVersion(s.getJavaVersion());
54                systemData.setHeapSize(s.getHeapSize());
55            }
56        }
57    }
58    // end::update[]
59
60    // tag::removeSystem[]
61    public boolean removeSystem(SystemData s) {
62        return systems.remove(s);
63    }
64    // end::removeSystem[]
65}

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 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 v1.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-v10.html
 8 *
 9 * Contributors:
10 *     IBM Corporation - Initial implementation
11 *******************************************************************************/
12// end::copyright[]
13package io.openliberty.deepdive.rest.model;
14
15public class SystemData {
16
17    private int id;
18    private String hostname;
19    private String osName;
20    private String javaVersion;
21    private Long   heapSize;
22
23    public SystemData() {
24    }
25
26    public SystemData(String hostname, String osName, String javaVersion, Long heapSize) {
27        this.hostname = hostname;
28        this.osName = osName;
29        this.javaVersion = javaVersion;
30        this.heapSize = heapSize;
31    }
32
33    public int getId() {
34        return id;
35    }
36
37    public void setId(int id) {
38        this.id = id;
39    }
40
41    public String getHostname() {
42        return hostname;
43    }
44
45    public void setHostname(String hostname) {
46        this.hostname = hostname;
47    }
48
49    public String getOsName() {
50        return osName;
51    }
52
53    public void setOsName(String osName) {
54        this.osName = osName;
55    }
56
57    public String getJavaVersion() {
58        return javaVersion;
59    }
60
61    public void setJavaVersion(String javaVersion) {
62        this.javaVersion = javaVersion;
63    }
64
65    public Long getHeapSize() {
66        return heapSize;
67    }
68
69    public void setHeapSize(Long heapSize) {
70        this.heapSize = heapSize;
71    }
72
73    @Override
74    public boolean equals(Object host) {
75      if (host instanceof SystemData) {
76        return hostname.equals(((SystemData) host).getHostname());
77      }
78      return false;
79    }
80}

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

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.

pom.xml

 1<?xml version="1.0" encoding="UTF-8" ?>
 2<project xmlns="http://maven.apache.org/POM/4.0.0"
 3    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 5    <modelVersion>4.0.0</modelVersion>
 6
 7    <groupId>io.openliberty.deepdive</groupId>
 8    <artifactId>inventory</artifactId>
 9    <version>1.0-SNAPSHOT</version>
10    <packaging>war</packaging>
11
12    <properties>
13        <maven.compiler.source>11</maven.compiler.source>
14        <maven.compiler.target>11</maven.compiler.target>
15        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
16    </properties>
17
18    <dependencies>
19        <dependency>
20            <groupId>jakarta.platform</groupId>
21            <artifactId>jakarta.jakartaee-api</artifactId>
22            <version>9.1.0</version>
23            <scope>provided</scope>
24        </dependency>
25        <!-- tag::mp5[] -->
26        <dependency>
27            <groupId>org.eclipse.microprofile</groupId>
28            <artifactId>microprofile</artifactId>
29            <version>5.0</version>
30            <type>pom</type>
31            <scope>provided</scope>
32        </dependency>
33        <!-- end::mp5[] -->
34    </dependencies>
35
36    <build>
37        <finalName>inventory</finalName>
38        <pluginManagement>
39            <plugins>
40                <plugin>
41                    <groupId>org.apache.maven.plugins</groupId>
42                    <artifactId>maven-war-plugin</artifactId>
43                    <version>3.3.2</version>
44                </plugin>
45                <plugin>
46                    <groupId>io.openliberty.tools</groupId>
47                    <artifactId>liberty-maven-plugin</artifactId>
48                    <version>3.5.1</version>
49                </plugin>
50            </plugins>
51        </pluginManagement>
52        <plugins>
53            <plugin>
54                <groupId>io.openliberty.tools</groupId>
55                <artifactId>liberty-maven-plugin</artifactId>
56            </plugin>
57        </plugins>
58    </build>
59</project>

server.xml

 1<?xml version="1.0" encoding="UTF-8"?>
 2<server description="inventory">
 3
 4    <!-- Enable features -->
 5    <featureManager>
 6        <feature>jakartaee-9.1</feature>
 7        <!-- tag::mp5[] -->
 8        <feature>microProfile-5.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    <!-- Configures the application on a specified context root -->
21    <webApplication contextRoot="/inventory" location="inventory.war" />
22
23    <!-- Default SSL configuration enables trust for default certificates from the Java runtime -->
24    <ssl id="defaultSSLConfig" trustDefaultCerts="true" />
25</server>

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

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 avaialble 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 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 v1.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-v10.html
 8 *
 9 * Contributors:
10 *     IBM Corporation - Initial implementation
11 *******************************************************************************/
12// end::copyright[]
13package io.openliberty.deepdive.rest.model;
14
15import org.eclipse.microprofile.openapi.annotations.media.Schema;
16//tag::SystemDataSchema[]
17@Schema(name = "SystemData",
18        description = "POJO that represents a single inventory entry.")
19public class SystemData {
20//end::SystemDataSchema[]
21
22    private int id;
23
24    //tag::hostnameSchema[]
25    @Schema(required = true)
26    private String hostname;
27    //end::hostnameSchema[]
28
29    private String osName;
30    private String javaVersion;
31    private Long   heapSize;
32
33    public SystemData() {
34    }
35
36    public SystemData(String hostname, String osName, String javaVer, Long heapSize) {
37        this.hostname = hostname;
38        this.osName = osName;
39        this.javaVersion = javaVer;
40        this.heapSize = heapSize;
41    }
42
43    public int getId() {
44        return id;
45    }
46
47    public void setId(int id) {
48        this.id = id;
49    }
50
51    public String getHostname() {
52        return hostname;
53    }
54
55    public void setHostname(String hostname) {
56        this.hostname = hostname;
57    }
58
59    public String getOsName() {
60        return osName;
61    }
62
63    public void setOsName(String osName) {
64        this.osName = osName;
65    }
66
67    public String getJavaVersion() {
68        return javaVersion;
69    }
70
71    public void setJavaVersion(String javaVersion) {
72        this.javaVersion = javaVersion;
73    }
74
75    public Long getHeapSize() {
76        return heapSize;
77    }
78
79    public void setHeapSize(Long heapSize) {
80        this.heapSize = heapSize;
81    }
82
83    @Override
84    public boolean equals(Object host) {
85      if (host instanceof SystemData) {
86        return hostname.equals(((SystemData) host).getHostname());
87      }
88      return false;
89    }
90}

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-9.1</feature>
 7        <feature>microProfile-5.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 pom.xml file.
pom.xml

pom.xml

 1<?xml version="1.0" encoding="UTF-8" ?>
 2<project xmlns="http://maven.apache.org/POM/4.0.0"
 3    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 5    <modelVersion>4.0.0</modelVersion>
 6
 7    <groupId>io.openliberty.deepdive</groupId>
 8    <artifactId>inventory</artifactId>
 9    <version>1.0-SNAPSHOT</version>
10    <packaging>war</packaging>
11
12    <properties>
13        <maven.compiler.source>11</maven.compiler.source>
14        <maven.compiler.target>11</maven.compiler.target>
15        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
16        <!-- tag::httpPort[] -->
17        <liberty.var.default.http.port>9080</liberty.var.default.http.port>
18        <!-- end::httpPort[] -->
19        <!-- tag::httpsPort[] -->
20        <liberty.var.default.https.port>9443</liberty.var.default.https.port>
21        <!-- end::httpsPort[] -->
22        <!-- tag::contextRoot[] -->
23        <liberty.var.default.context.root>/inventory</liberty.var.default.context.root>
24        <!-- end::contextRoot[] -->
25    </properties>
26
27    <dependencies>
28        <dependency>
29            <groupId>jakarta.platform</groupId>
30            <artifactId>jakarta.jakartaee-api</artifactId>
31            <version>9.1.0</version>
32            <scope>provided</scope>
33        </dependency>
34        <dependency>
35            <groupId>org.eclipse.microprofile</groupId>
36            <artifactId>microprofile</artifactId>
37            <version>5.0</version>
38            <type>pom</type>
39            <scope>provided</scope>
40        </dependency>
41    </dependencies>
42
43    <build>
44        <finalName>inventory</finalName>
45        <pluginManagement>
46            <plugins>
47                <plugin>
48                    <groupId>org.apache.maven.plugins</groupId>
49                    <artifactId>maven-war-plugin</artifactId>
50                    <version>3.3.2</version>
51                </plugin>
52                <plugin>
53                    <groupId>io.openliberty.tools</groupId>
54                    <artifactId>liberty-maven-plugin</artifactId>
55                    <version>3.5.1</version>
56                </plugin>
57            </plugins>
58        </pluginManagement>
59        <plugins>
60            <plugin>
61                <groupId>io.openliberty.tools</groupId>
62                <artifactId>liberty-maven-plugin</artifactId>
63            </plugin>
64        </plugins>
65    </build>
66</project>

Add properties for the HTTP port, HTTPS port, and the context root to the pom.xml file.

You can try changing the value of these variables in the pom.xml file:

  • update liberty.var.default.http.port to 9081

  • update liberty.var.default.https.port to 9445

  • update liberty.var.default.context.root to /trial.

Because you are using dev mode, these changes are automatically picked up by the server.

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/.

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

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

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

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

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

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.

Persisting data

Next, you’ll persist the system data into the PostgreSQL database by using the Jakarta Persistence API (JPA).

Navigate to your application directory.

cd start/inventory

Defining a JPA entity class

To store Java objects in a database, you must define a JPA entity class. A JPA entity is a Java object whose nontransient and nonstatic fields are persisted to the database. Any POJO class can be designated as a JPA entity. However, the class must be annotated with the @Entity annotation, must not be declared final, and must have a public or protected nonargument constructor. JPA maps an entity type to a database table and persisted instances will be represented as rows in the table.

The SystemData class is a data model that represents systems in the inventory microservice. Annotate it with JPA 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 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 v1.0
  6 * which accompanies this distribution, and is available at
  7 * http://www.eclipse.org/legal/epl-v10.html
  8 *
  9 * Contributors:
 10 *     IBM Corporation - Initial implementation
 11 *******************************************************************************/
 12// end::copyright[]
 13package io.openliberty.deepdive.rest.model;
 14
 15import java.io.Serializable;
 16
 17import org.eclipse.microprofile.openapi.annotations.media.Schema;
 18
 19import jakarta.persistence.Column;
 20import jakarta.persistence.Entity;
 21import jakarta.persistence.GeneratedValue;
 22import jakarta.persistence.GenerationType;
 23import jakarta.persistence.Id;
 24import jakarta.persistence.NamedQuery;
 25import jakarta.persistence.SequenceGenerator;
 26import jakarta.persistence.Table;
 27
 28@Schema(name = "SystemData",
 29        description = "POJO that represents a single inventory entry.")
 30// tag::Entity[]
 31@Entity
 32// end::Entity[]
 33// tag::Table[]
 34@Table(name = "SystemData")
 35// end::Table[]
 36// tag::findAll[]
 37@NamedQuery(name = "SystemData.findAll", query = "SELECT e FROM SystemData e")
 38//end::findAll[]
 39//tag::findSystem[]
 40@NamedQuery(name = "SystemData.findSystem",
 41            query = "SELECT e FROM SystemData e WHERE e.hostname = :hostname")
 42// end::findSystem[]
 43// tag::SystemData[]
 44public class SystemData implements Serializable {
 45    private static final long serialVersionUID = 1L;
 46
 47    // tag::GeneratedValue[]
 48    @SequenceGenerator(name = "SEQ",
 49                       sequenceName = "systemData_id_seq",
 50                       allocationSize = 1)
 51    @GeneratedValue(strategy = GenerationType.IDENTITY, generator = "SEQ")
 52    // end::GeneratedValue[]
 53    // tag::Id[]
 54    @Id
 55    // end::Id[]
 56    // tag::columnId[]
 57    @Column(name = "id")
 58    // end::columnId[]
 59    private int id;
 60
 61    @Schema(required = true)
 62    // tag::columnHostname[]
 63    @Column(name = "hostname")
 64    // end::columnHostname[]
 65    private String hostname;
 66
 67    // tag::columnOsName[]
 68    @Column(name = "osName")
 69    // end::columnOsName[]
 70    private String osName;
 71    // tag::columnJavaVersion[]
 72    @Column(name = "javaVersion")
 73    // end::columnJavaVersion[]
 74    private String javaVersion;
 75    // tag::columnHeapSize[]
 76    @Column(name = "heapSize")
 77    // end::columnHeapSize[]
 78    private Long heapSize;
 79
 80    public SystemData() {
 81    }
 82
 83    public SystemData(String hostname, String osName, String javaVer, Long heapSize) {
 84        this.hostname = hostname;
 85        this.osName = osName;
 86        this.javaVersion = javaVer;
 87        this.heapSize = heapSize;
 88    }
 89
 90    public int getId() {
 91        return id;
 92    }
 93
 94    public void setId(int id) {
 95        this.id = id;
 96    }
 97
 98    public String getHostname() {
 99        return hostname;
100    }
101
102    public void setHostname(String hostname) {
103        this.hostname = hostname;
104    }
105
106    public String getOsName() {
107        return osName;
108    }
109
110    public void setOsName(String osName) {
111        this.osName = osName;
112    }
113
114    public String getJavaVersion() {
115        return javaVersion;
116    }
117
118    public void setJavaVersion(String javaVersion) {
119        this.javaVersion = javaVersion;
120    }
121
122    public Long getHeapSize() {
123        return heapSize;
124    }
125
126    public void setHeapSize(Long heapSize) {
127        this.heapSize = heapSize;
128    }
129
130    @Override
131    public int hashCode() {
132        return hostname.hashCode();
133    }
134
135    @Override
136    public boolean equals(Object host) {
137        if (host instanceof SystemData) {
138            return hostname.equals(((SystemData) host).getHostname());
139        }
140        return false;
141    }
142}
143// end::SystemData[]

The following table breaks down the new annotations:

Annotation Description

@Entity

Declares the class as an entity.

@Table

Specifies details of the table such as name.

@NamedQuery

Specifies a predefined database query that is run by an EntityManager instance.

@Id

Declares the primary key of the entity.

@GeneratedValue

Specifies the strategy that is used for generating the value of the primary key. The strategy = GenerationType.IDENTITY code indicates that the database automatically increments the inventoryid upon inserting it into the database.

@Column

Specifies that the field is mapped to a column in the database table. The name attribute is optional and indicates the name of the column in the table.

Performing CRUD operations using JPA

The create, retrieve, update, and delete (CRUD) operations are defined in the Inventory. To perform these operations by using JPA, you need to update the Inventory class.

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

Inventory.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2022 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 v1.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-v10.html
 8 *
 9 * Contributors:
10 *     IBM Corporation - Initial implementation
11 *******************************************************************************/
12// end::copyright[]
13package io.openliberty.deepdive.rest;
14
15import java.util.List;
16
17import io.openliberty.deepdive.rest.model.SystemData;
18import jakarta.enterprise.context.ApplicationScoped;
19import jakarta.persistence.EntityManager;
20import jakarta.persistence.PersistenceContext;
21
22@ApplicationScoped
23// tag::Inventory[]
24public class Inventory {
25
26    // tag::PersistenceContext[]
27    @PersistenceContext(name = "jpa-unit")
28    // end::PersistenceContext[]
29    private EntityManager em;
30
31    // tag::getSystems[]
32    public List<SystemData> getSystems() {
33        return em.createNamedQuery("SystemData.findAll", SystemData.class)
34                         .getResultList();
35    }
36    // end::getSystems[]
37
38    // tag::getSystem[]
39    public SystemData getSystem(String hostname) {
40        // tag::find[]
41        List<SystemData> systems = 
42            em.createNamedQuery("SystemData.findSystem", SystemData.class)
43              .setParameter("hostname", hostname)
44              .getResultList();
45        return systems == null || systems.isEmpty() ? null : systems.get(0);
46        // end::find[]          
47    }
48    // end::getSystem[]
49
50    // tag::add[]
51    public void add(String hostname, String osName, String javaVersion, Long heapSize) {
52        // tag::Persist[]
53        em.persist(new SystemData(hostname, osName, javaVersion, heapSize));
54        // end::Persist[]
55    }
56    // end::add[]
57
58    // tag::update[]
59    public void update(SystemData s) {
60            // tag::Merge[]
61            em.merge(s);
62            // end::Merge[]
63    }
64    // end::update[]
65
66    // tag::removeSystem[]
67    public void removeSystem(SystemData s) {
68        // tag::Remove[]
69            em.remove(s);
70            // end::Remove[]
71    }
72    // end::removeSystem[]
73
74}
75// end::Inventory[]

To use the entity manager at run time, inject it into your CDI bean through the @PersistenceContext annotation. The entity manager interacts with the persistence context. Every EntityManager instance is associated with a persistence context. The persistence context manages a set of entities and is aware of the different states that an entity can have. The persistence context synchronizes with the database when a transaction commits.

SystemData.java

  1// tag::copyright[]
  2/*******************************************************************************
  3 * Copyright (c) 2022 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 v1.0
  6 * which accompanies this distribution, and is available at
  7 * http://www.eclipse.org/legal/epl-v10.html
  8 *
  9 * Contributors:
 10 *     IBM Corporation - Initial implementation
 11 *******************************************************************************/
 12// end::copyright[]
 13package io.openliberty.deepdive.rest.model;
 14
 15import java.io.Serializable;
 16
 17import org.eclipse.microprofile.openapi.annotations.media.Schema;
 18
 19import jakarta.persistence.Column;
 20import jakarta.persistence.Entity;
 21import jakarta.persistence.GeneratedValue;
 22import jakarta.persistence.GenerationType;
 23import jakarta.persistence.Id;
 24import jakarta.persistence.NamedQuery;
 25import jakarta.persistence.SequenceGenerator;
 26import jakarta.persistence.Table;
 27
 28@Schema(name = "SystemData",
 29        description = "POJO that represents a single inventory entry.")
 30// tag::Entity[]
 31@Entity
 32// end::Entity[]
 33// tag::Table[]
 34@Table(name = "SystemData")
 35// end::Table[]
 36// tag::findAll[]
 37@NamedQuery(name = "SystemData.findAll", query = "SELECT e FROM SystemData e")
 38//end::findAll[]
 39//tag::findSystem[]
 40@NamedQuery(name = "SystemData.findSystem",
 41            query = "SELECT e FROM SystemData e WHERE e.hostname = :hostname")
 42// end::findSystem[]
 43// tag::SystemData[]
 44public class SystemData implements Serializable {
 45    private static final long serialVersionUID = 1L;
 46
 47    // tag::GeneratedValue[]
 48    @SequenceGenerator(name = "SEQ",
 49                       sequenceName = "systemData_id_seq",
 50                       allocationSize = 1)
 51    @GeneratedValue(strategy = GenerationType.IDENTITY, generator = "SEQ")
 52    // end::GeneratedValue[]
 53    // tag::Id[]
 54    @Id
 55    // end::Id[]
 56    // tag::columnId[]
 57    @Column(name = "id")
 58    // end::columnId[]
 59    private int id;
 60
 61    @Schema(required = true)
 62    // tag::columnHostname[]
 63    @Column(name = "hostname")
 64    // end::columnHostname[]
 65    private String hostname;
 66
 67    // tag::columnOsName[]
 68    @Column(name = "osName")
 69    // end::columnOsName[]
 70    private String osName;
 71    // tag::columnJavaVersion[]
 72    @Column(name = "javaVersion")
 73    // end::columnJavaVersion[]
 74    private String javaVersion;
 75    // tag::columnHeapSize[]
 76    @Column(name = "heapSize")
 77    // end::columnHeapSize[]
 78    private Long heapSize;
 79
 80    public SystemData() {
 81    }
 82
 83    public SystemData(String hostname, String osName, String javaVer, Long heapSize) {
 84        this.hostname = hostname;
 85        this.osName = osName;
 86        this.javaVersion = javaVer;
 87        this.heapSize = heapSize;
 88    }
 89
 90    public int getId() {
 91        return id;
 92    }
 93
 94    public void setId(int id) {
 95        this.id = id;
 96    }
 97
 98    public String getHostname() {
 99        return hostname;
100    }
101
102    public void setHostname(String hostname) {
103        this.hostname = hostname;
104    }
105
106    public String getOsName() {
107        return osName;
108    }
109
110    public void setOsName(String osName) {
111        this.osName = osName;
112    }
113
114    public String getJavaVersion() {
115        return javaVersion;
116    }
117
118    public void setJavaVersion(String javaVersion) {
119        this.javaVersion = javaVersion;
120    }
121
122    public Long getHeapSize() {
123        return heapSize;
124    }
125
126    public void setHeapSize(Long heapSize) {
127        this.heapSize = heapSize;
128    }
129
130    @Override
131    public int hashCode() {
132        return hostname.hashCode();
133    }
134
135    @Override
136    public boolean equals(Object host) {
137        if (host instanceof SystemData) {
138            return hostname.equals(((SystemData) host).getHostname());
139        }
140        return false;
141    }
142}
143// end::SystemData[]

The Inventory class has a method for each CRUD operation, so let’s break them down:

  • The add() method persists an instance of the SystemData entity class to the data store by calling the persist() method on an EntityManager instance. The entity instance becomes managed and changes to it are tracked by the entity manager.

  • The getSystems() method demonstrates a way to retrieve system objects from the database. This method returns a list of instances of the SystemData entity class by using the SystemData.findAll query that is specified in the @NamedQuery annotation on the SystemData class. Similarly, the getSystem() method uses the SystemData.findSystem named query to find a system with the given hostname.

  • The update() method creates a managed instance of a detached entity instance. The entity manager automatically tracks all managed entity objects in its persistence context for changes and synchronizes them with the database. However, if an entity becomes detached, you must merge that entity into the persistence context by calling the merge() method so that changes to loaded fields of the detached entity are tracked.

  • The removeSystem() method removes an instance of the SystemData entity class from the database by calling the remove() method on an EntityManager instance. The state of the entity is changed to removed and is removed from the database upon transaction commit.

Declare the endpoints with transaction management.

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

SystemResource.java

  1// tag::copyright[]
  2/*******************************************************************************
  3 * Copyright (c) 2022 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 v1.0
  6 * which accompanies this distribution, and is available at
  7 * http://www.eclipse.org/legal/epl-v10.html
  8 *
  9 * Contributors:
 10 *     IBM Corporation - Initial implementation
 11 *******************************************************************************/
 12// end::copyright[]
 13package io.openliberty.deepdive.rest;
 14
 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;
 26
 27import org.eclipse.microprofile.config.inject.ConfigProperty;
 28
 29import io.openliberty.deepdive.rest.model.SystemData;
 30import jakarta.enterprise.context.ApplicationScoped;
 31import jakarta.inject.Inject;
 32import jakarta.transaction.Transactional;
 33import jakarta.ws.rs.Consumes;
 34import jakarta.ws.rs.DELETE;
 35import jakarta.ws.rs.GET;
 36import jakarta.ws.rs.POST;
 37import jakarta.ws.rs.PUT;
 38import jakarta.ws.rs.Path;
 39import jakarta.ws.rs.PathParam;
 40import jakarta.ws.rs.Produces;
 41import jakarta.ws.rs.QueryParam;
 42import jakarta.ws.rs.core.MediaType;
 43import jakarta.ws.rs.core.Response;
 44
 45@ApplicationScoped
 46@Path("/systems")
 47// tag::SystemResource[]
 48public class SystemResource {
 49
 50    // tag::inventory[]
 51    @Inject
 52    Inventory inventory;
 53    // end::inventory[]
 54
 55    // tag::inject[]
 56    @Inject
 57    // end::inject[]
 58    // tag::configProperty[]
 59    @ConfigProperty(name = "client.https.port")
 60    // end::configProperty[]
 61    String CLIENT_PORT;
 62
 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    // tag::postTransactional[]
102    @POST
103    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
104    @Produces(MediaType.APPLICATION_JSON)
105    @Transactional
106    // end::postTransactional[]
107    @APIResponses(value = {
108        @APIResponse(responseCode = "200",
109            description = "Successfully added system to inventory"),
110        @APIResponse(responseCode = "400",
111            description = "Unable to add system to inventory")
112    })
113    @Parameters(value = {
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    })
135    @Operation(
136        summary = "Add system",
137        description = "Add a system and its data to the inventory.",
138        operationId = "addSystem"
139    )
140    public Response addSystem(
141        @QueryParam("hostname") String hostname,
142        @QueryParam("osName") String osName,
143        @QueryParam("javaVersion") String javaVersion,
144        @QueryParam("heapSize") Long heapSize) {
145
146        SystemData s = inventory.getSystem(hostname);
147        if (s != null) {
148            return fail(hostname + " already exists.");
149        }
150        inventory.add(hostname, osName, javaVersion, heapSize);
151        return success(hostname + " was added.");
152    }
153
154    // tag::putTransactional[]
155    @PUT
156    @Path("/{hostname}")
157    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
158    @Produces(MediaType.APPLICATION_JSON)
159    @Transactional
160    // end::putTransactional[]
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::deleteTransactional[]
213    @DELETE
214    @Path("/{hostname}")
215    @Produces(MediaType.APPLICATION_JSON)
216    @Transactional
217    // end::deleteTransactional[]
218    @APIResponses(value = {
219        @APIResponse(responseCode = "200",
220            description = "Successfully deleted the system from inventory"),
221        @APIResponse(responseCode = "400",
222            description =
223                "Unable to delete because the system does not exist in the inventory")
224    })
225    @Parameter(
226        name = "hostname", in = ParameterIn.PATH,
227        description = "The hostname of the system",
228        required = true, example = "localhost",
229        schema = @Schema(type = SchemaType.STRING)
230    )
231    @Operation(
232        summary = "Remove system",
233        description = "Removes a system from the inventory.",
234        operationId = "removeSystem"
235    )
236    public Response removeSystem(@PathParam("hostname") String hostname) {
237        SystemData s = inventory.getSystem(hostname);
238        if (s != null) {
239            inventory.removeSystem(s);
240            return success(hostname + " was removed.");
241        } else {
242            return fail(hostname + " does not exists.");
243        }
244    }
245
246    @POST
247    @Path("/client/{hostname}")
248    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
249    @Produces(MediaType.APPLICATION_JSON)
250    @Transactional
251    @APIResponses(value = {
252        @APIResponse(responseCode = "200",
253            description = "Successfully added system client"),
254        @APIResponse(responseCode = "400",
255            description = "Unable to add system client")
256    })
257    @Parameter(
258        name = "hostname", in = ParameterIn.PATH,
259        description = "The hostname of the system",
260        required = true, example = "localhost",
261        schema = @Schema(type = SchemaType.STRING)
262    )
263    @Operation(
264        summary = "Add system client",
265        description = "This adds a system client.",
266        operationId = "addSystemClient"
267    )
268    //tag::printClientPort[]
269    public Response addSystemClient(@PathParam("hostname") String hostname) {
270        System.out.println(CLIENT_PORT);
271        return success("Client Port: " + CLIENT_PORT);
272    }
273    //end::printClientPort[]
274
275    private Response success(String message) {
276        return Response.ok("{ \"ok\" : \"" + message + "\" }").build();
277    }
278
279    private Response fail(String message) {
280        return Response.status(Response.Status.BAD_REQUEST)
281                       .entity("{ \"error\" : \"" + message + "\" }")
282                       .build();
283    }
284}
285// end::SystemResource[]

The @Transactional annotation is used in the POST, PUT, and DELETE endpoints of the SystemResource class to declaratively control the transaction boundaries on the inventory CDI bean. This configuration ensures that the methods run within the boundaries of an active global transaction, and therefore you don’t need to explicitly begin, commit, or rollback transactions. At the end of the transactional method invocation, the transaction commits and the persistence context flushes any changes to the Event entity instances that it is managing to the database.

Configuring JPA

The persistence.xml file is a configuration file that defines a persistence unit. The persistence unit specifies configuration information for the entity manager.

Create the configuration file.
src/main/resources/META-INF/persistence.xml

persistence.xml

 1<?xml version="1.0" encoding="UTF-8"?>
 2<persistence version="2.2"
 3    xmlns="http://xmlns.jcp.org/xml/ns/persistence" 
 4    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 5    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence 
 6                        http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
 7    <!-- tag::persistence-unit[] -->
 8    <!-- tag::transaction-type[] -->
 9    <persistence-unit name="jpa-unit" transaction-type="JTA">
10    <!-- end::transaction-type[] -->
11      <!-- tag::jta-data[] -->
12      <jta-data-source>jdbc/postgresql</jta-data-source>
13      <!-- end::jta-data[] -->
14      <exclude-unlisted-classes>false</exclude-unlisted-classes>
15      <properties>
16        <property name="eclipselink.target-database" value="PostgreSQL"/>
17        <property name="eclipselink.ddl-generation" value="create-or-extend-tables" /> 
18        <property name="eclipselink.logging.level" value="FINE"/>
19        <property name="eclipselink.jdbc.url" value="jdbc/postgresql:/admin"/>         
20        <property name="eclipselink.persistence.jdbc.driver" value="org.postgresql.Driver"/>  
21        <property name="eclipselink.persistence.jdbc.user" value="admin"/>  
22        <property name="eclipselink.persistence.jdbc.password" value="adminpwd"/> 
23      </properties>
24    </persistence-unit>
25    <!-- end::persistence-unit[] -->
26</persistence>

The persistence unit is defined by the persistence-unit XML element. The name attribute is required. This attribute identifies the persistent unit when you use the @PersistenceContext annotation to inject the entity manager later in this exercise. The transaction-type="JTA" attribute specifies to use Java Transaction API (JTA) transaction management. When you use a container-managed entity manager, you must use JTA transactions.

A JTA transaction type requires a JTA data source to be provided. The jta-data-source element specifies the Java Naming and Directory Interface (JNDI) name of the data source that is used.

Configure the jdbc/postgresql data source in the Liberty server configuration file.

Replace the server.xml configuration 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-9.1</feature>
 6        <feature>microProfile-5.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    <variable name="postgres/hostname" defaultValue="localhost" />
13    <variable name="postgres/portnum" defaultValue="5432" />
14
15    <httpEndpoint id="defaultHttpEndpoint"
16                  httpPort="${default.http.port}" 
17                  httpsPort="${default.https.port}" />
18
19    <!-- Automatically expand WAR files and EAR files -->
20    <applicationManager autoExpand="true"/>
21
22    <!-- Configures the application on a specified context root -->
23    <webApplication contextRoot="${default.context.root}" 
24                    location="inventory.war" /> 
25
26    <!-- Default SSL configuration enables trust for default certificates from the Java runtime -->
27    <ssl id="defaultSSLConfig" trustDefaultCerts="true" />
28    
29    <!-- tag::postgresqlLibrary[] -->
30    <library id="postgresql-library">
31        <fileset dir="${shared.resource.dir}/" includes="*.jar" />
32    </library>
33    <!-- end::postgresqlLibrary[] -->
34
35    <!-- Datasource Configuration -->
36    <!-- tag::dataSource[] -->
37    <dataSource id="DefaultDataSource" jndiName="jdbc/postgresql">
38        <jdbcDriver libraryRef="postgresql-library" />
39        <properties.postgresql databaseName="admin"
40                               serverName="localhost"
41                               portNumber="5432"
42                               user="admin"
43                               password="adminpwd"/>
44    </dataSource>
45    <!-- end::dataSource[] -->
46</server>

The library element tells the Liberty server where to find the PostgreSQL library. The dataSource element points to where the Java Database Connectivity (JDBC) connects, along with some database vendor-specific properties.

To use a PostgreSQL database, you need to download its library and store it to the Liberty shared resources directory. Configure the Liberty Maven plug-in in the pom.xml file.

Replace the pom.xml configuration file.
pom.xml

pom.xml

 1<?xml version="1.0" encoding="UTF-8" ?>
 2<project xmlns="http://maven.apache.org/POM/4.0.0"
 3    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 5    <modelVersion>4.0.0</modelVersion>
 6
 7    <groupId>io.openliberty.deepdive</groupId>
 8    <artifactId>inventory</artifactId>
 9    <packaging>war</packaging>
10    <version>1.0-SNAPSHOT</version>
11
12    <properties>
13        <maven.compiler.source>11</maven.compiler.source>
14        <maven.compiler.target>11</maven.compiler.target>
15        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
16        <liberty.var.default.http.port>9080</liberty.var.default.http.port>
17        <liberty.var.default.https.port>9443</liberty.var.default.https.port>
18        <liberty.var.default.context.root>/inventory</liberty.var.default.context.root>
19    </properties>
20
21    <dependencies>
22        <dependency>
23            <groupId>jakarta.platform</groupId>
24            <artifactId>jakarta.jakartaee-api</artifactId>
25            <version>9.1.0</version>
26            <scope>provided</scope>
27        </dependency>
28        <dependency>
29            <groupId>org.eclipse.microprofile</groupId>
30            <artifactId>microprofile</artifactId>
31            <version>5.0</version>
32            <type>pom</type>
33            <scope>provided</scope>
34        </dependency>
35        <!-- tag::postgresql[] -->        
36        <dependency>
37            <groupId>org.postgresql</groupId>
38            <artifactId>postgresql</artifactId>
39            <version>42.3.1</version>
40            <scope>provided</scope>
41        </dependency>
42        <!-- end::postgresql[] -->        
43    </dependencies>
44
45    <build>
46        <finalName>inventory</finalName>
47        <pluginManagement>
48            <plugins>
49                <plugin>
50                    <groupId>org.apache.maven.plugins</groupId>
51                    <artifactId>maven-war-plugin</artifactId>
52                    <version>3.3.2</version>
53                </plugin>
54                <plugin>
55                    <groupId>io.openliberty.tools</groupId>
56                    <artifactId>liberty-maven-plugin</artifactId>
57                    <version>3.5.1</version>
58                </plugin>
59            </plugins>
60        </pluginManagement>
61        <plugins>
62            <plugin>
63                <groupId>io.openliberty.tools</groupId>
64                <artifactId>liberty-maven-plugin</artifactId>
65                <!-- tag::copyDependencies[] -->        
66                <configuration>
67                    <copyDependencies>
68                        <dependencyGroup>
69                            <location>${project.build.directory}/liberty/wlp/usr/shared/resources</location>
70                            <dependency>
71                                <groupId>org.postgresql</groupId>
72                                <artifactId>postgresql</artifactId>
73                                <version>42.3.1</version>
74                            </dependency>
75                        </dependencyGroup>
76                    </copyDependencies>
77                </configuration>
78                <!-- end::copyDependencies[] -->        
79            </plugin>
80        </plugins>
81    </build>
82</project>

The postgresql dependency ensures that Maven downloads the PostgreSQL library to local project. The copyDependencies configuration tells the Liberty Maven plug-in to copy the library to the Liberty shared resources directory.

Starting PostgreSQL database

Use Docker to run an instance of the PostgreSQL database for a fast installation and setup.

A container file is provided for you. First, navigate to the finish/postgres directory. Then, run the following commands to use the Dockerfile to build the image, run the image in a Docker container, and map 5432 port from the container to your machine:

cd ../../finish/postgres
docker build -t postgres-sample .
docker run --name postgres-container -p 5432:5432 -d postgres-sample

Running the application

In your dev mode console for the inventory microservice, type r and press enter/return key to restart the server.

Point your browser to the http://localhost:9080/openapi/ui/ URL. This URL displays the available REST endpoints.

First, make a POST request to the /api/systems/ endpoint. To make this request, expand the first POST endpoint on the UI, click the Try it out button, provide values to the heapSize, hostname, javaVersion, and osName parameters, and then click the Execute button. The POST request adds a system with the specified values to the database.

Next, make a GET request to the /api/systems endpoint. To make this request, expand the GET endpoint on the UI, click the Try it out button, and then click the Execute button. The GET request returns all systems from the database.

Next, make a PUT request to the /api/systems/{hostname} endpoint. To make this request, expand the PUT endpoint on the UI, click the Try it out button. Then, provide the same value to the hostname parameter as in the previous step, provide different values to the heapSize, javaVersion, and osName parameters, and click the Execute button. The PUT request updates the system with the specified values.

To see the updated system, make a GET request to the /api/systems/{hostname} endpoint. To make this request, expand the GET endpoint on the UI, click the Try it out button, provide the same value to the hostname parameter as the previous step, and then click the Execute button. The GET request returns the system from the database.

Next, make a DELETE request to the /api/systems/{hostname} endpoint. To make this request, expand the DELETE endpoint on the UI, click the Try it out button, and then click Execute. The DELETE request removes the system from the database. Run the GET request again to see that the system no longer exists in the database.

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-9.1</feature>
 6        <feature>microProfile-5.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    <variable name="postgres/hostname" defaultValue="localhost" />
13    <variable name="postgres/portnum" defaultValue="5432" />
14
15    <httpEndpoint id="defaultHttpEndpoint"
16                  httpPort="${default.http.port}" 
17                  httpsPort="${default.https.port}" />
18
19    <!-- Automatically expand WAR files and EAR files -->
20    <applicationManager autoExpand="true"/>
21
22    <!-- tag::basicregistry[] -->
23    <basicRegistry id="basic" realm="WebRealm">
24        <user name="bob" password="{xor}PTA9Lyg7" />
25        <user name="alice" password="{xor}PjM2PDovKDs=" />
26        <!-- tag::myadmins[] -->
27        <group name="admin">
28            <member name="bob" />
29        </group>
30        <!-- end::myadmins[] -->
31        <!-- tag::myusers[] -->
32        <group name="user">
33            <member name="bob" />
34            <member name="alice" />
35        </group>
36        <!-- end::myusers[] -->
37    </basicRegistry>
38    <!-- end::basicregistry[] -->
39
40    <!-- Configures the application on a specified context root -->
41    <webApplication contextRoot="${default.context.root}"
42                    location="inventory.war">
43        <application-bnd>
44            <!-- tag::securityrole[] -->
45            <!-- tag::adminrole[] -->
46            <security-role name="admin">
47                <group name="admin" />
48            </security-role>
49            <!-- end::adminrole[] -->
50            <!-- tag::userrole[] -->
51            <security-role name="user">
52                <group name="user" />
53            </security-role>
54            <!-- end::userrole[] -->
55            <!-- end::securityrole[] -->
56        </application-bnd>
57     </webApplication>
58
59    <!-- Default SSL configuration enables trust for default certificates from the Java runtime -->
60    <ssl id="defaultSSLConfig" trustDefaultCerts="true" />
61
62    <library id="postgresql-library">
63        <fileset dir="${shared.resource.dir}/" includes="*.jar" />
64    </library>
65
66    <dataSource id="DefaultDataSource" jndiName="jdbc/postgresql">
67        <jdbcDriver libraryRef="postgresql-library" />
68        <properties.postgresql databaseName="admin"
69                               serverName="localhost"
70                               portNumber="5432"
71                               user="admin"
72                               password="adminpwd"/>
73    </dataSource>
74</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 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 v1.0
  6 * which accompanies this distribution, and is available at
  7 * http://www.eclipse.org/legal/epl-v10.html
  8 *
  9 * Contributors:
 10 *     IBM Corporation - Initial implementation
 11 *******************************************************************************/
 12// end::copyright[]
 13package io.openliberty.deepdive.rest;
 14
 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;
 26
 27import org.eclipse.microprofile.config.inject.ConfigProperty;
 28
 29import io.openliberty.deepdive.rest.model.SystemData;
 30import jakarta.enterprise.context.ApplicationScoped;
 31import jakarta.annotation.security.RolesAllowed;
 32import jakarta.inject.Inject;
 33import jakarta.transaction.Transactional;
 34import jakarta.ws.rs.Consumes;
 35import jakarta.ws.rs.DELETE;
 36import jakarta.ws.rs.GET;
 37import jakarta.ws.rs.POST;
 38import jakarta.ws.rs.PUT;
 39import jakarta.ws.rs.Path;
 40import jakarta.ws.rs.PathParam;
 41import jakarta.ws.rs.Produces;
 42import jakarta.ws.rs.QueryParam;
 43import jakarta.ws.rs.core.MediaType;
 44import jakarta.ws.rs.core.Response;
 45
 46@ApplicationScoped
 47@Path("/systems")
 48// tag::SystemResource[]
 49public class SystemResource {
 50
 51    // tag::inventory[]
 52    @Inject
 53    Inventory inventory;
 54    // end::inventory[]
 55
 56    // tag::inject[]
 57    @Inject
 58    // end::inject[]
 59    // tag::configProperty[]
 60    @ConfigProperty(name = "client.https.port")
 61    // end::configProperty[]
 62    String CLIENT_PORT;
 63
 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    // tag::postTransactional[]
103    @POST
104    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
105    @Produces(MediaType.APPLICATION_JSON)
106    @Transactional
107    // end::postTransactional[]
108    @APIResponses(value = {
109        @APIResponse(responseCode = "200",
110            description = "Successfully added system to inventory"),
111        @APIResponse(responseCode = "400",
112            description = "Unable to add system to inventory")
113    })
114    @Parameters(value = {
115        @Parameter(
116            name = "hostname", in = ParameterIn.QUERY,
117            description = "The hostname of the system",
118            required = true, example = "localhost",
119            schema = @Schema(type = SchemaType.STRING)),
120        @Parameter(
121            name = "osName", in = ParameterIn.QUERY,
122            description = "The operating system of the system",
123            required = true, example = "linux",
124            schema = @Schema(type = SchemaType.STRING)),
125        @Parameter(
126            name = "javaVersion", in = ParameterIn.QUERY,
127            description = "The Java version of the system",
128            required = true, example = "11",
129            schema = @Schema(type = SchemaType.STRING)),
130        @Parameter(
131            name = "heapSize", in = ParameterIn.QUERY,
132            description = "The heap size of the system",
133            required = true, example = "1048576",
134            schema = @Schema(type = SchemaType.NUMBER)),
135    })
136    @Operation(
137        summary = "Add system",
138        description = "Add a system and its data to the inventory.",
139        operationId = "addSystem"
140    )
141    public Response addSystem(
142        @QueryParam("hostname") String hostname,
143        @QueryParam("osName") String osName,
144        @QueryParam("javaVersion") String javaVersion,
145        @QueryParam("heapSize") Long heapSize) {
146
147        SystemData s = inventory.getSystem(hostname);
148        if (s != null) {
149            return fail(hostname + " already exists.");
150        }
151        inventory.add(hostname, osName, javaVersion, heapSize);
152        return success(hostname + " was added.");
153    }
154
155    // tag::putTransactional[]
156    // tag::put[]
157    @PUT
158    // end::put[]
159    // tag::putEndpoint[]
160    @Path("/{hostname}")
161    // end::putEndpoint[]
162    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
163    @Produces(MediaType.APPLICATION_JSON)
164    @Transactional
165    // end::putTransactional[]
166    // tag::putRolesAllowed[]
167    @RolesAllowed({ "admin", "user" })
168    // end::putRolesAllowed[]
169    @APIResponses(value = {
170        @APIResponse(responseCode = "200",
171            description = "Successfully updated system"),
172        @APIResponse(responseCode = "400",
173           description =
174           "Unable to update because the system does not exist in the inventory.")
175    })
176    @Parameters(value = {
177        @Parameter(
178            name = "hostname", in = ParameterIn.PATH,
179            description = "The hostname of the system",
180            required = true, example = "localhost",
181            schema = @Schema(type = SchemaType.STRING)),
182        @Parameter(
183            name = "osName", in = ParameterIn.QUERY,
184            description = "The operating system of the system",
185            required = true, example = "linux",
186            schema = @Schema(type = SchemaType.STRING)),
187        @Parameter(
188            name = "javaVersion", in = ParameterIn.QUERY,
189            description = "The Java version of the system",
190            required = true, example = "11",
191            schema = @Schema(type = SchemaType.STRING)),
192        @Parameter(
193            name = "heapSize", in = ParameterIn.QUERY,
194            description = "The heap size of the system",
195            required = true, example = "1048576",
196            schema = @Schema(type = SchemaType.NUMBER)),
197    })
198    @Operation(
199        summary = "Update system",
200        description = "Update a system and its data on the inventory.",
201        operationId = "updateSystem"
202    )
203    public Response updateSystem(
204        @PathParam("hostname") String hostname,
205        @QueryParam("osName") String osName,
206        @QueryParam("javaVersion") String javaVersion,
207        @QueryParam("heapSize") Long heapSize) {
208
209        SystemData s = inventory.getSystem(hostname);
210        if (s == null) {
211            return fail(hostname + " does not exists.");
212        }
213        s.setOsName(osName);
214        s.setJavaVersion(javaVersion);
215        s.setHeapSize(heapSize);
216        inventory.update(s);
217        return success(hostname + " was updated.");
218    }
219
220    // tag::deleteTransactional[]
221    // tag::delete[]
222    @DELETE
223    // end::delete[]
224    // tag::deleteEndpoint[]
225    @Path("/{hostname}")
226    // end::deleteEndpoint[]
227    @Produces(MediaType.APPLICATION_JSON)
228    @Transactional
229    // end::deleteTransactional[]
230    // tag::deleteRolesAllowed[]
231    @RolesAllowed({ "admin" })
232    // end::deleteRolesAllowed[]
233    @APIResponses(value = {
234        @APIResponse(responseCode = "200",
235            description = "Successfully deleted the system from inventory"),
236        @APIResponse(responseCode = "400",
237            description =
238                "Unable to delete because the system does not exist in the inventory")
239    })
240    @Parameter(
241        name = "hostname", in = ParameterIn.PATH,
242        description = "The hostname of the system",
243        required = true, example = "localhost",
244        schema = @Schema(type = SchemaType.STRING)
245    )
246    @Operation(
247        summary = "Remove system",
248        description = "Removes a system from the inventory.",
249        operationId = "removeSystem"
250    )
251    public Response removeSystem(@PathParam("hostname") String hostname) {
252        SystemData s = inventory.getSystem(hostname);
253        if (s != null) {
254            inventory.removeSystem(s);
255            return success(hostname + " was removed.");
256        } else {
257            return fail(hostname + " does not exists.");
258        }
259    }
260
261    // tag::postTransactional[]
262    // tag::addSystemClient[]
263    @POST
264    @Path("/client/{hostname}")
265    // end::addSystemClient[]
266    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
267    @Produces(MediaType.APPLICATION_JSON)
268    @Transactional
269    @RolesAllowed({ "admin" })
270    @APIResponses(value = {
271        @APIResponse(responseCode = "200",
272            description = "Successfully added system client"),
273        @APIResponse(responseCode = "400",
274            description = "Unable to add system client")
275    })
276    @Parameter(
277        name = "hostname", in = ParameterIn.PATH,
278        description = "The hostname of the system",
279        required = true, example = "localhost",
280        schema = @Schema(type = SchemaType.STRING)
281    )
282    @Operation(
283        summary = "Add system client",
284        description = "This adds a system client.",
285        operationId = "addSystemClient"
286    )
287    //tag::printClientPort[]
288    public Response addSystemClient(@PathParam("hostname") String hostname) {
289        System.out.println(CLIENT_PORT);
290        return success("Client Port: " + CLIENT_PORT);
291    }
292    //end::printClientPort[]
293
294    private Response success(String message) {
295        return Response.ok("{ \"ok\" : \"" + message + "\" }").build();
296    }
297
298    private Response fail(String message) {
299        return Response.status(Response.Status.BAD_REQUEST)
300                       .entity("{ \"error\" : \"" + message + "\" }")
301                       .build();
302    }
303}
304// 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' | jq

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 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 v1.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-v10.html
 8 *
 9 * Contributors:
10 *     IBM Corporation - Initial implementation
11 *******************************************************************************/
12// end::copyright[]
13package io.openliberty.deepdive.rest.client;
14
15import jakarta.ws.rs.GET;
16import jakarta.ws.rs.HeaderParam;
17import jakarta.ws.rs.Path;
18import jakarta.ws.rs.PathParam;
19import jakarta.ws.rs.Produces;
20import jakarta.ws.rs.core.MediaType;
21
22@Path("/api")
23public interface SystemClient extends AutoCloseable {
24
25    @GET
26    @Path("/property/{property}")
27    @Produces(MediaType.TEXT_PLAIN)
28    String getProperty(@HeaderParam("Authorization") String authHeader,
29                       @PathParam("property") String property);
30
31    @GET
32    @Path("/heapsize")
33    @Produces(MediaType.TEXT_PLAIN)
34    Long getHeapSize(@HeaderParam("Authorization") String authHeader);
35
36}

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 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 v1.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-v10.html
 8 *
 9 * Contributors:
10 *     IBM Corporation - Initial implementation
11 *******************************************************************************/
12// end::copyright[]
13package io.openliberty.deepdive.rest.client;
14
15public class UnknownUriException extends Exception {
16
17    private static final long serialVersionUID = 1L;
18
19    public UnknownUriException() {
20        super();
21    }
22
23    public UnknownUriException(String message) {
24        super(message);
25    }
26
27}

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 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 v1.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-v10.html
 8 *
 9 * Contributors:
10 *     IBM Corporation - Initial implementation
11 *******************************************************************************/
12// end::copyright[]
13package io.openliberty.deepdive.rest.client;
14
15import java.util.logging.Logger;
16
17import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper;
18
19import jakarta.ws.rs.core.MultivaluedMap;
20import jakarta.ws.rs.core.Response;
21
22public class UnknownUriExceptionMapper
23    implements ResponseExceptionMapper<UnknownUriException> {
24    Logger LOG = Logger.getLogger(UnknownUriExceptionMapper.class.getName());
25
26    @Override
27    public boolean handles(int status, MultivaluedMap<String, Object> headers) {
28        LOG.info("status = " + status);
29        return status == 404;
30    }
31
32    @Override
33    public UnknownUriException toThrowable(Response response) {
34        return new UnknownUriException();
35    }
36}

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

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-9.1</feature>
 6        <feature>microProfile-5.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    <variable name="postgres/hostname" defaultValue="localhost" />
16    <variable name="postgres/portnum" defaultValue="5432" />
17
18    <httpEndpoint id="defaultHttpEndpoint"
19                  httpPort="${default.http.port}" 
20                  httpsPort="${default.https.port}" />
21
22    <!-- Automatically expand WAR files and EAR files -->
23    <applicationManager autoExpand="true"/>
24    
25    <!-- tag::keyStore[] -->
26    <keyStore id="defaultKeyStore" password="secret" />
27    <!-- end::keyStore[] -->
28    
29    <basicRegistry id="basic" realm="WebRealm">
30        <user name="bob" password="{xor}PTA9Lyg7" />
31        <user name="alice" password="{xor}PjM2PDovKDs=" />
32
33        <group name="admin">
34            <member name="bob" />
35        </group>
36
37        <group name="user">
38            <member name="bob" />
39            <member name="alice" />
40        </group>
41    </basicRegistry>
42
43    <!-- Configures the application on a specified context root -->
44    <webApplication contextRoot="${default.context.root}"
45                    location="inventory.war">
46        <application-bnd>
47            <security-role name="admin">
48                <group name="admin" />
49            </security-role>
50            <security-role name="user">
51                <group name="user" />
52            </security-role>
53        </application-bnd>
54    </webApplication>
55
56    <!-- tag::jwtSsoConfig[] -->
57    <jwtSso jwtBuilderRef="jwtInventoryBuilder"/> 
58    <!-- end::jwtSsoConfig[] -->
59    <!-- tag::jwtBuilder[] -->
60    <jwtBuilder id="jwtInventoryBuilder" 
61                issuer="http://openliberty.io" 
62                audiences="systemService"
63                expiry="24h"/>
64    <!-- end::jwtBuilder[] -->
65    <!-- tag::mpJwt[] -->
66    <mpJwt audiences="systemService" 
67           groupNameAttribute="groups" 
68           id="myMpJwt" 
69           issuer="http://openliberty.io"/>
70    <!-- end::mpJwt[] -->
71
72    <!-- Default SSL configuration enables trust for default certificates from the Java runtime -->
73    <ssl id="defaultSSLConfig" trustDefaultCerts="true" />
74
75    <library id="postgresql-library">
76        <fileset dir="${shared.resource.dir}/" includes="*.jar" />
77    </library>
78
79    <dataSource id="DefaultDataSource" jndiName="jdbc/postgresql">
80        <jdbcDriver libraryRef="postgresql-library" />
81        <properties.postgresql databaseName="admin"
82                               serverName="localhost"
83                               portNumber="5432"
84                               user="admin"
85                               password="adminpwd"/>
86    </dataSource>
87</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.

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 pom.xml configuration file.

Replace the pom.xml file.
pom.xml

pom.xml

 1<?xml version="1.0" encoding="UTF-8" ?>
 2<project xmlns="http://maven.apache.org/POM/4.0.0"
 3               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 5
 6    <modelVersion>4.0.0</modelVersion>
 7
 8    <groupId>io.openliberty.deepdive</groupId>
 9    <artifactId>inventory</artifactId>
10    <packaging>war</packaging>
11    <version>1.0-SNAPSHOT</version>
12
13    <properties>
14        <maven.compiler.source>11</maven.compiler.source>
15        <maven.compiler.target>11</maven.compiler.target>
16        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
17        <liberty.var.default.http.port>9080</liberty.var.default.http.port>
18        <liberty.var.default.https.port>9443</liberty.var.default.https.port>
19        <liberty.var.default.context.root>/inventory</liberty.var.default.context.root>
20        <!-- tag::https[] -->
21        <liberty.var.client.https.port>9444</liberty.var.client.https.port>
22        <!-- end::https[] -->
23    </properties>
24
25    <dependencies>
26        <dependency>
27            <groupId>jakarta.platform</groupId>
28            <artifactId>jakarta.jakartaee-api</artifactId>
29            <version>9.1.0</version>
30            <scope>provided</scope>
31        </dependency>
32        <dependency>
33            <groupId>org.eclipse.microprofile</groupId>
34            <artifactId>microprofile</artifactId>
35            <version>5.0</version>
36            <type>pom</type>
37            <scope>provided</scope>
38        </dependency>
39        <dependency>
40            <groupId>org.postgresql</groupId>
41            <artifactId>postgresql</artifactId>
42            <version>42.3.1</version>
43            <scope>provided</scope>
44        </dependency>
45    </dependencies>
46
47    <build>
48        <finalName>inventory</finalName>
49        <pluginManagement>
50            <plugins>
51                <plugin>
52                    <groupId>org.apache.maven.plugins</groupId>
53                    <artifactId>maven-war-plugin</artifactId>
54                    <version>3.3.2</version>
55                </plugin>
56                <plugin>
57                    <groupId>io.openliberty.tools</groupId>
58                    <artifactId>liberty-maven-plugin</artifactId>
59                    <version>3.5.1</version>
60                </plugin>
61            </plugins>
62        </pluginManagement>
63        <plugins>
64            <plugin>
65                <groupId>io.openliberty.tools</groupId>
66                <artifactId>liberty-maven-plugin</artifactId>
67                <configuration>
68                    <copyDependencies>
69                        <dependencyGroup>
70                            <location>${project.build.directory}/liberty/wlp/usr/shared/resources</location>
71                            <dependency>
72                                <groupId>org.postgresql</groupId>
73                                <artifactId>postgresql</artifactId>
74                                <version>42.3.1</version>
75                            </dependency>
76                        </dependencyGroup>
77                    </copyDependencies>
78                </configuration>
79            </plugin>
80        </plugins>
81    </build>
82</project>

Configure the client https port by setting the <liberty.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.

mvn liberty:dev

Running the /client/{hostname} endpoint

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

cd finish/system
mvn liberty:run

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 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 v1.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-v10.html
 8 *
 9 * Contributors:
10 *     IBM Corporation - Initial implementation
11 *******************************************************************************/
12// end::copyright[]
13// tag::StartupCheck[]
14package io.openliberty.deepdive.rest.health;
15
16import java.lang.management.ManagementFactory;
17import com.sun.management.OperatingSystemMXBean;
18import jakarta.enterprise.context.ApplicationScoped;
19import org.eclipse.microprofile.health.Startup;
20import org.eclipse.microprofile.health.HealthCheck;
21import org.eclipse.microprofile.health.HealthCheckResponse;
22
23// tag::Startup[]
24@Startup
25// end::Startup[]
26@ApplicationScoped
27public class StartupCheck implements HealthCheck {
28
29    @Override
30    public HealthCheckResponse call() {
31        OperatingSystemMXBean bean = (com.sun.management.OperatingSystemMXBean)
32        ManagementFactory.getOperatingSystemMXBean();
33        double cpuUsed = bean.getSystemCpuLoad();
34        String cpuUsage = String.valueOf(cpuUsed);
35        return HealthCheckResponse.named("Startup Check")
36                                  .status(cpuUsed < 0.95).build();
37    }
38}
39// 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 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 v1.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-v10.html
 8 *
 9 * Contributors:
10 *     IBM Corporation - Initial implementation
11 *******************************************************************************/
12// end::copyright[]
13// tag::LivenessCheck[]
14package io.openliberty.deepdive.rest.health;
15
16import java.lang.management.ManagementFactory;
17import java.lang.management.MemoryMXBean;
18
19import jakarta.enterprise.context.ApplicationScoped;
20import org.eclipse.microprofile.health.Liveness;
21import org.eclipse.microprofile.health.HealthCheck;
22import org.eclipse.microprofile.health.HealthCheckResponse;
23
24// tag::Liveness[]
25@Liveness
26// end::Liveness[]
27@ApplicationScoped
28public class LivenessCheck implements HealthCheck {
29
30    @Override
31    public HealthCheckResponse call() {
32        MemoryMXBean memBean = ManagementFactory.getMemoryMXBean();
33        long memUsed = memBean.getHeapMemoryUsage().getUsed();
34        long memMax = memBean.getHeapMemoryUsage().getMax();
35
36        return HealthCheckResponse.named("Liveness Check")
37                                  .status(memUsed < memMax * 0.9)
38                                  .build();
39    }
40}
41// 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 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 v1.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-v10.html
 8 *
 9 * Contributors:
10 *     IBM Corporation - Initial implementation
11 *******************************************************************************/
12// end::copyright[]
13// tag::ReadinessCheck[]
14package io.openliberty.deepdive.rest.health;
15
16import jakarta.enterprise.context.ApplicationScoped;
17import jakarta.inject.Inject;
18
19import org.eclipse.microprofile.config.inject.ConfigProperty;
20import org.eclipse.microprofile.health.Readiness;
21import org.eclipse.microprofile.health.HealthCheck;
22import org.eclipse.microprofile.health.HealthCheckResponse;
23import org.eclipse.microprofile.health.HealthCheckResponseBuilder;
24
25import java.io.IOException;
26import java.net.Socket;
27
28// tag::Readiness[]
29@Readiness
30// end::Readiness[]
31// tag::ApplicationScoped[]
32@ApplicationScoped
33// end::ApplicationScoped[]
34public class ReadinessCheck implements HealthCheck {
35
36    @Inject
37    @ConfigProperty(name = "postgres/hostname")
38    private String host;
39
40    @Inject
41    @ConfigProperty(name = "postgres/portnum")
42    private int port;
43
44    @Override
45    public HealthCheckResponse call() {
46        HealthCheckResponseBuilder responseBuilder =
47            HealthCheckResponse.named("Readiness Check");
48
49        try {
50            Socket socket = new Socket(host, port);
51            socket.close();
52            responseBuilder.up();
53        } catch (Exception e) {
54            responseBuilder.down();
55        }
56        return responseBuilder.build();
57    }
58}
59// 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 liveness health check. This 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-9.1</feature>
 6        <feature>microProfile-5.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    <variable name="postgres/hostname" defaultValue="localhost" />
14    <variable name="postgres/portnum" defaultValue="5432" />
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    <keyStore id="defaultKeyStore" password="secret" />
24    
25    <basicRegistry id="basic" realm="WebRealm">
26        <user name="bob" password="{xor}PTA9Lyg7" />
27        <user name="alice" password="{xor}PjM2PDovKDs=" />
28
29        <group name="admin">
30            <member name="bob" />
31        </group>
32
33        <group name="user">
34            <member name="bob" />
35            <member name="alice" />
36        </group>
37    </basicRegistry>
38
39    <!-- tag::administrator[] -->
40    <administrator-role>
41        <user>bob</user>
42        <group>AuthorizedGroup</group>
43    </administrator-role>
44    <!-- end::administrator[] -->
45
46    <!-- Configures the application on a specified context root -->
47    <webApplication contextRoot="${default.context.root}"
48                    location="inventory.war">
49        <application-bnd>
50            <security-role name="admin">
51                <group name="admin" />
52            </security-role>
53            <security-role name="user">
54                <group name="user" />
55            </security-role>
56        </application-bnd>
57    </webApplication>
58
59    <jwtSso jwtBuilderRef="jwtInventoryBuilder"/> 
60    <jwtBuilder id="jwtInventoryBuilder" 
61                issuer="http://openliberty.io" 
62                audiences="systemService"
63                expiry="24h"/>
64    <mpJwt audiences="systemService" 
65           groupNameAttribute="groups" 
66           id="myMpJwt" 
67           issuer="http://openliberty.io"/>
68
69    <!-- Default SSL configuration enables trust for default certificates from the Java runtime -->
70    <ssl id="defaultSSLConfig" trustDefaultCerts="true" />
71
72    <library id="postgresql-library">
73        <fileset dir="${shared.resource.dir}/" includes="*.jar" />
74    </library>
75
76    <dataSource id="DefaultDataSource" jndiName="jdbc/postgresql">
77        <jdbcDriver libraryRef="postgresql-library" />
78        <properties.postgresql databaseName="admin"
79                               serverName="localhost"
80                               portNumber="5432"
81                               user="admin"
82                               password="adminpwd"/>
83    </dataSource>
84</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 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 v1.0
  6 * which accompanies this distribution, and is available at
  7 * http://www.eclipse.org/legal/epl-v10.html
  8 *
  9 * Contributors:
 10 *     IBM Corporation - Initial implementation
 11 *******************************************************************************/
 12// end::copyright[]
 13package io.openliberty.deepdive.rest;
 14
 15import java.net.URI;
 16import java.util.List;
 17
 18import org.eclipse.microprofile.openapi.annotations.Operation;
 19import org.eclipse.microprofile.openapi.annotations.enums.ParameterIn;
 20import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
 21import org.eclipse.microprofile.openapi.annotations.media.Schema;
 22import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
 23import org.eclipse.microprofile.openapi.annotations.parameters.Parameters;
 24import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
 25import org.eclipse.microprofile.openapi.annotations.responses.APIResponseSchema;
 26import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
 27import org.eclipse.microprofile.rest.client.RestClientBuilder;
 28import org.eclipse.microprofile.config.inject.ConfigProperty;
 29import org.eclipse.microprofile.jwt.JsonWebToken;
 30// tag::metricsImport[]
 31import org.eclipse.microprofile.metrics.annotation.Counted;
 32// end::metricsImport[]
 33
 34import io.openliberty.deepdive.rest.client.SystemClient;
 35import io.openliberty.deepdive.rest.client.UnknownUriExceptionMapper;
 36import io.openliberty.deepdive.rest.model.SystemData;
 37import jakarta.annotation.security.RolesAllowed;
 38import jakarta.enterprise.context.ApplicationScoped;
 39import jakarta.inject.Inject;
 40import jakarta.transaction.Transactional;
 41import jakarta.ws.rs.Consumes;
 42import jakarta.ws.rs.DELETE;
 43import jakarta.ws.rs.GET;
 44import jakarta.ws.rs.POST;
 45import jakarta.ws.rs.PUT;
 46import jakarta.ws.rs.Path;
 47import jakarta.ws.rs.PathParam;
 48import jakarta.ws.rs.Produces;
 49import jakarta.ws.rs.QueryParam;
 50import jakarta.ws.rs.core.MediaType;
 51import jakarta.ws.rs.core.Response;
 52
 53@ApplicationScoped
 54@Path("/systems")
 55public class SystemResource {
 56
 57    @Inject
 58    Inventory inventory;
 59
 60    @Inject
 61    @ConfigProperty(name = "client.https.port")
 62    String CLIENT_PORT;
 63
 64    @Inject
 65    JsonWebToken jwt;
 66
 67    @GET
 68    @Path("/")
 69    @Produces(MediaType.APPLICATION_JSON)
 70    @APIResponseSchema(value = SystemData.class,
 71        responseDescription = "A list of system data stored within the inventory.",
 72        responseCode = "200")
 73    @Operation(
 74        summary = "List contents.",
 75        description = "Returns the currently stored system data in the inventory.",
 76        operationId = "listContents")
 77    public List<SystemData> listContents() {
 78        return inventory.getSystems();
 79    }
 80
 81    @GET
 82    @Path("/{hostname}")
 83    @Produces(MediaType.APPLICATION_JSON)
 84    @APIResponseSchema(value = SystemData.class,
 85        responseDescription = "System data of a particular host.",
 86        responseCode = "200")
 87    @Operation(
 88        summary = "Get System",
 89        description = "Retrieves and returns the system data from the system "
 90                      + "service running on the particular host.",
 91        operationId = "getSystem"
 92    )
 93    public SystemData getSystem(
 94        @Parameter(
 95            name = "hostname", in = ParameterIn.PATH,
 96            description = "The hostname of the system",
 97            required = true, example = "localhost",
 98            schema = @Schema(type = SchemaType.STRING)
 99        )
100        @PathParam("hostname") String hostname) {
101        return inventory.getSystem(hostname);
102    }
103
104    @POST
105    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
106    @Produces(MediaType.APPLICATION_JSON)
107    @Transactional
108    // tag::metricsAddSystem[]
109    @Counted(name = "addSystem",
110             absolute = true,
111             description = "Number of times adding system endpoint is called")
112    // end::metricsAddSystem[]
113    @APIResponses(value = {
114        @APIResponse(responseCode = "200",
115            description = "Successfully added system to inventory"),
116        @APIResponse(responseCode = "400",
117            description = "Unable to add system to inventory")
118    })
119    @Parameters(value = {
120        @Parameter(
121            name = "hostname", in = ParameterIn.QUERY,
122            description = "The hostname of the system",
123            required = true, example = "localhost",
124            schema = @Schema(type = SchemaType.STRING)),
125        @Parameter(
126            name = "osName", in = ParameterIn.QUERY,
127            description = "The operating system of the system",
128            required = true, example = "linux",
129            schema = @Schema(type = SchemaType.STRING)),
130        @Parameter(
131            name = "javaVersion", in = ParameterIn.QUERY,
132            description = "The Java version of the system",
133            required = true, example = "11",
134            schema = @Schema(type = SchemaType.STRING)),
135        @Parameter(
136            name = "heapSize", in = ParameterIn.QUERY,
137            description = "The heap size of the system",
138            required = true, example = "1048576",
139            schema = @Schema(type = SchemaType.NUMBER)),
140    })
141    @Operation(
142        summary = "Add system",
143        description = "Add a system and its data to the inventory.",
144        operationId = "addSystem"
145    )
146    public Response addSystem(
147        @QueryParam("hostname") String hostname,
148        @QueryParam("osName") String osName,
149        @QueryParam("javaVersion") String javaVersion,
150        @QueryParam("heapSize") Long heapSize) {
151
152        SystemData s = inventory.getSystem(hostname);
153        if (s != null) {
154            return fail(hostname + " already exists.");
155        }
156        inventory.add(hostname, osName, javaVersion, heapSize);
157        return success(hostname + " was added.");
158    }
159
160    @PUT
161    @Path("/{hostname}")
162    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
163    @Produces(MediaType.APPLICATION_JSON)
164    @Transactional
165    @RolesAllowed({ "admin", "user" })
166    // tag::metricsUpdateSystem[]
167    @Counted(name = "updateSystem",
168             absolute = true,
169             description = "Number of times updating a system endpoint is called")
170    // end::metricsUpdateSystem[]
171    @APIResponses(value = {
172        @APIResponse(responseCode = "200",
173            description = "Successfully updated system"),
174        @APIResponse(responseCode = "400",
175           description =
176               "Unable to update because the system does not exist in the inventory.")
177    })
178    @Parameters(value = {
179        @Parameter(
180            name = "hostname", in = ParameterIn.PATH,
181            description = "The hostname of the system",
182            required = true, example = "localhost",
183            schema = @Schema(type = SchemaType.STRING)),
184        @Parameter(
185            name = "osName", in = ParameterIn.QUERY,
186            description = "The operating system of the system",
187            required = true, example = "linux",
188            schema = @Schema(type = SchemaType.STRING)),
189        @Parameter(
190            name = "javaVersion", in = ParameterIn.QUERY,
191            description = "The Java version of the system",
192            required = true, example = "11",
193            schema = @Schema(type = SchemaType.STRING)),
194        @Parameter(
195            name = "heapSize", in = ParameterIn.QUERY,
196            description = "The heap size of the system",
197            required = true, example = "1048576",
198            schema = @Schema(type = SchemaType.NUMBER)),
199    })
200    @Operation(
201        summary = "Update system",
202        description = "Update a system and its data on the inventory.",
203        operationId = "updateSystem"
204    )
205    public Response updateSystem(
206        @PathParam("hostname") String hostname,
207        @QueryParam("osName") String osName,
208        @QueryParam("javaVersion") String javaVersion,
209        @QueryParam("heapSize") Long heapSize) {
210
211        SystemData s = inventory.getSystem(hostname);
212        if (s == null) {
213            return fail(hostname + " does not exists.");
214        }
215        s.setOsName(osName);
216        s.setJavaVersion(javaVersion);
217        s.setHeapSize(heapSize);
218        inventory.update(s);
219        return success(hostname + " was updated.");
220    }
221
222    @DELETE
223    @Path("/{hostname}")
224    @Produces(MediaType.APPLICATION_JSON)
225    @Transactional
226    @RolesAllowed({ "admin" })
227    // tag::metricsRemoveSystem[]
228    @Counted(name = "removeSystem",
229             absolute = true,
230             description = "Number of times removing a system endpoint is called")
231    // end::metricsRemoveSystem[]
232    @APIResponses(value = {
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    })
239    @Parameter(
240        name = "hostname", in = ParameterIn.PATH,
241        description = "The hostname of the system",
242        required = true, example = "localhost",
243        schema = @Schema(type = SchemaType.STRING)
244    )
245    @Operation(
246        summary = "Remove system",
247        description = "Removes a system from the inventory.",
248        operationId = "removeSystem"
249    )
250    public Response removeSystem(@PathParam("hostname") String hostname) {
251        SystemData s = inventory.getSystem(hostname);
252        if (s != null) {
253            inventory.removeSystem(s);
254            return success(hostname + " was removed.");
255        } else {
256            return fail(hostname + " does not exists.");
257        }
258    }
259
260    @POST
261    @Path("/client/{hostname}")
262    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
263    @Produces(MediaType.APPLICATION_JSON)
264    @Transactional
265    @RolesAllowed({ "admin" })
266    // tag::metricsAddSystemClient[]
267    @Counted(name = "addSystemClient",
268             absolute = true,
269             description = "Number of times adding a system by client is called")
270    // end::metricsAddSystemClient[]
271    @APIResponses(value = {
272        @APIResponse(responseCode = "200",
273            description = "Successfully added system client"),
274        @APIResponse(responseCode = "400",
275            description = "Unable to add system client")
276    })
277    @Parameter(
278        name = "hostname", in = ParameterIn.PATH,
279        description = "The hostname of the system",
280        required = true, example = "localhost",
281        schema = @Schema(type = SchemaType.STRING)
282    )
283    @Operation(
284        summary = "Add system client",
285        description = "This adds a system client.",
286        operationId = "addSystemClient"
287    )
288    public Response addSystemClient(@PathParam("hostname") String hostname) {
289
290        SystemData s = inventory.getSystem(hostname);
291        if (s != null) {
292            return fail(hostname + " already exists.");
293        }
294
295        SystemClient customRestClient = null;
296        try {
297            customRestClient = getSystemClient(hostname);
298        } catch (Exception e) {
299            return fail("Failed to create the client " + hostname + ".");
300        }
301
302        String authHeader = "Bearer " + jwt.getRawToken();
303        try {
304            String osName = customRestClient.getProperty(authHeader, "os.name");
305            String javaVer = customRestClient.getProperty(authHeader, "java.version");
306            Long heapSize = customRestClient.getHeapSize(authHeader);
307            inventory.add(hostname, osName, javaVer, heapSize);
308        } catch (Exception e) {
309            return fail("Failed to reach the client " + hostname + ".");
310        }
311        return success(hostname + " was added.");
312    }
313
314    private SystemClient getSystemClient(String hostname) throws Exception {
315        String customURIString = "https://" + hostname + ":" + CLIENT_PORT + "/system";
316        URI customURI = URI.create(customURIString);
317        return RestClientBuilder.newBuilder()
318                                .baseUri(customURI)
319                                .register(UnknownUriExceptionMapper.class)
320                                .build(SystemClient.class);
321    }
322
323    private Response success(String message) {
324        return Response.ok("{ \"ok\" : \"" + message + "\" }").build();
325    }
326
327    private Response fail(String message) {
328        return Response.status(Response.Status.BAD_REQUEST)
329                       .entity("{ \"error\" : \"" + message + "\" }")
330                       .build();
331    }
332}

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/application endpoint provides you with application-specific metrics.

  • The /metrics/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/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/application. You can expect to see your application metrics in the output.

# TYPE application_addSystemClient_total counter
# HELP application_addSystemClient_total Number of times adding a system by client is called
application_addSystemClient_total 0
# TYPE application_addSystem_total counter
# HELP application_addSystem_total Number of times adding system endpoint is called
application_addSystem_total 1
# TYPE application_updateSystem_total counter
# HELP application_updateSystem_total Number of times updating a system endpoint is called
application_updateSystem_total 1
# TYPE application_removeSystem_total counter
# HELP application_removeSystem_total Number of times removing a system endpoint is called
application_removeSystem_total 1

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

Building the container 

Press CTRL+C in the command-line session to stop the mvn liberty:dev 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 Docker container is creating a Dockerfile. A Dockerfile is a collection of instructions for building a Docker image that can then be run as a container.

Make sure to start your Docker daemon before you proceed.

Create the Dockerfile in the start/inventory directory.
Dockerfile

Dockerfile

 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    target/inventory.war \
44    # end::inventory-war[]
45    # tag::config-apps[]
46    /config/apps
47    # end::config-apps[]
48# end::copy-war[]
49
50# tag::copy-postgres[]
51COPY --chown=1001:0 \
52    target/liberty/wlp/usr/shared/resources/*.jar \
53    /opt/ol/wlp/usr/shared/resources/
54# end::copy-postgres[]
55# end::copy[]
56
57USER 1001
58
59RUN 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 Docker 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. The third COPY instruction copies the PostgreSQL library file to the Liberty shared resources directory.

Developing the application in a container

Make the PostgreSQL database configurable in the Liberty server configuraton 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-9.1</feature>
 6        <feature>microProfile-5.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    <!-- tag::contextRoot[] -->
13    <variable name="default.context.root" defaultValue="/inventory" />
14    <!-- end::contextRoot[] -->
15    <!-- tag::variables[] -->
16    <variable name="postgres/hostname" defaultValue="localhost" />
17    <variable name="postgres/portnum" defaultValue="5432" />
18    <variable name="postgres/username" defaultValue="admin" />
19    <variable name="postgres/password" defaultValue="adminpwd" />
20    <!-- end::variables[] -->
21
22    <httpEndpoint id="defaultHttpEndpoint"
23                  httpPort="${default.http.port}" 
24                  httpsPort="${default.https.port}" />
25
26    <!-- Automatically expand WAR files and EAR files -->
27    <applicationManager autoExpand="true"/>
28
29    <keyStore id="defaultKeyStore" password="secret" />
30    
31    <basicRegistry id="basic" realm="WebRealm">
32        <user name="bob" password="{xor}PTA9Lyg7" />
33        <user name="alice" password="{xor}PjM2PDovKDs=" />
34
35        <group name="admin">
36            <member name="bob" />
37        </group>
38
39        <group name="user">
40            <member name="bob" />
41            <member name="alice" />
42        </group>
43    </basicRegistry>
44
45    <!-- Configures the application on a specified context root -->
46    <webApplication contextRoot="${default.context.root}"
47                    location="inventory.war">
48        <application-bnd>
49            <security-role name="admin">
50                <group name="admin" />
51            </security-role>
52            <security-role name="user">
53                <group name="user" />
54            </security-role>
55        </application-bnd>
56    </webApplication>
57
58    <jwtSso jwtBuilderRef="jwtInventoryBuilder"/> 
59    <jwtBuilder id="jwtInventoryBuilder" 
60                issuer="http://openliberty.io" 
61                audiences="systemService"
62                expiry="24h"/>
63    <mpJwt audiences="systemService" 
64           groupNameAttribute="groups" 
65           id="myMpJwt" 
66           issuer="http://openliberty.io"/>
67
68    <!-- Default SSL configuration enables trust for default certificates from the Java runtime -->
69    <ssl id="defaultSSLConfig" trustDefaultCerts="true" />
70
71    <library id="postgresql-library">
72        <fileset dir="${shared.resource.dir}/" includes="*.jar" />
73    </library>
74
75    <dataSource id="DefaultDataSource" jndiName="jdbc/postgresql">
76        <jdbcDriver libraryRef="postgresql-library" />
77        <!-- tag::postgresProperties[] -->
78        <properties.postgresql databaseName="admin"
79                               serverName="${postgres/hostname}"
80                               portNumber="${postgres/portnum}"
81                               user="${postgres/username}"
82                               password="${postgres/password}"/>
83        <!-- end::postgresProperties[] -->
84    </dataSource>
85</server>

Instead of the hard-coded serverName, portNumber, user, and password values in the properties.postgresql properties, use ${postgres/hostname}, ${postgres/portnum}, ${postgres/username}, and ${postgres/password}, which are defined by the variable elements.

You can use the Dockerfile to try out your application with the PostGreSQL database by running the devc goal.

The Open Liberty Maven plug-in includes a devc goal that simplifies developing your application in a container by starting dev mode with container support. This goal builds a Docker image, mounts the required directories, binds the required ports, and then runs the application inside of a container. Dev mode also listens for any changes in the application source code or configuration and rebuilds the image and restarts the container as necessary.

Retrieve the PostgreSQL container IP address by running the following command:

docker inspect -f "{{.NetworkSettings.IPAddress }}" postgres-container

The command returns the PostgreSQL container IP address:

172.17.0.2

Build and run the container by running the devc goal with the PostgreSQL container IP address from the start/inventory directory. If your PostgreSQL container IP address is not 172.17.0.2, replace the command with the right IP address.

mvn liberty:devc -DdockerRunOpts="-e POSTGRES_HOSTNAME=172.17.0.2" -DserverStartTimeout=240

You need to wait a while to let dev mode start. After you see the following message, your application server in dev mode is ready:

**************************************************************
*    Liberty is running in dev mode.
*    ...
*    Docker network information:
*        Container name: [ liberty-dev ]
*        IP address [ 172.17.0.2 ] on Docker network [ bridge ]
*    ...

Open another command-line session and run the following command to make sure that your container is running and didn’t crash:

docker ps

You can see something similar to the following output:

CONTAINER ID  IMAGE               COMMAND                 CREATED        STATUS        PORTS                                                                   NAMES
ee2daf0b33e1  inventory-dev-mode  "/opt/ol/helpers/run…"  2 minutes ago  Up 2 minutes  0.0.0.0:7777->7777/tcp, 0.0.0.0:9080->9080/tcp, 0.0.0.0:9443->9443/tcp  liberty-dev

Point your browser to the http://localhost:9080/openapi/ui URL to try out your application.

When you’re finished trying out the microservice, press CTRL+C in the command-line session where you started dev mode to stop and remove the container.

Also, run the following commands to stop the PostgreSQL container that was started in the previous section.

docker stop postgres-container
docker rm postgres-container

Building the container image

Run the mvn package command from the start/inventory directory so that the .war file resides in the target directory.

mvn package

Build your Docker image with the following commands:

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

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

docker images

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

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

Testing the microservice with Testcontainers

Although you can test your microservice manually, you should rely on automated tests. In this section, you can learn how to use Testcontainers to verify your microservice in the same Docker container that you’ll use in production.

First, create the test directory at the src directory of your Maven project.

mkdir src\test\java\it\io\openliberty\deepdive\rest
mkdir src\test\resources
mkdir -p src/test/java/it/io/openliberty/deepdive/rest
mkdir src/test/resources

Create a RESTful client interface for the inventory microservice.

Create the SystemResourceClient.java file.
src/test/java/it/io/openliberty/deepdive/rest/SystemResourceClient.java

SystemResourceClient.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2022 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 v1.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-v10.html
 8 *
 9 * Contributors:
10 *     IBM Corporation - Initial implementation
11 *******************************************************************************/
12// end::copyright[]
13package it.io.openliberty.deepdive.rest;
14
15import java.util.List;
16
17import jakarta.annotation.security.RolesAllowed;
18import jakarta.enterprise.context.ApplicationScoped;
19import jakarta.ws.rs.Consumes;
20import jakarta.ws.rs.DELETE;
21import jakarta.ws.rs.GET;
22import jakarta.ws.rs.HeaderParam;
23import jakarta.ws.rs.POST;
24import jakarta.ws.rs.PUT;
25import jakarta.ws.rs.Path;
26import jakarta.ws.rs.PathParam;
27import jakarta.ws.rs.Produces;
28import jakarta.ws.rs.QueryParam;
29import jakarta.ws.rs.core.MediaType;
30import jakarta.ws.rs.core.Response;
31
32
33@ApplicationScoped
34@Path("/systems")
35public interface SystemResourceClient {
36
37    // tag::listContents[]
38    @GET
39    @Path("/")
40    @Produces(MediaType.APPLICATION_JSON)
41    List<SystemData> listContents();
42    // end::listContents[]
43
44    // tag::getSystem[]
45    @GET
46    @Path("/{hostname}")
47    @Produces(MediaType.APPLICATION_JSON)
48    SystemData getSystem(
49        @PathParam("hostname") String hostname);
50    // end::getSystem[]
51
52    // tag::addSystem[]
53    @POST
54    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
55    @Produces(MediaType.APPLICATION_JSON)
56    Response addSystem(
57        @QueryParam("hostname") String hostname,
58        @QueryParam("osName") String osName,
59        @QueryParam("javaVersion") String javaVersion,
60        @QueryParam("heapSize") Long heapSize);
61    // end::addSystem[]
62
63    // tag::updateSystem[]
64    @PUT
65    @Path("/{hostname}")
66    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
67    @Produces(MediaType.APPLICATION_JSON)
68    @RolesAllowed({ "admin", "user" })
69    Response updateSystem(
70        @HeaderParam("Authorization") String authHeader,
71        @PathParam("hostname") String hostname,
72        @QueryParam("osName") String osName,
73        @QueryParam("javaVersion") String javaVersion,
74        @QueryParam("heapSize") Long heapSize);
75    // end::updateSystem[]
76
77    // tag::removeSystem[]
78    @DELETE
79    @Path("/{hostname}")
80    @Produces(MediaType.APPLICATION_JSON)
81    @RolesAllowed({ "admin" })
82    Response removeSystem(
83        @HeaderParam("Authorization") String authHeader,
84        @PathParam("hostname") String hostname);
85    // end::removeSystem[]
86}

This interface declares listContents(), getSystem(), addSystem(), updateSystem(), and removeSystem() methods for accessing each of the endpoints that are set up to access the inventory microservice.

Create the SystemData data model for each system in the inventory.

Create the SystemData.java file.
src/test/java/it/io/openliberty/deepdive/rest/SystemData.java

SystemData.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2022 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 v1.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-v10.html
 8 *
 9 * Contributors:
10 *     IBM Corporation - Initial implementation
11 *******************************************************************************/
12// end::copyright[]
13package it.io.openliberty.deepdive.rest;
14
15public class SystemData {
16
17    // tag::fields[]
18    private int id;
19    private String hostname;
20    private String osName;
21    private String javaVersion;
22    private Long heapSize;
23    // end::fields[]
24
25    public SystemData() {
26    }
27
28    // tag::getMethods[]
29    public int getId() {
30        return id;
31    }
32
33    public String getHostname() {
34        return hostname;
35    }
36
37    public String getOsName() {
38        return osName;
39    }
40
41    public String getJavaVersion() {
42        return javaVersion;
43    }
44
45    public Long getHeapSize() {
46        return heapSize;
47    }
48    // end::getMethods[]
49
50    // tag::setMethods[]
51    public void setId(int id) {
52        this.id = id;
53    }
54
55    public void setHostname(String hostname) {
56        this.hostname = hostname;
57    }
58
59    public void setOsName(String osName) {
60        this.osName = osName;
61    }
62
63    public void setJavaVersion(String javaVersion) {
64        this.javaVersion = javaVersion;
65    }
66
67    public void setHeapSize(Long heapSize) {
68        this.heapSize = heapSize;
69    }
70    // end::setMethods[]
71}

The SystemData class contains the ID, hostname, operating system name, Java version, and heap size properties. The various get and set methods within this class enable you to view and edit the properties of each system in the inventory.

Create the test container class that access the inventory docker image that you built in previous section.

Create the LibertyContainer.java file.
src/test/java/it/io/openliberty/deepdive/rest/LibertyContainer.java

LibertyContainer.java

  1// tag::copyright[]
  2/*******************************************************************************
  3 * Copyright (c) 2022 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 v1.0
  6 * which accompanies this distribution, and is available at
  7 * http://www.eclipse.org/legal/epl-v10.html
  8 *
  9 * Contributors:
 10 *     IBM Corporation - Initial implementation
 11 *******************************************************************************/
 12// end::copyright[]
 13package it.io.openliberty.deepdive.rest;
 14
 15import java.io.FileInputStream;
 16import java.security.KeyStore;
 17import java.security.SecureRandom;
 18import java.security.cert.CertificateException;
 19import java.security.cert.X509Certificate;
 20
 21import javax.net.ssl.KeyManagerFactory;
 22import javax.net.ssl.SSLContext;
 23import javax.net.ssl.TrustManager;
 24import javax.net.ssl.X509TrustManager;
 25
 26// imports for a JAXRS client to simplify the code
 27import org.jboss.resteasy.client.jaxrs.ResteasyClient;
 28import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
 29import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget;
 30// logger imports
 31import org.slf4j.Logger;
 32import org.slf4j.LoggerFactory;
 33// testcontainers imports
 34import org.testcontainers.containers.GenericContainer;
 35import org.testcontainers.containers.wait.strategy.Wait;
 36
 37import jakarta.ws.rs.client.ClientBuilder;
 38// simple import to build a URI/URL
 39import jakarta.ws.rs.core.UriBuilder;
 40
 41public class LibertyContainer extends GenericContainer<LibertyContainer> {
 42
 43    static final Logger LOGGER = LoggerFactory.getLogger(LibertyContainer.class);
 44
 45    private String baseURL;
 46
 47    private KeyStore keystore;
 48    private SSLContext sslContext;
 49
 50    public static String getProtocol() {
 51        return System.getProperty("test.protocol", "https");
 52    }
 53
 54    public static boolean testHttps() {
 55        return getProtocol().equalsIgnoreCase("https");
 56    }
 57
 58    public LibertyContainer(final String dockerImageName) {
 59        super(dockerImageName);
 60        // wait for smarter planet message by default
 61        waitingFor(Wait.forLogMessage("^.*CWWKF0011I.*$", 1));
 62        init();
 63    }
 64
 65    // tag::createRestClient[]
 66    public <T> T createRestClient(Class<T> clazz, String applicationPath) {
 67        String urlPath = getBaseURL();
 68        if (applicationPath != null) {
 69            urlPath += applicationPath;
 70        }
 71        ClientBuilder builder = ResteasyClientBuilder.newBuilder();
 72        if (testHttps()) {
 73            builder.sslContext(sslContext);
 74            builder.trustStore(keystore);
 75        }
 76        ResteasyClient client = (ResteasyClient) builder.build();
 77        ResteasyWebTarget target = client.target(UriBuilder.fromPath(urlPath));
 78        return target.proxy(clazz);
 79    }
 80    // end::createRestClient[]
 81
 82    // tag::getBaseURL[]
 83    public String getBaseURL() throws IllegalStateException {
 84        if (baseURL != null) {
 85            return baseURL;
 86        }
 87        if (!this.isRunning()) {
 88            throw new IllegalStateException(
 89                "Container must be running to determine hostname and port");
 90        }
 91        baseURL =  getProtocol() + "://" + this.getContainerIpAddress()
 92            + ":" + this.getFirstMappedPort();
 93        System.out.println("TEST: " + baseURL);
 94        return baseURL;
 95    }
 96    // end::getBaseURL[]
 97
 98    private void init() {
 99
100        if (!testHttps()) {
101            this.addExposedPorts(9080);
102            return;
103        }
104
105        this.addExposedPorts(9443, 9080);
106        try {
107            String keystoreFile = System.getProperty("user.dir")
108                    + "/../../finish/system/src/main"
109                    + "/liberty/config/resources/security/key.p12";
110            keystore = KeyStore.getInstance("PKCS12");
111            keystore.load(new FileInputStream(keystoreFile), "secret".toCharArray());
112            KeyManagerFactory kmf = KeyManagerFactory.getInstance(
113                                        KeyManagerFactory.getDefaultAlgorithm());
114            kmf.init(keystore, "secret".toCharArray());
115            X509TrustManager xtm = new X509TrustManager() {
116                @Override
117                public void checkClientTrusted(X509Certificate[] chain, String authType)
118                    throws CertificateException { }
119
120                @Override
121                public void checkServerTrusted(X509Certificate[] chain, String authType)
122                    throws CertificateException { }
123
124                @Override
125                public X509Certificate[] getAcceptedIssuers() {
126                    return null;
127                }
128            };
129            TrustManager[] tm = new TrustManager[] {
130                                    xtm
131                                };
132            sslContext = SSLContext.getInstance("TLS");
133            sslContext.init(kmf.getKeyManagers(), tm, new SecureRandom());
134        } catch (Exception e) {
135            e.printStackTrace();
136        }
137    }
138}

The createRestClient() method creates a REST client instance with the SystemResourceClient interface. The getBaseURL() method constructs the URL that can access the inventory docker image.

Now, you can create your integration test cases.

Create the SystemResourceIT.java file.
src/test/java/it/io/openliberty/deepdive/rest/SystemResourceIT.java

SystemResourceIT.java

  1// tag::copyright[]
  2/*******************************************************************************
  3 * Copyright (c) 2022 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 v1.0
  6 * which accompanies this distribution, and is available at
  7 * http://www.eclipse.org/legal/epl-v10.html
  8 *
  9 * Contributors:
 10 *     IBM Corporation - Initial implementation
 11 *******************************************************************************/
 12// end::copyright[]
 13package it.io.openliberty.deepdive.rest;
 14
 15import static org.junit.jupiter.api.Assertions.assertEquals;
 16
 17import java.util.Base64;
 18import java.util.List;
 19
 20import org.junit.jupiter.api.BeforeAll;
 21import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
 22import org.junit.jupiter.api.Order;
 23import org.junit.jupiter.api.Test;
 24import org.junit.jupiter.api.TestMethodOrder;
 25import org.slf4j.Logger;
 26import org.slf4j.LoggerFactory;
 27import org.testcontainers.containers.GenericContainer;
 28import org.testcontainers.containers.Network;
 29import org.testcontainers.containers.output.Slf4jLogConsumer;
 30import org.testcontainers.containers.wait.strategy.Wait;
 31import org.testcontainers.junit.jupiter.Container;
 32import org.testcontainers.junit.jupiter.Testcontainers;
 33
 34@Testcontainers
 35@TestMethodOrder(OrderAnnotation.class)
 36public class SystemResourceIT {
 37
 38    private static Logger logger = LoggerFactory.getLogger(SystemResourceIT.class);
 39    private static String appPath = "/inventory/api";
 40    private static String postgresHost = "postgres";
 41    private static String postgresImageName = "postgres-sample:latest";
 42    private static String appImageName = "liberty-deepdive-inventory:1.0-SNAPSHOT";
 43
 44    public static SystemResourceClient client;
 45    // tag::network[]
 46    public static Network network = Network.newNetwork();
 47    // end::network[]
 48    private static String authHeader;
 49
 50    // tag::postgresSetup[]
 51    @Container
 52    public static GenericContainer<?> postgresContainer
 53        = new GenericContainer<>(postgresImageName)
 54              // tag::pNetwork[]
 55              .withNetwork(network)
 56              // end::pNetwork[]
 57              .withExposedPorts(5432)
 58              .withNetworkAliases(postgresHost)
 59              .withLogConsumer(new Slf4jLogConsumer(logger));
 60    // end::postgresSetup[]
 61
 62    // tag::libertySetup[]
 63    @Container
 64    public static LibertyContainer libertyContainer
 65        = new LibertyContainer(appImageName)
 66              .withEnv("POSTGRES_HOSTNAME", postgresHost)
 67              // tag::lNetwork[]
 68              .withNetwork(network)
 69              // end::lNetwork[]
 70              // tag::health[]
 71              .waitingFor(Wait.forHttp("/health/ready"))
 72              // end::health[]
 73              .withLogConsumer(new Slf4jLogConsumer(logger));
 74    // end::libertySetup[]
 75
 76    @BeforeAll
 77    public static void setupTestClass() throws Exception {
 78        System.out.println("TEST: Starting Liberty Container setup");
 79        client = libertyContainer.createRestClient(
 80            SystemResourceClient.class, appPath);
 81        String userPassword = "bob" + ":" + "bobpwd";
 82        authHeader = "Basic "
 83            + Base64.getEncoder().encodeToString(userPassword.getBytes());
 84    }
 85
 86    private void showSystemData(SystemData system) {
 87        System.out.println("TEST: SystemData > "
 88            + system.getId() + ", "
 89            + system.getHostname() + ", "
 90            + system.getOsName() + ", "
 91            + system.getJavaVersion() + ", "
 92            + system.getHeapSize());
 93    }
 94
 95    // tag::testAddSystem[]
 96    @Test
 97    @Order(1)
 98    public void testAddSystem() {
 99        System.out.println("TEST: Testing add a system");
100        // tag::addSystem[]
101        client.addSystem("localhost", "linux", "11", Long.valueOf(2048));
102        // end::addSystem[]
103        // tag::listContents[]
104        List<SystemData> systems = client.listContents();
105        // end::listContents[]
106        assertEquals(1, systems.size());
107        showSystemData(systems.get(0));
108        assertEquals("11", systems.get(0).getJavaVersion());
109        assertEquals(Long.valueOf(2048), systems.get(0).getHeapSize());
110    }
111    // end::testAddSystem[]
112
113    // tag::testUpdateSystem[]
114    @Test
115    @Order(2)
116    public void testUpdateSystem() {
117        System.out.println("TEST: Testing update a system");
118        // tag::updateSystem[]
119        client.updateSystem(authHeader, "localhost", "linux", "8", Long.valueOf(1024));
120        // end::updateSystem[]
121        // tag::getSystem[]
122        SystemData system = client.getSystem("localhost");
123        // end::getSystem[]
124        showSystemData(system);
125        assertEquals("8", system.getJavaVersion());
126        assertEquals(Long.valueOf(1024), system.getHeapSize());
127    }
128    // end::testUpdateSystem[]
129
130    // tag::testRemoveSystem[]
131    @Test
132    @Order(3)
133    public void testRemoveSystem() {
134        System.out.println("TEST: Testing remove a system");
135        // tag::removeSystem[]
136        client.removeSystem(authHeader, "localhost");
137        // end::removeSystem[]
138        List<SystemData> systems = client.listContents();
139        assertEquals(0, systems.size());
140    }
141    // end::testRemoveSystem[]
142}

Define the postgresContainer test container to start up the PostgreSQL docker image, and define the libertyContainer test container to start up the inventory docker image. Make sure that both containers use the same network. The /health/ready endpoint can tell you whether the container is ready to start testing.

The testAddSystem() verifies the addSystem and listContents endpoints.

The testUpdateSystem() verifies the updateSystem and getSystem endpoints.

The testRemoveSystem() verifies the removeSystem endpoint.

Create the log4j properites that are required by the Testcontainers framework.

Create the log4j.properties file.
src/test/resources/log4j.properties

log4j.properties

 1log4j.rootLogger=INFO, stdout
 2
 3log4j.appender=org.apache.log4j.ConsoleAppender
 4log4j.appender.layout=org.apache.log4j.PatternLayout
 5
 6log4j.appender.stdout=org.apache.log4j.ConsoleAppender
 7log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
 8log4j.appender.stdout.layout.ConversionPattern=%r %p %c %x - %m%n
 9
10log4j.logger.io.openliberty.guides.testing=DEBUG

Update the Maven configuration file with the required dependencies.

Replace the pom.xml file.
pom.xml

pom.xml

  1<?xml version="1.0" encoding="UTF-8" ?>
  2<project xmlns="http://maven.apache.org/POM/4.0.0"
  3               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5
  6    <modelVersion>4.0.0</modelVersion>
  7
  8    <groupId>io.openliberty.deepdive</groupId>
  9    <artifactId>inventory</artifactId>
 10    <packaging>war</packaging>
 11    <version>1.0-SNAPSHOT</version>
 12
 13    <properties>
 14        <maven.compiler.source>11</maven.compiler.source>
 15        <maven.compiler.target>11</maven.compiler.target>
 16        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 17        <liberty.var.default.http.port>9080</liberty.var.default.http.port>
 18        <liberty.var.default.https.port>9443</liberty.var.default.https.port>
 19        <liberty.var.default.context.root>/inventory</liberty.var.default.context.root>
 20        <liberty.var.client.https.port>9444</liberty.var.client.https.port>
 21    </properties>
 22
 23    <dependencies>
 24        <dependency>
 25            <groupId>jakarta.platform</groupId>
 26            <artifactId>jakarta.jakartaee-api</artifactId>
 27            <version>9.1.0</version>
 28            <scope>provided</scope>
 29        </dependency>
 30        <dependency>
 31            <groupId>org.eclipse.microprofile</groupId>
 32            <artifactId>microprofile</artifactId>
 33            <version>5.0</version>
 34            <type>pom</type>
 35            <scope>provided</scope>
 36        </dependency>
 37        <dependency>
 38            <groupId>org.postgresql</groupId>
 39            <artifactId>postgresql</artifactId>
 40            <version>42.3.1</version>
 41            <scope>provided</scope>
 42        </dependency>
 43        
 44        <!-- tag::testDependenies[] -->
 45        <!-- Test dependencies -->
 46        <dependency>
 47            <groupId>org.junit.jupiter</groupId>
 48            <artifactId>junit-jupiter</artifactId>
 49            <version>5.8.2</version>
 50            <scope>test</scope>
 51        </dependency>
 52        <dependency>
 53            <groupId>org.testcontainers</groupId>
 54            <artifactId>testcontainers</artifactId>
 55            <version>1.16.3</version>
 56            <scope>test</scope>
 57        </dependency>
 58        <dependency>
 59            <groupId>org.testcontainers</groupId>
 60            <artifactId>junit-jupiter</artifactId>
 61            <version>1.16.3</version>
 62            <scope>test</scope>
 63        </dependency>
 64        <dependency>
 65            <groupId>org.slf4j</groupId>
 66            <artifactId>slf4j-log4j12</artifactId>
 67            <version>1.7.36</version>
 68            <scope>test</scope>
 69        </dependency>
 70        <dependency>
 71            <groupId>org.jboss.resteasy</groupId>
 72            <artifactId>resteasy-client</artifactId>
 73            <version>6.0.0.Final</version>
 74            <scope>test</scope>
 75        </dependency>
 76        <dependency>
 77            <groupId>org.jboss.resteasy</groupId>
 78            <artifactId>resteasy-json-binding-provider</artifactId>
 79            <version>6.0.0.Final</version>
 80            <scope>test</scope>
 81        </dependency>
 82        <dependency>
 83            <groupId>org.glassfish</groupId>
 84            <artifactId>jakarta.json</artifactId>
 85            <version>2.0.1</version>
 86            <scope>test</scope>
 87        </dependency>
 88        <dependency>
 89            <groupId>org.eclipse</groupId>
 90            <artifactId>yasson</artifactId>
 91            <version>2.0.4</version>
 92            <scope>test</scope>
 93        </dependency>
 94        <dependency>
 95            <groupId>cglib</groupId>
 96            <artifactId>cglib-nodep</artifactId>
 97            <version>3.3.0</version>
 98            <scope>test</scope>
 99        </dependency>
100        <dependency>
101            <groupId>io.vertx</groupId>
102            <artifactId>vertx-auth-jwt</artifactId>
103            <version>4.0.3</version>
104            <scope>test</scope>
105        </dependency>
106        <!-- end::testDependenies[] -->
107    </dependencies>
108
109    <build>
110        <finalName>inventory</finalName>
111        <pluginManagement>
112            <plugins>
113                <plugin>
114                    <groupId>org.apache.maven.plugins</groupId>
115                    <artifactId>maven-war-plugin</artifactId>
116                    <version>3.3.2</version>
117                </plugin>
118                <plugin>
119                    <groupId>io.openliberty.tools</groupId>
120                    <artifactId>liberty-maven-plugin</artifactId>
121                    <version>3.5.1</version>
122                </plugin>
123            </plugins>
124        </pluginManagement>
125        <plugins>
126            <plugin>
127                <groupId>io.openliberty.tools</groupId>
128                <artifactId>liberty-maven-plugin</artifactId>
129                <configuration>
130                    <copyDependencies>
131                        <dependencyGroup>
132                            <location>${project.build.directory}/liberty/wlp/usr/shared/resources</location>
133                            <dependency>
134                                <groupId>org.postgresql</groupId>
135                                <artifactId>postgresql</artifactId>
136                                <version>42.3.1</version>
137                            </dependency>
138                        </dependencyGroup>
139                    </copyDependencies>
140                </configuration>
141            </plugin>
142            <!-- tag::failsafe[] -->
143            <plugin>
144                <groupId>org.apache.maven.plugins</groupId>
145                <artifactId>maven-failsafe-plugin</artifactId>
146                <version>2.22.0</version>
147                <executions>
148                    <execution>
149                        <goals>
150                            <goal>integration-test</goal>
151                            <goal>verify</goal>
152                        </goals>
153                    </execution>
154                </executions>
155            </plugin>
156            <!-- end::failsafe[] -->
157        </plugins>
158    </build>
159</project>

Add each required dependency with test scope, including JUnit5, Testcontainers, Log4J, JBoss RESTEasy client, Glassfish JSON, and Vert.x libraries. Also, add the maven-failsafe-plugin plugin, so that the integration test can be run by the Maven verify goal.

Running the tests

You can run the Maven verify goal, which compiles the java files, starts the containers, runs the tests, and then stops the containers.

mvn verify

In this Skills Network environment, you can test the HTTP protcol only.

You will see the following output:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running it.io.openliberty.deepdive.rest.SystemResourceIT
...
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 17.413 s - in it.io.openliberty.deepdive.rest.SystemResourceIT

Results :

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

Starting and preparing your cluster for deployment

Start your Kubernetes cluster.

Start your Docker Desktop environment.

Ensure that Kubernetes is running on Docker Desktop and that the context is set to docker-desktop.

Run the following command from a command-line session:

minikube start

Next, validate that you have a healthy Kubernetes environment by running the following command from the active command-line session.

kubectl get nodes

This command should return a Ready status for the master node.

You do not need to do any other step.

Run the following command to configure the Docker CLI to use Minikube’s Docker daemon. After you run this command, you will be able to interact with Minikube’s Docker daemon and build new images directly to it from your host machine:

eval $(minikube docker-env)

Deploying the microservice to Kubernetes

Now that the containerized application is built and tested, deploy it to a local Kubernetes cluster.

Installing the Open Liberty Operator

Install the Open Liberty Operator to deploy the microservice to Kubernetes.

First, install Custom Resource Definitions (CRDs) for the Open Liberty Operator by running the following command:

kubectl apply -f https://raw.githubusercontent.com/OpenLiberty/open-liberty-operator/main/deploy/releases/0.8.0/kubectl/openliberty-app-crd.yaml

Custom Resources extend the Kubernetes API and enhance its functionality.

Set environment variables for namespaces for the Operator by running the following commands:

set OPERATOR_NAMESPACE=default
set WATCH_NAMESPACE=\"\"
OPERATOR_NAMESPACE=default
WATCH_NAMESPACE='""'

Next, run the following commands to install cluster-level role-based access:

curl https://raw.githubusercontent.com/OpenLiberty/open-liberty-operator/main/deploy/releases/0.8.0/kubectl/openliberty-app-rbac-watch-all.yaml -o openliberty-app-rbac-watch-all.yaml

powershell -Command "(gc .\openliberty-app-rbac-watch-all.yaml) -replace 'OPEN_LIBERTY_OPERATOR_NAMESPACE', '%OPERATOR_NAMESPACE%' | Out-File -encoding ASCII .\openliberty-app-rbac-watch-all.yaml"

kubectl apply -f .\openliberty-app-rbac-watch-all.yaml
curl -L https://raw.githubusercontent.com/OpenLiberty/open-liberty-operator/main/deploy/releases/0.8.0/kubectl/openliberty-app-rbac-watch-all.yaml \
  | sed -e "s/OPEN_LIBERTY_OPERATOR_NAMESPACE/${OPERATOR_NAMESPACE}/" \
  | kubectl apply -f -

Finally, run the following commands to install the Operator:

curl https://raw.githubusercontent.com/OpenLiberty/open-liberty-operator/main/deploy/releases/0.8.0/kubectl/openliberty-app-operator.yaml -o openliberty-app-operator.yaml

powershell -Command "(gc .\openliberty-app-operator.yaml) -replace 'OPEN_LIBERTY_WATCH_NAMESPACE', '%WATCH_NAMESPACE%' | Out-File -encoding ASCII .\openliberty-app-operator.yaml"

kubectl apply -n %OPERATOR_NAMESPACE% -f .\openliberty-app-operator.yaml
curl -L https://raw.githubusercontent.com/OpenLiberty/open-liberty-operator/main/deploy/releases/0.8.0/kubectl/openliberty-app-operator.yaml \
  | sed -e "s/OPEN_LIBERTY_WATCH_NAMESPACE/${WATCH_NAMESPACE}/" \
  | kubectl apply -n ${OPERATOR_NAMESPACE} -f -

To check that the Open Liberty Operator is installed successfully, run the following command to view all the supported API resources that are available through the Open Liberty Operator:

kubectl api-resources --api-group=apps.openliberty.io

Look for the following output, which shows the custom resource definitions (CRDs) that can be used by the Open Liberty Operator:

NAME                      SHORTNAMES         APIGROUP              NAMESPACED   KIND
openlibertyapplications   olapp,olapps       apps.openliberty.io   true         OpenLibertyApplication
openlibertydumps          oldump,oldumps     apps.openliberty.io   true         OpenLibertyDump
openlibertytraces         oltrace,oltraces   apps.openliberty.io   true         OpenLibertyTrace

Each CRD defines a kind of object that can be used, which is specified in the previous example by the KIND value. The SHORTNAME value specifies alternative names that you can substitute in the configuration to refer to an object kind. For example, you can refer to the OpenLibertyApplication object kind by one of its specified shortnames, such as olapps.

The openlibertyapplications CRD defines a set of configurations for deploying an Open Liberty-based application, including the application image, number of instances, and storage settings. The Open Liberty Operator watches for changes to instances of the OpenLibertyApplication object kind and creates Kubernetes resources that are based on the configuration that is defined in the CRD.

Deploying the container image

Create the inventory.yaml in the start/inventory directory.
inventory.yaml

inventory.yaml

 1apiVersion: apps.openliberty.io/v1beta2
 2# tag::kind[]
 3kind: OpenLibertyApplication
 4# end::kind[]
 5metadata:
 6  name: inventory-deployment
 7  labels:
 8    name: inventory-deployment
 9spec:
10  # tag::applicationImage[]
11  applicationImage: liberty-deepdive-inventory:1.0-SNAPSHOT
12  # end::applicationImage[]
13  service:
14    port: 9443
15  env:
16    - name: POSTGRES_HOSTNAME
17      value: "postgres"

In the inventory.yaml file, the custom resource (CR) is specified to be OpenLibertyApplication. The CR triggers the Open Liberty Operator to create, update, or delete Kubernetes resources that are needed by the application to run on your cluster. Additionally, the applicationImage field must be specified and set to the image that was created in the previous module.

postgres.yaml

 1kind: ConfigMap
 2apiVersion: v1
 3metadata:
 4  namespace: default
 5  name: poststarthook
 6data:
 7  schema.sql: |
 8    CREATE TABLE SystemData (
 9        id SERIAL,
10        hostname varchar(50),
11        osName varchar(50),
12        javaVersion varchar(50),
13        heapSize bigint,
14        primary key(id)
15    );
16
17    CREATE SEQUENCE systemData_id
18    START 1
19    INCREMENT 1
20    OWNED BY SystemData.id;
21---
22# tag::deployment[]
23apiVersion: apps/v1
24kind: Deployment
25metadata:
26  name: postgres
27  labels:
28    app: postgres
29    group: db
30spec:
31  replicas: 1
32  selector:
33    matchLabels:
34      app: postgres
35  template:
36    metadata:
37      labels:
38        app: postgres
39        type: db
40    spec:
41      volumes:                             
42        - name: hookvolume
43          configMap:
44            name: poststarthook
45            defaultMode: 0755
46      containers:
47        - name: postgres
48          image: postgres:14.1
49          ports:
50            - containerPort: 5432
51          volumeMounts:
52            - name: hookvolume
53              mountPath: /docker-entrypoint-initdb.d
54          # tag::env[]
55          env:
56            - name: POSTGRES_USER
57              valueFrom:
58                secretKeyRef:
59                  name: post-app-credentials
60                  key: username
61            - name: POSTGRES_PASSWORD
62              valueFrom:
63                secretKeyRef:
64                  name: post-app-credentials
65                  key: password
66          # end::env[]
67# end::deployment[]
68---
69# tag::service[]
70apiVersion: v1
71kind: Service
72metadata:
73  name: postgres
74  labels: 
75    group: db
76spec:
77  type: ClusterIP
78  selector:             
79    app: postgres
80  ports:
81  - protocol: TCP
82    port: 5432
83# tag::service[]

Similarly, a Kubernetes resource definition is provided in the postgres.yaml file at the finish/postgres directory. In the postgres.yaml file, the deployment for the PostgreSQL database is defined.

Create a Kubernetes Secret to configure the credentials for the admin user to access the database.

kubectl create secret generic post-app-credentials --from-literal username=admin --from-literal password=adminpwd

The credentials are passed to the PostgreSQL database service as environment variables in the env field.

Run the following command to deploy the application and database:

kubectl apply -f inventory.yaml
kubectl apply -f ../../finish/postgres/postgres.yaml

When your pods are deployed, run the following command to check their status:

kubectl get pods

If all the pods are working correctly, you see an output similar to the following example:

NAME                                    READY   STATUS    RESTARTS   AGE
inventory-deployment-75f9dc56d9-g9lzl   1/1     Running   0          35s
postgres-58bd9b55c7-6vzz8               1/1     Running   0          13s
olo-controller-manager-6fc6b456dc-s29wl 1/1     Running   0          10m

Run the following command to set up port forwarding to access the inventory microservice:

kubectl port-forward svc/inventory-deployment 9443

You can check out the service at the https://localhost:9443/openapi/ui/ URL. The servers dropdown list shows the https://localhost:9443/inventory URL. Or, you can run the following command to access the inventory microservice:

curl -k https://localhost:9443/inventory/api/systems

When you’re done trying out the microservice, press CTRL+C in the command line session where you ran the kubectl port-forward command to stop the port forwarding.

Customizing deployments

server.xml

 1<?xml version="1.0" encoding="UTF-8"?>
 2<server description="inventory">
 3
 4    <featureManager>
 5        <feature>jakartaee-9.1</feature>
 6        <feature>microProfile-5.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    <!-- tag::contextRoot[] -->
13    <variable name="default.context.root" defaultValue="/inventory" />
14    <!-- end::contextRoot[] -->
15    <!-- tag::variables[] -->
16    <variable name="postgres/hostname" defaultValue="localhost" />
17    <variable name="postgres/portnum" defaultValue="5432" />
18    <variable name="postgres/username" defaultValue="admin" />
19    <variable name="postgres/password" defaultValue="adminpwd" />
20    <!-- end::variables[] -->
21
22    <httpEndpoint id="defaultHttpEndpoint"
23                  httpPort="${default.http.port}" 
24                  httpsPort="${default.https.port}" />
25
26    <!-- Automatically expand WAR files and EAR files -->
27    <applicationManager autoExpand="true"/>
28
29    <keyStore id="defaultKeyStore" password="secret" />
30    
31    <basicRegistry id="basic" realm="WebRealm">
32        <user name="bob" password="{xor}PTA9Lyg7" />
33        <user name="alice" password="{xor}PjM2PDovKDs=" />
34
35        <group name="admin">
36            <member name="bob" />
37        </group>
38
39        <group name="user">
40            <member name="bob" />
41            <member name="alice" />
42        </group>
43    </basicRegistry>
44
45    <!-- Configures the application on a specified context root -->
46    <webApplication contextRoot="${default.context.root}"
47                    location="inventory.war">
48        <application-bnd>
49            <security-role name="admin">
50                <group name="admin" />
51            </security-role>
52            <security-role name="user">
53                <group name="user" />
54            </security-role>
55        </application-bnd>
56    </webApplication>
57
58    <jwtSso jwtBuilderRef="jwtInventoryBuilder"/> 
59    <jwtBuilder id="jwtInventoryBuilder" 
60                issuer="http://openliberty.io" 
61                audiences="systemService"
62                expiry="24h"/>
63    <mpJwt audiences="systemService" 
64           groupNameAttribute="groups" 
65           id="myMpJwt" 
66           issuer="http://openliberty.io"/>
67
68    <!-- Default SSL configuration enables trust for default certificates from the Java runtime -->
69    <ssl id="defaultSSLConfig" trustDefaultCerts="true" />
70
71    <library id="postgresql-library">
72        <fileset dir="${shared.resource.dir}/" includes="*.jar" />
73    </library>
74
75    <dataSource id="DefaultDataSource" jndiName="jdbc/postgresql">
76        <jdbcDriver libraryRef="postgresql-library" />
77        <!-- tag::postgresProperties[] -->
78        <properties.postgresql databaseName="admin"
79                               serverName="${postgres/hostname}"
80                               portNumber="${postgres/portnum}"
81                               user="${postgres/username}"
82                               password="${postgres/password}"/>
83        <!-- end::postgresProperties[] -->
84    </dataSource>
85</server>

You can modify the inventory deployment to customize the service. Customizations for a service include changing the port number, changing the context root, and passing confidential information by using Secrets.

The default.context.root variable is defined in the server.xml file. The context root for the inventory service can be changed by using this variable. The value for the default.context.root variable can be defined in a ConfigMap and accessed as an environment variable.

Create a ConfigMap to configure the app name with the following kubectl command.

kubectl create configmap inv-app-root --from-literal contextRoot=/dev

This command deploys a ConfigMap named inv-app-root to your cluster. It has a key called contextRoot with a value of /dev. The --from-literal flag specifies individual key-value pairs to store in this ConfigMap.

Replace the inventory.yaml file.
inventory.yaml

inventory.yaml

 1apiVersion: apps.openliberty.io/v1beta2
 2# tag::kind[]
 3kind: OpenLibertyApplication
 4# end::kind[]
 5metadata:
 6  name: inventory-deployment
 7  labels:
 8    name: inventory-deployment
 9spec:
10  # tag::applicationImage[]
11  applicationImage: liberty-deepdive-inventory:1.0-SNAPSHOT
12  # end::applicationImage[]
13  service:
14    port: 9443
15  volumeMounts:
16  - name: postgres
17   # tag::mountPath[]
18    mountPath: "/config/variables/postgres"
19    # end::mountPath[]
20    readOnly: true
21  volumes:
22  - name: postgres
23    secret:
24      secretName: post-app-credentials 
25  env:
26    - name: POSTGRES_HOSTNAME
27      value: "postgres"
28    - name: DEFAULT_CONTEXT_ROOT
29      valueFrom:
30        configMapKeyRef:
31          name: inv-app-root
32          key: contextRoot

During deployment, the post-app-credentials secret can be mounted to the /config/variables/postgres in the pod to create Liberty config variables. Liberty creates variables from the files in the /config/variables/postgres directory. Instead of including confidential information in the server.xml, users can access it using normal Liberty variable syntax, ${postgres/username} and ${postgres/password}.

Run the following command to deploy your changes.

kubectl apply -f inventory.yaml

Run the following command to set up port forwarding to access the inventory microservice:

kubectl port-forward svc/inventory-deployment 9443

You can now check out the service at the https://localhost:9443/openapi/ui/ URL. The servers dropdown list shows the https://localhost:9443/dev URL. Or, you can run the following command to access the inventory microservice:

curl -k https://localhost:9443/dev/api/systems

Tearing down the environment

When you’re finished trying out the microservice, press CTRL+C in the command line session where you ran the kubectl port-forward command to stop the port forwarding. You can delete all Kubernetes resources by running the kubectl delete commands:

kubectl delete -f inventory.yaml
kubectl delete -f ../../finish/postgres/postgres.yaml
kubectl delete configmap inv-app-root
kubectl delete secret post-app-credentials

To uninstall the Open Liberty Operator, run the following commands:

set OPERATOR_NAMESPACE=default
set WATCH_NAMESPACE=\"\"

kubectl delete -n %OPERATOR_NAMESPACE% -f .\openliberty-app-operator.yaml

kubectl delete -f .\openliberty-app-rbac-watch-all.yaml

kubectl delete -f https://raw.githubusercontent.com/OpenLiberty/open-liberty-operator/main/deploy/releases/0.8.0/kubectl/openliberty-app-crd.yaml
OPERATOR_NAMESPACE=default
WATCH_NAMESPACE='""'

curl -L https://raw.githubusercontent.com/OpenLiberty/open-liberty-operator/main/deploy/releases/0.8.0/kubectl/openliberty-app-operator.yaml \
  | sed -e "s/OPEN_LIBERTY_WATCH_NAMESPACE/${WATCH_NAMESPACE}/" \
  | kubectl delete -n ${OPERATOR_NAMESPACE} -f -

curl -L https://raw.githubusercontent.com/OpenLiberty/open-liberty-operator/main/deploy/releases/0.8.0/kubectl/openliberty-app-rbac-watch-all.yaml \
  | sed -e "s/OPEN_LIBERTY_OPERATOR_NAMESPACE/${OPERATOR_NAMESPACE}/" \
  | kubectl delete -f -

kubectl delete -f https://raw.githubusercontent.com/OpenLiberty/open-liberty-operator/main/deploy/releases/0.8.0/kubectl/openliberty-app-crd.yaml

Stop your Kubernetes cluster.

Nothing more needs to be done for Docker Desktop.

Perform the following steps to return your environment to a clean state.

  1. Point the Docker daemon back to your local machine:

    eval $(minikube docker-env -u)
  2. Stop your Minikube cluster:

    minikube stop
  3. Delete your cluster:

    minikube delete

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 pom.xml, add the <configuration> element as the following:

  <plugin>
      <groupId>io.openliberty.tools</groupId>
      <artifactId>liberty-maven-plugin</artifactId>
      <version>3.5.1</version>
      <configuration>
          <runtimeArtifact>
              <groupId>com.ibm.websphere.appserver.runtime</groupId>
              <artifactId>wlp-kernel</artifactId>
               <version>[22.0.0.4,)</version>
               <type>zip</type>
          </runtimeArtifact>
      </configuration>
  </plugin>

Rebuild and restart the inventory service by dev mode:

mvn clean
mvn liberty:dev

In the Dockerfile, 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

Copied to clipboard
Copy code block
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