Improve support for Content-Disposition types
See gh-25647
This commit is contained in:
committed by
Rossen Stoyanchev
parent
e66e34766e
commit
6e640c806f
@@ -48,6 +48,18 @@ public final class ContentDisposition {
|
||||
private static final String INVALID_HEADER_FIELD_PARAMETER_FORMAT =
|
||||
"Invalid header field parameter format (as defined in RFC 5987)";
|
||||
|
||||
/**
|
||||
* The {@literal attachment} content-disposition type.
|
||||
*/
|
||||
private static final String ATTACHMENT = "attachment";
|
||||
/**
|
||||
* The {@literal form-data} content-disposition type.
|
||||
*/
|
||||
private static final String FORM_DATA = "form-data";
|
||||
/**
|
||||
* The {@literal inline} content-disposition type.
|
||||
*/
|
||||
private static final String INLINE = "inline";
|
||||
|
||||
@Nullable
|
||||
private final String type;
|
||||
@@ -174,6 +186,29 @@ public final class ContentDisposition {
|
||||
return this.readDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the {@link #getType() type} is {@literal attachment}.
|
||||
* @since 5.3
|
||||
*/
|
||||
public boolean isAttachment() {
|
||||
return ATTACHMENT.equals(this.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the {@link #getType() type} is {@literal form-data}.
|
||||
* @since 5.3
|
||||
*/
|
||||
public boolean isFormData() {
|
||||
return FORM_DATA.equals(this.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the {@link #getType() type} is {@literal inline}.
|
||||
* @since 5.3
|
||||
*/
|
||||
public boolean isInline() {
|
||||
return INLINE.equals(this.type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
@@ -264,6 +299,36 @@ public final class ContentDisposition {
|
||||
return new BuilderImpl(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a builder for a {@code ContentDisposition} with
|
||||
* the {@link #ATTACHMENT attachment} type.
|
||||
* @return the builder
|
||||
* @since 5.3
|
||||
*/
|
||||
public static Builder attachment() {
|
||||
return builder(ATTACHMENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a builder for a {@code ContentDisposition} with
|
||||
* the {@link #FORM_DATA form-data} type.
|
||||
* @return the builder
|
||||
* @since 5.3
|
||||
*/
|
||||
public static Builder formData() {
|
||||
return builder(FORM_DATA);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a builder for a {@code ContentDisposition} with
|
||||
* the {@link #INLINE inline} type.
|
||||
* @return the builder
|
||||
* @since 5.3
|
||||
*/
|
||||
public static Builder inline() {
|
||||
return builder(INLINE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an empty content disposition.
|
||||
*/
|
||||
|
||||
@@ -860,7 +860,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
|
||||
*/
|
||||
public void setContentDispositionFormData(String name, @Nullable String filename) {
|
||||
Assert.notNull(name, "Name must not be null");
|
||||
ContentDisposition.Builder disposition = ContentDisposition.builder("form-data").name(name);
|
||||
ContentDisposition.Builder disposition = ContentDisposition.formData().name(name);
|
||||
if (StringUtils.hasText(filename)) {
|
||||
disposition.filename(filename);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.springframework.http.ContentDisposition.builder;
|
||||
import static org.springframework.http.ContentDisposition.attachment;
|
||||
import static org.springframework.http.ContentDisposition.formData;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ContentDisposition}
|
||||
@@ -41,7 +42,7 @@ class ContentDispositionTests {
|
||||
@SuppressWarnings("deprecation")
|
||||
void parse() {
|
||||
assertThat(parse("form-data; name=\"foo\"; filename=\"foo.txt\"; size=123"))
|
||||
.isEqualTo(builder("form-data")
|
||||
.isEqualTo(formData()
|
||||
.name("foo")
|
||||
.filename("foo.txt")
|
||||
.size(123L)
|
||||
@@ -51,7 +52,7 @@ class ContentDispositionTests {
|
||||
@Test
|
||||
void parseFilenameUnquoted() {
|
||||
assertThat(parse("form-data; filename=unquoted"))
|
||||
.isEqualTo(builder("form-data")
|
||||
.isEqualTo(formData()
|
||||
.filename("unquoted")
|
||||
.build());
|
||||
}
|
||||
@@ -59,7 +60,7 @@ class ContentDispositionTests {
|
||||
@Test // SPR-16091
|
||||
void parseFilenameWithSemicolon() {
|
||||
assertThat(parse("attachment; filename=\"filename with ; semicolon.txt\""))
|
||||
.isEqualTo(builder("attachment")
|
||||
.isEqualTo(attachment()
|
||||
.filename("filename with ; semicolon.txt")
|
||||
.build());
|
||||
}
|
||||
@@ -67,7 +68,7 @@ class ContentDispositionTests {
|
||||
@Test
|
||||
void parseEncodedFilename() {
|
||||
assertThat(parse("form-data; name=\"name\"; filename*=UTF-8''%E4%B8%AD%E6%96%87.txt"))
|
||||
.isEqualTo(builder("form-data")
|
||||
.isEqualTo(formData()
|
||||
.name("name")
|
||||
.filename("中文.txt", StandardCharsets.UTF_8)
|
||||
.build());
|
||||
@@ -76,7 +77,7 @@ class ContentDispositionTests {
|
||||
@Test // gh-24112
|
||||
void parseEncodedFilenameWithPaddedCharset() {
|
||||
assertThat(parse("attachment; filename*= UTF-8''some-file.zip"))
|
||||
.isEqualTo(builder("attachment")
|
||||
.isEqualTo(attachment()
|
||||
.filename("some-file.zip", StandardCharsets.UTF_8)
|
||||
.build());
|
||||
}
|
||||
@@ -84,7 +85,7 @@ class ContentDispositionTests {
|
||||
@Test
|
||||
void parseEncodedFilenameWithoutCharset() {
|
||||
assertThat(parse("form-data; name=\"name\"; filename*=test.txt"))
|
||||
.isEqualTo(builder("form-data")
|
||||
.isEqualTo(formData()
|
||||
.name("name")
|
||||
.filename("test.txt")
|
||||
.build());
|
||||
@@ -111,7 +112,7 @@ class ContentDispositionTests {
|
||||
BiConsumer<String, String> tester = (description, filename) ->
|
||||
assertThat(parse("form-data; name=\"file\"; filename=\"" + filename + "\"; size=123"))
|
||||
.as(description)
|
||||
.isEqualTo(builder("form-data").name("file").filename(filename).size(123L).build());
|
||||
.isEqualTo(formData().name("file").filename(filename).size(123L).build());
|
||||
|
||||
tester.accept("Escaped quotes should be ignored",
|
||||
"\\\"The Twilight Zone\\\".txt");
|
||||
@@ -130,7 +131,7 @@ class ContentDispositionTests {
|
||||
@SuppressWarnings("deprecation")
|
||||
void parseWithExtraSemicolons() {
|
||||
assertThat(parse("form-data; name=\"foo\";; ; filename=\"foo.txt\"; size=123"))
|
||||
.isEqualTo(builder("form-data")
|
||||
.isEqualTo(formData()
|
||||
.name("foo")
|
||||
.filename("foo.txt")
|
||||
.size(123L)
|
||||
@@ -149,7 +150,7 @@ class ContentDispositionTests {
|
||||
"creation-date=\"" + creationTime.format(formatter) + "\"; " +
|
||||
"modification-date=\"" + modificationTime.format(formatter) + "\"; " +
|
||||
"read-date=\"" + readTime.format(formatter) + "\"")).isEqualTo(
|
||||
builder("attachment")
|
||||
attachment()
|
||||
.creationDate(creationTime)
|
||||
.modificationDate(modificationTime)
|
||||
.readDate(readTime)
|
||||
@@ -166,7 +167,7 @@ class ContentDispositionTests {
|
||||
"creation-date=\"-1\"; " +
|
||||
"modification-date=\"-1\"; " +
|
||||
"read-date=\"" + readTime.format(formatter) + "\"")).isEqualTo(
|
||||
builder("attachment")
|
||||
attachment()
|
||||
.readDate(readTime)
|
||||
.build());
|
||||
}
|
||||
@@ -195,7 +196,7 @@ class ContentDispositionTests {
|
||||
@SuppressWarnings("deprecation")
|
||||
void format() {
|
||||
assertThat(
|
||||
builder("form-data")
|
||||
formData()
|
||||
.name("foo")
|
||||
.filename("foo.txt")
|
||||
.size(123L)
|
||||
@@ -206,7 +207,7 @@ class ContentDispositionTests {
|
||||
@Test
|
||||
void formatWithEncodedFilename() {
|
||||
assertThat(
|
||||
builder("form-data")
|
||||
formData()
|
||||
.name("name")
|
||||
.filename("中文.txt", StandardCharsets.UTF_8)
|
||||
.build().toString())
|
||||
@@ -216,7 +217,7 @@ class ContentDispositionTests {
|
||||
@Test
|
||||
void formatWithEncodedFilenameUsingUsAscii() {
|
||||
assertThat(
|
||||
builder("form-data")
|
||||
formData()
|
||||
.name("name")
|
||||
.filename("test.txt", StandardCharsets.US_ASCII)
|
||||
.build()
|
||||
@@ -229,10 +230,10 @@ class ContentDispositionTests {
|
||||
|
||||
BiConsumer<String, String> tester = (input, output) -> {
|
||||
|
||||
assertThat(builder("form-data").filename(input).build().toString())
|
||||
assertThat(formData().filename(input).build().toString())
|
||||
.isEqualTo("form-data; filename=\"" + output + "\"");
|
||||
|
||||
assertThat(builder("form-data").filename(input, StandardCharsets.US_ASCII).build().toString())
|
||||
assertThat(formData().filename(input, StandardCharsets.US_ASCII).build().toString())
|
||||
.isEqualTo("form-data; filename=\"" + output + "\"");
|
||||
};
|
||||
|
||||
@@ -262,7 +263,7 @@ class ContentDispositionTests {
|
||||
@Test
|
||||
void formatWithEncodedFilenameUsingInvalidCharset() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
builder("form-data")
|
||||
formData()
|
||||
.name("name")
|
||||
.filename("test.txt", StandardCharsets.UTF_16)
|
||||
.build()
|
||||
|
||||
@@ -384,7 +384,7 @@ public class HttpHeadersTests {
|
||||
assertThat(disposition).isNotNull();
|
||||
assertThat(headers.getContentDisposition()).as("Invalid Content-Disposition header").isEqualTo(ContentDisposition.empty());
|
||||
|
||||
disposition = ContentDisposition.builder("attachment").name("foo").filename("foo.txt").size(123L).build();
|
||||
disposition = ContentDisposition.attachment().name("foo").filename("foo.txt").size(123L).build();
|
||||
headers.setContentDisposition(disposition);
|
||||
assertThat(headers.getContentDisposition()).as("Invalid Content-Disposition header").isEqualTo(disposition);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ public class ResourceHttpMessageReaderTests extends AbstractLeakCheckingTests {
|
||||
String body = "Test resource content";
|
||||
|
||||
ContentDisposition contentDisposition =
|
||||
ContentDisposition.builder("attachment").name("file").filename(filename).build();
|
||||
ContentDisposition.attachment().name("file").filename(filename).build();
|
||||
|
||||
MockClientHttpResponse response = new MockClientHttpResponse(HttpStatus.OK);
|
||||
response.getHeaders().setContentType(MediaType.TEXT_PLAIN);
|
||||
|
||||
@@ -67,7 +67,7 @@ public class ResourceHttpMessageConverterTests {
|
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body);
|
||||
inputMessage.getHeaders().setContentType(MediaType.IMAGE_JPEG);
|
||||
inputMessage.getHeaders().setContentDisposition(
|
||||
ContentDisposition.builder("attachment").filename("yourlogo.jpg").build());
|
||||
ContentDisposition.attachment().filename("yourlogo.jpg").build());
|
||||
Resource actualResource = converter.read(Resource.class, inputMessage);
|
||||
assertThat(FileCopyUtils.copyToByteArray(actualResource.getInputStream())).isEqualTo(body);
|
||||
assertThat(actualResource.getFilename()).isEqualTo("yourlogo.jpg");
|
||||
@@ -79,7 +79,7 @@ public class ResourceHttpMessageConverterTests {
|
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body);
|
||||
inputMessage.getHeaders().setContentType(MediaType.IMAGE_JPEG);
|
||||
inputMessage.getHeaders().setContentDisposition(
|
||||
ContentDisposition.builder("attachment").filename("yourlogo.jpg").build());
|
||||
ContentDisposition.attachment().filename("yourlogo.jpg").build());
|
||||
inputMessage.getHeaders().setContentLength(123);
|
||||
Resource actualResource = converter.read(InputStreamResource.class, inputMessage);
|
||||
assertThat(actualResource).isInstanceOf(InputStreamResource.class);
|
||||
|
||||
Reference in New Issue
Block a user