Skip to content

Commit 1250028

Browse files
committed
Add JsonConverterDelegate
The JsonConverterDelegate interface replaces usages of HttpMessageContentConverter to provides the flexibility to use either message converters or WebFlux codecs. HttpMessageContentConverter is deprecated, and replaced with a package private copy (DefaultJsonConverterDelegate) in the org.springframework.test.json package that is accessible through a static method on JsonConverterDelegate. See gh-35737
1 parent 5f895d7 commit 1250028

19 files changed

+384
-129
lines changed

spring-test/src/main/java/org/springframework/test/http/HttpMessageContentConverter.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@
1818

1919
import java.io.IOException;
2020
import java.lang.reflect.Type;
21+
import java.nio.charset.StandardCharsets;
2122
import java.util.Arrays;
2223
import java.util.List;
2324
import java.util.stream.StreamSupport;
2425

2526
import org.springframework.core.ResolvableType;
27+
import org.springframework.http.HttpHeaders;
2628
import org.springframework.http.HttpInputMessage;
2729
import org.springframework.http.MediaType;
2830
import org.springframework.http.converter.GenericHttpMessageConverter;
@@ -31,6 +33,7 @@
3133
import org.springframework.http.converter.SmartHttpMessageConverter;
3234
import org.springframework.mock.http.MockHttpInputMessage;
3335
import org.springframework.mock.http.MockHttpOutputMessage;
36+
import org.springframework.test.json.JsonConverterDelegate;
3437
import org.springframework.util.Assert;
3538
import org.springframework.util.function.SingletonSupplier;
3639

@@ -39,8 +42,10 @@
3942
*
4043
* @author Stephane Nicoll
4144
* @since 6.2
45+
* @deprecated in favor of static factory methods in {@link JsonConverterDelegate}
4246
*/
43-
public class HttpMessageContentConverter {
47+
@Deprecated(since = "7.0", forRemoval = true)
48+
public class HttpMessageContentConverter implements JsonConverterDelegate {
4449

4550
private static final MediaType JSON = MediaType.APPLICATION_JSON;
4651

@@ -69,6 +74,19 @@ public static HttpMessageContentConverter of(HttpMessageConverter<?>... candidat
6974
}
7075

7176

77+
@Override
78+
public <T> T read(String content, ResolvableType targetType) throws IOException{
79+
HttpInputMessage message = new MockHttpInputMessage(content.getBytes(StandardCharsets.UTF_8));
80+
message.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
81+
return convert(message, MediaType.APPLICATION_JSON, targetType);
82+
}
83+
84+
@Override
85+
public <T> T map(Object value, ResolvableType targetType) throws IOException {
86+
return convertViaJson(value, targetType);
87+
}
88+
89+
7290
/**
7391
* Convert the given {@link HttpInputMessage} whose content must match the
7492
* given {@link MediaType} to the requested {@code targetType}.

spring-test/src/main/java/org/springframework/test/json/AbstractJsonContentAssert.java

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import java.io.InputStream;
2121
import java.lang.reflect.Type;
2222
import java.nio.charset.Charset;
23-
import java.nio.charset.StandardCharsets;
2423
import java.nio.file.Path;
2524
import java.util.function.Consumer;
2625

@@ -42,11 +41,6 @@
4241
import org.springframework.core.io.FileSystemResource;
4342
import org.springframework.core.io.InputStreamResource;
4443
import org.springframework.core.io.Resource;
45-
import org.springframework.http.HttpHeaders;
46-
import org.springframework.http.HttpInputMessage;
47-
import org.springframework.http.MediaType;
48-
import org.springframework.mock.http.MockHttpInputMessage;
49-
import org.springframework.test.http.HttpMessageContentConverter;
5044
import org.springframework.util.Assert;
5145

5246
/**
@@ -77,7 +71,7 @@ public abstract class AbstractJsonContentAssert<SELF extends AbstractJsonContent
7771
private static final Failures failures = Failures.instance();
7872

7973

80-
private final @Nullable HttpMessageContentConverter contentConverter;
74+
private final @Nullable JsonConverterDelegate converterDelegate;
8175

8276
private @Nullable Class<?> resourceLoadClass;
8377

@@ -92,7 +86,7 @@ public abstract class AbstractJsonContentAssert<SELF extends AbstractJsonContent
9286
*/
9387
protected AbstractJsonContentAssert(@Nullable JsonContent actual, Class<?> selfType) {
9488
super(actual, selfType);
95-
this.contentConverter = (actual != null ? actual.getContentConverter() : null);
89+
this.converterDelegate = (actual != null ? actual.getJsonConverterDelegate() : null);
9690
this.jsonLoader = new JsonLoader(null, null);
9791
as("JSON content");
9892
}
@@ -131,13 +125,12 @@ public <T> AbstractObjectAssert<?, T> convertTo(Class<T> target) {
131125

132126
private <T> T convertToTargetType(Type targetType) {
133127
String json = this.actual.getJson();
134-
if (this.contentConverter == null) {
128+
if (this.converterDelegate == null) {
135129
throw new IllegalStateException(
136130
"No JSON message converter available to convert %s".formatted(json));
137131
}
138132
try {
139-
return this.contentConverter.convert(fromJson(json), MediaType.APPLICATION_JSON,
140-
ResolvableType.forType(targetType));
133+
return this.converterDelegate.read(json, ResolvableType.forType(targetType));
141134
}
142135
catch (Exception ex) {
143136
throw failure(new ValueProcessingFailed(json,
@@ -146,12 +139,6 @@ private <T> T convertToTargetType(Type targetType) {
146139
}
147140
}
148141

149-
private HttpInputMessage fromJson(String json) {
150-
MockHttpInputMessage inputMessage = new MockHttpInputMessage(json.getBytes(StandardCharsets.UTF_8));
151-
inputMessage.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
152-
return inputMessage;
153-
}
154-
155142

156143
// JsonPath support
157144

@@ -163,7 +150,7 @@ private HttpInputMessage fromJson(String json) {
163150
*/
164151
public JsonPathValueAssert extractingPath(String path) {
165152
Object value = new JsonPathValue(path).getValue();
166-
return new JsonPathValueAssert(value, path, this.contentConverter);
153+
return new JsonPathValueAssert(value, path, this.converterDelegate);
167154
}
168155

169156
/**
@@ -174,7 +161,7 @@ public JsonPathValueAssert extractingPath(String path) {
174161
*/
175162
public SELF hasPathSatisfying(String path, Consumer<AssertProvider<JsonPathValueAssert>> valueRequirements) {
176163
Object value = new JsonPathValue(path).assertHasPath();
177-
JsonPathValueAssert valueAssert = new JsonPathValueAssert(value, path, this.contentConverter);
164+
JsonPathValueAssert valueAssert = new JsonPathValueAssert(value, path, this.converterDelegate);
178165
valueRequirements.accept(() -> valueAssert);
179166
return this.myself;
180167
}

spring-test/src/main/java/org/springframework/test/json/AbstractJsonValueAssert.java

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535

3636
import org.springframework.core.ResolvableType;
3737
import org.springframework.http.converter.GenericHttpMessageConverter;
38-
import org.springframework.test.http.HttpMessageContentConverter;
3938
import org.springframework.util.ObjectUtils;
4039
import org.springframework.util.StringUtils;
4140

@@ -64,14 +63,23 @@ public abstract class AbstractJsonValueAssert<SELF extends AbstractJsonValueAsse
6463

6564
private final Failures failures = Failures.instance();
6665

67-
private final @Nullable HttpMessageContentConverter contentConverter;
66+
private final @Nullable JsonConverterDelegate converterDelegate;
6867

6968

70-
protected AbstractJsonValueAssert(@Nullable Object actual, Class<?> selfType,
71-
@Nullable HttpMessageContentConverter contentConverter) {
69+
protected AbstractJsonValueAssert(
70+
@Nullable Object actual, Class<?> selfType, @Nullable JsonConverterDelegate converter) {
7271

7372
super(actual, selfType);
74-
this.contentConverter = contentConverter;
73+
this.converterDelegate = converter;
74+
}
75+
76+
@SuppressWarnings("removal")
77+
@Deprecated(since = "7.0", forRemoval = true)
78+
protected AbstractJsonValueAssert(
79+
@Nullable Object actual, Class<?> selfType,
80+
org.springframework.test.http.@Nullable HttpMessageContentConverter converter) {
81+
82+
this(actual, selfType, (JsonConverterDelegate) converter);
7583
}
7684

7785

@@ -196,12 +204,12 @@ public SELF isNotEmpty() {
196204
}
197205

198206
private <T> T convertToTargetType(Type targetType) {
199-
if (this.contentConverter == null) {
207+
if (this.converterDelegate == null) {
200208
throw new IllegalStateException(
201209
"No JSON message converter available to convert %s".formatted(actualToString()));
202210
}
203211
try {
204-
return this.contentConverter.convertViaJson(this.actual, ResolvableType.forType(targetType));
212+
return this.converterDelegate.map(this.actual, ResolvableType.forType(targetType));
205213
}
206214
catch (Exception ex) {
207215
throw valueProcessingFailed("To convert successfully to:%n %s%nBut it failed:%n %s%n"
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
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.json;
18+
19+
import java.io.IOException;
20+
import java.lang.reflect.Type;
21+
import java.nio.charset.StandardCharsets;
22+
import java.util.List;
23+
import java.util.stream.StreamSupport;
24+
25+
import org.springframework.core.ResolvableType;
26+
import org.springframework.http.HttpHeaders;
27+
import org.springframework.http.HttpInputMessage;
28+
import org.springframework.http.MediaType;
29+
import org.springframework.http.converter.GenericHttpMessageConverter;
30+
import org.springframework.http.converter.HttpMessageConverter;
31+
import org.springframework.http.converter.HttpMessageNotReadableException;
32+
import org.springframework.http.converter.SmartHttpMessageConverter;
33+
import org.springframework.mock.http.MockHttpInputMessage;
34+
import org.springframework.mock.http.MockHttpOutputMessage;
35+
import org.springframework.util.Assert;
36+
import org.springframework.util.function.SingletonSupplier;
37+
38+
/**
39+
* Default {@link JsonConverterDelegate} based on {@link HttpMessageConverter}s.
40+
*
41+
* @author Stephane Nicoll
42+
* @author Rossen Stoyanchev
43+
* @since 7.0
44+
*/
45+
final class DefaultJsonConverterDelegate implements JsonConverterDelegate {
46+
47+
private static final MediaType JSON = MediaType.APPLICATION_JSON;
48+
49+
private final List<HttpMessageConverter<?>> messageConverters;
50+
51+
52+
DefaultJsonConverterDelegate(Iterable<HttpMessageConverter<?>> messageConverters) {
53+
this.messageConverters = StreamSupport.stream(messageConverters.spliterator(), false).toList();
54+
Assert.notEmpty(this.messageConverters, "At least one message converter needs to be specified");
55+
}
56+
57+
58+
@Override
59+
public <T> T read(String content, ResolvableType targetType) throws IOException{
60+
HttpInputMessage message = new MockHttpInputMessage(content.getBytes(StandardCharsets.UTF_8));
61+
message.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
62+
return read(message, MediaType.APPLICATION_JSON, targetType);
63+
}
64+
65+
/**
66+
* Convert the given {@link HttpInputMessage} whose content must match the
67+
* given {@link MediaType} to the requested {@code targetType}.
68+
* @param message an input message
69+
* @param mediaType the media type of the input
70+
* @param targetType the target type
71+
* @param <T> the converted object type
72+
* @return a value of the given {@code targetType}
73+
*/
74+
@SuppressWarnings("unchecked")
75+
<T> T read(HttpInputMessage message, MediaType mediaType, ResolvableType targetType)
76+
throws IOException, HttpMessageNotReadableException {
77+
78+
Class<?> contextClass = targetType.getRawClass();
79+
SingletonSupplier<Type> javaType = SingletonSupplier.of(targetType::getType);
80+
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
81+
if (messageConverter instanceof GenericHttpMessageConverter<?> genericMessageConverter) {
82+
Type type = javaType.obtain();
83+
if (genericMessageConverter.canRead(type, contextClass, mediaType)) {
84+
return (T) genericMessageConverter.read(type, contextClass, message);
85+
}
86+
}
87+
else if (messageConverter instanceof SmartHttpMessageConverter<?> smartMessageConverter) {
88+
if (smartMessageConverter.canRead(targetType, mediaType)) {
89+
return (T) smartMessageConverter.read(targetType, message, null);
90+
}
91+
}
92+
else {
93+
Class<?> targetClass = (contextClass != null ? contextClass : Object.class);
94+
if (messageConverter.canRead(targetClass, mediaType)) {
95+
HttpMessageConverter<T> simpleMessageConverter = (HttpMessageConverter<T>) messageConverter;
96+
Class<? extends T> clazz = (Class<? extends T>) targetClass;
97+
return simpleMessageConverter.read(clazz, message);
98+
}
99+
}
100+
}
101+
throw new IllegalStateException("No converter found to read [%s] to [%s]".formatted(mediaType, targetType));
102+
}
103+
104+
/**
105+
* Convert the given raw value to the given {@code targetType} by writing
106+
* it first to JSON and reading it back.
107+
* @param value the value to convert
108+
* @param targetType the target type
109+
* @param <T> the converted object type
110+
* @return a value of the given {@code targetType}
111+
*/
112+
@Override
113+
public <T> T map(Object value, ResolvableType targetType) throws IOException {
114+
MockHttpOutputMessage outputMessage = writeToJson(value, ResolvableType.forInstance(value));
115+
return read(fromHttpOutputMessage(outputMessage), JSON, targetType);
116+
}
117+
118+
@SuppressWarnings({ "rawtypes", "unchecked" })
119+
private MockHttpOutputMessage writeToJson(Object value, ResolvableType valueType) throws IOException {
120+
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
121+
Class<?> valueClass = value.getClass();
122+
SingletonSupplier<Type> javaType = SingletonSupplier.of(valueType::getType);
123+
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
124+
if (messageConverter instanceof GenericHttpMessageConverter genericMessageConverter) {
125+
Type type = javaType.obtain();
126+
if (genericMessageConverter.canWrite(type, valueClass, JSON)) {
127+
genericMessageConverter.write(value, type, JSON, outputMessage);
128+
return outputMessage;
129+
}
130+
}
131+
else if (messageConverter instanceof SmartHttpMessageConverter smartMessageConverter) {
132+
if (smartMessageConverter.canWrite(valueType, valueClass, JSON)) {
133+
smartMessageConverter.write(value, valueType, JSON, outputMessage, null);
134+
return outputMessage;
135+
}
136+
}
137+
else if (messageConverter.canWrite(valueClass, JSON)) {
138+
((HttpMessageConverter<Object>) messageConverter).write(value, JSON, outputMessage);
139+
return outputMessage;
140+
}
141+
}
142+
throw new IllegalStateException("No converter found to convert [%s] to JSON".formatted(valueType));
143+
}
144+
145+
private static HttpInputMessage fromHttpOutputMessage(MockHttpOutputMessage message) {
146+
MockHttpInputMessage inputMessage = new MockHttpInputMessage(message.getBodyAsBytes());
147+
inputMessage.getHeaders().addAll(message.getHeaders());
148+
return inputMessage;
149+
}
150+
151+
}

0 commit comments

Comments
 (0)