diff --git a/spring-web/src/main/java/org/springframework/http/converter/AbstractGenericHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/AbstractGenericHttpMessageConverter.java index ce3eadc42bb..be025f95c80 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/AbstractGenericHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/AbstractGenericHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -88,16 +88,27 @@ public abstract class AbstractGenericHttpMessageConverter extends AbstractHtt addDefaultHeaders(headers, t, contentType); if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) { - streamingOutputMessage.setBody(outputStream -> writeInternal(t, type, new HttpOutputMessage() { + streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() { @Override - public OutputStream getBody() { - return outputStream; + public void writeTo(OutputStream outputStream) throws IOException { + writeInternal(t, type, new HttpOutputMessage() { + @Override + public OutputStream getBody() { + return outputStream; + } + + @Override + public HttpHeaders getHeaders() { + return headers; + } + }); } + @Override - public HttpHeaders getHeaders() { - return headers; + public boolean repeatable() { + return supportsRepeatableWrites(t); } - })); + }); } else { writeInternal(t, type, outputMessage); diff --git a/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java index 740d0091b0a..7f83a463592 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java @@ -210,16 +210,27 @@ public abstract class AbstractHttpMessageConverter implements HttpMessageConv addDefaultHeaders(headers, t, contentType); if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) { - streamingOutputMessage.setBody(outputStream -> writeInternal(t, new HttpOutputMessage() { + streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() { @Override - public OutputStream getBody() { - return outputStream; + public void writeTo(OutputStream outputStream) throws IOException { + writeInternal(t, new HttpOutputMessage() { + @Override + public OutputStream getBody() { + return outputStream; + } + + @Override + public HttpHeaders getHeaders() { + return headers; + } + }); } + @Override - public HttpHeaders getHeaders() { - return headers; + public boolean repeatable() { + return supportsRepeatableWrites(t); } - })); + }); } else { writeInternal(t, outputMessage); @@ -289,6 +300,21 @@ public abstract class AbstractHttpMessageConverter implements HttpMessageConv return null; } + /** + * Indicates whether this message converter can + * {@linkplain #write(Object, MediaType, HttpOutputMessage) write} the + * given object multiple times. + * + *

Default implementation returns {@code false}. + * @param t the object t + * @return {@code true} if {@code t} can be written repeatedly; + * {@code false} otherwise + * @since 6.1 + */ + protected boolean supportsRepeatableWrites(T t) { + return false; + } + /** * Indicates whether the given class is supported by this converter. diff --git a/spring-web/src/main/java/org/springframework/http/converter/AbstractKotlinSerializationHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/AbstractKotlinSerializationHttpMessageConverter.java index 1b6f9fc1c9b..0c73ecc01a0 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/AbstractKotlinSerializationHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/AbstractKotlinSerializationHttpMessageConverter.java @@ -178,4 +178,9 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter writeInternal(image, selectedContentType, outputStream)); + streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() { + @Override + public void writeTo(OutputStream outputStream) throws IOException { + BufferedImageHttpMessageConverter.this.writeInternal(image, selectedContentType, outputStream); + } + + @Override + public boolean repeatable() { + return true; + } + }); } else { writeInternal(image, selectedContentType, outputMessage.getBody()); diff --git a/spring-web/src/main/java/org/springframework/http/converter/ByteArrayHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/ByteArrayHttpMessageConverter.java index 0d664c5d6f9..abe9a0eeb88 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/ByteArrayHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/ByteArrayHttpMessageConverter.java @@ -67,4 +67,8 @@ public class ByteArrayHttpMessageConverter extends AbstractHttpMessageConverter< StreamUtils.copy(bytes, outputMessage.getBody()); } + @Override + protected boolean supportsRepeatableWrites(byte[] bytes) { + return true; + } } diff --git a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java index 09ab8f63448..d7a64f2cf5d 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java @@ -400,7 +400,17 @@ public class FormHttpMessageConverter implements HttpMessageConverter StreamUtils.copy(bytes, outputStream)); + streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() { + @Override + public void writeTo(OutputStream outputStream) throws IOException { + StreamUtils.copy(bytes, outputStream); + } + + @Override + public boolean repeatable() { + return true; + } + }); } else { StreamUtils.copy(bytes, outputMessage.getBody()); diff --git a/spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java index 6f6ac69bd45..8ac15191274 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -135,4 +135,8 @@ public class ObjectToStringHttpMessageConverter extends AbstractHttpMessageConve return this.stringHttpMessageConverter.getContentLength(value, contentType); } + @Override + protected boolean supportsRepeatableWrites(Object o) { + return true; + } } diff --git a/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java index 6eb6cea988a..c441d53674d 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -166,4 +166,8 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter regions = (Collection) object; + for (ResourceRegion region : regions) { + if (!supportsRepeatableWrites(region)) { + return false; + } + } + return true; + } + } + + private boolean supportsRepeatableWrites(ResourceRegion region) { + return !(region.getResource() instanceof InputStreamResource); + } } diff --git a/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java index 5a408d87497..04adf380cf6 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java @@ -163,4 +163,8 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter } } + @Override + protected boolean supportsRepeatableWrites(T t) { + return true; + } } diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java index 9c585ab6ae5..0352dc1e08d 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java @@ -568,4 +568,8 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener return super.getContentLength(object, contentType); } + @Override + protected boolean supportsRepeatableWrites(Object o) { + return true; + } } diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java index 8c363b4eae1..e872d7daf4c 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -107,4 +107,8 @@ public class GsonHttpMessageConverter extends AbstractJsonHttpMessageConverter { } } + @Override + protected boolean supportsRepeatableWrites(Object o) { + return true; + } } diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/JsonbHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/JsonbHttpMessageConverter.java index 48b2dbbd8ac..97c06b4b86a 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/JsonbHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/JsonbHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -110,4 +110,8 @@ public class JsonbHttpMessageConverter extends AbstractJsonHttpMessageConverter } } + @Override + protected boolean supportsRepeatableWrites(Object o) { + return true; + } } diff --git a/spring-web/src/main/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverter.java index dbaa8bf4756..a87143836de 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverter.java @@ -247,6 +247,10 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter new InputSource(new StringReader("")); diff --git a/spring-web/src/main/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverter.java index 0f6a968cfd0..908b0246c5d 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -137,4 +137,8 @@ public class MarshallingHttpMessageConverter extends AbstractXmlHttpMessageConve this.marshaller.marshal(o, result); } + @Override + protected boolean supportsRepeatableWrites(Object o) { + return true; + } } diff --git a/spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java index 40d4b4de422..2c91fb32c76 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -269,6 +269,11 @@ public class SourceHttpMessageConverter extends AbstractHttpMe this.transformerFactory.newTransformer().transform(source, result); } + @Override + protected boolean supportsRepeatableWrites(T t) { + return t instanceof DOMSource; + } + private static class CountingOutputStream extends OutputStream { diff --git a/spring-web/src/test/java/org/springframework/http/converter/ByteArrayHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/ByteArrayHttpMessageConverterTests.java index 30fe8eed62c..a701f9db370 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/ByteArrayHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/ByteArrayHttpMessageConverterTests.java @@ -79,4 +79,18 @@ public class ByteArrayHttpMessageConverterTests { assertThat(outputMessage.getHeaders().getContentLength()).isEqualTo(2); } + @Test + public void repeatableWrites() throws IOException { + MockHttpOutputMessage outputMessage1 = new MockHttpOutputMessage(); + byte[] body = new byte[]{0x1, 0x2}; + assertThat(converter.supportsRepeatableWrites(body)).isTrue(); + + converter.write(body, null, outputMessage1); + assertThat(outputMessage1.getBodyAsBytes()).isEqualTo(body); + + MockHttpOutputMessage outputMessage2 = new MockHttpOutputMessage(); + converter.write(body, null, outputMessage2); + assertThat(outputMessage2.getBodyAsBytes()).isEqualTo(body); + } + } diff --git a/spring-web/src/test/java/org/springframework/http/converter/StringHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/StringHttpMessageConverterTests.java index 36bbff8866c..2885aec9a9e 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/StringHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/StringHttpMessageConverterTests.java @@ -171,4 +171,19 @@ class StringHttpMessageConverterTests { assertThat(headers.getAcceptCharset()).isEmpty(); } + @Test + public void repeatableWrites() throws IOException { + MockHttpOutputMessage outputMessage1 = new MockHttpOutputMessage(); + String body = "Hello World"; + assertThat(converter.supportsRepeatableWrites(body)).isTrue(); + + converter.write(body, TEXT_PLAIN_UTF_8, outputMessage1); + assertThat(outputMessage1.getBodyAsString()).isEqualTo(body); + + MockHttpOutputMessage outputMessage2 = new MockHttpOutputMessage(); + converter.write(body, TEXT_PLAIN_UTF_8, outputMessage2); + assertThat(outputMessage2.getBodyAsString()).isEqualTo(body); + } + + } diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java index 8fe4c9429cd..2caed086497 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java @@ -580,6 +580,22 @@ public class MappingJackson2HttpMessageConverterTests { assertThat(result2).contains("\"property\":\"Value2\""); } + @Test + public void repeatableWrites() throws IOException { + MockHttpOutputMessage outputMessage1 = new MockHttpOutputMessage(); + MyBean body = new MyBean(); + body.setString("Foo"); + converter.write(body, null, outputMessage1); + String result = outputMessage1.getBodyAsString(StandardCharsets.UTF_8); + assertThat(result).contains("\"string\":\"Foo\""); + + MockHttpOutputMessage outputMessage2 = new MockHttpOutputMessage(); + converter.write(body, null, outputMessage2); + result = outputMessage2.getBodyAsString(StandardCharsets.UTF_8); + assertThat(result).contains("\"string\":\"Foo\""); + } + + interface MyInterface { diff --git a/spring-web/src/test/java/org/springframework/http/converter/xml/SourceHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/xml/SourceHttpMessageConverterTests.java index 6f7b5403109..9b4016354c8 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/xml/SourceHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/xml/SourceHttpMessageConverterTests.java @@ -290,6 +290,7 @@ public class SourceHttpMessageConverterTests { DOMSource domSource = new DOMSource(document); MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + assertThat(converter.supportsRepeatableWrites(domSource)).isTrue(); converter.write(domSource, null, outputMessage); assertThat(XmlContent.of(outputMessage.getBodyAsString(StandardCharsets.UTF_8))) .isSimilarTo("Hello World"); @@ -305,6 +306,7 @@ public class SourceHttpMessageConverterTests { SAXSource saxSource = new SAXSource(new InputSource(new StringReader(xml))); MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + assertThat(converter.supportsRepeatableWrites(saxSource)).isFalse(); converter.write(saxSource, null, outputMessage); assertThat(XmlContent.of(outputMessage.getBodyAsString(StandardCharsets.UTF_8))) .isSimilarTo("Hello World"); @@ -318,6 +320,7 @@ public class SourceHttpMessageConverterTests { StreamSource streamSource = new StreamSource(new StringReader(xml)); MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + assertThat(converter.supportsRepeatableWrites(streamSource)).isFalse(); converter.write(streamSource, null, outputMessage); assertThat(XmlContent.of(outputMessage.getBodyAsString(StandardCharsets.UTF_8))) .isSimilarTo("Hello World");