1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63 package org.apache.commons.fileupload;
64
65
66 import java.io.IOException;
67 import java.io.InputStream;
68 import java.io.OutputStream;
69 import java.util.ArrayList;
70 import java.util.HashMap;
71 import java.util.List;
72 import java.util.Map;
73
74 import javax.portlet.ActionRequest;
75
76
77 /***
78 * <p>High level API for processing file uploads.</p>
79 *
80 * <p>This class handles multiple files per single HTML widget, sent using
81 * <code>multipart/mixed</code> encoding type, as specified by
82 * <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>. Use {@link
83 * #parseRequest(HttpServletRequest)} to acquire a list of {@link
84 * org.apache.commons.fileupload.FileItem}s associated with a given HTML
85 * widget.</p>
86 *
87 * <p>How the data for individual parts is stored is determined by the factory
88 * used to create them; a given part may be in memory, on disk, or somewhere
89 * else.</p>
90 *
91 * @author <a href="mailto:Rafal.Krzewski@e-point.pl">Rafal Krzewski</a>
92 * @author <a href="mailto:dlr@collab.net">Daniel Rall</a>
93 * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
94 * @author <a href="mailto:jmcnally@collab.net">John McNally</a>
95 * @author <a href="mailto:martinc@apache.org">Martin Cooper</a>
96 * @author Sean C. Sullivan
97 *
98 * @version $Id: PortletFileUploadBase.java,v 1.1 2003/10/01 22:21:43 jsackett Exp $
99 */
100 public abstract class PortletFileUploadBase
101 {
102
103
104
105
106 /***
107 * Utility method that determines whether the request contains multipart
108 * content.
109 *
110 * @param req The servlet request to be evaluated. Must be non-null.
111 *
112 * @return <code>true</code> if the request is multipart;
113 * <code>false</code> otherwise.
114 */
115 public static final boolean isMultipartContent(ActionRequest req)
116 {
117 String contentType = req.getContentType();
118 if (contentType == null)
119 {
120 return false;
121 }
122 if (contentType.startsWith(MULTIPART))
123 {
124 return true;
125 }
126 return false;
127 }
128
129
130
131
132
133 /***
134 * HTTP content type header name.
135 */
136 public static final String CONTENT_TYPE = "Content-type";
137
138
139 /***
140 * HTTP content disposition header name.
141 */
142 public static final String CONTENT_DISPOSITION = "Content-disposition";
143
144
145 /***
146 * Content-disposition value for form data.
147 */
148 public static final String FORM_DATA = "form-data";
149
150
151 /***
152 * Content-disposition value for file attachment.
153 */
154 public static final String ATTACHMENT = "attachment";
155
156
157 /***
158 * Part of HTTP content type header.
159 */
160 public static final String MULTIPART = "multipart/";
161
162
163 /***
164 * HTTP content type header for multipart forms.
165 */
166 public static final String MULTIPART_FORM_DATA = "multipart/form-data";
167
168
169 /***
170 * HTTP content type header for multiple uploads.
171 */
172 public static final String MULTIPART_MIXED = "multipart/mixed";
173
174
175 /***
176 * The maximum length of a single header line that will be parsed
177 * (1024 bytes).
178 */
179 public static final int MAX_HEADER_SIZE = 1024;
180
181
182
183
184
185 /***
186 * The maximum size permitted for an uploaded file. A value of -1 indicates
187 * no maximum.
188 */
189 private long sizeMax = -1;
190
191
192 /***
193 * The content encoding to use when reading part headers.
194 */
195 private String headerEncoding;
196
197
198
199
200
201 /***
202 * Returns the factory class used when creating file items.
203 *
204 * @return The factory class for new file items.
205 */
206 public abstract FileItemFactory getFileItemFactory();
207
208
209 /***
210 * Sets the factory class to use when creating file items.
211 *
212 * @param factory The factory class for new file items.
213 */
214 public abstract void setFileItemFactory(FileItemFactory factory);
215
216
217 /***
218 * Returns the maximum allowed upload size.
219 *
220 * @return The maximum allowed size, in bytes.
221 *
222 * @see #setSizeMax(long)
223 *
224 */
225 public long getSizeMax()
226 {
227 return sizeMax;
228 }
229
230
231 /***
232 * Sets the maximum allowed upload size. If negative, there is no maximum.
233 *
234 * @param sizeMax The maximum allowed size, in bytes, or -1 for no maximum.
235 *
236 * @see #getSizeMax()
237 *
238 */
239 public void setSizeMax(long sizeMax)
240 {
241 this.sizeMax = sizeMax;
242 }
243
244
245 /***
246 * Retrieves the character encoding used when reading the headers of an
247 * individual part. When not specified, or <code>null</code>, the platform
248 * default encoding is used.
249 *
250 * @return The encoding used to read part headers.
251 */
252 public String getHeaderEncoding()
253 {
254 return headerEncoding;
255 }
256
257
258 /***
259 * Specifies the character encoding to be used when reading the headers of
260 * individual parts. When not specified, or <code>null</code>, the platform
261 * default encoding is used.
262 *
263 * @param encoding The encoding used to read part headers.
264 */
265 public void setHeaderEncoding(String encoding)
266 {
267 headerEncoding = encoding;
268 }
269
270
271
272
273
274 /***
275 * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
276 * compliant <code>multipart/form-data</code> stream. If files are stored
277 * on disk, the path is given by <code>getRepository()</code>.
278 *
279 * @param req The servlet request to be parsed.
280 *
281 * @return A list of <code>FileItem</code> instances parsed from the
282 * request, in the order that they were transmitted.
283 *
284 * @exception FileUploadException if there are problems reading/parsing
285 * the request or storing files.
286 */
287 public List
288 throws FileUploadException
289 {
290 if (null == req)
291 {
292 throw new NullPointerException("req parameter");
293 }
294
295 ArrayList items = new ArrayList();
296 String contentType = req.getContentType();
297
298 if ((null == contentType) || (!contentType.startsWith(MULTIPART)))
299 {
300 throw new InvalidContentTypeException(
301 "the request doesn't contain a "
302 + MULTIPART_FORM_DATA
303 + " or "
304 + MULTIPART_MIXED
305 + " stream, content type header is "
306 + contentType);
307 }
308 int requestSize = req.getContentLength();
309
310 if (requestSize == -1)
311 {
312 throw new UnknownSizeException(
313 "the request was rejected because it's size is unknown");
314 }
315
316 if (sizeMax >= 0 && requestSize > sizeMax)
317 {
318 throw new SizeLimitExceededException(
319 "the request was rejected because "
320 + "it's size exceeds allowed range");
321 }
322
323 try
324 {
325 int boundaryIndex = contentType.indexOf("boundary=");
326 if (boundaryIndex < 0)
327 {
328 throw new FileUploadException(
329 "the request was rejected because "
330 + "no multipart boundary was found");
331 }
332 byte[] boundary = contentType.substring(
333 boundaryIndex + 9).getBytes();
334
335 InputStream input = req.getPortletInputStream();
336
337 MultipartStream multi = new MultipartStream(input, boundary);
338 multi.setHeaderEncoding(headerEncoding);
339
340 boolean nextPart = multi.skipPreamble();
341 while (nextPart)
342 {
343 Map headers = parseHeaders(multi.readHeaders());
344 String fieldName = getFieldName(headers);
345 if (fieldName != null)
346 {
347 String subContentType = getHeader(headers, CONTENT_TYPE);
348 if (subContentType != null && subContentType
349 .startsWith(MULTIPART_MIXED))
350 {
351
352 byte[] subBoundary =
353 subContentType.substring(
354 subContentType
355 .indexOf("boundary=") + 9).getBytes();
356 multi.setBoundary(subBoundary);
357 boolean nextSubPart = multi.skipPreamble();
358 while (nextSubPart)
359 {
360 headers = parseHeaders(multi.readHeaders());
361 if (getFileName(headers) != null)
362 {
363 FileItem item =
364 createItem(headers, false);
365 OutputStream os = item.getOutputStream();
366 try
367 {
368 multi.readBodyData(os);
369 }
370 finally
371 {
372 os.close();
373 }
374 items.add(item);
375 }
376 else
377 {
378
379
380 multi.discardBodyData();
381 }
382 nextSubPart = multi.readBoundary();
383 }
384 multi.setBoundary(boundary);
385 }
386 else
387 {
388 if (getFileName(headers) != null)
389 {
390
391 FileItem item = createItem(headers, false);
392 OutputStream os = item.getOutputStream();
393 try
394 {
395 multi.readBodyData(os);
396 }
397 finally
398 {
399 os.close();
400 }
401 items.add(item);
402 }
403 else
404 {
405
406 FileItem item = createItem(headers, true);
407 OutputStream os = item.getOutputStream();
408 try
409 {
410 multi.readBodyData(os);
411 }
412 finally
413 {
414 os.close();
415 }
416 items.add(item);
417 }
418 }
419 }
420 else
421 {
422
423 multi.discardBodyData();
424 }
425 nextPart = multi.readBoundary();
426 }
427 }
428 catch (IOException e)
429 {
430 throw new FileUploadException(
431 "Processing of " + MULTIPART_FORM_DATA
432 + " request failed. " + e.getMessage());
433 }
434
435 return items;
436 }
437
438
439
440
441
442 /***
443 * Retrieves the file name from the <code>Content-disposition</code>
444 * header.
445 *
446 * @param headers A <code>Map</code> containing the HTTP request headers.
447 *
448 * @return The file name for the current <code>encapsulation</code>.
449 */
450 protected String getFileName(Map
451 {
452 String fileName = null;
453 String cd = getHeader(headers, CONTENT_DISPOSITION);
454 if (cd.startsWith(FORM_DATA) || cd.startsWith(ATTACHMENT))
455 {
456 int start = cd.indexOf("filename=\"");
457 int end = cd.indexOf('"', start + 10);
458 if (start != -1 && end != -1)
459 {
460 fileName = cd.substring(start + 10, end).trim();
461 }
462 }
463 return fileName;
464 }
465
466
467 /***
468 * Retrieves the field name from the <code>Content-disposition</code>
469 * header.
470 *
471 * @param headers A <code>Map</code> containing the HTTP request headers.
472 *
473 * @return The field name for the current <code>encapsulation</code>.
474 */
475 protected String getFieldName(Map
476 {
477 String fieldName = null;
478 String cd = getHeader(headers, CONTENT_DISPOSITION);
479 if (cd != null && cd.startsWith(FORM_DATA))
480 {
481 int start = cd.indexOf("name=\"");
482 int end = cd.indexOf('"', start + 6);
483 if (start != -1 && end != -1)
484 {
485 fieldName = cd.substring(start + 6, end);
486 }
487 }
488 return fieldName;
489 }
490
491
492 /***
493 * Creates a new {@link FileItem} instance.
494 *
495 * @param headers A <code>Map</code> containing the HTTP request
496 * headers.
497 * @param isFormField Whether or not this item is a form field, as
498 * opposed to a file.
499 *
500 * @return A newly created <code>FileItem</code> instance.
501 *
502 * @exception FileUploadException if an error occurs.
503 */
504 protected FileItem createItem(Map
505 boolean isFormField)
506 throws FileUploadException
507 {
508 return getFileItemFactory().createItem(getFieldName(headers),
509 getHeader(headers, CONTENT_TYPE),
510 isFormField,
511 getFileName(headers));
512 }
513
514
515 /***
516 * <p> Parses the <code>header-part</code> and returns as key/value
517 * pairs.
518 *
519 * <p> If there are multiple headers of the same names, the name
520 * will map to a comma-separated list containing the values.
521 *
522 * @param headerPart The <code>header-part</code> of the current
523 * <code>encapsulation</code>.
524 *
525 * @return A <code>Map</code> containing the parsed HTTP request headers.
526 */
527 protected Map
528 {
529 Map headers = new HashMap();
530 char buffer[] = new char[MAX_HEADER_SIZE];
531 boolean done = false;
532 int j = 0;
533 int i;
534 String header, headerName, headerValue;
535 try
536 {
537 while (!done)
538 {
539 i = 0;
540
541
542 while (i < 2 || buffer[i - 2] != '\r' || buffer[i - 1] != '\n')
543 {
544 buffer[i++] = headerPart.charAt(j++);
545 }
546 header = new String(buffer, 0, i - 2);
547 if (header.equals(""))
548 {
549 done = true;
550 }
551 else
552 {
553 if (header.indexOf(':') == -1)
554 {
555
556 continue;
557 }
558 headerName = header.substring(0, header.indexOf(':'))
559 .trim().toLowerCase();
560 headerValue =
561 header.substring(header.indexOf(':') + 1).trim();
562 if (getHeader(headers, headerName) != null)
563 {
564
565
566 headers.put(headerName,
567 getHeader(headers, headerName) + ','
568 + headerValue);
569 }
570 else
571 {
572 headers.put(headerName, headerValue);
573 }
574 }
575 }
576 }
577 catch (IndexOutOfBoundsException e)
578 {
579
580
581 }
582 return headers;
583 }
584
585
586 /***
587 * Returns the header with the specified name from the supplied map. The
588 * header lookup is case-insensitive.
589 *
590 * @param headers A <code>Map</code> containing the HTTP request headers.
591 * @param name The name of the header to return.
592 *
593 * @return The value of specified header, or a comma-separated list if
594 * there were multiple headers of that name.
595 */
596 protected final String getHeader(Map
597 String name)
598 {
599 return (String) headers.get(name.toLowerCase());
600 }
601
602
603 /***
604 * Thrown to indicate that the request is not a multipart request.
605 */
606 public static class InvalidContentTypeException
607 extends FileUploadException
608 {
609 /***
610 * Constructs a <code>InvalidContentTypeException</code> with no
611 * detail message.
612 */
613 public InvalidContentTypeException()
614 {
615 super();
616 }
617
618 /***
619 * Constructs an <code>InvalidContentTypeException</code> with
620 * the specified detail message.
621 *
622 * @param message The detail message.
623 */
624 public InvalidContentTypeException(String message)
625 {
626 super(message);
627 }
628 }
629
630
631 /***
632 * Thrown to indicate that the request size is not specified.
633 */
634 public static class UnknownSizeException
635 extends FileUploadException
636 {
637 /***
638 * Constructs a <code>UnknownSizeException</code> with no
639 * detail message.
640 */
641 public UnknownSizeException()
642 {
643 super();
644 }
645
646 /***
647 * Constructs an <code>UnknownSizeException</code> with
648 * the specified detail message.
649 *
650 * @param message The detail message.
651 */
652 public UnknownSizeException(String message)
653 {
654 super(message);
655 }
656 }
657
658
659 /***
660 * Thrown to indicate that the request size exceeds the configured maximum.
661 */
662 public static class SizeLimitExceededException
663 extends FileUploadException
664 {
665 /***
666 * Constructs a <code>SizeExceededException</code> with no
667 * detail message.
668 */
669 public SizeLimitExceededException()
670 {
671 super();
672 }
673
674 /***
675 * Constructs an <code>SizeExceededException</code> with
676 * the specified detail message.
677 *
678 * @param message The detail message.
679 */
680 public SizeLimitExceededException(String message)
681 {
682 super(message);
683 }
684 }
685
686 }