├── src
├── main
│ ├── resources
│ │ ├── webapp
│ │ │ └── ROOT
│ │ ├── META-INF
│ │ │ └── services
│ │ │ │ └── com.vaadin.flow.server.VaadinServiceInitListener
│ │ ├── db
│ │ │ └── migration
│ │ │ │ ├── V01__CreateCategory.sql
│ │ │ │ └── V02__CreateReview.sql
│ │ └── simplelogger.properties
│ ├── kotlin
│ │ └── com
│ │ │ └── vaadin
│ │ │ └── starter
│ │ │ └── beveragebuddy
│ │ │ ├── Main.kt
│ │ │ ├── backend
│ │ │ ├── RestService.kt
│ │ │ ├── Category.kt
│ │ │ ├── Review.kt
│ │ │ └── DemoData.kt
│ │ │ ├── ui
│ │ │ ├── Toolbar.kt
│ │ │ ├── MainLayout.kt
│ │ │ ├── categories
│ │ │ │ ├── CategoryEditorDialog.kt
│ │ │ │ └── CategoriesList.kt
│ │ │ ├── EditorDialogFrame.kt
│ │ │ └── reviews
│ │ │ │ ├── ReviewEditorDialog.kt
│ │ │ │ └── ReviewsList.kt
│ │ │ └── Bootstrap.kt
│ └── frontend
│ │ ├── index.html
│ │ └── themes
│ │ └── my-theme
│ │ └── styles.css
└── test
│ └── kotlin
│ └── com
│ └── vaadin
│ └── starter
│ └── beveragebuddy
│ ├── backend
│ ├── ReviewTest.kt
│ ├── ReviewWithCategoryTest.kt
│ ├── CategoryTest.kt
│ └── RestServiceTest.kt
│ ├── AbstractAppTest.kt
│ └── ui
│ ├── ReviewsListTest.kt
│ ├── CategoryEditorDialogTest.kt
│ ├── ReviewEditorDialogTest.kt
│ └── CategoriesListTest.kt
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── .gitignore
├── .github
└── workflows
│ └── gradle.yml
├── LICENSE
├── Dockerfile
├── gradlew.bat
├── README.md
└── gradlew
/src/main/resources/webapp/ROOT:
--------------------------------------------------------------------------------
1 | Don't delete this file; see Main.java for details.
2 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mvysny/beverage-buddy-vok/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/src/main/resources/META-INF/services/com.vaadin.flow.server.VaadinServiceInitListener:
--------------------------------------------------------------------------------
1 | com.vaadin.starter.beveragebuddy.MyServiceInitListener
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | build
3 | .gradle
4 | classes
5 | local.properties
6 | *.iml
7 | out
8 |
9 | target
10 | src/main/frontend/generated
11 |
--------------------------------------------------------------------------------
/src/main/resources/db/migration/V01__CreateCategory.sql:
--------------------------------------------------------------------------------
1 | create TABLE CATEGORY (
2 | id bigint auto_increment PRIMARY KEY,
3 | name varchar(200) NOT NULL
4 | );
5 | create UNIQUE INDEX idx_category_name ON CATEGORY(name);
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/vaadin/starter/beveragebuddy/Main.kt:
--------------------------------------------------------------------------------
1 | package com.vaadin.starter.beveragebuddy
2 |
3 | import com.github.mvysny.vaadinboot.VaadinBoot
4 |
5 | /**
6 | * Run this function to launch your app in Embedded Jetty.
7 | */
8 | fun main() {
9 | VaadinBoot().run()
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/resources/simplelogger.properties:
--------------------------------------------------------------------------------
1 | org.slf4j.simpleLogger.defaultLogLevel = info
2 | org.slf4j.simpleLogger.showDateTime = true
3 | org.slf4j.simpleLogger.dateTimeFormat = yyyy-MM-dd HH:mm:ss.SSS
4 | org.slf4j.simpleLogger.log.org.atmosphere = warn
5 | org.slf4j.simpleLogger.log.org.eclipse.jetty = warn
6 | org.slf4j.simpleLogger.log.org.eclipse.jetty.annotations.AnnotationParser = error
7 |
--------------------------------------------------------------------------------
/src/main/resources/db/migration/V02__CreateReview.sql:
--------------------------------------------------------------------------------
1 | create TABLE REVIEW (
2 | id bigint auto_increment PRIMARY KEY,
3 | name VARCHAR(200) not null,
4 | score TINYINT NOT NULL,
5 | date DATE not NULL,
6 | category BIGINT,
7 | count TINYINT not null
8 | );
9 | alter table Review add CONSTRAINT r_score_range CHECK (score >= 1 and score <= 5);
10 | alter table Review add FOREIGN KEY (category) REFERENCES Category(ID);
11 | alter table Review add CONSTRAINT r_count_range CHECK (count >= 1 and count <= 99);
12 | create INDEX idx_review_name ON Review(name);
13 |
--------------------------------------------------------------------------------
/src/main/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/test/kotlin/com/vaadin/starter/beveragebuddy/backend/ReviewTest.kt:
--------------------------------------------------------------------------------
1 | package com.vaadin.starter.beveragebuddy.backend
2 |
3 | import org.junit.jupiter.api.Nested
4 | import org.junit.jupiter.api.Test
5 | import kotlin.test.expect
6 |
7 | class ReviewTest {
8 | @Nested inner class validation() {
9 | @Test fun smoke() {
10 | expect(true) { Review(name = "Foo", category = 1L).isValid }
11 | expect(false) { Review().isValid }
12 | expect(false) { Review(category = 1L).isValid }
13 | expect(false) { Review(name = "Foo").isValid }
14 | expect(false) { Review(name = "F", category = 1L).isValid }
15 | expect(false) { Review(score = 10, name = "Foo", category = 1L).isValid }
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/.github/workflows/gradle.yml:
--------------------------------------------------------------------------------
1 | name: Gradle
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 |
8 | strategy:
9 | matrix:
10 | os: [ubuntu-latest, windows-latest]
11 | java: [17]
12 |
13 | runs-on: ${{ matrix.os }}
14 |
15 | steps:
16 | - uses: actions/checkout@v3
17 | - name: Set up JDK ${{ matrix.java }}
18 | uses: actions/setup-java@v4
19 | with:
20 | java-version: ${{ matrix.java }}
21 | distribution: 'temurin'
22 | - name: Cache Gradle packages
23 | uses: actions/cache@v4
24 | with:
25 | path: |
26 | ~/.gradle/caches
27 | ~/.gradle/wrapper
28 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts', 'gradle/wrapper/gradle-wrapper.properties', 'gradle.properties') }}
29 | - name: Build with Gradle
30 | run: ./gradlew clean build '-Pvaadin.productionMode' --stacktrace --info --no-daemon
31 |
32 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/vaadin/starter/beveragebuddy/backend/RestService.kt:
--------------------------------------------------------------------------------
1 | package com.vaadin.starter.beveragebuddy.backend
2 |
3 | import eu.vaadinonkotlin.rest.*
4 | import io.javalin.Javalin
5 | import jakarta.servlet.annotation.WebServlet
6 | import jakarta.servlet.http.HttpServlet
7 | import jakarta.servlet.http.HttpServletRequest
8 | import jakarta.servlet.http.HttpServletResponse
9 |
10 | /**
11 | * Provides access to person list. To test, just run `curl http://localhost:8080/rest/categories`
12 | */
13 | @WebServlet(urlPatterns = ["/rest/*"], name = "JavalinRestServlet", asyncSupported = false)
14 | class JavalinRestServlet : HttpServlet() {
15 | val javalin = Javalin.createStandalone { it.gsonMapper(VokRest.gson) } .apply {
16 | get("/rest/categories") { ctx -> ctx.json(Category.findAll()) }
17 | crud2("/rest/reviews", Review.getCrudHandler(false))
18 | }.javalinServlet()
19 |
20 | override fun service(req: HttpServletRequest, resp: HttpServletResponse) {
21 | javalin.service(req, resp)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | vaadin = "24.9.7"
3 | # https://repo1.maven.org/maven2/org/slf4j/slf4j-api/
4 | slf4j = "2.0.17"
5 | vok = "0.18.1"
6 |
7 | [libraries]
8 | slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
9 | slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
10 | vaadin-boot = "com.github.mvysny.vaadin-boot:vaadin-boot:13.5"
11 | vaadin-core = { module = "com.vaadin:vaadin-core", version.ref = "vaadin" }
12 | karibu-testing = "com.github.mvysny.kaributesting:karibu-testing-v24:2.4.3"
13 | karibu-dsl = "com.github.mvysny.karibudsl:karibu-dsl-v23:2.5.0"
14 | flyway = "org.flywaydb:flyway-core:11.18.0"
15 | h2 = "com.h2database:h2:2.2.224"
16 | hikaricp = "com.zaxxer:HikariCP:7.0.2"
17 | vok-db = { module = "eu.vaadinonkotlin:vok-framework-vokdb", version.ref = "vok" }
18 | vok-rest-server = { module = "eu.vaadinonkotlin:vok-rest", version.ref = "vok" }
19 | vok-rest-client = { module = "eu.vaadinonkotlin:vok-rest-client", version.ref = "vok" }
20 | junit = "org.junit.jupiter:junit-jupiter-engine:6.0.1"
21 |
22 | [plugins]
23 | vaadin = { id = "com.vaadin", version.ref = "vaadin" }
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020 Martin Vyšný
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Allows you to run this app easily as a docker container.
2 | # See README.md for more details.
3 | #
4 | # 1. Build the image with: docker build -t test/beverage-buddy-vok:latest .
5 | # 2. Run the image with: docker run --rm -ti -p8080:8080 test/beverage-buddy-vok
6 | #
7 | # Uses Docker Multi-stage builds: https://docs.docker.com/build/building/multi-stage/
8 |
9 | # The "Build" stage. Copies the entire project into the container, into the /app/ folder, and builds it.
10 | FROM eclipse-temurin:21 AS builder
11 | COPY . /app/
12 | WORKDIR /app/
13 | RUN --mount=type=cache,target=/root/.gradle --mount=type=cache,target=/root/.vaadin ./gradlew clean build -Pvaadin.productionMode --no-daemon --info --stacktrace
14 | WORKDIR /app/build/distributions/
15 | RUN tar xvf app.tar
16 | # At this point, we have the app (executable bash scrip plus a bunch of jars) in the
17 | # /app/build/distributions/app/ folder.
18 |
19 | # The "Run" stage. Start with a clean image, and copy over just the app itself, omitting gradle, npm and any intermediate build files.
20 | FROM eclipse-temurin:21
21 | COPY --from=builder /app/build/distributions/app /app/
22 | WORKDIR /app/bin
23 | EXPOSE 8080
24 | ENTRYPOINT ["./app"]
25 |
--------------------------------------------------------------------------------
/src/test/kotlin/com/vaadin/starter/beveragebuddy/backend/ReviewWithCategoryTest.kt:
--------------------------------------------------------------------------------
1 | package com.vaadin.starter.beveragebuddy.backend
2 |
3 | import com.github.mvysny.kaributesting.v10.expectList
4 | import com.gitlab.mvysny.jdbiorm.condition.Condition
5 | import com.vaadin.flow.data.provider.Query
6 | import com.vaadin.flow.data.provider.QuerySortOrder
7 | import com.vaadin.flow.data.provider.SortDirection
8 | import com.vaadin.starter.beveragebuddy.AbstractAppTest
9 | import org.junit.jupiter.api.Test
10 | import kotlin.test.expect
11 |
12 | class ReviewWithCategoryTest : AbstractAppTest() {
13 | @Test fun smoke() {
14 | val category = Category(name = "Foo")
15 | category.save()
16 | val review = Review(name = "Bar", category = category.id)
17 | review.save()
18 |
19 | expectList(ReviewWithCategory(review, "Foo")) { ReviewWithCategory.dataProvider.fetch(Query()).toList() }
20 | expect(1) { ReviewWithCategory.dataProvider.size(Query()) }
21 | val query = Query(
22 | 0,
23 | 30,
24 | listOf(QuerySortOrder(Review.NAME.toExternalString(), SortDirection.ASCENDING)),
25 | null,
26 | null
27 | )
28 | expectList(
29 | ReviewWithCategory(
30 | review,
31 | "Foo"
32 | )
33 | ) { ReviewWithCategory.dataProvider.fetch(query).toList() }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/vaadin/starter/beveragebuddy/backend/Category.kt:
--------------------------------------------------------------------------------
1 | package com.vaadin.starter.beveragebuddy.backend
2 |
3 | import com.github.vokorm.*
4 | import com.gitlab.mvysny.jdbiorm.Dao
5 | import jakarta.validation.constraints.NotBlank
6 | import jakarta.validation.constraints.NotEmpty
7 | import jakarta.validation.constraints.NotNull
8 |
9 | /**
10 | * Represents a beverage category.
11 | * @property id
12 | * @property name the category name
13 | */
14 | data class Category(
15 | override var id: Long? = null,
16 |
17 | @field:NotBlank
18 | var name: String = ""
19 | ) : KEntity {
20 |
21 | companion object : Dao(Category::class.java) {
22 | fun findByName(name: String): Category? = findSingleBy { Category::name eq name }
23 | fun getByName(name: String): Category = singleBy { Category::name eq name }
24 | fun existsWithName(name: String): Boolean = existsBy { Category::name eq name }
25 | override fun deleteAll() {
26 | db {
27 | handle.createUpdate("update Review set category = NULL").execute()
28 | super.deleteAll()
29 | }
30 | }
31 | }
32 |
33 | override fun delete() {
34 | db {
35 | if (id != null) {
36 | handle.createUpdate("update Review set category = NULL where category=:catId")
37 | .bind("catId", id!!)
38 | .execute()
39 | }
40 | super.delete()
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/test/kotlin/com/vaadin/starter/beveragebuddy/AbstractAppTest.kt:
--------------------------------------------------------------------------------
1 | package com.vaadin.starter.beveragebuddy
2 |
3 | import com.github.mvysny.kaributesting.v10.MockVaadin
4 | import com.github.mvysny.kaributesting.v10.Routes
5 | import com.vaadin.starter.beveragebuddy.backend.Category
6 | import com.vaadin.starter.beveragebuddy.backend.Review
7 | import org.junit.jupiter.api.AfterAll
8 | import org.junit.jupiter.api.AfterEach
9 | import org.junit.jupiter.api.BeforeAll
10 | import org.junit.jupiter.api.BeforeEach
11 |
12 | /**
13 | * Properly configures the app in the test context, so that the app is properly initialized, and the database is emptied before every test.
14 | */
15 | abstract class AbstractAppTest {
16 | companion object {
17 | // since there is no servlet environment, Flow won't auto-detect the @Routes. We need to auto-discover all @Routes
18 | // and populate the RouteRegistry properly.
19 | private val routes = Routes().autoDiscoverViews("com.vaadin.starter.beveragebuddy")
20 |
21 | @BeforeAll @JvmStatic fun startApp() { Bootstrap().contextInitialized(null) }
22 | @AfterAll @JvmStatic fun stopApp() { Bootstrap().contextDestroyed(null) }
23 | }
24 |
25 | @BeforeEach fun setupVaadin() { MockVaadin.setup(routes) }
26 | @AfterEach fun teardownVaadin() { MockVaadin.tearDown() }
27 |
28 | // it's a good practice to clear up the db before every test, to start every test with a predefined state.
29 | @BeforeEach @AfterEach fun cleanupDb() { Category.deleteAll(); Review.deleteAll() }
30 | }
31 |
--------------------------------------------------------------------------------
/src/test/kotlin/com/vaadin/starter/beveragebuddy/backend/CategoryTest.kt:
--------------------------------------------------------------------------------
1 | package com.vaadin.starter.beveragebuddy.backend
2 |
3 | import com.github.mvysny.kaributesting.v10.expectList
4 | import com.vaadin.starter.beveragebuddy.AbstractAppTest
5 | import org.junit.jupiter.api.Nested
6 | import org.junit.jupiter.api.Test
7 | import java.time.LocalDate
8 | import kotlin.test.expect
9 |
10 | class CategoryTest : AbstractAppTest() {
11 | @Nested inner class validation {
12 | @Test fun smoke() {
13 | expect(false) { Category().isValid }
14 | expect(false) { Category(name = " ").isValid }
15 | expect(true) { Category(name = "F").isValid }
16 | }
17 | }
18 |
19 | @Nested inner class delete {
20 | @Test fun smoke() {
21 | val cat = Category(name = "Foo")
22 | cat.save()
23 | cat.delete()
24 | expectList() { Category.findAll() }
25 | }
26 | @Test fun `deleting category fixes foreign keys`() {
27 | val cat = Category(name = "Foo")
28 | cat.save()
29 | val review = Review(name = "Foo", score = 1, date = LocalDate.now(), category = cat.id!!, count = 1)
30 | review.save()
31 |
32 | cat.delete()
33 | expectList() { Category.findAll() }
34 | expect(null) { Review.single().category }
35 | }
36 | }
37 |
38 | @Test fun existsWithName() {
39 | expect(false) { Category.existsWithName("Foo") }
40 | Category(name = "Foo").save()
41 | expect(true) { Category.existsWithName("Foo") }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/test/kotlin/com/vaadin/starter/beveragebuddy/ui/ReviewsListTest.kt:
--------------------------------------------------------------------------------
1 | package com.vaadin.starter.beveragebuddy.ui
2 |
3 | import com.github.mvysny.kaributesting.v10._click
4 | import com.github.mvysny.kaributesting.v10._expectNone
5 | import com.github.mvysny.kaributesting.v10._expectOne
6 | import com.github.mvysny.kaributesting.v10._get
7 | import com.github.mvysny.kaributesting.v23.expectRows
8 | import com.vaadin.flow.component.UI
9 | import com.vaadin.flow.component.button.Button
10 | import com.vaadin.flow.component.virtuallist.VirtualList
11 | import com.vaadin.starter.beveragebuddy.AbstractAppTest
12 | import com.vaadin.starter.beveragebuddy.backend.Category
13 | import com.vaadin.starter.beveragebuddy.backend.Review
14 | import com.vaadin.starter.beveragebuddy.backend.ReviewWithCategory
15 | import org.junit.jupiter.api.Test
16 |
17 | class ReviewsListTest : AbstractAppTest() {
18 | @Test fun `no reviews initially`() {
19 | _get>().expectRows(0)
20 | }
21 |
22 | @Test fun reviewsListed() {
23 | // prepare testing data
24 | val cat = Category(name = "Beers")
25 | cat.save()
26 | Review(score = 1, name = "Good!", category = cat.id).save()
27 | _get>().expectRows(1)
28 | }
29 |
30 | @Test fun `'new review' smoke test`() {
31 | UI.getCurrent().navigate("")
32 | _get