├── .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 |
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 | Name |
13 | Price |
14 |
15 |
16 |
17 | @items.map { item =>
18 |
19 | |
20 |
21 |
22 |
23 | |
24 | @item.name |
25 | $@item.price |
26 |
27 | }
28 |
29 |
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 | ItemID |
13 | Amount |
14 | Customer |
15 |
16 |
17 |
18 | @orders.map { order =>
19 |
20 | |
21 |
22 |
23 |
24 | |
25 | @order.itemId |
26 | @order.amount |
27 | @order.customer |
28 |
29 | }
30 |
31 |
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 |
--------------------------------------------------------------------------------