1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 package org.talika.tarsis.filters.upload;
24
25 import java.io.BufferedInputStream;
26 import java.io.BufferedOutputStream;
27 import java.io.File;
28 import java.io.FileOutputStream;
29 import java.io.IOException;
30
31 import javax.servlet.ServletRequest;
32
33 /**
34 * Multipart iterator allows us to iterate multipart request element as like a
35 * <code>java.util.Iterator</code> although it doesn't implement this interface.
36 *
37 * @author Jose M. Palomar
38 * @version $Revision: 127 $
39 */
40 public final class MultipartIterator {
41
42
43 /**
44 * Default charset encoding.
45 */
46 private static final String DEFAULT_ENCODING = "iso-8859-1";
47
48 /**
49 * Content type parameter name.
50 */
51 private static final String CONTENT_TYPE = "content-type";
52
53 /**
54 * "text/plain" content type literal.
55 */
56 private static final String CONTENT_TYPE_TEXT_PLAIN = "text/plain";
57
58 /**
59 * "application/octet-stream" content type literal.
60 */
61 private static final String CONTENT_TYPE_APPLICATION_OCTET_STREAM
62 = "application/octet-stream";
63 /**
64 * Content disposition parameter name.
65 */
66 private static final String CONTENT_DISPOSITION = "content-disposition";
67
68 /**
69 * Default content disposition.
70 */
71 private static final String DEFAULT_CONTENT_DISPOSITION
72 = "form-data";
73 /**
74 * Boundary parameter name.
75 */
76 private static final String PARAMETER_BOUNDARY = "boundary";
77
78 /**
79 * Name parameter name.
80 */
81 private static final String PARAMETER_NAME = "name";
82
83 /**
84 * Filename parameter name.
85 */
86 private static final String PARAMETER_FILENAME = "filename";
87
88 /**
89 * Charset parameter name.
90 */
91 private static final String PARAMETER_CHARSET = "charset";
92
93 /**
94 * Double dash.
95 */
96 private static final String DOUBLE_DASH = "--";
97
98 /**
99 * Temporal filename prefix.
100 */
101 private static final String TMP_PREFIX = "trss";
102
103 /**
104 * Default buffer size.
105 */
106 private static final int DEFAULT_BUFFER_SIZE = 4 * 1024;
107
108 /**
109 * Default disk buffer size.
110 */
111 private static final int DISK_BUFFER_SIZE = 20 * 1024;
112
113 /**
114 * Default text buffer size.
115 */
116 private static final int TEXT_BUFFER_SIZE = 1 * 1024;
117
118
119 /**
120 * Client's request.
121 */
122 private ServletRequest servletRequest;
123
124 /**
125 * Multipart input stream.
126 */
127 private MultipartInputStream inputStream;
128
129 /**
130 * Request's max size.
131 */
132 private int maxSize;
133
134 /**
135 * Request's size.
136 */
137 private int size;
138
139 /**
140 * Buffer size.
141 */
142 private int bufferSize;
143
144 /**
145 * Temporal directory.
146 */
147 private String tmpDir;
148
149 /**
150 * Boundary value.
151 */
152 private String boundary;
153
154 /**
155 * Boundary start value.
156 */
157 private String startBoundary;
158
159 /**
160 * Boundary end value.
161 */
162 private String endBoundary;
163
164 /**
165 * Default charset encoding.
166 */
167 private String defaultEncoding;
168
169
170 /**
171 * Creates a new <code>MultipartIterator</code> using given client's request.
172 *
173 * @param servletRequest ServletRequest client's request.
174 * @param maxSize int max allowad size.
175 * @param bufferSize int buffer size.
176 * @param tmpDir String temporal directory.
177 * @throws MultipartRequestException if there is any error procesing multipart
178 * request.
179 * @throws IOException if there is any I/O error.
180 */
181 public MultipartIterator(ServletRequest servletRequest, int maxSize, int bufferSize,
182 String tmpDir) throws MultipartRequestException, IOException {
183
184 this.servletRequest = servletRequest;
185 this.maxSize = maxSize;
186 if (bufferSize > DEFAULT_BUFFER_SIZE) {
187 this.bufferSize = bufferSize;
188 }
189 else {
190 this.bufferSize = DEFAULT_BUFFER_SIZE;
191 }
192 this.tmpDir = tmpDir;
193
194 this.boundary = getBoundary();
195 if (this.boundary == null) {
196 throw new MultipartRequestException("Can't retrieve boundary");
197 }
198 this.startBoundary = DOUBLE_DASH + this.boundary;
199 this.endBoundary = DOUBLE_DASH + this.boundary + DOUBLE_DASH;
200
201 this.size = getSize();
202 if (this.size > this.maxSize) {
203 throw new MultipartRequestException("Max Content-Length exceeded");
204 }
205
206 this.defaultEncoding = getEncoding();
207
208 this.inputStream = new MultipartInputStream(
209 new BufferedInputStream(servletRequest.getInputStream(),
210 this.bufferSize),
211 this.boundary, this.defaultEncoding);
212
213 }
214
215
216 /**
217 * Returns multipart request boundary value.
218 *
219 * @return String multipart request boundary value.
220 */
221 private String getBoundary() {
222
223 String contentType = servletRequest.getContentType();
224 if (contentType != null && contentType.lastIndexOf(PARAMETER_BOUNDARY) != -1) {
225
226 String boundary =
227 contentType.substring(contentType.lastIndexOf(PARAMETER_BOUNDARY) + 9);
228 if (boundary.endsWith("\n")) {
229 boundary = boundary.substring(0, this.boundary.length() - 1);
230 }
231
232 return boundary;
233
234 }
235 else {
236 return null;
237 }
238
239 }
240
241 /**
242 * Returns multipart request size.
243 *
244 * @return int multipart request size.
245 */
246 private int getSize() {
247 return servletRequest.getContentLength();
248 }
249
250 /**
251 * Returns multipart request charset encoding.
252 *
253 * @return String multipart request charset encoding.
254 */
255 private String getEncoding() {
256
257 String encoding = this.servletRequest.getCharacterEncoding();
258 if (encoding == null) {
259 encoding = DEFAULT_ENCODING;
260 }
261
262 return encoding;
263
264 }
265
266 /**
267 * Returns <code>true</code> if the iteration has more elements.
268 *
269 * @return boolean <code>true</code> if the iteration has more elements.
270 * @throws MultipartRequestException if there is any error procesing multipart
271 * request.
272 * @throws IOException if there is any I/O error.
273 */
274 public boolean hasNext() throws MultipartRequestException, IOException {
275 return readBoundary();
276 }
277
278 /**
279 * Reads boundary from input stream.
280 *
281 * @return boolean <code>true</code> if boundary start was readed;
282 * <code>false</code> if boundary end was readed.
283 * @throws MultipartRequestException if there is any error procesing multipart
284 * request.
285 * @throws IOException if there is any I/O error.
286 */
287 private boolean readBoundary() throws MultipartRequestException, IOException {
288
289 String line = inputStream.readLine();
290 if (line.equals(this.startBoundary)) {
291 return true;
292 }
293 else if (line.equals(this.endBoundary)) {
294 return false;
295 }
296 else {
297 throw new MultipartRequestException("Invalid boundary");
298 }
299
300 }
301
302 /**
303 * Returns the next element in the iteration.
304 *
305 * @return MultipartElement next element in the iteration.
306 * @throws MultipartRequestException if there is any error procesing multipart
307 * request.
308 * @throws IOException if there is any I/O error.
309 */
310 public MultipartElement next() throws MultipartRequestException, IOException {
311
312
313 String contentDispositionLine = inputStream.readLine();
314 String contentDisposition = parseContentDisposition(contentDispositionLine);
315 if ((contentDisposition == null) ||
316 (!contentDisposition.equalsIgnoreCase(DEFAULT_CONTENT_DISPOSITION))) {
317 throw new MultipartRequestException("Invalid Content-Disposition");
318 }
319 String name = parseParameter(PARAMETER_NAME, contentDispositionLine);
320 if (name == null) {
321 throw new MultipartRequestException("Invalid Content-Disposition, no parameter name");
322 }
323 String filename = parseParameter(PARAMETER_FILENAME, contentDispositionLine);
324
325
326 String contentTypeLine = inputStream.readLine();
327 String contentType = parseContentType(contentTypeLine);
328 String charset = null;
329 if ((contentType == null) || (contentType.length() == 0)) {
330 if (filename == null) {
331 contentType = CONTENT_TYPE_TEXT_PLAIN;
332 }
333 else {
334 contentType = CONTENT_TYPE_APPLICATION_OCTET_STREAM;
335 }
336 charset = parseParameter(PARAMETER_CHARSET, contentDispositionLine);
337 if (charset == null) {
338 charset = this.defaultEncoding;
339 }
340 }
341 else {
342 String line = inputStream.readLine();
343 if (line.length() != 0) {
344 throw new MultipartRequestException("Invalid Content-Type, no empty line");
345 }
346 }
347
348
349 if (filename == null) {
350 String text = readTextValue(charset);
351 return new MultipartElement(name, text);
352 }
353 else {
354 MultipartFile file = null;
355 if (filename.length() != 0) {
356 file = readFileValue(filename, contentType);
357 }
358 else {
359 String line = inputStream.readLine();
360 if (line.length() != 0) {
361 throw new MultipartRequestException("Invalid null file, no empty line");
362 }
363 }
364 return new MultipartElement(name, filename, file);
365 }
366
367 }
368
369 /**
370 * Parses <code>content-diposition</code> parameter.
371 *
372 * @param line String line to parse.
373 * @return String <code>content-diposition</code> parameter value.
374 */
375 private String parseContentDisposition(String line) {
376
377 if (!line.toLowerCase().startsWith(CONTENT_DISPOSITION)) {
378 return null;
379 }
380
381 int beginIndex = line.indexOf(':');
382 if (beginIndex < 0) {
383 return null;
384 }
385
386 int endIndex = line.indexOf(';');
387 if (endIndex < 0) {
388 return line.substring(beginIndex).trim();
389 }
390
391 return line.substring(beginIndex + 1, endIndex).trim();
392
393 }
394
395 /**
396 * Parses <code>content-type</code> parameter.
397 *
398 * @param line String line to parse.
399 * @return String <code>content-type</code> parameter value.
400 */
401 private String parseContentType(String line) {
402
403 if (!line.toLowerCase().startsWith(CONTENT_TYPE)) {
404 return null;
405 }
406
407 int beginIndex = line.indexOf(':');
408 if (beginIndex < 0) {
409 return null;
410 }
411
412 int endIndex = line.indexOf(';');
413 if (endIndex < 0) {
414 return line.substring(beginIndex).trim();
415 }
416
417 return line.substring(beginIndex + 1, endIndex).trim();
418
419 }
420
421 /**
422 * Parses matching name parameter.
423 *
424 * @param name String name of parameter to parse.
425 * @param line String line to parse.
426 * @return String matching name parameter value or <code>null</code> if not
427 * found.
428 */
429 private String parseParameter(String name, String line) {
430
431 int index = line.indexOf(name);
432 if (index < 0) {
433 return null;
434 }
435
436 int beginIndex = line.indexOf('\"', index);
437 if (beginIndex < 0) {
438 return null;
439 }
440
441 int endIndex = line.indexOf('\"', beginIndex + 1);
442 if (endIndex < 0) {
443 return null;
444 }
445
446 return line.substring(beginIndex + 1, endIndex);
447
448 }
449
450 /**
451 * Reads text element value.
452 *
453 * @param charset String charset encoding.
454 * @return String readed text.
455 * @throws MultipartRequestException if there is any error procesing multipart
456 * request.
457 * @throws IOException if there is any I/O error.
458 */
459 private String readTextValue(String charset) throws MultipartRequestException, IOException {
460 return inputStream.readLine(charset);
461 }
462
463 /**
464 * Reads file element value.
465 *
466 * @param filename String name of file.
467 * @param contentType String content type of file.
468 * @return MultipartFile readed file.
469 * @throws IOException if there is any I/O error.
470 */
471 private MultipartFile readFileValue(String filename, String contentType)
472 throws IOException {
473
474 File tmpFile = File.createTempFile(TMP_PREFIX, null, new File(this.tmpDir));
475 BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(tmpFile), DISK_BUFFER_SIZE);
476 byte[] buffer = new byte[this.bufferSize];
477 int count = inputStream.readData(buffer, 0, buffer.length);
478 while (count > 0) {
479 bos.write(buffer, 0, count);
480 count = inputStream.readData(buffer, 0, buffer.length);
481 }
482 bos.close();
483
484 return new MultipartFile(tmpFile.getAbsolutePath(), filename, contentType);
485
486 }
487
488 }