├── .sbtopts ├── front-end ├── app │ ├── views │ │ ├── index.scala.html │ │ ├── items │ │ │ ├── edit.scala.html │ │ │ ├── create.scala.html │ │ │ └── index.scala.html │ │ ├── customfields │ │ │ └── amountText.scala.html │ │ ├── orders │ │ │ ├── edit.scala.html │ │ │ ├── create.scala.html │ │ │ └── index.scala.html │ │ └── main.scala.html │ ├── models │ │ ├── Item.scala │ │ └── Order.scala │ ├── controllers │ │ ├── HomeController.scala │ │ ├── ItemController.scala │ │ └── OrderController.scala │ ├── Filters.scala │ ├── filters │ │ └── ExampleFilter.scala │ └── FrontendLoader.scala ├── bundle-configuration │ └── default │ │ └── runtime-config.sh ├── public │ ├── images │ │ └── favicon.png │ ├── javascripts │ │ └── hello.js │ └── stylesheets │ │ └── main.css ├── project │ ├── build.properties │ └── plugins.sbt ├── test │ ├── IntegrationSpec.scala │ └── ApplicationSpec.scala └── conf │ ├── routes │ ├── logback.xml │ └── application.conf ├── Lagom-in-Practice-JWorks-Yannick-De-Turck.pdf ├── project ├── build.properties └── plugins.sbt ├── .gitignore ├── item-impl └── src │ ├── main │ ├── java │ │ └── be │ │ │ └── yannickdeturck │ │ │ └── lagomshop │ │ │ └── item │ │ │ └── impl │ │ │ ├── ItemEventTag.java │ │ │ ├── ItemModule.java │ │ │ ├── AbstractItemState.java │ │ │ ├── ItemEvent.java │ │ │ ├── ItemCommand.java │ │ │ ├── ItemEntity.java │ │ │ ├── ItemEventProcessor.java │ │ │ └── ItemServiceImpl.java │ └── resources │ │ ├── application.conf │ │ └── logback.xml │ └── test │ └── java │ └── be │ └── yannickdeturck │ └── lagomshop │ └── item │ └── impl │ ├── ItemEntityTest.java │ └── ItemServiceTest.java ├── order-impl └── src │ ├── main │ ├── resources │ │ ├── application.conf │ │ └── logback.xml │ └── java │ │ └── be │ │ └── yannickdeturck │ │ └── lagomshop │ │ └── order │ │ └── impl │ │ ├── OrderEventTag.java │ │ ├── OrderModule.java │ │ ├── AbstractOrderState.java │ │ ├── OrderEvent.java │ │ ├── OrderCommand.java │ │ ├── OrderEntity.java │ │ ├── OrderEventProcessor.java │ │ └── OrderServiceImpl.java │ └── test │ └── java │ └── be │ └── yannickdeturck │ └── lagomshop │ └── order │ └── impl │ ├── OrderEntityTest.java │ └── OrderServiceTest.java ├── item-api └── src │ └── main │ └── java │ └── be │ └── yannickdeturck │ └── lagomshop │ └── item │ └── api │ ├── AbstractCreateItemResponse.java │ ├── AbstractItem.java │ ├── AbstractCreateItemRequest.java │ ├── ItemService.java │ └── ItemEvent.java ├── order-api └── src │ └── main │ └── java │ └── be │ └── yannickdeturck │ └── lagomshop │ └── order │ └── impl │ ├── AbstractCreateOrderResponse.java │ ├── AbstractOrder.java │ ├── AbstractCreateOrderRequest.java │ └── OrderService.java ├── LICENSE └── README.md /.sbtopts: -------------------------------------------------------------------------------- 1 | -J-Xms512M 2 | -J-Xmx1024M 3 | -J-Xss2M 4 | -J-XX:MaxMetaspaceSize=512M -------------------------------------------------------------------------------- /front-end/app/views/index.scala.html: -------------------------------------------------------------------------------- 1 | @() 2 | @main("Home", "index") { 3 |

Home

4 | } 5 | -------------------------------------------------------------------------------- /front-end/bundle-configuration/default/runtime-config.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | export APPLICATION_SECRET=replacewithactualsecret -------------------------------------------------------------------------------- /front-end/public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannickdeturck/lagom-shop/HEAD/front-end/public/images/favicon.png -------------------------------------------------------------------------------- /front-end/public/javascripts/hello.js: -------------------------------------------------------------------------------- 1 | if (window.console) { 2 | console.log("Welcome to your Play application's JavaScript!"); 3 | } 4 | -------------------------------------------------------------------------------- /Lagom-in-Practice-JWorks-Yannick-De-Turck.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannickdeturck/lagom-shop/HEAD/Lagom-in-Practice-JWorks-Yannick-De-Turck.pdf -------------------------------------------------------------------------------- /front-end/public/stylesheets/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | } 4 | 5 | .starter-template { 6 | padding: 40px 15px; 7 | text-align: center; 8 | } -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | #Activator-generated Properties 2 | #Tue May 10 23:00:22 CEST 2016 3 | template.uuid=b7ab3360-7861-48c2-93bb-497f200891e4 4 | sbt.version=0.13.13 5 | -------------------------------------------------------------------------------- /front-end/project/build.properties: -------------------------------------------------------------------------------- 1 | #Activator-generated Properties 2 | #Sun May 29 16:33:34 CEST 2016 3 | template.uuid=15c371e1-78e0-429a-84ff-11b1d09dd4a2 4 | sbt.version=0.13.13 5 | -------------------------------------------------------------------------------- /front-end/app/models/Item.scala: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | /** 4 | * @author Yannick De Turck 5 | */ 6 | case class Item(id: Option[String], name: String, price: BigDecimal) 7 | 8 | object Item { 9 | } 10 | -------------------------------------------------------------------------------- /front-end/app/models/Order.scala: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | /** 4 | * @author Yannick De Turck 5 | */ 6 | case class Order(id: Option[String], itemId: String, amount: Int, customer: String) 7 | 8 | object Order { 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | ideatarget 3 | .target 4 | bin 5 | /.idea 6 | /.idea_modules 7 | .cache 8 | .cache-main 9 | .cache-tests 10 | .classpath 11 | .project 12 | /RUNNING_PID 13 | .tmpBin 14 | .factorypath 15 | .settings 16 | logs 17 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2016 Lightbend Inc. 3 | // 4 | 5 | // The Lagom M2 plugin 6 | addSbtPlugin("com.lightbend.lagom" % "lagom-sbt-plugin" % "1.3.10") 7 | 8 | addSbtPlugin("com.lightbend.conductr" % "sbt-conductr" % "2.3.4") -------------------------------------------------------------------------------- /front-end/app/controllers/HomeController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import javax.inject._ 4 | 5 | import play.api.mvc._ 6 | 7 | /** 8 | * @author Yannick De Turck 9 | */ 10 | @Singleton 11 | class HomeController @Inject() extends Controller { 12 | 13 | def index = Action { 14 | Ok(views.html.index()) 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /item-impl/src/main/java/be/yannickdeturck/lagomshop/item/impl/ItemEventTag.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.item.impl; 2 | 3 | import com.lightbend.lagom.javadsl.persistence.AggregateEventTag; 4 | 5 | public class ItemEventTag { 6 | public static final AggregateEventTag INSTANCE = 7 | AggregateEventTag.of(ItemEvent.class); 8 | } 9 | -------------------------------------------------------------------------------- /item-impl/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | play.modules.enabled += be.yannickdeturck.lagomshop.item.impl.ItemModule 2 | 3 | // Check https://github.com/akka/akka-persistence-cassandra/blob/master/src/main/resources/reference.conf#L427 4 | cassandra-query-journal { 5 | refresh-interval = 3s 6 | eventual-consistency-delay = 3s 7 | delayed-event-timeout = 30s 8 | } -------------------------------------------------------------------------------- /order-impl/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | play.modules.enabled += be.yannickdeturck.lagomshop.order.impl.OrderModule 2 | 3 | // Check https://github.com/akka/akka-persistence-cassandra/blob/master/src/main/resources/reference.conf#L427 4 | cassandra-query-journal { 5 | refresh-interval = 3s 6 | eventual-consistency-delay = 3s 7 | delayed-event-timeout = 30s 8 | } -------------------------------------------------------------------------------- /order-impl/src/main/java/be/yannickdeturck/lagomshop/order/impl/OrderEventTag.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.order.impl; 2 | 3 | import com.lightbend.lagom.javadsl.persistence.AggregateEventTag; 4 | 5 | /** 6 | * @author Yannick De Turck 7 | */ 8 | public class OrderEventTag { 9 | public static final AggregateEventTag INSTANCE = 10 | AggregateEventTag.of(OrderEvent.class); 11 | } 12 | -------------------------------------------------------------------------------- /front-end/app/views/items/edit.scala.html: -------------------------------------------------------------------------------- 1 | @import b3.vertical.fieldConstructor 2 | @import customfields.amountText 3 | @import models.Item 4 | 5 | @(form: Form[Item])(implicit messages: Messages) 6 | @main("View Item", "viewItem") { 7 |

View item

8 | @b3.form(routes.ItemController.createItem()) { 9 | @b3.text(form("name"), '_label -> "Name") 10 | @amountText(form("price"), '_label -> "Price") 11 | Back 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /item-impl/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %date{ISO8601} %-5level %logger{10} - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /order-impl/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %date{ISO8601} %-5level %logger{10} - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /item-impl/src/main/java/be/yannickdeturck/lagomshop/item/impl/ItemModule.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.item.impl; 2 | 3 | import be.yannickdeturck.lagomshop.item.api.ItemService; 4 | import com.google.inject.AbstractModule; 5 | import com.lightbend.lagom.javadsl.server.ServiceGuiceSupport; 6 | 7 | public class ItemModule extends AbstractModule implements ServiceGuiceSupport { 8 | 9 | @Override 10 | protected void configure() { 11 | bindServices(serviceBinding( 12 | ItemService.class, ItemServiceImpl.class)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /front-end/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | // The Play plugin 2 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.3") 3 | 4 | // web plugins 5 | 6 | addSbtPlugin("com.typesafe.sbt" % "sbt-coffeescript" % "1.0.0") 7 | 8 | addSbtPlugin("com.typesafe.sbt" % "sbt-less" % "1.1.0") 9 | 10 | addSbtPlugin("com.typesafe.sbt" % "sbt-jshint" % "1.0.3") 11 | 12 | addSbtPlugin("com.typesafe.sbt" % "sbt-rjs" % "1.0.7") 13 | 14 | addSbtPlugin("com.typesafe.sbt" % "sbt-digest" % "1.1.0") 15 | 16 | addSbtPlugin("com.typesafe.sbt" % "sbt-mocha" % "1.1.0") 17 | 18 | addSbtPlugin("org.irundaia.sbt" % "sbt-sassify" % "1.4.2") 19 | -------------------------------------------------------------------------------- /front-end/app/views/customfields/amountText.scala.html: -------------------------------------------------------------------------------- 1 | @(field: Field, args: (Symbol, Any)*)(implicit handler: b3.B3FieldConstructor, messages: Messages) 2 | @b3.inputFormGroup(field, withFeedback = false, withLabelFor = true, bs.Args.withDefault(args, 'class -> "form-control")) { fieldInfo => 3 |
4 |
$
5 | 6 |
.00
7 |
8 | } -------------------------------------------------------------------------------- /front-end/app/views/items/create.scala.html: -------------------------------------------------------------------------------- 1 | @import b3.vertical.fieldConstructor 2 | @import customfields.amountText 3 | @import models.Item 4 | 5 | @(form: Form[Item])(implicit messages: Messages) 6 | @main("Create Item", "createItem") { 7 |

Create item

8 | @b3.form(routes.ItemController.createItem()) { 9 | @b3.text(form("name"), '_label -> "Name") 10 | @amountText(form("price"), '_label -> "Price") 11 | 12 | Cancel 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /front-end/app/views/orders/edit.scala.html: -------------------------------------------------------------------------------- 1 | @import b3.vertical.fieldConstructor 2 | @import models.Order 3 | 4 | @(form: Form[Order], items: List[Item])(implicit messages: Messages) 5 | @main("View Order", "viewOrder") { 6 |

View order

7 | @b3.form(routes.OrderController.createOrder()) { 8 | @b3.select(form("itemId"), options = items.map(i => (i.id.get, i.name)), '_label -> "Item") 9 | @b3.number(form("amount"), '_label -> "Amount") 10 | @b3.text(form("customer"), '_label -> "Customer") 11 | Back 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /item-api/src/main/java/be/yannickdeturck/lagomshop/item/api/AbstractCreateItemResponse.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.item.api; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import com.lightbend.lagom.javadsl.immutable.ImmutableStyle; 5 | import com.lightbend.lagom.serialization.Jsonable; 6 | import org.immutables.value.Value; 7 | 8 | import java.util.UUID; 9 | 10 | /** 11 | * @author Yannick De Turck 12 | */ 13 | @Value.Immutable 14 | @ImmutableStyle 15 | @JsonDeserialize 16 | public interface AbstractCreateItemResponse extends Jsonable { 17 | 18 | @Value.Parameter 19 | UUID getId(); 20 | } 21 | -------------------------------------------------------------------------------- /front-end/test/IntegrationSpec.scala: -------------------------------------------------------------------------------- 1 | import org.scalatestplus.play._ 2 | import play.api.test._ 3 | import play.api.test.Helpers._ 4 | 5 | /** 6 | * add your integration spec here. 7 | * An integration test will fire up a whole play application in a real (or headless) browser 8 | */ 9 | class IntegrationSpec extends PlaySpec with OneServerPerTest with OneBrowserPerTest with HtmlUnitFactory { 10 | 11 | // "Application" should { 12 | // 13 | // "work from within a browser" in { 14 | // 15 | // go to ("http://localhost:" + port) 16 | // 17 | // pageSource must include ("Your new application is ready.") 18 | // } 19 | // } 20 | } 21 | -------------------------------------------------------------------------------- /order-api/src/main/java/be/yannickdeturck/lagomshop/order/impl/AbstractCreateOrderResponse.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.order.impl; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import com.lightbend.lagom.javadsl.immutable.ImmutableStyle; 5 | import com.lightbend.lagom.serialization.Jsonable; 6 | import org.immutables.value.Value; 7 | 8 | import java.util.UUID; 9 | 10 | /** 11 | * @author Yannick De Turck 12 | */ 13 | @Value.Immutable 14 | @ImmutableStyle 15 | @JsonDeserialize 16 | public interface AbstractCreateOrderResponse extends Jsonable { 17 | 18 | @Value.Parameter 19 | UUID getId(); 20 | } 21 | -------------------------------------------------------------------------------- /order-impl/src/main/java/be/yannickdeturck/lagomshop/order/impl/OrderModule.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.order.impl; 2 | 3 | import be.yannickdeturck.lagomshop.item.api.ItemService; 4 | import com.google.inject.AbstractModule; 5 | import com.lightbend.lagom.javadsl.server.ServiceGuiceSupport; 6 | 7 | /** 8 | * @author Yannick De Turck 9 | */ 10 | public class OrderModule extends AbstractModule implements ServiceGuiceSupport { 11 | @Override 12 | protected void configure() { 13 | bindServices(serviceBinding( 14 | OrderService.class, OrderServiceImpl.class)); 15 | bindClient(ItemService.class); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under the Apache 2 license, quoted below. 2 | 3 | Copyright 2016 Lightbend Inc. [http://www.lightbend.com] 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); you may not 6 | use this file except in compliance with the License. You may obtain a copy of 7 | the License at 8 | 9 | [http://www.apache.org/licenses/LICENSE-2.0] 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | License for the specific language governing permissions and limitations under 15 | the License. 16 | -------------------------------------------------------------------------------- /front-end/app/views/orders/create.scala.html: -------------------------------------------------------------------------------- 1 | @import b3.vertical.fieldConstructor 2 | @import models.Order 3 | 4 | @(form: Form[Order], items: List[Item])(implicit messages: Messages) 5 | @main("Create Order", "createOrder") { 6 |

Create order

7 | @b3.form(routes.OrderController.createOrder()) { 8 | @b3.select(form("itemId"), options = items.map(i => (i.id.get, i.name)), '_label -> "Item") 9 | @b3.number(form("amount"), '_label -> "Amount") 10 | @b3.text(form("customer"), '_label -> "Customer") 11 | 12 | Cancel 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /item-impl/src/main/java/be/yannickdeturck/lagomshop/item/impl/AbstractItemState.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.item.impl; 2 | 3 | import be.yannickdeturck.lagomshop.item.api.Item; 4 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 5 | import com.lightbend.lagom.javadsl.immutable.ImmutableStyle; 6 | import com.lightbend.lagom.serialization.Jsonable; 7 | import org.immutables.value.Value; 8 | 9 | import java.time.LocalDateTime; 10 | import java.util.Optional; 11 | 12 | @Value.Immutable 13 | @ImmutableStyle 14 | @JsonDeserialize 15 | public interface AbstractItemState extends Jsonable { 16 | 17 | @Value.Parameter 18 | Optional getItem(); 19 | 20 | @Value.Parameter 21 | LocalDateTime getTimestamp(); 22 | } 23 | -------------------------------------------------------------------------------- /order-impl/src/main/java/be/yannickdeturck/lagomshop/order/impl/AbstractOrderState.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.order.impl; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import com.lightbend.lagom.javadsl.immutable.ImmutableStyle; 5 | import com.lightbend.lagom.serialization.Jsonable; 6 | import org.immutables.value.Value; 7 | 8 | import java.time.LocalDateTime; 9 | import java.util.Optional; 10 | 11 | /** 12 | * @author Yannick De Turck 13 | */ 14 | @Value.Immutable 15 | @ImmutableStyle 16 | @JsonDeserialize 17 | public interface AbstractOrderState extends Jsonable { 18 | 19 | @Value.Parameter 20 | Optional getOrder(); 21 | 22 | @Value.Parameter 23 | LocalDateTime getTimestamp(); 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lagom Shop 2 | This Lagom project contains two services 3 | * *Item service* that serves as an API for creating and looking up items 4 | * *Order service* that served as an API for creating and looking up orders for certain items 5 | 6 | The project also contains a frontend written in Play with multiple screens for working with items and orders. 7 | 8 | ## Setup 9 | Install sbt 10 | ``` 11 | brew install sbt 12 | ``` 13 | 14 | Navigate to the project and run `$ sbt` 15 | 16 | Start up the project by executing `$ runAll` 17 | 18 | ## Importing the project in an IDE 19 | Import the project as an sbt project in your IDE. 20 | 21 | This project uses the [Immutables](https://immutables.github.io) library, be sure to consult [Set up Immutables in your IDE](http://www.lagomframework.com/documentation/1.0.x/ImmutablesInIDEs.html). -------------------------------------------------------------------------------- /item-api/src/main/java/be/yannickdeturck/lagomshop/item/api/AbstractItem.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.item.api; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import com.google.common.base.Preconditions; 5 | import com.lightbend.lagom.javadsl.immutable.ImmutableStyle; 6 | import org.immutables.value.Value; 7 | import java.math.BigDecimal; 8 | import java.util.UUID; 9 | 10 | /** 11 | * @author Yannick De Turck 12 | */ 13 | @Value.Immutable 14 | @ImmutableStyle 15 | @JsonDeserialize 16 | public interface AbstractItem { 17 | 18 | @Value.Parameter 19 | UUID getId(); 20 | 21 | @Value.Parameter 22 | String getName(); 23 | 24 | @Value.Parameter 25 | BigDecimal getPrice(); 26 | 27 | @Value.Check 28 | default void check() { 29 | Preconditions.checkState(getPrice().signum() > 0, "Price must be a positive value"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /item-api/src/main/java/be/yannickdeturck/lagomshop/item/api/AbstractCreateItemRequest.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.item.api; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import com.google.common.base.Preconditions; 5 | import com.lightbend.lagom.javadsl.immutable.ImmutableStyle; 6 | import com.lightbend.lagom.serialization.Jsonable; 7 | import org.immutables.value.Value; 8 | 9 | import java.math.BigDecimal; 10 | 11 | /** 12 | * @author Yannick De Turck 13 | */ 14 | @Value.Immutable 15 | @ImmutableStyle 16 | @JsonDeserialize 17 | public interface AbstractCreateItemRequest extends Jsonable { 18 | 19 | @Value.Parameter 20 | String getName(); 21 | 22 | @Value.Parameter 23 | BigDecimal getPrice(); 24 | 25 | @Value.Check 26 | default void check() { 27 | Preconditions.checkState(getPrice().signum() > 0, "Price must be a positive value"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /order-api/src/main/java/be/yannickdeturck/lagomshop/order/impl/AbstractOrder.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.order.impl; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import com.google.common.base.Preconditions; 5 | import com.lightbend.lagom.javadsl.immutable.ImmutableStyle; 6 | import org.immutables.value.Value; 7 | 8 | import java.util.UUID; 9 | 10 | /** 11 | * @author Yannick De Turck 12 | */ 13 | @Value.Immutable 14 | @ImmutableStyle 15 | @JsonDeserialize 16 | public interface AbstractOrder { 17 | @Value.Parameter 18 | UUID getId(); 19 | 20 | @Value.Parameter 21 | UUID getItemId(); 22 | 23 | @Value.Parameter 24 | Integer getAmount(); 25 | 26 | @Value.Parameter 27 | String getCustomer(); 28 | 29 | @Value.Check 30 | default void check() { 31 | Preconditions.checkState(getAmount() > 0, "Amount must be a positive value"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /order-api/src/main/java/be/yannickdeturck/lagomshop/order/impl/AbstractCreateOrderRequest.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.order.impl; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import com.google.common.base.Preconditions; 5 | import com.lightbend.lagom.javadsl.immutable.ImmutableStyle; 6 | import com.lightbend.lagom.serialization.Jsonable; 7 | import org.immutables.value.Value; 8 | 9 | import java.util.UUID; 10 | 11 | /** 12 | * @author Yannick De Turck 13 | */ 14 | @Value.Immutable 15 | @ImmutableStyle 16 | @JsonDeserialize 17 | public interface AbstractCreateOrderRequest extends Jsonable { 18 | 19 | @Value.Parameter 20 | UUID getItemId(); 21 | 22 | @Value.Parameter 23 | Integer getAmount(); 24 | 25 | @Value.Parameter 26 | String getCustomer(); 27 | 28 | @Value.Check 29 | default void check() { 30 | Preconditions.checkState(getAmount() > 0, "Amount must be a positive value"); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /front-end/conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | GET / controllers.HomeController.index 6 | 7 | GET /items controllers.ItemController.index 8 | GET /items/create controllers.ItemController.newItem 9 | POST /items/create controllers.ItemController.createItem 10 | GET /items/:id controllers.ItemController.editItem(id: String) 11 | 12 | GET /orders controllers.OrderController.index 13 | GET /orders/create controllers.OrderController.newOrder 14 | POST /orders/create controllers.OrderController.createOrder 15 | GET /orders/:id controllers.OrderController.editOrder(id: String) 16 | 17 | # Map static resources from the /public folder to the /assets URL path 18 | GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) 19 | -------------------------------------------------------------------------------- /front-end/app/views/items/index.scala.html: -------------------------------------------------------------------------------- 1 | @import models.Item 2 | 3 | @(items: List[Item]) 4 | @main("Items", "items") { 5 |

Items

6 | 7 | Create item 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | @items.map { item => 18 | 19 | 24 | 25 | 26 | 27 | } 28 | 29 |
NamePrice
20 | 21 | 22 | 23 | @item.name$@item.price
30 | } 31 | -------------------------------------------------------------------------------- /item-impl/src/main/java/be/yannickdeturck/lagomshop/item/impl/ItemEvent.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.item.impl; 2 | 3 | import be.yannickdeturck.lagomshop.item.api.Item; 4 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 5 | import com.lightbend.lagom.javadsl.immutable.ImmutableStyle; 6 | import com.lightbend.lagom.javadsl.persistence.AggregateEvent; 7 | import com.lightbend.lagom.javadsl.persistence.AggregateEventTag; 8 | import com.lightbend.lagom.serialization.Jsonable; 9 | import org.immutables.value.Value; 10 | 11 | import java.time.Instant; 12 | 13 | public interface ItemEvent extends Jsonable, AggregateEvent { 14 | 15 | @Value.Immutable 16 | @ImmutableStyle 17 | @JsonDeserialize 18 | interface AbstractItemCreated extends ItemEvent { 19 | @Override 20 | default AggregateEventTag aggregateTag() { 21 | return ItemEventTag.INSTANCE; 22 | } 23 | 24 | @Value.Parameter 25 | Item getItem(); 26 | 27 | @Value.Default 28 | default Instant getTimestamp() { 29 | return Instant.now(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /order-impl/src/main/java/be/yannickdeturck/lagomshop/order/impl/OrderEvent.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.order.impl; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import com.lightbend.lagom.javadsl.immutable.ImmutableStyle; 5 | import com.lightbend.lagom.javadsl.persistence.AggregateEvent; 6 | import com.lightbend.lagom.javadsl.persistence.AggregateEventTag; 7 | import com.lightbend.lagom.serialization.Jsonable; 8 | import org.immutables.value.Value; 9 | 10 | import java.time.Instant; 11 | 12 | /** 13 | * @author Yannick De Turck 14 | */ 15 | public interface OrderEvent extends Jsonable, AggregateEvent { 16 | 17 | @Value.Immutable 18 | @ImmutableStyle 19 | @JsonDeserialize 20 | interface AbstractOrderCreated extends OrderEvent { 21 | @Override 22 | default AggregateEventTag aggregateTag() { 23 | return OrderEventTag.INSTANCE; 24 | } 25 | 26 | @Value.Parameter 27 | Order getOrder(); 28 | 29 | @Value.Default 30 | default Instant getTimestamp() { 31 | return Instant.now(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /front-end/app/views/orders/index.scala.html: -------------------------------------------------------------------------------- 1 | @import models.Order 2 | 3 | @(orders: List[Order]) 4 | @main("Orders", "orders") { 5 |

Orders

6 | 7 | Create order 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | @orders.map { order => 19 | 20 | 25 | 26 | 27 | 28 | 29 | } 30 | 31 |
ItemIDAmountCustomer
21 | 22 | 23 | 24 | @order.itemId@order.amount@order.customer
32 | } 33 | -------------------------------------------------------------------------------- /front-end/app/Filters.scala: -------------------------------------------------------------------------------- 1 | import javax.inject._ 2 | 3 | import filters.ExampleFilter 4 | import play.api._ 5 | import play.api.http.HttpFilters 6 | 7 | /** 8 | * This class configures filters that run on every request. This 9 | * class is queried by Play to get a list of filters. 10 | * 11 | * Play will automatically use filters from any class called 12 | * `Filters` that is placed the root package. You can load filters 13 | * from a different class by adding a `play.http.filters` setting to 14 | * the `application.conf` configuration file. 15 | * 16 | * @param env Basic environment settings for the current application. 17 | * @param exampleFilter A demonstration filter that adds a header to 18 | * each response. 19 | * 20 | * @author Yannick De Turck 21 | */ 22 | @Singleton 23 | class Filters @Inject()(env: Environment, 24 | exampleFilter: ExampleFilter) extends HttpFilters { 25 | 26 | override val filters = { 27 | // Use the example filter if we're running development mode. If 28 | // we're running in production or test mode then don't use any 29 | // filters at all. 30 | if (env.mode == Mode.Dev) Seq(exampleFilter) else Seq.empty 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /front-end/test/ApplicationSpec.scala: -------------------------------------------------------------------------------- 1 | import org.scalatestplus.play._ 2 | import play.api.test._ 3 | import play.api.test.Helpers._ 4 | 5 | /** 6 | * Add your spec here. 7 | * You can mock out a whole application including requests, plugins etc. 8 | * For more information, consult the wiki. 9 | */ 10 | class ApplicationSpec extends PlaySpec with OneAppPerTest { 11 | 12 | // "Routes" should { 13 | // 14 | // "send 404 on a bad request" in { 15 | // route(app, FakeRequest(GET, "/boum")).map(status(_)) mustBe Some(NOT_FOUND) 16 | // } 17 | // 18 | // } 19 | // 20 | // "HomeController" should { 21 | // 22 | // "render the index page" in { 23 | // val home = route(app, FakeRequest(GET, "/")).get 24 | // 25 | // status(home) mustBe OK 26 | // contentType(home) mustBe Some("text/html") 27 | // contentAsString(home) must include ("Your new application is ready.") 28 | // } 29 | // 30 | // } 31 | // 32 | // "CountController" should { 33 | // 34 | // "return an increasing count" in { 35 | // contentAsString(route(app, FakeRequest(GET, "/count")).get) mustBe "0" 36 | // contentAsString(route(app, FakeRequest(GET, "/count")).get) mustBe "1" 37 | // contentAsString(route(app, FakeRequest(GET, "/count")).get) mustBe "2" 38 | // } 39 | // 40 | // } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /front-end/app/filters/ExampleFilter.scala: -------------------------------------------------------------------------------- 1 | package filters 2 | 3 | import javax.inject._ 4 | 5 | import akka.stream.Materializer 6 | import play.api.mvc._ 7 | 8 | import scala.concurrent.{ExecutionContext, Future} 9 | 10 | /** 11 | * This is a simple filter that adds a header to all requests. It's 12 | * added to the application's list of filters by the 13 | * [[ExampleFilters]] class. 14 | * 15 | * @param mat This object is needed to handle streaming of requests 16 | * and responses. 17 | * @param exec This class is needed to execute code asynchronously. 18 | * It is used below by the `map` method. 19 | * 20 | * @author Yannick De Turck 21 | */ 22 | @Singleton 23 | class ExampleFilter @Inject()(implicit override val mat: Materializer, 24 | exec: ExecutionContext) extends Filter { 25 | 26 | override def apply(nextFilter: RequestHeader => Future[Result]) 27 | (requestHeader: RequestHeader): Future[Result] = { 28 | // Run the next filter in the chain. This will call other filters 29 | // and eventually call the action. Take the result and modify it 30 | // by adding a new header. 31 | nextFilter(requestHeader).map { result => 32 | result.withHeaders("X-ExampleFilter" -> "foo") 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /order-impl/src/main/java/be/yannickdeturck/lagomshop/order/impl/OrderCommand.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.order.impl; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import com.lightbend.lagom.javadsl.immutable.ImmutableStyle; 5 | import com.lightbend.lagom.javadsl.persistence.PersistentEntity; 6 | import com.lightbend.lagom.serialization.CompressedJsonable; 7 | import com.lightbend.lagom.serialization.Jsonable; 8 | import org.immutables.value.Value; 9 | 10 | import java.util.Optional; 11 | 12 | /** 13 | * @author Yannick De Turck 14 | */ 15 | public interface OrderCommand extends Jsonable { 16 | 17 | @Value.Immutable 18 | @ImmutableStyle 19 | @JsonDeserialize 20 | public interface AbstractCreateOrder extends OrderCommand, CompressedJsonable, PersistentEntity.ReplyType { 21 | 22 | @Value.Parameter 23 | CreateOrderRequest getCreateOrderRequest(); 24 | } 25 | 26 | @Value.Immutable(singleton = true) 27 | @ImmutableStyle 28 | @JsonDeserialize 29 | public interface AbstractGetOrder extends OrderCommand, CompressedJsonable, PersistentEntity.ReplyType { 30 | 31 | } 32 | 33 | @Value.Immutable 34 | @ImmutableStyle 35 | @JsonDeserialize 36 | public interface AbstractGetOrderReply extends Jsonable { 37 | 38 | @Value.Parameter 39 | Optional getOrder(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /item-impl/src/main/java/be/yannickdeturck/lagomshop/item/impl/ItemCommand.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.item.impl; 2 | 3 | import be.yannickdeturck.lagomshop.item.api.CreateItemRequest; 4 | import be.yannickdeturck.lagomshop.item.api.CreateItemResponse; 5 | import be.yannickdeturck.lagomshop.item.api.Item; 6 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 7 | import com.lightbend.lagom.javadsl.immutable.ImmutableStyle; 8 | import com.lightbend.lagom.javadsl.persistence.PersistentEntity; 9 | import com.lightbend.lagom.serialization.CompressedJsonable; 10 | import com.lightbend.lagom.serialization.Jsonable; 11 | import org.immutables.value.Value; 12 | 13 | import java.util.Optional; 14 | 15 | public interface ItemCommand extends Jsonable { 16 | 17 | @Value.Immutable 18 | @ImmutableStyle 19 | @JsonDeserialize 20 | public interface AbstractCreateItem extends ItemCommand, CompressedJsonable, PersistentEntity.ReplyType { 21 | 22 | @Value.Parameter 23 | CreateItemRequest getCreateItemRequest(); 24 | } 25 | 26 | @Value.Immutable(singleton = true) 27 | @ImmutableStyle 28 | @JsonDeserialize 29 | public interface AbstractGetItem extends ItemCommand, CompressedJsonable, PersistentEntity.ReplyType { 30 | 31 | } 32 | 33 | @Value.Immutable 34 | @ImmutableStyle 35 | @JsonDeserialize 36 | public interface AbstractGetItemReply extends Jsonable { 37 | 38 | @Value.Parameter 39 | Optional getItem(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /front-end/conf/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ${application.home:-.}/logs/application.log 8 | 9 | %date [%level] from %logger in %thread - %message%n%xException 10 | 11 | 12 | 13 | 14 | 15 | %coloredLevel %logger{15} - %message%n%xException{10} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /order-api/src/main/java/be/yannickdeturck/lagomshop/order/impl/OrderService.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.order.impl; 2 | 3 | import akka.NotUsed; 4 | import akka.stream.javadsl.Source; 5 | import com.lightbend.lagom.javadsl.api.Descriptor; 6 | import com.lightbend.lagom.javadsl.api.Service; 7 | import com.lightbend.lagom.javadsl.api.ServiceCall; 8 | import com.lightbend.lagom.javadsl.api.transport.Method; 9 | import org.pcollections.PSequence; 10 | 11 | /** 12 | * @author Yannick De Turck 13 | */ 14 | public interface OrderService extends Service { 15 | /** 16 | * Example: curl http://localhost:9000/api/orders/5e59ff61-214c-461f-9e29-89de0cf88f90 17 | */ 18 | ServiceCall getOrder(String id); 19 | 20 | /** 21 | * Example: curl http://localhost:9000/api/orders 22 | */ 23 | ServiceCall> getAllOrders(); 24 | 25 | /** 26 | * Example: 27 | * curl -v -H "Content-Type: application/json" -X POST -d 28 | * '{"name": "Chair", "price": 10.50}' http://localhost:9000/api/orders 29 | */ 30 | ServiceCall createOrder(); 31 | 32 | /** 33 | * 34 | */ 35 | ServiceCall> getLiveOrders(); 36 | 37 | @Override 38 | default Descriptor descriptor() { 39 | return Service.named("orderservice").withCalls( 40 | Service.pathCall("/api/orders/live", this::getLiveOrders), 41 | Service.restCall(Method.GET, "/api/orders/:id", this::getOrder), 42 | Service.restCall(Method.GET, "/api/orders", this::getAllOrders), 43 | Service.restCall(Method.POST, "/api/orders", this::createOrder) 44 | ).withAutoAcl(true); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /front-end/app/FrontendLoader.scala: -------------------------------------------------------------------------------- 1 | import javax.inject.Inject 2 | import com.lightbend.lagom.scaladsl.api.ServiceLocator.NoServiceLocator 3 | import com.lightbend.lagom.scaladsl.api.{ServiceAcl, ServiceInfo} 4 | import com.lightbend.lagom.scaladsl.client.LagomServiceClientComponents 5 | import com.lightbend.lagom.scaladsl.devmode.LagomDevModeComponents 6 | import com.softwaremill.macwire._ 7 | import controllers.{Assets, HomeController, ItemController, OrderController} 8 | import play.api.ApplicationLoader.Context 9 | import play.api.i18n.I18nComponents 10 | import play.api.libs.ws.ahc.AhcWSComponents 11 | import play.api._ 12 | import router.Routes 13 | 14 | import scala.collection.immutable 15 | import scala.concurrent.ExecutionContext 16 | 17 | abstract class Frontend @Inject()(context: Context) extends BuiltInComponentsFromContext(context) 18 | with I18nComponents 19 | with AhcWSComponents 20 | with LagomServiceClientComponents { 21 | 22 | override lazy val serviceInfo: ServiceInfo = ServiceInfo( 23 | "frontend", 24 | Map( 25 | "frontend" -> immutable.Seq(ServiceAcl.forPathRegex("(?!/api/).*")) 26 | ) 27 | ) 28 | override implicit lazy val executionContext: ExecutionContext = actorSystem.dispatcher 29 | override lazy val router = { 30 | val prefix = "/" 31 | wire[Routes] 32 | } 33 | 34 | lazy val homeController = wire[HomeController] 35 | lazy val itemController = wire[ItemController] 36 | lazy val orderController = wire[OrderController] 37 | lazy val assets = wire[Assets] 38 | } 39 | 40 | class FrontendLoader extends ApplicationLoader { 41 | override def load(context: Context): Application = context.environment.mode match { 42 | case Mode.Dev => 43 | new Frontend(context) with LagomDevModeComponents {}.application 44 | case _ => 45 | new Frontend(context) { 46 | override def serviceLocator = NoServiceLocator 47 | }.application 48 | } 49 | } -------------------------------------------------------------------------------- /item-api/src/main/java/be/yannickdeturck/lagomshop/item/api/ItemService.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.item.api; 2 | 3 | import akka.NotUsed; 4 | import com.lightbend.lagom.javadsl.api.Descriptor; 5 | import com.lightbend.lagom.javadsl.api.Service; 6 | import com.lightbend.lagom.javadsl.api.ServiceCall; 7 | import com.lightbend.lagom.javadsl.api.broker.Topic; 8 | import com.lightbend.lagom.javadsl.api.transport.Method; 9 | import org.pcollections.PSequence; 10 | 11 | /** 12 | * @author Yannick De Turck 13 | */ 14 | public interface ItemService extends Service { 15 | 16 | /** 17 | * Example: curl http://localhost:9000/api/items/5e59ff61-214c-461f-9e29-89de0cf88f90 18 | */ 19 | ServiceCall getItem(String id); 20 | 21 | /** 22 | * Example: curl http://localhost:9000/api/items 23 | */ 24 | ServiceCall> getAllItems(); 25 | 26 | /** 27 | * Example: 28 | * curl -v -H "Content-Type: application/json" -X POST -d 29 | * '{"name": "Chair", "price": 10.50}' http://localhost:9000/api/items 30 | */ 31 | ServiceCall createItem(); 32 | 33 | /** 34 | * Topic for created items. 35 | */ 36 | Topic createdItemsTopic(); 37 | 38 | /** 39 | * Other useful URLs: 40 | * 41 | * http://localhost:8000/services - Lists the available services 42 | * http://localhost:{SERVICE_PORT}/_status/circuit-breaker/current - Snapshot of current circuit breaker status 43 | * http://localhost:{SERVICE_PORT}/_status/circuit-breaker/stream - Stream of circuit breaker status 44 | */ 45 | 46 | @Override 47 | default Descriptor descriptor() { 48 | return Service.named("itemservice").withCalls( 49 | Service.restCall(Method.GET, "/api/items/:id", this::getItem), 50 | Service.restCall(Method.GET, "/api/items", this::getAllItems), 51 | Service.restCall(Method.POST, "/api/items", this::createItem) 52 | ).publishing( 53 | Service.topic("createdItems", this::createdItemsTopic) 54 | ).withAutoAcl(true); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /item-api/src/main/java/be/yannickdeturck/lagomshop/item/api/ItemEvent.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.item.api; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonSubTypes; 5 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 6 | import com.fasterxml.jackson.annotation.JsonTypeName; 7 | 8 | import java.math.BigDecimal; 9 | import java.util.UUID; 10 | 11 | /** 12 | * @author Yannick De Turck 13 | */ 14 | @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", defaultImpl = Void.class) 15 | @JsonSubTypes({ 16 | @JsonSubTypes.Type(ItemEvent.ItemCreated.class) 17 | }) 18 | public interface ItemEvent { 19 | UUID getId(); 20 | 21 | @JsonTypeName("item-created") 22 | final class ItemCreated implements ItemEvent { 23 | private final UUID id; 24 | private final String name; 25 | private final BigDecimal price; 26 | 27 | @JsonCreator 28 | public ItemCreated(UUID id, String name, BigDecimal price) { 29 | this.id = id; 30 | this.name = name; 31 | this.price = price; 32 | } 33 | 34 | @Override 35 | public UUID getId() { 36 | return id; 37 | } 38 | 39 | public String getName() { 40 | return name; 41 | } 42 | 43 | public BigDecimal getPrice() { 44 | return price; 45 | } 46 | 47 | @Override 48 | public boolean equals(Object o) { 49 | if (this == o) return true; 50 | if (o == null || getClass() != o.getClass()) return false; 51 | 52 | ItemCreated that = (ItemCreated) o; 53 | 54 | if (id != null ? !id.equals(that.id) : that.id != null) return false; 55 | if (name != null ? !name.equals(that.name) : that.name != null) return false; 56 | return price != null ? price.equals(that.price) : that.price == null; 57 | } 58 | 59 | @Override 60 | public int hashCode() { 61 | int result = id != null ? id.hashCode() : 0; 62 | result = 31 * result + (name != null ? name.hashCode() : 0); 63 | result = 31 * result + (price != null ? price.hashCode() : 0); 64 | return result; 65 | } 66 | 67 | @Override 68 | public String toString() { 69 | return "ItemCreated{" + 70 | "id=" + id + 71 | ", name='" + name + '\'' + 72 | ", price=" + price + 73 | '}'; 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /item-impl/src/main/java/be/yannickdeturck/lagomshop/item/impl/ItemEntity.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.item.impl; 2 | 3 | import be.yannickdeturck.lagomshop.item.api.CreateItemResponse; 4 | import be.yannickdeturck.lagomshop.item.api.Item; 5 | import com.lightbend.lagom.javadsl.persistence.PersistentEntity; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import java.time.LocalDateTime; 10 | import java.util.Optional; 11 | import java.util.UUID; 12 | 13 | public class ItemEntity extends PersistentEntity { 14 | 15 | private static final Logger logger = LoggerFactory.getLogger(ItemEntity.class); 16 | 17 | @Override 18 | public Behavior initialBehavior(Optional snapshotState) { 19 | logger.info("Setting up initialBehaviour with snapshotState = {}", snapshotState); 20 | BehaviorBuilder b = newBehaviorBuilder(snapshotState.orElse( 21 | ItemState.of(Optional.empty(), LocalDateTime.now())) 22 | ); 23 | 24 | // Register command handler 25 | b.setCommandHandler(CreateItem.class, (cmd, ctx) -> { 26 | if (state().getItem().isPresent()) { 27 | ctx.invalidCommand(String.format("Item %s is already created", entityId())); 28 | return ctx.done(); 29 | } else { 30 | Item item = Item.of(UUID.fromString(entityId()), cmd.getCreateItemRequest().getName(), 31 | cmd.getCreateItemRequest().getPrice()); 32 | final ItemCreated itemCreated = ItemCreated.builder().item(item).build(); 33 | logger.info("Processed CreateItem command into ItemCreated event {}", itemCreated); 34 | return ctx.thenPersist(itemCreated, evt -> 35 | ctx.reply(CreateItemResponse.of(itemCreated.getItem().getId()))); 36 | } 37 | }); 38 | 39 | // Register event handler 40 | b.setEventHandler(ItemCreated.class, evt -> { 41 | logger.info("Processed ItemCreated event, updated item state"); 42 | return state().withItem(evt.getItem()) 43 | .withTimestamp(LocalDateTime.now()); 44 | } 45 | ); 46 | 47 | // Register read-only handler eg a handler that doesn't result in events being created 48 | b.setReadOnlyCommandHandler(GetItem.class, 49 | (cmd, ctx) -> { 50 | logger.info("Processed GetItem command, returned item"); 51 | ctx.reply(GetItemReply.of(state().getItem())); 52 | } 53 | ); 54 | 55 | return b.build(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /order-impl/src/main/java/be/yannickdeturck/lagomshop/order/impl/OrderEntity.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.order.impl; 2 | 3 | import com.lightbend.lagom.javadsl.persistence.PersistentEntity; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import java.time.LocalDateTime; 8 | import java.util.Optional; 9 | import java.util.UUID; 10 | 11 | /** 12 | * @author Yannick De Turck 13 | */ 14 | public class OrderEntity extends PersistentEntity { 15 | 16 | public static final Logger logger = LoggerFactory.getLogger(OrderEntity.class); 17 | 18 | @Override 19 | public Behavior initialBehavior(Optional snapshotState) { 20 | logger.info("Setting up initialBehaviour with snapshotState = {}", snapshotState); 21 | BehaviorBuilder b = newBehaviorBuilder(snapshotState.orElse( 22 | OrderState.of(Optional.empty(), LocalDateTime.now())) 23 | ); 24 | 25 | // Register command handler 26 | b.setCommandHandler(CreateOrder.class, (cmd, ctx) -> { 27 | if (state().getOrder().isPresent()) { 28 | ctx.invalidCommand(String.format("Order %s is already created", entityId())); 29 | return ctx.done(); 30 | } else { 31 | CreateOrderRequest orderRequest = cmd.getCreateOrderRequest(); 32 | Order order = Order.of(UUID.fromString(entityId()), orderRequest.getItemId(), orderRequest.getAmount(), 33 | orderRequest.getCustomer()); 34 | final OrderCreated orderCreated = OrderCreated.builder().order(order).build(); 35 | logger.info("Processed CreateOrder command into OrderCreated event {}", orderCreated); 36 | return ctx.thenPersist(orderCreated, evt -> 37 | ctx.reply(CreateOrderResponse.of(orderCreated.getOrder().getId()))); 38 | } 39 | }); 40 | 41 | // Register event handler 42 | b.setEventHandler(OrderCreated.class, evt -> { 43 | logger.info("Processed OrderCreated event, updated order state"); 44 | return state().withOrder(evt.getOrder()) 45 | .withTimestamp(LocalDateTime.now()); 46 | } 47 | ); 48 | 49 | // Register read-only handler eg a handler that doesn't result in events being created 50 | b.setReadOnlyCommandHandler(GetOrder.class, 51 | (cmd, ctx) -> { 52 | logger.info("Processed GetOrder command, returned order"); 53 | ctx.reply(GetOrderReply.of(state().getOrder())); 54 | } 55 | ); 56 | 57 | return b.build(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /front-end/app/views/main.scala.html: -------------------------------------------------------------------------------- 1 | @(title: String, pageName: String)(content: Html) 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Lagom Shop - @title 10 | 11 | 12 | 13 | 14 | 15 | 16 | 39 |
40 |
41 | @content 42 |
43 |
44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /front-end/app/controllers/ItemController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import javax.inject._ 4 | 5 | import models.Item 6 | import play.api.Logger 7 | import play.api.data.Forms._ 8 | import play.api.data._ 9 | import play.api.i18n.{I18nSupport, MessagesApi} 10 | import play.api.libs.json.{JsError, JsSuccess, Json} 11 | import play.api.libs.ws.WSClient 12 | import play.api.mvc._ 13 | 14 | import scala.concurrent.duration._ 15 | import scala.concurrent.{ExecutionContext, Future} 16 | 17 | /** 18 | * @author Yannick De Turck 19 | */ 20 | @Singleton 21 | class ItemController @Inject()(val messagesApi: MessagesApi, val ws: WSClient)(implicit context: ExecutionContext) 22 | extends Controller with I18nSupport { 23 | 24 | val itemForm: Form[Item] = Form( 25 | mapping( 26 | "id" -> ignored(None: Option[String]), 27 | "name" -> nonEmptyText(maxLength = 28), 28 | "price" -> bigDecimal(8, 2).verifying("Price must be a positive value", price => price.signum > 0) 29 | )(Item.apply)(Item.unapply) 30 | ) 31 | 32 | def index = Action.async { implicit request => 33 | val getItems = ws.url("http://" + request.host + "/api/items") 34 | .withHeaders("Accept" -> "application/json") 35 | .withRequestTimeout(10000.millis) 36 | implicit val itemReads = Json.reads[Item] 37 | getItems.get().map { 38 | response => response.json.validate[List[Item]] match { 39 | case JsError(errors) => 40 | Logger.error("Error while trying to treat getItems response") 41 | InternalServerError(errors.toString()) 42 | case JsSuccess(items, _) => 43 | Ok(views.html.items.index(items)) 44 | } 45 | } 46 | } 47 | 48 | def newItem = Action { implicit request => 49 | Ok(views.html.items.create(itemForm)) 50 | } 51 | 52 | def createItem = Action.async { implicit request => 53 | itemForm.bindFromRequest.fold( 54 | errors => Future.successful(BadRequest(views.html.items.create(errors))), { 55 | item => 56 | val createItem = ws.url("http://" + request.host + "/api/items") 57 | .withHeaders("Accept" -> "application/json") 58 | .withRequestTimeout(10000.millis) 59 | implicit val itemReads = Json.format[Item] 60 | val response = createItem.post(Json.toJson(item)) 61 | response.map { 62 | r => 63 | val id = (r.json \ "id").as[String] 64 | Redirect(routes.ItemController.editItem(id)) 65 | } recover { 66 | case t: Throwable => 67 | Logger.error("Error while trying to treat createItem response") 68 | InternalServerError(t.getMessage) 69 | } 70 | } 71 | ) 72 | } 73 | 74 | def editItem(id: String) = Action.async { implicit request => 75 | val getItem = ws.url(s"http://${request.host}/api/items/$id") 76 | getItem.get().map { 77 | response => 78 | implicit val itemReads = Json.format[Item] 79 | response.json.validate[Item] match { 80 | case JsError(errors) => 81 | Redirect(routes.ItemController.index()) 82 | case JsSuccess(item, _) => 83 | Ok(views.html.items.edit(itemForm.fill(item))) 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /item-impl/src/main/java/be/yannickdeturck/lagomshop/item/impl/ItemEventProcessor.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.item.impl; 2 | 3 | import akka.Done; 4 | import com.datastax.driver.core.BoundStatement; 5 | import com.datastax.driver.core.PreparedStatement; 6 | import com.lightbend.lagom.javadsl.persistence.AggregateEventTag; 7 | import com.lightbend.lagom.javadsl.persistence.ReadSideProcessor; 8 | import com.lightbend.lagom.javadsl.persistence.cassandra.CassandraReadSide; 9 | import com.lightbend.lagom.javadsl.persistence.cassandra.CassandraSession; 10 | import org.pcollections.PSequence; 11 | import org.pcollections.TreePVector; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import javax.inject.Inject; 16 | import java.util.Arrays; 17 | import java.util.List; 18 | import java.util.concurrent.CompletionStage; 19 | 20 | /** 21 | * @author Yannick De Turck 22 | */ 23 | public class ItemEventProcessor extends ReadSideProcessor { 24 | 25 | private static final Logger logger = LoggerFactory.getLogger(ItemEventProcessor.class); 26 | 27 | private final CassandraSession session; 28 | private final CassandraReadSide readSide; 29 | 30 | private PreparedStatement writeItem = null; // initialized in prepare 31 | 32 | @Inject 33 | public ItemEventProcessor(CassandraSession session, CassandraReadSide readSide) { 34 | this.session = session; 35 | this.readSide = readSide; 36 | } 37 | 38 | private void setWriteItem(PreparedStatement writeItem) { 39 | this.writeItem = writeItem; 40 | } 41 | 42 | private CompletionStage prepareCreateTables(CassandraSession session) { 43 | logger.info("Creating Cassandra tables..."); 44 | return session.executeCreateTable( 45 | "CREATE TABLE IF NOT EXISTS item (" 46 | + "itemId uuid, name text, price decimal, PRIMARY KEY (itemId))"); 47 | } 48 | 49 | private CompletionStage prepareWriteItem(CassandraSession session) { 50 | logger.info("Inserting into read-side table item..."); 51 | return session.prepare("INSERT INTO item (itemId, name, price) VALUES (?, ?, ?)") 52 | .thenApply(ps -> { 53 | setWriteItem(ps); 54 | return Done.getInstance(); 55 | }); 56 | } 57 | 58 | /** 59 | * Write a persistent event into the read-side optimized database. 60 | */ 61 | private CompletionStage> processItemCreated(ItemCreated event) { 62 | BoundStatement bindWriteItem = writeItem.bind(); 63 | bindWriteItem.setUUID("itemId", event.getItem().getId()); 64 | bindWriteItem.setString("name", event.getItem().getName()); 65 | bindWriteItem.setDecimal("price", event.getItem().getPrice()); 66 | logger.info("Persisted Item {}", event.getItem()); 67 | return CassandraReadSide.completedStatements(Arrays.asList(bindWriteItem)); 68 | } 69 | 70 | @Override 71 | public ReadSideHandler buildHandler() { 72 | CassandraReadSide.ReadSideHandlerBuilder builder = readSide.builder("item_offset"); 73 | builder.setGlobalPrepare(() -> prepareCreateTables(session)); 74 | builder.setPrepare(tag -> prepareWriteItem(session)); 75 | logger.info("Setting up read-side event handlers..."); 76 | builder.setEventHandler(ItemCreated.class, this::processItemCreated); 77 | return builder.build(); 78 | } 79 | 80 | @Override 81 | public PSequence> aggregateTags() { 82 | return TreePVector.singleton(ItemEventTag.INSTANCE); 83 | } 84 | } -------------------------------------------------------------------------------- /order-impl/src/main/java/be/yannickdeturck/lagomshop/order/impl/OrderEventProcessor.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.order.impl; 2 | 3 | import akka.Done; 4 | import com.datastax.driver.core.BoundStatement; 5 | import com.datastax.driver.core.PreparedStatement; 6 | import com.lightbend.lagom.javadsl.persistence.AggregateEventTag; 7 | import com.lightbend.lagom.javadsl.persistence.ReadSideProcessor; 8 | import com.lightbend.lagom.javadsl.persistence.cassandra.CassandraReadSide; 9 | import com.lightbend.lagom.javadsl.persistence.cassandra.CassandraSession; 10 | import org.pcollections.PSequence; 11 | import org.pcollections.TreePVector; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import javax.inject.Inject; 16 | import java.util.Collections; 17 | import java.util.List; 18 | import java.util.concurrent.CompletionStage; 19 | 20 | /** 21 | * @author Yannick De Turck 22 | */ 23 | public class OrderEventProcessor extends ReadSideProcessor { 24 | 25 | private static final Logger logger = LoggerFactory.getLogger(OrderEventProcessor.class); 26 | 27 | private final CassandraSession session; 28 | private final CassandraReadSide readSide; 29 | 30 | private PreparedStatement writeOrder = null; // initialized in prepare 31 | 32 | @Inject 33 | public OrderEventProcessor(CassandraSession session, CassandraReadSide readSide) { 34 | this.session = session; 35 | this.readSide = readSide; 36 | } 37 | 38 | private void setWriteOrder(PreparedStatement writeOrder) { 39 | this.writeOrder = writeOrder; 40 | } 41 | 42 | private CompletionStage prepareCreateTables(CassandraSession session) { 43 | logger.info("Creating Cassandra tables..."); 44 | return session.executeCreateTable( 45 | "CREATE TABLE IF NOT EXISTS item_order (" 46 | + "orderId uuid, itemId uuid, amount int, customer text, PRIMARY KEY (orderId))"); 47 | } 48 | 49 | private CompletionStage prepareWriteOrder(CassandraSession session) { 50 | logger.info("Inserting into read-side table item_order..."); 51 | return session.prepare("INSERT INTO item_order (orderId, itemId, amount, customer) VALUES (?, ?, ?, ?)") 52 | .thenApply(ps -> { 53 | setWriteOrder(ps); 54 | return Done.getInstance(); 55 | }); 56 | } 57 | 58 | /** 59 | * Write a persistent event into the read-side optimized database. 60 | */ 61 | private CompletionStage> processOrderCreated(OrderCreated event) { 62 | BoundStatement bindWriteOrder = writeOrder.bind(); 63 | bindWriteOrder.setUUID("orderId", event.getOrder().getId()); 64 | bindWriteOrder.setUUID("itemId", event.getOrder().getItemId()); 65 | bindWriteOrder.setInt("amount", event.getOrder().getAmount()); 66 | bindWriteOrder.setString("customer", event.getOrder().getCustomer()); 67 | logger.info("Persisted Order {}", event.getOrder()); 68 | return CassandraReadSide.completedStatements(Collections.singletonList(bindWriteOrder)); 69 | } 70 | 71 | @Override 72 | public ReadSideHandler buildHandler() { 73 | CassandraReadSide.ReadSideHandlerBuilder builder = readSide.builder("item_order_offset"); 74 | builder.setGlobalPrepare(() -> prepareCreateTables(session)); 75 | builder.setPrepare(tag -> prepareWriteOrder(session)); 76 | logger.info("Setting up read-side event handlers..."); 77 | builder.setEventHandler(OrderCreated.class, this::processOrderCreated); 78 | return builder.build(); 79 | } 80 | 81 | @Override 82 | public PSequence> aggregateTags() { 83 | return TreePVector.singleton(OrderEventTag.INSTANCE); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /front-end/app/controllers/OrderController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import models.{Item, Order} 6 | import play.api.Logger 7 | import play.api.data.Form 8 | import play.api.data.Forms._ 9 | import play.api.i18n.{I18nSupport, MessagesApi} 10 | import play.api.libs.json.{JsError, JsSuccess, Json} 11 | import play.api.libs.ws.WSClient 12 | import play.api.mvc.{Action, AnyContent, Controller, Request} 13 | 14 | import scala.concurrent.duration._ 15 | import scala.concurrent.{Await, ExecutionContext, Future} 16 | 17 | /** 18 | * @author Yannick De Turck 19 | */ 20 | @Singleton 21 | class OrderController @Inject()(val messagesApi: MessagesApi, val ws: WSClient)(implicit context: ExecutionContext) 22 | extends Controller with I18nSupport { 23 | 24 | val orderForm: Form[Order] = Form( 25 | mapping( 26 | "id" -> ignored(None: Option[String]), 27 | "itemId" -> nonEmptyText(maxLength = 36, minLength = 36), 28 | "amount" -> number(0, 100), 29 | "customer" -> nonEmptyText(maxLength = 48) 30 | )(Order.apply)(Order.unapply) 31 | ) 32 | 33 | def index = Action.async { implicit request => 34 | val getOrders = ws.url("http://" + request.host + "/api/orders") 35 | .withHeaders("Accept" -> "application/json") 36 | .withRequestTimeout(10000.millis) 37 | implicit val orderReads = Json.reads[Order] 38 | getOrders.get().map { 39 | response => response.json.validate[List[Order]] match { 40 | case JsError(errors) => 41 | Logger.error("Error while trying to treat getOrders response") 42 | InternalServerError(errors.toString()) 43 | case JsSuccess(orders, _) => 44 | Ok(views.html.orders.index(orders)) 45 | } 46 | } 47 | } 48 | 49 | def newOrder = Action { implicit request => 50 | Ok(views.html.orders.create(orderForm, getItems(request))) 51 | } 52 | 53 | def createOrder = Action.async { implicit request => 54 | orderForm.bindFromRequest.fold( 55 | errors => Future.successful(BadRequest(views.html.orders.create(errors, getItems(request)))), { 56 | order => 57 | val createOrder = ws.url("http://" + request.host + "/api/orders") 58 | .withHeaders("Accept" -> "application/json") 59 | .withRequestTimeout(10000.millis) 60 | implicit val orderReads = Json.format[Order] 61 | val response = createOrder.post(Json.toJson(order)) 62 | response.map { 63 | r => 64 | val id = (r.json \ "id").as[String] 65 | Redirect(routes.OrderController.editOrder(id)) 66 | } recover { 67 | case t: Throwable => 68 | Logger.error("Error while trying to treat createOrder response") 69 | InternalServerError(t.getMessage) 70 | } 71 | } 72 | ) 73 | } 74 | 75 | def editOrder(id: String) = Action.async { implicit request => 76 | val getOrder = ws.url(s"http://${request.host}/api/orders/$id") 77 | getOrder.get().map { 78 | response => 79 | implicit val orderReads = Json.format[Order] 80 | response.json.validate[Order] match { 81 | case JsError(errors) => 82 | Redirect(routes.OrderController.index()) 83 | case JsSuccess(order, _) => 84 | Ok(views.html.orders.edit(orderForm.fill(order), getItems(request))) 85 | } 86 | } 87 | } 88 | 89 | // TODO move to a service 90 | def getItems(request: Request[AnyContent]): List[Item] = { 91 | val getItems = ws.url("http://" + request.host + "/api/items") 92 | .withHeaders("Accept" -> "application/json") 93 | .withRequestTimeout(10000.millis) 94 | implicit val itemReads = Json.reads[Item] 95 | val items = getItems.get().map { 96 | response => response.json.validate[List[Item]] match { 97 | case JsError(errors) => 98 | Logger.error("Error while trying to treat getItems response") 99 | List.empty[Item] 100 | case JsSuccess(items, _) => 101 | items 102 | } 103 | } 104 | Await.result(items, 10.seconds) 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /item-impl/src/main/java/be/yannickdeturck/lagomshop/item/impl/ItemServiceImpl.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.item.impl; 2 | 3 | import akka.NotUsed; 4 | import akka.japi.Pair; 5 | import be.yannickdeturck.lagomshop.item.api.CreateItemRequest; 6 | import be.yannickdeturck.lagomshop.item.api.CreateItemResponse; 7 | import be.yannickdeturck.lagomshop.item.api.Item; 8 | import be.yannickdeturck.lagomshop.item.api.ItemService; 9 | import com.lightbend.lagom.javadsl.api.ServiceCall; 10 | import com.lightbend.lagom.javadsl.api.broker.Topic; 11 | import com.lightbend.lagom.javadsl.api.transport.NotFound; 12 | import com.lightbend.lagom.javadsl.broker.TopicProducer; 13 | import com.lightbend.lagom.javadsl.persistence.Offset; 14 | import com.lightbend.lagom.javadsl.persistence.PersistentEntityRegistry; 15 | import com.lightbend.lagom.javadsl.persistence.ReadSide; 16 | import com.lightbend.lagom.javadsl.persistence.cassandra.CassandraSession; 17 | import org.pcollections.PSequence; 18 | import org.pcollections.TreePVector; 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | 22 | import javax.inject.Inject; 23 | import java.util.List; 24 | import java.util.UUID; 25 | import java.util.concurrent.CompletionStage; 26 | import java.util.stream.Collectors; 27 | 28 | public class ItemServiceImpl implements ItemService { 29 | 30 | private static final Logger logger = LoggerFactory.getLogger(ItemServiceImpl.class); 31 | 32 | private final PersistentEntityRegistry persistentEntities; 33 | private final CassandraSession db; 34 | 35 | @Inject 36 | public ItemServiceImpl(PersistentEntityRegistry persistentEntities, ReadSide readSide, 37 | CassandraSession db) { 38 | this.persistentEntities = persistentEntities; 39 | this.db = db; 40 | 41 | persistentEntities.register(ItemEntity.class); 42 | readSide.register(ItemEventProcessor.class); 43 | } 44 | 45 | @Override 46 | public ServiceCall getItem(String id) { 47 | return (req) -> { 48 | return persistentEntities.refFor(ItemEntity.class, id) 49 | .ask(GetItem.of()).thenApply(reply -> { 50 | logger.info(String.format("Looking up item %s", id)); 51 | if (reply.getItem().isPresent()) 52 | return reply.getItem().get(); 53 | else 54 | throw new NotFound(String.format("No item found for id %s", id)); 55 | }); 56 | }; 57 | } 58 | 59 | @Override 60 | public ServiceCall> getAllItems() { 61 | return (req) -> { 62 | logger.info("Looking up all items"); 63 | CompletionStage> result = db.selectAll("SELECT itemId, name, price FROM item") 64 | .thenApply(rows -> { 65 | List items = rows.stream().map(row -> Item.of(row.getUUID("itemId"), 66 | row.getString("name"), 67 | row.getDecimal("price"))).collect(Collectors.toList()); 68 | return TreePVector.from(items); 69 | }); 70 | return result; 71 | }; 72 | } 73 | 74 | @Override 75 | public ServiceCall createItem() { 76 | return request -> { 77 | logger.info("Creating item: {}", request); 78 | UUID uuid = UUID.randomUUID(); 79 | return persistentEntities.refFor(ItemEntity.class, uuid.toString()) 80 | .ask(CreateItem.of(request)); 81 | }; 82 | } 83 | 84 | @Override 85 | public Topic createdItemsTopic() { 86 | return TopicProducer.singleStreamWithOffset(offset -> { 87 | return persistentEntities 88 | .eventStream(ItemEventTag.INSTANCE, offset) 89 | .filter(eventOffSet -> eventOffSet.first() instanceof ItemCreated) 90 | .map(this::convertItem); 91 | }); 92 | } 93 | 94 | private Pair convertItem(Pair pair) { 95 | Item item = ((ItemCreated)pair.first()).getItem(); 96 | logger.info("Converting ItemEvent" + item); 97 | return new Pair<>(new be.yannickdeturck.lagomshop.item.api.ItemEvent.ItemCreated(item.getId(), item.getName(), 98 | item.getPrice()), pair.second()); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /order-impl/src/main/java/be/yannickdeturck/lagomshop/order/impl/OrderServiceImpl.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.order.impl; 2 | 3 | import akka.Done; 4 | import akka.NotUsed; 5 | import akka.stream.javadsl.Source; 6 | import akka.stream.javadsl.Flow; 7 | import be.yannickdeturck.lagomshop.item.api.Item; 8 | import be.yannickdeturck.lagomshop.item.api.ItemService; 9 | import com.lightbend.lagom.javadsl.api.ServiceCall; 10 | import com.lightbend.lagom.javadsl.api.deser.ExceptionMessage; 11 | import com.lightbend.lagom.javadsl.api.transport.NotFound; 12 | import com.lightbend.lagom.javadsl.api.transport.TransportErrorCode; 13 | import com.lightbend.lagom.javadsl.api.transport.TransportException; 14 | import com.lightbend.lagom.javadsl.persistence.PersistentEntityRegistry; 15 | import com.lightbend.lagom.javadsl.persistence.ReadSide; 16 | import com.lightbend.lagom.javadsl.persistence.cassandra.CassandraSession; 17 | import com.lightbend.lagom.javadsl.pubsub.PubSubRef; 18 | import com.lightbend.lagom.javadsl.pubsub.PubSubRegistry; 19 | import com.lightbend.lagom.javadsl.pubsub.TopicId; 20 | import org.pcollections.PSequence; 21 | import org.pcollections.TreePVector; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | 25 | import javax.inject.Inject; 26 | import java.util.List; 27 | import java.util.UUID; 28 | import java.util.concurrent.CompletableFuture; 29 | import java.util.concurrent.CompletionStage; 30 | import java.util.stream.Collectors; 31 | 32 | /** 33 | * @author Yannick De Turck 34 | */ 35 | public class OrderServiceImpl implements OrderService { 36 | 37 | private static final Logger logger = LoggerFactory.getLogger(OrderServiceImpl.class); 38 | 39 | private final PersistentEntityRegistry persistentEntities; 40 | private final CassandraSession db; 41 | private final ItemService itemService; 42 | private final PubSubRegistry pubSubRegistry; 43 | 44 | @Inject 45 | public OrderServiceImpl(PersistentEntityRegistry persistentEntities, ReadSide readSide, 46 | ItemService itemService, PubSubRegistry topics, CassandraSession db) { 47 | this.persistentEntities = persistentEntities; 48 | this.pubSubRegistry = topics; 49 | this.db = db; 50 | this.itemService = itemService; 51 | 52 | persistentEntities.register(OrderEntity.class); 53 | readSide.register(OrderEventProcessor.class); 54 | itemService.createdItemsTopic() 55 | .subscribe() 56 | .atLeastOnce(Flow.fromFunction((be.yannickdeturck.lagomshop.item.api.ItemEvent item) -> { 57 | logger.info("Subscriber: doing something with the created item " + item); 58 | return Done.getInstance(); 59 | })); 60 | } 61 | 62 | @Override 63 | public ServiceCall getOrder(String id) { 64 | return (req) -> { 65 | return persistentEntities.refFor(OrderEntity.class, id) 66 | .ask(GetOrder.of()).thenApply(reply -> { 67 | logger.info(String.format("Looking up order %s", id)); 68 | if (reply.getOrder().isPresent()) 69 | return reply.getOrder().get(); 70 | else 71 | throw new NotFound(String.format("No order found for id %s", id)); 72 | }); 73 | }; 74 | } 75 | 76 | @Override 77 | public ServiceCall> getAllOrders() { 78 | return (req) -> { 79 | logger.info("Looking up all orders"); 80 | CompletionStage> result = 81 | db.selectAll("SELECT orderId, itemId, amount, customer FROM item_order") 82 | .thenApply(rows -> { 83 | List items = rows.stream().map(row -> Order.of( 84 | row.getUUID("orderId"), 85 | row.getUUID("itemId"), 86 | row.getInt("amount"), 87 | row.getString("customer"))).collect(Collectors.toList()); 88 | return TreePVector.from(items); 89 | }); 90 | return result; 91 | }; 92 | } 93 | 94 | @Override 95 | public ServiceCall createOrder() { 96 | return request -> { 97 | PubSubRef topic = pubSubRegistry.refFor(TopicId.of(CreateOrderRequest.class, "topic")); 98 | topic.publish(request); 99 | CompletionStage response = 100 | itemService.getItem(request.getItemId().toString()).invoke(NotUsed.getInstance()); 101 | Item item = response.toCompletableFuture().join(); 102 | if (item == null) { 103 | // TODO custom BadRequest Exception? 104 | throw new TransportException(TransportErrorCode.ProtocolError, 105 | new ExceptionMessage("Bad Request", String.format("No item found for id %s", 106 | request.getItemId().toString()))); 107 | } 108 | logger.info("Creating order {}", request); 109 | UUID uuid = UUID.randomUUID(); 110 | return persistentEntities.refFor(OrderEntity.class, uuid.toString()) 111 | .ask(CreateOrder.of(request)); 112 | }; 113 | } 114 | 115 | public ServiceCall> getLiveOrders() { 116 | return request -> { 117 | final PubSubRef topic = 118 | pubSubRegistry.refFor(TopicId.of(CreateOrderRequest.class, "topic")); 119 | return CompletableFuture.completedFuture(topic.subscriber()); 120 | }; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /item-impl/src/test/java/be/yannickdeturck/lagomshop/item/impl/ItemEntityTest.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.item.impl; 2 | 3 | import akka.actor.ActorSystem; 4 | import akka.testkit.JavaTestKit; 5 | import be.yannickdeturck.lagomshop.item.api.CreateItemRequest; 6 | import be.yannickdeturck.lagomshop.item.api.CreateItemResponse; 7 | import be.yannickdeturck.lagomshop.item.api.Item; 8 | import com.lightbend.lagom.javadsl.persistence.PersistentEntity; 9 | import com.lightbend.lagom.javadsl.testkit.PersistentEntityTestDriver; 10 | import org.junit.AfterClass; 11 | import org.junit.Assert; 12 | import org.junit.BeforeClass; 13 | import org.junit.Test; 14 | 15 | import java.math.BigDecimal; 16 | import java.util.Collections; 17 | import java.util.Optional; 18 | import java.util.UUID; 19 | 20 | /** 21 | * @author Yannick De Turck 22 | */ 23 | public class ItemEntityTest { 24 | 25 | private static ActorSystem system; 26 | 27 | @BeforeClass 28 | public static void setup() { 29 | system = ActorSystem.create("ItemEntityTest"); 30 | } 31 | 32 | @AfterClass 33 | public static void teardown() { 34 | JavaTestKit.shutdownActorSystem(system); 35 | system = null; 36 | } 37 | 38 | @Test 39 | public void createItem_Should_CreateItemCreatedEvent() { 40 | // given 41 | UUID id = UUID.randomUUID(); 42 | PersistentEntityTestDriver driver = new PersistentEntityTestDriver<>( 43 | system, new ItemEntity(), id.toString()); 44 | 45 | // when 46 | PersistentEntityTestDriver.Outcome outcome = driver.run( 47 | CreateItem.of(CreateItemRequest.of("Chair", BigDecimal.valueOf(14.99)))); 48 | 49 | // then 50 | Assert.assertTrue(outcome.getReplies().get(0) instanceof CreateItemResponse); 51 | Assert.assertEquals(CreateItemResponse.of(id), outcome.getReplies().get(0)); 52 | ItemCreated itemCreated = ((ItemCreated) outcome.events().get(0)); 53 | Assert.assertEquals(id, itemCreated.getItem().getId()); 54 | Assert.assertEquals("Chair", itemCreated.getItem().getName()); 55 | Assert.assertEquals(BigDecimal.valueOf(14.99), itemCreated.getItem().getPrice()); 56 | Assert.assertNotNull(((ItemCreated) outcome.events().get(0)).getTimestamp()); 57 | Assert.assertEquals(Collections.emptyList(), driver.getAllIssues()); 58 | } 59 | 60 | @Test 61 | public void addExistingItem_Should_ThrowInvalidCommandException() { 62 | // given 63 | UUID id = UUID.randomUUID(); 64 | PersistentEntityTestDriver driver = new PersistentEntityTestDriver<>( 65 | system, new ItemEntity(), id.toString()); 66 | driver.run(CreateItem.of(CreateItemRequest.of("Chair", BigDecimal.valueOf(14.99)))); 67 | 68 | // when 69 | PersistentEntityTestDriver.Outcome outcome = driver.run( 70 | CreateItem.of(CreateItemRequest.of("Chair2", BigDecimal.valueOf(14.99)))); 71 | 72 | // then 73 | Assert.assertEquals(PersistentEntity.InvalidCommandException.class, outcome.getReplies().get(0).getClass()); 74 | Assert.assertEquals(Collections.emptyList(), outcome.events()); 75 | Assert.assertEquals(Collections.emptyList(), driver.getAllIssues()); 76 | } 77 | 78 | @Test 79 | public void createItemWithoutName_Should_ThrowNullPointerException() { 80 | // given 81 | UUID id = UUID.randomUUID(); 82 | PersistentEntityTestDriver driver = new PersistentEntityTestDriver<>( 83 | system, new ItemEntity(), id.toString()); 84 | 85 | // when 86 | try { 87 | driver.run(CreateItem.of(CreateItemRequest.of(null, BigDecimal.valueOf(14.99)))); 88 | Assert.fail(); 89 | } catch (NullPointerException e) { 90 | // then 91 | Assert.assertEquals("name", e.getMessage()); 92 | Assert.assertEquals(Collections.emptyList(), driver.getAllIssues()); 93 | } 94 | } 95 | 96 | @Test 97 | public void createItemWithoutPrice_Should_ThrowNullPointerException() { 98 | // given 99 | UUID id = UUID.randomUUID(); 100 | PersistentEntityTestDriver driver = new PersistentEntityTestDriver<>( 101 | system, new ItemEntity(), id.toString()); 102 | 103 | // when 104 | try { 105 | driver.run(CreateItem.of(CreateItemRequest.of("Chair", null))); 106 | Assert.fail(); 107 | } catch (NullPointerException e) { 108 | // then 109 | Assert.assertEquals("price", e.getMessage()); 110 | Assert.assertEquals(Collections.emptyList(), driver.getAllIssues()); 111 | } 112 | } 113 | 114 | @Test 115 | public void createItemWithNegativePrice_Should_ThrowSomeException() { 116 | // given 117 | UUID id = UUID.randomUUID(); 118 | PersistentEntityTestDriver driver = new PersistentEntityTestDriver<>( 119 | system, new ItemEntity(), id.toString()); 120 | 121 | // when 122 | try { 123 | driver.run(CreateItem.of(CreateItemRequest.of("Chair", BigDecimal.valueOf(-14.99)))); 124 | Assert.fail(); 125 | } catch (IllegalStateException e) { 126 | // then 127 | Assert.assertEquals("Price must be a positive value", e.getMessage()); 128 | Assert.assertEquals(Collections.emptyList(), driver.getAllIssues()); 129 | } 130 | } 131 | 132 | @Test 133 | public void getItem_Should_ReturnGetItemReply() { 134 | // given 135 | UUID id = UUID.randomUUID(); 136 | PersistentEntityTestDriver driver = new PersistentEntityTestDriver<>( 137 | system, new ItemEntity(), id.toString()); 138 | driver.run(CreateItem.of(CreateItemRequest.of("Chair", BigDecimal.valueOf(14.99)))); 139 | Item chair = Item.of(id, "Chair", BigDecimal.valueOf(14.99)); 140 | 141 | // when 142 | PersistentEntityTestDriver.Outcome outcome = driver.run(GetItem.of()); 143 | 144 | // then 145 | Assert.assertEquals(GetItemReply.of(Optional.of(chair)), outcome.getReplies().get(0)); 146 | Assert.assertEquals(Collections.emptyList(), outcome.events()); 147 | Assert.assertEquals(Collections.emptyList(), driver.getAllIssues()); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /item-impl/src/test/java/be/yannickdeturck/lagomshop/item/impl/ItemServiceTest.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.item.impl; 2 | 3 | import akka.NotUsed; 4 | import be.yannickdeturck.lagomshop.item.api.CreateItemRequest; 5 | import be.yannickdeturck.lagomshop.item.api.CreateItemResponse; 6 | import be.yannickdeturck.lagomshop.item.api.Item; 7 | import be.yannickdeturck.lagomshop.item.api.ItemService; 8 | import com.lightbend.lagom.javadsl.testkit.ServiceTest; 9 | import org.junit.AfterClass; 10 | import org.junit.Assert; 11 | import org.junit.BeforeClass; 12 | import org.junit.Test; 13 | import org.pcollections.PSequence; 14 | import scala.concurrent.duration.FiniteDuration; 15 | 16 | import java.math.BigDecimal; 17 | import java.util.UUID; 18 | 19 | import static java.util.concurrent.TimeUnit.MILLISECONDS; 20 | import static java.util.concurrent.TimeUnit.SECONDS; 21 | 22 | /** 23 | * @author Yannick De Turck 24 | */ 25 | public class ItemServiceTest { 26 | private static ServiceTest.TestServer server; 27 | 28 | @BeforeClass 29 | public static void setUp() { 30 | server = ServiceTest.startServer(ServiceTest.defaultSetup() 31 | .withCassandra(true)); 32 | } 33 | 34 | @AfterClass 35 | public static void tearDown() { 36 | if (server != null) { 37 | server.stop(); 38 | server = null; 39 | } 40 | } 41 | 42 | @Test 43 | public void createItem_Should_GenerateId() throws Exception { 44 | // given 45 | ItemService service = server.client(ItemService.class); 46 | CreateItemRequest createItemRequest = CreateItemRequest.builder() 47 | .name("Chair") 48 | .price(new BigDecimal(10.00)) 49 | .build(); 50 | 51 | // when 52 | CreateItemResponse response = service.createItem().invoke(createItemRequest) 53 | .toCompletableFuture().get(3, SECONDS); 54 | 55 | // then 56 | Assert.assertNotNull(response.getId()); 57 | } 58 | 59 | @Test 60 | public void getItem_Should_ReturnCreatedItem() throws Exception { 61 | // given 62 | ItemService service = server.client(ItemService.class); 63 | CreateItemRequest createItemRequest = CreateItemRequest.builder() 64 | .name("Chair") 65 | .price(new BigDecimal(10.00)) 66 | .build(); 67 | CreateItemResponse createItemResponse = service.createItem().invoke(createItemRequest).toCompletableFuture().get(3, SECONDS); 68 | 69 | // when 70 | // TODO disable circuit breaker to reduce time needed? 71 | // TODO should a 'get' service actually have a circuit breaker? 72 | ServiceTest.eventually(FiniteDuration.create(10, SECONDS), FiniteDuration.create(1000, MILLISECONDS), () -> { 73 | Item response = service.getItem(createItemResponse.getId().toString()) 74 | .invoke(NotUsed.getInstance()).toCompletableFuture().get(3, SECONDS); 75 | 76 | // then 77 | Assert.assertEquals(createItemResponse.getId(), response.getId()); 78 | Assert.assertEquals("Chair", response.getName()); 79 | Assert.assertEquals(new BigDecimal(10.00), response.getPrice()); 80 | }); 81 | } 82 | 83 | @Test 84 | public void getItem_Should_ReturnErrorForNonExistingItem() throws Exception { 85 | // given 86 | ItemService service = server.client(ItemService.class); 87 | 88 | // when 89 | UUID randomId = UUID.randomUUID(); 90 | try { 91 | service.getItem(randomId.toString()) 92 | .invoke(NotUsed.getInstance()).toCompletableFuture().get(3, SECONDS); 93 | Assert.fail("getItem should've returned an error"); 94 | } catch (Exception e) { 95 | // then 96 | Assert.assertEquals(String.format("com.lightbend.lagom.javadsl.api.transport.NotFound: " + 97 | "No item found for id %s " + 98 | "(TransportErrorCode{http=404, webSocket=1008, description='Policy Violation'})", 99 | randomId.toString()), e.getMessage()); 100 | } 101 | } 102 | 103 | @Test 104 | public void getAllItems_Should_ReturnCreatedItems() throws Exception { 105 | // given 106 | ItemService service = server.client(ItemService.class); 107 | CreateItemRequest createItemRequest = CreateItemRequest.builder() 108 | .name("Chair") 109 | .price(new BigDecimal(10.00)) 110 | .build(); 111 | CreateItemResponse createItemResponse = service.createItem().invoke(createItemRequest).toCompletableFuture().get(3, SECONDS); 112 | CreateItemRequest createItemRequest2 = CreateItemRequest.builder() 113 | .name("Table") 114 | .price(new BigDecimal(24.99)) 115 | .build(); 116 | CreateItemResponse createItemResponse2 = service.createItem().invoke(createItemRequest2).toCompletableFuture().get(3, SECONDS); 117 | 118 | // when 119 | ServiceTest.eventually(FiniteDuration.create(10, SECONDS), FiniteDuration.create(1000, MILLISECONDS), () -> { 120 | PSequence response = service.getAllItems() 121 | .invoke(NotUsed.getInstance()).toCompletableFuture().get(3, SECONDS); 122 | 123 | // then 124 | // TODO find a way to truncate Cassandra tables and check on size 125 | // Assert.assertEquals(2, response.size()); 126 | Assert.assertTrue(String.format("Doesn't contain item %s", createItemResponse.getId()), 127 | response.stream().anyMatch(i -> createItemResponse.getId().equals(i.getId()))); 128 | Assert.assertTrue(String.format("Doesn't contain item %s", createItemResponse2.getId()), 129 | response.stream().anyMatch(i -> createItemResponse2.getId().equals(i.getId()))); 130 | response.stream() 131 | .filter(i -> i.getId().equals(createItemResponse.getId())) 132 | .forEach(i -> { 133 | Assert.assertEquals(createItemResponse.getId(), i.getId()); 134 | Assert.assertEquals("Chair", i.getName()); 135 | Assert.assertEquals(new BigDecimal(10.00), i.getPrice()); 136 | } 137 | ); 138 | response.stream() 139 | .filter(i -> i.getId().equals(createItemResponse2.getId())) 140 | .forEach(i -> { 141 | Assert.assertEquals(createItemResponse2.getId(), i.getId()); 142 | Assert.assertEquals("Table", i.getName()); 143 | Assert.assertEquals(new BigDecimal(24.99), i.getPrice()); 144 | } 145 | ); 146 | 147 | 148 | }); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /order-impl/src/test/java/be/yannickdeturck/lagomshop/order/impl/OrderEntityTest.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.order.impl; 2 | 3 | import akka.actor.ActorSystem; 4 | import akka.testkit.JavaTestKit; 5 | import com.lightbend.lagom.javadsl.persistence.PersistentEntity; 6 | import com.lightbend.lagom.javadsl.testkit.PersistentEntityTestDriver; 7 | import org.junit.AfterClass; 8 | import org.junit.Assert; 9 | import org.junit.BeforeClass; 10 | import org.junit.Test; 11 | 12 | import java.util.Collections; 13 | import java.util.Optional; 14 | import java.util.UUID; 15 | 16 | /** 17 | * @author Yannick De Turck 18 | */ 19 | public class OrderEntityTest { 20 | private static ActorSystem system; 21 | 22 | @BeforeClass 23 | public static void setup() { 24 | system = ActorSystem.create("OrderEntityTest"); 25 | } 26 | 27 | @AfterClass 28 | public static void teardown() { 29 | JavaTestKit.shutdownActorSystem(system); 30 | system = null; 31 | } 32 | 33 | @Test 34 | public void createOrder_Should_CreateOrderCreatedEvent() { 35 | // given 36 | UUID id = UUID.randomUUID(); 37 | UUID itemId = UUID.randomUUID(); 38 | PersistentEntityTestDriver driver = new PersistentEntityTestDriver<>( 39 | system, new OrderEntity(), id.toString()); 40 | 41 | // when 42 | PersistentEntityTestDriver.Outcome outcome = driver.run( 43 | CreateOrder.of(CreateOrderRequest.of(itemId, 2, "Yannick"))); 44 | 45 | // then 46 | Assert.assertTrue(outcome.getReplies().get(0) instanceof CreateOrderResponse); 47 | Assert.assertEquals(CreateOrderResponse.of(id), outcome.getReplies().get(0)); 48 | OrderCreated orderCreated = ((OrderCreated) outcome.events().get(0)); 49 | Assert.assertEquals(id, orderCreated.getOrder().getId()); 50 | Assert.assertEquals(itemId, orderCreated.getOrder().getItemId()); 51 | Assert.assertEquals(2, orderCreated.getOrder().getAmount().intValue()); 52 | Assert.assertEquals("Yannick", orderCreated.getOrder().getCustomer()); 53 | Assert.assertNotNull(((OrderCreated) outcome.events().get(0)).getTimestamp()); 54 | Assert.assertEquals(Collections.emptyList(), driver.getAllIssues()); 55 | } 56 | 57 | @Test 58 | public void createExistingOrder_Should_ThrowInvalidCommandException() { 59 | // given 60 | UUID id = UUID.randomUUID(); 61 | UUID itemId = UUID.randomUUID(); 62 | PersistentEntityTestDriver driver = new PersistentEntityTestDriver<>( 63 | system, new OrderEntity(), id.toString()); 64 | driver.run(CreateOrder.of(CreateOrderRequest.of(itemId, 2, "Yannick"))); 65 | 66 | // when 67 | PersistentEntityTestDriver.Outcome outcome = driver.run( 68 | CreateOrder.of(CreateOrderRequest.of(itemId, 2, "Yannick"))); 69 | 70 | // then 71 | Assert.assertEquals(PersistentEntity.InvalidCommandException.class, outcome.getReplies().get(0).getClass()); 72 | Assert.assertEquals(Collections.emptyList(), outcome.events()); 73 | Assert.assertEquals(Collections.emptyList(), driver.getAllIssues()); 74 | } 75 | 76 | @Test 77 | public void createOrderWithoutItemId_Should_ThrowNullPointerException() { 78 | // given 79 | UUID id = UUID.randomUUID(); 80 | PersistentEntityTestDriver driver = new PersistentEntityTestDriver<>( 81 | system, new OrderEntity(), id.toString()); 82 | 83 | // when 84 | try { 85 | driver.run(CreateOrder.of(CreateOrderRequest.of(null, 2, "Yannick"))); 86 | Assert.fail(); 87 | } catch (NullPointerException e) { 88 | // then 89 | Assert.assertEquals("itemId", e.getMessage()); 90 | Assert.assertEquals(Collections.emptyList(), driver.getAllIssues()); 91 | } 92 | } 93 | 94 | @Test 95 | public void createOrderWithoutAmount_Should_ThrowNullPointerException() { 96 | // given 97 | UUID id = UUID.randomUUID(); 98 | UUID itemId = UUID.randomUUID(); 99 | PersistentEntityTestDriver driver = new PersistentEntityTestDriver<>( 100 | system, new OrderEntity(), id.toString()); 101 | 102 | // when 103 | try { 104 | driver.run(CreateOrder.of(CreateOrderRequest.of(itemId, null, "Yannick"))); 105 | Assert.fail(); 106 | } catch (NullPointerException e) { 107 | // then 108 | Assert.assertEquals("amount", e.getMessage()); 109 | Assert.assertEquals(Collections.emptyList(), driver.getAllIssues()); 110 | } 111 | } 112 | 113 | @Test 114 | public void createOrderWithoutCustomer_Should_ThrowNullPointerException() { 115 | // given 116 | UUID id = UUID.randomUUID(); 117 | UUID itemId = UUID.randomUUID(); 118 | PersistentEntityTestDriver driver = new PersistentEntityTestDriver<>( 119 | system, new OrderEntity(), id.toString()); 120 | 121 | // when 122 | try { 123 | driver.run(CreateOrder.of(CreateOrderRequest.of(itemId, 2, null))); 124 | Assert.fail(); 125 | } catch (NullPointerException e) { 126 | // then 127 | Assert.assertEquals("customer", e.getMessage()); 128 | Assert.assertEquals(Collections.emptyList(), driver.getAllIssues()); 129 | } 130 | } 131 | 132 | @Test 133 | public void createOrderWithNegativeAmount_Should_ThrowSomeException() { 134 | // given 135 | UUID id = UUID.randomUUID(); 136 | UUID itemId = UUID.randomUUID(); 137 | PersistentEntityTestDriver driver = new PersistentEntityTestDriver<>( 138 | system, new OrderEntity(), id.toString()); 139 | 140 | // when 141 | try { 142 | driver.run(CreateOrder.of(CreateOrderRequest.of(itemId, -2, "Yannick"))); 143 | Assert.fail(); 144 | } catch (IllegalStateException e) { 145 | // then 146 | Assert.assertEquals("Amount must be a positive value", e.getMessage()); 147 | Assert.assertEquals(Collections.emptyList(), driver.getAllIssues()); 148 | } 149 | } 150 | 151 | @Test 152 | public void getOrder_Should_ReturnGetOrderReply() { 153 | // given 154 | UUID id = UUID.randomUUID(); 155 | UUID itemId = UUID.randomUUID(); 156 | PersistentEntityTestDriver driver = new PersistentEntityTestDriver<>( 157 | system, new OrderEntity(), id.toString()); 158 | driver.run(CreateOrder.of(CreateOrderRequest.of(itemId, 2, "Yannick"))); 159 | Order order = Order.of(id, itemId, 2, "Yannick"); 160 | 161 | // when 162 | PersistentEntityTestDriver.Outcome outcome = driver.run(GetOrder.of()); 163 | 164 | // then 165 | Assert.assertEquals(GetOrderReply.of(Optional.of(order)), outcome.getReplies().get(0)); 166 | Assert.assertEquals(Collections.emptyList(), outcome.events()); 167 | Assert.assertEquals(Collections.emptyList(), driver.getAllIssues()); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /order-impl/src/test/java/be/yannickdeturck/lagomshop/order/impl/OrderServiceTest.java: -------------------------------------------------------------------------------- 1 | package be.yannickdeturck.lagomshop.order.impl; 2 | 3 | import akka.Done; 4 | import akka.NotUsed; 5 | import akka.stream.javadsl.Flow; 6 | import akka.stream.javadsl.Source; 7 | import be.yannickdeturck.lagomshop.item.api.Item; 8 | import be.yannickdeturck.lagomshop.item.api.ItemEvent; 9 | import be.yannickdeturck.lagomshop.item.api.ItemService; 10 | import com.lightbend.lagom.javadsl.api.broker.Subscriber; 11 | import com.lightbend.lagom.javadsl.api.broker.Topic; 12 | import com.lightbend.lagom.javadsl.testkit.ServiceTest; 13 | import org.junit.AfterClass; 14 | import org.junit.Assert; 15 | import org.junit.BeforeClass; 16 | import org.junit.Test; 17 | import org.mockito.Mockito; 18 | import org.pcollections.PSequence; 19 | import play.inject.Bindings; 20 | import scala.concurrent.duration.FiniteDuration; 21 | 22 | import java.math.BigDecimal; 23 | import java.util.UUID; 24 | import java.util.concurrent.CompletableFuture; 25 | import java.util.concurrent.CompletionStage; 26 | 27 | import static java.util.concurrent.TimeUnit.MILLISECONDS; 28 | import static java.util.concurrent.TimeUnit.SECONDS; 29 | 30 | /** 31 | * @author Yannick De Turck 32 | */ 33 | public class OrderServiceTest { 34 | private static ServiceTest.TestServer server; 35 | 36 | private static ItemService itemService = Mockito.mock(ItemService.class); 37 | 38 | @BeforeClass 39 | public static void setUp() { 40 | prepareTopic(); 41 | server = ServiceTest.startServer(ServiceTest.defaultSetup() 42 | .withCassandra(true) 43 | .withConfigureBuilder(b -> b.overrides( 44 | Bindings.bind(ItemService.class).toInstance(itemService)))); 45 | } 46 | 47 | // TODO is there a more elegant way to mock this? 48 | public static void prepareTopic(){ 49 | Mockito.when(itemService.createdItemsTopic()) 50 | .thenReturn(new Topic() { 51 | @Override 52 | public TopicId topicId() { 53 | return null; 54 | } 55 | 56 | @Override 57 | public Subscriber subscribe() { 58 | return new Subscriber() { 59 | @Override 60 | public Subscriber withGroupId(String groupId) throws IllegalArgumentException { 61 | return null; 62 | } 63 | 64 | @Override 65 | public Source atMostOnceSource() { 66 | return null; 67 | } 68 | 69 | @Override 70 | public CompletionStage atLeastOnce(Flow flow) { 71 | return null; 72 | } 73 | }; 74 | } 75 | }); 76 | } 77 | 78 | @AfterClass 79 | public static void tearDown() { 80 | if (server != null) { 81 | server.stop(); 82 | server = null; 83 | } 84 | } 85 | 86 | @Test 87 | public void createOrder_Should_GenerateId() throws Exception { 88 | // given 89 | UUID itemId = UUID.randomUUID(); 90 | Mockito.when(itemService.getItem(itemId.toString())) 91 | .thenReturn(req -> CompletableFuture.completedFuture( 92 | Item.of(itemId, "Chair", BigDecimal.valueOf(14.99)))); 93 | OrderService service = server.client(OrderService.class); 94 | CreateOrderRequest createOrderRequest = CreateOrderRequest.builder() 95 | .itemId(itemId) 96 | .amount(2) 97 | .customer("Yannick") 98 | .build(); 99 | 100 | // when 101 | CreateOrderResponse response = service.createOrder().invoke(createOrderRequest) 102 | .toCompletableFuture().get(3, SECONDS); 103 | 104 | // then 105 | Assert.assertNotNull(response.getId()); 106 | } 107 | 108 | @Test 109 | public void createOrderWithNonExistingItem_Should_ReturnError() throws Exception { 110 | // given 111 | UUID itemId = UUID.randomUUID(); 112 | Mockito.when(itemService.getItem(itemId.toString())) 113 | .thenReturn(req -> CompletableFuture.completedFuture( 114 | null)); 115 | OrderService service = server.client(OrderService.class); 116 | CreateOrderRequest createOrderRequest = CreateOrderRequest.builder() 117 | .itemId(itemId) 118 | .amount(2) 119 | .customer("Yannick") 120 | .build(); 121 | 122 | // when 123 | try { 124 | service.createOrder().invoke(createOrderRequest).toCompletableFuture().get(3, SECONDS); 125 | Assert.fail("createOrder should've returned an error"); 126 | } catch (Exception e) { 127 | // then 128 | Assert.assertEquals(String.format("com.lightbend.lagom.javadsl.api.deser.DeserializationException: " + 129 | "No item found for id %s " + 130 | "(TransportErrorCode{http=400, webSocket=1003, description='Unsupported Data/Bad Request'})", 131 | itemId.toString()), e.getMessage()); 132 | } 133 | } 134 | 135 | @Test 136 | public void getOrder_Should_ReturnCreatedOrder() throws Exception { 137 | // given 138 | UUID itemId = UUID.randomUUID(); 139 | Mockito.when(itemService.getItem(itemId.toString())) 140 | .thenReturn(req -> CompletableFuture.completedFuture( 141 | Item.of(itemId, "Chair", BigDecimal.valueOf(14.99)))); 142 | OrderService service = server.client(OrderService.class); 143 | CreateOrderRequest createOrderRequest = CreateOrderRequest.builder() 144 | .itemId(itemId) 145 | .amount(2) 146 | .customer("Yannick") 147 | .build(); 148 | CreateOrderResponse createOrderResponse = service.createOrder().invoke(createOrderRequest) 149 | .toCompletableFuture().get(3, SECONDS); 150 | 151 | // when 152 | ServiceTest.eventually(FiniteDuration.create(10, SECONDS), FiniteDuration.create(1000, MILLISECONDS), () -> { 153 | Order response = service.getOrder(createOrderResponse.getId().toString()) 154 | .invoke(NotUsed.getInstance()).toCompletableFuture().get(3, SECONDS); 155 | 156 | // then 157 | Assert.assertEquals(createOrderResponse.getId(), response.getId()); 158 | Assert.assertEquals(itemId, response.getItemId()); 159 | Assert.assertEquals(2, response.getAmount().intValue()); 160 | Assert.assertEquals("Yannick", response.getCustomer()); 161 | }); 162 | } 163 | 164 | @Test 165 | public void getOrder_Should_ReturnErrorForNonExistingOrder() throws Exception { 166 | // given 167 | OrderService service = server.client(OrderService.class); 168 | 169 | // when 170 | UUID randomId = UUID.randomUUID(); 171 | try { 172 | service.getOrder(randomId.toString()) 173 | .invoke(NotUsed.getInstance()).toCompletableFuture().get(3, SECONDS); 174 | Assert.fail("getOrder should've returned an error"); 175 | } catch (Exception e) { 176 | // then 177 | Assert.assertEquals(String.format("com.lightbend.lagom.javadsl.api.transport.NotFound: " + 178 | "No order found for id %s " + 179 | "(TransportErrorCode{http=404, webSocket=1008, description='Policy Violation'})", 180 | randomId.toString()), e.getMessage()); 181 | } 182 | } 183 | 184 | @Test 185 | public void getAllOrders_Should_ReturnCreatedOrders() throws Exception { 186 | // given 187 | OrderService service = server.client(OrderService.class); 188 | UUID itemId = UUID.randomUUID(); 189 | Mockito.when(itemService.getItem(itemId.toString())) 190 | .thenReturn(req -> CompletableFuture.completedFuture( 191 | Item.of(itemId, "Chair", BigDecimal.valueOf(14.99)))); 192 | CreateOrderRequest createOrderRequest = CreateOrderRequest.builder() 193 | .itemId(itemId) 194 | .amount(2) 195 | .customer("Yannick") 196 | .build(); 197 | CreateOrderResponse createOrderResponse = service.createOrder().invoke(createOrderRequest) 198 | .toCompletableFuture().get(3, SECONDS); 199 | CreateOrderRequest createOrderRequest2 = CreateOrderRequest.builder() 200 | .itemId(itemId) 201 | .amount(5) 202 | .customer("John") 203 | .build(); 204 | CreateOrderResponse createOrderResponse2 = service.createOrder().invoke(createOrderRequest2) 205 | .toCompletableFuture().get(3, SECONDS); 206 | 207 | // when 208 | ServiceTest.eventually(FiniteDuration.create(10, SECONDS), FiniteDuration.create(1000, MILLISECONDS), () -> { 209 | PSequence response = service.getAllOrders() 210 | .invoke(NotUsed.getInstance()).toCompletableFuture().get(3, SECONDS); 211 | 212 | // then 213 | // TODO find a way to truncate Cassandra tables and check on size 214 | // Assert.assertEquals(2, response.size()); 215 | Assert.assertTrue(String.format("Doesn't contain order %s", createOrderResponse.getId()), 216 | response.stream().anyMatch(i -> createOrderResponse.getId().equals(i.getId()))); 217 | Assert.assertTrue(String.format("Doesn't contain order %s", createOrderResponse2.getId()), 218 | response.stream().anyMatch(i -> createOrderResponse2.getId().equals(i.getId()))); 219 | response.stream() 220 | .filter(i -> i.getId().equals(createOrderResponse.getId())) 221 | .forEach(i -> { 222 | Assert.assertEquals(createOrderResponse.getId(), i.getId()); 223 | Assert.assertEquals(itemId, i.getItemId()); 224 | Assert.assertEquals(2, i.getAmount().intValue()); 225 | Assert.assertEquals("Yannick", i.getCustomer()); 226 | } 227 | ); 228 | response.stream() 229 | .filter(i -> i.getId().equals(createOrderResponse2.getId())) 230 | .forEach(i -> { 231 | Assert.assertEquals(createOrderResponse2.getId(), i.getId()); 232 | Assert.assertEquals(itemId, i.getItemId()); 233 | Assert.assertEquals(5, i.getAmount().intValue()); 234 | Assert.assertEquals("John", i.getCustomer()); 235 | } 236 | ); 237 | 238 | 239 | }); 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /front-end/conf/application.conf: -------------------------------------------------------------------------------- 1 | # This is the main configuration file for the application. 2 | # https://www.playframework.com/documentation/latest/ConfigFile 3 | # ~~~~~ 4 | # Play uses HOCON as its configuration file format. HOCON has a number 5 | # of advantages over other config formats, but there are two things that 6 | # can be used when modifying settings. 7 | # 8 | # You can include other configuration files in this main application.conf file: 9 | #include "extra-config.conf" 10 | # 11 | # You can declare variables and substitute for them: 12 | #mykey = ${some.value} 13 | # 14 | # And if an environment variable exists when there is no other subsitution, then 15 | # HOCON will fall back to substituting environment variable: 16 | #mykey = ${JAVA_HOME} 17 | 18 | play.application.loader = FrontendLoader 19 | 20 | ## Lagom stuff 21 | lagom.play { 22 | service-name = "lagom-shop-front-end" 23 | acls = [ 24 | { 25 | path-regex = "(?!/api/).*" 26 | } 27 | ] 28 | } 29 | 30 | 31 | ## Akka 32 | # https://www.playframework.com/documentation/latest/ScalaAkka#Configuration 33 | # https://www.playframework.com/documentation/latest/JavaAkka#Configuration 34 | # ~~~~~ 35 | # Play uses Akka internally and exposes Akka Streams and actors in Websockets and 36 | # other streaming HTTP responses. 37 | akka { 38 | # "akka.log-config-on-start" is extraordinarly useful because it log the complete 39 | # configuration at INFO level, including defaults and overrides, so it s worth 40 | # putting at the very top. 41 | # 42 | # Put the following in your conf/logback.xml file: 43 | # 44 | # 45 | # 46 | # And then uncomment this line to debug the configuration. 47 | # 48 | #log-config-on-start = true 49 | } 50 | 51 | ## Secret key 52 | # http://www.playframework.com/documentation/latest/ApplicationSecret 53 | # ~~~~~ 54 | # The secret key is used to sign Play's session cookie. 55 | # This must be changed for production, but we don't recommend you change it in this file. 56 | play.crypto.secret = ${?APPLICATION_SECRET} 57 | 58 | ## Modules 59 | # https://www.playframework.com/documentation/latest/Modules 60 | # ~~~~~ 61 | # Control which modules are loaded when Play starts. Note that modules are 62 | # the replacement for "GlobalSettings", which are deprecated in 2.5.x. 63 | # Please see https://www.playframework.com/documentation/latest/GlobalSettings 64 | # for more information. 65 | # 66 | # You can also extend Play functionality by using one of the publically available 67 | # Play modules: https://playframework.com/documentation/latest/ModuleDirectory 68 | play.modules { 69 | # By default, Play will load any class called Module that is defined 70 | # in the root package (the "app" directory), or you can define them 71 | # explicitly below. 72 | # If there are any built-in modules that you want to disable, you can list them here. 73 | #enabled += my.application.Module 74 | 75 | # If there are any built-in modules that you want to disable, you can list them here. 76 | #disabled += "" 77 | } 78 | 79 | ## IDE 80 | # https://www.playframework.com/documentation/latest/IDE 81 | # ~~~~~ 82 | # Depending on your IDE, you can add a hyperlink for errors that will jump you 83 | # directly to the code location in the IDE in dev mode. The following line makes 84 | # use of the IntelliJ IDEA REST interface: 85 | #play.editor=http://localhost:63342/api/file/?file=%s&line=%s 86 | 87 | ## Internationalisation 88 | # https://www.playframework.com/documentation/latest/JavaI18N 89 | # https://www.playframework.com/documentation/latest/ScalaI18N 90 | # ~~~~~ 91 | # Play comes with its own i18n settings, which allow the user's preferred language 92 | # to map through to internal messages, or allow the language to be stored in a cookie. 93 | play.i18n { 94 | # The application languages 95 | langs = [ "en" ] 96 | 97 | # Whether the language cookie should be secure or not 98 | #langCookieSecure = true 99 | 100 | # Whether the HTTP only attribute of the cookie should be set to true 101 | #langCookieHttpOnly = true 102 | } 103 | 104 | ## Play HTTP settings 105 | # ~~~~~ 106 | play.http { 107 | ## Router 108 | # https://www.playframework.com/documentation/latest/JavaRouting 109 | # https://www.playframework.com/documentation/latest/ScalaRouting 110 | # ~~~~~ 111 | # Define the Router object to use for this application. 112 | # This router will be looked up first when the application is starting up, 113 | # so make sure this is the entry point. 114 | # Furthermore, it's assumed your route file is named properly. 115 | # So for an application router like `my.application.Router`, 116 | # you may need to define a router file `conf/my.application.routes`. 117 | # Default to Routes in the root package (aka "apps" folder) (and conf/routes) 118 | #router = my.application.Router 119 | 120 | ## Action Creator 121 | # https://www.playframework.com/documentation/latest/JavaActionCreator 122 | # ~~~~~ 123 | #actionCreator = null 124 | 125 | ## ErrorHandler 126 | # https://www.playframework.com/documentation/latest/JavaRouting 127 | # https://www.playframework.com/documentation/latest/ScalaRouting 128 | # ~~~~~ 129 | # If null, will attempt to load a class called ErrorHandler in the root package, 130 | #errorHandler = null 131 | 132 | ## Filters 133 | # https://www.playframework.com/documentation/latest/ScalaHttpFilters 134 | # https://www.playframework.com/documentation/latest/JavaHttpFilters 135 | # ~~~~~ 136 | # Filters run code on every request. They can be used to perform 137 | # common logic for all your actions, e.g. adding common headers. 138 | # Defaults to "Filters" in the root package (aka "apps" folder) 139 | # Alternatively you can explicitly register a class here. 140 | #filters = my.application.Filters 141 | 142 | ## Session & Flash 143 | # https://www.playframework.com/documentation/latest/JavaSessionFlash 144 | # https://www.playframework.com/documentation/latest/ScalaSessionFlash 145 | # ~~~~~ 146 | session { 147 | # Sets the cookie to be sent only over HTTPS. 148 | #secure = true 149 | 150 | # Sets the cookie to be accessed only by the server. 151 | #httpOnly = true 152 | 153 | # Sets the max-age field of the cookie to 5 minutes. 154 | # NOTE: this only sets when the browser will discard the cookie. Play will consider any 155 | # cookie value with a valid signature to be a valid session forever. To implement a server side session timeout, 156 | # you need to put a timestamp in the session and check it at regular intervals to possibly expire it. 157 | #maxAge = 300 158 | 159 | # Sets the domain on the session cookie. 160 | #domain = "example.com" 161 | } 162 | 163 | flash { 164 | # Sets the cookie to be sent only over HTTPS. 165 | #secure = true 166 | 167 | # Sets the cookie to be accessed only by the server. 168 | #httpOnly = true 169 | } 170 | } 171 | 172 | ## Netty Provider 173 | # https://www.playframework.com/documentation/latest/SettingsNetty 174 | # ~~~~~ 175 | play.server.netty { 176 | # Whether the Netty wire should be logged 177 | #log.wire = true 178 | 179 | # If you run Play on Linux, you can use Netty's native socket transport 180 | # for higher performance with less garbage. 181 | #transport = "native" 182 | } 183 | 184 | ## WS (HTTP Client) 185 | # https://www.playframework.com/documentation/latest/ScalaWS#Configuring-WS 186 | # ~~~~~ 187 | # The HTTP client primarily used for REST APIs. The default client can be 188 | # configured directly, but you can also create different client instances 189 | # with customized settings. You must enable this by adding to build.sbt: 190 | # 191 | # libraryDependencies += ws // or javaWs if using java 192 | # 193 | play.ws { 194 | # Sets HTTP requests not to follow 302 requests 195 | #followRedirects = false 196 | 197 | # Sets the maximum number of open HTTP connections for the client. 198 | #ahc.maxConnectionsTotal = 50 199 | 200 | ## WS SSL 201 | # https://www.playframework.com/documentation/latest/WsSSL 202 | # ~~~~~ 203 | ssl { 204 | # Configuring HTTPS with Play WS does not require programming. You can 205 | # set up both trustManager and keyManager for mutual authentication, and 206 | # turn on JSSE debugging in development with a reload. 207 | #debug.handshake = true 208 | #trustManager = { 209 | # stores = [ 210 | # { type = "JKS", path = "exampletrust.jks" } 211 | # ] 212 | #} 213 | } 214 | } 215 | 216 | ## Cache 217 | # https://www.playframework.com/documentation/latest/JavaCache 218 | # https://www.playframework.com/documentation/latest/ScalaCache 219 | # ~~~~~ 220 | # Play comes with an integrated cache API that can reduce the operational 221 | # overhead of repeated requests. You must enable this by adding to build.sbt: 222 | # 223 | # libraryDependencies += cache 224 | # 225 | play.cache { 226 | # If you want to bind several caches, you can bind the individually 227 | #bindCaches = ["db-cache", "user-cache", "session-cache"] 228 | } 229 | 230 | ## Filters 231 | # https://www.playframework.com/documentation/latest/Filters 232 | # ~~~~~ 233 | # There are a number of built-in filters that can be enabled and configured 234 | # to give Play greater security. You must enable this by adding to build.sbt: 235 | # 236 | # libraryDependencies += filters 237 | # 238 | play.filters { 239 | ## CORS filter configuration 240 | # https://www.playframework.com/documentation/latest/CorsFilter 241 | # ~~~~~ 242 | # CORS is a protocol that allows web applications to make requests from the browser 243 | # across different domains. 244 | # NOTE: You MUST apply the CORS configuration before the CSRF filter, as CSRF has 245 | # dependencies on CORS settings. 246 | cors { 247 | # Filter paths by a whitelist of path prefixes 248 | #pathPrefixes = ["/some/path", ...] 249 | 250 | # The allowed origins. If null, all origins are allowed. 251 | #allowedOrigins = ["http://www.example.com"] 252 | 253 | # The allowed HTTP methods. If null, all methods are allowed 254 | #allowedHttpMethods = ["GET", "POST"] 255 | } 256 | 257 | ## CSRF Filter 258 | # https://www.playframework.com/documentation/latest/ScalaCsrf#Applying-a-global-CSRF-filter 259 | # https://www.playframework.com/documentation/latest/JavaCsrf#Applying-a-global-CSRF-filter 260 | # ~~~~~ 261 | # Play supports multiple methods for verifying that a request is not a CSRF request. 262 | # The primary mechanism is a CSRF token. This token gets placed either in the query string 263 | # or body of every form submitted, and also gets placed in the users session. 264 | # Play then verifies that both tokens are present and match. 265 | csrf { 266 | # Sets the cookie to be sent only over HTTPS 267 | #cookie.secure = true 268 | 269 | # Defaults to CSRFErrorHandler in the root package. 270 | #errorHandler = MyCSRFErrorHandler 271 | } 272 | 273 | ## Security headers filter configuration 274 | # https://www.playframework.com/documentation/latest/SecurityHeaders 275 | # ~~~~~ 276 | # Defines security headers that prevent XSS attacks. 277 | # If enabled, then all options are set to the below configuration by default: 278 | headers { 279 | # The X-Frame-Options header. If null, the header is not set. 280 | #frameOptions = "DENY" 281 | 282 | # The X-XSS-Protection header. If null, the header is not set. 283 | #xssProtection = "1; mode=block" 284 | 285 | # The X-Content-Type-Options header. If null, the header is not set. 286 | #contentTypeOptions = "nosniff" 287 | 288 | # The X-Permitted-Cross-Domain-Policies header. If null, the header is not set. 289 | #permittedCrossDomainPolicies = "master-only" 290 | 291 | # The Content-Security-Policy header. If null, the header is not set. 292 | #contentSecurityPolicy = "default-src 'self'" 293 | } 294 | 295 | ## Allowed hosts filter configuration 296 | # https://www.playframework.com/documentation/latest/AllowedHostsFilter 297 | # ~~~~~ 298 | # Play provides a filter that lets you configure which hosts can access your application. 299 | # This is useful to prevent cache poisoning attacks. 300 | hosts { 301 | # Allow requests to example.com, its subdomains, and localhost:9000. 302 | #allowed = [".example.com", "localhost:9000"] 303 | } 304 | } 305 | 306 | ## Evolutions 307 | # https://www.playframework.com/documentation/latest/Evolutions 308 | # ~~~~~ 309 | # Evolutions allows database scripts to be automatically run on startup in dev mode 310 | # for database migrations. You must enable this by adding to build.sbt: 311 | # 312 | # libraryDependencies += evolutions 313 | # 314 | play.evolutions { 315 | # You can disable evolutions for a specific datasource if necessary 316 | #db.default.enabled = false 317 | } 318 | 319 | ## Database Connection Pool 320 | # https://www.playframework.com/documentation/latest/SettingsJDBC 321 | # ~~~~~ 322 | # Play doesn't require a JDBC database to run, but you can easily enable one. 323 | # 324 | # libraryDependencies += jdbc 325 | # 326 | play.db { 327 | # The combination of these two settings results in "db.default" as the 328 | # default JDBC pool: 329 | #config = "db" 330 | #default = "default" 331 | 332 | # Play uses HikariCP as the default connection pool. You can override 333 | # settings by changing the prototype: 334 | prototype { 335 | # Sets a fixed JDBC connection pool size of 50 336 | #hikaricp.minimumIdle = 50 337 | #hikaricp.maximumPoolSize = 50 338 | } 339 | } 340 | 341 | ## JDBC Datasource 342 | # https://www.playframework.com/documentation/latest/JavaDatabase 343 | # https://www.playframework.com/documentation/latest/ScalaDatabase 344 | # ~~~~~ 345 | # Once JDBC datasource is set up, you can work with several different 346 | # database options: 347 | # 348 | # Slick (Scala preferred option): https://www.playframework.com/documentation/latest/PlaySlick 349 | # JPA (Java preferred option): https://playframework.com/documentation/latest/JavaJPA 350 | # EBean: https://playframework.com/documentation/latest/JavaEbean 351 | # Anorm: https://www.playframework.com/documentation/latest/ScalaAnorm 352 | # 353 | db { 354 | # You can declare as many datasources as you want. 355 | # By convention, the default datasource is named `default` 356 | 357 | # https://www.playframework.com/documentation/latest/Developing-with-the-H2-Database 358 | #default.driver = org.h2.Driver 359 | #default.url = "jdbc:h2:mem:play" 360 | #default.username = sa 361 | #default.password = "" 362 | 363 | # You can turn on SQL logging for any datasource 364 | # https://www.playframework.com/documentation/latest/Highlights25#Logging-SQL-statements 365 | #default.logSql=true 366 | } 367 | --------------------------------------------------------------------------------