1. Prerequisites - What students have to prepare
-
OS for developing
-
up-to-date JDK installed (Adoptium/Temurin)
-
JDK checked - has to run in a shell
-
Windows: in a shell
-
java -version
-
javac -version
-
echo %JAVA_HOME%
-
-
Linux: in a shell
-
java -version
-
javac -version
-
echo $JAVA_HOME
-
-
-
Docker locally installed
-
up-to-date DerbyDB downloaded and extracted in i.e.
/opt/db-derby-10.16.1.1-bin
-
eventually: Minikube or kind
-
up-to-date JetBrains Ultimate IDE
-
nice-to-have:
-
maven-wrapper (update with
./mvnw wrapper:wrapper
)
3. Introduction to JakartaEE and microprofile
4. REST Introduction
4.2. Create a Project
4.3. Implement a service
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.5. Request the Restful API
$> http :8080/hello
HTTP/1.1 200 OK
Content-Length: 5
Content-Type: text/plain;charset=UTF-8
hello
$> curl localhost:8080/hello
hello%
GET localhost:8080/hello
###
hello
-
RESTClient for Mozilla Firefox
-
Advanced REST client for Google Chrome
4.6. Test the Result
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?
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:"));
}
}
-
in the terminal
-
in the IDE
./mvnw test
Every test shall fail at least once |
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.4. Entity Providers (JSON-Binding)
Converts ie Json to Object-Types like String or ie Person.
-
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()
orthrow 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 ...
}
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.18. Request- und Response-Filter
4.19. Reactive Endpoints
-
Writing simpler reactive REST services with Quarkus Virtual Thread support (ehemals Project Loom) == REST with Entities
4.20. Create the Vehicle Project
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 .
quarkus create app at.htlleonding.vehicle:vehicle
./mvnw quarkus:add-extension -Dextensions="jdbc-postgresql,hibernate-orm"
<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
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
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
./mvnw quarkus:add-extension -Dextensions="io.quarkus:quarkus-resteasy-reactive-jackson"
GET localhost:8080/vehicle
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()
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. |
...
... 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(…)
GET localhost:8080/vehicle/123
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
@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")
);
}
...
... 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
5. Persistence / JPA
5.1. Example Car Rental
5.1.1. Entity Model
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
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.4. Start a postgres in docker-compose
We are using the https://bit.ly/htl-leonding-scripts |
-
download postgres-download-scripts.sh
-
move the file into the project root
chmod +x ./postgres-download-scripts.sh
./postgres-download-scripts.sh
./postgres-create-db.sh
./postgres-start.sh
-
use services for viewing the logs
5.1.5. Create a Datasource in IntelliJ IDEA
Option 1 - manually
-
Option 2 - use "bit.ly/htl-leonding-scripts"
-
open datasource.txt
-
select-all → copy
-
+
-
Import Data Sources …
-
the first time you have to Download the JDBC-driver
-
check if the connection works: Test Connection
5.1.6. Add Dependencies to pom.xml
./mvnw quarkus:add-extension -Dextensions="quarkus-hibernate-orm,quarkus-jdbc-postgresql,quarkus-resteasy-jackson"
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] ------------------------------------------------------------------------
<!-- 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
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:
|
2 | Observer-Pattern. Callback-method after starting |
5.1.8. First start
./mvnw clean quarkus:dev
quarkus dev --clean
...
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
# 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
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
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
5.1.13. VehicleRepository
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();
}
}
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
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.24.2</version>
<scope>test</scope>
</dependency>
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 |
Examples for assertJ: Testing Java Collections with AssertJ
it is possible to start the tests in the IDE and also to debug it |
./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] ------------------------------------------------------------------------
7. Websockets
7.1. Add Dependency to pom.xml
./mvnw quarkus:add-extension -Dextensions="websockets"
7.2. Implement Server
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
<!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>
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