View Javadoc

1   /*
2    * Copyright 2003-2005 The Apache Software Foundation.
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * 
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
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      // ---------------------------------------------------------- Class methods
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      // ----------------------------------------------------- Manifest constants
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     // ----------------------------------------------------------- Data members
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     // ----------------------------------------------------- Property accessors
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     // --------------------------------------------------------- Public methods
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 /* FileItem */ parseRequest(ActionRequest req)
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                         // Multiple files.
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                                 // Ignore anything but files inside
332                                 // multipart/mixed.
333                                 multi.discardBodyData();
334                             }
335                             nextSubPart = multi.readBoundary();
336                         }
337                         multi.setBoundary(boundary);
338                     }
339                     else
340                     {
341                         if (getFileName(headers) != null)
342                         {
343                             // A single file.
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                             // A form field.
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                     // Skip this part.
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     // ------------------------------------------------------ Protected methods
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 /* String, String */ headers)
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 /* String, String */ headers)
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 /* String, String */ headers,
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 /* String, String */ parseHeaders(String headerPart)
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                 // Copy a single line of characters into the buffer,
492                 // omitting trailing CRLF.
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                         // This header line is malformed, skip it.
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                         // More that one heder of that name exists,
516                         // append to the list.
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             // Headers were malformed. continue with all that was
531             // parsed.
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 /* String, String */ headers,
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 }