Skip to content

Commit 99a8510

Browse files
committed
Introduce HttpHeaders get/setContentDisposition()
This commit introduces a new ContentDisposition class designed to parse and generate Content-Disposition header value as defined in RFC 2183. It supports the disposition type and the name, filename (or filename* when encoded according to RFC 5987) and size parameters. This new class is usually used thanks to HttpHeaders#getContentDisposition() and HttpHeaders#setContentDisposition(ContentDisposition). Issue: SPR-14408
1 parent c44c607 commit 99a8510

File tree

4 files changed

+558
-80
lines changed

4 files changed

+558
-80
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,383 @@
1+
/*
2+
* Copyright 2002-2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http;
18+
19+
import java.io.ByteArrayOutputStream;
20+
import java.nio.charset.Charset;
21+
import java.nio.charset.StandardCharsets;
22+
23+
import org.springframework.util.Assert;
24+
import org.springframework.util.StringUtils;
25+
26+
/**
27+
* Represent the content disposition type and parameters as defined in RFC 2183.
28+
*
29+
* @author Sebastien Deleuze
30+
* @since 5.0
31+
* @see <a href="https://tools.ietf.org/html/rfc2183">RFC 2183</a>
32+
*/
33+
public class ContentDisposition {
34+
35+
private final String type;
36+
37+
private final String name;
38+
39+
private final String filename;
40+
41+
private final Charset charset;
42+
43+
private final Long size;
44+
45+
/**
46+
* Create a {@code ContentDisposition} instance with the specified disposition type
47+
* and {@litteral name}, {@litteral filename} (encoded with the specified {@link Charset}
48+
* if any) and {@litteral size} parameter values.
49+
*/
50+
private ContentDisposition(String type, String name, String filename, Charset charset, Long size) {
51+
this.type = type;
52+
this.name = name;
53+
this.filename = filename;
54+
this.charset = charset;
55+
this.size = size;
56+
}
57+
58+
/**
59+
* Return a builder for a {@code ContentDisposition}.
60+
* @param type the disposition type like for example {@literal inline}, {@literal attachment},
61+
* or {@literal form-data}
62+
* @return a content disposition builder
63+
*/
64+
public static Builder builder(String type) {
65+
return new BuilderImpl(type);
66+
}
67+
68+
/**
69+
* @return an empty content disposition
70+
*/
71+
public static ContentDisposition empty() {
72+
return new ContentDisposition(null, null, null, null, null);
73+
}
74+
75+
/**
76+
* Return the disposition type, like for example {@literal inline}, {@literal attachment},
77+
* {@literal form-data}, or {@code null} if not defined.
78+
*/
79+
public String getType() {
80+
return this.type;
81+
}
82+
83+
/**
84+
* Return the value of the {@literal name} parameter, or {@code null} if not defined.
85+
*/
86+
public String getName() {
87+
return this.name;
88+
}
89+
90+
/**
91+
* Return the value of the {@literal filename} parameter (or the value of the
92+
* {@literal filename*} one decoded as defined in the RFC 5987), or {@code null} if not defined.
93+
*/
94+
public String getFilename() {
95+
return this.filename;
96+
}
97+
98+
/**
99+
* Return the charset defined in {@literal filename*} parameter, or {@code null} if not defined.
100+
*/
101+
public Charset getCharset() {
102+
return this.charset;
103+
}
104+
105+
/**
106+
* Return the value of the {@literal size} parameter, or {@code null} if not defined.
107+
*/
108+
public Long getSize() {
109+
return this.size;
110+
}
111+
112+
/**
113+
* Parse a {@literal Content-Disposition} header value as defined in RFC 2183.
114+
* @param contentDisposition the {@literal Content-Disposition} header value
115+
* @return the parsed content disposition
116+
* @see #toString()
117+
*/
118+
public static ContentDisposition parse(String contentDisposition) {
119+
String[] parts = StringUtils.tokenizeToStringArray(contentDisposition, ";");
120+
Assert.isTrue(parts.length >= 1);
121+
String type = parts[0];
122+
String name = null;
123+
String filename = null;
124+
Charset charset = null;
125+
Long size = null;
126+
for (int i = 1; i < parts.length; i++) {
127+
String parameter = parts[i];
128+
int eqIndex = parameter.indexOf('=');
129+
if (eqIndex != -1) {
130+
String attribute = parameter.substring(0, eqIndex);
131+
String value = (parameter.startsWith("\"", eqIndex + 1) && parameter.endsWith("\"") ?
132+
parameter.substring(eqIndex + 2, parameter.length() - 1) :
133+
parameter.substring(eqIndex + 1, parameter.length()));
134+
if (attribute.equals("name") ) {
135+
name = value;
136+
}
137+
else if (attribute.equals("filename*") ) {
138+
filename = decodeHeaderFieldParam(value);
139+
charset = Charset.forName(value.substring(0, value.indexOf("'")));
140+
Assert.isTrue(StandardCharsets.UTF_8.equals(charset) || StandardCharsets.ISO_8859_1.equals(charset),
141+
"Charset should be UTF-8 or ISO-8859-1");
142+
}
143+
else if (attribute.equals("filename") && (filename == null)) {
144+
filename = value;
145+
}
146+
else if (attribute.equals("size") ) {
147+
size = Long.parseLong(value);
148+
}
149+
}
150+
else {
151+
throw new IllegalArgumentException("Invalid content disposition format");
152+
}
153+
}
154+
return new ContentDisposition(type, name, filename, charset, size);
155+
}
156+
157+
/**
158+
* Encode the given header field param as describe in RFC 5987.
159+
* @param input the header field param
160+
* @param charset the charset of the header field param string,
161+
* only the US-ASCII, UTF-8 and ISO-8859-1 charsets are supported
162+
* @return the encoded header field param
163+
* @see <a href="https://tools.ietf.org/html/rfc5987">RFC 5987</a>
164+
*/
165+
private static String encodeHeaderFieldParam(String input, Charset charset) {
166+
Assert.notNull(input, "Input String should not be null");
167+
Assert.notNull(charset, "Charset should not be null");
168+
if (StandardCharsets.US_ASCII.equals(charset)) {
169+
return input;
170+
}
171+
Assert.isTrue(StandardCharsets.UTF_8.equals(charset) || StandardCharsets.ISO_8859_1.equals(charset),
172+
"Charset should be UTF-8 or ISO-8859-1");
173+
byte[] source = input.getBytes(charset);
174+
int len = source.length;
175+
StringBuilder sb = new StringBuilder(len << 1);
176+
sb.append(charset.name());
177+
sb.append("''");
178+
for (byte b : source) {
179+
if (isRFC5987AttrChar(b)) {
180+
sb.append((char) b);
181+
}
182+
else {
183+
sb.append('%');
184+
char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16));
185+
char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16));
186+
sb.append(hex1);
187+
sb.append(hex2);
188+
}
189+
}
190+
return sb.toString();
191+
}
192+
193+
/**
194+
* Decode the given header field param as describe in RFC 5987.
195+
* <p>Only the US-ASCII, UTF-8 and ISO-8859-1 charsets are supported.
196+
* @param input the header field param
197+
* @return the encoded header field param
198+
* @see <a href="https://tools.ietf.org/html/rfc5987">RFC 5987</a>
199+
*/
200+
private static String decodeHeaderFieldParam(String input) {
201+
Assert.notNull(input, "Input String should not be null");
202+
int firstQuoteIndex = input.indexOf("'");
203+
int secondQuoteIndex = input.indexOf("'", firstQuoteIndex + 1);
204+
// US_ASCII
205+
if (firstQuoteIndex == -1 || secondQuoteIndex == -1) {
206+
return input;
207+
}
208+
Charset charset = Charset.forName(input.substring(0, firstQuoteIndex));
209+
Assert.isTrue(StandardCharsets.UTF_8.equals(charset) || StandardCharsets.ISO_8859_1.equals(charset),
210+
"Charset should be UTF-8 or ISO-8859-1");
211+
byte[] value = input.substring(secondQuoteIndex + 1, input.length()).getBytes(charset);
212+
ByteArrayOutputStream bos = new ByteArrayOutputStream();
213+
int index = 0;
214+
while (index < value.length) {
215+
byte b = value[index];
216+
if (isRFC5987AttrChar(b)) {
217+
bos.write((char) b);
218+
index++;
219+
}
220+
else if (b == '%') {
221+
char[] array = { (char)value[index + 1], (char)value[index + 2]};
222+
bos.write(Integer.parseInt(String.valueOf(array), 16));
223+
index+=3;
224+
}
225+
else {
226+
throw new IllegalArgumentException("Invalid header field parameter format (as defined in RFC 5987)");
227+
}
228+
}
229+
return new String(bos.toByteArray(), charset);
230+
}
231+
232+
private static boolean isRFC5987AttrChar(byte c) {
233+
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
234+
c == '!' || c == '#' || c == '$' || c == '&' || c == '+' || c == '-' ||
235+
c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~';
236+
}
237+
238+
@Override
239+
public boolean equals(Object o) {
240+
if (this == o) {
241+
return true;
242+
}
243+
if (o == null || getClass() != o.getClass()) {
244+
return false;
245+
}
246+
ContentDisposition that = (ContentDisposition) o;
247+
if (type != null ? !type.equals(that.type) : that.type != null) {
248+
return false;
249+
}
250+
if (name != null ? !name.equals(that.name) : that.name != null) {
251+
return false;
252+
}
253+
if (filename != null ? !filename.equals(that.filename) : that.filename != null) {
254+
return false;
255+
}
256+
if (charset != null ? !charset.equals(that.charset) : that.charset != null) {
257+
return false;
258+
}
259+
return size != null ? size.equals(that.size) : that.size == null;
260+
}
261+
262+
@Override
263+
public int hashCode() {
264+
int result = type != null ? type.hashCode() : 0;
265+
result = 31 * result + (name != null ? name.hashCode() : 0);
266+
result = 31 * result + (filename != null ? filename.hashCode() : 0);
267+
result = 31 * result + (charset != null ? charset.hashCode() : 0);
268+
result = 31 * result + (size != null ? size.hashCode() : 0);
269+
return result;
270+
}
271+
272+
/**
273+
* Return the header value for this content disposition as defined in RFC 2183.
274+
* @see #parse(String)
275+
*/
276+
@Override
277+
public String toString() {
278+
StringBuilder builder = new StringBuilder(this.type);
279+
if (this.name != null) {
280+
builder.append("; name=\"");
281+
builder.append(this.name).append('\"');
282+
}
283+
if (this.filename != null) {
284+
if(this.charset == null || StandardCharsets.US_ASCII.equals(this.charset)) {
285+
builder.append("; filename=\"");
286+
builder.append(this.filename).append('\"');
287+
}
288+
else {
289+
builder.append("; filename*=");
290+
builder.append(encodeHeaderFieldParam(this.filename, this.charset));
291+
}
292+
}
293+
if (this.size != null) {
294+
builder.append("; size=");
295+
builder.append(this.size);
296+
}
297+
return builder.toString();
298+
}
299+
300+
301+
/**
302+
* A mutable builder for {@code ContentDisposition}.
303+
*/
304+
public interface Builder {
305+
306+
/**
307+
* Set the value of the {@literal name} parameter
308+
*/
309+
Builder name(String name);
310+
311+
/**
312+
* Set the value of the {@literal filename} parameter
313+
*/
314+
Builder filename(String filename);
315+
316+
/**
317+
* Set the value of the {@literal filename*} that will be encoded as defined in
318+
* the RFC 5987. Only the US-ASCII, UTF-8 and ISO-8859-1 charsets are supported.
319+
*/
320+
Builder filename(String filename, Charset charset);
321+
322+
/**
323+
* Set the value of the {@literal size} parameter
324+
*/
325+
Builder size(Long size);
326+
327+
/**
328+
* Build the content disposition
329+
*/
330+
ContentDisposition build();
331+
332+
}
333+
334+
private static class BuilderImpl implements Builder {
335+
336+
private String type;
337+
338+
private String name;
339+
340+
private String filename;
341+
342+
private Charset charset;
343+
344+
private Long size;
345+
346+
public BuilderImpl(String type) {
347+
Assert.hasText(type, "'type' must not be not empty");
348+
this.type = type;
349+
}
350+
351+
@Override
352+
public Builder name(String name) {
353+
this.name = name;
354+
return this;
355+
}
356+
357+
@Override
358+
public Builder filename(String filename) {
359+
this.filename = filename;
360+
return this;
361+
}
362+
363+
@Override
364+
public Builder filename(String filename, Charset charset) {
365+
this.filename = filename;
366+
this.charset = charset;
367+
return this;
368+
}
369+
370+
@Override
371+
public Builder size(Long size) {
372+
this.size = size;
373+
return this;
374+
}
375+
376+
@Override
377+
public ContentDisposition build() {
378+
return new ContentDisposition(this.type, this.name, this.filename, this.charset, this.size);
379+
}
380+
381+
}
382+
383+
}

0 commit comments

Comments
 (0)