Skip to content

Commit 6dd93d4

Browse files
committed
Allow repeatable writes in HttpMessageConverter
This commit ensures that the StreamingHttpOutputMessage.Body.repeatable flag is set in message converters for bodies that can be written repeatedly. Closes gh-31516 See gh-31449
1 parent ab316d9 commit 6dd93d4

22 files changed

+206
-25
lines changed

spring-web/src/main/java/org/springframework/http/converter/AbstractGenericHttpMessageConverter.java

+18-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -88,16 +88,27 @@ public final void write(final T t, @Nullable final Type type, @Nullable MediaTyp
8888
addDefaultHeaders(headers, t, contentType);
8989

9090
if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) {
91-
streamingOutputMessage.setBody(outputStream -> writeInternal(t, type, new HttpOutputMessage() {
91+
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
9292
@Override
93-
public OutputStream getBody() {
94-
return outputStream;
93+
public void writeTo(OutputStream outputStream) throws IOException {
94+
writeInternal(t, type, new HttpOutputMessage() {
95+
@Override
96+
public OutputStream getBody() {
97+
return outputStream;
98+
}
99+
100+
@Override
101+
public HttpHeaders getHeaders() {
102+
return headers;
103+
}
104+
});
95105
}
106+
96107
@Override
97-
public HttpHeaders getHeaders() {
98-
return headers;
108+
public boolean repeatable() {
109+
return supportsRepeatableWrites(t);
99110
}
100-
}));
111+
});
101112
}
102113
else {
103114
writeInternal(t, type, outputMessage);

spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java

+32-6
Original file line numberDiff line numberDiff line change
@@ -210,16 +210,27 @@ public final void write(final T t, @Nullable MediaType contentType, HttpOutputMe
210210
addDefaultHeaders(headers, t, contentType);
211211

212212
if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) {
213-
streamingOutputMessage.setBody(outputStream -> writeInternal(t, new HttpOutputMessage() {
213+
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
214214
@Override
215-
public OutputStream getBody() {
216-
return outputStream;
215+
public void writeTo(OutputStream outputStream) throws IOException {
216+
writeInternal(t, new HttpOutputMessage() {
217+
@Override
218+
public OutputStream getBody() {
219+
return outputStream;
220+
}
221+
222+
@Override
223+
public HttpHeaders getHeaders() {
224+
return headers;
225+
}
226+
});
217227
}
228+
218229
@Override
219-
public HttpHeaders getHeaders() {
220-
return headers;
230+
public boolean repeatable() {
231+
return supportsRepeatableWrites(t);
221232
}
222-
}));
233+
});
223234
}
224235
else {
225236
writeInternal(t, outputMessage);
@@ -289,6 +300,21 @@ protected Long getContentLength(T t, @Nullable MediaType contentType) throws IOE
289300
return null;
290301
}
291302

303+
/**
304+
* Indicates whether this message converter can
305+
* {@linkplain #write(Object, MediaType, HttpOutputMessage) write} the
306+
* given object multiple times.
307+
*
308+
* <p>Default implementation returns {@code false}.
309+
* @param t the object t
310+
* @return {@code true} if {@code t} can be written repeatedly;
311+
* {@code false} otherwise
312+
* @since 6.1
313+
*/
314+
protected boolean supportsRepeatableWrites(T t) {
315+
return false;
316+
}
317+
292318

293319
/**
294320
* Indicates whether the given class is supported by this converter.

spring-web/src/main/java/org/springframework/http/converter/AbstractKotlinSerializationHttpMessageConverter.java

+5
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,9 @@ private boolean hasPolymorphism(SerialDescriptor descriptor, Set<String> already
178178
}
179179
return false;
180180
}
181+
182+
@Override
183+
protected boolean supportsRepeatableWrites(Object object) {
184+
return true;
185+
}
181186
}

spring-web/src/main/java/org/springframework/http/converter/BufferedImageHttpMessageConverter.java

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -226,7 +226,17 @@ public void write(final BufferedImage image, @Nullable final MediaType contentTy
226226
outputMessage.getHeaders().setContentType(selectedContentType);
227227

228228
if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) {
229-
streamingOutputMessage.setBody(outputStream -> writeInternal(image, selectedContentType, outputStream));
229+
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
230+
@Override
231+
public void writeTo(OutputStream outputStream) throws IOException {
232+
BufferedImageHttpMessageConverter.this.writeInternal(image, selectedContentType, outputStream);
233+
}
234+
235+
@Override
236+
public boolean repeatable() {
237+
return true;
238+
}
239+
});
230240
}
231241
else {
232242
writeInternal(image, selectedContentType, outputMessage.getBody());

spring-web/src/main/java/org/springframework/http/converter/ByteArrayHttpMessageConverter.java

+4
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,8 @@ protected void writeInternal(byte[] bytes, HttpOutputMessage outputMessage) thro
6767
StreamUtils.copy(bytes, outputMessage.getBody());
6868
}
6969

70+
@Override
71+
protected boolean supportsRepeatableWrites(byte[] bytes) {
72+
return true;
73+
}
7074
}

spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java

+11-1
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,17 @@ private void writeForm(MultiValueMap<String, Object> formData, @Nullable MediaTy
400400
outputMessage.getHeaders().setContentLength(bytes.length);
401401

402402
if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) {
403-
streamingOutputMessage.setBody(outputStream -> StreamUtils.copy(bytes, outputStream));
403+
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
404+
@Override
405+
public void writeTo(OutputStream outputStream) throws IOException {
406+
StreamUtils.copy(bytes, outputStream);
407+
}
408+
409+
@Override
410+
public boolean repeatable() {
411+
return true;
412+
}
413+
});
404414
}
405415
else {
406416
StreamUtils.copy(bytes, outputMessage.getBody());

spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -135,4 +135,8 @@ protected Long getContentLength(Object obj, @Nullable MediaType contentType) {
135135
return this.stringHttpMessageConverter.getContentLength(value, contentType);
136136
}
137137

138+
@Override
139+
protected boolean supportsRepeatableWrites(Object o) {
140+
return true;
141+
}
138142
}

spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -166,4 +166,8 @@ protected void writeContent(Resource resource, HttpOutputMessage outputMessage)
166166
}
167167
}
168168

169+
@Override
170+
protected boolean supportsRepeatableWrites(Resource resource) {
171+
return !(resource instanceof InputStreamResource);
172+
}
169173
}

spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@
2424
import java.nio.charset.StandardCharsets;
2525
import java.util.Collection;
2626

27+
import org.springframework.core.io.InputStreamResource;
2728
import org.springframework.core.io.Resource;
2829
import org.springframework.core.io.support.ResourceRegion;
2930
import org.springframework.http.HttpHeaders;
@@ -238,4 +239,24 @@ private static void print(OutputStream os, String buf) throws IOException {
238239
os.write(buf.getBytes(StandardCharsets.US_ASCII));
239240
}
240241

242+
@Override
243+
@SuppressWarnings("unchecked")
244+
protected boolean supportsRepeatableWrites(Object object) {
245+
if (object instanceof ResourceRegion resourceRegion) {
246+
return supportsRepeatableWrites(resourceRegion);
247+
}
248+
else {
249+
Collection<ResourceRegion> regions = (Collection<ResourceRegion>) object;
250+
for (ResourceRegion region : regions) {
251+
if (!supportsRepeatableWrites(region)) {
252+
return false;
253+
}
254+
}
255+
return true;
256+
}
257+
}
258+
259+
private boolean supportsRepeatableWrites(ResourceRegion region) {
260+
return !(region.getResource() instanceof InputStreamResource);
261+
}
241262
}

spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java

+4
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,8 @@ else if (contentType.isCompatibleWith(MediaType.APPLICATION_JSON) ||
163163
return charset;
164164
}
165165

166+
@Override
167+
protected boolean supportsRepeatableWrites(String s) {
168+
return true;
169+
}
166170
}

spring-web/src/main/java/org/springframework/http/converter/feed/AbstractWireFeedHttpMessageConverter.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -107,4 +107,8 @@ protected void writeInternal(T wireFeed, HttpOutputMessage outputMessage)
107107
}
108108
}
109109

110+
@Override
111+
protected boolean supportsRepeatableWrites(T t) {
112+
return true;
113+
}
110114
}

spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java

+4
Original file line numberDiff line numberDiff line change
@@ -568,4 +568,8 @@ protected Long getContentLength(Object object, @Nullable MediaType contentType)
568568
return super.getContentLength(object, contentType);
569569
}
570570

571+
@Override
572+
protected boolean supportsRepeatableWrites(Object o) {
573+
return true;
574+
}
571575
}

spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -107,4 +107,8 @@ protected void writeInternal(Object object, @Nullable Type type, Writer writer)
107107
}
108108
}
109109

110+
@Override
111+
protected boolean supportsRepeatableWrites(Object o) {
112+
return true;
113+
}
110114
}

spring-web/src/main/java/org/springframework/http/converter/json/JsonbHttpMessageConverter.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -110,4 +110,8 @@ protected void writeInternal(Object object, @Nullable Type type, Writer writer)
110110
}
111111
}
112112

113+
@Override
114+
protected boolean supportsRepeatableWrites(Object o) {
115+
return true;
116+
}
113117
}

spring-web/src/main/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverter.java

+4
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,10 @@ private void setProtoHeader(HttpOutputMessage response, Message message) {
247247
response.getHeaders().set(X_PROTOBUF_MESSAGE_HEADER, message.getDescriptorForType().getFullName());
248248
}
249249

250+
@Override
251+
protected boolean supportsRepeatableWrites(Message message) {
252+
return true;
253+
}
250254

251255
/**
252256
* Protobuf format support.

spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -200,6 +200,11 @@ private void setCharset(@Nullable MediaType contentType, Marshaller marshaller)
200200
}
201201
}
202202

203+
@Override
204+
protected boolean supportsRepeatableWrites(Object o) {
205+
return true;
206+
}
207+
203208

204209
private static final EntityResolver NO_OP_ENTITY_RESOLVER =
205210
(publicId, systemId) -> new InputSource(new StringReader(""));

spring-web/src/main/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverter.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -137,4 +137,8 @@ protected void writeToResult(Object o, HttpHeaders headers, Result result) throw
137137
this.marshaller.marshal(o, result);
138138
}
139139

140+
@Override
141+
protected boolean supportsRepeatableWrites(Object o) {
142+
return true;
143+
}
140144
}

spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -269,6 +269,11 @@ private void transform(Source source, Result result) throws TransformerException
269269
this.transformerFactory.newTransformer().transform(source, result);
270270
}
271271

272+
@Override
273+
protected boolean supportsRepeatableWrites(T t) {
274+
return t instanceof DOMSource;
275+
}
276+
272277

273278
private static class CountingOutputStream extends OutputStream {
274279

spring-web/src/test/java/org/springframework/http/converter/ByteArrayHttpMessageConverterTests.java

+14
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,18 @@ public void write() throws IOException {
7979
assertThat(outputMessage.getHeaders().getContentLength()).isEqualTo(2);
8080
}
8181

82+
@Test
83+
public void repeatableWrites() throws IOException {
84+
MockHttpOutputMessage outputMessage1 = new MockHttpOutputMessage();
85+
byte[] body = new byte[]{0x1, 0x2};
86+
assertThat(converter.supportsRepeatableWrites(body)).isTrue();
87+
88+
converter.write(body, null, outputMessage1);
89+
assertThat(outputMessage1.getBodyAsBytes()).isEqualTo(body);
90+
91+
MockHttpOutputMessage outputMessage2 = new MockHttpOutputMessage();
92+
converter.write(body, null, outputMessage2);
93+
assertThat(outputMessage2.getBodyAsBytes()).isEqualTo(body);
94+
}
95+
8296
}

0 commit comments

Comments
 (0)