├── 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