16
16
17
17
package org .springframework .http .converter ;
18
18
19
+ import java .io .FileNotFoundException ;
19
20
import java .io .IOException ;
20
21
import java .io .InputStream ;
22
+ import java .io .OutputStream ;
23
+ import java .util .List ;
24
+
21
25
import javax .activation .FileTypeMap ;
22
26
import javax .activation .MimetypesFileTypeMap ;
23
27
24
28
import org .springframework .core .io .ByteArrayResource ;
25
29
import org .springframework .core .io .ClassPathResource ;
26
30
import org .springframework .core .io .InputStreamResource ;
27
31
import org .springframework .core .io .Resource ;
32
+ import org .springframework .http .HttpHeaders ;
28
33
import org .springframework .http .HttpInputMessage ;
29
34
import org .springframework .http .HttpOutputMessage ;
35
+ import org .springframework .http .HttpRange ;
36
+ import org .springframework .http .HttpRangeResource ;
30
37
import org .springframework .http .MediaType ;
38
+ import org .springframework .util .Assert ;
31
39
import org .springframework .util .ClassUtils ;
40
+ import org .springframework .util .MimeTypeUtils ;
32
41
import org .springframework .util .StreamUtils ;
33
42
import org .springframework .util .StringUtils ;
34
43
35
44
/**
36
- * Implementation of {@link HttpMessageConverter} that can read and write {@link Resource Resources}.
45
+ * Implementation of {@link HttpMessageConverter} that can read and write {@link Resource Resources}
46
+ * and supports byte range requests.
37
47
*
38
48
* <p>By default, this converter can read all media types. The Java Activation Framework (JAF) -
39
49
* if available - is used to determine the {@code Content-Type} of written resources.
40
50
* If JAF is not available, {@code application/octet-stream} is used.
41
51
*
52
+ * <p>This converter supports HTTP byte range requests and can write partial content, when provided
53
+ * with an {@link HttpRangeResource} instance containing the required Range information.
54
+ *
42
55
* @author Arjen Poutsma
43
56
* @author Juergen Hoeller
44
57
* @author Kazuki Shimizu
58
+ * @author Brian Clozel
45
59
* @since 3.0.2
46
60
*/
47
61
public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter <Resource > {
@@ -64,7 +78,7 @@ protected boolean supports(Class<?> clazz) {
64
78
protected Resource readInternal (Class <? extends Resource > clazz , HttpInputMessage inputMessage )
65
79
throws IOException , HttpMessageNotReadableException {
66
80
67
- if (InputStreamResource .class == clazz ){
81
+ if (InputStreamResource .class == clazz ) {
68
82
return new InputStreamResource (inputMessage .getBody ());
69
83
}
70
84
else if (clazz .isAssignableFrom (ByteArrayResource .class )) {
@@ -94,25 +108,150 @@ protected Long getContentLength(Resource resource, MediaType contentType) throws
94
108
return null ;
95
109
}
96
110
long contentLength = resource .contentLength ();
111
+ if (contentLength > Integer .MAX_VALUE ) {
112
+ throw new IOException ("Resource content too long (beyond Integer.MAX_VALUE): " + resource );
113
+ }
97
114
return (contentLength < 0 ? null : contentLength );
98
115
}
99
116
100
117
@ Override
101
118
protected void writeInternal (Resource resource , HttpOutputMessage outputMessage )
102
119
throws IOException , HttpMessageNotWritableException {
103
120
104
- InputStream in = resource .getInputStream ();
121
+ outputMessage .getHeaders ().add (HttpHeaders .ACCEPT_RANGES , "bytes" );
122
+ if (resource instanceof HttpRangeResource ) {
123
+ writePartialContent ((HttpRangeResource ) resource , outputMessage );
124
+ }
125
+ else {
126
+ writeContent (resource , outputMessage );
127
+ }
128
+ }
129
+
130
+ protected void writeContent (Resource resource , HttpOutputMessage outputMessage )
131
+ throws IOException , HttpMessageNotWritableException {
105
132
try {
106
- StreamUtils .copy (in , outputMessage .getBody ());
133
+ InputStream in = resource .getInputStream ();
134
+ try {
135
+ StreamUtils .copy (in , outputMessage .getBody ());
136
+ }
137
+ catch (NullPointerException ex ) {
138
+ // ignore, see SPR-13620
139
+ }
140
+ finally {
141
+ try {
142
+ in .close ();
143
+ }
144
+ catch (Throwable ex ) {
145
+ // ignore, see SPR-12999
146
+ }
147
+ }
107
148
}
108
- finally {
149
+ catch (FileNotFoundException ex ) {
150
+ // ignore, see SPR-12999
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Write parts of the resource as indicated by the request {@code Range} header.
156
+ * @param resource the identified resource (never {@code null})
157
+ * @param outputMessage current servlet response
158
+ * @throws IOException in case of errors while writing the content
159
+ */
160
+ protected void writePartialContent (HttpRangeResource resource , HttpOutputMessage outputMessage ) throws IOException {
161
+
162
+ Assert .notNull (resource , "Resource should not be null" );
163
+
164
+ List <HttpRange > ranges = resource .getHttpRanges ();
165
+ HttpHeaders responseHeaders = outputMessage .getHeaders ();
166
+ MediaType contentType = responseHeaders .getContentType ();
167
+ Long length = getContentLength (resource , contentType );
168
+
169
+ if (ranges .size () == 1 ) {
170
+ HttpRange range = ranges .get (0 );
171
+
172
+ long start = range .getRangeStart (length );
173
+ long end = range .getRangeEnd (length );
174
+ long rangeLength = end - start + 1 ;
175
+
176
+ responseHeaders .add ("Content-Range" , "bytes " + start + "-" + end + "/" + length );
177
+ responseHeaders .setContentLength ((int ) rangeLength );
178
+
179
+ InputStream in = resource .getInputStream ();
109
180
try {
110
- in .close ();
181
+ copyRange (in , outputMessage .getBody (), start , end );
182
+ }
183
+ finally {
184
+ try {
185
+ in .close ();
186
+ }
187
+ catch (IOException ex ) {
188
+ // ignore
189
+ }
190
+ }
191
+ }
192
+ else {
193
+ String boundaryString = MimeTypeUtils .generateMultipartBoundaryString ();
194
+ responseHeaders .set (HttpHeaders .CONTENT_TYPE , "multipart/byteranges; boundary=" + boundaryString );
195
+
196
+ OutputStream out = outputMessage .getBody ();
197
+
198
+ for (HttpRange range : ranges ) {
199
+ long start = range .getRangeStart (length );
200
+ long end = range .getRangeEnd (length );
201
+
202
+ InputStream in = resource .getInputStream ();
203
+
204
+ // Writing MIME header.
205
+ println (out );
206
+ print (out , "--" + boundaryString );
207
+ println (out );
208
+ if (contentType != null ) {
209
+ print (out , "Content-Type: " + contentType .toString ());
210
+ println (out );
211
+ }
212
+ print (out , "Content-Range: bytes " + start + "-" + end + "/" + length );
213
+ println (out );
214
+ println (out );
215
+
216
+ // Printing content
217
+ copyRange (in , out , start , end );
218
+ }
219
+ println (out );
220
+ print (out , "--" + boundaryString + "--" );
221
+ }
222
+ }
223
+
224
+ private static void println (OutputStream os ) throws IOException {
225
+ os .write ('\r' );
226
+ os .write ('\n' );
227
+ }
228
+
229
+ private static void print (OutputStream os , String buf ) throws IOException {
230
+ os .write (buf .getBytes ("US-ASCII" ));
231
+ }
232
+
233
+ private void copyRange (InputStream in , OutputStream out , long start , long end ) throws IOException {
234
+ long skipped = in .skip (start );
235
+ if (skipped < start ) {
236
+ throw new IOException ("Skipped only " + skipped + " bytes out of " + start + " required." );
237
+ }
238
+
239
+ long bytesToCopy = end - start + 1 ;
240
+ byte buffer [] = new byte [StreamUtils .BUFFER_SIZE ];
241
+ while (bytesToCopy > 0 ) {
242
+ int bytesRead = in .read (buffer );
243
+ if (bytesRead <= bytesToCopy ) {
244
+ out .write (buffer , 0 , bytesRead );
245
+ bytesToCopy -= bytesRead ;
246
+ }
247
+ else {
248
+ out .write (buffer , 0 , (int ) bytesToCopy );
249
+ bytesToCopy = 0 ;
111
250
}
112
- catch (IOException ex ) {
251
+ if (bytesRead == -1 ) {
252
+ break ;
113
253
}
114
254
}
115
- outputMessage .getBody ().flush ();
116
255
}
117
256
118
257
0 commit comments