Support date properties in Content-Disposition HTTP header
Issue: SPR-15555
This commit is contained in:
parent
e0e6736bc5
commit
97909f2258
|
@ -19,6 +19,8 @@ package org.springframework.http;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.format.DateTimeParseException;
|
||||||
|
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
@ -26,6 +28,7 @@ import org.springframework.util.ObjectUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import static java.nio.charset.StandardCharsets.*;
|
import static java.nio.charset.StandardCharsets.*;
|
||||||
|
import static java.time.format.DateTimeFormatter.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represent the Content-Disposition type and parameters as defined in RFC 2183.
|
* Represent the Content-Disposition type and parameters as defined in RFC 2183.
|
||||||
|
@ -47,18 +50,26 @@ public class ContentDisposition {
|
||||||
|
|
||||||
private final Long size;
|
private final Long size;
|
||||||
|
|
||||||
|
private final ZonedDateTime creationDate;
|
||||||
|
|
||||||
|
private final ZonedDateTime modificationDate;
|
||||||
|
|
||||||
|
private final ZonedDateTime readDate;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Private constructor. See static factory methods in this class.
|
* Private constructor. See static factory methods in this class.
|
||||||
*/
|
*/
|
||||||
private ContentDisposition(@Nullable String type, @Nullable String name, @Nullable String filename,
|
private ContentDisposition(@Nullable String type, @Nullable String name, @Nullable String filename, @Nullable Charset charset, @Nullable Long size,
|
||||||
@Nullable Charset charset, @Nullable Long size) {
|
@Nullable ZonedDateTime creationDate, @Nullable ZonedDateTime modificationDate, @Nullable ZonedDateTime readDate) {
|
||||||
|
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.filename = filename;
|
this.filename = filename;
|
||||||
this.charset = charset;
|
this.charset = charset;
|
||||||
this.size = size;
|
this.size = size;
|
||||||
|
this.creationDate = creationDate;
|
||||||
|
this.modificationDate = modificationDate;
|
||||||
|
this.readDate = readDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -104,6 +115,30 @@ public class ContentDisposition {
|
||||||
return this.size;
|
return this.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the value of the {@literal creation-date} parameter, or {@code null} if not defined.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public ZonedDateTime getCreationDate() {
|
||||||
|
return this.creationDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the value of the {@literal modification-date} parameter, or {@code null} if not defined.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public ZonedDateTime getModificationDate() {
|
||||||
|
return this.modificationDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the value of the {@literal read-date} parameter, or {@code null} if not defined.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public ZonedDateTime getReadDate() {
|
||||||
|
return this.readDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a builder for a {@code ContentDisposition}.
|
* Return a builder for a {@code ContentDisposition}.
|
||||||
|
@ -119,11 +154,12 @@ public class ContentDisposition {
|
||||||
* Return an empty content disposition.
|
* Return an empty content disposition.
|
||||||
*/
|
*/
|
||||||
public static ContentDisposition empty() {
|
public static ContentDisposition empty() {
|
||||||
return new ContentDisposition("", null, null, null, null);
|
return new ContentDisposition("", null, null, null, null, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a {@literal Content-Disposition} header value as defined in RFC 2183.
|
* Parse a {@literal Content-Disposition} header value as defined in RFC 2183.
|
||||||
|
*
|
||||||
* @param contentDisposition the {@literal Content-Disposition} header value
|
* @param contentDisposition the {@literal Content-Disposition} header value
|
||||||
* @return the parsed content disposition
|
* @return the parsed content disposition
|
||||||
* @see #toString()
|
* @see #toString()
|
||||||
|
@ -136,6 +172,9 @@ public class ContentDisposition {
|
||||||
String filename = null;
|
String filename = null;
|
||||||
Charset charset = null;
|
Charset charset = null;
|
||||||
Long size = null;
|
Long size = null;
|
||||||
|
ZonedDateTime creationDate = null;
|
||||||
|
ZonedDateTime modificationDate = null;
|
||||||
|
ZonedDateTime readDate = null;
|
||||||
for (int i = 1; i < parts.length; i++) {
|
for (int i = 1; i < parts.length; i++) {
|
||||||
String part = parts[i];
|
String part = parts[i];
|
||||||
int eqIndex = part.indexOf('=');
|
int eqIndex = part.indexOf('=');
|
||||||
|
@ -159,12 +198,36 @@ public class ContentDisposition {
|
||||||
else if (attribute.equals("size") ) {
|
else if (attribute.equals("size") ) {
|
||||||
size = Long.parseLong(value);
|
size = Long.parseLong(value);
|
||||||
}
|
}
|
||||||
|
else if (attribute.equals("creation-date")) {
|
||||||
|
try {
|
||||||
|
creationDate = ZonedDateTime.parse(value, RFC_1123_DATE_TIME);
|
||||||
|
}
|
||||||
|
catch (DateTimeParseException ex) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (attribute.equals("modification-date")) {
|
||||||
|
try {
|
||||||
|
modificationDate = ZonedDateTime.parse(value, RFC_1123_DATE_TIME);
|
||||||
|
}
|
||||||
|
catch (DateTimeParseException ex) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (attribute.equals("read-date")) {
|
||||||
|
try {
|
||||||
|
readDate = ZonedDateTime.parse(value, RFC_1123_DATE_TIME);
|
||||||
|
}
|
||||||
|
catch (DateTimeParseException ex) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new IllegalArgumentException("Invalid content disposition format");
|
throw new IllegalArgumentException("Invalid content disposition format");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new ContentDisposition(type, name, filename, charset, size);
|
return new ContentDisposition(type, name, filename, charset, size, creationDate, modificationDate, readDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -225,7 +288,10 @@ public class ContentDisposition {
|
||||||
ObjectUtils.nullSafeEquals(this.name, otherCd.name) &&
|
ObjectUtils.nullSafeEquals(this.name, otherCd.name) &&
|
||||||
ObjectUtils.nullSafeEquals(this.filename, otherCd.filename) &&
|
ObjectUtils.nullSafeEquals(this.filename, otherCd.filename) &&
|
||||||
ObjectUtils.nullSafeEquals(this.charset, otherCd.charset) &&
|
ObjectUtils.nullSafeEquals(this.charset, otherCd.charset) &&
|
||||||
ObjectUtils.nullSafeEquals(this.size, otherCd.size));
|
ObjectUtils.nullSafeEquals(this.size, otherCd.size) &&
|
||||||
|
ObjectUtils.nullSafeEquals(this.creationDate, otherCd.creationDate)&&
|
||||||
|
ObjectUtils.nullSafeEquals(this.modificationDate, otherCd.modificationDate)&&
|
||||||
|
ObjectUtils.nullSafeEquals(this.readDate, otherCd.readDate));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -235,6 +301,9 @@ public class ContentDisposition {
|
||||||
result = 31 * result + ObjectUtils.nullSafeHashCode(this.filename);
|
result = 31 * result + ObjectUtils.nullSafeHashCode(this.filename);
|
||||||
result = 31 * result + ObjectUtils.nullSafeHashCode(this.charset);
|
result = 31 * result + ObjectUtils.nullSafeHashCode(this.charset);
|
||||||
result = 31 * result + ObjectUtils.nullSafeHashCode(this.size);
|
result = 31 * result + ObjectUtils.nullSafeHashCode(this.size);
|
||||||
|
result = 31 * result + (creationDate != null ? creationDate.hashCode() : 0);
|
||||||
|
result = 31 * result + (modificationDate != null ? modificationDate.hashCode() : 0);
|
||||||
|
result = 31 * result + (readDate != null ? readDate.hashCode() : 0);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,6 +335,21 @@ public class ContentDisposition {
|
||||||
sb.append("; size=");
|
sb.append("; size=");
|
||||||
sb.append(this.size);
|
sb.append(this.size);
|
||||||
}
|
}
|
||||||
|
if (this.creationDate != null) {
|
||||||
|
sb.append("; creation-date=\"");
|
||||||
|
sb.append(RFC_1123_DATE_TIME.format(this.creationDate));
|
||||||
|
sb.append('\"');
|
||||||
|
}
|
||||||
|
if (this.modificationDate != null) {
|
||||||
|
sb.append("; modification-date=\"");
|
||||||
|
sb.append(RFC_1123_DATE_TIME.format(this.modificationDate));
|
||||||
|
sb.append('\"');
|
||||||
|
}
|
||||||
|
if (this.readDate != null) {
|
||||||
|
sb.append("; read-date=\"");
|
||||||
|
sb.append(RFC_1123_DATE_TIME.format(this.readDate));
|
||||||
|
sb.append('\"');
|
||||||
|
}
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -332,6 +416,21 @@ public class ContentDisposition {
|
||||||
*/
|
*/
|
||||||
Builder size(Long size);
|
Builder size(Long size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the value of the {@literal creation-date} parameter.
|
||||||
|
*/
|
||||||
|
Builder creationDate(ZonedDateTime creationDate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the value of the {@literal modification-date} parameter.
|
||||||
|
*/
|
||||||
|
Builder modificationDate(ZonedDateTime modificationDate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the value of the {@literal read-date} parameter.
|
||||||
|
*/
|
||||||
|
Builder readDate(ZonedDateTime readDate);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the content disposition
|
* Build the content disposition
|
||||||
*/
|
*/
|
||||||
|
@ -351,6 +450,13 @@ public class ContentDisposition {
|
||||||
|
|
||||||
private Long size;
|
private Long size;
|
||||||
|
|
||||||
|
private ZonedDateTime creationDate;
|
||||||
|
|
||||||
|
private ZonedDateTime modificationDate;
|
||||||
|
|
||||||
|
private ZonedDateTime readDate;
|
||||||
|
|
||||||
|
|
||||||
public BuilderImpl(String type) {
|
public BuilderImpl(String type) {
|
||||||
Assert.hasText(type, "'type' must not be not empty");
|
Assert.hasText(type, "'type' must not be not empty");
|
||||||
this.type = type;
|
this.type = type;
|
||||||
|
@ -381,9 +487,28 @@ public class ContentDisposition {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Builder creationDate(ZonedDateTime creationDate) {
|
||||||
|
this.creationDate = creationDate;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Builder modificationDate(ZonedDateTime modificationDate) {
|
||||||
|
this.modificationDate = modificationDate;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Builder readDate(ZonedDateTime readDate) {
|
||||||
|
this.readDate = readDate;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ContentDisposition build() {
|
public ContentDisposition build() {
|
||||||
return new ContentDisposition(this.type, this.name, this.filename, this.charset, this.size);
|
return new ContentDisposition(this.type, this.name, this.filename, this.charset,
|
||||||
|
this.size, this.creationDate, this.modificationDate, this.readDate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,8 @@ package org.springframework.http;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
@ -76,6 +78,26 @@ public class ContentDispositionTests {
|
||||||
ContentDisposition.parse("foo;bar");
|
ContentDisposition.parse("foo;bar");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseDates() {
|
||||||
|
ContentDisposition disposition = ContentDisposition
|
||||||
|
.parse("attachment; creation-date=\"Mon, 12 Feb 2007 10:15:30 -0500\"; modification-date=\"Tue, 13 Feb 2007 10:15:30 -0500\"; read-date=\"Wed, 14 Feb 2007 10:15:30 -0500\"");
|
||||||
|
assertEquals(ContentDisposition.builder("attachment")
|
||||||
|
.creationDate(ZonedDateTime.parse("Mon, 12 Feb 2007 10:15:30 -0500", DateTimeFormatter.RFC_1123_DATE_TIME))
|
||||||
|
.modificationDate(ZonedDateTime.parse("Tue, 13 Feb 2007 10:15:30 -0500", DateTimeFormatter.RFC_1123_DATE_TIME))
|
||||||
|
.readDate(ZonedDateTime.parse("Wed, 14 Feb 2007 10:15:30 -0500", DateTimeFormatter.RFC_1123_DATE_TIME))
|
||||||
|
.build(), disposition);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseInvalidDates() {
|
||||||
|
ContentDisposition disposition = ContentDisposition
|
||||||
|
.parse("attachment; creation-date=\"-1\"; modification-date=\"-1\"; read-date=\"Wed, 14 Feb 2007 10:15:30 -0500\"");
|
||||||
|
assertEquals(ContentDisposition.builder("attachment")
|
||||||
|
.readDate(ZonedDateTime.parse("Wed, 14 Feb 2007 10:15:30 -0500", DateTimeFormatter.RFC_1123_DATE_TIME))
|
||||||
|
.build(), disposition);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void headerValue() {
|
public void headerValue() {
|
||||||
ContentDisposition disposition = ContentDisposition.builder("form-data")
|
ContentDisposition disposition = ContentDisposition.builder("form-data")
|
||||||
|
|
Loading…
Reference in New Issue