diff --git a/pom.xml b/pom.xml
index 9242adcf..a15b4f46 100644
--- a/pom.xml
+++ b/pom.xml
@@ -80,6 +80,7 @@
1.7.25
5.0.11.RELEASE
2.0.0.BUILD-SNAPSHOT
+ 1.2.71
@@ -422,6 +423,26 @@
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+ ${kotlin.version}
+ true
+
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+ ${kotlin.version}
+ true
+
+
+ org.jetbrains.kotlin
+ kotlin-test
+ ${kotlin.version}
+ test
+
+
org.springframework
spring-aop
@@ -554,13 +575,6 @@
test
-
- xmlunit
- xmlunit
- 1.6
- test
-
-
net.jadler
jadler-all
@@ -663,6 +677,43 @@
false
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${kotlin.version}
+
+
+ compile
+
+ compile
+
+
+
+ ${project.basedir}/src/main/kotlin
+ ${project.basedir}/src/main/java
+
+
+
+
+ test-compile
+
+ test-compile
+
+
+
+ ${project.basedir}/src/test/kotlin
+ ${project.basedir}/src/test/java
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-deploy-plugin
+ 2.8.2
+
org.apache.maven.plugins
diff --git a/src/main/kotlin/org/springframework/hateoas/AffordanceBuilderDsl.kt b/src/main/kotlin/org/springframework/hateoas/AffordanceBuilderDsl.kt
new file mode 100644
index 00000000..8fd9e62c
--- /dev/null
+++ b/src/main/kotlin/org/springframework/hateoas/AffordanceBuilderDsl.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2002-2019 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 org.springframework.hateoas
+
+import org.springframework.hateoas.mvc.ControllerLinkBuilder
+import org.springframework.hateoas.mvc.ControllerLinkBuilder.afford
+import org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn
+
+/**
+ * Create a new [Link] with additional [Affordance]s.
+ *
+ * @author Roland Kulcsár
+ * @since 1.0
+ */
+inline infix fun Link.andAffordances(setup: AffordanceBuilderDsl.() -> Unit): Link {
+
+ val builder = AffordanceBuilderDsl()
+ builder.setup()
+
+ return andAffordances(builder.affordances)
+}
+
+/**
+ * Extract a [Link] from the [ControllerLinkBuilder] and look up the related [Affordance]. Should
+ * only be one.
+ *
+ * @author Roland Kulcsár
+ * @since 1.0
+ */
+inline fun afford(func: C.() -> Unit): Affordance = afford(methodOn(C::class.java).apply(func))
+
+/**
+ * Provide an [Affordance]s DSL to help write idiomatic Kotlin code.
+ *
+ * @author Roland Kulcsár
+ * @since 1.0
+ */
+open class AffordanceBuilderDsl(val affordances: MutableList = mutableListOf()) {
+
+ inline fun afford(func: C.() -> Any) {
+
+ val affordance = afford(methodOn(C::class.java).func())
+ affordances.add(affordance)
+ }
+}
diff --git a/src/main/kotlin/org/springframework/hateoas/LinkBuilderDsl.kt b/src/main/kotlin/org/springframework/hateoas/LinkBuilderDsl.kt
new file mode 100644
index 00000000..426a1d45
--- /dev/null
+++ b/src/main/kotlin/org/springframework/hateoas/LinkBuilderDsl.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2002-2019 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 org.springframework.hateoas
+
+import org.springframework.hateoas.mvc.ControllerLinkBuilder
+import org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo
+import org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn
+import kotlin.reflect.KClass
+
+/**
+ * Create a [ControllerLinkBuilder] pointing to a [func] method.
+ *
+ * @author Roland Kulcsár
+ * @since 1.0
+ */
+inline fun linkTo(func: C.() -> Unit): ControllerLinkBuilder = linkTo(methodOn(C::class.java).apply(func))
+
+/**
+ * Create a [Link] with the given [rel].
+ *
+ * @author Roland Kulcsár
+ * @since 1.0
+ */
+infix fun ControllerLinkBuilder.withRel(rel: String): Link = withRel(rel)
+
+/**
+ * Add [links] to the [R] resource.
+ *
+ * @author Roland Kulcsár
+ * @since 1.0
+ */
+fun R.add(controller: Class, links: LinkBuilderDsl.(R) -> Unit): R {
+
+ val builder = LinkBuilderDsl(controller, this)
+ builder.links(this)
+
+ return this
+}
+
+/**
+ * Add [links] to the [R] resource.
+ *
+ * @author Roland Kulcsár
+ * @since 1.0
+ */
+fun R.add(controller: KClass, links: LinkBuilderDsl.(R) -> Unit): R {
+ return add(controller.java, links)
+}
+
+/**
+ * Provide a [LinkBuilder] DSL to help write idiomatic Kotlin code.
+ *
+ * @author Roland Kulcsár
+ * @since 1.0
+ */
+open class LinkBuilderDsl(val controller: Class, val resource: R) {
+
+ /**
+ * Create a [ControllerLinkBuilder] pointing to [func] method.
+ */
+ fun linkTo(func: C.() -> R): ControllerLinkBuilder = linkTo(methodOn(controller).run(func))
+
+ /**
+ * Add a link with the given [rel] to the [resource].
+ */
+ infix fun ControllerLinkBuilder.withRel(rel: String): Link {
+
+ val link = withRel(rel)
+ resource.add(link)
+
+ return link
+ }
+}
diff --git a/src/test/kotlin/org/springframework/hateoas/AffordanceBuilderDslUnitTest.kt b/src/test/kotlin/org/springframework/hateoas/AffordanceBuilderDslUnitTest.kt
new file mode 100644
index 00000000..4aec1aa7
--- /dev/null
+++ b/src/test/kotlin/org/springframework/hateoas/AffordanceBuilderDslUnitTest.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2002-2019 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 org.springframework.hateoas
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+import org.springframework.http.HttpMethod
+import org.springframework.http.ResponseEntity
+import org.springframework.web.bind.annotation.*
+
+/**
+ * Unit tests for [LinkBuilderDsl] and [AffordanceBuilderDsl].
+ *
+ * @author Roland Kulcsár
+ */
+class AffordanceBuilderDslUnitTest : TestUtils() {
+
+ /**
+ * @see #715
+ */
+ @Test
+ fun `creates affordance to controller method`() {
+
+ val delete = afford { delete("15") }
+
+ assertThat(delete.httpMethod).isEqualTo(HttpMethod.DELETE)
+ assertThat(delete.name).isEqualTo("delete")
+ }
+
+ /**
+ * @see #715
+ */
+ @Test
+ fun `creates link to controller method with affordances`() {
+
+ val id = "15"
+ val self = linkTo { findById(id) } withRel Link.REL_SELF
+ val selfWithAffordances = self andAffordances {
+ afford { update(id, CustomerDTO("John Doe")) }
+ afford { delete(id) }
+ }
+
+ assertThat(selfWithAffordances.rel).isEqualTo(Link.REL_SELF)
+ assertThat(selfWithAffordances.href).isEqualTo("http://localhost/customers/15")
+
+ assertThat(selfWithAffordances.affordances).hasSize(3)
+ assertThat(selfWithAffordances.affordances[0].httpMethod).isEqualTo(HttpMethod.GET)
+ assertThat(selfWithAffordances.affordances[0].name).isEqualTo("findById")
+
+ assertThat(selfWithAffordances.affordances[1].httpMethod).isEqualTo(HttpMethod.PUT)
+ assertThat(selfWithAffordances.affordances[1].name).isEqualTo("update")
+
+ assertThat(selfWithAffordances.affordances[2].httpMethod).isEqualTo(HttpMethod.DELETE)
+ assertThat(selfWithAffordances.affordances[2].name).isEqualTo("delete")
+
+ assertThat(selfWithAffordances.hashCode()).isNotEqualTo(self.hashCode())
+ assertThat(selfWithAffordances).isNotEqualTo(self)
+ }
+
+ data class Customer(val id: String, val name: String)
+ data class CustomerDTO(val name: String)
+ open class CustomerResource(val id: String, val name: String) : ResourceSupport()
+ open class ProductResource(val id: String) : ResourceSupport()
+
+ @RequestMapping("/customers")
+ interface CustomerController {
+
+ @GetMapping("/{id}")
+ fun findById(@PathVariable id: String): ResponseEntity
+
+ @GetMapping("/{id}/products")
+ fun findProductsById(@PathVariable id: String): PagedResources
+
+ @PutMapping("/{id}")
+ fun update(@PathVariable id: String, @RequestBody customer: CustomerDTO): ResponseEntity
+
+ @DeleteMapping("/{id}")
+ fun delete(@PathVariable id: String): ResponseEntity
+ }
+}
diff --git a/src/test/kotlin/org/springframework/hateoas/LinkBuilderDslUnitTest.kt b/src/test/kotlin/org/springframework/hateoas/LinkBuilderDslUnitTest.kt
new file mode 100644
index 00000000..55198af0
--- /dev/null
+++ b/src/test/kotlin/org/springframework/hateoas/LinkBuilderDslUnitTest.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2002-2019 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 org.springframework.hateoas
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+import org.springframework.http.ResponseEntity
+import org.springframework.web.bind.annotation.*
+
+/**
+ * Unit tests for [LinkBuilderDsl] and [AffordanceBuilderDsl].
+ *
+ * @author Roland Kulcsár
+ */
+class LinkBuilderDslUnitTest : TestUtils() {
+
+ private val REL_PRODUCTS = "products"
+
+ /**
+ * @see #715
+ */
+ @Test
+ fun `creates link to controller method`() {
+
+ val self = linkTo { findById("15") } withRel Link.REL_SELF
+
+ assertPointsToMockServer(self)
+ assertThat(self.rel).isEqualTo(Link.REL_SELF)
+ assertThat(self.href).endsWith("/customers/15")
+ }
+
+ /**
+ * @see #715
+ */
+ @Test
+ fun `adds links to wrapped domain object`() {
+
+ val customer = Resource(Customer("15", "John Doe"))
+
+ customer.add(CustomerController::class) {
+ linkTo { findById(it.content.id) } withRel Link.REL_SELF
+ linkTo { findProductsById(it.content.id) } withRel REL_PRODUCTS
+ }
+
+ customer.links.forEach { assertPointsToMockServer(it) }
+ assertThat(customer.hasLink(Link.REL_SELF)).isTrue()
+ assertThat(customer.hasLink(REL_PRODUCTS)).isTrue()
+ }
+
+ /**
+ * @see #715
+ */
+ @Test
+ fun `adds links to resourcesupport object`() {
+
+ val customer = CustomerResource("15", "John Doe")
+
+ customer.add(CustomerController::class) {
+ linkTo { findById(it.id) } withRel Link.REL_SELF
+ linkTo { findProductsById(it.id) } withRel REL_PRODUCTS
+ }
+
+ customer.links.forEach { assertPointsToMockServer(it) }
+ assertThat(customer.hasLink(Link.REL_SELF)).isTrue()
+ assertThat(customer.hasLink(REL_PRODUCTS)).isTrue()
+ }
+
+ data class Customer(val id: String, val name: String)
+ data class CustomerDTO(val name: String)
+ open class CustomerResource(val id: String, val name: String) : ResourceSupport()
+ open class ProductResource(val id: String) : ResourceSupport()
+
+ @RequestMapping("/customers")
+ interface CustomerController {
+
+ @GetMapping("/{id}")
+ fun findById(@PathVariable id: String): ResponseEntity
+
+ @GetMapping("/{id}/products")
+ fun findProductsById(@PathVariable id: String): PagedResources
+
+ @PutMapping("/{id}")
+ fun update(@PathVariable id: String, @RequestBody customer: CustomerDTO): ResponseEntity
+
+ @DeleteMapping("/{id}")
+ fun delete(@PathVariable id: String): ResponseEntity
+ }
+}