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
5. REST with Entities
-
First we build a simple RESTful service with a simple entity.
5.1. Create the Vehicle Project
mvn io.quarkus.platform:quarkus-maven-plugin:3.15.1:create \
-DprojectGroupId=at.htlleonding.vehicle \
-DprojectArtifactId=vehicle-simple \
-Dextensions='resteasy' \
-DclassName="at.htlleonding.vehicle.boundary.VehicleResource" \
-Dpath="/vehicle"
cd vehicle
idea . # open in IntelliJ idea
see also: Creating a new project
quarkus create app at.htlleonding.vehicle:vehicle
./mvnw quarkus:add-extension -Dextensions="smallrye-openapi"
5.2. Create the Entity
package at.htlleonding.vehicle.entity;
// imports omitted for brevity
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
}
5.3. 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;
}
}
5.3.1. Add JSON-Binding
./mvnw quarkus:add-extension -Dextensions="io.quarkus:quarkus-resteasy-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
5.3.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
5.3.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
5.4. 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)
5.5. Exercises
-
Create an Vehicle Service with CRUD functionality
-
(GET)
-
POST
-
DELETE
-
PATCH
-
PUT
-
-
Create Vehicle service tests (REST-assured) with AssertJ instead of Hamcrest
6. Comparing the Data-Types when POSTing Entities
There are several ways create entities per REST:
-
with the Entity-/Object-Types (Person, Vehicle, …)
-
with a json-parsing-api like
-
with the DTO pattern (Data-Transfer-Object)
In the next example we use the datafaker-library
6.1. Create the Quarkus Project
-
or use this maven-command
mvn io.quarkus.platform:quarkus-maven-plugin:3.15.1:create \
-DprojectGroupId=at.htlleonding.vehicle \
-DprojectArtifactId=vehicle \
-Dextensions='resteasy-jsonb, resteasy-jackson, resteasy-jaxb' \
-DclassName="at.htlleonding.vehicle.boundary.VehicleResource" \
-Dpath="/api-jsonb"
cd vehicle
idea . # open in IntelliJ idea
-
add swagger (openapi) to project
6.2. Create the Entity
package at.htlleonding.vehicle.entity;
// omitted imports for brevity
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Vehicle {
private String brand;
private String model;
@JsonProperty("license-plate-no") // wird auch beim jsonb-Bsp gebraucht, da die REST-assured-Tests jackson verwenden
@JsonbProperty("license-plate-no") // da jsonb im Endpoint verwendet wird
private String licensePlateNo;
public Vehicle() {
}
public Vehicle(String brand, String model) {
this.brand = brand;
this.model = model;
}
public Vehicle(String brand, String model, String licensePlateNo) {
this.brand = brand;
this.model = model;
this.licensePlateNo = licensePlateNo;
}
// getter and setter omitted for brevity
@Override
public String toString() {
return String.format("%s: %s %s", licensePlateNo, brand, model);
}
}
6.3. Create the Repository
package at.htlleonding.vehicle.control;
import at.htlleonding.vehicle.entity.Vehicle;
import jakarta.enterprise.context.ApplicationScoped;
import net.datafaker.Faker;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@ApplicationScoped
public class VehicleRepository {
private final Map<String, Vehicle> vehicles;
public VehicleRepository() {
vehicles = new HashMap<>();
init();
}
void init() {
vehicles.clear();
save(new Vehicle("Opel", "Blitz", "UU-12345A"));
save(new Vehicle("Opel", "Kadett", "LL-12345B"));
save(new Vehicle("Opel", "Commodore", "W-12345C"));
save(new Vehicle("VW", "Käfer", "L-12345D"));
}
public void save(Vehicle vehicle) {
vehicles.put(vehicle.getLicensePlateNo(), vehicle);
}
public void save(String brand, String model, String licensePlateNo) {
save(new Vehicle(brand, model, licensePlateNo));
}
public void delete(String licensePlate) {
vehicles.remove(licensePlate);
}
public Vehicle findById(String licensePlateNo) {
return vehicles.get(licensePlateNo);
}
public List<Vehicle> findByBrand(String brand) {
return vehicles
.values()
.stream()
.filter(x -> x.getBrand().equalsIgnoreCase(brand))
.collect(Collectors.toList());
}
public List<Vehicle> findAll() {
return Collections.synchronizedList(
Collections.unmodifiableList(
new LinkedList<>(vehicles.values())
)
);
}
public List<Vehicle> generateFakeCars(int noOfCars) {
return IntStream.rangeClosed(1, noOfCars)
.boxed()
.map(i -> createFakeCar())
.collect(Collectors.toList());
}
private Vehicle createFakeCar() {
var faker = new Faker();
Random rnd = new Random();
int min = 10000;
int max = 99999;
String brand = faker.dune().planet();
String model = faker.ancient().titan();
String licensePlate = faker.address().countryCode() + "-" + (rnd.nextInt(max - min) + min);
return new Vehicle(brand, model, licensePlate);
}
}
6.4. Variante 1: POST with JSON-B
package at.htlleonding.vehicle.boundary;
import at.htlleonding.vehicle.control.VehicleRepository;
import at.htlleonding.vehicle.entity.Vehicle;
import jakarta.inject.Inject;
import jakarta.json.JsonObject;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.*;
import org.jboss.logging.Logger;
import java.util.List;
@Path("/api-jsonb")
public class VehicleResourceJsonb {
private static final Logger LOG = Logger.getLogger(VehicleResourceJsonb.class.getSimpleName());
@Inject
VehicleRepository vehicleRepository;
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response create(
JsonObject vehicleJson,
@Context UriInfo uriInfo
) {
if (!vehicleJson.containsKey("license-plate-no")) {
throw new WebApplicationException("LicensePlate is missing", Response.Status.BAD_REQUEST);
}
if (vehicleJson.getString("license-plate-no").isBlank()) {
throw new WebApplicationException("LicensePlate not valid", Response.Status.BAD_REQUEST);
}
// aus dem Json-Objekt wird ein Java-Objekt erstellt (jedes Element einzeln)
Vehicle vehicle = new Vehicle(
vehicleJson.getString("brand"),
vehicleJson.getString("model"),
vehicleJson.getString("license-plate-no"));
// aus dem Json-Objekt wird ebenfalls ein Java-Objekt erstellt,
// diesmal automatisch mit einem JsonBuilder
Vehicle vehicle2;
try (Jsonb jsonb = JsonbBuilder.create()) {
vehicle2 = jsonb.fromJson(
vehicleJson.toString(),
Vehicle.class
);
//throw new RuntimeException("intended error");
} catch (Exception e) {
throw new RuntimeException("Marshalling failed", e);
}
LOG.info("vehicle 2 -> " + vehicle2);
vehicleRepository.save(vehicle);
// https://stackoverflow.com/a/26094619
UriBuilder uriBuilder = uriInfo
.getAbsolutePathBuilder()
.path(vehicle.getLicensePlateNo());
LOG.info(uriInfo
.getAbsolutePathBuilder()
.path(vehicle.getLicensePlateNo()).build());
return Response.created(uriBuilder.build()).build();
}
@GET
@Produces({
MediaType.APPLICATION_JSON,
MediaType.APPLICATION_XML
})
@Path("{id}")
public Vehicle find(@PathParam("id") String id) {
return vehicleRepository.findById(id);
}
@GET
@Produces({
MediaType.APPLICATION_JSON
, MediaType.APPLICATION_XML
})
public List<Vehicle> findAll() {
return vehicleRepository.findAll();
}
@GET
@Path("/exception/{id}")
@Produces(MediaType.TEXT_HTML)
public String exceptionDemo(@PathParam("id") int id) {
if (id == 1) {
Response.ResponseBuilder builder = Response.status(Response.Status.BAD_REQUEST);
builder.type("text/html")
.header("x-my-message", "very bad request")
.entity("<html><h1>BAD REQUEST</h1></html>");
throw new WebApplicationException(builder.build());
} else if (id == 3) {
throw new WebApplicationException(Response.Status.BAD_REQUEST);
}
return "ok";
}
}
6.5. Create IntelliJ REST Client
# choose an environment in "Run with:"
### 1. create messerschmitt kabinenroller
POST {{host}}/{{path}}
Content-Type: application/json
{
"brand": "Messerschmitt",
"model": "Kabinenroller",
"license-plate-no": "LL-4711ABC"
}
### 2. create messerschmitt kabinenroller w/o plates
POST {{host}}/{{path}}
Content-Type: application/json
{
"brand": "Messerschmitt",
"model": "Kabinenroller",
"license-plate-no": " "
}
### 3. create messerschmitt kabinenroller w/o plates
POST {{host}}/{{path}}
Content-Type: application/json
{
"brand": "Messerschmitt",
"model": "Kabinenroller"
}
### 4. read license-plate LL-4711ABC (json)
GET {{host}}/{{path}}/LL-4711ABC
Accept: application/json
### 5. read all vehicle (json)
GET {{host}}/{{path}}
Accept: application/json
### 6. read all vehicle (xml)
GET {{host}}/{{path}}
Accept: application/xml
### 7. read license-plate L-12345D (json)
GET {{host}}/{{path}}/L-12345D
Accept: application/json
### 8. read license-plate L-12345D (xml)
GET {{host}}/{{path}}/L-12345D
Accept: application/xml
###
-
define the host- and path-variable
{
"jsonb": {
"host": "localhost:8080",
"path": "api-jsonb"
},
"jackson": {
"host": "localhost:8080",
"path": "api-jackson"
},
"objects": {
"host": "localhost:8080",
"path": "api-object"
},
"dto": {
"host": "localhost:8080",
"path": "api-dto"
}
}
-
At the moment we need only the jsonb-environment
-
First, we start the project with
./mvnw clean quarkus:dev
-
Now we choose the jsonb-environment in the IntelliJ REST Client
-
-
All requests should work as expected
6.6. Variante 2: POST with Jackson
package at.htlleonding.vehicle.boundary;
// imports omitted for brevity
@Path("/api-jackson")
public class VehicleResourceJackson {
private static final Logger LOG = Logger.getLogger(VehicleResourceJackson.class.getSimpleName());
@Inject
VehicleRepository vehicleRepository;
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response create(
JsonNode vehicleJson,
@Context UriInfo uriInfo
) {
if (!vehicleJson.has("license-plate-no")) {
throw new WebApplicationException("LicensePlate is missing", Response.Status.BAD_REQUEST);
}
if (vehicleJson.get("license-plate-no").asText().isBlank()) {
throw new WebApplicationException("LicensePlate not valid", Response.Status.BAD_REQUEST);
}
// aus dem Json-Objekt wird ein Java-Objekt erstellt (jedes Element einzeln)
Vehicle vehicle = new Vehicle(
vehicleJson.get("brand").asText(),
vehicleJson.get("model").asText(),
vehicleJson.get("license-plate-no").asText());
// aus dem Json-Objekt wird ebenfalls ein Java-Objekt erstellt,
// diesmal automatisch mit einem ObjectMapper
Vehicle vehicle2;
try {
ObjectMapper objectMapper = new ObjectMapper();
vehicle2 = objectMapper.treeToValue(vehicleJson, Vehicle.class);
//throw new RuntimeException("intended error");
} catch (Exception e) {
throw new RuntimeException("Marshalling failed", e);
}
LOG.info("vehicle 2 -> " + vehicle2);
vehicleRepository.save(vehicle);
// https://stackoverflow.com/a/26094619
UriBuilder uriBuilder = uriInfo
.getAbsolutePathBuilder()
.path(vehicle.getLicensePlateNo());
LOG.info(uriInfo
.getAbsolutePathBuilder()
.path(vehicle.getLicensePlateNo()).build());
return Response.created(uriBuilder.build()).build();
}
@GET
@Produces({
MediaType.APPLICATION_JSON,
MediaType.APPLICATION_XML
})
@Path("{id}")
public Vehicle find(@PathParam("id") String id) {
return vehicleRepository.findById(id);
}
@GET
@Produces({
MediaType.APPLICATION_JSON
, MediaType.APPLICATION_XML
})
public List<Vehicle> findAll() {
return vehicleRepository.findAll();
}
@GET
@Path("/exception/{id}")
@Produces(MediaType.TEXT_HTML)
public String exceptionDemo(@PathParam("id") int id) {
if (id == 1) {
Response.ResponseBuilder builder = Response.status(Response.Status.BAD_REQUEST);
builder.type("text/html")
.header("x-my-message", "very bad request")
.entity("<html><h1>BAD REQUEST</h1></html>");
throw new WebApplicationException(builder.build());
} else if (id == 3) {
throw new WebApplicationException(Response.Status.BAD_REQUEST);
}
return "ok";
}
}
-
Choose the jackson-environment in the IntelliJ REST Client
-
The requests should work as expected
6.7. Variante 3: POST with Objects
package at.htlleonding.vehicle.boundary;
import at.htlleonding.vehicle.control.VehicleRepository;
import at.htlleonding.vehicle.entity.Vehicle;
import io.quarkus.logging.Log;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.*;
import java.util.List;
@Path("/api-object")
public class VehicleResourceObject {
@Inject
VehicleRepository vehicleRepository;
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response create(Vehicle vehicle, @Context UriInfo uriInfo) {
if (vehicle.getLicensePlateNo() == null) {
throw new WebApplicationException("LicensePlate is missing", Response.Status.BAD_REQUEST);
}
if (vehicle.getLicensePlateNo().isBlank()) {
throw new WebApplicationException("LicensePlate not valid", Response.Status.BAD_REQUEST);
}
vehicleRepository.save(vehicle);
// https://stackoverflow.com/a/26094619
UriBuilder uriBuilder = uriInfo
.getAbsolutePathBuilder()
.path(vehicle.getLicensePlateNo());
Log.info(uriInfo
.getAbsolutePathBuilder()
.path(vehicle.getLicensePlateNo()).build());
return Response.created(uriBuilder.build()).build();
}
@GET
@Produces({
MediaType.APPLICATION_JSON,
MediaType.APPLICATION_XML
})
@Path("{id}")
public Vehicle find(@PathParam("id") String id) {
return vehicleRepository.findById(id);
}
@GET
@Produces({
MediaType.APPLICATION_JSON
, MediaType.APPLICATION_XML
})
public List<Vehicle> findAll() {
return vehicleRepository.findAll();
}
@GET
@Path("/exception/{id}")
@Produces(MediaType.TEXT_HTML)
public String exceptionDemo(@PathParam("id") int id) {
if (id == 1) {
Response.ResponseBuilder builder = Response.status(Response.Status.BAD_REQUEST);
builder.type("text/html")
.header("x-my-message", "very bad request")
.entity("<html><h1>BAD REQUEST</h1></html>");
throw new WebApplicationException(builder.build());
} else if (id == 3) {
throw new WebApplicationException(Response.Status.BAD_REQUEST);
}
return "ok";
}
}
-
Choose the objects-environment in the IntelliJ REST Client
-
The requests should work as expected
-
When using the entity data-type, you do not have to convert the JSON-Object to a Java-Object manually
6.8. Variante 4: POST with DTO
-
add a Vehicle DTO
package at.htlleonding.vehicle.entity.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.json.bind.annotation.JsonbProperty;
public record VehicleDto(
String brand,
String model,
@JsonProperty("license-plate-no") // für jackson
@JsonbProperty("license-plate-no") // für jsonb
String licensePlateNo
) { }
-
add two methods to convert the DTO to the entity and vice versa
package at.htlleonding.vehicle.entity;
// imports omitted for brevity
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Vehicle {
// ...
public static VehicleDto toDto(Vehicle vehicle) {
return new VehicleDto(
vehicle.getBrand(),
vehicle.getModel(),
vehicle.getLicensePlateNo()
);
}
public static Vehicle fromDto(VehicleDto vehicleDto) {
return new Vehicle(
vehicleDto.brand(),
vehicleDto.model(),
vehicleDto.licensePlateNo()
);
}
// ...
}
-
add a new REST-Resource
package at.htlleonding.vehicle.boundary;
import at.htlleonding.vehicle.control.VehicleRepository;
import at.htlleonding.vehicle.entity.Vehicle;
import at.htlleonding.vehicle.entity.dto.VehicleDto;
import io.quarkus.logging.Log;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.*;
import java.util.List;
@Path("/api-dto")
public class VehicleResourceDto {
@Inject
VehicleRepository vehicleRepository;
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response create(VehicleDto vehicleDto, @Context UriInfo uriInfo) {
if (vehicleDto.licensePlateNo() == null) {
throw new WebApplicationException("LicensePlate is missing", Response.Status.BAD_REQUEST);
}
if (vehicleDto.licensePlateNo().isBlank()) {
throw new WebApplicationException("LicensePlate not valid", Response.Status.BAD_REQUEST);
}
Vehicle vehicle = Vehicle.fromDto(vehicleDto);
vehicleRepository.save(vehicle);
// https://stackoverflow.com/a/26094619
UriBuilder uriBuilder = uriInfo
.getAbsolutePathBuilder()
.path(vehicle.getLicensePlateNo());
Log.info(uriInfo
.getAbsolutePathBuilder()
.path(vehicle.getLicensePlateNo()).build());
return Response.created(uriBuilder.build()).build();
}
@GET
@Produces({
MediaType.APPLICATION_JSON,
MediaType.APPLICATION_XML
})
@Path("{id}")
public Vehicle find(@PathParam("id") String id) {
return vehicleRepository.findById(id);
}
@GET
@Produces({
MediaType.APPLICATION_JSON
, MediaType.APPLICATION_XML
})
public List<Vehicle> findAll() {
return vehicleRepository.findAll();
}
}
-
Choose the dto-environment in the IntelliJ REST Client
-
The requests should work as expected
6.9. Unit-Tests for all environments
package at.htlleonding.vehicle.boundary;
// imports omitted for brevity
import static org.assertj.core.api.Assertions.*;
@QuarkusTest
public class VehicleResourceTest {
@ParameterizedTest
@ValueSource(strings = {"api-jsonb", "api-jackson", "api-object", "api-dto"})
public void testVehicleEndpointWithAssertj(String route) {
List<Vehicle> vehicles = new ArrayList<>();
vehicles = given()
.accept(ContentType.JSON)
.when()
//.log().body() // to log the request body (here it is empty)
.get("/" + route)
.then()
.log().body() // to log the response body
.statusCode(200)
.extract()
.body()
.jsonPath()
.getList(".", Vehicle.class)
;
System.out.println(vehicles);
// das Attribut 'brand' darf nur Opel oder VW enthalten
assertThat(vehicles)
.isNotEmpty()
.hasSize(4)
.extracting(Vehicle::getBrand)
.containsOnly("VW", "Opel");
// alle Elemente werden genau geprüft
List<Vehicle> expectedVehicles = Arrays.asList(
new Vehicle("Opel", "Blitz", "UU-12345A"),
new Vehicle("Opel", "Commodore", "W-12345C"),
new Vehicle("Opel", "Kadett", "LL-12345B"),
new Vehicle("VW", "Käfer", "L-12345D")
);
assertThat(vehicles)
.usingRecursiveFieldByFieldElementComparator()
.containsExactlyInAnyOrderElementsOf(expectedVehicles);
}
@ParameterizedTest
@ValueSource(strings = {"api-jsonb", "api-jackson", "api-object", "api-dto"})
public void testVehicleEndpointWithLicensePlate(String route) {
given()
.pathParam("id", "W-12345C")
.accept("application/json")
.log().all()
.when()
.log().body() // to log the request body (here it is empty)
.get("/" + route + "/{id}")
.then()
.log().all() // to log the response body
.statusCode(200)
.body(
"brand", is("Opel"),
"model", is("Commodore"),
"license-plate-no", is("W-12345C")
);
}
}
7. Persistence / JPA
7.1. Example Car Rental
7.1.1. Entity Model
We could use a composite id for rental (Ultimate Guide on Composite IDs, baeldung). For reasons of brevity, we use a surrogate key. |
in production, don’t use "double" as currency-data type → there is JSR 354 |
7.1.2. Start a postgres db in Docker
docker run --rm \
--name postgres-db \
-e POSTGRES_USER=app \
-e POSTGRES_PASSWORD=app \
-e POSTGRES_DB=db \
-p 5432:5432 postgres:17-alpine
terminal output from starting the db
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 2024-10-24 20:53:39.927 UTC [36] WARNING: no usable system locales were found ok syncing data to disk ... ok Success. You can now start the database server using: pg_ctl -D /var/lib/postgresql/data -l logfile start 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. waiting for server to start....2024-10-24 20:53:40.689 UTC [42] LOG: starting PostgreSQL 17.0 on aarch64-unknown-linux-musl, compiled by gcc (Alpine 13.2.1_git20240309) 13.2.1 20240309, 64-bit 2024-10-24 20:53:40.691 UTC [42] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432" 2024-10-24 20:53:40.695 UTC [45] LOG: database system was shut down at 2024-10-24 20:53:40 UTC 2024-10-24 20:53:40.699 UTC [42] 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...2024-10-24 20:53:40.967 UTC [42] LOG: received fast shutdown request .2024-10-24 20:53:40.968 UTC [42] LOG: aborting any active transactions 2024-10-24 20:53:40.970 UTC [42] LOG: background worker "logical replication launcher" (PID 48) exited with exit code 1 2024-10-24 20:53:40.970 UTC [43] LOG: shutting down 2024-10-24 20:53:40.970 UTC [43] LOG: checkpoint starting: shutdown immediate 2024-10-24 20:53:41.061 UTC [43] LOG: checkpoint complete: wrote 924 buffers (5.6%); 0 WAL file(s) added, 0 removed, 0 recycled; write=0.072 s, sync=0.017 s, total=0.091 s; sync files=301, longest=0.001 s, average=0.001 s; distance=4250 kB, estimate=4250 kB; lsn=0/1911638, redo lsn=0/1911638 2024-10-24 20:53:41.072 UTC [42] LOG: database system is shut down done server stopped PostgreSQL init process complete; ready for start up. 2024-10-24 20:53:41.184 UTC [1] LOG: starting PostgreSQL 17.0 on aarch64-unknown-linux-musl, compiled by gcc (Alpine 13.2.1_git20240309) 13.2.1 20240309, 64-bit 2024-10-24 20:53:41.184 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432 2024-10-24 20:53:41.185 UTC [1] LOG: listening on IPv6 address "::", port 5432 2024-10-24 20:53:41.186 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432" 2024-10-24 20:53:41.192 UTC [58] LOG: database system was shut down at 2024-10-24 20:53:41 UTC 2024-10-24 20:53:41.198 UTC [1] LOG: database system is ready to accept connections
-
Originalversion der DB: 14.1
-
This is a developer db. The data is stored inside the container, that means it is not really persistent.
7.1.4. Start a postgres in docker-compose
We are using the https://bit.ly/htl-leonding-scripts |
-
download postgres-download-scripts-17.0.sh
-
move the file into the project root
chmod +x ./postgres-download-scripts-17.0.sh
./postgres-download-scripts-17.0.sh
./postgres-create-db.sh
./postgres-start.sh
-
use services for viewing the logs
7.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
7.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>
7.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 |
7.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.
...
7.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
7.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);
}
}
7.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
7.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();
}
}
7.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
7.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] ------------------------------------------------------------------------
9. Websockets
9.1. Add Dependency to pom.xml
./mvnw quarkus:add-extension -Dextensions="websockets"
9.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 |
9.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>
9.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