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 a72f1ab1873..e80ddc4a330 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 @@ -117,7 +117,7 @@ public abstract class AbstractGenericHttpMessageConverter extends AbstractHtt } @Override public boolean repeatable() { - return supportsRepeatableWrites(t); + return canWriteRepeatedly(t, contentType); } }); } 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 c1f587eecc8..1c6b5fee0a3 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 @@ -304,9 +304,11 @@ public abstract class AbstractHttpMessageConverter implements HttpMessageConv * @return {@code true} if {@code t} can be written repeatedly; * {@code false} otherwise * @since 6.1 + * @deprecated since 7.1 in favor of {@link #canWriteRepeatedly(Object, MediaType)}. */ + @Deprecated(since = "7.1", forRemoval = true) protected boolean supportsRepeatableWrites(T t) { - return false; + return canWriteRepeatedly(t, null); } diff --git a/spring-web/src/main/java/org/springframework/http/converter/AbstractJacksonHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/AbstractJacksonHttpMessageConverter.java index 40d6847b484..b9f730f0dcf 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/AbstractJacksonHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/AbstractJacksonHttpMessageConverter.java @@ -285,6 +285,11 @@ public abstract class AbstractJacksonHttpMessageConverter hints) throws IOException, HttpMessageNotReadableException { @@ -194,6 +199,7 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter extends AbstractHttpM } @Override public boolean repeatable() { - return supportsRepeatableWrites(t); + return canWriteRepeatedly(t, contentType); } }); } 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 b924827739e..b893b6a4972 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 @@ -50,6 +50,11 @@ public class ByteArrayHttpMessageConverter extends AbstractHttpMessageConverter< return byte[].class == clazz; } + @Override + public boolean canWriteRepeatedly(byte[] bytes, @Nullable MediaType contentType) { + return true; + } + @Override public byte[] readInternal(Class clazz, HttpInputMessage message) throws IOException { long length = message.getHeaders().getContentLength(); @@ -68,6 +73,7 @@ public class ByteArrayHttpMessageConverter extends AbstractHttpMessageConverter< } @Override + @SuppressWarnings("removal") 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 fd3ad3012f1..193a5429949 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 @@ -486,7 +486,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter boolean checkPartsRepeatable(MultiValueMap map) { + private boolean checkPartsRepeatable(MultiValueMap map, MediaType contentType) { return map.entrySet().stream().allMatch(e -> e.getValue().stream().filter(Objects::nonNull).allMatch(part -> { HttpHeaders headers = null; Object body = part; @@ -515,9 +515,8 @@ public class FormHttpMessageConverter implements HttpMessageConverter converter = findConverterFor(e.getKey(), headers, body); - return (converter instanceof AbstractHttpMessageConverter ahmc && - ((AbstractHttpMessageConverter) ahmc).supportsRepeatableWrites((T) body)); + HttpMessageConverter converter = (HttpMessageConverter) findConverterFor(e.getKey(), headers, body); + return converter != null && converter.canWriteRepeatedly((T) body, contentType); })); } diff --git a/spring-web/src/main/java/org/springframework/http/converter/HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/HttpMessageConverter.java index f2419d004a6..b7adaeb3492 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/HttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/HttpMessageConverter.java @@ -108,4 +108,22 @@ public interface HttpMessageConverter { void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException; + /** + * Indicates whether this message converter can + * {@linkplain #write(Object, MediaType, HttpOutputMessage) write} the + * given payload multiple times. + *

This can be used by HTTP client libraries to know whether a message can be + * sent again, for example after an HTTP redirect. The default implementation + * returns {@code false}. This typically returns false if the payload can be read + * only once. + * @param t the object t + * @param contentType the content type to use when writing. + * @return {@code true} if {@code t} can be written repeatedly; + * {@code false} otherwise + * @since 7.1 + */ + default boolean canWriteRepeatedly(T t, @Nullable MediaType contentType) { + return false; + } + } 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 b305180831e..14a1cfc5bb4 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 @@ -99,6 +99,11 @@ public class ObjectToStringHttpMessageConverter extends AbstractHttpMessageConve return canWrite(mediaType) && this.conversionService.canConvert(clazz, String.class); } + @Override + public boolean canWriteRepeatedly(Object o, @Nullable MediaType contentType) { + return true; + } + @Override protected boolean supports(Class clazz) { // should not be called, since we override canRead/Write @@ -137,6 +142,7 @@ public class ObjectToStringHttpMessageConverter extends AbstractHttpMessageConve } @Override + @SuppressWarnings("removal") 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 6365f10c3f0..0faea6dc8a5 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 @@ -71,6 +71,11 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter clazz) { return Resource.class.isAssignableFrom(clazz); @@ -141,8 +146,9 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter regions = (Collection) object; + for (ResourceRegion region : regions) { + if (!supportsRepeatableWrites(region)) { + return false; + } + } + return true; + } + } + @Override protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { @@ -140,20 +157,9 @@ public class ResourceRegionHttpMessageConverter extends AbstractGenericHttpMessa } @Override + @SuppressWarnings("removal") protected boolean supportsRepeatableWrites(Object object) { - if (object instanceof ResourceRegion resourceRegion) { - return supportsRepeatableWrites(resourceRegion); - } - else { - @SuppressWarnings("unchecked") - Collection regions = (Collection) object; - for (ResourceRegion region : regions) { - if (!supportsRepeatableWrites(region)) { - return false; - } - } - return true; - } + return canWriteRepeatedly(object, null); } private boolean supportsRepeatableWrites(ResourceRegion region) { 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 5a9e81e90da..e280c0b4f10 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 @@ -89,6 +89,11 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter clazz, HttpInputMessage inputMessage) throws IOException { Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType()); @@ -161,6 +166,7 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter } + @Override + public boolean canWriteRepeatedly(T t, @Nullable MediaType contentType) { + return true; + } + @Override @SuppressWarnings("unchecked") protected T readInternal(Class clazz, HttpInputMessage inputMessage) @@ -108,6 +114,7 @@ public abstract class AbstractWireFeedHttpMessageConverter } @Override + @SuppressWarnings("removal") 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 62a8219bb45..22b07ac1928 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 @@ -292,6 +292,11 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener return false; } + @Override + public boolean canWriteRepeatedly(Object o, @Nullable MediaType contentType) { + return true; + } + /** * Select an ObjectMapper to use, either the main ObjectMapper or another * if the handling for the given Class has been customized through @@ -570,6 +575,7 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener } @Override + @SuppressWarnings("removal") 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 183a8afb388..b1f58ec8bd6 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 @@ -24,6 +24,7 @@ import java.lang.reflect.Type; import com.google.gson.Gson; import org.jspecify.annotations.Nullable; +import org.springframework.http.MediaType; import org.springframework.util.Assert; /** @@ -86,6 +87,10 @@ public class GsonHttpMessageConverter extends AbstractJsonHttpMessageConverter { return this.gson; } + @Override + public boolean canWriteRepeatedly(Object o, @Nullable MediaType contentType) { + return true; + } @Override protected Object readInternal(Type resolvedType, Reader reader) throws Exception { @@ -109,6 +114,7 @@ public class GsonHttpMessageConverter extends AbstractJsonHttpMessageConverter { } @Override + @SuppressWarnings("removal") 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 53300311cf2..5cc7e5ae98f 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 @@ -26,6 +26,7 @@ import jakarta.json.bind.JsonbBuilder; import jakarta.json.bind.JsonbConfig; import org.jspecify.annotations.Nullable; +import org.springframework.http.MediaType; import org.springframework.util.Assert; /** @@ -94,6 +95,10 @@ public class JsonbHttpMessageConverter extends AbstractJsonHttpMessageConverter return this.jsonb; } + @Override + public boolean canWriteRepeatedly(Object o, @Nullable MediaType contentType) { + return true; + } @Override protected Object readInternal(Type resolvedType, Reader reader) throws Exception { @@ -111,6 +116,7 @@ public class JsonbHttpMessageConverter extends AbstractJsonHttpMessageConverter } @Override + @SuppressWarnings("removal") 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 8f5eaacbe7c..8e4b12767e5 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 @@ -122,6 +122,12 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter clazz) { // should not be called, since we override canRead/Write @@ -235,6 +240,7 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa } @Override + @SuppressWarnings("removal") protected boolean supportsRepeatableWrites(Object o) { return true; } 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 459436a4fcf..0c383e5b958 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 @@ -114,6 +114,11 @@ public class MarshallingHttpMessageConverter extends AbstractXmlHttpMessageConve return (canWrite(mediaType) && this.marshaller != null && this.marshaller.supports(clazz)); } + @Override + public boolean canWriteRepeatedly(Object o, @Nullable MediaType contentType) { + return true; + } + @Override protected boolean supports(Class clazz) { // should not be called, since we override canRead()/canWrite() @@ -137,6 +142,7 @@ public class MarshallingHttpMessageConverter extends AbstractXmlHttpMessageConve } @Override + @SuppressWarnings("removal") 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 3cc4cb533e4..80ea8de6acc 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 @@ -148,6 +148,11 @@ public class SourceHttpMessageConverter extends AbstractHttpMe return SUPPORTED_CLASSES.contains(clazz); } + @Override + public boolean canWriteRepeatedly(T t, @Nullable MediaType contentType) { + return t instanceof DOMSource; + } + @Override @SuppressWarnings("unchecked") protected T readInternal(Class clazz, HttpInputMessage inputMessage) @@ -293,8 +298,9 @@ public class SourceHttpMessageConverter extends AbstractHttpMe } @Override + @SuppressWarnings("removal") protected boolean supportsRepeatableWrites(T t) { - return t instanceof DOMSource; + return canWriteRepeatedly(t, null); }