Initial commit

This commit is contained in:
Sebastien Deleuze
2018-05-04 16:29:35 +02:00
commit fc5a6393da
32 changed files with 2238 additions and 0 deletions

28
.gitignore vendored Normal file
View File

@@ -0,0 +1,28 @@
/.gradle/
/build/
/out/
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/

BIN
.mvn/wrapper/maven-wrapper.jar vendored Normal file

Binary file not shown.

1
.mvn/wrapper/maven-wrapper.properties vendored Normal file
View File

@@ -0,0 +1 @@
distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.3/apache-maven-3.5.3-bin.zip

897
README.adoc Normal file
View File

@@ -0,0 +1,897 @@
---
tags: [kotlin,rest,data]
projects: [spring-data-jpa,spring-framework,spring-boot]
---
:toc:
:icons: font
:source-highlighter: prettify
:project_id: spring-boot
:tabsize: 2
This guide shows you how to build efficiently a sample blog application by combining the power of https://projects.spring.io/spring-boot/[Spring Boot] and http://kotlinlang.org/[Kotlin].
If you are starting with Kotlin, you can learn the language by reading the https://kotlinlang.org/docs/reference/[reference documentation] and following the online https://try.kotlinlang.org[Kotlin Koans tutorial].
Spring Kotlin support is documented in https://docs.spring.io/spring/docs/current/spring-framework-reference/languages.html#kotlin[Spring Framework] and https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-kotlin.html[Spring Boot] reference documentation. If you need help, search or ask questions with https://stackoverflow.com/questions/tagged/kotlin+spring[`spring` and `kotlin` tags on StackOverflow] or come discuss in the `#spring` channel of http://slack.kotlinlang.org/[Kotlin Slack].
== Creating a New Project
First we need to create a Spring Boot application, which can be done in a number of ways. For the sake of example, we will use Gradle build system since this is the most popular in Kotlin ecosystem, but feel free to use Maven if you prefer it (a Maven pom.xml equivalent to the Gradle build https://github.com/spring-guides/tut-spring-boot-kotlin/blob/master/pom.xml[is available] as part of the sample blog project).
[[using-the-initializr-website]]
=== Using the Initializr Website
Go to http://start.spring.io and choose Kotlin language. You can also directly go to https://start.spring.io/#!language=kotlin in order to get Kotlin preselected.
Then choose Gradle build system, "blog" artifact, "blog" package name (in advanced settings) and also add "Web", "Mustache", "JPA" and "H2" dependencies as starting points, then click on "Generate Project".
image::https://github.com/spring-guides/tut-spring-boot-kotlin/raw/master/images/initializr.png[]
The .zip file contains a standard Gradle project in the root directory, so you might want to create an empty directory before you unpack it.
[[using-command-line]]
=== Using command line
You can use Initializr HTTP API https://docs.spring.io/initializr/docs/current/reference/htmlsingle/#command-line[from the command line] with, for example, curl on a UN*X like system:
[source]
----
$ mkdir blog && cd blog
$ curl https://start.spring.io/starter.zip -d type=gradle-project -d language=kotlin -d style=web,mustache,jpa,h2 -d packageName=blog -d name=Blog -o blog.zip
----
[[using-intellij-idea]]
=== Using IntelliJ IDEA
Spring Initializr is also integrated in IntelliJ IDEA Ultimate edition and allows you to create and import a new project without having to leave the IDE for the command-line or the web UI.
To access the wizard, go to File | New | Project, and select Spring Initializr.
Follow the steps of the wizard to use the following parameters:
- Package name: "blog"
- Artifact: "blog"
- Type: Gradle Project
- Language: Kotlin
- Name: "Blog"
- Dependencies: "Web", "Mustache", JPA" and "H2"
== Understanding the generated project
=== Gradle build
==== Plugins
In addition to the obvious https://kotlinlang.org/docs/reference/using-gradle.html[Kotlin Gradle plugin], the default configuration also declares the https://kotlinlang.org/docs/reference/compiler-plugins.html#spring-support[kotlin-spring plugin] which automatically opens classes and methods (unlike in Java, the defaut qualifier is `final` in Kotlin) annotated or meta-annotated with Spring annotations. This is useful to be able to create `@Configuration` or `@Transactional` beans without having to add the `open` qualifier required by CGLIB proxies for example.
`build.gradle`
[source,groovy]
----
buildscript {
ext {
kotlinVersion = '1.2.41'
springBootVersion = '2.0.1.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}")
}
}
apply plugin: 'kotlin'
apply plugin: 'kotlin-spring'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
----
==== Compiler options
One of Kotlin's key features is https://kotlinlang.org/docs/reference/null-safety.html[null-safety] - which cleanly deals with `null` values at compile time rather than bumping into the famous `NullPointerException` at runtime. This makes applications safer through nullability declarations and expressing "value or no value" semantics without paying the cost of wrappers like `Optional`. Note that Kotlin allows using functional constructs with nullable values; check out this http://www.baeldung.com/kotlin-null-safety[comprehensive guide to Kotlin null-safety].
Although Java does not allow one to express null-safety in its type-system, Spring Framework provides null-safety of the whole Spring Framework API via tooling-friendly annotations declared in the `org.springframework.lang` package. By default, types from Java APIs used in Kotlin are recognized as https://kotlinlang.org/docs/reference/java-interop.html#null-safety-and-platform-types[platform types] for which null-checks are relaxed. https://kotlinlang.org/docs/reference/java-interop.html#jsr-305-support[Kotlin support for JSR 305 annotations] + Spring nullability annotations provide null-safety for the whole Spring Framework API to Kotlin developers, with the advantage of dealing with `null` related issues at compile time.
This feature can be enabled by adding the `-Xjsr305` compiler flag with the `strict` options.
Notice also that Kotlin compiler is configured to generate Java 8 bytecode (Java 6 by default).
`build.gradle`
[source,groovy]
----
sourceCompatibility = 1.8
compileKotlin {
kotlinOptions {
freeCompilerArgs = ["-Xjsr305=strict"]
jvmTarget = "1.8"
}
}
compileTestKotlin {
kotlinOptions {
freeCompilerArgs = ["-Xjsr305=strict"]
jvmTarget = "1.8"
}
}
----
==== Dependencies
3 Kotlin specific libraries are required for such Spring Boot web application and configured by default:
- `kotlin-stdlib-jdk8` is the Java 8 variant of Kotlin standard library
- `kotlin-reflect` is Kotlin reflection library (mandatory as of Spring Framework 5)
- `jackson-module-kotlin` is almost mandatory to serialize/deserialize Kotlin classes
`build.gradle`
[source,groovy]
----
dependencies {
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.springframework.boot:spring-boot-starter-mustache')
compile('com.fasterxml.jackson.module:jackson-module-kotlin')
compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
compile("org.jetbrains.kotlin:kotlin-reflect")
testCompile('org.springframework.boot:spring-boot-starter-test')
}
----
Spring Boot Gradle plugin automatically uses the Kotlin version declared on the Kotlin Gradle plugin.
=== Application
`src/main/kotlin/blog/BlogApplication.kt`
[source,kotlin]
----
package blog
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class BlogApplication
fun main(args: Array<String>) {
runApplication<BlogApplication>(*args)
}
----
Compared to Java, you can notice the lack of semicolons, the lack of brackets on empty class (you can add some if you need to declare beans via `@Bean` annotation) and the use of `runApplication` top level function. `runApplication<BlogApplication>(*args)` is Kotlin idiomatic alternative to `SpringApplication.run(BlogApplication::class.java, *args)` and can be used to customize the application with following syntax.
`src/main/kotlin/blog/BlogApplication.kt`
[source,kotlin]
----
fun main(args: Array<String>) {
runApplication<BlogApplication>(*args) {
setBannerMode(Banner.Mode.OFF)
}
}
----
== Writing your first Kotlin controller
Let's create a simple controller to display a simple web page.
`src/main/kotlin/blog/HtmlController.kt`
[source,kotlin]
----
package blog.web
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.ui.set
import org.springframework.web.bind.annotation.GetMapping
@Controller
class HtmlController() {
@GetMapping("/")
fun blog(model: Model): String {
model["title"] = "Blog"
return "blog"
}
}
----
Notice that we are using here a https://kotlinlang.org/docs/reference/extensions.html[Kotlin extension] that allows to add Kotlin functions or operators to existing Spring types. Here we import the `org.springframework.ui.set` extension function in order to be able to write `model["title"] = "Blog"` instead of `model.addAttribute("title", "Blog")`.
We also need to create the associated Mustache templates.
`src/main/resources/templates/header.mustache`
[source]
----
<html>
<head>
<title>{{title}}</title>
</head>
<body>
----
`src/main/resources/templates/footer.mustache`
[source]
----
</body>
</html>
----
`src/main/resources/templates/blog.mustache`
[source]
----
{{> header}}
<h1>{{title}}</h1>
{{> footer}}
----
Start the web application by running the `main` function of `BlogApplication.kt`, and go to `http://localhost:8080/`, you should see a sober web page with a "Blog" headline.
== Testing with JUnit 5
While JUnit 4 is still the default testing framework provided with Spring Boot 2, JUnit 5 provides various features very handy with Kotlin, including https://docs.spring.io/spring/docs/current/spring-framework-reference/testing.html#testcontext-junit-jupiter-di[autowiring of contructor/method parameters] which allows to use non-nullable `val` properties and the possibility to use `@BeforeAll`/`@AfterAll` on regular non-static methods.
=== Switching from JUnit 4 to JUnit 5
First make sure you are using Gradle 4.6+ by running `./gradlew -version` in order to be able to leverage https://docs.gradle.org/4.6/release-notes.html#junit-5-support[native JUnit 5 support]. If you are using an older version, you can update it by running `./gradlew wrapper --gradle-version 4.7` for a more recent https://docs.gradle.org/current/release-notes.html[Gradle release].
Enable JUnit 5 support by adding the following line to your `build.gradle` file:
`build.gradle`
[source,groovy]
----
test {
useJUnitPlatform()
}
----
Then exclude `junit` from `spring-boot-starter-test` transitive dependencies and add `junit-jupiter-api` and `junit-jupiter-engine` ones.
`build.gradle`
[source,groovy]
----
dependencies {
testCompile('org.springframework.boot:spring-boot-starter-test') {
exclude module: 'junit'
}
testImplementation('org.junit.jupiter:junit-jupiter-api')
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine')
}
----
Refresh Gradle configuration, and open `BlogApplicationTests` to replace `@RunWith(SpringRunner::class)` by `@ExtendWith(SpringExtension::class)`.
`src/test/kotlin/blog/BlogApplicationTests.kt`
[source,kotlin]
----
@ExtendWith(SpringExtension::class)
@SpringBootTest
class BlogApplicationTests {
@Test
fun contextLoads() {
}
}
----
The test should run fine both in command line and in the IDE.
=== Writing JUnit 5 tests in Kotlin
For the sake of this example, let's create an integration test in order to demonstrate various features:
- We use real sentences between backticks instead of camel-case to provide expressive test function names
- JUnit 5 allows to inject constructor and method parameters, which is a good fit with Kotlin immutable and non-nullable properties
- This code leverages `getForObject` and `getForEntity` Kotlin extensions (you need to import them)
`src/test/kotlin/blog/IntegrationTests.kt`
[source,kotlin]
----
@ExtendWith(SpringExtension::class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class IntegrationTests(@Autowired val restTemplate: TestRestTemplate) {
@Test
fun `Assert blog page title, content and status code`() {
val entity = restTemplate.getForEntity<String>("/")
assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
assertThat(entity.body).contains("<h1>Blog</h1>", "reactor")
}
}
----
=== Test instance lifecycle
Sometimes you need to execute a method before or after all tests of a given class. Like Junit 4, JUnit 5 requires by default these methods to be static (which translates to https://kotlinlang.org/docs/reference/object-declarations.html#companion-objects[`companion object`] in Kotlin, which is quite verbose and not straightforward) because test classes are instantiated one time per test.
But Junit 5 allows you to change this default behavior and instantiate test classes one time per class. This can be done in https://junit.org/junit5/docs/current/user-guide/#writing-tests-test-instance-lifecycle[various ways], here we will use a property file to change the default behavior for the whole project:
`src/test/resources/junit-platform.properties`
[source,properties]
----
junit.jupiter.testinstance.lifecycle.default = per_class
----
With this configuration, we can now use `@BeforeAll` and `@AfterAll` annotations on regular methods like shown in updated version of `IntegrationTests` above.
`src/test/kotlin/blog/IntegrationTests.kt`
[source,kotlin]
----
@ExtendWith(SpringExtension::class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class IntegrationTests(@Autowired val restTemplate: TestRestTemplate) {
@BeforeAll
fun setup() {
println(">> Setup")
}
@Test
fun `Assert blog page title, content and status code`() {
println(">> Assert blog page title, content and status code")
val entity = restTemplate.getForEntity<String>("/")
assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
assertThat(entity.body).contains("<h1>Blog</h1>")
}
@Test
fun `Assert article page title, content and status code`() {
println(">> TODO")
}
@AfterAll
fun teardown() {
println(">> Tear down")
}
}
----
== Persistence with JPA
In order to be able to use Kotlin immutable classes, we need to enable https://kotlinlang.org/docs/reference/compiler-plugins.html#jpa-support[Kotlin JPA plugin]. It will generate no-arg constructors for any class annotated with `@Entity`, `@MappedSuperclass` or `@Embeddable`.
`build.gradle`
[source,groovy]
----
buildscript {
dependencies {
classpath("org.jetbrains.kotlin:kotlin-noarg:${kotlinVersion}")
}
}
apply plugin: 'kotlin-jpa'
----
Then we create our model by using Kotlin https://kotlinlang.org/docs/reference/data-classes.html[data classes] which are designed to hold data and automatically provide `equals()`, `hashCode()`, `toString()`, `componentN()` functions and `copy()`.
`src/main/kotlin/blog/Model.kt`
[source,kotlin]
----
@Entity
data class Article(
val title: String,
val headline: String,
val content: String,
@ManyToOne @JoinColumn val author: User,
@Id @GeneratedValue val id: Long? = null,
val addedAt: LocalDateTime = LocalDateTime.now())
@Entity
data class User(
@Id val login: String,
val firstname: String,
val lastname: String,
val description: String? = null)
----
Optional parameters with default values are defined at the last position in order to make in possible to omit them when using positional arguments (Kotlin also supports https://kotlinlang.org/docs/reference/functions.html#named-arguments[named arguments]). Notice that in Kotlin it is not unusual to group concise class declarations in the same file.
We also declares our Spring Data JPA repositories as following.
`src/main/kotlin/blog/Repositories.kt`
[source,kotlin]
----
interface ArticleRepository : CrudRepository<Article, Long> {
fun findAllByOrderByAddedAtDesc(): Iterable<Article>
}
interface UserRepository : CrudRepository<User, String>
----
And we write JPA tests to check basic use case works as expected.
`src/test/kotlin/blog/RepositoriesTests.kt`
[source,kotlin]
----
@ExtendWith(SpringExtension::class)
@DataJpaTest
class RepositoriesTests(@Autowired val entityManager: TestEntityManager,
@Autowired val userRepository: UserRepository,
@Autowired val articleRepository: ArticleRepository) {
@Test
fun `When findById then return Article`() {
val juergen = User("springjuergen", "Juergen", "Hoeller")
entityManager.persist(juergen)
val article = Article("Spring Framework 5.0 goes GA", "Dear Spring community ...", "Lorem ipsum", juergen)
entityManager.persist(article)
entityManager.flush()
val found = articleRepository.findById(article.id!!)
assertThat(found.get()).isEqualTo(article)
}
@Test
fun `When findById then return User`() {
val juergen = User("springjuergen", "Juergen", "Hoeller")
entityManager.persist(juergen)
entityManager.flush()
val found = userRepository.findById(juergen.login)
assertThat(found.get()).isEqualTo(juergen)
}
}
----
== Creating your own extensions
Instead of using util classes with abstract methods like in Java, it is usual in Kotlin to provide such functionalities via Kotlin extensions. Here we are going to add a `format()` function to the existing `LocalDateTime` type in order to generate text with the english date format.
`src/main/kotlin/blog/Extensions.kt`
[source,kotlin]
----
fun LocalDateTime.format() = this.format(englishDateFormatter)
private val daysLookup = (1..31).associate { it.toLong() to getOrdinal(it) }
private val englishDateFormatter = DateTimeFormatterBuilder()
.appendPattern("MMMM")
.appendLiteral(" ")
.appendText(ChronoField.DAY_OF_MONTH, daysLookup)
.appendLiteral(" ")
.appendPattern("yyyy")
.toFormatter(Locale.ENGLISH)
private fun getOrdinal(n: Int) = when {
n in 11..13 -> "${n}th"
n % 10 == 1 -> "${n}st"
n % 10 == 2 -> "${n}nd"
n % 10 == 3 -> "${n}rd"
else -> "${n}th"
}
----
we will leverage this extension in the next section.
== Implementing the blog engine
The blog engine we are implementing needs to render Markdown to HTML, and we are going to use `commonmark` library for that purpose.
`build.gradle`
[source,groovy]
----
dependencies {
compile("com.atlassian.commonmark:commonmark:0.11.0")
compile("com.atlassian.commonmark:commonmark-ext-autolink:0.11.0")
}
----
We introduce a `MarkdownConverter` bean, which leverages https://kotlinlang.org/docs/reference/lambdas.html#function-types[Kotlin function type].
`src/main/kotlin/blog/MarkdownConverter.kt`
[source,kotlin]
----
@Service
class MarkdownConverter : (String?) -> String {
private val parser = Parser.builder().extensions(Arrays.asList(AutolinkExtension.create())).build()
private val renderer = HtmlRenderer.builder().build()
override fun invoke(input: String?): String {
if (input == null || input == "") {
return ""
}
return renderer.render(parser.parse(input))
}
}
----
And we provide a custom `Mustache.Compiler` bean to be able to render HTML.
`src/main/kotlin/blog/BlogApplication.kt`
[source,kotlin]
----
@SpringBootApplication
class BlogApplication {
@Bean
fun mustacheCompiler(loader: Mustache.TemplateLoader?) =
Mustache.compiler().escapeHTML(false).withLoader(loader)
}
----
The nullable `Mustache.TemplateLoader?` means that it is an optional bean (in order to avoid failure when running JPA-only tests).
We update the "blog" Mustache templates.
`src/main/resources/templates/blog.mustache`
[source]
----
{{> header}}
<div class="articles">
{{#articles}}
<section>
<header class="article-header">
<h2 class="article-title"><a href="/article/{{slug}}">{{title}}</a></h2>
<div class="article-meta">By <strong>{{author.firstname}}</strong>, on <strong>{{addedAt}}</strong></div>
</header>
<div class="article-description">
{{headline}}
</div>
</section>
{{/articles}}
</div>
{{> footer}}
----
And we create an "article new one".
`src/main/resources/templates/blog.mustache`
[source]
----
{{> header}}
<section class="article">
<header class="article-header">
<h1 class="article-title">{{article.title}}</h1>
<p class="article-meta">By <strong>{{article.author.firstname}}</strong>, on <strong>{{article.addedAt}}</strong></p>
</header>
<div class="article-description">
{{article.headline}}
{{article.content}}
</div>
</section>
{{> footer}}
----
We update the `HtmlController` in order to render blog and article pages with rendered markdown and formatted date.
`src/main/kotlin/blog/HtmlController.kt`
[source,kotlin]
----
@Controller
class HtmlController(private val repository: ArticleRepository,
private val markdownConverter: MarkdownConverter) {
@GetMapping("/")
fun blog(model: Model): String {
model["title"] = properties.title
model["banner"] = properties.banner
model["articles"] = repository.findAllByOrderByAddedAtDesc().map { it.render() }
return "blog"
}
@GetMapping("/article/{id}")
fun article(@PathVariable id: Long, model: Model): String {
val article = repository
.findById(id)
.orElseThrow { IllegalArgumentException("Wrong article slug provided") }
.render()
model["title"] = article.title
model["article"] = article
return "article"
}
fun Article.render() = RenderedArticle(
title,
markdownConverter.invoke(headline),
markdownConverter.invoke(content),
author,
id,
addedAt.format()
)
data class RenderedArticle(
val title: String,
val headline: String,
val content: String,
val author: User,
val id: Long?,
val addedAt: String)
}
----
We add data initialization to `BlogApplication`.
`src/main/kotlin/blog/BlogApplication.kt`
[source,kotlin]
----
@Bean
fun databaseInitializer(userRepository: UserRepository, articleRepository: ArticleRepository) = CommandLineRunner {
val smaldlini = User("smaldini", "Stéphane", "Maldini")
userRepository.save(smaldlini)
articleRepository.save(Article(
"Reactor Bismuth is out",
"Lorem ipsum",
"dolor **sit** amet https://projectreactor.io/",
smaldlini,
1
))
articleRepository.save(Article(
"Reactor Aluminium has landed",
"Lorem ipsum",
"dolor **sit** amet https://projectreactor.io/",
smaldlini,
2
))
}
----
And we also update the integration tests accordingly.
`src/test/kotlin/blog/IntegrationTests.kt`
[source,kotlin]
----
@ExtendWith(SpringExtension::class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class IntegrationTests(@Autowired val restTemplate: TestRestTemplate) {
@BeforeAll
fun setup() {
println(">> Setup")
}
@Test
fun `Assert blog page title, content and status code`() {
println(">> Assert blog page title, content and status code")
val entity = restTemplate.getForEntity<String>("/")
assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
assertThat(entity.body).contains("<h1>Blog</h1>", "Reactor")
}
@Test
fun `Assert article page title, content and status code`() {
println(">> Assert article page title, content and status code")
val entity = restTemplate.getForEntity<String>("/article/2")
assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
assertThat(entity.body).contains("<a href=\"https://projectreactor.io/\">https://projectreactor.io/</a>")
}
@AfterAll
fun teardown() {
println(">> Tear down")
}
}
----
Start (or restart) the web application, and go to `http://localhost:8080/`, you should see the list of articles with clickable links to see a specific article.
== Exposing HTTP API
We are now going to implement the HTTP API via `@RestController` annotated controllers.
`src/main/kotlin/blog/HttpApi.kt`
[source,kotlin]
----
@RestController
@RequestMapping("/api/article")
class ArticleController(private val repository: ArticleRepository,
private val markdownConverter: MarkdownConverter) {
@GetMapping("/")
fun findAll() = repository.findAllByOrderByAddedAtDesc()
@GetMapping("/{id}")
fun findOne(@PathVariable id: Long, @RequestParam converter: String?) = when (converter) {
"markdown" -> repository.findById(id).map { it.copy(
headline = markdownConverter.invoke(it.headline),
content = markdownConverter.invoke(it.content)) }
null -> repository.findById(id)
else -> throw IllegalArgumentException("Only markdown converter is supported")
}
}
@RestController
@RequestMapping("/api/user")
class UserController(private val repository: UserRepository) {
@GetMapping("/")
fun findAll() = repository.findAll()
@GetMapping("/{login}")
fun findOne(@PathVariable login: String) = repository.findById(login)
}
----
For tests, instead of integration tests we choose to leverage `@WebMvcTest` and `@MockBean` to test only the web layer.
`src/test/kotlin/blog/HttpApiTests.kt`
[source,kotlin]
----
@ExtendWith(SpringExtension::class)
@WebMvcTest
class HttpApiTests(@Autowired val mockMvc: MockMvc) {
@MockBean
private lateinit var userRepository: UserRepository
@MockBean
private lateinit var articleRepository: ArticleRepository
@MockBean
private lateinit var markdownConverter: MarkdownConverter
@Test
fun `List articles`() {
val juergen = User("springjuergen", "Juergen", "Hoeller")
val spring5Article = Article("Spring Framework 5.0 goes GA", "Dear Spring community ...", "Lorem ipsum", juergen, 1)
val spring43Article = Article("Spring Framework 4.3 goes GA", "Dear Spring community ...", "Lorem ipsum", juergen, 2)
whenever(articleRepository.findAllByOrderByAddedAtDesc()).thenReturn(listOf(spring5Article, spring43Article))
whenever(markdownConverter.invoke(any())).thenAnswer { it.arguments[0] }
mockMvc.perform(get("/api/article/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk)
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(jsonPath("\$.[0].author.login").value(juergen.login))
.andExpect(jsonPath("\$.[0].id").value(spring5Article.id!!))
.andExpect(jsonPath("\$.[1].author.login").value(juergen.login))
.andExpect(jsonPath("\$.[1].id").value(spring43Article.id!!))
}
@Test
fun `List users`() {
val juergen = User("springjuergen", "Juergen", "Hoeller")
val smaldlini = User("smaldini", "Stéphane", "Maldini")
whenever(userRepository.findAll()).thenReturn(listOf(juergen, smaldlini))
mockMvc.perform(get("/api/user/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk)
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(jsonPath("\$.[0].login").value(juergen.login))
.andExpect(jsonPath("\$.[1].login").value(smaldlini.login))
}
}
----
Notice that `when` is a reserved Kotlin keyword, that's why we choose to use https://github.com/nhaarman/mockito-kotlin/[mockito-kotlin] library which provides a `whenever` alias (using escaped `{backtick}when{backtick}`). In order to use it, add following dependency.
`build.gradle`
[source,groovy]
----
dependencies {
testCompile("com.nhaarman:mockito-kotlin:1.5.0")
}
----
`$` also needs to be escaped in strings as it is used for string interpolation. There is https://github.com/spring-projects/spring-boot/issues/13113[not yet] `@MockBean` JUnit 5 parameter resolver, so we need to use `lateinit var` for now.
== Configuration properties
The recommended way to manage your application properties is to leverage `@ConfigurationProperties`. Immutable properties are https://github.com/spring-projects/spring-boot/issues/8762[not yet supported], but you can use `lateinit var` when you need to deal with non-nullable properties.
`src/main/kotlin/blog/BlogProperties.kt`
[source,kotlin]
----
@ConfigurationProperties("blog")
class BlogProperties {
lateinit var title: String
val banner = Banner()
class Banner {
var title: String? = null
lateinit var content: String
}
}
----
Then we enable it at `BlogApplication` level.
`src/main/kotlin/blog/BlogApplication.kt`
[source,kotlin]
----
@SpringBootApplication
@EnableConfigurationProperties(BlogProperties::class)
class BlogApplication {
// ...
}
----
To generate https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#configuration-metadata-annotation-processor[your own metadata] in order to get these custom properties recognized by your IDE, https://kotlinlang.org/docs/reference/kapt.html[kapt should be configured] with the `spring-boot-configuration-processor` dependency as following.
`build.gradle`
[source,groovy]
----
apply plugin: 'kotlin-kapt'
dependencies {
kapt("org.springframework.boot:spring-boot-configuration-processor")
}
----
In IntelliJ IDEA:
- Make sure Spring Boot plugin in enabled in menu File | Settings | Plugins | Spring Boot
- Enable annotation processing via menu File | Settings | Build, Execution, Deployement | Compiler | Annotation Processors | Enable annotation processing
- Since https://youtrack.jetbrains.com/issue/KT-15040[Kapt is not yet integrated in IDEA], you need to run manually the command `./gradlew kaptKotlin` to generate the metadata
Your custom properties should now be recognized when editing `application.properties` (autocomplete, validation, etc.).
`src/main/resources/application.properties`
[source,properties]
----
blog.title=Blog
blog.banner.title=Warning
blog.banner.content=The blog will be down tomorrow.
----
Edit the template and the controller accordingly.
`src/main/resources/templates/blog.mustache`
[source]
----
{{> header}}
<div class="articles">
{{#banner.title}}
<section>
<header class="banner">
<h2 class="banner-title">{{banner.title}}</h2>
</header>
<div class="banner-content">
{{banner.content}}
</div>
</section>
{{/banner.title}}
...
</div>
{{> footer}}
----
`src/main/kotlin/blog/HtmlController.kt`
[source,kotlin]
----
@Controller
class HtmlController(private val repository: ArticleRepository,
private val markdownConverter: MarkdownConverter,
private val properties: BlogProperties) {
@GetMapping("/")
fun blog(model: Model): String {
model["title"] = properties.title
model["banner"] = properties.banner
model["articles"] = repository.findAllByOrderByAddedAtDesc().map { it.render() }
return "blog"
}
// ...
}
----
Restart the web application, refresh `http://localhost:8080/`, you should see the banner on the blog homepage.

66
build.gradle Normal file
View File

@@ -0,0 +1,66 @@
buildscript {
ext {
kotlinVersion = '1.2.41'
springBootVersion = '2.0.1.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}")
classpath("org.jetbrains.kotlin:kotlin-noarg:${kotlinVersion}")
}
}
apply plugin: 'kotlin'
apply plugin: 'kotlin-spring'
apply plugin: 'kotlin-jpa'
apply plugin: 'kotlin-kapt'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
compileKotlin {
kotlinOptions {
freeCompilerArgs = ["-Xjsr305=strict"]
jvmTarget = "1.8"
}
}
compileTestKotlin {
kotlinOptions {
freeCompilerArgs = ["-Xjsr305=strict"]
jvmTarget = "1.8"
}
}
repositories {
mavenCentral()
}
test {
useJUnitPlatform()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.springframework.boot:spring-boot-starter-mustache')
compile('com.fasterxml.jackson.module:jackson-module-kotlin')
compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
compile("org.jetbrains.kotlin:kotlin-reflect")
compile("com.atlassian.commonmark:commonmark:0.11.0")
compile("com.atlassian.commonmark:commonmark-ext-autolink:0.11.0")
runtime("com.h2database:h2")
kapt("org.springframework.boot:spring-boot-configuration-processor")
testCompile('org.springframework.boot:spring-boot-starter-test') {
exclude module: 'junit'
}
testCompile("com.nhaarman:mockito-kotlin:1.5.0")
testImplementation('org.junit.jupiter:junit-jupiter-api')
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine')
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

172
gradlew vendored Executable file
View File

@@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

84
gradlew.bat vendored Normal file
View File

@@ -0,0 +1,84 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

BIN
images/initializr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

225
mvnw vendored Executable file
View File

@@ -0,0 +1,225 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Maven2 Start Up Batch script
#
# Required ENV vars:
# ------------------
# JAVA_HOME - location of a JDK home dir
#
# Optional ENV vars
# -----------------
# M2_HOME - location of maven2's installed home dir
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
# e.g. to debug Maven itself, use
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
# ----------------------------------------------------------------------------
if [ -z "$MAVEN_SKIP_RC" ] ; then
if [ -f /etc/mavenrc ] ; then
. /etc/mavenrc
fi
if [ -f "$HOME/.mavenrc" ] ; then
. "$HOME/.mavenrc"
fi
fi
# OS specific support. $var _must_ be set to either true or false.
cygwin=false;
darwin=false;
mingw=false
case "`uname`" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
if [ -z "$JAVA_HOME" ]; then
if [ -x "/usr/libexec/java_home" ]; then
export JAVA_HOME="`/usr/libexec/java_home`"
else
export JAVA_HOME="/Library/Java/Home"
fi
fi
;;
esac
if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
JAVA_HOME=`java-config --jre-home`
fi
fi
if [ -z "$M2_HOME" ] ; then
## resolve links - $0 may be a link to maven's home
PRG="$0"
# need this for relative symlinks
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG="`dirname "$PRG"`/$link"
fi
done
saveddir=`pwd`
M2_HOME=`dirname "$PRG"`/..
# make it fully qualified
M2_HOME=`cd "$M2_HOME" && pwd`
cd "$saveddir"
# echo Using m2 at $M2_HOME
fi
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin ; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --unix "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
fi
# For Migwn, ensure paths are in UNIX format before anything is touched
if $mingw ; then
[ -n "$M2_HOME" ] &&
M2_HOME="`(cd "$M2_HOME"; pwd)`"
[ -n "$JAVA_HOME" ] &&
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
# TODO classpath?
fi
if [ -z "$JAVA_HOME" ]; then
javaExecutable="`which javac`"
if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
# readlink(1) is not available as standard on Solaris 10.
readLink=`which readlink`
if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
if $darwin ; then
javaHome="`dirname \"$javaExecutable\"`"
javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
else
javaExecutable="`readlink -f \"$javaExecutable\"`"
fi
javaHome="`dirname \"$javaExecutable\"`"
javaHome=`expr "$javaHome" : '\(.*\)/bin'`
JAVA_HOME="$javaHome"
export JAVA_HOME
fi
fi
fi
if [ -z "$JAVACMD" ] ; then
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
else
JAVACMD="`which java`"
fi
fi
if [ ! -x "$JAVACMD" ] ; then
echo "Error: JAVA_HOME is not defined correctly." >&2
echo " We cannot execute $JAVACMD" >&2
exit 1
fi
if [ -z "$JAVA_HOME" ] ; then
echo "Warning: JAVA_HOME environment variable is not set."
fi
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
if [ -z "$1" ]
then
echo "Path not specified to find_maven_basedir"
return 1
fi
basedir="$1"
wdir="$1"
while [ "$wdir" != '/' ] ; do
if [ -d "$wdir"/.mvn ] ; then
basedir=$wdir
break
fi
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
if [ -d "${wdir}" ]; then
wdir=`cd "$wdir/.."; pwd`
fi
# end of workaround
done
echo "${basedir}"
}
# concatenates all lines of a file
concat_lines() {
if [ -f "$1" ]; then
echo "$(tr -s '\n' ' ' < "$1")"
fi
}
BASE_DIR=`find_maven_basedir "$(pwd)"`
if [ -z "$BASE_DIR" ]; then
exit 1;
fi
export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
echo $MAVEN_PROJECTBASEDIR
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --path --windows "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
fi
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
exec "$JAVACMD" \
$MAVEN_OPTS \
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
"-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

143
mvnw.cmd vendored Normal file
View File

@@ -0,0 +1,143 @@
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Maven2 Start Up Batch script
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@REM
@REM Optional ENV vars
@REM M2_HOME - location of maven2's installed home dir
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
@REM ----------------------------------------------------------------------------
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
@REM set %HOME% to equivalent of $HOME
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
@REM Execute a user defined script before this one
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
:skipRcPre
@setlocal
set ERROR_CODE=0
@REM To isolate internal variables from possible post scripts, we use another setlocal
@setlocal
@REM ==== START VALIDATION ====
if not "%JAVA_HOME%" == "" goto OkJHome
echo.
echo Error: JAVA_HOME not found in your environment. >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
:OkJHome
if exist "%JAVA_HOME%\bin\java.exe" goto init
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
echo JAVA_HOME = "%JAVA_HOME%" >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
@REM ==== END VALIDATION ====
:init
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
set EXEC_DIR=%CD%
set WDIR=%EXEC_DIR%
:findBaseDir
IF EXIST "%WDIR%"\.mvn goto baseDirFound
cd ..
IF "%WDIR%"=="%CD%" goto baseDirNotFound
set WDIR=%CD%
goto findBaseDir
:baseDirFound
set MAVEN_PROJECTBASEDIR=%WDIR%
cd "%EXEC_DIR%"
goto endDetectBaseDir
:baseDirNotFound
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
cd "%EXEC_DIR%"
:endDetectBaseDir
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
@setlocal EnableExtensions EnableDelayedExpansion
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
if ERRORLEVEL 1 goto error
goto end
:error
set ERROR_CODE=1
:end
@endlocal & set ERROR_CODE=%ERROR_CODE%
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
@REM check for post script, once with legacy .bat ending and once with .cmd ending
if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
:skipRcPost
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
if "%MAVEN_BATCH_PAUSE%" == "on" pause
if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
exit /B %ERROR_CODE%

159
pom.xml Normal file
View File

@@ -0,0 +1,159 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>blog</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>blog</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<kotlin.version>1.2.41</kotlin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mustache</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
</dependency>
<dependency>
<groupId>com.atlassian.commonmark</groupId>
<artifactId>commonmark</artifactId>
<version>0.11.0</version>
</dependency>
<dependency>
<groupId>com.atlassian.commonmark</groupId>
<artifactId>commonmark-ext-autolink</artifactId>
<version>0.11.0</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>junit</artifactId>
<groupId>junit</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.nhaarman</groupId>
<artifactId>mockito-kotlin</artifactId>
<version>1.5.0</version>
</dependency>
</dependencies>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<configuration>
<args>
<arg>-Xjsr305=strict</arg>
</args>
<compilerPlugins>
<plugin>spring</plugin>
<plugin>jpa</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-noarg</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>kapt</id>
<goals>
<goal>kapt</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>src/main/kotlin</sourceDir>
</sourceDirs>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>${project.parent.version}</version>
</annotationProcessorPath>
</annotationProcessorPaths>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<dependencies>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-surefire-provider</artifactId>
<version>${junit-platform.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>

1
settings.gradle Normal file
View File

@@ -0,0 +1 @@
rootProject.name = 'demo'

View File

@@ -0,0 +1,44 @@
package blog
import com.samskivert.mustache.Mustache
import org.springframework.boot.CommandLineRunner
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
@SpringBootApplication
@EnableConfigurationProperties(BlogProperties::class)
class BlogApplication {
@Bean
fun mustacheCompiler(loader: Mustache.TemplateLoader?) =
Mustache.compiler().escapeHTML(false).withLoader(loader)
@Bean
fun databaseInitializer(userRepository: UserRepository, articleRepository: ArticleRepository) = CommandLineRunner {
val smaldlini = User("smaldini", "Stéphane", "Maldini")
userRepository.save(smaldlini)
articleRepository.save(Article(
"Reactor Bismuth is out",
"Lorem ipsum",
"dolor **sit** amet https://projectreactor.io/",
smaldlini,
1
))
articleRepository.save(Article(
"Reactor Aluminium has landed",
"Lorem ipsum",
"dolor **sit** amet https://projectreactor.io/",
smaldlini,
2
))
}
}
fun main(args: Array<String>) {
runApplication<BlogApplication>(*args)
}

View File

@@ -0,0 +1,16 @@
package blog
import org.springframework.boot.context.properties.ConfigurationProperties
// TODO Use "val" instead of "lateinit var" when spring-boot#8762 will be fixed
@ConfigurationProperties("blog")
class BlogProperties {
lateinit var title: String
val banner = Banner()
class Banner {
var title: String? = null
lateinit var content: String
}
}

View File

@@ -0,0 +1,26 @@
package blog
import java.time.LocalDateTime
import java.time.format.DateTimeFormatterBuilder
import java.time.temporal.ChronoField
import java.util.*
fun LocalDateTime.format() = this.format(englishDateFormatter)
private val daysLookup = (1..31).associate { it.toLong() to getOrdinal(it) }
private val englishDateFormatter = DateTimeFormatterBuilder()
.appendPattern("MMMM")
.appendLiteral(" ")
.appendText(ChronoField.DAY_OF_MONTH, daysLookup)
.appendLiteral(" ")
.appendPattern("yyyy")
.toFormatter(Locale.ENGLISH)
private fun getOrdinal(n: Int) = when {
n in 11..13 -> "${n}th"
n % 10 == 1 -> "${n}st"
n % 10 == 2 -> "${n}nd"
n % 10 == 3 -> "${n}rd"
else -> "${n}th"
}

View File

@@ -0,0 +1,52 @@
package blog
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.ui.set
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
@Controller
class HtmlController(private val repository: ArticleRepository,
private val markdownConverter: MarkdownConverter,
private val properties: BlogProperties) {
@GetMapping("/")
fun blog(model: Model): String {
model["title"] = properties.title
model["banner"] = properties.banner
model["articles"] = repository.findAllByOrderByAddedAtDesc().map { it.render() }
return "blog"
}
@GetMapping("/article/{id}")
fun article(@PathVariable id: Long, model: Model): String {
val article = repository
.findById(id)
.orElseThrow { IllegalArgumentException("Wrong article slug provided") }
.render()
model["title"] = article.title
model["article"] = article
return "article"
}
fun Article.render() = RenderedArticle(
title,
markdownConverter.invoke(headline),
markdownConverter.invoke(content),
author,
id,
addedAt.format()
)
data class RenderedArticle(
val title: String,
val headline: String,
val content: String,
val author: User,
val id: Long?,
val addedAt: String)
}

View File

@@ -0,0 +1,32 @@
package blog
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/api/article")
class ArticleController(private val repository: ArticleRepository,
private val markdownConverter: MarkdownConverter) {
@GetMapping("/")
fun findAll() = repository.findAllByOrderByAddedAtDesc()
@GetMapping("/{id}")
fun findOne(@PathVariable id: Long, @RequestParam converter: String?) = when (converter) {
"markdown" -> repository.findById(id).map { it.copy(
headline = markdownConverter.invoke(it.headline),
content = markdownConverter.invoke(it.content)) }
null -> repository.findById(id)
else -> throw IllegalArgumentException("Only markdown converter is supported")
}
}
@RestController
@RequestMapping("/api/user")
class UserController(private val repository: UserRepository) {
@GetMapping("/")
fun findAll() = repository.findAll()
@GetMapping("/{login}")
fun findOne(@PathVariable login: String) = repository.findById(login)
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2002-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package blog
import org.commonmark.ext.autolink.AutolinkExtension
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import org.springframework.stereotype.Service
@Service
class MarkdownConverter : (String?) -> String {
private val parser = Parser.builder().extensions(listOf(AutolinkExtension.create())).build()
private val renderer = HtmlRenderer.builder().build()
override fun invoke(input: String?): String {
if (input == null || input == "") {
return ""
}
return renderer.render(parser.parse(input))
}
}

View File

@@ -0,0 +1,20 @@
package blog
import java.time.LocalDateTime
import javax.persistence.*
@Entity
data class Article(
val title: String,
val headline: String,
val content: String,
@ManyToOne @JoinColumn val author: User,
@Id @GeneratedValue val id: Long? = null, // At the end to make it optional
val addedAt: LocalDateTime = LocalDateTime.now())
@Entity
data class User(
@Id val login: String,
val firstname: String,
val lastname: String,
val description: String? = null)

View File

@@ -0,0 +1,11 @@
package blog
import org.springframework.data.repository.CrudRepository
// @Repository is not required
interface ArticleRepository : CrudRepository<Article, Long> {
fun findAllByOrderByAddedAtDesc(): Iterable<Article>
}
// @Repository is not required
interface UserRepository : CrudRepository<User, String>

View File

@@ -0,0 +1,3 @@
blog.title=Blog
blog.banner.title=Warning
blog.banner.content=The blog will be down tomorrow.

View File

@@ -0,0 +1,16 @@
{{> header}}
<section class="article">
<header class="article-header">
<h1 class="article-title">{{article.title}}</h1>
<p class="article-meta">By <strong>{{article.author.firstname}}</strong>, on <strong>{{article.addedAt}}</strong></p>
</header>
<div class="article-description">
{{article.headline}}
{{article.content}}
</div>
</section>
{{> footer}}

View File

@@ -0,0 +1,29 @@
{{> header}}
<div class="articles">
{{#banner.title}}
<section>
<header class="banner">
<h2 class="banner-title">{{banner.title}}</h2>
</header>
<div class="banner-content">
{{banner.content}}
</div>
</section>
{{/banner.title}}
{{#articles}}
<section>
<header class="article-header">
<h2 class="article-title"><a href="/article/{{id}}">{{title}}</a></h2>
<div class="article-meta">By <strong>{{author.firstname}}</strong>, on <strong>{{addedAt}}</strong></div>
</header>
<div class="article-headline">
{{headline}}
</div>
</section>
{{/articles}}
</div>
{{> footer}}

View File

@@ -0,0 +1,2 @@
</body>
</html>

View File

@@ -0,0 +1,6 @@
<html>
<head>
<title>{{title}}</title>
</head>
<body>
<h1>{{title}}</h1>

View File

@@ -0,0 +1,16 @@
package blog
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.junit.jupiter.SpringExtension
@ExtendWith(SpringExtension::class)
@SpringBootTest
class BlogApplicationTests {
@Test
fun contextLoads() {
}
}

View File

@@ -0,0 +1,59 @@
package blog
import com.nhaarman.mockito_kotlin.whenever
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.test.context.junit.jupiter.SpringExtension
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.mockito.Mockito.any
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
@ExtendWith(SpringExtension::class)
@WebMvcTest
// TODO Use constructor-based val property for @MockBean when supported, see issue spring-boot#13113
class HttpApiTests(@Autowired val mockMvc: MockMvc) {
@MockBean
private lateinit var userRepository: UserRepository
@MockBean
private lateinit var articleRepository: ArticleRepository
@MockBean
private lateinit var markdownConverter: MarkdownConverter
@Test
fun `List articles`() {
val juergen = User("springjuergen", "Juergen", "Hoeller")
val spring5Article = Article("Spring Framework 5.0 goes GA", "Dear Spring community ...", "Lorem ipsum", juergen, 1)
val spring43Article = Article("Spring Framework 4.3 goes GA", "Dear Spring community ...", "Lorem ipsum", juergen, 2)
whenever(articleRepository.findAllByOrderByAddedAtDesc()).thenReturn(listOf(spring5Article, spring43Article))
whenever(markdownConverter.invoke(any())).thenAnswer { it.arguments[0] }
mockMvc.perform(get("/api/article/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk)
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(jsonPath("\$.[0].author.login").value(juergen.login))
.andExpect(jsonPath("\$.[0].id").value(spring5Article.id!!))
.andExpect(jsonPath("\$.[1].author.login").value(juergen.login))
.andExpect(jsonPath("\$.[1].id").value(spring43Article.id!!))
}
@Test
fun `List users`() {
val juergen = User("springjuergen", "Juergen", "Hoeller")
val smaldlini = User("smaldini", "Stéphane", "Maldini")
whenever(userRepository.findAll()).thenReturn(listOf(juergen, smaldlini))
mockMvc.perform(get("/api/user/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk)
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(jsonPath("\$.[0].login").value(juergen.login))
.andExpect(jsonPath("\$.[1].login").value(smaldlini.login))
}
}

View File

@@ -0,0 +1,47 @@
package blog
import org.assertj.core.api.Assertions.*
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
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.test.web.client.getForEntity
import org.springframework.boot.test.web.client.getForObject
import org.springframework.http.HttpStatus
import org.springframework.test.context.junit.jupiter.SpringExtension
@ExtendWith(SpringExtension::class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class IntegrationTests(@Autowired val restTemplate: TestRestTemplate) {
@BeforeAll
fun setup() {
println(">> Setup")
}
@Test
fun `Assert blog page title, content and status code`() {
println(">> Assert blog page title, content and status code")
val entity = restTemplate.getForEntity<String>("/")
assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
assertThat(entity.body).contains("<h1>Blog</h1>", "Reactor")
}
@Test
fun `Assert article page title, content and status code`() {
println(">> Assert article page title, content and status code")
val entity = restTemplate.getForEntity<String>("/article/2")
assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
assertThat(entity.body).contains("<a href=\"https://projectreactor.io/\">https://projectreactor.io/</a>")
}
@AfterAll
fun teardown() {
println(">> Tear down")
}
}

View File

@@ -0,0 +1,41 @@
package blog
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager
import org.springframework.test.context.junit.jupiter.SpringExtension
@ExtendWith(SpringExtension::class)
@DataJpaTest
class RepositoriesTests(@Autowired val entityManager: TestEntityManager,
@Autowired val userRepository: UserRepository,
@Autowired val articleRepository: ArticleRepository) {
@Test
fun `When findById then return Article`() {
val juergen = User("springjuergen", "Juergen", "Hoeller")
entityManager.persist(juergen)
val article = Article("Spring Framework 5.0 goes GA", "Dear Spring community ...", "Lorem ipsum", juergen)
entityManager.persist(article)
entityManager.flush()
val found = articleRepository.findById(article.id!!)
assertThat(found.get()).isEqualTo(article)
}
@Test
fun `When findById then return User`() {
val juergen = User("springjuergen", "Juergen", "Hoeller")
entityManager.persist(juergen)
entityManager.flush()
val found = userRepository.findById(juergen.login)
assertThat(found.get()).isEqualTo(juergen)
}
}

View File

@@ -0,0 +1 @@
junit.jupiter.testinstance.lifecycle.default = per_class