Skip to content

Commit

Permalink
Fixed: #3963 Added Kotlin Spring Boot Example (#3965)
Browse files Browse the repository at this point in the history
# 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
  • Loading branch information
himanshumahajan138 authored Nov 17, 2024
1 parent db891bd commit c193ce6
Show file tree
Hide file tree
Showing 18 changed files with 548 additions and 0 deletions.
8 changes: 8 additions & 0 deletions docs/modules/ROOT/pages/kotlinlib/web-examples.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,11 @@ include::partial$example/kotlinlib/web/4-webapp-kotlinjs.adoc[]
== Ktor KotlinJS Code Sharing

include::partial$example/kotlinlib/web/5-webapp-kotlinjs-shared.adoc[]

== Spring Boot Hello World App

include::partial$example/kotlinlib/web/6-hello-spring-boot.adoc[]

== Spring Boot TodoMvc App

include::partial$example/kotlinlib/web/7-todo-spring-boot.adoc[]
37 changes: 37 additions & 0 deletions example/kotlinlib/web/6-hello-spring-boot/build.mill
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package build
import mill._, kotlinlib._

object `package` extends RootModule with KotlinModule {

def kotlinVersion = "1.9.24"

def mainClass = Some("com.example.HelloSpringBootKt")

def ivyDeps = Agg(
ivy"org.springframework.boot:spring-boot-starter-web:2.5.6",
ivy"org.springframework.boot:spring-boot-starter-actuator:2.5.6"
)

object test extends KotlinTests with TestModule.Junit5 {
def ivyDeps = super.ivyDeps() ++ Agg(
ivy"org.springframework.boot:spring-boot-starter-test:2.5.6"
)
}
}

// This example demonstrates how to set up a simple Spring Boot webserver,
// able to handle a single HTTP request at `/` and reply with a single response.

/** Usage

> mill test
...com.example.HelloSpringBootTest#shouldReturnDefaultMessage() finished...

> mill runBackground

> curl http://localhost:8095
...<h1>Hello, World!</h1>...

> mill clean runBackground

*/
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
server.port=8095
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.example

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@SpringBootApplication
open class HelloSpringBoot

fun main(args: Array<String>) {
runApplication<HelloSpringBoot>(*args)
}

@RestController
class HelloController {
@GetMapping("/")
fun hello(): String = "<h1>Hello, World!</h1>"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.example

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.client.TestRestTemplate
import org.springframework.boot.web.server.LocalServerPort

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class HelloSpringBootTest {

@LocalServerPort
private var port: Int = 0

@Autowired
private lateinit var restTemplate: TestRestTemplate

@Test
fun shouldReturnDefaultMessage() {
val response = restTemplate.getForObject("http://localhost:$port/", String::class.java)
assertEquals("<h1>Hello, World!</h1>", response)
}
}
70 changes: 70 additions & 0 deletions example/kotlinlib/web/7-todo-spring-boot/build.mill
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package build
import mill._, kotlinlib._

object `package` extends RootModule with KotlinModule {
def kotlinVersion = "1.9.24"

def mainClass = Some("com.example.TodomvcApplicationKt")
def ivyDeps = Agg(
ivy"org.springframework.boot:spring-boot-starter-data-jpa:2.5.4",
ivy"org.springframework.boot:spring-boot-starter-thymeleaf:2.5.4",
ivy"org.springframework.boot:spring-boot-starter-validation:2.5.4",
ivy"org.springframework.boot:spring-boot-starter-web:2.5.4",
ivy"org.jetbrains.kotlin:kotlin-reflect:2.0.21",
ivy"javax.xml.bind:jaxb-api:2.3.1",
ivy"org.webjars:webjars-locator:0.41",
ivy"org.webjars.npm:todomvc-common:1.0.5",
ivy"org.webjars.npm:todomvc-app-css:2.4.1"
)

trait HelloTests extends KotlinTests with TestModule.Junit5 {
def mainClass = Some("com.example.TodomvcApplicationKt")
def ivyDeps = super.ivyDeps() ++ Agg(
ivy"org.springframework.boot:spring-boot-starter-test:2.5.6"
)
}

object test extends HelloTests {
def ivyDeps = super.ivyDeps() ++ Agg(
ivy"com.h2database:h2:2.3.230"
)
}

object integration extends HelloTests {
def ivyDeps = super.ivyDeps() ++ Agg(
ivy"org.testcontainers:testcontainers:1.18.0",
ivy"org.testcontainers:junit-jupiter:1.18.0",
ivy"org.testcontainers:postgresql:1.18.0",
ivy"org.postgresql:postgresql:42.6.0"
)
}
}

// This is a larger example using Spring Boot, implementing the well known
// https://todomvc.com/[TodoMVC] example app. Apart from running a webserver,
// this example also demonstrates:
//
// * Serving HTML templates using Thymeleaf
// * Serving static Javascript and CSS using Webjars
// * Querying a SQL database using JPA and H2
// * Unit testing using a H2 in-memory database
// * Integration testing using Testcontainers Postgres in Docker

/** Usage

> mill test
...com.example.TodomvcTests#homePageLoads() finished...
...com.example.TodomvcTests#addNewTodoItem() finished...

> mill integration
...com.example.TodomvcIntegrationTests#homePageLoads() finished...
...com.example.TodomvcIntegrationTests#addNewTodoItem() finished...

> mill test.runBackground

> curl http://localhost:8099
...<h1>todos</h1>...

> mill clean runBackground

*/
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
spring.mvc.hiddenmethod.filter.enabled=true
spring.jpa.hibernate.ddl-auto=update
spring.datasource.driver-class-name=org.postgresql.Driver
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.example

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.client.TestRestTemplate
import org.springframework.boot.web.server.LocalServerPort
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpMethod
import org.springframework.http.MediaType
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers

@Testcontainers
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class TodomvcIntegrationTests {

companion object {
@Container
val postgresContainer = PostgreSQLContainer<Nothing>("postgres:latest").apply {
withDatabaseName("test")
withUsername("test")
withPassword("test")
}

@JvmStatic
@DynamicPropertySource
fun postgresProperties(registry: DynamicPropertyRegistry) {
registry.add("spring.datasource.url", postgresContainer::getJdbcUrl)
registry.add("spring.datasource.username", postgresContainer::getUsername)
registry.add("spring.datasource.password", postgresContainer::getPassword)
}
}

@LocalServerPort
private var port: Int = 0

@Autowired
private lateinit var restTemplate: TestRestTemplate

@Test
fun homePageLoads() {
val response = restTemplate.getForEntity("http://localhost:$port/", String::class.java)
assertThat(response.statusCode.is2xxSuccessful).isTrue
assertThat(response.body).contains("<h1>todos</h1>")
}

@Test
fun addNewTodoItem() {
// Set up headers and form data for the POST request
val headers = HttpHeaders().apply {
contentType = MediaType.APPLICATION_FORM_URLENCODED
}
val newTodo = "title=Test+Todo"
val entity = HttpEntity(newTodo, headers)

// Send the POST request to add a new todo item
val postResponse = restTemplate.exchange(
"http://localhost:$port/",
HttpMethod.POST,
entity,
String::class.java,
)
assertThat(postResponse.statusCode.is3xxRedirection).isTrue

// Send a GET request to verify the new todo item was added
val getResponse = restTemplate.getForEntity("http://localhost:$port/", String::class.java)
assertThat(getResponse.statusCode.is2xxSuccessful).isTrue
assertThat(getResponse.body).contains("Test Todo")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" lang="en">
<li th:fragment="todoItem(item)" th:classappend="${item.completed?'completed':''}">
<div class="view">
<form th:action="@{/{id}/toggle(id=${item.id})}" th:method="put">
<input class="toggle" type="checkbox"
onchange="this.form.submit()"
th:attrappend="checked=${item.completed?'true':null}">
<label th:text="${item.title}">Taste JavaScript</label>
</form>
<form th:action="@{/{id}(id=${item.id})}" th:method="delete">
<button class="destroy"></button>
</form>
</div>
<input class="edit" value="Create a TodoMVC template">
</li>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Template • TodoMVC</title>
<link rel="stylesheet" th:href="@{/webjars/todomvc-common/base.css}">
<link rel="stylesheet" th:href="@{/webjars/todomvc-app-css/index.css}">
</head>
<body>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<form th:action="@{/}" method="post" th:object="${item}">
<input class="new-todo" placeholder="What needs to be done?" autofocus
th:field="*{title}">
</form>
</header>
<!-- This section should be hidden by default and shown when there are todos -->
<section class="main" th:if="${totalItemCount > 0}">
<form th:action="@{/toggle-all}" th:method="put">
<input id="toggle-all" class="toggle-all" type="checkbox"
onclick="this.form.submit()">
<label for="toggle-all">Mark all as complete</label>
</form>
<ul class="todo-list" th:remove="all-but-first">
<li th:insert="fragments :: todoItem(${item})" th:each="item : ${todoItems}" th:remove="tag">
</li>
<!-- These are here just to show the structure of the list items -->
<!-- List items should get the class `editing` when editing and `completed` when marked as completed -->
<li class="completed">
<div class="view">
<input class="toggle" type="checkbox" checked>
<label>Taste JavaScript</label>
<button class="destroy"></button>
</div>
<input class="edit" value="Create a TodoMVC template">
</li>
<li>
<div class="view">
<input class="toggle" type="checkbox">
<label>Buy a unicorn</label>
<button class="destroy"></button>
</div>
<input class="edit" value="Rule the web">
</li>
</ul>
</section>
<!-- This footer should be hidden by default and shown when there are todos -->
<footer class="footer" th:if="${totalItemCount > 0}">
<th:block th:unless="${activeItemCount == 1}">
<span class="todo-count"><strong th:text="${activeItemCount}">0</strong> items left</span>
</th:block>
<th:block th:if="${activeItemCount == 1}">
<span class="todo-count"><strong>1</strong> item left</span>
</th:block>
<ul class="filters">
<li>
<a th:href="@{/}"
th:classappend="${todoFilter.name() == 'ALL'?'selected':''}">All</a>
</li>
<li>
<a th:href="@{/active}"
th:classappend="${todoFilter.name() == 'ACTIVE'?'selected':''}">Active</a>
</li>
<li>
<a th:href="@{/completed}"
th:classappend="${todoFilter.name() == 'COMPLETED'?'selected':''}">Completed</a>
</li>
</ul>
<form th:action="@{/completed}" th:method="delete"
th:if="${completedItemCount > 0}">
<button class="clear-completed">Clear completed</button>
</form>
</footer>
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
</footer>
<script th:src="@{/webjars/todomvc-common/base.js}"></script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example

import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication

@SpringBootApplication
open class TodomvcApplication

fun main(args: Array<String>) {
SpringApplication.run(TodomvcApplication::class.java, *args)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.todoitem

import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.Id
import javax.validation.constraints.NotBlank

@Entity
data class TodoItem(
@Id
@GeneratedValue
val id: Long? = null,

@field:NotBlank
var title: String = "",

var completed: Boolean = false,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.todoitem

import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.ResponseStatus

@ResponseStatus(HttpStatus.NOT_FOUND)
class TodoItemNotFoundException(itemId: Long) : RuntimeException("TodoItem itemId=$itemId not found")
Loading

0 comments on commit c193ce6

Please # to comment.