‏ ‏ ‎ ‏ ‏ ‎

1. Prerequisites - What students have to prepare

2. Learning Resources

3. Introduction to JakartaEE and microprofile

3.1. Syllabus

  • JakartaEE / JavaEE

  • Deployment

  • Application-Server

    • Download and Starting Application Server

  • Lecture-Example: Simple REST Endpoint

  • Big Picture: From IDE to a Running App in the Application Server

3.2. Questions

  • Difference Web-Server and Application Server

  • Why is an implementation of JakartaEE necessary?

  • What is a reference implementation?

  • What means "deployment"?

4. REST Introduction

4.1. First Example

4.2. Create a Project

4.2.2. Maven

mvn io.quarkus.platform:quarkus-maven-plugin:2.16.3.Final:create \
    -DprojectGroupId=at.htl.gettingstarted \
    -DprojectArtifactId=getting-started \
    -Dextensions='resteasy-jackson' \
    -DclassName="at.htl.gettingstarted.GreetingResource" \
    -Dpath="/hello"
cd getting-started
idea .

4.2.3. Quarkus CLI

quarkus create app at.htl.gettingstarted:getting-started \
    --extension='resteasy-jackson'
cd getting-started
idea .

4.3. Implement a service

GreetingResource.java
import org.eclipse.microprofile.config.inject.ConfigProperty;

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

@Path("/hello")
public class GreetingResource {

    @ConfigProperty(name = "test", defaultValue = "hello")
    String test;

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return test;
    }
}

4.4. Development mode

Starting in the development mode
./mvnw clean quarkus:dev

4.5. Request the Restful API

using httpie
$> http :8080/hello
HTTP/1.1 200 OK
Content-Length: 5
Content-Type: text/plain;charset=UTF-8

hello
using curl
$> curl localhost:8080/hello
hello%
using http-requests in IntelliJ
GET localhost:8080/hello

###
result
hello

http request in intellij

in browser

rest result in browser

using rest-clients

4.6. Test the Result

src/test/java/at/htl/getting/started/GreetingResourceTest.java
package at.htl.getting.started;

import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;

@QuarkusTest  (1)
public class GreetingResourceTest {

    @Test
    public void testHelloEndpoint() {
        given()
          .when().get("/hello")
          .then()
             .statusCode(200)
             .body(is("hello"));
    }
}
1 The first time a @QuarkusTest annotated tests is running, the Quarkus test extension will start a Quarkus instance. This instance will then remain running for the duration of the test run. This makes testing very fast, because Quarkus is only started once, however it does limit you to testing a single configuration per module, as you can’t restart Quarkus in a different configuration. Test profiles lift this limitation. (source)

4.7. What is a Restful Endpoint?

src/test/java/at/htl/getting/started/TimeServerResourceTest.java
package at.htl.getting.started;

import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.when;
import static org.hamcrest.Matchers.startsWith;

@QuarkusTest
class TimeServerResourceTest {

    @Test
    public void fetchTime() {
        when()
                .get("time")
        .then()
                .statusCode(200)
                .body(startsWith("Time:"));
    }
}
execute the tests
  • in the terminal

  • in the IDE

start the test in the terminal
./mvnw test
Every test shall fail at least once

4.8. HTTP Request Method

4.8.1. GET

4.8.2. POST

4.8.3. PUT

4.8.4. PATCH

4.8.5. DELETE

4.9. MIME-Types (MediaType)

4.9.1. text/plain

4.9.2. application/json

4.9.3. application/xml

4.10. Parametertypen

4.10.1. QueryParam

4.10.2. PathParam

4.10.3. FormParam

4.11. ParamConverter

The LocalDate type as a @QueryParam is not directly supported in JAX-RS. The JAX-RS runtime doesn’t know how to convert a String (which is what query parameters are in the HTTP request) into a LocalDate object. However, you can create a ParamConverter to tell JAX-RS how to convert a String into a LocalDate. Here’s an example of how you can do this:

import jakarta.ws.rs.ext.ParamConverter;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class LocalDateConverter implements ParamConverter<LocalDate> {

    @Override
    public LocalDate fromString(String value) {
        return LocalDate.parse(value, DateTimeFormatter.ISO_LOCAL_DATE);
    }

    @Override
    public String toString(LocalDate value) {
        return DateTimeFormatter.ISO_LOCAL_DATE.format(value);
    }
}

4.12. Response-Formate (MIME-Type)

  • JSON is a first class citizen

  • XML still works - it is a little bit outdated

  • Sources:

    • JavaEE 8 and Angular

4.12.1. void

4.12.2. Response

4.12.3. JsonValue/JsonObject/JsonArray

4.12.4. Entity Providers (JSON-Binding)

Converts ie Json to Object-Types like String or ie Person.

marshalling
Figure 1. Marshalling
unmarshalling
Figure 2. Unmarshalling
  • Libraries:

    • JSON-B für JSON

    • JAXB für XML

  • bei XML bei den Entitäten @XmlRootElement nicht vergessen

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class Person {
  //...
}

4.14. WebApplicationException

  • Why using WebApplicationExceptions, when the Response can also return error status codes:

    • Throwing the exception makes your code cleaner, easier to reason about and thus easier to understand. It’s then cleaner to throw new ProductNotFoundException() or throw new AccessDeniedException() and let the framework handle it instead of building a Response every time and later follow the details used to build it to figure out what’s happening in that section of code.

    • If you are using transactions, it allows the container to rollback any changes that you had previously made to your data within that request. If you just return a regular response, you will need to take care of that by your self, source

  • Example: Exception Handling

4.15. ExceptionMapper

import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import org.hibernate.exception.ConstraintViolationException; (1)

@Provider
public class HibernateConstraintViolationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {

    @Override
    public Response toResponse(ConstraintViolationException exception) {
        // You can create a custom response here
        return Response.status(Response.Status.BAD_REQUEST)
                .entity("Validation error: " + exception.getMessage())
                .build();
    }
}
import jakarta.validation.ConstraintViolationException;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;

@Provider
public class JakartaConstraintViolationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {

    @Override
    public Response toResponse(ConstraintViolationException exception) {
        // You can create a custom response here
        return Response.status(Response.Status.BAD_REQUEST)
                .entity("Validation error: " + exception.getMessage())
                .build();
    }
}
Create a custom exception mapper for hibernate- and jakarta-validation exceptions.
  • Hibernate-Exception are thrown, when using the constraints it @Column and @Table.

  • Jakarta-Validation-Exceptions are thrown, when using the quarkus-hibernate-validator-library (ie. @NotNull, @Size, …​) in the entity.

4.16. Context

When you are returning the path to a new created resource (after CREATE) you need the fully qualified url, which you can get with @Context UriInfo uriDetails.

@Path("checkup")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class CheckupResource {

    @Inject
    CheckupRepository checkupRepository;

    @Inject
    PersonRepository personRepository;

    @POST
    @Transactional
    public Response createCheckup(
            @QueryParam("name") String name,
            // removed for brevity ...
            @Context UriInfo uriInfo  (1)
            ) {

        // removed for brevity ...

        var checkup = new Checkup(person, date, size, weight);
        checkupRepository.persist(checkup);
        UriBuilder uriBuilder = uriInfo  (2)
                .getAbsolutePathBuilder()
                .path(checkup.getId().toString()
                );

        return Response
                .created(uriBuilder.build()) (3)
                .build();
    }


    // removed for brevity ...

}
Response
HTTP/1.1 201 Created
Location: http://localhost:8080/api/checkup/1  (4)
content-length: 0

<Response body is empty>
1 The @Context UriInfo uriDetails is used to get the fully qualified url of the new created resource. To the absolutePath the primary key of the entity (checkup.getId()) is attached.
2 The Response.created(uriBuilder.build()) is used to return the fully qualified url of the new created resource.
3 Location: http://localhost:8080/api/checkup/1 is the result in the header of the response.

4.17. Json-Libraries

4.17.1. JSON-B

Begriffe
  • Mapping

  • Default Mapping

  • Customizing with Annotations

4.20. Create the Vehicle Project

create-vehicle.sh
mvn io.quarkus.platform:quarkus-maven-plugin:3.4.1:create \
    -DprojectGroupId=at.htlleonding.vehicle \
    -DprojectArtifactId=vehicle \
    -Dextensions='resteasy-jackson' \
    -DclassName="at.htlleonding.vehicle.boundary.VehicleResource" \
    -Dpath="/vehicle"
cd vehicle
idea .
with quarkus cli
quarkus create app at.htlleonding.vehicle:vehicle
add dependencies with maven
./mvnw quarkus:add-extension -Dextensions="jdbc-postgresql,hibernate-orm"
check, if you use the resteasy reactive dependency
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>
We use for resteasy the reactive extension, even when the other dependencies are classic (blocking, synchron)

4.21. Create the Entity

src/main/java/at/htlleonding/vehicle/entity/Vehicle.java
package at.htlleonding.vehicle.entity;

public class Vehicle {
    private String brand;
    private String model;

    public Vehicle() {
    }

    public Vehicle(String brand, String model) {
        this.brand = brand;
        this.model = model;
    }

    //region getter and setter
    public String getBrand() {
        return brand;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }

    public String getModel() {
        return model;
    }

    public void setModel(String model) {
        this.model = model;
    }
    //endregion
}

4.22. Create the Restful Service

./src/main/java/at/htlleonding/vehicle/boundary/VehicleResource.java
package at.htlleonding.vehicle.boundary;

import at.htlleonding.vehicle.entity.Vehicle;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.util.ArrayList;
import java.util.List;

@Path("/vehicle")
public class VehicleResource {

    @GET
    @Path("{id}")
    public Vehicle find(@PathParam("id") long id) {
        return new Vehicle("Opel " + id, "Commodore");
    }

    @GET
    public List<Vehicle> findAll() {
        List<Vehicle> all = new ArrayList<>();
        all.add(find(42));
        return all;
    }
}

4.22.1. Add JSON-Binding

execute in terminal to add JSON-Binding
./mvnw quarkus:add-extension -Dextensions="io.quarkus:quarkus-resteasy-reactive-jackson"
http-requests/requests.http
GET localhost:8080/vehicle
result in terminal
GET http://localhost:8080/vehicle

HTTP/1.1 200 OK
Content-Length: 41
Content-Type: application/json

[
  {
    "brand": "Opel 42",
    "model": "Commodore"
  }
]

Response code: 200 (OK); Time: 25ms; Content length: 41 bytes

4.22.2. Test findAll()

unit test with REST-assured and Hamcrest
package at.htl.vehicle.boundary;

import at.htl.vehicle.entity.Vehicle;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;

import java.util.List;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;

@QuarkusTest
public class VehicleResourceTest {

    @Test
    public void testVehicleEndpoint() {
        given()
        .when()
             //.log().body() // to log the request body (here it is empty)
             .get("/vehicle")
        .then()
             .log().body()   // to log the response body (1)
             .statusCode(200)
             .body("brand[0]",is("Opel 42"),  (2)
                   "model[0]",is("Commodore")
             );
    }
}
1 you can print out the body, headers, …​ very conveniently.
2 because the result is an array, you access an element of the array.
test result in terminal
...
... INFO  [io.quarkus] (main) Quarkus 3.4.1 on JVM started in 1.205s. Listening on: http://0.0.0.0:8081
... INFO  [io.quarkus] (main) Profile test activated.
... 21:55:38,368 INFO  [io.quarkus] (main) Installed features: [cdi, resteasy, resteasy-jsonb]

[
    {
        "brand": "Opel 42",
        "model": "Commodore"
    }
]

... INFO  [io.quarkus] (main) Quarkus stopped in 0.021s

Process finished with exit code 0

4.22.3. Test find(…​)

http-requests/requests.http
GET localhost:8080/vehicle/123
result in terminal
GET http://localhost:8080/vehicle/123

HTTP/1.1 200 OK
Content-Length: 40
Content-Type: application/json

{
  "brand": "Opel 123",
  "model": "Commodore"
}

Response code: 200 (OK); Time: 24ms; Content length: 40 bytes
unit test with REST-assured and Hamcrest
@Test
public void testVehicleEndpointWithId() {
    given()
         .pathParam("id", "123") (1)
    .when()
         //.log().body() // to log the request body (here is empty)
         .get("/vehicle/{id}")
    .then()
         .log().body()   // to log the response body
         .statusCode(200)
         .body("brand",is("Opel 123"),
               "model",is("Commodore")
         );
}
test output in terminal
...
... INFO  [io.quarkus] (main) Quarkus 3.4.1.Final on JVM started in 1.172s. Listening on: http://0.0.0.0:8081
... INFO  [io.quarkus] (main) Profile test activated.
... INFO  [io.quarkus] (main) Installed features: [cdi, resteasy, resteasy-jsonb]

{
    "brand": "Opel 123",
    "model": "Commodore"
}

... INFO  [io.quarkus] (main) Quarkus stopped in 0.022s

Process finished with exit code 0

4.23. Questions

  • Why is the unit test executing w/o starting Quarkus explicitly?
    (@QuarkusTest starts Quarkus on Port 8081)

  • Why do you need to start Quarkus when using the file "request.http"?
    (Because this are only requests, the app must run therefore)

4.24. Exercises

  • Create an Vehicle Service with CRUD functionality

    • (GET)

    • POST

    • DELETE

    • PATCH

    • PUT

  • Create Vehicle service tests (REST-assured) with AssertJ instead of Hamcrest

4.25. Keynote Presentation

4.26. TODO

  • Example with CRUD functionality

  • Example with AssertJ

5. Persistence / JPA

5.1. Example Car Rental

5.1.1. Entity Model

cld
We could use a composite id for rental (jpa buddy, baeldung). For reasons of brevity, we use a surrogate key.
in production, don’t use "double" as currency-data type → there is JSR 354

5.1.2. Start a postgres db in Docker

start postgresql in a Docker container (docker-hub)
docker run --ulimit memlock=-1:-1 -it --rm=true --memory-swappiness=0 \
           --name postgres-db -e POSTGRES_USER=app \
           -e POSTGRES_PASSWORD=app -e POSTGRES_DB=db \
           -p 5432:5432 postgres:15.2-alpine
terminal output from starting the db
 $❯ docker run --ulimit memlock=-1:-1 -it --rm=true --memory-swappiness=0 \
           --name postgres-db -e POSTGRES_USER=app \
           -e POSTGRES_PASSWORD=app -e POSTGRES_DB=db \
           -p 5432:5432 postgres:15.2-alpine
Unable to find image 'postgres:15.2-alpine' locally
15.2-alpine: Pulling from library/postgres
af6eaf76a39c: Pull complete
71286d2ce0cc: Pull complete
b82afe47906a: Pull complete
75d514bb4aa7: Pull complete
217da6f41d9e: Pull complete
39a3f4823126: Pull complete
ed6571a6afcc: Pull complete
8ae7d38f54c4: Pull complete
Digest: sha256:6e3513dbe0e4049d9385a33f1cb6e40a32f86c24ca0c62306a6d916aa126b9f7
Status: Downloaded newer image for postgres:15.2-alpine
WARNING: Your kernel does not support memory swappiness capabilities or the cgroup is not mounted. Memory swappiness discarded.
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.

The database cluster will be initialized with locale "en_US.utf8".
The default database encoding has accordingly been set to "UTF8".
The default text search configuration will be set to "english".

Data page checksums are disabled.

fixing permissions on existing directory /var/lib/postgresql/data ... ok
creating subdirectories ... ok
selecting dynamic shared memory implementation ... posix
selecting default max_connections ... 100
selecting default shared_buffers ... 128MB
selecting default time zone ... UTC
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... sh: locale: not found
2023-02-25 09:13:51.852 UTC [30] WARNING:  no usable system locales were found
ok
syncing data to disk ... ok

initdb: warning: enabling "trust" authentication for local connections
initdb: hint: You can change this by editing pg_hba.conf or using the option -A, or --auth-local and --auth-host, the next time you run initdb.

Success. You can now start the database server using:

    pg_ctl -D /var/lib/postgresql/data -l logfile start

waiting for server to start....2023-02-25 09:13:52.267 UTC [36] LOG:  starting PostgreSQL 15.2 on aarch64-unknown-linux-musl, compiled by gcc (Alpine 12.2.1_git20220924-r4) 12.2.1 20220924, 64-bit
2023-02-25 09:13:52.270 UTC [36] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2023-02-25 09:13:52.275 UTC [39] LOG:  database system was shut down at 2023-02-25 09:13:52 UTC
2023-02-25 09:13:52.279 UTC [36] LOG:  database system is ready to accept connections
 done
server started
CREATE DATABASE


/usr/local/bin/docker-entrypoint.sh: ignoring /docker-entrypoint-initdb.d/*

waiting for server to shut down....2023-02-25 09:13:52.410 UTC [36] LOG:  received fast shutdown request
2023-02-25 09:13:52.411 UTC [36] LOG:  aborting any active transactions
2023-02-25 09:13:52.414 UTC [36] LOG:  background worker "logical replication launcher" (PID 42) exited with exit code 1
2023-02-25 09:13:52.415 UTC [37] LOG:  shutting down
2023-02-25 09:13:52.417 UTC [37] LOG:  checkpoint starting: shutdown immediate
2023-02-25 09:13:52.460 UTC [37] LOG:  checkpoint complete: wrote 918 buffers (5.6%); 0 WAL file(s) added, 0 removed, 0 recycled; write=0.013 s, sync=0.025 s, total=0.045 s; sync files=250, longest=0.009 s, average=0.001 s; distance=4222 kB, estimate=4222 kB
2023-02-25 09:13:52.480 UTC [36] LOG:  database system is shut down
 done
server stopped

PostgreSQL init process complete; ready for start up.

2023-02-25 09:13:52.530 UTC [1] LOG:  starting PostgreSQL 15.2 on aarch64-unknown-linux-musl, compiled by gcc (Alpine 12.2.1_git20220924-r4) 12.2.1 20220924, 64-bit
2023-02-25 09:13:52.530 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
2023-02-25 09:13:52.530 UTC [1] LOG:  listening on IPv6 address "::", port 5432
2023-02-25 09:13:52.533 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2023-02-25 09:13:52.537 UTC [52] LOG:  database system was shut down at 2023-02-25 09:13:52 UTC
2023-02-25 09:13:52.541 UTC [1] LOG:  database system is ready to accept connections
  • Originalversion der DB: 14.1

  • memlock
    maximum locked-in-memory address space (KB)
    This is memory that will not be paged out. It is frequently used by
    database management applications such as Oracle or Sybase to lock
    shared memory for a shared pool so that it is always in memory for
    access by multiple sessions.

  • This is a developer db. The data is stored inside the container, that means it is not really persistent.

5.1.3. Service Dashboard in IntelliJ for Docker

service dashboard for docker

5.1.4. Start a postgres in docker-compose

database download script
open terminal in IDE
chmod +x ./postgres-download-scripts.sh
./postgres-download-scripts.sh
./postgres-create-db.sh
./postgres-start.sh
  • use services for viewing the logs

database services postgres docker compose

5.1.5. Create a Datasource in IntelliJ IDEA

Option 1 - manually
create a datasource in IntelliJ

jpa intellij create datasource 1

configure the datasource

jpa intellij create datasource 2

how to create a table manually

jpa intellij create table

  • Option 2 - use "bit.ly/htl-leonding-scripts"

datasource create
  1. open datasource.txt

  2. select-all → copy

  3. +

  4. Import Data Sources …​

datasource import
  • the first time you have to Download the JDBC-driver

  • check if the connection works: Test Connection

datasource test

5.1.6. Add Dependencies to pom.xml

add extension to pom.xml
./mvnw quarkus:add-extension -Dextensions="quarkus-hibernate-orm,quarkus-jdbc-postgresql,quarkus-resteasy-jackson"
also possible:
quarkus ext add jdbc-postgres hibernate-orm resteasy-jackson
terminal output
[INFO] Scanning for projects...
[INFO]
[INFO] -----------------------< at.htl.vehicle:vehicle >-----------------------
[INFO] Building vehicle 1.0.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- quarkus-maven-plugin:2.16.3.Final:add-extension (default-cli) @ vehicle ---
[INFO] Looking for the newly published extensions in registry.quarkus.io
[INFO] [SUCCESS] ✅  Extension io.quarkus:quarkus-hibernate-orm has been installed
[INFO] [SUCCESS] ✅  Extension io.quarkus:quarkus-jdbc-postgresql has been installed
[INFO] [SUCCESS] ✅  Extension io.quarkus:quarkus-resteasy-jackson has been installed
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  3.782 s
[INFO] Finished at: 2023-02-26T11:56:59+01:00
[INFO] ------------------------------------------------------------------------
add assertj-core and -db for testing
<!-- testing -->
<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.24.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-db</artifactId>
    <version>2.0.2</version>
    <scope>test</scope>
</dependency>

5.1.7. Startup Method

src/main/java/at/htl/vehicle/control/InitBean.java
package at.htl.vehicle.control;

import io.quarkus.runtime.StartupEvent;
import org.jboss.logging.Logger;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;

@ApplicationScoped
public class InitBean {

    private static final Logger LOG = Logger.getLogger(InitBean.class); (1)

    void startup(@Observes StartupEvent event) { (2)
        LOG.info("It works!");
    }
}
1 Initialize the Logging. When using the jboss-logger no additional dependencies are necessary. An another option is:
import org.jboss.logging.Logger;
//...
@Inject
Logger LOG;
//...
2 Observer-Pattern. Callback-method after starting

5.1.8. First start

use maven-wrapper
./mvnw clean quarkus:dev
use quarkus-cli
quarkus dev --clean
output
...
Listening for transport dt_socket at address: 5005
__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2023-02-26 12:01:27,681 WARN  [org.hib.eng.jdb.spi.SqlExceptionHelper] (JPA Startup Thread) SQL Warning Code: 0, SQLState: 00000

2023-02-26 12:01:27,758 INFO  [at.htl.veh.con.InitBean] (Quarkus Main Thread) It works
2023-02-26 12:01:27,796 INFO  [io.quarkus] (Quarkus Main Thread) vehicle 1.0.0-SNAPSHOT on JVM (powered by Quarkus 2.16.3.Final) started in 1.357s. Listening on: http://localhost:8080
2023-02-26 12:01:27,797 INFO  [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
...

5.1.9. Configure application.properties

  • replace existing application.properties

database replace application properties
# datasource configuration
quarkus.datasource.db-kind = postgresql
quarkus.datasource.username = app
quarkus.datasource.password = app
quarkus.datasource.jdbc.url = jdbc:postgresql://localhost:5432/db

# drop and create the database at startup (use `update` to only update the schema)
quarkus.hibernate-orm.database.generation=drop-and-create

5.1.11. Entity

src/main/java/at/htl/vehicle/entity/Vehicle.java
package at.htl.vehicle.entity;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Vehicle {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String brand;
    private String model;
    private double pricePerDay;

    //region constructors
    public Vehicle() {
    }

    public Vehicle(String brand, String model) {
        this.brand = brand;
        this.model = model;
    }

    public Vehicle(String brand, String model, double pricePerDay) {
        this.brand = brand;
        this.model = model;
        this.pricePerDay = pricePerDay;
    }
    //endregion

    //region getter and setter
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getBrand() {
        return brand;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }

    public String getModel() {
        return model;
    }

    public void setModel(String model) {
        this.model = model;
    }

    public double getPricePerDay() {
        return pricePerDay;
    }

    public void setPricePerDay(double pricePerDay) {
        this.pricePerDay = pricePerDay;
    }

    //endregion


    @Override
    public String toString() {
        return String.format("%d: %s %s (%.2f €)", id, brand, model, pricePerDay);
    }
}

5.1.12. Insert Data when Starting

src/main/resources/import.sql
insert into vehicle (brand, model, PRICEPERDAY) VALUES ('VW', 'Käfer 1400', 30.0);
insert into vehicle (brand, model, PRICEPERDAY) VALUES ('Opel', 'Blitz', 50.0);
  • the name of the insert-sql-file is import.sql (placed in the root of the resources folder)

  • when you want to change this name use this property: quarkus.hibernate-orm.sql-load-script

jpa create table after startup

5.1.13. VehicleRepository

src/main/java/at/htl/vehicle/control/VehicleRepository.java
package at.htl.vehicle.control;

import at.htl.vehicle.entity.Vehicle;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.persistence.EntityManager;

@ApplicationScoped
public class VehicleRepository {

    @Inject
    EntityManager em;

    public void save(Vehicle vehicle) {
        em.persist(vehicle);
    }

    public void save(String brand, String model) {
        em.persist(new Vehicle(brand, model));
    }

    public Vehicle findById(long id) {
        return em.find(Vehicle.class, id);
    }

    public List<Vehicle> findAll() {
        TypedQuery<Vehicle> query = em.createQuery("select v from Vehicle v", Vehicle.class);
        return query.getResultList();
    }
}
src/main/java/at/htl/vehicle/boundary/VehicleResource.java
package at.htl.vehicle.boundary;

import at.htl.vehicle.control.VehicleRepository;
import at.htl.vehicle.entity.Vehicle;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.util.ArrayList;
import java.util.List;

@Path("/vehicle")
public class VehicleResource {

    @Inject
    VehicleRepository vehicleRepository;

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("{id}")
    public Vehicle find(@PathParam("id") long id) {
        return vehicleRepository.findById(id);
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<Vehicle> findAll() {
        return vehicleRepository.findAll();
    }
}

5.1.14. A quick glimpse to the result

GET localhost:8080/vehicle
terminal output
GET http://localhost:8080/vehicle

HTTP/1.1 200 OK
Content-Length: 85
Content-Type: application/json

[
  {
    "brand": "VW",
    "id": 1,
    "model": "Käfer 1400"
  },
  {
    "brand": "Opel",
    "id": 2,
    "model": "Blitz"
  }
]

Response code: 200 (OK); Time: 89ms; Content length: 84 bytes
GET localhost:8080/vehicle/2
terminal output
GET http://localhost:8080/vehicle/2

HTTP/1.1 200 OK
Content-Length: 39
Content-Type: application/json

{
  "brand": "Opel",
  "id": 2,
  "model": "Blitz"
}

Response code: 200 (OK); Time: 40ms; Content length: 39 bytes

5.1.15. Testing w/ JUnit and assertJ

add to pom.xml
<dependency>
  <groupId>org.assertj</groupId>
  <artifactId>assertj-core</artifactId>
  <version>3.24.2</version>
  <scope>test</scope>
</dependency>
VehicleResourceTest.java
package at.htl.vehicle.boundary;

import at.htl.vehicle.entity.Vehicle;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import javax.ws.rs.client.Entity;
import java.util.ArrayList;
import java.util.List;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;

import static org.assertj.core.api.Assertions.*;

@QuarkusTest
public class VehicleResourceTest {

    @Test
    public void testVehicleEndpoint() {
        List<Vehicle> vehicles = new ArrayList<>();

        vehicles = given()
             .when()
                //.log().body() // to log the request body (here it is empty)
                .get("/vehicle")
             .then()
                .log().body()   // to log the response body
                .statusCode(200)
                .extract().body().jsonPath().getList(".", Vehicle.class);

        System.out.println(vehicles);

        assertThat(vehicles) (1)
                .isNotEmpty()
                .hasSize(2)
                .extracting(Vehicle::getBrand)
                .containsOnly("VW", "Opel");
    }

    @Test
    public void testVehicleEndpointWithId() {
        given()
                .pathParam("id", "2")
        .when()
                //.log().body() // to log the request body (here is empty)
                .get("/vehicle/{id}")
        .then()
                .log().body()   // to log the response body
                .statusCode(200)
                .body("brand", is("Opel"),  (2)
                      "model", is("Blitz"));
    }
}
1 assertThat is from assertJ
2 is(…​) is a hamcrest-matcher
it is possible to start the tests in the IDE and also to debug it
start tests
./mvnw test
terminal output
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------------< at.htl:vehicle >---------------------------
[INFO] Building vehicle 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- quarkus-maven-plugin:1.7.1.Final:prepare (default) @ vehicle ---
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ vehicle ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 3 resources
[INFO]
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ vehicle ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- quarkus-maven-plugin:1.7.1.Final:prepare-tests (default) @ vehicle ---
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ vehicle ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /Users/stuetz/SynologyDrive/htl/skripten/themen/jakartaee-microprofile/quarkus-lecture-notes/labs/100-rest/vehicle/src/test/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ vehicle ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-surefire-plugin:3.0.0-M5:test (default-test) @ vehicle ---
[INFO]
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running at.htl.vehicle.boundary.VehicleResourceTest
2020-08-30 16:48:27,117 INFO  [at.htl.veh.con.InitBean] (main) It works!
2020-08-30 16:48:27,204 INFO  [io.quarkus] (main) Quarkus 1.7.1.Final on JVM started in 2.082s. Listening on: http://0.0.0.0:8081
2020-08-30 16:48:27,205 INFO  [io.quarkus] (main) Profile test activated.
2020-08-30 16:48:27,205 INFO  [io.quarkus] (main) Installed features: [agroal, cdi, hibernate-orm, jdbc-postgresql, mutiny, narayana-jta, resteasy, resteasy-jsonb, smallrye-context-propagation]
{
    "brand": "Opel",
    "id": 2,
    "model": "Blitz"
}
[
    {
        "brand": "VW",
        "id": 1,
        "model": "Käfer 1400"
    },
    {
        "brand": "Opel",
        "id": 2,
        "model": "Blitz"
    }
]
[at.htl.vehicle.entity.Vehicle@45f6181a, at.htl.vehicle.entity.Vehicle@19d0d1ab]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.848 s - in at.htl.vehicle.boundary.VehicleResourceTest
2020-08-30 16:48:28,763 INFO  [io.quarkus] (main) Quarkus stopped in 0.022s
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  7.085 s
[INFO] Finished at: 2020-08-30T16:48:28+02:00
[INFO] ------------------------------------------------------------------------

5.1.16. Exercise

  • Add custom insert-scripts (ins_vehicle.sql) in sql to prepare the application with data.

  • Add the other entity classes (Rental, Customer) - Don’t forget the associations.

  • Add CRUD-functionality to the restful API.

  • Add named queries

6. Persistence with Panache

7. Websockets

7.1. Add Dependency to pom.xml

add dependency to pom.xml
./mvnw quarkus:add-extension -Dextensions="websockets"

7.2. Implement Server

src/main/java/at/htl/vehicle/boundary/ChatSocket.java
package at.htl.vehicle.boundary;

import javax.enterprise.context.ApplicationScoped;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@ServerEndpoint("/chat/{username}")
@ApplicationScoped
public class ChatSocket {

    Map<String, Session> sessions = new ConcurrentHashMap<>();

    @OnOpen
    public void onOpen(Session session, @PathParam("username") String username) {
        sessions.put(username, session);
        broadcast("User " + username + " joined");
    }

    @OnClose
    public void onClose(Session session, @PathParam("username") String username) {
        sessions.remove(username);
        broadcast("User " + username + " left");
    }

    @OnError
    public void onError(Session session, @PathParam("username") String username, Throwable throwable) {
        sessions.remove(username);
        broadcast("User " + username + " left on error: " + throwable);
    }

    @OnMessage
    public void onMessage(String message, @PathParam("username") String username) {
        broadcast(">> " + username + ": " + message);
    }

    private void broadcast(String message) {
        sessions.values().forEach(s -> {
            s.getAsyncRemote().sendObject(message, result ->  {  (1)
                if (result.getException() != null) {
                    System.out.println("Unable to send message: " + result.getException());
                }
            });
        });
    }

}
1 beside the getAsyncRemote()-method is also a synchronous getBasicremote()-method available

7.4. Client - html-page

src/main/resources/META-INF/resources/index.html
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>Quarkus Chat!</title>
    <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/patternfly/3.24.0/css/patternfly.min.css">
    <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/patternfly/3.24.0/css/patternfly-additions.min.css">

    <style>
        #chat {
            resize: none;
            overflow: hidden;
            min-height: 300px;
            max-height: 300px;
        }
    </style>
</head>

<body>
<nav class="navbar navbar-default navbar-pf" role="navigation">
    <div class="navbar-header">
        <a class="navbar-brand" href="/">
            <p><strong>>> Quarkus Chat!</strong></p>
        </a>
    </div>
</nav>
<div class="container">
    <br/>
    <div class="row">
        <input id="name" class="col-md-4" type="text" placeholder="your name">
        <button id="connect" class="col-md-1 btn btn-primary" type="button">connect</button>
        <br/>
        <br/>
    </div>
    <div class="row">
          <textarea class="col-md-8" id="chat">
            </textarea>
    </div>
    <div class="row">
        <input class="col-md-6" id="msg" type="text" placeholder="enter your message">
        <button class="col-md-1 btn btn-primary" id="send" type="button" disabled>send</button>
    </div>

</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/patternfly/3.24.0/js/patternfly.min.js"></script>

<script type="text/javascript">
    var connected = false;
    var socket;

    $( document ).ready(function() {
        $("#connect").click(connect);
        $("#send").click(sendMessage);

        $("#name").keypress(function(event){
            if(event.keyCode == 13 || event.which == 13) {
                connect();
            }
        });

        $("#msg").keypress(function(event) {
            if(event.keyCode == 13 || event.which == 13) {
                sendMessage();
            }
        });

        $("#chat").change(function() {
            scrollToBottom();
        });

        $("#name").focus();
    });

    var connect = function() {
        if (! connected) {
            var name = $("#name").val();
            console.log("Val: " + name);
            socket = new WebSocket("ws://" + location.host + "/chat/" + name);
            socket.onopen = function() {
                connected = true;
                console.log("Connected to the web socket");
                $("#send").attr("disabled", false);
                $("#connect").attr("disabled", true);
                $("#name").attr("disabled", true);
                $("#msg").focus();
            };
            socket.onmessage =function(m) {
                console.log("Got message: " + m.data);
                $("#chat").append(m.data + "\n");
                scrollToBottom();
            };
        }
    };

    var sendMessage = function() {
        if (connected) {
            var value = $("#msg").val();
            console.log("Sending " + value);
            socket.send(value);
            $("#msg").val("");
        }
    };

    var scrollToBottom = function () {
        $('#chat').scrollTop($('#chat')[0].scrollHeight);
    };

</script>
</body>

</html>

websocket html 1 websocket html 2

7.6. Exercises

  • Create a quarkus application for chatting.
    All sessions receiving a message get the prefix "<<<" in the log.
    The session sending a message get the prefix ">>>" in the log.

  • Create a quarkus application with a websocket endpoint.
    The server informes the client about goals netween team1 and team2.
    Every client shows the result on his screen.
    The communication is in Json. The client is wriiten in Java
    When the client is a team1 follower then on the screen is printed "Hooray" when team1 scores else "It is a pity".
    When the client is a team2 follwer the actions are vice versa.

  • RBAC

  • Security Realms

  • KeyCloak

8. Security

8.1. Clone Project

git clone https://github.com/aisge/securitydemo.git

8.2. Start Database

docker run --ulimit memlock=-1:-1 -it --rm=true --memory-swappiness=0 \
           --name postgres-db -e POSTGRES_USER=app \
           -e POSTGRES_PASSWORD=app -e POSTGRES_DB=db \
           -p 5432:5432 postgres:12.4

8.3. Annotate Security-Policy

  • GET-methods: @RolesAllowed("user")

  • UPDATE-methods: @RolesAllowed("admin")

9. Quarkus - Cloud

  • Native

  • Docker

  • Kubernetes

9.1. Create an Container Images with Jib, Docker, S2I

9.2. Minikube

9.3. Google Cloud

9.4. Oracle Cloud Platform

9.5. IBM Cloud Platform

10. Qute: Server-Side-Web-Pages

11. microprofile / Quarkus - Introduction

  • Health Check

  • OpenTracing

  • Metrics

  • Fault Tolerance

12. Error Handling

12.1. in restful Services

12.2. db handling

13. Miscancellous

  • Logging

  • noSQL Database

  • Flyway / Liquibase

14. Sources

15. Furthermore interesting sources

16. Cheat Sheet