├── .gitignore ├── Dockerfile ├── README.adoc ├── deployment └── coffee-shop.yaml ├── pom.xml ├── presentation ├── slide001 ├── slide002 ├── slide003 ├── slide004 └── syntax.vim └── src ├── main ├── java │ └── com │ │ └── sebastian_daschner │ │ └── coffee_shop │ │ ├── JAXRSConfiguration.java │ │ ├── health │ │ └── Health.java │ │ ├── orders │ │ ├── CoffeeTypeDeserializer.java │ │ ├── boundary │ │ │ ├── CoffeeShop.java │ │ │ └── OrdersResource.java │ │ ├── control │ │ │ └── Orders.java │ │ └── entity │ │ │ ├── CoffeeOrder.java │ │ │ ├── CoffeeType.java │ │ │ └── OrderStatus.java │ │ └── price │ │ └── control │ │ └── PriceCalculator.java ├── liberty │ └── config │ │ └── server.xml ├── resources │ └── META-INF │ │ └── microprofile-config.properties └── webapp │ └── WEB-INF │ └── beans.xml └── test └── java └── com └── sebastian_daschner └── coffee_shop └── orders ├── CoffeeTypeDeserializerTest.java └── boundary ├── CoffeeShopIT.java └── CoffeeShopSystem.java /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pom.xml.tag 3 | pom.xml.releaseBackup 4 | pom.xml.versionsBackup 5 | pom.xml.next 6 | release.properties 7 | 8 | *.iml 9 | .idea/ 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM open-liberty:javaee8-java12 2 | 3 | COPY src/main/liberty/config/server.xml /config/ 4 | 5 | COPY target/coffee-shop.war /config/dropins/ -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = Enterprise Coffee 2 | 3 | The example project of my Jakarta EE Tech Talk _Best practices for modern Enterprise Java projects_. 4 | 5 | Comprises a _coffee-shop_ application, which can be deployed locally, or to a Kubernetes and Istio cluster. 6 | -------------------------------------------------------------------------------- /deployment/coffee-shop.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: coffee-shop 5 | labels: 6 | app: coffee-shop 7 | spec: 8 | selector: 9 | app: coffee-shop 10 | ports: 11 | - port: 9080 12 | name: http 13 | --- 14 | 15 | kind: Deployment 16 | apiVersion: apps/v1beta1 17 | metadata: 18 | name: coffee-shop 19 | spec: 20 | replicas: 1 21 | template: 22 | metadata: 23 | labels: 24 | app: coffee-shop 25 | version: v1 26 | spec: 27 | containers: 28 | - name: coffee-shop 29 | image: sdaschner/coffee-shop:ee-best-practices-1 30 | imagePullPolicy: Always 31 | ports: 32 | - containerPort: 9080 33 | readinessProbe: 34 | exec: 35 | command: 36 | - /bin/sh 37 | - -c 38 | - curl -f localhost:9080/health 39 | initialDelaySeconds: 30 40 | restartPolicy: Always 41 | --- 42 | 43 | kind: Gateway 44 | apiVersion: networking.istio.io/v1alpha3 45 | metadata: 46 | name: coffee-shop-gateway 47 | spec: 48 | selector: 49 | istio: ingressgateway 50 | servers: 51 | - port: 52 | number: 80 53 | name: http 54 | protocol: HTTP 55 | hosts: 56 | - "*" 57 | --- 58 | 59 | kind: VirtualService 60 | apiVersion: networking.istio.io/v1alpha3 61 | metadata: 62 | name: coffee-shop 63 | spec: 64 | hosts: 65 | - "*" 66 | gateways: 67 | - coffee-shop-gateway 68 | http: 69 | - route: 70 | - destination: 71 | host: coffee-shop 72 | port: 73 | number: 9080 74 | subset: v1 75 | --- 76 | 77 | kind: DestinationRule 78 | apiVersion: networking.istio.io/v1alpha3 79 | metadata: 80 | name: coffee-shop 81 | spec: 82 | host: coffee-shop 83 | subsets: 84 | - name: v1 85 | labels: 86 | version: v1 87 | --- 88 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | com.sebastian-daschner 5 | coffee-shop 6 | 1.0-SNAPSHOT 7 | war 8 | 9 | 10 | 11 | jakarta.platform 12 | jakarta.jakartaee-api 13 | 8.0.0 14 | provided 15 | 16 | 17 | org.eclipse.microprofile 18 | microprofile 19 | 3.0 20 | pom 21 | provided 22 | 23 | 24 | 25 | 26 | org.junit.jupiter 27 | junit-jupiter-api 28 | 5.4.2 29 | test 30 | 31 | 32 | org.junit.jupiter 33 | junit-jupiter-engine 34 | 5.4.2 35 | test 36 | 37 | 38 | org.junit.jupiter 39 | junit-jupiter-params 40 | 5.3.1 41 | test 42 | 43 | 44 | org.mockito 45 | mockito-core 46 | 2.27.0 47 | test 48 | 49 | 50 | org.assertj 51 | assertj-core 52 | 3.12.1 53 | test 54 | 55 | 56 | 57 | 58 | org.glassfish.jersey.core 59 | jersey-client 60 | 2.28 61 | test 62 | 63 | 64 | org.glassfish.jersey.inject 65 | jersey-hk2 66 | 2.28 67 | test 68 | 69 | 70 | org.glassfish.jersey.media 71 | jersey-media-json-jackson 72 | 2.28 73 | test 74 | 75 | 76 | org.glassfish.jersey.media 77 | jersey-media-json-processing 78 | 2.28 79 | test 80 | 81 | 82 | 83 | 84 | javax.xml.bind 85 | jaxb-api 86 | 2.3.1 87 | test 88 | 89 | 90 | javax.activation 91 | activation 92 | 1.1 93 | test 94 | 95 | 96 | 97 | 98 | coffee-shop 99 | 100 | 101 | io.openliberty.tools 102 | liberty-maven-plugin 103 | 3.2 104 | 105 | 106 | maven-war-plugin 107 | 3.2.2 108 | 109 | 110 | maven-surefire-plugin 111 | 2.22.2 112 | 113 | 114 | maven-failsafe-plugin 115 | 2.22.2 116 | 117 | 118 | 119 | 120 | 121 | 12 122 | 12 123 | UTF-8 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /presentation/slide001: -------------------------------------------------------------------------------- 1 | 2 | ___ _ _ _ 3 | | _ ) ___ __| |_ _ __ _ _ __ _ __| |_(_)__ ___ ___ 4 | | _ \/ -_|_-< _| | '_ \ '_/ _` / _| _| / _/ -_|_-< 5 | |___/\___/__/\__| | .__/_| \__,_\__|\__|_\__\___/__/ 6 | |_| 7 | __ _ 8 | / _|___ _ _ _ __ ___ __| |___ _ _ _ _ 9 | | _/ _ \ '_| | ' \/ _ \/ _` / -_) '_| ' \ 10 | |_| \___/_| |_|_|_\___/\__,_\___|_| |_||_| 11 | 12 | ___ _ _ _ 13 | | __|_ _| |_ ___ _ _ _ __ _ _(_)___ ___ _ | |__ ___ ____ _ 14 | | _|| ' \ _/ -_) '_| '_ \ '_| (_- { 8 | 9 | @Override 10 | public String adaptToJson(CoffeeType type) { 11 | return type.name(); 12 | } 13 | 14 | @Override 15 | public CoffeeType adaptFromJson(String type) { 16 | return CoffeeType.fromString(type); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/sebastian_daschner/coffee_shop/orders/boundary/CoffeeShop.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.boundary; 2 | 3 | import com.sebastian_daschner.coffee_shop.orders.control.Orders; 4 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeOrder; 5 | import com.sebastian_daschner.coffee_shop.price.control.PriceCalculator; 6 | import org.eclipse.microprofile.metrics.annotation.Counted; 7 | 8 | import javax.enterprise.context.ApplicationScoped; 9 | import javax.inject.Inject; 10 | import java.util.List; 11 | import java.util.UUID; 12 | 13 | @ApplicationScoped 14 | public class CoffeeShop { 15 | 16 | @Inject 17 | Orders orders; 18 | 19 | @Inject 20 | PriceCalculator priceCalculator; 21 | 22 | public List getOrders() { 23 | return orders.retrieveAll(); 24 | } 25 | 26 | public CoffeeOrder getOrder(UUID id) { 27 | return orders.retrieve(id); 28 | } 29 | 30 | @Counted(name = "coffees_total") 31 | public CoffeeOrder orderCoffee(CoffeeOrder order) { 32 | order.setId(UUID.randomUUID()); 33 | order.setPrice(priceCalculator.calculatePrice(order)); 34 | 35 | orders.store(order.getId(), order); 36 | 37 | return order; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/sebastian_daschner/coffee_shop/orders/boundary/OrdersResource.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.boundary; 2 | 3 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeOrder; 4 | 5 | import javax.inject.Inject; 6 | import javax.json.Json; 7 | import javax.json.JsonArray; 8 | import javax.json.JsonObject; 9 | import javax.json.stream.JsonCollectors; 10 | import javax.servlet.http.HttpServletRequest; 11 | import javax.validation.Valid; 12 | import javax.validation.constraints.NotNull; 13 | import javax.ws.rs.*; 14 | import javax.ws.rs.core.Context; 15 | import javax.ws.rs.core.MediaType; 16 | import javax.ws.rs.core.Response; 17 | import javax.ws.rs.core.UriInfo; 18 | import java.net.URI; 19 | import java.util.UUID; 20 | 21 | @Path("orders") 22 | @Produces(MediaType.APPLICATION_JSON) 23 | @Consumes(MediaType.APPLICATION_JSON) 24 | public class OrdersResource { 25 | 26 | @Inject 27 | CoffeeShop coffeeShop; 28 | 29 | @Context 30 | UriInfo uriInfo; 31 | 32 | @Context 33 | HttpServletRequest request; 34 | 35 | @GET 36 | public JsonArray getOrders() { 37 | return coffeeShop.getOrders().stream() 38 | .map(this::buildOrder) 39 | .collect(JsonCollectors.toJsonArray()); 40 | } 41 | 42 | private JsonObject buildOrder(CoffeeOrder order) { 43 | return Json.createObjectBuilder() 44 | .add("type", order.getType().name()) 45 | .add("status", order.getStatus().name()) 46 | .add("_self", buildUri(order).toString()) 47 | .build(); 48 | } 49 | 50 | @GET 51 | @Path("{id}") 52 | public CoffeeOrder getOrder(@PathParam("id") UUID id) { 53 | return coffeeShop.getOrder(id); 54 | } 55 | 56 | @POST 57 | public Response orderCoffee(@Valid @NotNull CoffeeOrder order) { 58 | final CoffeeOrder storedOrder = coffeeShop.orderCoffee(order); 59 | return Response.created(buildUri(storedOrder)).build(); 60 | } 61 | 62 | private URI buildUri(CoffeeOrder order) { 63 | return uriInfo.getBaseUriBuilder() 64 | .path(OrdersResource.class) 65 | .path(OrdersResource.class, "getOrder") 66 | .build(order.getId()); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/sebastian_daschner/coffee_shop/orders/control/Orders.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.control; 2 | 3 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeOrder; 4 | 5 | import javax.enterprise.context.ApplicationScoped; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.UUID; 9 | import java.util.concurrent.ConcurrentHashMap; 10 | import java.util.stream.Collectors; 11 | 12 | @ApplicationScoped 13 | public class Orders { 14 | 15 | private final ConcurrentHashMap orders = new ConcurrentHashMap<>(); 16 | 17 | public List retrieveAll() { 18 | return orders.entrySet().stream() 19 | .map(Map.Entry::getValue) 20 | .collect(Collectors.toList()); 21 | } 22 | 23 | public CoffeeOrder retrieve(UUID id) { 24 | return orders.get(id); 25 | } 26 | 27 | public void store(UUID id, CoffeeOrder order) { 28 | orders.put(id, order); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/sebastian_daschner/coffee_shop/orders/entity/CoffeeOrder.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.entity; 2 | 3 | import com.sebastian_daschner.coffee_shop.orders.CoffeeTypeDeserializer; 4 | 5 | import javax.json.bind.annotation.JsonbTransient; 6 | import javax.json.bind.annotation.JsonbTypeAdapter; 7 | import javax.validation.constraints.NotNull; 8 | import java.util.UUID; 9 | 10 | public class CoffeeOrder { 11 | 12 | @JsonbTransient 13 | private UUID id; 14 | 15 | @NotNull 16 | @JsonbTypeAdapter(CoffeeTypeDeserializer.class) 17 | private CoffeeType type; 18 | 19 | private double price; 20 | 21 | private OrderStatus status = OrderStatus.PREPARING; 22 | 23 | public UUID getId() { 24 | return id; 25 | } 26 | 27 | public void setId(UUID id) { 28 | this.id = id; 29 | } 30 | 31 | public CoffeeType getType() { 32 | return type; 33 | } 34 | 35 | public void setType(CoffeeType type) { 36 | this.type = type; 37 | } 38 | 39 | public double getPrice() { 40 | return price; 41 | } 42 | 43 | public void setPrice(double price) { 44 | this.price = price; 45 | } 46 | 47 | public OrderStatus getStatus() { 48 | return status; 49 | } 50 | 51 | public void setStatus(OrderStatus status) { 52 | this.status = status; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/sebastian_daschner/coffee_shop/orders/entity/CoffeeType.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.entity; 2 | 3 | import java.util.stream.Stream; 4 | 5 | public enum CoffeeType { 6 | 7 | ESPRESSO, 8 | LATTE, 9 | POUR_OVER; 10 | 11 | public static CoffeeType fromString(String string) { 12 | return Stream.of(CoffeeType.values()) 13 | .filter(t -> t.name().equalsIgnoreCase(string)) 14 | .findAny().orElse(null); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/sebastian_daschner/coffee_shop/orders/entity/OrderStatus.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.entity; 2 | 3 | public enum OrderStatus { 4 | 5 | PREPARING, 6 | FINISHED 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/sebastian_daschner/coffee_shop/price/control/PriceCalculator.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.price.control; 2 | 3 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeOrder; 4 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeType; 5 | import org.eclipse.microprofile.config.ConfigProvider; 6 | 7 | public class PriceCalculator { 8 | 9 | public double calculatePrice(CoffeeOrder order) { 10 | return getConfiguredPrice(order.getType()); 11 | } 12 | 13 | private double getConfiguredPrice(CoffeeType type) { 14 | String key = "coffee.prices." + type.name().toLowerCase(); 15 | return ConfigProvider.getConfig().getValue(key, double.class); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/liberty/config/server.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | jaxrs-2.1 4 | cdi-2.0 5 | jsonb-1.0 6 | mpHealth-2.0 7 | mpConfig-1.3 8 | mpMetrics-2.0 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/microprofile-config.properties: -------------------------------------------------------------------------------- 1 | coffee.prices.espresso=1.5 2 | coffee.prices.pour_over=4.0 3 | coffee.prices.latte=3.0 4 | 5 | version=1.2.3 6 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/beans.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /src/test/java/com/sebastian_daschner/coffee_shop/orders/CoffeeTypeDeserializerTest.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders; 2 | 3 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeType; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.params.ParameterizedTest; 6 | import org.junit.jupiter.params.provider.MethodSource; 7 | 8 | import static org.assertj.core.api.AssertionsForClassTypes.assertThat; 9 | 10 | class CoffeeTypeDeserializerTest { 11 | 12 | private CoffeeTypeDeserializer deserializer; 13 | 14 | @BeforeEach 15 | void setUp() { 16 | deserializer = new CoffeeTypeDeserializer(); 17 | } 18 | 19 | @ParameterizedTest 20 | @MethodSource("testData") 21 | void testDeserialize(String serializedType, CoffeeType expectedType) { 22 | assertThat(deserializer.adaptFromJson(serializedType)).isEqualTo(expectedType); 23 | } 24 | 25 | private static Object[][] testData() { 26 | return new Object[][]{ 27 | new Object[]{"Espresso", CoffeeType.ESPRESSO}, 28 | new Object[]{"ESPRESSO", CoffeeType.ESPRESSO}, 29 | new Object[]{"espresso", CoffeeType.ESPRESSO}, 30 | new Object[]{"Latte", CoffeeType.LATTE}, 31 | new Object[]{"LAtte", CoffeeType.LATTE}, 32 | new Object[]{"pour_over", CoffeeType.POUR_OVER} 33 | }; 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /src/test/java/com/sebastian_daschner/coffee_shop/orders/boundary/CoffeeShopIT.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.boundary; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | class CoffeeShopIT { 8 | 9 | private CoffeeShopSystem coffeeShopSystem = new CoffeeShopSystem(); 10 | 11 | @Test 12 | void testIsSystemRunning() { 13 | assertThat(coffeeShopSystem.isSystemUp()).isTrue(); 14 | } 15 | 16 | @Test 17 | void testVersion() { 18 | assertThat(coffeeShopSystem.getAppVersion()).isEqualTo("1.2.3"); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/test/java/com/sebastian_daschner/coffee_shop/orders/boundary/CoffeeShopSystem.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.boundary; 2 | 3 | import javax.json.JsonObject; 4 | import javax.ws.rs.client.Client; 5 | import javax.ws.rs.client.ClientBuilder; 6 | import javax.ws.rs.client.WebTarget; 7 | import javax.ws.rs.core.MediaType; 8 | import javax.ws.rs.core.UriBuilder; 9 | import java.net.URI; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | class CoffeeShopSystem { 13 | 14 | private final Client client; 15 | private final WebTarget healthTarget; 16 | 17 | public CoffeeShopSystem() { 18 | client = ClientBuilder.newBuilder() 19 | .connectTimeout(5, TimeUnit.SECONDS) 20 | .readTimeout(5, TimeUnit.SECONDS) 21 | .build(); 22 | healthTarget = client.target(buildHealthUri()); 23 | } 24 | 25 | private URI buildHealthUri() { 26 | String host = System.getProperty("coffee-shop.test.host", "localhost"); 27 | String port = System.getProperty("coffee-shop.test.port", "9080"); 28 | return UriBuilder.fromUri("http://{host}:{port}/health/").build(host, port); 29 | } 30 | 31 | public boolean isSystemUp() { 32 | String status = healthTarget.request(MediaType.APPLICATION_JSON_TYPE) 33 | .get(JsonObject.class) 34 | .getString("status"); 35 | return "UP".equalsIgnoreCase(status); 36 | } 37 | 38 | public String getAppVersion() { 39 | return healthTarget.request(MediaType.APPLICATION_JSON_TYPE) 40 | .get(JsonObject.class) 41 | .getJsonArray("checks") 42 | .getValuesAs(JsonObject.class) 43 | .stream() 44 | .filter(o -> o.getString("name").equalsIgnoreCase("coffee-shop")) 45 | .map(o -> o.getJsonObject("data").getString("version")) 46 | .findAny().orElse(null); 47 | } 48 | 49 | } 50 | --------------------------------------------------------------------------------