Skip to content

Commit c193ce6

Browse files
Fixed: #3963 Added Kotlin Spring Boot Example (#3965)
# Pull Request Added Kotlin Example for Spring Boot Fixes: #3963 ## Description Adds Example for Kotlin Spring Boot `6-hello-spring-boot` and `7-todo-spring-boot` ## Related Issues - Link to related issue #3963. ## Checklist - [x] New Example For Kotlin Spring Boot Hello World - [x] New Example for Kotlin Spring Boot Todo MVC ## Status Completed Addition
1 parent db891bd commit c193ce6

File tree

18 files changed

+548
-0
lines changed

18 files changed

+548
-0
lines changed

docs/modules/ROOT/pages/kotlinlib/web-examples.adoc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,11 @@ include::partial$example/kotlinlib/web/4-webapp-kotlinjs.adoc[]
2424
== Ktor KotlinJS Code Sharing
2525

2626
include::partial$example/kotlinlib/web/5-webapp-kotlinjs-shared.adoc[]
27+
28+
== Spring Boot Hello World App
29+
30+
include::partial$example/kotlinlib/web/6-hello-spring-boot.adoc[]
31+
32+
== Spring Boot TodoMvc App
33+
34+
include::partial$example/kotlinlib/web/7-todo-spring-boot.adoc[]
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package build
2+
import mill._, kotlinlib._
3+
4+
object `package` extends RootModule with KotlinModule {
5+
6+
def kotlinVersion = "1.9.24"
7+
8+
def mainClass = Some("com.example.HelloSpringBootKt")
9+
10+
def ivyDeps = Agg(
11+
ivy"org.springframework.boot:spring-boot-starter-web:2.5.6",
12+
ivy"org.springframework.boot:spring-boot-starter-actuator:2.5.6"
13+
)
14+
15+
object test extends KotlinTests with TestModule.Junit5 {
16+
def ivyDeps = super.ivyDeps() ++ Agg(
17+
ivy"org.springframework.boot:spring-boot-starter-test:2.5.6"
18+
)
19+
}
20+
}
21+
22+
// This example demonstrates how to set up a simple Spring Boot webserver,
23+
// able to handle a single HTTP request at `/` and reply with a single response.
24+
25+
/** Usage
26+
27+
> mill test
28+
...com.example.HelloSpringBootTest#shouldReturnDefaultMessage() finished...
29+
30+
> mill runBackground
31+
32+
> curl http://localhost:8095
33+
...<h1>Hello, World!</h1>...
34+
35+
> mill clean runBackground
36+
37+
*/
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
server.port=8095
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.example
2+
3+
import org.springframework.boot.autoconfigure.SpringBootApplication
4+
import org.springframework.boot.runApplication
5+
import org.springframework.web.bind.annotation.GetMapping
6+
import org.springframework.web.bind.annotation.RestController
7+
8+
@SpringBootApplication
9+
open class HelloSpringBoot
10+
11+
fun main(args: Array<String>) {
12+
runApplication<HelloSpringBoot>(*args)
13+
}
14+
15+
@RestController
16+
class HelloController {
17+
@GetMapping("/")
18+
fun hello(): String = "<h1>Hello, World!</h1>"
19+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.example
2+
3+
import org.junit.jupiter.api.Assertions.assertEquals
4+
import org.junit.jupiter.api.Test
5+
import org.springframework.beans.factory.annotation.Autowired
6+
import org.springframework.boot.test.context.SpringBootTest
7+
import org.springframework.boot.test.web.client.TestRestTemplate
8+
import org.springframework.boot.web.server.LocalServerPort
9+
10+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
11+
class HelloSpringBootTest {
12+
13+
@LocalServerPort
14+
private var port: Int = 0
15+
16+
@Autowired
17+
private lateinit var restTemplate: TestRestTemplate
18+
19+
@Test
20+
fun shouldReturnDefaultMessage() {
21+
val response = restTemplate.getForObject("http://localhost:$port/", String::class.java)
22+
assertEquals("<h1>Hello, World!</h1>", response)
23+
}
24+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package build
2+
import mill._, kotlinlib._
3+
4+
object `package` extends RootModule with KotlinModule {
5+
def kotlinVersion = "1.9.24"
6+
7+
def mainClass = Some("com.example.TodomvcApplicationKt")
8+
def ivyDeps = Agg(
9+
ivy"org.springframework.boot:spring-boot-starter-data-jpa:2.5.4",
10+
ivy"org.springframework.boot:spring-boot-starter-thymeleaf:2.5.4",
11+
ivy"org.springframework.boot:spring-boot-starter-validation:2.5.4",
12+
ivy"org.springframework.boot:spring-boot-starter-web:2.5.4",
13+
ivy"org.jetbrains.kotlin:kotlin-reflect:2.0.21",
14+
ivy"javax.xml.bind:jaxb-api:2.3.1",
15+
ivy"org.webjars:webjars-locator:0.41",
16+
ivy"org.webjars.npm:todomvc-common:1.0.5",
17+
ivy"org.webjars.npm:todomvc-app-css:2.4.1"
18+
)
19+
20+
trait HelloTests extends KotlinTests with TestModule.Junit5 {
21+
def mainClass = Some("com.example.TodomvcApplicationKt")
22+
def ivyDeps = super.ivyDeps() ++ Agg(
23+
ivy"org.springframework.boot:spring-boot-starter-test:2.5.6"
24+
)
25+
}
26+
27+
object test extends HelloTests {
28+
def ivyDeps = super.ivyDeps() ++ Agg(
29+
ivy"com.h2database:h2:2.3.230"
30+
)
31+
}
32+
33+
object integration extends HelloTests {
34+
def ivyDeps = super.ivyDeps() ++ Agg(
35+
ivy"org.testcontainers:testcontainers:1.18.0",
36+
ivy"org.testcontainers:junit-jupiter:1.18.0",
37+
ivy"org.testcontainers:postgresql:1.18.0",
38+
ivy"org.postgresql:postgresql:42.6.0"
39+
)
40+
}
41+
}
42+
43+
// This is a larger example using Spring Boot, implementing the well known
44+
// https://todomvc.com/[TodoMVC] example app. Apart from running a webserver,
45+
// this example also demonstrates:
46+
//
47+
// * Serving HTML templates using Thymeleaf
48+
// * Serving static Javascript and CSS using Webjars
49+
// * Querying a SQL database using JPA and H2
50+
// * Unit testing using a H2 in-memory database
51+
// * Integration testing using Testcontainers Postgres in Docker
52+
53+
/** Usage
54+
55+
> mill test
56+
...com.example.TodomvcTests#homePageLoads() finished...
57+
...com.example.TodomvcTests#addNewTodoItem() finished...
58+
59+
> mill integration
60+
...com.example.TodomvcIntegrationTests#homePageLoads() finished...
61+
...com.example.TodomvcIntegrationTests#addNewTodoItem() finished...
62+
63+
> mill test.runBackground
64+
65+
> curl http://localhost:8099
66+
...<h1>todos</h1>...
67+
68+
> mill clean runBackground
69+
70+
*/
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
spring.mvc.hiddenmethod.filter.enabled=true
2+
spring.jpa.hibernate.ddl-auto=update
3+
spring.datasource.driver-class-name=org.postgresql.Driver
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.example
2+
3+
import org.assertj.core.api.Assertions.assertThat
4+
import org.junit.jupiter.api.Test
5+
import org.springframework.beans.factory.annotation.Autowired
6+
import org.springframework.boot.test.context.SpringBootTest
7+
import org.springframework.boot.test.web.client.TestRestTemplate
8+
import org.springframework.boot.web.server.LocalServerPort
9+
import org.springframework.http.HttpEntity
10+
import org.springframework.http.HttpHeaders
11+
import org.springframework.http.HttpMethod
12+
import org.springframework.http.MediaType
13+
import org.springframework.test.context.DynamicPropertyRegistry
14+
import org.springframework.test.context.DynamicPropertySource
15+
import org.testcontainers.containers.PostgreSQLContainer
16+
import org.testcontainers.junit.jupiter.Container
17+
import org.testcontainers.junit.jupiter.Testcontainers
18+
19+
@Testcontainers
20+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
21+
class TodomvcIntegrationTests {
22+
23+
companion object {
24+
@Container
25+
val postgresContainer = PostgreSQLContainer<Nothing>("postgres:latest").apply {
26+
withDatabaseName("test")
27+
withUsername("test")
28+
withPassword("test")
29+
}
30+
31+
@JvmStatic
32+
@DynamicPropertySource
33+
fun postgresProperties(registry: DynamicPropertyRegistry) {
34+
registry.add("spring.datasource.url", postgresContainer::getJdbcUrl)
35+
registry.add("spring.datasource.username", postgresContainer::getUsername)
36+
registry.add("spring.datasource.password", postgresContainer::getPassword)
37+
}
38+
}
39+
40+
@LocalServerPort
41+
private var port: Int = 0
42+
43+
@Autowired
44+
private lateinit var restTemplate: TestRestTemplate
45+
46+
@Test
47+
fun homePageLoads() {
48+
val response = restTemplate.getForEntity("http://localhost:$port/", String::class.java)
49+
assertThat(response.statusCode.is2xxSuccessful).isTrue
50+
assertThat(response.body).contains("<h1>todos</h1>")
51+
}
52+
53+
@Test
54+
fun addNewTodoItem() {
55+
// Set up headers and form data for the POST request
56+
val headers = HttpHeaders().apply {
57+
contentType = MediaType.APPLICATION_FORM_URLENCODED
58+
}
59+
val newTodo = "title=Test+Todo"
60+
val entity = HttpEntity(newTodo, headers)
61+
62+
// Send the POST request to add a new todo item
63+
val postResponse = restTemplate.exchange(
64+
"http://localhost:$port/",
65+
HttpMethod.POST,
66+
entity,
67+
String::class.java,
68+
)
69+
assertThat(postResponse.statusCode.is3xxRedirection).isTrue
70+
71+
// Send a GET request to verify the new todo item was added
72+
val getResponse = restTemplate.getForEntity("http://localhost:$port/", String::class.java)
73+
assertThat(getResponse.statusCode.is2xxSuccessful).isTrue
74+
assertThat(getResponse.body).contains("Test Todo")
75+
}
76+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" lang="en">
2+
<li th:fragment="todoItem(item)" th:classappend="${item.completed?'completed':''}">
3+
<div class="view">
4+
<form th:action="@{/{id}/toggle(id=${item.id})}" th:method="put">
5+
<input class="toggle" type="checkbox"
6+
onchange="this.form.submit()"
7+
th:attrappend="checked=${item.completed?'true':null}">
8+
<label th:text="${item.title}">Taste JavaScript</label>
9+
</form>
10+
<form th:action="@{/{id}(id=${item.id})}" th:method="delete">
11+
<button class="destroy"></button>
12+
</form>
13+
</div>
14+
<input class="edit" value="Create a TodoMVC template">
15+
</li>
16+
</html>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<!doctype html>
2+
<html xmlns="http://www.w3.org/1999/xhtml"
3+
xmlns:th="http://www.thymeleaf.org"
4+
lang="en">
5+
<head>
6+
<meta charset="utf-8">
7+
<meta name="viewport" content="width=device-width, initial-scale=1">
8+
<title>Template • TodoMVC</title>
9+
<link rel="stylesheet" th:href="@{/webjars/todomvc-common/base.css}">
10+
<link rel="stylesheet" th:href="@{/webjars/todomvc-app-css/index.css}">
11+
</head>
12+
<body>
13+
<section class="todoapp">
14+
<header class="header">
15+
<h1>todos</h1>
16+
<form th:action="@{/}" method="post" th:object="${item}">
17+
<input class="new-todo" placeholder="What needs to be done?" autofocus
18+
th:field="*{title}">
19+
</form>
20+
</header>
21+
<!-- This section should be hidden by default and shown when there are todos -->
22+
<section class="main" th:if="${totalItemCount > 0}">
23+
<form th:action="@{/toggle-all}" th:method="put">
24+
<input id="toggle-all" class="toggle-all" type="checkbox"
25+
onclick="this.form.submit()">
26+
<label for="toggle-all">Mark all as complete</label>
27+
</form>
28+
<ul class="todo-list" th:remove="all-but-first">
29+
<li th:insert="fragments :: todoItem(${item})" th:each="item : ${todoItems}" th:remove="tag">
30+
</li>
31+
<!-- These are here just to show the structure of the list items -->
32+
<!-- List items should get the class `editing` when editing and `completed` when marked as completed -->
33+
<li class="completed">
34+
<div class="view">
35+
<input class="toggle" type="checkbox" checked>
36+
<label>Taste JavaScript</label>
37+
<button class="destroy"></button>
38+
</div>
39+
<input class="edit" value="Create a TodoMVC template">
40+
</li>
41+
<li>
42+
<div class="view">
43+
<input class="toggle" type="checkbox">
44+
<label>Buy a unicorn</label>
45+
<button class="destroy"></button>
46+
</div>
47+
<input class="edit" value="Rule the web">
48+
</li>
49+
</ul>
50+
</section>
51+
<!-- This footer should be hidden by default and shown when there are todos -->
52+
<footer class="footer" th:if="${totalItemCount > 0}">
53+
<th:block th:unless="${activeItemCount == 1}">
54+
<span class="todo-count"><strong th:text="${activeItemCount}">0</strong> items left</span>
55+
</th:block>
56+
<th:block th:if="${activeItemCount == 1}">
57+
<span class="todo-count"><strong>1</strong> item left</span>
58+
</th:block>
59+
<ul class="filters">
60+
<li>
61+
<a th:href="@{/}"
62+
th:classappend="${todoFilter.name() == 'ALL'?'selected':''}">All</a>
63+
</li>
64+
<li>
65+
<a th:href="@{/active}"
66+
th:classappend="${todoFilter.name() == 'ACTIVE'?'selected':''}">Active</a>
67+
</li>
68+
<li>
69+
<a th:href="@{/completed}"
70+
th:classappend="${todoFilter.name() == 'COMPLETED'?'selected':''}">Completed</a>
71+
</li>
72+
</ul>
73+
<form th:action="@{/completed}" th:method="delete"
74+
th:if="${completedItemCount > 0}">
75+
<button class="clear-completed">Clear completed</button>
76+
</form>
77+
</footer>
78+
</section>
79+
<footer class="info">
80+
<p>Double-click to edit a todo</p>
81+
</footer>
82+
<script th:src="@{/webjars/todomvc-common/base.js}"></script>
83+
</body>
84+
</html>

0 commit comments

Comments
 (0)