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.disk;
18  
19  import java.io.BufferedInputStream;
20  import java.io.BufferedOutputStream;
21  import java.io.ByteArrayInputStream;
22  import java.io.File;
23  import java.io.FileInputStream;
24  import java.io.FileOutputStream;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.OutputStream;
28  import java.io.ObjectOutputStream;
29  import java.io.ObjectInputStream;
30  import java.io.UnsupportedEncodingException;
31  import java.util.Map;
32  import org.apache.commons.io.IOUtils;
33  import org.apache.commons.io.FileCleaner;
34  import org.apache.commons.io.output.DeferredFileOutputStream;
35  
36  import org.apache.commons.fileupload.FileItem;
37  import org.apache.commons.fileupload.FileUploadException;
38  import org.apache.commons.fileupload.ParameterParser;
39  
40  
41  /**
42   * <p> The default implementation of the
43   * {@link org.apache.commons.fileupload.FileItem FileItem} interface.
44   *
45   * <p> After retrieving an instance of this class from a {@link
46   * org.apache.commons.fileupload.DiskFileUpload DiskFileUpload} instance (see
47   * {@link org.apache.commons.fileupload.DiskFileUpload
48   * #parseRequest(javax.servlet.http.HttpServletRequest)}), you may
49   * either request all contents of file at once using {@link #get()} or
50   * request an {@link java.io.InputStream InputStream} with
51   * {@link #getInputStream()} and process the file without attempting to load
52   * it into memory, which may come handy with large files.
53   *
54   * <p>When using the <code>DiskFileItemFactory</code>, then you should
55   * consider the following: Temporary files are automatically deleted as
56   * soon as they are no longer needed. (More precisely, when the
57   * corresponding instance of {@link java.io.File} is garbage collected.)
58   * This is done by the so-called reaper thread, which is started
59   * automatically when the class {@link FileCleaner} is loaded.
60   * It might make sense to terminate that thread, for example, if
61   * your web application ends. See the section on "Resource cleanup"
62   * in the users guide of commons-fileupload.</p>
63   *
64   * @author <a href="mailto:Rafal.Krzewski@e-point.pl">Rafal Krzewski</a>
65   * @author <a href="mailto:sean@informage.net">Sean Legassick</a>
66   * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
67   * @author <a href="mailto:jmcnally@apache.org">John McNally</a>
68   * @author <a href="mailto:martinc@apache.org">Martin Cooper</a>
69   * @author Sean C. Sullivan
70   *
71   * @since FileUpload 1.1
72   *
73   * @version $Id: DiskFileItem.java 482993 2006-12-06 09:42:01Z jochen $
74   */
75  public class DiskFileItem
76      implements FileItem {
77  
78      // ----------------------------------------------------- Manifest constants
79  
80  
81      /**
82       * Default content charset to be used when no explicit charset
83       * parameter is provided by the sender. Media subtypes of the
84       * "text" type are defined to have a default charset value of
85       * "ISO-8859-1" when received via HTTP.
86       */
87      public static final String DEFAULT_CHARSET = "ISO-8859-1";
88  
89  
90      // ----------------------------------------------------------- Data members
91  
92  
93      /**
94       * UID used in unique file name generation.
95       */
96      private static final String UID =
97              new java.rmi.server.UID().toString()
98                  .replace(':', '_').replace('-', '_');
99  
100     /**
101      * Counter used in unique identifier generation.
102      */
103     private static int counter = 0;
104 
105 
106     /**
107      * The name of the form field as provided by the browser.
108      */
109     private String fieldName;
110 
111 
112     /**
113      * The content type passed by the browser, or <code>null</code> if
114      * not defined.
115      */
116     private String contentType;
117 
118 
119     /**
120      * Whether or not this item is a simple form field.
121      */
122     private boolean isFormField;
123 
124 
125     /**
126      * The original filename in the user's filesystem.
127      */
128     private String fileName;
129 
130 
131     /**
132      * The size of the item, in bytes. This is used to cache the size when a
133      * file item is moved from its original location.
134      */
135     private long size = -1;
136 
137 
138     /**
139      * The threshold above which uploads will be stored on disk.
140      */
141     private int sizeThreshold;
142 
143 
144     /**
145      * The directory in which uploaded files will be stored, if stored on disk.
146      */
147     private File repository;
148 
149 
150     /**
151      * Cached contents of the file.
152      */
153     private byte[] cachedContent;
154 
155 
156     /**
157      * Output stream for this item.
158      */
159     private transient DeferredFileOutputStream dfos;
160 
161     /**
162      * File to allow for serialization of the content of this item.
163      */
164     private File dfosFile;
165 
166 
167     // ----------------------------------------------------------- Constructors
168 
169 
170     /**
171      * Constructs a new <code>DiskFileItem</code> instance.
172      *
173      * @param fieldName     The name of the form field.
174      * @param contentType   The content type passed by the browser or
175      *                      <code>null</code> if not specified.
176      * @param isFormField   Whether or not this item is a plain form field, as
177      *                      opposed to a file upload.
178      * @param fileName      The original filename in the user's filesystem, or
179      *                      <code>null</code> if not specified.
180      * @param sizeThreshold The threshold, in bytes, below which items will be
181      *                      retained in memory and above which they will be
182      *                      stored as a file.
183      * @param repository    The data repository, which is the directory in
184      *                      which files will be created, should the item size
185      *                      exceed the threshold.
186      */
187     public DiskFileItem(String fieldName, String contentType,
188             boolean isFormField, String fileName, int sizeThreshold,
189             File repository) {
190         this.fieldName = fieldName;
191         this.contentType = contentType;
192         this.isFormField = isFormField;
193         this.fileName = fileName;
194         this.sizeThreshold = sizeThreshold;
195         this.repository = repository;
196     }
197 
198 
199     // ------------------------------- Methods from javax.activation.DataSource
200 
201 
202     /**
203      * Returns an {@link java.io.InputStream InputStream} that can be
204      * used to retrieve the contents of the file.
205      *
206      * @return An {@link java.io.InputStream InputStream} that can be
207      *         used to retrieve the contents of the file.
208      *
209      * @throws IOException if an error occurs.
210      */
211     public InputStream getInputStream()
212         throws IOException {
213         if (!isInMemory()) {
214             return new FileInputStream(dfos.getFile());
215         }
216 
217         if (cachedContent == null) {
218             cachedContent = dfos.getData();
219         }
220         return new ByteArrayInputStream(cachedContent);
221     }
222 
223 
224     /**
225      * Returns the content type passed by the agent or <code>null</code> if
226      * not defined.
227      *
228      * @return The content type passed by the agent or <code>null</code> if
229      *         not defined.
230      */
231     public String getContentType() {
232         return contentType;
233     }
234 
235 
236     /**
237      * Returns the content charset passed by the agent or <code>null</code> if
238      * not defined.
239      *
240      * @return The content charset passed by the agent or <code>null</code> if
241      *         not defined.
242      */
243     public String getCharSet() {
244         ParameterParser parser = new ParameterParser();
245         parser.setLowerCaseNames(true);
246         // Parameter parser can handle null input
247         Map params = parser.parse(getContentType(), ';');
248         return (String) params.get("charset");
249     }
250 
251 
252     /**
253      * Returns the original filename in the client's filesystem.
254      *
255      * @return The original filename in the client's filesystem.
256      */
257     public String getName() {
258         return fileName;
259     }
260 
261 
262     // ------------------------------------------------------- FileItem methods
263 
264 
265     /**
266      * Provides a hint as to whether or not the file contents will be read
267      * from memory.
268      *
269      * @return <code>true</code> if the file contents will be read
270      *         from memory; <code>false</code> otherwise.
271      */
272     public boolean isInMemory() {
273         if (cachedContent != null) {
274             return true;
275         }
276         return dfos.isInMemory();
277     }
278 
279 
280     /**
281      * Returns the size of the file.
282      *
283      * @return The size of the file, in bytes.
284      */
285     public long getSize() {
286         if (size >= 0) {
287             return size;
288         } else if (cachedContent != null) {
289             return cachedContent.length;
290         } else if (dfos.isInMemory()) {
291             return dfos.getData().length;
292         } else {
293             return dfos.getFile().length();
294         }
295     }
296 
297 
298     /**
299      * Returns the contents of the file as an array of bytes.  If the
300      * contents of the file were not yet cached in memory, they will be
301      * loaded from the disk storage and cached.
302      *
303      * @return The contents of the file as an array of bytes.
304      */
305     public byte[] get() {
306         if (isInMemory()) {
307             if (cachedContent == null) {
308                 cachedContent = dfos.getData();
309             }
310             return cachedContent;
311         }
312 
313         byte[] fileData = new byte[(int) getSize()];
314         FileInputStream fis = null;
315 
316         try {
317             fis = new FileInputStream(dfos.getFile());
318             fis.read(fileData);
319         } catch (IOException e) {
320             fileData = null;
321         } finally {
322             if (fis != null) {
323                 try {
324                     fis.close();
325                 } catch (IOException e) {
326                     // ignore
327                 }
328             }
329         }
330 
331         return fileData;
332     }
333 
334 
335     /**
336      * Returns the contents of the file as a String, using the specified
337      * encoding.  This method uses {@link #get()} to retrieve the
338      * contents of the file.
339      *
340      * @param charset The charset to use.
341      *
342      * @return The contents of the file, as a string.
343      *
344      * @throws UnsupportedEncodingException if the requested character
345      *                                      encoding is not available.
346      */
347     public String getString(final String charset)
348         throws UnsupportedEncodingException {
349         return new String(get(), charset);
350     }
351 
352 
353     /**
354      * Returns the contents of the file as a String, using the default
355      * character encoding.  This method uses {@link #get()} to retrieve the
356      * contents of the file.
357      *
358      * @return The contents of the file, as a string.
359      *
360      * @todo Consider making this method throw UnsupportedEncodingException.
361      */
362     public String getString() {
363         byte[] rawdata = get();
364         String charset = getCharSet();
365         if (charset == null) {
366             charset = DEFAULT_CHARSET;
367         }
368         try {
369             return new String(rawdata, charset);
370         } catch (UnsupportedEncodingException e) {
371             return new String(rawdata);
372         }
373     }
374 
375 
376     /**
377      * A convenience method to write an uploaded item to disk. The client code
378      * is not concerned with whether or not the item is stored in memory, or on
379      * disk in a temporary location. They just want to write the uploaded item
380      * to a file.
381      * <p>
382      * This implementation first attempts to rename the uploaded item to the
383      * specified destination file, if the item was originally written to disk.
384      * Otherwise, the data will be copied to the specified file.
385      * <p>
386      * This method is only guaranteed to work <em>once</em>, the first time it
387      * is invoked for a particular item. This is because, in the event that the
388      * method renames a temporary file, that file will no longer be available
389      * to copy or rename again at a later time.
390      *
391      * @param file The <code>File</code> into which the uploaded item should
392      *             be stored.
393      *
394      * @throws Exception if an error occurs.
395      */
396     public void write(File file) throws Exception {
397         if (isInMemory()) {
398             FileOutputStream fout = null;
399             try {
400                 fout = new FileOutputStream(file);
401                 fout.write(get());
402             } finally {
403                 if (fout != null) {
404                     fout.close();
405                 }
406             }
407         } else {
408             File outputFile = getStoreLocation();
409             if (outputFile != null) {
410                 // Save the length of the file
411                 size = outputFile.length();
412                 /*
413                  * The uploaded file is being stored on disk
414                  * in a temporary location so move it to the
415                  * desired file.
416                  */
417                 if (!outputFile.renameTo(file)) {
418                     BufferedInputStream in = null;
419                     BufferedOutputStream out = null;
420                     try {
421                         in = new BufferedInputStream(
422                             new FileInputStream(outputFile));
423                         out = new BufferedOutputStream(
424                                 new FileOutputStream(file));
425                         IOUtils.copy(in, out);
426                     } finally {
427                         if (in != null) {
428                             try {
429                                 in.close();
430                             } catch (IOException e) {
431                                 // ignore
432                             }
433                         }
434                         if (out != null) {
435                             try {
436                                 out.close();
437                             } catch (IOException e) {
438                                 // ignore
439                             }
440                         }
441                     }
442                 }
443             } else {
444                 /*
445                  * For whatever reason we cannot write the
446                  * file to disk.
447                  */
448                 throw new FileUploadException(
449                     "Cannot write uploaded file to disk!");
450             }
451         }
452     }
453 
454 
455     /**
456      * Deletes the underlying storage for a file item, including deleting any
457      * associated temporary disk file. Although this storage will be deleted
458      * automatically when the <code>FileItem</code> instance is garbage
459      * collected, this method can be used to ensure that this is done at an
460      * earlier time, thus preserving system resources.
461      */
462     public void delete() {
463         cachedContent = null;
464         File outputFile = getStoreLocation();
465         if (outputFile != null && outputFile.exists()) {
466             outputFile.delete();
467         }
468     }
469 
470 
471     /**
472      * Returns the name of the field in the multipart form corresponding to
473      * this file item.
474      *
475      * @return The name of the form field.
476      *
477      * @see #setFieldName(java.lang.String)
478      *
479      */
480     public String getFieldName() {
481         return fieldName;
482     }
483 
484 
485     /**
486      * Sets the field name used to reference this file item.
487      *
488      * @param fieldName The name of the form field.
489      *
490      * @see #getFieldName()
491      *
492      */
493     public void setFieldName(String fieldName) {
494         this.fieldName = fieldName;
495     }
496 
497 
498     /**
499      * Determines whether or not a <code>FileItem</code> instance represents
500      * a simple form field.
501      *
502      * @return <code>true</code> if the instance represents a simple form
503      *         field; <code>false</code> if it represents an uploaded file.
504      *
505      * @see #setFormField(boolean)
506      *
507      */
508     public boolean isFormField() {
509         return isFormField;
510     }
511 
512 
513     /**
514      * Specifies whether or not a <code>FileItem</code> instance represents
515      * a simple form field.
516      *
517      * @param state <code>true</code> if the instance represents a simple form
518      *              field; <code>false</code> if it represents an uploaded file.
519      *
520      * @see #isFormField()
521      *
522      */
523     public void setFormField(boolean state) {
524         isFormField = state;
525     }
526 
527 
528     /**
529      * Returns an {@link java.io.OutputStream OutputStream} that can
530      * be used for storing the contents of the file.
531      *
532      * @return An {@link java.io.OutputStream OutputStream} that can be used
533      *         for storing the contensts of the file.
534      *
535      * @throws IOException if an error occurs.
536      */
537     public OutputStream getOutputStream()
538         throws IOException {
539         if (dfos == null) {
540             File outputFile = getTempFile();
541             dfos = new DeferredFileOutputStream(sizeThreshold, outputFile);
542         }
543         return dfos;
544     }
545 
546 
547     // --------------------------------------------------------- Public methods
548 
549 
550     /**
551      * Returns the {@link java.io.File} object for the <code>FileItem</code>'s
552      * data's temporary location on the disk. Note that for
553      * <code>FileItem</code>s that have their data stored in memory,
554      * this method will return <code>null</code>. When handling large
555      * files, you can use {@link java.io.File#renameTo(java.io.File)} to
556      * move the file to new location without copying the data, if the
557      * source and destination locations reside within the same logical
558      * volume.
559      *
560      * @return The data file, or <code>null</code> if the data is stored in
561      *         memory.
562      */
563     public File getStoreLocation() {
564         return dfos.getFile();
565     }
566 
567 
568     // ------------------------------------------------------ Protected methods
569 
570 
571     /**
572      * Removes the file contents from the temporary storage.
573      */
574     protected void finalize() {
575         File outputFile = dfos.getFile();
576 
577         if (outputFile != null && outputFile.exists()) {
578             outputFile.delete();
579         }
580     }
581 
582 
583     /**
584      * Creates and returns a {@link java.io.File File} representing a uniquely
585      * named temporary file in the configured repository path. The lifetime of
586      * the file is tied to the lifetime of the <code>FileItem</code> instance;
587      * the file will be deleted when the instance is garbage collected.
588      *
589      * @return The {@link java.io.File File} to be used for temporary storage.
590      */
591     protected File getTempFile() {
592         File tempDir = repository;
593         if (tempDir == null) {
594             tempDir = new File(System.getProperty("java.io.tmpdir"));
595         }
596 
597         String tempFileName = "upload_" + UID + "_" + getUniqueId() + ".tmp";
598 
599         File f = new File(tempDir, tempFileName);
600         FileCleaner.track(f, this);
601         return f;
602     }
603 
604 
605     // -------------------------------------------------------- Private methods
606 
607 
608     /**
609      * Returns an identifier that is unique within the class loader used to
610      * load this class, but does not have random-like apearance.
611      *
612      * @return A String with the non-random looking instance identifier.
613      */
614     private static String getUniqueId() {
615         final int limit = 100000000;
616         int current;
617         synchronized (DiskFileItem.class) {
618             current = counter++;
619         }
620         String id = Integer.toString(current);
621 
622         // If you manage to get more than 100 million of ids, you'll
623         // start getting ids longer than 8 characters.
624         if (current < limit) {
625             id = ("00000000" + id).substring(id.length());
626         }
627         return id;
628     }
629 
630 
631 
632 
633     /**
634      * Returns a string representation of this object.
635      *
636      * @return a string representation of this object.
637      */
638     public String toString() {
639         return "name=" + this.getName()
640             + ", StoreLocation="
641             + String.valueOf(this.getStoreLocation())
642             + ", size="
643             + this.getSize()
644             + "bytes, "
645             + "isFormField=" + isFormField()
646             + ", FieldName="
647             + this.getFieldName();
648     }
649 
650 
651     // -------------------------------------------------- Serialization methods
652 
653 
654     /**
655      * Writes the state of this object during serialization.
656      *
657      * @param out The stream to which the state should be written.
658      *
659      * @throws IOException if an error occurs.
660      */
661     private void writeObject(ObjectOutputStream out) throws IOException {
662         // Read the data
663         if (dfos.isInMemory()) {
664             cachedContent = get();
665         } else {
666             cachedContent = null;
667             dfosFile = dfos.getFile();
668         }
669 
670         // write out values
671         out.defaultWriteObject();
672     }
673 
674     /**
675      * Reads the state of this object during deserialization.
676      *
677      * @param in The stream from which the state should be read.
678      *
679      * @throws IOException if an error occurs.
680      * @throws ClassNotFoundException if class cannot be found.
681      */
682     private void readObject(ObjectInputStream in)
683             throws IOException, ClassNotFoundException {
684         // read values
685         in.defaultReadObject();
686 
687         OutputStream output = getOutputStream();
688         if (cachedContent != null) {
689             output.write(cachedContent);
690         } else {
691             FileInputStream input = new FileInputStream(dfosFile);
692 
693             IOUtils.copy(input, output);
694             dfosFile.delete();
695             dfosFile = null;
696         }
697         output.close();
698 
699         cachedContent = null;
700     }
701 
702 }