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