Browse Source

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
pull/31513/head
Arjen Poutsma 2 years ago
parent
commit
6dd93d4d85
  1. 25
      spring-web/src/main/java/org/springframework/http/converter/AbstractGenericHttpMessageConverter.java
  2. 38
      spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java
  3. 5
      spring-web/src/main/java/org/springframework/http/converter/AbstractKotlinSerializationHttpMessageConverter.java
  4. 14
      spring-web/src/main/java/org/springframework/http/converter/BufferedImageHttpMessageConverter.java
  5. 4
      spring-web/src/main/java/org/springframework/http/converter/ByteArrayHttpMessageConverter.java
  6. 12
      spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java
  7. 6
      spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java
  8. 6
      spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java
  9. 23
      spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java
  10. 4
      spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java
  11. 6
      spring-web/src/main/java/org/springframework/http/converter/feed/AbstractWireFeedHttpMessageConverter.java
  12. 4
      spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java
  13. 6
      spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java
  14. 6
      spring-web/src/main/java/org/springframework/http/converter/json/JsonbHttpMessageConverter.java
  15. 4
      spring-web/src/main/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverter.java
  16. 7
      spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java
  17. 6
      spring-web/src/main/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverter.java
  18. 7
      spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java
  19. 14
      spring-web/src/test/java/org/springframework/http/converter/ByteArrayHttpMessageConverterTests.java
  20. 15
      spring-web/src/test/java/org/springframework/http/converter/StringHttpMessageConverterTests.java
  21. 16
      spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java
  22. 3
      spring-web/src/test/java/org/springframework/http/converter/xml/SourceHttpMessageConverterTests.java

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

@ -1,5 +1,5 @@ @@ -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<T> extends AbstractHtt @@ -88,16 +88,27 @@ public abstract class AbstractGenericHttpMessageConverter<T> 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);

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

@ -210,16 +210,27 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv @@ -210,16 +210,27 @@ public abstract class AbstractHttpMessageConverter<T> 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<T> implements HttpMessageConv @@ -289,6 +300,21 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv
return null;
}
/**
* Indicates whether this message converter can
* {@linkplain #write(Object, MediaType, HttpOutputMessage) write} the
* given object multiple times.
*
* <p>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.

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

@ -178,4 +178,9 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends @@ -178,4 +178,9 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends
}
return false;
}
@Override
protected boolean supportsRepeatableWrites(Object object) {
return true;
}
}

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

@ -1,5 +1,5 @@ @@ -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.
@ -226,7 +226,17 @@ public class BufferedImageHttpMessageConverter implements HttpMessageConverter<B @@ -226,7 +226,17 @@ public class BufferedImageHttpMessageConverter implements HttpMessageConverter<B
outputMessage.getHeaders().setContentType(selectedContentType);
if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) {
streamingOutputMessage.setBody(outputStream -> 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());

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

@ -67,4 +67,8 @@ public class ByteArrayHttpMessageConverter extends AbstractHttpMessageConverter< @@ -67,4 +67,8 @@ public class ByteArrayHttpMessageConverter extends AbstractHttpMessageConverter<
StreamUtils.copy(bytes, outputMessage.getBody());
}
@Override
protected boolean supportsRepeatableWrites(byte[] bytes) {
return true;
}
}

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

@ -400,7 +400,17 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue @@ -400,7 +400,17 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
outputMessage.getHeaders().setContentLength(bytes.length);
if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) {
streamingOutputMessage.setBody(outputStream -> 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());

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

@ -1,5 +1,5 @@ @@ -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 @@ -135,4 +135,8 @@ public class ObjectToStringHttpMessageConverter extends AbstractHttpMessageConve
return this.stringHttpMessageConverter.getContentLength(value, contentType);
}
@Override
protected boolean supportsRepeatableWrites(Object o) {
return true;
}
}

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

@ -1,5 +1,5 @@ @@ -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<R @@ -166,4 +166,8 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<R
}
}
@Override
protected boolean supportsRepeatableWrites(Resource resource) {
return !(resource instanceof InputStreamResource);
}
}

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

@ -1,5 +1,5 @@ @@ -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.
@ -24,6 +24,7 @@ import java.lang.reflect.Type; @@ -24,6 +24,7 @@ import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourceRegion;
import org.springframework.http.HttpHeaders;
@ -238,4 +239,24 @@ public class ResourceRegionHttpMessageConverter extends AbstractGenericHttpMessa @@ -238,4 +239,24 @@ public class ResourceRegionHttpMessageConverter extends AbstractGenericHttpMessa
os.write(buf.getBytes(StandardCharsets.US_ASCII));
}
@Override
@SuppressWarnings("unchecked")
protected boolean supportsRepeatableWrites(Object object) {
if (object instanceof ResourceRegion resourceRegion) {
return supportsRepeatableWrites(resourceRegion);
}
else {
Collection<ResourceRegion> regions = (Collection<ResourceRegion>) object;
for (ResourceRegion region : regions) {
if (!supportsRepeatableWrites(region)) {
return false;
}
}
return true;
}
}
private boolean supportsRepeatableWrites(ResourceRegion region) {
return !(region.getResource() instanceof InputStreamResource);
}
}

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

@ -163,4 +163,8 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter<Str @@ -163,4 +163,8 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter<Str
return charset;
}
@Override
protected boolean supportsRepeatableWrites(String s) {
return true;
}
}

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

@ -1,5 +1,5 @@ @@ -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.
@ -107,4 +107,8 @@ public abstract class AbstractWireFeedHttpMessageConverter<T extends WireFeed> @@ -107,4 +107,8 @@ public abstract class AbstractWireFeedHttpMessageConverter<T extends WireFeed>
}
}
@Override
protected boolean supportsRepeatableWrites(T t) {
return true;
}
}

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

@ -568,4 +568,8 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener @@ -568,4 +568,8 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
return super.getContentLength(object, contentType);
}
@Override
protected boolean supportsRepeatableWrites(Object o) {
return true;
}
}

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

@ -1,5 +1,5 @@ @@ -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 { @@ -107,4 +107,8 @@ public class GsonHttpMessageConverter extends AbstractJsonHttpMessageConverter {
}
}
@Override
protected boolean supportsRepeatableWrites(Object o) {
return true;
}
}

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

@ -1,5 +1,5 @@ @@ -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 @@ -110,4 +110,8 @@ public class JsonbHttpMessageConverter extends AbstractJsonHttpMessageConverter
}
}
@Override
protected boolean supportsRepeatableWrites(Object o) {
return true;
}
}

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

@ -247,6 +247,10 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<M @@ -247,6 +247,10 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<M
response.getHeaders().set(X_PROTOBUF_MESSAGE_HEADER, message.getDescriptorForType().getFullName());
}
@Override
protected boolean supportsRepeatableWrites(Message message) {
return true;
}
/**
* Protobuf format support.

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

@ -1,5 +1,5 @@ @@ -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.
@ -200,6 +200,11 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa @@ -200,6 +200,11 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa
}
}
@Override
protected boolean supportsRepeatableWrites(Object o) {
return true;
}
private static final EntityResolver NO_OP_ENTITY_RESOLVER =
(publicId, systemId) -> new InputSource(new StringReader(""));

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

@ -1,5 +1,5 @@ @@ -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 @@ -137,4 +137,8 @@ public class MarshallingHttpMessageConverter extends AbstractXmlHttpMessageConve
this.marshaller.marshal(o, result);
}
@Override
protected boolean supportsRepeatableWrites(Object o) {
return true;
}
}

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

@ -1,5 +1,5 @@ @@ -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<T extends Source> extends AbstractHttpMe @@ -269,6 +269,11 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe
this.transformerFactory.newTransformer().transform(source, result);
}
@Override
protected boolean supportsRepeatableWrites(T t) {
return t instanceof DOMSource;
}
private static class CountingOutputStream extends OutputStream {

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

@ -79,4 +79,18 @@ public class ByteArrayHttpMessageConverterTests { @@ -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);
}
}

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

@ -171,4 +171,19 @@ class StringHttpMessageConverterTests { @@ -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);
}
}

16
spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java

@ -580,6 +580,22 @@ public class MappingJackson2HttpMessageConverterTests { @@ -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 {

3
spring-web/src/test/java/org/springframework/http/converter/xml/SourceHttpMessageConverterTests.java

@ -290,6 +290,7 @@ public class SourceHttpMessageConverterTests { @@ -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("<root>Hello World</root>");
@ -305,6 +306,7 @@ public class SourceHttpMessageConverterTests { @@ -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("<root>Hello World</root>");
@ -318,6 +320,7 @@ public class SourceHttpMessageConverterTests { @@ -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("<root>Hello World</root>");

Loading…
Cancel
Save