|
|
|
@ -28,6 +28,7 @@ import java.util.Collections; |
|
|
|
import java.util.LinkedHashMap; |
|
|
|
import java.util.LinkedHashMap; |
|
|
|
import java.util.List; |
|
|
|
import java.util.List; |
|
|
|
import java.util.Map; |
|
|
|
import java.util.Map; |
|
|
|
|
|
|
|
import java.util.Objects; |
|
|
|
|
|
|
|
|
|
|
|
import org.jspecify.annotations.Nullable; |
|
|
|
import org.jspecify.annotations.Nullable; |
|
|
|
|
|
|
|
|
|
|
|
@ -485,9 +486,18 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue |
|
|
|
outputMessage.getHeaders().setContentType(contentType); |
|
|
|
outputMessage.getHeaders().setContentType(contentType); |
|
|
|
|
|
|
|
|
|
|
|
if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) { |
|
|
|
if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) { |
|
|
|
streamingOutputMessage.setBody(outputStream -> { |
|
|
|
boolean repeatable = checkPartsRepeatable(parts); |
|
|
|
writeParts(outputStream, parts, boundary); |
|
|
|
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() { |
|
|
|
writeEnd(outputStream, boundary); |
|
|
|
@Override |
|
|
|
|
|
|
|
public void writeTo(OutputStream outputStream) throws IOException { |
|
|
|
|
|
|
|
FormHttpMessageConverter.this.writeParts(outputStream, parts, boundary); |
|
|
|
|
|
|
|
writeEnd(outputStream, boundary); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
|
|
|
|
public boolean repeatable() { |
|
|
|
|
|
|
|
return repeatable; |
|
|
|
|
|
|
|
} |
|
|
|
}); |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
else { |
|
|
|
else { |
|
|
|
@ -496,6 +506,35 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@SuppressWarnings({"unchecked", "ConstantValue"}) |
|
|
|
|
|
|
|
private <T> boolean checkPartsRepeatable(MultiValueMap<String, Object> map) { |
|
|
|
|
|
|
|
return map.entrySet().stream().allMatch(e -> e.getValue().stream().filter(Objects::nonNull).allMatch(part -> { |
|
|
|
|
|
|
|
HttpHeaders headers = null; |
|
|
|
|
|
|
|
Object body = part; |
|
|
|
|
|
|
|
if (part instanceof HttpEntity<?> entity) { |
|
|
|
|
|
|
|
headers = entity.getHeaders(); |
|
|
|
|
|
|
|
body = entity.getBody(); |
|
|
|
|
|
|
|
Assert.state(body != null, "Empty body for part '" + e.getKey() + "': " + part); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
HttpMessageConverter<?> converter = findConverterFor(e.getKey(), headers, body); |
|
|
|
|
|
|
|
return (converter instanceof AbstractHttpMessageConverter<?> ahmc && |
|
|
|
|
|
|
|
((AbstractHttpMessageConverter<T>) ahmc).supportsRepeatableWrites((T) body)); |
|
|
|
|
|
|
|
})); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private @Nullable HttpMessageConverter<?> findConverterFor( |
|
|
|
|
|
|
|
String name, @Nullable HttpHeaders headers, Object body) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Class<?> partType = body.getClass(); |
|
|
|
|
|
|
|
MediaType contentType = (headers != null ? headers.getContentType() : null); |
|
|
|
|
|
|
|
for (HttpMessageConverter<?> converter : this.partConverters) { |
|
|
|
|
|
|
|
if (converter.canWrite(partType, contentType)) { |
|
|
|
|
|
|
|
return converter; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return null; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* When {@link #setMultipartCharset(Charset)} is configured (i.e. RFC 2047, |
|
|
|
* When {@link #setMultipartCharset(Charset)} is configured (i.e. RFC 2047, |
|
|
|
* {@code encoded-word} syntax) we need to use ASCII for part headers, or |
|
|
|
* {@code encoded-word} syntax) we need to use ASCII for part headers, or |
|
|
|
@ -521,32 +560,27 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue |
|
|
|
@SuppressWarnings("unchecked") |
|
|
|
@SuppressWarnings("unchecked") |
|
|
|
private void writePart(String name, HttpEntity<?> partEntity, OutputStream os) throws IOException { |
|
|
|
private void writePart(String name, HttpEntity<?> partEntity, OutputStream os) throws IOException { |
|
|
|
Object partBody = partEntity.getBody(); |
|
|
|
Object partBody = partEntity.getBody(); |
|
|
|
if (partBody == null) { |
|
|
|
Assert.state(partBody != null, "Empty body for part '" + name + "': " + partEntity); |
|
|
|
throw new IllegalStateException("Empty body for part '" + name + "': " + partEntity); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
Class<?> partType = partBody.getClass(); |
|
|
|
|
|
|
|
HttpHeaders partHeaders = partEntity.getHeaders(); |
|
|
|
HttpHeaders partHeaders = partEntity.getHeaders(); |
|
|
|
MediaType partContentType = partHeaders.getContentType(); |
|
|
|
MediaType partContentType = partHeaders.getContentType(); |
|
|
|
for (HttpMessageConverter<?> messageConverter : this.partConverters) { |
|
|
|
HttpMessageConverter<?> converter = findConverterFor(name, partHeaders, partBody); |
|
|
|
if (messageConverter.canWrite(partType, partContentType)) { |
|
|
|
if (converter != null) { |
|
|
|
Charset charset = isFilenameCharsetSet() ? StandardCharsets.US_ASCII : this.charset; |
|
|
|
Charset charset = isFilenameCharsetSet() ? StandardCharsets.US_ASCII : this.charset; |
|
|
|
HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage(os, charset); |
|
|
|
HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage(os, charset); |
|
|
|
String filename = getFilename(partBody); |
|
|
|
String filename = getFilename(partBody); |
|
|
|
ContentDisposition.Builder cd = ContentDisposition.formData() |
|
|
|
ContentDisposition.Builder cd = ContentDisposition.formData().name(name); |
|
|
|
.name(name); |
|
|
|
if (filename != null) { |
|
|
|
if (filename != null) { |
|
|
|
cd.filename(filename, this.multipartCharset); |
|
|
|
cd.filename(filename, this.multipartCharset); |
|
|
|
} |
|
|
|
} |
|
|
|
multipartMessage.getHeaders().setContentDisposition(cd.build()); |
|
|
|
multipartMessage.getHeaders().setContentDisposition(cd.build()); |
|
|
|
if (!partHeaders.isEmpty()) { |
|
|
|
if (!partHeaders.isEmpty()) { |
|
|
|
multipartMessage.getHeaders().putAll(partHeaders); |
|
|
|
multipartMessage.getHeaders().putAll(partHeaders); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
((HttpMessageConverter<Object>) messageConverter).write(partBody, partContentType, multipartMessage); |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
((HttpMessageConverter<Object>) converter).write(partBody, partContentType, multipartMessage); |
|
|
|
|
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
throw new HttpMessageNotWritableException("Could not write request: no suitable HttpMessageConverter " + |
|
|
|
throw new HttpMessageNotWritableException("Could not write request: " + |
|
|
|
"found for request type [" + partType.getName() + "]"); |
|
|
|
"no suitable HttpMessageConverter found for request type [" + partBody.getClass().getName() + "]"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
|