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