View Javadoc

1   /*
2    * $Id: CommonsMultipartRequestHandler.java 421119 2006-07-12 04:49:11Z wsmoak $
3    *
4    * Copyright 1999-2006 The Apache Software Foundation.
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *      http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.apache.struts.upload;
19  
20  import org.apache.commons.fileupload.DiskFileUpload;
21  import org.apache.commons.fileupload.disk.DiskFileItem;
22  import org.apache.commons.fileupload.FileItem;
23  import org.apache.commons.fileupload.FileUploadException;
24  import org.apache.commons.logging.Log;
25  import org.apache.commons.logging.LogFactory;
26  import org.apache.struts.Globals;
27  import org.apache.struts.action.ActionMapping;
28  import org.apache.struts.action.ActionServlet;
29  import org.apache.struts.config.ModuleConfig;
30  
31  import javax.servlet.ServletContext;
32  import javax.servlet.ServletException;
33  import javax.servlet.http.HttpServletRequest;
34  
35  import java.io.File;
36  import java.io.FileNotFoundException;
37  import java.io.IOException;
38  import java.io.InputStream;
39  import java.io.Serializable;
40  
41  import java.util.Hashtable;
42  import java.util.Iterator;
43  import java.util.List;
44  
45  /***
46   * <p> This class implements the <code>MultipartRequestHandler</code>
47   * interface by providing a wrapper around the Jakarta Commons FileUpload
48   * library. </p>
49   *
50   * @version $Rev: 421119 $ $Date: 2006-07-11 21:49:11 -0700 (Tue, 11 Jul 2006) $
51   * @since Struts 1.1
52   */
53  public class CommonsMultipartRequestHandler implements MultipartRequestHandler {
54      // ----------------------------------------------------- Manifest Constants
55  
56      /***
57       * <p> The default value for the maximum allowable size, in bytes, of an
58       * uploaded file. The value is equivalent to 250MB. </p>
59       */
60      public static final long DEFAULT_SIZE_MAX = 250 * 1024 * 1024;
61  
62      /***
63       * <p> The default value for the threshold which determines whether an
64       * uploaded file will be written to disk or cached in memory. The value is
65       * equivalent to 250KB. </p>
66       */
67      public static final int DEFAULT_SIZE_THRESHOLD = 256 * 1024;
68  
69      // ----------------------------------------------------- Instance Variables
70  
71      /***
72       * <p> Commons Logging instance. </p>
73       */
74      protected static Log log =
75          LogFactory.getLog(CommonsMultipartRequestHandler.class);
76  
77      /***
78       * <p> The combined text and file request parameters. </p>
79       */
80      private Hashtable elementsAll;
81  
82      /***
83       * <p> The file request parameters. </p>
84       */
85      private Hashtable elementsFile;
86  
87      /***
88       * <p> The text request parameters. </p>
89       */
90      private Hashtable elementsText;
91  
92      /***
93       * <p> The action mapping  with which this handler is associated. </p>
94       */
95      private ActionMapping mapping;
96  
97      /***
98       * <p> The servlet with which this handler is associated. </p>
99       */
100     private ActionServlet servlet;
101 
102     // ---------------------------------------- MultipartRequestHandler Methods
103 
104     /***
105      * <p> Retrieves the servlet with which this handler is associated. </p>
106      *
107      * @return The associated servlet.
108      */
109     public ActionServlet getServlet() {
110         return this.servlet;
111     }
112 
113     /***
114      * <p> Sets the servlet with which this handler is associated. </p>
115      *
116      * @param servlet The associated servlet.
117      */
118     public void setServlet(ActionServlet servlet) {
119         this.servlet = servlet;
120     }
121 
122     /***
123      * <p> Retrieves the action mapping with which this handler is associated.
124      * </p>
125      *
126      * @return The associated action mapping.
127      */
128     public ActionMapping getMapping() {
129         return this.mapping;
130     }
131 
132     /***
133      * <p> Sets the action mapping with which this handler is associated.
134      * </p>
135      *
136      * @param mapping The associated action mapping.
137      */
138     public void setMapping(ActionMapping mapping) {
139         this.mapping = mapping;
140     }
141 
142     /***
143      * <p> Parses the input stream and partitions the parsed items into a set
144      * of form fields and a set of file items. In the process, the parsed
145      * items are translated from Commons FileUpload <code>FileItem</code>
146      * instances to Struts <code>FormFile</code> instances. </p>
147      *
148      * @param request The multipart request to be processed.
149      * @throws ServletException if an unrecoverable error occurs.
150      */
151     public void handleRequest(HttpServletRequest request)
152         throws ServletException {
153         // Get the app config for the current request.
154         ModuleConfig ac =
155             (ModuleConfig) request.getAttribute(Globals.MODULE_KEY);
156 
157         // Create and configure a DIskFileUpload instance.
158         DiskFileUpload upload = new DiskFileUpload();
159 
160         // The following line is to support an "EncodingFilter"
161         // see http://issues.apache.org/bugzilla/show_bug.cgi?id=23255
162         upload.setHeaderEncoding(request.getCharacterEncoding());
163 
164         // Set the maximum size before a FileUploadException will be thrown.
165         upload.setSizeMax(getSizeMax(ac));
166 
167         // Set the maximum size that will be stored in memory.
168         upload.setSizeThreshold((int) getSizeThreshold(ac));
169 
170         // Set the the location for saving data on disk.
171         upload.setRepositoryPath(getRepositoryPath(ac));
172 
173         // Create the hash tables to be populated.
174         elementsText = new Hashtable();
175         elementsFile = new Hashtable();
176         elementsAll = new Hashtable();
177 
178         // Parse the request into file items.
179         List items = null;
180 
181         try {
182             items = upload.parseRequest(request);
183         } catch (DiskFileUpload.SizeLimitExceededException e) {
184             // Special handling for uploads that are too big.
185             request.setAttribute(MultipartRequestHandler.ATTRIBUTE_MAX_LENGTH_EXCEEDED,
186                 Boolean.TRUE);
187 
188             return;
189         } catch (FileUploadException e) {
190             log.error("Failed to parse multipart request", e);
191             throw new ServletException(e);
192         }
193 
194         // Partition the items into form fields and files.
195         Iterator iter = items.iterator();
196 
197         while (iter.hasNext()) {
198             FileItem item = (FileItem) iter.next();
199 
200             if (item.isFormField()) {
201                 addTextParameter(request, item);
202             } else {
203                 addFileParameter(item);
204             }
205         }
206     }
207 
208     /***
209      * <p> Returns a hash table containing the text (that is, non-file)
210      * request parameters. </p>
211      *
212      * @return The text request parameters.
213      */
214     public Hashtable getTextElements() {
215         return this.elementsText;
216     }
217 
218     /***
219      * <p> Returns a hash table containing the file (that is, non-text)
220      * request parameters. </p>
221      *
222      * @return The file request parameters.
223      */
224     public Hashtable getFileElements() {
225         return this.elementsFile;
226     }
227 
228     /***
229      * <p> Returns a hash table containing both text and file request
230      * parameters. </p>
231      *
232      * @return The text and file request parameters.
233      */
234     public Hashtable getAllElements() {
235         return this.elementsAll;
236     }
237 
238     /***
239      * <p> Cleans up when a problem occurs during request processing. </p>
240      */
241     public void rollback() {
242         Iterator iter = elementsFile.values().iterator();
243 
244         while (iter.hasNext()) {
245             FormFile formFile = (FormFile) iter.next();
246 
247             formFile.destroy();
248         }
249     }
250 
251     /***
252      * <p> Cleans up at the end of a request. </p>
253      */
254     public void finish() {
255         rollback();
256     }
257 
258     // -------------------------------------------------------- Support Methods
259 
260     /***
261      * <p> Returns the maximum allowable size, in bytes, of an uploaded file.
262      * The value is obtained from the current module's controller
263      * configuration. </p>
264      *
265      * @param mc The current module's configuration.
266      * @return The maximum allowable file size, in bytes.
267      */
268     protected long getSizeMax(ModuleConfig mc) {
269         return convertSizeToBytes(mc.getControllerConfig().getMaxFileSize(),
270             DEFAULT_SIZE_MAX);
271     }
272 
273     /***
274      * <p> Returns the size threshold which determines whether an uploaded
275      * file will be written to disk or cached in memory. </p>
276      *
277      * @param mc The current module's configuration.
278      * @return The size threshold, in bytes.
279      */
280     protected long getSizeThreshold(ModuleConfig mc) {
281         return convertSizeToBytes(mc.getControllerConfig().getMemFileSize(),
282             DEFAULT_SIZE_THRESHOLD);
283     }
284 
285     /***
286      * <p> Converts a size value from a string representation to its numeric
287      * value. The string must be of the form nnnm, where nnn is an arbitrary
288      * decimal value, and m is a multiplier. The multiplier must be one of
289      * 'K', 'M' and 'G', representing kilobytes, megabytes and gigabytes
290      * respectively. </p><p> If the size value cannot be converted, for
291      * example due to invalid syntax, the supplied default is returned
292      * instead. </p>
293      *
294      * @param sizeString  The string representation of the size to be
295      *                    converted.
296      * @param defaultSize The value to be returned if the string is invalid.
297      * @return The actual size in bytes.
298      */
299     protected long convertSizeToBytes(String sizeString, long defaultSize) {
300         int multiplier = 1;
301 
302         if (sizeString.endsWith("K")) {
303             multiplier = 1024;
304         } else if (sizeString.endsWith("M")) {
305             multiplier = 1024 * 1024;
306         } else if (sizeString.endsWith("G")) {
307             multiplier = 1024 * 1024 * 1024;
308         }
309 
310         if (multiplier != 1) {
311             sizeString = sizeString.substring(0, sizeString.length() - 1);
312         }
313 
314         long size = 0;
315 
316         try {
317             size = Long.parseLong(sizeString);
318         } catch (NumberFormatException nfe) {
319             log.warn("Invalid format for file size ('" + sizeString
320                 + "'). Using default.");
321             size = defaultSize;
322             multiplier = 1;
323         }
324 
325         return (size * multiplier);
326     }
327 
328     /***
329      * <p> Returns the path to the temporary directory to be used for uploaded
330      * files which are written to disk. The directory used is determined from
331      * the first of the following to be non-empty. <ol> <li>A temp dir
332      * explicitly defined either using the <code>tempDir</code> servlet init
333      * param, or the <code>tempDir</code> attribute of the &lt;controller&gt;
334      * element in the Struts config file.</li> <li>The container-specified
335      * temp dir, obtained from the <code>javax.servlet.context.tempdir</code>
336      * servlet context attribute.</li> <li>The temp dir specified by the
337      * <code>java.io.tmpdir</code> system property.</li> (/ol> </p>
338      *
339      * @param mc The module config instance for which the path should be
340      *           determined.
341      * @return The path to the directory to be used to store uploaded files.
342      */
343     protected String getRepositoryPath(ModuleConfig mc) {
344         // First, look for an explicitly defined temp dir.
345         String tempDir = mc.getControllerConfig().getTempDir();
346 
347         // If none, look for a container specified temp dir.
348         if ((tempDir == null) || (tempDir.length() == 0)) {
349             if (servlet != null) {
350                 ServletContext context = servlet.getServletContext();
351                 File tempDirFile =
352                     (File) context.getAttribute("javax.servlet.context.tempdir");
353 
354                 tempDir = tempDirFile.getAbsolutePath();
355             }
356 
357             // If none, pick up the system temp dir.
358             if ((tempDir == null) || (tempDir.length() == 0)) {
359                 tempDir = System.getProperty("java.io.tmpdir");
360             }
361         }
362 
363         if (log.isTraceEnabled()) {
364             log.trace("File upload temp dir: " + tempDir);
365         }
366 
367         return tempDir;
368     }
369 
370     /***
371      * <p> Adds a regular text parameter to the set of text parameters for
372      * this request and also to the list of all parameters. Handles the case
373      * of multiple values for the same parameter by using an array for the
374      * parameter value. </p>
375      *
376      * @param request The request in which the parameter was specified.
377      * @param item    The file item for the parameter to add.
378      */
379     protected void addTextParameter(HttpServletRequest request, FileItem item) {
380         String name = item.getFieldName();
381         String value = null;
382         boolean haveValue = false;
383         String encoding = null;
384 
385         if (item instanceof DiskFileItem) {
386             encoding = ((DiskFileItem)item).getCharSet();
387             if (log.isDebugEnabled()) {
388                 log.debug("DiskFileItem.getCharSet=[" + encoding + "]");
389             }
390         }
391 
392         if (encoding == null) {
393             encoding = request.getCharacterEncoding();
394             if (log.isDebugEnabled()) {
395                 log.debug("request.getCharacterEncoding=[" + encoding + "]");
396             }
397         }
398 
399         if (encoding != null) {
400             try {
401                 value = item.getString(encoding);
402                 haveValue = true;
403             } catch (Exception e) {
404                 // Handled below, since haveValue is false.
405             }
406         }
407 
408         if (!haveValue) {
409             try {
410                 value = item.getString("ISO-8859-1");
411             } catch (java.io.UnsupportedEncodingException uee) {
412                 value = item.getString();
413             }
414 
415             haveValue = true;
416         }
417 
418         if (request instanceof MultipartRequestWrapper) {
419             MultipartRequestWrapper wrapper = (MultipartRequestWrapper) request;
420 
421             wrapper.setParameter(name, value);
422         }
423 
424         String[] oldArray = (String[]) elementsText.get(name);
425         String[] newArray;
426 
427         if (oldArray != null) {
428             newArray = new String[oldArray.length + 1];
429             System.arraycopy(oldArray, 0, newArray, 0, oldArray.length);
430             newArray[oldArray.length] = value;
431         } else {
432             newArray = new String[] { value };
433         }
434 
435         elementsText.put(name, newArray);
436         elementsAll.put(name, newArray);
437     }
438 
439     /***
440      * <p> Adds a file parameter to the set of file parameters for this
441      * request and also to the list of all parameters. </p>
442      *
443      * @param item The file item for the parameter to add.
444      */
445     protected void addFileParameter(FileItem item) {
446         FormFile formFile = new CommonsFormFile(item);
447 
448         elementsFile.put(item.getFieldName(), formFile);
449         elementsAll.put(item.getFieldName(), formFile);
450     }
451 
452     // ---------------------------------------------------------- Inner Classes
453 
454     /***
455      * <p> This class implements the Struts <code>FormFile</code> interface by
456      * wrapping the Commons FileUpload <code>FileItem</code> interface. This
457      * implementation is <i>read-only</i>; any attempt to modify an instance
458      * of this class will result in an <code>UnsupportedOperationException</code>.
459      * </p>
460      */
461     static class CommonsFormFile implements FormFile, Serializable {
462         /***
463          * <p> The <code>FileItem</code> instance wrapped by this object.
464          * </p>
465          */
466         FileItem fileItem;
467 
468         /***
469          * Constructs an instance of this class which wraps the supplied file
470          * item. </p>
471          *
472          * @param fileItem The Commons file item to be wrapped.
473          */
474         public CommonsFormFile(FileItem fileItem) {
475             this.fileItem = fileItem;
476         }
477 
478         /***
479          * <p> Returns the content type for this file. </p>
480          *
481          * @return A String representing content type.
482          */
483         public String getContentType() {
484             return fileItem.getContentType();
485         }
486 
487         /***
488          * <p> Sets the content type for this file. <p> NOTE: This method is
489          * not supported in this implementation. </p>
490          *
491          * @param contentType A string representing the content type.
492          */
493         public void setContentType(String contentType) {
494             throw new UnsupportedOperationException(
495                 "The setContentType() method is not supported.");
496         }
497 
498         /***
499          * <p> Returns the size, in bytes, of this file. </p>
500          *
501          * @return The size of the file, in bytes.
502          */
503         public int getFileSize() {
504             return (int) fileItem.getSize();
505         }
506 
507         /***
508          * <p> Sets the size, in bytes, for this file. <p> NOTE: This method
509          * is not supported in this implementation. </p>
510          *
511          * @param filesize The size of the file, in bytes.
512          */
513         public void setFileSize(int filesize) {
514             throw new UnsupportedOperationException(
515                 "The setFileSize() method is not supported.");
516         }
517 
518         /***
519          * <p> Returns the (client-side) file name for this file. </p>
520          *
521          * @return The client-size file name.
522          */
523         public String getFileName() {
524             return getBaseFileName(fileItem.getName());
525         }
526 
527         /***
528          * <p> Sets the (client-side) file name for this file. <p> NOTE: This
529          * method is not supported in this implementation. </p>
530          *
531          * @param fileName The client-side name for the file.
532          */
533         public void setFileName(String fileName) {
534             throw new UnsupportedOperationException(
535                 "The setFileName() method is not supported.");
536         }
537 
538         /***
539          * <p> Returns the data for this file as a byte array. Note that this
540          * may result in excessive memory usage for large uploads. The use of
541          * the {@link #getInputStream() getInputStream} method is encouraged
542          * as an alternative. </p>
543          *
544          * @return An array of bytes representing the data contained in this
545          *         form file.
546          * @throws FileNotFoundException If some sort of file representation
547          *                               cannot be found for the FormFile
548          * @throws IOException           If there is some sort of IOException
549          */
550         public byte[] getFileData()
551             throws FileNotFoundException, IOException {
552             return fileItem.get();
553         }
554 
555         /***
556          * <p> Get an InputStream that represents this file.  This is the
557          * preferred method of getting file data. </p>
558          *
559          * @throws FileNotFoundException If some sort of file representation
560          *                               cannot be found for the FormFile
561          * @throws IOException           If there is some sort of IOException
562          */
563         public InputStream getInputStream()
564             throws FileNotFoundException, IOException {
565             return fileItem.getInputStream();
566         }
567 
568         /***
569          * <p> Destroy all content for this form file. Implementations should
570          * remove any temporary files or any temporary file data stored
571          * somewhere </p>
572          */
573         public void destroy() {
574             fileItem.delete();
575         }
576 
577         /***
578          * <p> Returns the base file name from the supplied file path. On the
579          * surface, this would appear to be a trivial task. Apparently,
580          * however, some Linux JDKs do not implement <code>File.getName()</code>
581          * correctly for Windows paths, so we attempt to take care of that
582          * here. </p>
583          *
584          * @param filePath The full path to the file.
585          * @return The base file name, from the end of the path.
586          */
587         protected String getBaseFileName(String filePath) {
588             // First, ask the JDK for the base file name.
589             String fileName = new File(filePath).getName();
590 
591             // Now check for a Windows file name parsed incorrectly.
592             int colonIndex = fileName.indexOf(":");
593 
594             if (colonIndex == -1) {
595                 // Check for a Windows SMB file path.
596                 colonIndex = fileName.indexOf("////");
597             }
598 
599             int backslashIndex = fileName.lastIndexOf("//");
600 
601             if ((colonIndex > -1) && (backslashIndex > -1)) {
602                 // Consider this filename to be a full Windows path, and parse it
603                 // accordingly to retrieve just the base file name.
604                 fileName = fileName.substring(backslashIndex + 1);
605             }
606 
607             return fileName;
608         }
609 
610         /***
611          * <p> Returns the (client-side) file name for this file. </p>
612          *
613          * @return The client-size file name.
614          */
615         public String toString() {
616             return getFileName();
617         }
618     }
619 }