Skip to content

Commit f3af119

Browse files
Initial fix for #10.
1 parent 876b5fe commit f3af119

File tree

10 files changed

+110
-6
lines changed

10 files changed

+110
-6
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ project(":spring-webflux-kotlin-coroutine") {
285285
testCompile "org.springframework.boot:spring-boot-autoconfigure:$spring_boot_version"
286286
testCompile "org.springframework.boot:spring-boot-starter-test:$spring_boot_version"
287287
testCompile "org.springframework.boot:spring-boot-starter-webflux:$spring_boot_version"
288+
testCompile "org.springframework.boot:spring-boot-starter-mustache:$spring_boot_version"
288289
testCompile "de.flapdoodle.embed:de.flapdoodle.embed.mongo:2.0.0"
289290
}
290291

spring-kotlin-coroutine/src/main/kotlin/org/springframework/kotlin/experimental/coroutine/EnableCoroutine.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,13 @@ import org.springframework.kotlin.experimental.coroutine.context.CoroutineConfig
2424
import org.springframework.kotlin.experimental.coroutine.context.CoroutineContextResolverConfiguration
2525
import org.springframework.kotlin.experimental.coroutine.context.CoroutineContexts
2626
import org.springframework.kotlin.experimental.coroutine.event.CoroutineEventSupportConfiguraton
27+
import org.springframework.kotlin.experimental.coroutine.reactive.CoroutineReactiveAdapterRegistryConfiguration
2728
import org.springframework.kotlin.experimental.coroutine.scheduler.CoroutineSchedulerConfiguration
2829

2930
@Target(AnnotationTarget.CLASS)
3031
@Retention(AnnotationRetention.RUNTIME)
3132
@MustBeDocumented
32-
@Import(CoroutineConfigurationSelector::class,
33+
@Import(CoroutineConfigurationSelector::class, CoroutineReactiveAdapterRegistryConfiguration::class,
3334
CoroutineContextResolverConfiguration::class, CoroutineContexts::class,
3435
CoroutineEventSupportConfiguraton::class, CoroutineCacheConfiguration::class,
3536
CoroutineSchedulerConfiguration::class)

spring-kotlin-coroutine/src/main/kotlin/org/springframework/kotlin/experimental/coroutine/SpringUtil.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
package org.springframework.kotlin.experimental.coroutine
22

3+
import kotlinx.coroutines.experimental.channels.Channel
4+
import kotlinx.coroutines.experimental.channels.ReceiveChannel
5+
import kotlinx.coroutines.experimental.reactive.asPublisher
6+
import kotlinx.coroutines.experimental.reactive.openSubscription
37
import org.springframework.context.annotation.Condition
48
import org.springframework.context.annotation.ConditionContext
59
import org.springframework.context.annotation.Conditional
10+
import org.springframework.core.ReactiveAdapterRegistry
11+
import org.springframework.core.ReactiveTypeDescriptor
612
import org.springframework.core.type.AnnotatedTypeMetadata
713
import java.lang.annotation.Inherited
814

@@ -26,4 +32,11 @@ internal open class OnClassCondition: Condition {
2632
} catch (e: ClassNotFoundException) {
2733
false
2834
}
35+
}
36+
37+
fun ReactiveAdapterRegistry.registerReceiveChannel() {
38+
registerReactiveType(
39+
ReactiveTypeDescriptor.multiValue(ReceiveChannel::class.java) {
40+
Channel<Any>().apply { close() }
41+
}, { (it as ReceiveChannel<*>).asPublisher() }, { it.openSubscription() })
2942
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.kotlin.experimental.coroutine.reactive
18+
19+
import org.springframework.beans.factory.InitializingBean
20+
import org.springframework.beans.factory.config.BeanDefinition
21+
import org.springframework.context.annotation.Bean
22+
import org.springframework.context.annotation.Configuration
23+
import org.springframework.context.annotation.Role
24+
import org.springframework.core.ReactiveAdapterRegistry
25+
import org.springframework.kotlin.experimental.coroutine.registerReceiveChannel
26+
27+
@Configuration
28+
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
29+
internal open class CoroutineReactiveAdapterRegistryConfiguration {
30+
@Bean
31+
open fun reactiveAdapterRegistryInit(): InitializingBean = InitializingBean {
32+
ReactiveAdapterRegistry.getSharedInstance().registerReceiveChannel()
33+
}
34+
}

spring-webflux-kotlin-coroutine/src/main/kotlin/org/springframework/kotlin/experimental/coroutine/web/CoroutinesWebFluxConfigurer.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import org.springframework.web.reactive.result.method.annotation.injectCustomCon
3535
import org.springframework.web.server.ServerWebExchange
3636
import reactor.core.publisher.Mono
3737
import java.util.concurrent.CompletableFuture
38+
import java.util.concurrent.atomic.AtomicReference
3839
import kotlin.coroutines.experimental.Continuation
3940
import kotlin.coroutines.experimental.CoroutineContext
4041
import kotlin.coroutines.experimental.EmptyCoroutineContext
@@ -57,7 +58,7 @@ open class CoroutinesWebFluxConfigurer(
5758
override fun resolveArgument(parameter: MethodParameter, bindingContext: BindingContext,
5859
exchange: ServerWebExchange): Mono<Any> =
5960
CompletableFutureContinuation().let {
60-
bindingContext.model.addAttribute("continuation", it)
61+
bindingContext.model.addAttribute("__continuation", AtomicReference(it))
6162

6263
Mono.just(it)
6364
}

spring-webflux-kotlin-coroutine/src/main/kotlin/org/springframework/web/reactive/result/method/annotation/CustomControllerMethodResolver.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import kotlinx.coroutines.experimental.reactive.asPublisher
2222
import org.reactivestreams.Publisher
2323
import org.springframework.context.ConfigurableApplicationContext
2424
import org.springframework.core.ReactiveAdapterRegistry
25-
import org.springframework.kotlin.experimental.coroutine.isCoroutineCollection
2625
import org.springframework.kotlin.experimental.coroutine.returnTypeMetadata
2726
import org.springframework.util.ClassUtils
2827
import org.springframework.web.method.HandlerMethod
@@ -31,12 +30,11 @@ import org.springframework.web.reactive.HandlerResult
3130
import org.springframework.web.reactive.result.method.InvocableHandlerMethod
3231
import org.springframework.web.reactive.result.method.SyncInvocableHandlerMethod
3332
import org.springframework.web.server.ServerWebExchange
34-
import reactor.core.publisher.Flux
3533
import reactor.core.publisher.Mono
3634
import java.lang.reflect.Proxy
3735
import java.util.concurrent.CompletableFuture
36+
import java.util.concurrent.atomic.AtomicReference
3837
import kotlin.coroutines.experimental.intrinsics.COROUTINE_SUSPENDED
39-
import kotlin.reflect.jvm.kotlinFunction
4038

4139
private class CustomControllerMethodResolver(
4240
private val delegate: ControllerMethodResolver
@@ -78,7 +76,7 @@ private class CoroutineInvocableHandlerMethod(private val handlerMethod: Handler
7876
super.invoke(exchange, bindingContext, *providedArgs)
7977
.map { result ->
8078
if (result.returnValue === COROUTINE_SUSPENDED) {
81-
val future = bindingContext.model.asMap()["continuation"] as CompletableFuture<*>
79+
val future = (bindingContext.model.asMap()["__continuation"] as AtomicReference<CompletableFuture<*>>).get()
8280
val metaData = handlerMethod.method.returnTypeMetadata
8381
val value = Mono.fromFuture(future).let { mono ->
8482
if (ClassUtils.isAssignable(ReceiveChannel::class.java, metaData.clazz)) {

spring-webflux-kotlin-coroutine/src/test/groovy/org/springframework/kotlin/experimental/coroutine/web/WebConfiguration.groovy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,9 @@ class WebConfiguration {
2323
CoroutineController coroutineController() {
2424
return new CoroutineController()
2525
}
26+
27+
@Bean
28+
HtmlController htmlController() {
29+
return new HtmlController()
30+
}
2631
}

spring-webflux-kotlin-coroutine/src/test/groovy/org/springframework/kotlin/experimental/coroutine/web/WebIntSpec.groovy

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import org.springframework.http.HttpStatus
2525
import org.springframework.kotlin.experimental.coroutine.IntSpecConfiguration
2626
import org.springframework.web.client.RestOperations
2727
import org.springframework.web.client.RestTemplate
28+
import spock.lang.Ignore
2829
import spock.lang.Specification
2930
import spock.lang.Unroll
3031

@@ -90,4 +91,23 @@ data:4"""
9091
result.statusCode == HttpStatus.OK
9192
result.body == "123456"
9293
}
94+
95+
def "should handle suspending controller functions which return view name"() {
96+
when:
97+
def result = restTemplate.getForEntity("http://localhost:$port/blog", String)
98+
99+
then:
100+
result.statusCode == HttpStatus.OK
101+
result.body.trim() == "<article><title>TestTitle</title><text>TestText</text></article>"
102+
}
103+
104+
@Ignore("Waiting for the fix in Spring 5")
105+
def "should handle suspending controller functions which return view name different from endpoint path"() {
106+
when:
107+
def result = restTemplate.getForEntity("http://localhost:$port/blogEndpoint", String)
108+
109+
then:
110+
result.statusCode == HttpStatus.OK
111+
result.body.trim() == "<article><title>TestTitle</title><text>TestText</text></article>"
112+
}
93113
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package org.springframework.kotlin.experimental.coroutine.web
2+
3+
import kotlinx.coroutines.experimental.channels.Channel
4+
import org.springframework.stereotype.Controller
5+
import org.springframework.ui.Model
6+
import org.springframework.ui.set
7+
import org.springframework.web.bind.annotation.GetMapping
8+
9+
@Controller
10+
open class HtmlController {
11+
@GetMapping("/blog")
12+
suspend fun blog(model: Model): String {
13+
model["articles"] = Channel<Article>(1).apply {
14+
send(Article("TestTitle", "TestText"))
15+
close()
16+
}
17+
18+
return "blog"
19+
}
20+
21+
@GetMapping("/blogEndpoint")
22+
suspend fun blogEndpoint(model: Model): String = blog(model)
23+
}
24+
25+
data class Article(
26+
val title: String,
27+
val text: String
28+
)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{{#articles}}
2+
<article><title>{{title}}</title><text>{{text}}</text></article>
3+
{{/articles}}

0 commit comments

Comments
 (0)