View Javadoc

    1   /*
    2    * Licensed to the Apache Software Foundation (ASF) under one or more
    3    * contributor license agreements.  See the NOTICE file distributed with
    4    * this work for additional information regarding copyright ownership.
    5    * The ASF licenses this file to You under the Apache License, Version 2.0
    6    * (the "License"); you may not use this file except in compliance with
    7    * the License.  You may obtain a copy of the License at
    8    *
    9    *      http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17  package org.apache.commons.fileupload;
    18
    19  import static java.lang.String.format;
    20
    21  import java.io.IOException;
    22  import java.io.InputStream;
    23  import java.io.UnsupportedEncodingException;
    24  import java.util.ArrayList;
    25  import java.util.HashMap;
    26  import java.util.Iterator;
    27  import java.util.List;
    28  import java.util.Locale;
    29  import java.util.Map;
    30  import java.util.NoSuchElementException;
    31
    32  import javax.servlet.http.HttpServletRequest;
    33
    34  import org.apache.commons.fileupload.MultipartStream.ItemInputStream;
    35  import org.apache.commons.fileupload.servlet.ServletFileUpload;
    36  import org.apache.commons.fileupload.servlet.ServletRequestContext;
    37  import org.apache.commons.fileupload.util.Closeable;
    38  import org.apache.commons.fileupload.util.FileItemHeadersImpl;
    39  import org.apache.commons.fileupload.util.LimitedInputStream;
    40  import org.apache.commons.fileupload.util.Streams;
    41  import org.apache.commons.io.IOUtils;
    42
    43  /**
    44   * <p>High level API for processing file uploads.</p>
    45   *
    46   * <p>This class handles multiple files per single HTML widget, sent using
    47   * <code>multipart/mixed</code> encoding type, as specified by
    48   * <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>.  Use {@link
    49   * #parseRequest(RequestContext)} to acquire a list of {@link
    50   * org.apache.commons.fileupload.FileItem}s associated with a given HTML
    51   * widget.</p>
    52   *
    53   * <p>How the data for individual parts is stored is determined by the factory
    54   * used to create them; a given part may be in memory, on disk, or somewhere
    55   * else.</p>
    56   */
    57  public abstract class FileUploadBase {
    58
    59      // ---------------------------------------------------------- Class methods
    60
    61      /**
    62       * <p>Utility method that determines whether the request contains multipart
    63       * content.</p>
    64       *
    65       * <p><strong>NOTE:</strong>This method will be moved to the
    66       * <code>ServletFileUpload</code> class after the FileUpload 1.1 release.
    67       * Unfortunately, since this method is static, it is not possible to
    68       * provide its replacement until this method is removed.</p>
    69       *
    70       * @param ctx The request context to be evaluated. Must be non-null.
    71       *
    72       * @return <code>true</code> if the request is multipart;
    73       *         <code>false</code> otherwise.
    74       */
    75      public static final boolean isMultipartContent(RequestContext ctx) {
    76          String contentType = ctx.getContentType();
    77          if (contentType == null) {
    78              return false;
    79          }
    80          if (contentType.toLowerCase(Locale.ENGLISH).startsWith(MULTIPART)) {
    81              return true;
    82          }
    83          return false;
    84      }
    85
    86      /**
    87       * Utility method that determines whether the request contains multipart
    88       * content.
    89       *
    90       * @param req The servlet request to be evaluated. Must be non-null.
    91       *
    92       * @return <code>true</code> if the request is multipart;
    93       *         <code>false</code> otherwise.
    94       *
    95       * @deprecated 1.1 Use the method on <code>ServletFileUpload</code> instead.
    96       */
    97      @Deprecated
    98      public static boolean isMultipartContent(HttpServletRequest req) {
    99          return ServletFileUpload.isMultipartContent(req);
    100     }
    101
    102     // ----------------------------------------------------- Manifest constants
    103
    104     /**
    105      * HTTP content type header name.
    106      */
    107     public static final String CONTENT_TYPE = "Content-type";
    108
    109     /**
    110      * HTTP content disposition header name.
    111      */
    112     public static final String CONTENT_DISPOSITION = "Content-disposition";
    113
    114     /**
    115      * HTTP content length header name.
    116      */
    117     public static final String CONTENT_LENGTH = "Content-length";
    118
    119     /**
    120      * Content-disposition value for form data.
    121      */
    122     public static final String FORM_DATA = "form-data";
    123
    124     /**
    125      * Content-disposition value for file attachment.
    126      */
    127     public static final String ATTACHMENT = "attachment";
    128
    129     /**
    130      * Part of HTTP content type header.
    131      */
    132     public static final String MULTIPART = "multipart/";
    133
    134     /**
    135      * HTTP content type header for multipart forms.
    136      */
    137     public static final String MULTIPART_FORM_DATA = "multipart/form-data";
    138
    139     /**
    140      * HTTP content type header for multiple uploads.
    141      */
    142     public static final String MULTIPART_MIXED = "multipart/mixed";
    143
    144     /**
    145      * The maximum length of a single header line that will be parsed
    146      * (1024 bytes).
    147      * @deprecated This constant is no longer used. As of commons-fileupload
    148      *   1.2, the only applicable limit is the total size of a parts headers,
    149      *   {@link MultipartStream#HEADER_PART_SIZE_MAX}.
    150      */
    151     @Deprecated
    152     public static final int MAX_HEADER_SIZE = 1024;
    153
    154     // ----------------------------------------------------------- Data members
    155
    156     /**
    157      * The maximum size permitted for the complete request, as opposed to
    158      * {@link #fileSizeMax}. A value of -1 indicates no maximum.
    159      */
    160     private long sizeMax = -1;
    161
    162     /**
    163      * The maximum size permitted for a single uploaded file, as opposed
    164      * to {@link #sizeMax}. A value of -1 indicates no maximum.
    165      */
    166     private long fileSizeMax = -1;
    167
    168     /**
    169      * The content encoding to use when reading part headers.
    170      */
    171     private String headerEncoding;
    172
    173     /**
    174      * The progress listener.
    175      */
    176     private ProgressListener listener;
    177
    178     // ----------------------------------------------------- Property accessors
    179
    180     /**
    181      * Returns the factory class used when creating file items.
    182      *
    183      * @return The factory class for new file items.
    184      */
    185     public abstract FileItemFactory getFileItemFactory();
    186
    187     /**
    188      * Sets the factory class to use when creating file items.
    189      *
    190      * @param factory The factory class for new file items.
    191      */
    192     public abstract void setFileItemFactory(FileItemFactory factory);
    193
    194     /**
    195      * Returns the maximum allowed size of a complete request, as opposed
    196      * to {@link #getFileSizeMax()}.
    197      *
    198      * @return The maximum allowed size, in bytes. The default value of
    199      *   -1 indicates, that there is no limit.
    200      *
    201      * @see #setSizeMax(long)
    202      *
    203      */
    204     public long getSizeMax() {
    205         return sizeMax;
    206     }
    207
    208     /**
    209      * Sets the maximum allowed size of a complete request, as opposed
    210      * to {@link #setFileSizeMax(long)}.
    211      *
    212      * @param sizeMax The maximum allowed size, in bytes. The default value of
    213      *   -1 indicates, that there is no limit.
    214      *
    215      * @see #getSizeMax()
    216      *
    217      */
    218     public void setSizeMax(long sizeMax) {
    219         this.sizeMax = sizeMax;
    220     }
    221
    222     /**
    223      * Returns the maximum allowed size of a single uploaded file,
    224      * as opposed to {@link #getSizeMax()}.
    225      *
    226      * @see #setFileSizeMax(long)
    227      * @return Maximum size of a single uploaded file.
    228      */
    229     public long getFileSizeMax() {
    230         return fileSizeMax;
    231     }
    232
    233     /**
    234      * Sets the maximum allowed size of a single uploaded file,
    235      * as opposed to {@link #getSizeMax()}.
    236      *
    237      * @see #getFileSizeMax()
    238      * @param fileSizeMax Maximum size of a single uploaded file.
    239      */
    240     public void setFileSizeMax(long fileSizeMax) {
    241         this.fileSizeMax = fileSizeMax;
    242     }
    243
    244     /**
    245      * Retrieves the character encoding used when reading the headers of an
    246      * individual part. When not specified, or <code>null</code>, the request
    247      * encoding is used. If that is also not specified, or <code>null</code>,
    248      * the platform default encoding is used.
    249      *
    250      * @return The encoding used to read part headers.
    251      */
    252     public String getHeaderEncoding() {
    253         return headerEncoding;
    254     }
    255
    256     /**
    257      * Specifies the character encoding to be used when reading the headers of
    258      * individual part. When not specified, or <code>null</code>, the request
    259      * encoding is used. If that is also not specified, or <code>null</code>,
    260      * the platform default encoding is used.
    261      *
    262      * @param encoding The encoding used to read part headers.
    263      */
    264     public void setHeaderEncoding(String encoding) {
    265         headerEncoding = encoding;
    266     }
    267
    268     // --------------------------------------------------------- Public methods
    269
    270     /**
    271      * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
    272      * compliant <code>multipart/form-data</code> stream.
    273      *
    274      * @param req The servlet request to be parsed.
    275      *
    276      * @return A list of <code>FileItem</code> instances parsed from the
    277      *         request, in the order that they were transmitted.
    278      *
    279      * @throws FileUploadException if there are problems reading/parsing
    280      *                             the request or storing files.
    281      *
    282      * @deprecated 1.1 Use {@link ServletFileUpload#parseRequest(HttpServletRequest)} instead.
    283      */
    284     @Deprecated
    285     public List<FileItem> parseRequest(HttpServletRequest req)
    286     throws FileUploadException {
    287         return parseRequest(new ServletRequestContext(req));
    288     }
    289
    290     /**
    291      * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
    292      * compliant <code>multipart/form-data</code> stream.
    293      *
    294      * @param ctx The context for the request to be parsed.
    295      *
    296      * @return An iterator to instances of <code>FileItemStream</code>
    297      *         parsed from the request, in the order that they were
    298      *         transmitted.
    299      *
    300      * @throws FileUploadException if there are problems reading/parsing
    301      *                             the request or storing files.
    302      * @throws IOException An I/O error occurred. This may be a network
    303      *   error while communicating with the client or a problem while
    304      *   storing the uploaded content.
    305      */
    306     public FileItemIterator getItemIterator(RequestContext ctx)
    307     throws FileUploadException, IOException {
    308         try {
    309             return new FileItemIteratorImpl(ctx);
    310         } catch (FileUploadIOException e) {
    311             // unwrap encapsulated SizeException
    312             throw (FileUploadException) e.getCause();
    313         }
    314     }
    315
    316     /**
    317      * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
    318      * compliant <code>multipart/form-data</code> stream.
    319      *
    320      * @param ctx The context for the request to be parsed.
    321      *
    322      * @return A list of <code>FileItem</code> instances parsed from the
    323      *         request, in the order that they were transmitted.
    324      *
    325      * @throws FileUploadException if there are problems reading/parsing
    326      *                             the request or storing files.
    327      */
    328     public List<FileItem> parseRequest(RequestContext ctx)
    329             throws FileUploadException {
    330         List<FileItem> items = new ArrayList<FileItem>();
    331         boolean successful = false;
    332         try {
    333             FileItemIterator iter = getItemIterator(ctx);
    334             FileItemFactory fac = getFileItemFactory();
    335             if (fac == null) {
    336                 throw new NullPointerException("No FileItemFactory has been set.");
    337             }
    338             while (iter.hasNext()) {
    339                 final FileItemStream item = iter.next();
    340                 // Don't use getName() here to prevent an InvalidFileNameException.
    341                 final String fileName = ((FileItemIteratorImpl.FileItemStreamImpl) item).name;
    342                 FileItem fileItem = fac.createItem(item.getFieldName(), item.getContentType(),
    343                                                    item.isFormField(), fileName);
    344                 items.add(fileItem);
    345                 try {
    346                     Streams.copy(item.openStream(), fileItem.getOutputStream(), true);
    347                 } catch (FileUploadIOException e) {
    348                     throw (FileUploadException) e.getCause();
    349                 } catch (IOException e) {
    350                     throw new IOFileUploadException(format("Processing of %s request failed. %s",
    351                                                            MULTIPART_FORM_DATA, e.getMessage()), e);
    352                 }
    353                 final FileItemHeaders fih = item.getHeaders();
    354                 fileItem.setHeaders(fih);
    355             }
    356             successful = true;
    357             return items;
    358         } catch (FileUploadIOException e) {
    359             throw (FileUploadException) e.getCause();
    360         } catch (IOException e) {
    361             throw new FileUploadException(e.getMessage(), e);
    362         } finally {
    363             if (!successful) {
    364                 for (FileItem fileItem : items) {
    365                     try {
    366                         fileItem.delete();
    367                     } catch (Exception ignored) {
    368                         // ignored TODO perhaps add to tracker delete failure list somehow?
    369                     }
    370                 }
    371             }
    372         }
    373     }
    374
    375     /**
    376      * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
    377      * compliant <code>multipart/form-data</code> stream.
    378      *
    379      * @param ctx The context for the request to be parsed.
    380      *
    381      * @return A map of <code>FileItem</code> instances parsed from the request.
    382      *
    383      * @throws FileUploadException if there are problems reading/parsing
    384      *                             the request or storing files.
    385      *
    386      * @since 1.3
    387      */
    388     public Map<String, List<FileItem>> parseParameterMap(RequestContext ctx)
    389             throws FileUploadException {
    390         final List<FileItem> items = parseRequest(ctx);
    391         final Map<String, List<FileItem>> itemsMap = new HashMap<String, List<FileItem>>(items.size());
    392
    393         for (FileItem fileItem : items) {
    394             String fieldName = fileItem.getFieldName();
    395             List<FileItem> mappedItems = itemsMap.get(fieldName);
    396
    397             if (mappedItems == null) {
    398                 mappedItems = new ArrayList<FileItem>();
    399                 itemsMap.put(fieldName, mappedItems);
    400             }
    401
    402             mappedItems.add(fileItem);
    403         }
    404
    405         return itemsMap;
    406     }
    407
    408     // ------------------------------------------------------ Protected methods
    409
    410     /**
    411      * Retrieves the boundary from the <code>Content-type</code> header.
    412      *
    413      * @param contentType The value of the content type header from which to
    414      *                    extract the boundary value.
    415      *
    416      * @return The boundary, as a byte array.
    417      */
    418     protected byte[] getBoundary(String contentType) {
    419         ParameterParser parser = new ParameterParser();
    420         parser.setLowerCaseNames(true);
    421         // Parameter parser can handle null input
    422         Map<String, String> params = parser.parse(contentType, new char[] {';', ','});
    423         String boundaryStr = params.get("boundary");
    424
    425         if (boundaryStr == null) {
    426             return null;
    427         }
    428         byte[] boundary;
    429         try {
    430             boundary = boundaryStr.getBytes("ISO-8859-1");
    431         } catch (UnsupportedEncodingException e) {
    432             boundary = boundaryStr.getBytes(); // Intentionally falls back to default charset
    433         }
    434         return boundary;
    435     }
    436
    437     /**
    438      * Retrieves the file name from the <code>Content-disposition</code>
    439      * header.
    440      *
    441      * @param headers A <code>Map</code> containing the HTTP request headers.
    442      *
    443      * @return The file name for the current <code>encapsulation</code>.
    444      * @deprecated 1.2.1 Use {@link #getFileName(FileItemHeaders)}.
    445      */
    446     @Deprecated
    447     protected String getFileName(Map<String, String> headers) {
    448         return getFileName(getHeader(headers, CONTENT_DISPOSITION));
    449     }
    450
    451     /**
    452      * Retrieves the file name from the <code>Content-disposition</code>
    453      * header.
    454      *
    455      * @param headers The HTTP headers object.
    456      *
    457      * @return The file name for the current <code>encapsulation</code>.
    458      */
    459     protected String getFileName(FileItemHeaders headers) {
    460         return getFileName(headers.getHeader(CONTENT_DISPOSITION));
    461     }
    462
    463     /**
    464      * Returns the given content-disposition headers file name.
    465      * @param pContentDisposition The content-disposition headers value.
    466      * @return The file name
    467      */
    468     private String getFileName(String pContentDisposition) {
    469         String fileName = null;
    470         if (pContentDisposition != null) {
    471             String cdl = pContentDisposition.toLowerCase(Locale.ENGLISH);
    472             if (cdl.startsWith(FORM_DATA) || cdl.startsWith(ATTACHMENT)) {
    473                 ParameterParser parser = new ParameterParser();
    474                 parser.setLowerCaseNames(true);
    475                 // Parameter parser can handle null input
    476                 Map<String, String> params = parser.parse(pContentDisposition, ';');
    477                 if (params.containsKey("filename")) {
    478                     fileName = params.get("filename");
    479                     if (fileName != null) {
    480                         fileName = fileName.trim();
    481                     } else {
    482                         // Even if there is no value, the parameter is present,
    483                         // so we return an empty file name rather than no file
    484                         // name.
    485                         fileName = "";
    486                     }
    487                 }
    488             }
    489         }
    490         return fileName;
    491     }
    492
    493     /**
    494      * Retrieves the field name from the <code>Content-disposition</code>
    495      * header.
    496      *
    497      * @param headers A <code>Map</code> containing the HTTP request headers.
    498      *
    499      * @return The field name for the current <code>encapsulation</code>.
    500      */
    501     protected String getFieldName(FileItemHeaders headers) {
    502         return getFieldName(headers.getHeader(CONTENT_DISPOSITION));
    503     }
    504
    505     /**
    506      * Returns the field name, which is given by the content-disposition
    507      * header.
    508      * @param pContentDisposition The content-dispositions header value.
    509      * @return The field jake
    510      */
    511     private String getFieldName(String pContentDisposition) {
    512         String fieldName = null;
    513         if (pContentDisposition != null
    514                 && pContentDisposition.toLowerCase(Locale.ENGLISH).startsWith(FORM_DATA)) {
    515             ParameterParser parser = new ParameterParser();
    516             parser.setLowerCaseNames(true);
    517             // Parameter parser can handle null input
    518             Map<String, String> params = parser.parse(pContentDisposition, ';');
    519             fieldName = params.get("name");
    520             if (fieldName != null) {
    521                 fieldName = fieldName.trim();
    522             }
    523         }
    524         return fieldName;
    525     }
    526
    527     /**
    528      * Retrieves the field name from the <code>Content-disposition</code>
    529      * header.
    530      *
    531      * @param headers A <code>Map</code> containing the HTTP request headers.
    532      *
    533      * @return The field name for the current <code>encapsulation</code>.
    534      * @deprecated 1.2.1 Use {@link #getFieldName(FileItemHeaders)}.
    535      */
    536     @Deprecated
    537     protected String getFieldName(Map<String, String> headers) {
    538         return getFieldName(getHeader(headers, CONTENT_DISPOSITION));
    539     }
    540
    541     /**
    542      * Creates a new {@link FileItem} instance.
    543      *
    544      * @param headers       A <code>Map</code> containing the HTTP request
    545      *                      headers.
    546      * @param isFormField   Whether or not this item is a form field, as
    547      *                      opposed to a file.
    548      *
    549      * @return A newly created <code>FileItem</code> instance.
    550      *
    551      * @throws FileUploadException if an error occurs.
    552      * @deprecated 1.2 This method is no longer used in favour of
    553      *   internally created instances of {@link FileItem}.
    554      */
    555     @Deprecated
    556     protected FileItem createItem(Map<String, String> headers,
    557                                   boolean isFormField)
    558         throws FileUploadException {
    559         return getFileItemFactory().createItem(getFieldName(headers),
    560                 getHeader(headers, CONTENT_TYPE),
    561                 isFormField,
    562                 getFileName(headers));
    563     }
    564
    565     /**
    566      * <p> Parses the <code>header-part</code> and returns as key/value
    567      * pairs.
    568      *
    569      * <p> If there are multiple headers of the same names, the name
    570      * will map to a comma-separated list containing the values.
    571      *
    572      * @param headerPart The <code>header-part</code> of the current
    573      *                   <code>encapsulation</code>.
    574      *
    575      * @return A <code>Map</code> containing the parsed HTTP request headers.
    576      */
    577     protected FileItemHeaders getParsedHeaders(String headerPart) {
    578         final int len = headerPart.length();
    579         FileItemHeadersImpl headers = newFileItemHeaders();
    580         int start = 0;
    581         for (;;) {
    582             int end = parseEndOfLine(headerPart, start);
    583             if (start == end) {
    584                 break;
    585             }
    586             StringBuilder header = new StringBuilder(headerPart.substring(start, end));
    587             start = end + 2;
    588             while (start < len) {
    589                 int nonWs = start;
    590                 while (nonWs < len) {
    591                     char c = headerPart.charAt(nonWs);
    592                     if (c != ' '  &&  c != '\t') {
    593                         break;
    594                     }
    595                     ++nonWs;
    596                 }
    597                 if (nonWs == start) {
    598                     break;
    599                 }
    600                 // Continuation line found
    601                 end = parseEndOfLine(headerPart, nonWs);
    602                 header.append(" ").append(headerPart.substring(nonWs, end));
    603                 start = end + 2;
    604             }
    605             parseHeaderLine(headers, header.toString());
    606         }
    607         return headers;
    608     }
    609
    610     /**
    611      * Creates a new instance of {@link FileItemHeaders}.
    612      * @return The new instance.
    613      */
    614     protected FileItemHeadersImpl newFileItemHeaders() {
    615         return new FileItemHeadersImpl();
    616     }
    617
    618     /**
    619      * <p> Parses the <code>header-part</code> and returns as key/value
    620      * pairs.
    621      *
    622      * <p> If there are multiple headers of the same names, the name
    623      * will map to a comma-separated list containing the values.
    624      *
    625      * @param headerPart The <code>header-part</code> of the current
    626      *                   <code>encapsulation</code>.
    627      *
    628      * @return A <code>Map</code> containing the parsed HTTP request headers.
    629      * @deprecated 1.2.1 Use {@link #getParsedHeaders(String)}
    630      */
    631     @Deprecated
    632     protected Map<String, String> parseHeaders(String headerPart) {
    633         FileItemHeaders headers = getParsedHeaders(headerPart);
    634         Map<String, String> result = new HashMap<String, String>();
    635         for (Iterator<String> iter = headers.getHeaderNames();  iter.hasNext();) {
    636             String headerName = iter.next();
    637             Iterator<String> iter2 = headers.getHeaders(headerName);
    638             StringBuilder headerValue = new StringBuilder(iter2.next());
    639             while (iter2.hasNext()) {
    640                 headerValue.append(",").append(iter2.next());
    641             }
    642             result.put(headerName, headerValue.toString());
    643         }
    644         return result;
    645     }
    646
    647     /**
    648      * Skips bytes until the end of the current line.
    649      * @param headerPart The headers, which are being parsed.
    650      * @param end Index of the last byte, which has yet been
    651      *   processed.
    652      * @return Index of the \r\n sequence, which indicates
    653      *   end of line.
    654      */
    655     private int parseEndOfLine(String headerPart, int end) {
    656         int index = end;
    657         for (;;) {
    658             int offset = headerPart.indexOf('\r', index);
    659             if (offset == -1  ||  offset + 1 >= headerPart.length()) {
    660                 throw new IllegalStateException(
    661                     "Expected headers to be terminated by an empty line.");
    662             }
    663             if (headerPart.charAt(offset + 1) == '\n') {
    664                 return offset;
    665             }
    666             index = offset + 1;
    667         }
    668     }
    669
    670     /**
    671      * Reads the next header line.
    672      * @param headers String with all headers.
    673      * @param header Map where to store the current header.
    674      */
    675     private void parseHeaderLine(FileItemHeadersImpl headers, String header) {
    676         final int colonOffset = header.indexOf(':');
    677         if (colonOffset == -1) {
    678             // This header line is malformed, skip it.
    679             return;
    680         }
    681         String headerName = header.substring(0, colonOffset).trim();
    682         String headerValue =
    683             header.substring(header.indexOf(':') + 1).trim();
    684         headers.addHeader(headerName, headerValue);
    685     }
    686
    687     /**
    688      * Returns the header with the specified name from the supplied map. The
    689      * header lookup is case-insensitive.
    690      *
    691      * @param headers A <code>Map</code> containing the HTTP request headers.
    692      * @param name    The name of the header to return.
    693      *
    694      * @return The value of specified header, or a comma-separated list if
    695      *         there were multiple headers of that name.
    696      * @deprecated 1.2.1 Use {@link FileItemHeaders#getHeader(String)}.
    697      */
    698     @Deprecated
    699     protected final String getHeader(Map<String, String> headers,
    700             String name) {
    701         return headers.get(name.toLowerCase(Locale.ENGLISH));
    702     }
    703
    704     /**
    705      * The iterator, which is returned by
    706      * {@link FileUploadBase#getItemIterator(RequestContext)}.
    707      */
    708     private class FileItemIteratorImpl implements FileItemIterator {
    709
    710         /**
    711          * Default implementation of {@link FileItemStream}.
    712          */
    713         class FileItemStreamImpl implements FileItemStream {
    714
    715             /**
    716              * The file items content type.
    717              */
    718             private final String contentType;
    719
    720             /**
    721              * The file items field name.
    722              */
    723             private final String fieldName;
    724
    725             /**
    726              * The file items file name.
    727              */
    728             private final String name;
    729
    730             /**
    731              * Whether the file item is a form field.
    732              */
    733             private final boolean formField;
    734
    735             /**
    736              * The file items input stream.
    737              */
    738             private final InputStream stream;
    739
    740             /**
    741              * Whether the file item was already opened.
    742              */
    743             private boolean opened;
    744
    745             /**
    746              * The headers, if any.
    747              */
    748             private FileItemHeaders headers;
    749
    750             /**
    751              * Creates a new instance.
    752              *
    753              * @param pName The items file name, or null.
    754              * @param pFieldName The items field name.
    755              * @param pContentType The items content type, or null.
    756              * @param pFormField Whether the item is a form field.
    757              * @param pContentLength The items content length, if known, or -1
    758              * @throws IOException Creating the file item failed.
    759              */
    760             FileItemStreamImpl(String pName, String pFieldName,
    761                     String pContentType, boolean pFormField,
    762                     long pContentLength) throws IOException {
    763                 name = pName;
    764                 fieldName = pFieldName;
    765                 contentType = pContentType;
    766                 formField = pFormField;
    767                 if (fileSizeMax != -1) { // Check if limit is already exceeded
    768                     if (pContentLength != -1
    769                             && pContentLength > fileSizeMax) {
    770                         FileSizeLimitExceededException e =
    771                                 new FileSizeLimitExceededException(
    772                                         format("The field %s exceeds its maximum permitted size of %s bytes.",
    773                                                 fieldName, Long.valueOf(fileSizeMax)),
    774                                         pContentLength, fileSizeMax);
    775                         e.setFileName(pName);
    776                         e.setFieldName(pFieldName);
    777                         throw new FileUploadIOException(e);
    778                     }
    779                 }
    780                 // OK to construct stream now
    781                 final ItemInputStream itemStream = multi.newInputStream();
    782                 InputStream istream = itemStream;
    783                 if (fileSizeMax != -1) {
    784                     istream = new LimitedInputStream(istream, fileSizeMax) {
    785                         @Override
    786                         protected void raiseError(long pSizeMax, long pCount)
    787                                 throws IOException {
    788                             itemStream.close(true);
    789                             FileSizeLimitExceededException e =
    790                                 new FileSizeLimitExceededException(
    791                                     format("The field %s exceeds its maximum permitted size of %s bytes.",
    792                                            fieldName, Long.valueOf(pSizeMax)),
    793                                     pCount, pSizeMax);
    794                             e.setFieldName(fieldName);
    795                             e.setFileName(name);
    796                             throw new FileUploadIOException(e);
    797                         }
    798                     };
    799                 }
    800                 stream = istream;
    801             }
    802
    803             /**
    804              * Returns the items content type, or null.
    805              *
    806              * @return Content type, if known, or null.
    807              */
    808             @Override
    809             public String getContentType() {
    810                 return contentType;
    811             }
    812
    813             /**
    814              * Returns the items field name.
    815              *
    816              * @return Field name.
    817              */
    818             @Override
    819             public String getFieldName() {
    820                 return fieldName;
    821             }
    822
    823             /**
    824              * Returns the items file name.
    825              *
    826              * @return File name, if known, or null.
    827              * @throws InvalidFileNameException The file name contains a NUL character,
    828              *   which might be an indicator of a security attack. If you intend to
    829              *   use the file name anyways, catch the exception and use
    830              *   InvalidFileNameException#getName().
    831              */
    832             @Override
    833             public String getName() {
    834                 return Streams.checkFileName(name);
    835             }
    836
    837             /**
    838              * Returns, whether this is a form field.
    839              *
    840              * @return True, if the item is a form field,
    841              *   otherwise false.
    842              */
    843             @Override
    844             public boolean isFormField() {
    845                 return formField;
    846             }
    847
    848             /**
    849              * Returns an input stream, which may be used to
    850              * read the items contents.
    851              *
    852              * @return Opened input stream.
    853              * @throws IOException An I/O error occurred.
    854              */
    855             @Override
    856             public InputStream openStream() throws IOException {
    857                 if (opened) {
    858                     throw new IllegalStateException(
    859                             "The stream was already opened.");
    860                 }
    861                 if (((Closeable) stream).isClosed()) {
    862                     throw new FileItemStream.ItemSkippedException();
    863                 }
    864                 return stream;
    865             }
    866
    867             /**
    868              * Closes the file item.
    869              *
    870              * @throws IOException An I/O error occurred.
    871              */
    872             void close() throws IOException {
    873                 stream.close();
    874             }
    875
    876             /**
    877              * Returns the file item headers.
    878              *
    879              * @return The items header object
    880              */
    881             @Override
    882             public FileItemHeaders getHeaders() {
    883                 return headers;
    884             }
    885
    886             /**
    887              * Sets the file item headers.
    888              *
    889              * @param pHeaders The items header object
    890              */
    891             @Override
    892             public void setHeaders(FileItemHeaders pHeaders) {
    893                 headers = pHeaders;
    894             }
    895
    896         }
    897
    898         /**
    899          * The multi part stream to process.
    900          */
    901         private final MultipartStream multi;
    902
    903         /**
    904          * The notifier, which used for triggering the
    905          * {@link ProgressListener}.
    906          */
    907         private final MultipartStream.ProgressNotifier notifier;
    908
    909         /**
    910          * The boundary, which separates the various parts.
    911          */
    912         private final byte[] boundary;
    913
    914         /**
    915          * The item, which we currently process.
    916          */
    917         private FileItemStreamImpl currentItem;
    918
    919         /**
    920          * The current items field name.
    921          */
    922         private String currentFieldName;
    923
    924         /**
    925          * Whether we are currently skipping the preamble.
    926          */
    927         private boolean skipPreamble;
    928
    929         /**
    930          * Whether the current item may still be read.
    931          */
    932         private boolean itemValid;
    933
    934         /**
    935          * Whether we have seen the end of the file.
    936          */
    937         private boolean eof;
    938
    939         /**
    940          * Creates a new instance.
    941          *
    942          * @param ctx The request context.
    943          * @throws FileUploadException An error occurred while
    944          *   parsing the request.
    945          * @throws IOException An I/O error occurred.
    946          */
    947         FileItemIteratorImpl(RequestContext ctx)
    948                 throws FileUploadException, IOException {
    949             if (ctx == null) {
    950                 throw new NullPointerException("ctx parameter");
    951             }
    952
    953             String contentType = ctx.getContentType();
    954             if ((null == contentType)
    955                     || (!contentType.toLowerCase(Locale.ENGLISH).startsWith(MULTIPART))) {
    956                 throw new InvalidContentTypeException(
    957                         format("the request doesn't contain a %s or %s stream, content type header is %s",
    958                                MULTIPART_FORM_DATA, MULTIPART_MIXED, contentType));
    959             }
    960
    961
    962             @SuppressWarnings("deprecation") // still has to be backward compatible
    963             final int contentLengthInt = ctx.getContentLength();
    964
    965             final long requestSize = UploadContext.class.isAssignableFrom(ctx.getClass())
    966                                      // Inline conditional is OK here CHECKSTYLE:OFF
    967                                      ? ((UploadContext) ctx).contentLength()
    968                                      : contentLengthInt;
    969                                      // CHECKSTYLE:ON
    970
    971             InputStream input; // N.B. this is eventually closed in MultipartStream processing
    972             if (sizeMax >= 0) {
    973                 if (requestSize != -1 && requestSize > sizeMax) {
    974                     throw new SizeLimitExceededException(
    975                         format("the request was rejected because its size (%s) exceeds the configured maximum (%s)",
    976                                 Long.valueOf(requestSize), Long.valueOf(sizeMax)),
    977                                requestSize, sizeMax);
    978                 }
    979                 // N.B. this is eventually closed in MultipartStream processing
    980                 input = new LimitedInputStream(ctx.getInputStream(), sizeMax) {
    981                     @Override
    982                     protected void raiseError(long pSizeMax, long pCount)
    983                             throws IOException {
    984                         FileUploadException ex = new SizeLimitExceededException(
    985                         format("the request was rejected because its size (%s) exceeds the configured maximum (%s)",
    986                                 Long.valueOf(pCount), Long.valueOf(pSizeMax)),
    987                                pCount, pSizeMax);
    988                         throw new FileUploadIOException(ex);
    989                     }
    990                 };
    991             } else {
    992                 input = ctx.getInputStream();
    993             }
    994
    995             String charEncoding = headerEncoding;
    996             if (charEncoding == null) {
    997                 charEncoding = ctx.getCharacterEncoding();
    998             }
    999
    1000             boundary = getBoundary(contentType);
    1001             if (boundary == null) {
    1002                 IOUtils.closeQuietly(input); // avoid possible resource leak
    1003                 throw new FileUploadException("the request was rejected because no multipart boundary was found");
    1004             }
    1005
    1006             notifier = new MultipartStream.ProgressNotifier(listener, requestSize);
    1007             try {
    1008                 multi = new MultipartStream(input, boundary, notifier);
    1009             } catch (IllegalArgumentException iae) {
    1010                 IOUtils.closeQuietly(input); // avoid possible resource leak
    1011                 throw new InvalidContentTypeException(
    1012                         format("The boundary specified in the %s header is too long", CONTENT_TYPE), iae);
    1013             }
    1014             multi.setHeaderEncoding(charEncoding);
    1015
    1016             skipPreamble = true;
    1017             findNextItem();
    1018         }
    1019
    1020         /**
    1021          * Called for finding the next item, if any.
    1022          *
    1023          * @return True, if an next item was found, otherwise false.
    1024          * @throws IOException An I/O error occurred.
    1025          */
    1026         private boolean findNextItem() throws IOException {
    1027             if (eof) {
    1028                 return false;
    1029             }
    1030             if (currentItem != null) {
    1031                 currentItem.close();
    1032                 currentItem = null;
    1033             }
    1034             for (;;) {
    1035                 boolean nextPart;
    1036                 if (skipPreamble) {
    1037                     nextPart = multi.skipPreamble();
    1038                 } else {
    1039                     nextPart = multi.readBoundary();
    1040                 }
    1041                 if (!nextPart) {
    1042                     if (currentFieldName == null) {
    1043                         // Outer multipart terminated -> No more data
    1044                         eof = true;
    1045                         return false;
    1046                     }
    1047                     // Inner multipart terminated -> Return to parsing the outer
    1048                     multi.setBoundary(boundary);
    1049                     currentFieldName = null;
    1050                     continue;
    1051                 }
    1052                 FileItemHeaders headers = getParsedHeaders(multi.readHeaders());
    1053                 if (currentFieldName == null) {
    1054                     // We're parsing the outer multipart
    1055                     String fieldName = getFieldName(headers);
    1056                     if (fieldName != null) {
    1057                         String subContentType = headers.getHeader(CONTENT_TYPE);
    1058                         if (subContentType != null
    1059                                 &&  subContentType.toLowerCase(Locale.ENGLISH)
    1060                                         .startsWith(MULTIPART_MIXED)) {
    1061                             currentFieldName = fieldName;
    1062                             // Multiple files associated with this field name
    1063                             byte[] subBoundary = getBoundary(subContentType);
    1064                             multi.setBoundary(subBoundary);
    1065                             skipPreamble = true;
    1066                             continue;
    1067                         }
    1068                         String fileName = getFileName(headers);
    1069                         currentItem = new FileItemStreamImpl(fileName,
    1070                                 fieldName, headers.getHeader(CONTENT_TYPE),
    1071                                 fileName == null, getContentLength(headers));
    1072                         currentItem.setHeaders(headers);
    1073                         notifier.noteItem();
    1074                         itemValid = true;
    1075                         return true;
    1076                     }
    1077                 } else {
    1078                     String fileName = getFileName(headers);
    1079                     if (fileName != null) {
    1080                         currentItem = new FileItemStreamImpl(fileName,
    1081                                 currentFieldName,
    1082                                 headers.getHeader(CONTENT_TYPE),
    1083                                 false, getContentLength(headers));
    1084                         currentItem.setHeaders(headers);
    1085                         notifier.noteItem();
    1086                         itemValid = true;
    1087                         return true;
    1088                     }
    1089                 }
    1090                 multi.discardBodyData();
    1091             }
    1092         }
    1093
    1094         private long getContentLength(FileItemHeaders pHeaders) {
    1095             try {
    1096                 return Long.parseLong(pHeaders.getHeader(CONTENT_LENGTH));
    1097             } catch (Exception e) {
    1098                 return -1;
    1099             }
    1100         }
    1101
    1102         /**
    1103          * Returns, whether another instance of {@link FileItemStream}
    1104          * is available.
    1105          *
    1106          * @throws FileUploadException Parsing or processing the
    1107          *   file item failed.
    1108          * @throws IOException Reading the file item failed.
    1109          * @return True, if one or more additional file items
    1110          *   are available, otherwise false.
    1111          */
    1112         @Override
    1113         public boolean hasNext() throws FileUploadException, IOException {
    1114             if (eof) {
    1115                 return false;
    1116             }
    1117             if (itemValid) {
    1118                 return true;
    1119             }
    1120             try {
    1121                 return findNextItem();
    1122             } catch (FileUploadIOException e) {
    1123                 // unwrap encapsulated SizeException
    1124                 throw (FileUploadException) e.getCause();
    1125             }
    1126         }
    1127
    1128         /**
    1129          * Returns the next available {@link FileItemStream}.
    1130          *
    1131          * @throws java.util.NoSuchElementException No more items are
    1132          *   available. Use {@link #hasNext()} to prevent this exception.
    1133          * @throws FileUploadException Parsing or processing the
    1134          *   file item failed.
    1135          * @throws IOException Reading the file item failed.
    1136          * @return FileItemStream instance, which provides
    1137          *   access to the next file item.
    1138          */
    1139         @Override
    1140         public FileItemStream next() throws FileUploadException, IOException {
    1141             if (eof  ||  (!itemValid && !hasNext())) {
    1142                 throw new NoSuchElementException();
    1143             }
    1144             itemValid = false;
    1145             return currentItem;
    1146         }
    1147
    1148     }
    1149
    1150     /**
    1151      * This exception is thrown for hiding an inner
    1152      * {@link FileUploadException} in an {@link IOException}.
    1153      */
    1154     public static class FileUploadIOException extends IOException {
    1155
    1156         /**
    1157          * The exceptions UID, for serializing an instance.
    1158          */
    1159         private static final long serialVersionUID = -7047616958165584154L;
    1160
    1161         /**
    1162          * The exceptions cause; we overwrite the parent
    1163          * classes field, which is available since Java
    1164          * 1.4 only.
    1165          */
    1166         private final FileUploadException cause;
    1167
    1168         /**
    1169          * Creates a <code>FileUploadIOException</code> with the
    1170          * given cause.
    1171          *
    1172          * @param pCause The exceptions cause, if any, or null.
    1173          */
    1174         public FileUploadIOException(FileUploadException pCause) {
    1175             // We're not doing super(pCause) cause of 1.3 compatibility.
    1176             cause = pCause;
    1177         }
    1178
    1179         /**
    1180          * Returns the exceptions cause.
    1181          *
    1182          * @return The exceptions cause, if any, or null.
    1183          */
    1184         @Override
    1185         public Throwable getCause() {
    1186             return cause;
    1187         }
    1188
    1189     }
    1190
    1191     /**
    1192      * Thrown to indicate that the request is not a multipart request.
    1193      */
    1194     public static class InvalidContentTypeException
    1195             extends FileUploadException {
    1196
    1197         /**
    1198          * The exceptions UID, for serializing an instance.
    1199          */
    1200         private static final long serialVersionUID = -9073026332015646668L;
    1201
    1202         /**
    1203          * Constructs a <code>InvalidContentTypeException</code> with no
    1204          * detail message.
    1205          */
    1206         public InvalidContentTypeException() {
    1207             super();
    1208         }
    1209
    1210         /**
    1211          * Constructs an <code>InvalidContentTypeException</code> with
    1212          * the specified detail message.
    1213          *
    1214          * @param message The detail message.
    1215          */
    1216         public InvalidContentTypeException(String message) {
    1217             super(message);
    1218         }
    1219
    1220         /**
    1221          * Constructs an <code>InvalidContentTypeException</code> with
    1222          * the specified detail message and cause.
    1223          *
    1224          * @param msg The detail message.
    1225          * @param cause the original cause
    1226          *
    1227          * @since 1.3.1
    1228          */
    1229         public InvalidContentTypeException(String msg, Throwable cause) {
    1230             super(msg, cause);
    1231         }
    1232     }
    1233
    1234     /**
    1235      * Thrown to indicate an IOException.
    1236      */
    1237     public static class IOFileUploadException extends FileUploadException {
    1238
    1239         /**
    1240          * The exceptions UID, for serializing an instance.
    1241          */
    1242         private static final long serialVersionUID = 1749796615868477269L;
    1243
    1244         /**
    1245          * The exceptions cause; we overwrite the parent
    1246          * classes field, which is available since Java
    1247          * 1.4 only.
    1248          */
    1249         private final IOException cause;
    1250
    1251         /**
    1252          * Creates a new instance with the given cause.
    1253          *
    1254          * @param pMsg The detail message.
    1255          * @param pException The exceptions cause.
    1256          */
    1257         public IOFileUploadException(String pMsg, IOException pException) {
    1258             super(pMsg);
    1259             cause = pException;
    1260         }
    1261
    1262         /**
    1263          * Returns the exceptions cause.
    1264          *
    1265          * @return The exceptions cause, if any, or null.
    1266          */
    1267         @Override
    1268         public Throwable getCause() {
    1269             return cause;
    1270         }
    1271
    1272     }
    1273
    1274     /**
    1275      * This exception is thrown, if a requests permitted size
    1276      * is exceeded.
    1277      */
    1278     protected abstract static class SizeException extends FileUploadException {
    1279
    1280         /**
    1281          * Serial version UID, being used, if serialized.
    1282          */
    1283         private static final long serialVersionUID = -8776225574705254126L;
    1284
    1285         /**
    1286          * The actual size of the request.
    1287          */
    1288         private final long actual;
    1289
    1290         /**
    1291          * The maximum permitted size of the request.
    1292          */
    1293         private final long permitted;
    1294
    1295         /**
    1296          * Creates a new instance.
    1297          *
    1298          * @param message The detail message.
    1299          * @param actual The actual number of bytes in the request.
    1300          * @param permitted The requests size limit, in bytes.
    1301          */
    1302         protected SizeException(String message, long actual, long permitted) {
    1303             super(message);
    1304             this.actual = actual;
    1305             this.permitted = permitted;
    1306         }
    1307
    1308         /**
    1309          * Retrieves the actual size of the request.
    1310          *
    1311          * @return The actual size of the request.
    1312          * @since 1.3
    1313          */
    1314         public long getActualSize() {
    1315             return actual;
    1316         }
    1317
    1318         /**
    1319          * Retrieves the permitted size of the request.
    1320          *
    1321          * @return The permitted size of the request.
    1322          * @since 1.3
    1323          */
    1324         public long getPermittedSize() {
    1325             return permitted;
    1326         }
    1327
    1328     }
    1329
    1330     /**
    1331      * Thrown to indicate that the request size is not specified. In other
    1332      * words, it is thrown, if the content-length header is missing or
    1333      * contains the value -1.
    1334      *
    1335      * @deprecated 1.2 As of commons-fileupload 1.2, the presence of a
    1336      *   content-length header is no longer required.
    1337      */
    1338     @Deprecated
    1339     public static class UnknownSizeException
    1340         extends FileUploadException {
    1341
    1342         /**
    1343          * The exceptions UID, for serializing an instance.
    1344          */
    1345         private static final long serialVersionUID = 7062279004812015273L;
    1346
    1347         /**
    1348          * Constructs a <code>UnknownSizeException</code> with no
    1349          * detail message.
    1350          */
    1351         public UnknownSizeException() {
    1352             super();
    1353         }
    1354
    1355         /**
    1356          * Constructs an <code>UnknownSizeException</code> with
    1357          * the specified detail message.
    1358          *
    1359          * @param message The detail message.
    1360          */
    1361         public UnknownSizeException(String message) {
    1362             super(message);
    1363         }
    1364
    1365     }
    1366
    1367     /**
    1368      * Thrown to indicate that the request size exceeds the configured maximum.
    1369      */
    1370     public static class SizeLimitExceededException
    1371             extends SizeException {
    1372
    1373         /**
    1374          * The exceptions UID, for serializing an instance.
    1375          */
    1376         private static final long serialVersionUID = -2474893167098052828L;
    1377
    1378         /**
    1379          * @deprecated 1.2 Replaced by
    1380          * {@link #SizeLimitExceededException(String, long, long)}
    1381          */
    1382         @Deprecated
    1383         public SizeLimitExceededException() {
    1384             this(null, 0, 0);
    1385         }
    1386
    1387         /**
    1388          * @deprecated 1.2 Replaced by
    1389          * {@link #SizeLimitExceededException(String, long, long)}
    1390          * @param message The exceptions detail message.
    1391          */
    1392         @Deprecated
    1393         public SizeLimitExceededException(String message) {
    1394             this(message, 0, 0);
    1395         }
    1396
    1397         /**
    1398          * Constructs a <code>SizeExceededException</code> with
    1399          * the specified detail message, and actual and permitted sizes.
    1400          *
    1401          * @param message   The detail message.
    1402          * @param actual    The actual request size.
    1403          * @param permitted The maximum permitted request size.
    1404          */
    1405         public SizeLimitExceededException(String message, long actual,
    1406                 long permitted) {
    1407             super(message, actual, permitted);
    1408         }
    1409
    1410     }
    1411
    1412     /**
    1413      * Thrown to indicate that A files size exceeds the configured maximum.
    1414      */
    1415     public static class FileSizeLimitExceededException
    1416             extends SizeException {
    1417
    1418         /**
    1419          * The exceptions UID, for serializing an instance.
    1420          */
    1421         private static final long serialVersionUID = 8150776562029630058L;
    1422
    1423         /**
    1424          * File name of the item, which caused the exception.
    1425          */
    1426         private String fileName;
    1427
    1428         /**
    1429          * Field name of the item, which caused the exception.
    1430          */
    1431         private String fieldName;
    1432
    1433         /**
    1434          * Constructs a <code>SizeExceededException</code> with
    1435          * the specified detail message, and actual and permitted sizes.
    1436          *
    1437          * @param message   The detail message.
    1438          * @param actual    The actual request size.
    1439          * @param permitted The maximum permitted request size.
    1440          */
    1441         public FileSizeLimitExceededException(String message, long actual,
    1442                 long permitted) {
    1443             super(message, actual, permitted);
    1444         }
    1445
    1446         /**
    1447          * Returns the file name of the item, which caused the
    1448          * exception.
    1449          *
    1450          * @return File name, if known, or null.
    1451          */
    1452         public String getFileName() {
    1453             return fileName;
    1454         }
    1455
    1456         /**
    1457          * Sets the file name of the item, which caused the
    1458          * exception.
    1459          *
    1460          * @param pFileName the file name of the item, which caused the exception.
    1461          */
    1462         public void setFileName(String pFileName) {
    1463             fileName = pFileName;
    1464         }
    1465
    1466         /**
    1467          * Returns the field name of the item, which caused the
    1468          * exception.
    1469          *
    1470          * @return Field name, if known, or null.
    1471          */
    1472         public String getFieldName() {
    1473             return fieldName;
    1474         }
    1475
    1476         /**
    1477          * Sets the field name of the item, which caused the
    1478          * exception.
    1479          *
    1480          * @param pFieldName the field name of the item,
    1481          *        which caused the exception.
    1482          */
    1483         public void setFieldName(String pFieldName) {
    1484             fieldName = pFieldName;
    1485         }
    1486
    1487     }
    1488
    1489     /**
    1490      * Returns the progress listener.
    1491      *
    1492      * @return The progress listener, if any, or null.
    1493      */
    1494     public ProgressListener getProgressListener() {
    1495         return listener;
    1496     }
    1497
    1498     /**
    1499      * Sets the progress listener.
    1500      *
    1501      * @param pListener The progress listener, if any. Defaults to null.
    1502      */
    1503     public void setProgressListener(ProgressListener pListener) {
    1504         listener = pListener;
    1505     }
    1506
    1507 }