View Javadoc

1   /*
2    * $Header: /cvsroot/director168/director_standard_portlets/src/org/apache/commons/fileupload/PortletFileUploadBase.java,v 1.1 2003/10/01 22:21:43 jsackett Exp $
3    * $Revision: 1.1 $
4    * $Date: 2003/10/01 22:21:43 $
5    *
6    * ====================================================================
7    *
8    * The Apache Software License, Version 1.1
9    *
10   * Copyright (c) 2001-2003 The Apache Software Foundation.  All rights
11   * reserved.
12   *
13   * Redistribution and use in source and binary forms, with or without
14   * modification, are permitted provided that the following conditions
15   * are met:
16   *
17   * 1. Redistributions of source code must retain the above copyright
18   *    notice, this list of conditions and the following disclaimer.
19   *
20   * 2. Redistributions in binary form must reproduce the above copyright
21   *    notice, this list of conditions and the following disclaimer in
22   *    the documentation and/or other materials provided with the
23   *    distribution.
24   *
25   * 3. The end-user documentation included with the redistribution, if
26   *    any, must include the following acknowlegement:
27   *       "This product includes software developed by the
28   *        Apache Software Foundation (http://www.apache.org/)."
29   *    Alternately, this acknowlegement may appear in the software itself,
30   *    if and wherever such third-party acknowlegements normally appear.
31   *
32   * 4. The names "The Jakarta Project", "Commons", and "Apache Software
33   *    Foundation" must not be used to endorse or promote products derived
34   *    from this software without prior written permission. For written
35   *    permission, please contact apache@apache.org.
36   *
37   * 5. Products derived from this software may not be called "Apache"
38   *    nor may "Apache" appear in their names without prior written
39   *    permission of the Apache Group.
40   *
41   * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
42   * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
43   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
44   * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
45   * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
46   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
47   * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
48   * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
49   * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
50   * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
51   * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
52   * SUCH DAMAGE.
53   * ====================================================================
54   *
55   * This software consists of voluntary contributions made by many
56   * individuals on behalf of the Apache Software Foundation.  For more
57   * information on the Apache Software Foundation, please see
58   * <http://www.apache.org/>.
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     // ---------------------------------------------------------- Class methods
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     // ----------------------------------------------------- Manifest constants
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     // ----------------------------------------------------------- Data members
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     // ----------------------------------------------------- Property accessors
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     // --------------------------------------------------------- Public methods
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 /* FileItem */ parseRequest(ActionRequest req)
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                         // Multiple files.
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                                 // Ignore anything but files inside
379                                 // multipart/mixed.
380                                 multi.discardBodyData();
381                             }
382                             nextSubPart = multi.readBoundary();
383                         }
384                         multi.setBoundary(boundary);
385                     }
386                     else
387                     {
388                         if (getFileName(headers) != null)
389                         {
390                             // A single file.
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                             // A form field.
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                     // Skip this part.
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     // ------------------------------------------------------ Protected methods
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 /* String, String */ headers)
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 /* String, String */ headers)
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 /* String, String */ headers,
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 /* String, String */ parseHeaders(String headerPart)
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                 // Copy a single line of characters into the buffer,
541                 // omitting trailing CRLF.
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                         // This header line is malformed, skip it.
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                         // More that one heder of that name exists,
565                         // append to the list.
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             // Headers were malformed. continue with all that was
580             // parsed.
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 /* String, String */ headers,
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 }