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
This commit is contained in:
parent
ab316d9bc8
commit
6dd93d4d85
|
|
@ -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
|
|||
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);
|
||||
|
|
|
|||
|
|
@ -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
|
|||
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.
|
||||
|
|
|
|||
|
|
@ -178,4 +178,9 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean supportsRepeatableWrites(Object object) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
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());
|
||||
|
|
|
|||
|
|
@ -67,4 +67,8 @@ public class ByteArrayHttpMessageConverter extends AbstractHttpMessageConverter<
|
|||
StreamUtils.copy(bytes, outputMessage.getBody());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean supportsRepeatableWrites(byte[] bytes) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean supportsRepeatableWrites(Resource resource) {
|
||||
return !(resource instanceof InputStreamResource);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
|||
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
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -163,4 +163,8 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter<Str
|
|||
return charset;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean supportsRepeatableWrites(String s) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean supportsRepeatableWrites(T t) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -568,4 +568,8 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
|
|||
return super.getContentLength(object, contentType);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean supportsRepeatableWrites(Object o) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean supportsRepeatableWrites(Object o) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private static final EntityResolver NO_OP_ENTITY_RESOLVER =
|
||||
(publicId, systemId) -> new InputSource(new StringReader(""));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
this.transformerFactory.newTransformer().transform(source, result);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean supportsRepeatableWrites(T t) {
|
||||
return t instanceof DOMSource;
|
||||
}
|
||||
|
||||
|
||||
private static class CountingOutputStream extends OutputStream {
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
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 {
|
|||
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…
Reference in New Issue