Skip to content

Commit fb34264

Browse files
committed
Merge branch '7.0.x'
2 parents c2cf5e0 + efb85d6 commit fb34264

11 files changed

Lines changed: 158 additions & 9 deletions

File tree

spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ public static Publisher<?> invokeSuspendingFunction(
134134
Object arg = args[index];
135135
if (!(parameter.isOptional() && arg == null)) {
136136
KType type = parameter.getType();
137-
if (!(type.isMarkedNullable() && arg == null) &&
137+
if (!type.isMarkedNullable() &&
138138
type.getClassifier() instanceof KClass<?> kClass &&
139139
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass))) {
140140
arg = box(kClass, arg);
@@ -166,7 +166,7 @@ public static Publisher<?> invokeSuspendingFunction(
166166
private static Object box(KClass<?> kClass, @Nullable Object arg) {
167167
KFunction<?> constructor = Objects.requireNonNull(KClasses.getPrimaryConstructor(kClass));
168168
KType type = constructor.getParameters().get(0).getType();
169-
if (!(type.isMarkedNullable() && arg == null) &&
169+
if (!type.isMarkedNullable() &&
170170
type.getClassifier() instanceof KClass<?> parameterClass &&
171171
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(parameterClass))) {
172172
arg = box(parameterClass, arg);

spring-core/src/test/kotlin/org/springframework/core/CoroutinesUtilsTests.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,13 @@ class CoroutinesUtilsTests {
229229
}
230230
}
231231

232+
@Test
233+
suspend fun invokeSuspendingFunctionWithNullableValueClassParameterAndNonNullParameter() {
234+
val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithNullableValueClass") }
235+
val mono = CoroutinesUtils.invokeSuspendingFunction(method, this, ValueClass("foo"), null) as Mono
236+
Assertions.assertThat(mono.awaitSingleOrNull()).isEqualTo("foo")
237+
}
238+
232239
@Test
233240
suspend fun invokeSuspendingFunctionWithNullableValueClassParameter() {
234241
val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithNullableValueClass") }

spring-test/spring-test.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
description = "Spring TestContext Framework"
22

33
apply plugin: "kotlin"
4+
apply plugin: "kotlinx-serialization"
45

56
dependencies {
67
api(project(":spring-core"))
@@ -81,6 +82,7 @@ dependencies {
8182
testImplementation("org.hibernate.orm:hibernate-core")
8283
testImplementation("org.hibernate.validator:hibernate-validator")
8384
testImplementation("org.hsqldb:hsqldb")
85+
testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json")
8486
testImplementation("org.junit.platform:junit-platform-testkit")
8587
testImplementation("tools.jackson.core:jackson-databind")
8688
testRuntimeOnly("com.sun.xml.bind:jaxb-core")
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2002-present 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+
* https://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.test.web.reactive.server
18+
19+
import kotlinx.serialization.Serializable
20+
import org.junit.jupiter.api.Test
21+
import org.springframework.http.MediaType
22+
import org.springframework.http.codec.json.KotlinSerializationJsonDecoder
23+
import org.springframework.web.bind.annotation.GetMapping
24+
import org.springframework.web.bind.annotation.RestController
25+
26+
class WebTestClientKotlinTests {
27+
28+
@Test
29+
fun expectBodyListKotlinSerialization() {
30+
val client = WebTestClient.bindToController(TestController::class.java)
31+
.configureClient()
32+
.codecs {
33+
it.registerDefaults(false)
34+
it.customCodecs().register(KotlinSerializationJsonDecoder())
35+
}.build()
36+
37+
client.get().uri("/test")
38+
.accept(MediaType.APPLICATION_JSON)
39+
.exchangeSuccessfully()
40+
.expectBodyList<Response>()
41+
.hasSize(2)
42+
.contains(Response("Hello"), Response("World"))
43+
}
44+
45+
@Serializable
46+
data class Response(val message: String)
47+
48+
@RestController
49+
class TestController {
50+
@GetMapping("test")
51+
fun test(): List<Response> = listOf(Response("Hello"), Response("World"))
52+
}
53+
}

spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationStringDecoder.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public abstract class KotlinSerializationStringDecoder<T extends StringFormat> e
5555
implements Decoder<Object> {
5656

5757
// String decoding needed for now, see https://github.com/Kotlin/kotlinx.serialization/issues/204 for more details
58-
private final StringDecoder stringDecoder = StringDecoder.allMimeTypes(StringDecoder.DEFAULT_DELIMITERS, false);
58+
protected final StringDecoder stringDecoder = StringDecoder.allMimeTypes(StringDecoder.DEFAULT_DELIMITERS, false);
5959

6060

6161
/**
@@ -158,7 +158,7 @@ public Mono<Object> decodeToMono(Publisher<DataBuffer> inputStream, ResolvableTy
158158
});
159159
}
160160

161-
private CodecException processException(IllegalArgumentException ex) {
161+
protected CodecException processException(IllegalArgumentException ex) {
162162
return new DecodingException("Decoding error: " + ex.getMessage(), ex);
163163
}
164164

spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonDecoder.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,22 @@
1616

1717
package org.springframework.http.codec.json;
1818

19+
import java.util.List;
20+
import java.util.Map;
21+
import java.util.Objects;
1922
import java.util.function.Predicate;
2023

24+
import kotlinx.serialization.KSerializer;
25+
import kotlinx.serialization.builtins.BuiltinSerializersKt;
2126
import kotlinx.serialization.json.Json;
27+
import org.jspecify.annotations.Nullable;
28+
import org.reactivestreams.Publisher;
29+
import reactor.core.publisher.Flux;
30+
import reactor.core.publisher.Mono;
2231

2332
import org.springframework.core.ResolvableType;
33+
import org.springframework.core.codec.DecodingException;
34+
import org.springframework.core.io.buffer.DataBuffer;
2435
import org.springframework.http.MediaType;
2536
import org.springframework.http.codec.KotlinSerializationStringDecoder;
2637
import org.springframework.util.MimeType;
@@ -96,4 +107,37 @@ public KotlinSerializationJsonDecoder(Json json, Predicate<ResolvableType> typeP
96107
super(json, typePredicate, DEFAULT_JSON_MIME_TYPES);
97108
}
98109

110+
@Override
111+
public Flux<Object> decode(Publisher<DataBuffer> inputStream, ResolvableType elementType,
112+
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
113+
return Flux.defer(() -> {
114+
KSerializer<Object> serializer = serializer(elementType);
115+
if (serializer == null) {
116+
return Mono.error(new DecodingException("Could not find KSerializer for " + elementType));
117+
}
118+
return this.stringDecoder
119+
.decode(inputStream, elementType, mimeType, hints)
120+
.switchOnFirst((signal, flux) -> {
121+
if (signal.hasValue()) {
122+
String value = Objects.requireNonNull(signal.get());
123+
if (value.stripLeading().startsWith("[") && !List.class.isAssignableFrom(elementType.toClass())) {
124+
KSerializer<List<Object>> listSerializer = BuiltinSerializersKt.ListSerializer(serializer);
125+
return flux
126+
.flatMapIterable(string -> format().decodeFromString(listSerializer, string))
127+
.onErrorMap(IllegalArgumentException.class, this::processException);
128+
}
129+
return flux.handle((string, sink) -> {
130+
try {
131+
sink.next(format().decodeFromString(serializer, string));
132+
}
133+
catch (IllegalArgumentException ex) {
134+
sink.error(processException(ex));
135+
}
136+
});
137+
}
138+
return flux;
139+
});
140+
});
141+
}
142+
99143
}

spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ private static class KotlinDelegate {
316316
Object arg = args[index];
317317
if (!(parameter.isOptional() && arg == null)) {
318318
KType type = parameter.getType();
319-
if (!(type.isMarkedNullable() && arg == null) &&
319+
if (!type.isMarkedNullable() &&
320320
type.getClassifier() instanceof KClass<?> kClass &&
321321
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass))) {
322322
arg = box(kClass, arg);
@@ -337,7 +337,7 @@ private static class KotlinDelegate {
337337
private static Object box(KClass<?> kClass, @Nullable Object arg) {
338338
KFunction<?> constructor = Objects.requireNonNull(KClasses.getPrimaryConstructor(kClass));
339339
KType type = constructor.getParameters().get(0).getType();
340-
if (!(type.isMarkedNullable() && arg == null) &&
340+
if (!type.isMarkedNullable() &&
341341
type.getClassifier() instanceof KClass<?> parameterClass &&
342342
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(parameterClass))) {
343343
arg = box(parameterClass, arg);

spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonDecoderTests.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,34 @@ class KotlinSerializationJsonDecoderTests : AbstractDecoderTests<KotlinSerializa
197197
}, null, null)
198198
}
199199

200+
@Test
201+
fun decodeJsonArrayToFlux() {
202+
val input = Flux.concat(
203+
stringBuffer("[{\"bar\":\"b1\",\"foo\":\"f1\"},"),
204+
stringBuffer("{\"bar\":\"b2\",\"foo\":\"f2\"}]"))
205+
206+
testDecodeAll(input, ResolvableType.forClass(Pojo::class.java), {
207+
it.expectNext(Pojo("f1", "b1"))
208+
.expectNext(Pojo("f2", "b2"))
209+
.expectComplete()
210+
.verify()
211+
}, null, null)
212+
}
213+
214+
@Test
215+
fun decodeJsonArrayToFluxOfList() {
216+
val input = Flux.concat(
217+
stringBuffer("[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]\n"),
218+
stringBuffer("[{\"bar\":\"b3\",\"foo\":\"f3\"},{\"bar\":\"b4\",\"foo\":\"f4\"}]"))
219+
220+
testDecodeAll(input, ResolvableType.forClassWithGenerics(List::class.java, Pojo::class.java), {
221+
it.expectNext(listOf(Pojo("f1", "b1"),Pojo("f2", "b2")))
222+
.expectNext(listOf(Pojo("f3", "b3"),Pojo("f4", "b4")))
223+
.expectComplete()
224+
.verify()
225+
}, null, null)
226+
}
227+
200228
@Test
201229
override fun decodeToMono() {
202230
val input = Flux.concat(

spring-web/src/test/kotlin/org/springframework/web/method/support/InvocableHandlerMethodKotlinTests.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,13 @@ class InvocableHandlerMethodKotlinTests {
148148
Assertions.assertThatIllegalArgumentException().isThrownBy { invocable.invokeForRequest(request, null) }
149149
}
150150

151+
@Test
152+
fun valueClassWithNullableAndNonNullParameter() {
153+
composite.addResolver(StubArgumentResolver(LongValueClass::class.java, LongValueClass(1)))
154+
val value = getInvocable(ValueClassHandler::valueClassWithNullable.javaMethod!!).invokeForRequest(request, null)
155+
Assertions.assertThat(value).isEqualTo(1L)
156+
}
157+
151158
@Test
152159
fun valueClassWithNullable() {
153160
composite.addResolver(StubArgumentResolver(LongValueClass::class.java, null))

spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ private static class KotlinDelegate {
329329
private static final String COROUTINE_CONTEXT_ATTRIBUTE = "org.springframework.web.server.CoWebFilter.context";
330330

331331
@SuppressWarnings("DataFlowIssue")
332-
public static @Nullable Object invokeFunction(Method method, Object target, Object[] args, boolean isSuspendingFunction,
332+
public static @Nullable Object invokeFunction(Method method, Object target, @Nullable Object[] args, boolean isSuspendingFunction,
333333
ServerWebExchange exchange) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
334334

335335
if (isSuspendingFunction) {
@@ -356,7 +356,7 @@ private static class KotlinDelegate {
356356
Object arg = args[index];
357357
if (!(parameter.isOptional() && arg == null)) {
358358
KType type = parameter.getType();
359-
if (!(type.isMarkedNullable() && arg == null) &&
359+
if (!type.isMarkedNullable() &&
360360
type.getClassifier() instanceof KClass<?> kClass &&
361361
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass))) {
362362
arg = box(kClass, arg);
@@ -378,7 +378,7 @@ private static class KotlinDelegate {
378378
private static Object box(KClass<?> kClass, @Nullable Object arg) {
379379
KFunction<?> constructor = Objects.requireNonNull(KClasses.getPrimaryConstructor(kClass));
380380
KType type = constructor.getParameters().get(0).getType();
381-
if (!(type.isMarkedNullable() && arg == null) &&
381+
if (!type.isMarkedNullable() &&
382382
type.getClassifier() instanceof KClass<?> parameterClass &&
383383
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(parameterClass))) {
384384
arg = box(parameterClass, arg);

0 commit comments

Comments
 (0)