001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.commons.fileupload.disk;
018:
019: import java.io.BufferedInputStream;
020: import java.io.BufferedOutputStream;
021: import java.io.ByteArrayInputStream;
022: import java.io.File;
023: import java.io.FileInputStream;
024: import java.io.FileOutputStream;
025: import java.io.IOException;
026: import java.io.InputStream;
027: import java.io.OutputStream;
028: import java.io.ObjectOutputStream;
029: import java.io.ObjectInputStream;
030: import java.io.UnsupportedEncodingException;
031: import java.util.Map;
032: import org.apache.commons.io.IOUtils;
033: import org.apache.commons.io.FileCleaner;
034: import org.apache.commons.io.output.DeferredFileOutputStream;
035:
036: import org.apache.commons.fileupload.FileItem;
037: import org.apache.commons.fileupload.FileUploadException;
038: import org.apache.commons.fileupload.ParameterParser;
039:
040: /**
041: * <p> The default implementation of the
042: * {@link org.apache.commons.fileupload.FileItem FileItem} interface.
043: *
044: * <p> After retrieving an instance of this class from a {@link
045: * org.apache.commons.fileupload.DiskFileUpload DiskFileUpload} instance (see
046: * {@link org.apache.commons.fileupload.DiskFileUpload
047: * #parseRequest(javax.servlet.http.HttpServletRequest)}), you may
048: * either request all contents of file at once using {@link #get()} or
049: * request an {@link java.io.InputStream InputStream} with
050: * {@link #getInputStream()} and process the file without attempting to load
051: * it into memory, which may come handy with large files.
052: *
053: * <p>When using the <code>DiskFileItemFactory</code>, then you should
054: * consider the following: Temporary files are automatically deleted as
055: * soon as they are no longer needed. (More precisely, when the
056: * corresponding instance of {@link java.io.File} is garbage collected.)
057: * This is done by the so-called reaper thread, which is started
058: * automatically when the class {@link FileCleaner} is loaded.
059: * It might make sense to terminate that thread, for example, if
060: * your web application ends. See the section on "Resource cleanup"
061: * in the users guide of commons-fileupload.</p>
062: *
063: * @author <a href="mailto:Rafal.Krzewski@e-point.pl">Rafal Krzewski</a>
064: * @author <a href="mailto:sean@informage.net">Sean Legassick</a>
065: * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
066: * @author <a href="mailto:jmcnally@apache.org">John McNally</a>
067: * @author <a href="mailto:martinc@apache.org">Martin Cooper</a>
068: * @author Sean C. Sullivan
069: *
070: * @since FileUpload 1.1
071: *
072: * @version $Id: DiskFileItem.java 482993 2006-12-06 09:42:01Z jochen $
073: */
074: public class DiskFileItem implements FileItem {
075:
076: // ----------------------------------------------------- Manifest constants
077:
078: /**
079: * Default content charset to be used when no explicit charset
080: * parameter is provided by the sender. Media subtypes of the
081: * "text" type are defined to have a default charset value of
082: * "ISO-8859-1" when received via HTTP.
083: */
084: public static final String DEFAULT_CHARSET = "ISO-8859-1";
085:
086: // ----------------------------------------------------------- Data members
087:
088: /**
089: * UID used in unique file name generation.
090: */
091: private static final String UID = new java.rmi.server.UID()
092: .toString().replace(':', '_').replace('-', '_');
093:
094: /**
095: * Counter used in unique identifier generation.
096: */
097: private static int counter = 0;
098:
099: /**
100: * The name of the form field as provided by the browser.
101: */
102: private String fieldName;
103:
104: /**
105: * The content type passed by the browser, or <code>null</code> if
106: * not defined.
107: */
108: private String contentType;
109:
110: /**
111: * Whether or not this item is a simple form field.
112: */
113: private boolean isFormField;
114:
115: /**
116: * The original filename in the user's filesystem.
117: */
118: private String fileName;
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: * The threshold above which uploads will be stored on disk.
128: */
129: private int sizeThreshold;
130:
131: /**
132: * The directory in which uploaded files will be stored, if stored on disk.
133: */
134: private File repository;
135:
136: /**
137: * Cached contents of the file.
138: */
139: private byte[] cachedContent;
140:
141: /**
142: * Output stream for this item.
143: */
144: private transient DeferredFileOutputStream dfos;
145:
146: /**
147: * File to allow for serialization of the content of this item.
148: */
149: private File dfosFile;
150:
151: // ----------------------------------------------------------- Constructors
152:
153: /**
154: * Constructs a new <code>DiskFileItem</code> instance.
155: *
156: * @param fieldName The name of the form field.
157: * @param contentType The content type passed by the browser or
158: * <code>null</code> if not specified.
159: * @param isFormField Whether or not this item is a plain form field, as
160: * opposed to a file upload.
161: * @param fileName The original filename in the user's filesystem, or
162: * <code>null</code> if not specified.
163: * @param sizeThreshold The threshold, in bytes, below which items will be
164: * retained in memory and above which they will be
165: * stored as a file.
166: * @param repository The data repository, which is the directory in
167: * which files will be created, should the item size
168: * exceed the threshold.
169: */
170: public DiskFileItem(String fieldName, String contentType,
171: boolean isFormField, String fileName, int sizeThreshold,
172: File repository) {
173: this .fieldName = fieldName;
174: this .contentType = contentType;
175: this .isFormField = isFormField;
176: this .fileName = fileName;
177: this .sizeThreshold = sizeThreshold;
178: this .repository = repository;
179: }
180:
181: // ------------------------------- Methods from javax.activation.DataSource
182:
183: /**
184: * Returns an {@link java.io.InputStream InputStream} that can be
185: * used to retrieve the contents of the file.
186: *
187: * @return An {@link java.io.InputStream InputStream} that can be
188: * used to retrieve the contents of the file.
189: *
190: * @throws IOException if an error occurs.
191: */
192: public InputStream getInputStream() throws IOException {
193: if (!isInMemory()) {
194: return new FileInputStream(dfos.getFile());
195: }
196:
197: if (cachedContent == null) {
198: cachedContent = dfos.getData();
199: }
200: return new ByteArrayInputStream(cachedContent);
201: }
202:
203: /**
204: * Returns the content type passed by the agent or <code>null</code> if
205: * not defined.
206: *
207: * @return The content type passed by the agent or <code>null</code> if
208: * not defined.
209: */
210: public String getContentType() {
211: return contentType;
212: }
213:
214: /**
215: * Returns the content charset passed by the agent or <code>null</code> if
216: * not defined.
217: *
218: * @return The content charset passed by the agent or <code>null</code> if
219: * not defined.
220: */
221: public String getCharSet() {
222: ParameterParser parser = new ParameterParser();
223: parser.setLowerCaseNames(true);
224: // Parameter parser can handle null input
225: Map params = parser.parse(getContentType(), ';');
226: return (String) params.get("charset");
227: }
228:
229: /**
230: * Returns the original filename in the client's filesystem.
231: *
232: * @return The original filename in the client's filesystem.
233: */
234: public String getName() {
235: return fileName;
236: }
237:
238: // ------------------------------------------------------- FileItem methods
239:
240: /**
241: * Provides a hint as to whether or not the file contents will be read
242: * from memory.
243: *
244: * @return <code>true</code> if the file contents will be read
245: * from memory; <code>false</code> otherwise.
246: */
247: public boolean isInMemory() {
248: if (cachedContent != null) {
249: return true;
250: }
251: return dfos.isInMemory();
252: }
253:
254: /**
255: * Returns the size of the file.
256: *
257: * @return The size of the file, in bytes.
258: */
259: public long getSize() {
260: if (size >= 0) {
261: return size;
262: } else if (cachedContent != null) {
263: return cachedContent.length;
264: } else if (dfos.isInMemory()) {
265: return dfos.getData().length;
266: } else {
267: return dfos.getFile().length();
268: }
269: }
270:
271: /**
272: * Returns the contents of the file as an array of bytes. If the
273: * contents of the file were not yet cached in memory, they will be
274: * loaded from the disk storage and cached.
275: *
276: * @return The contents of the file as an array of bytes.
277: */
278: public byte[] get() {
279: if (isInMemory()) {
280: if (cachedContent == null) {
281: cachedContent = dfos.getData();
282: }
283: return cachedContent;
284: }
285:
286: byte[] fileData = new byte[(int) getSize()];
287: FileInputStream fis = null;
288:
289: try {
290: fis = new FileInputStream(dfos.getFile());
291: fis.read(fileData);
292: } catch (IOException e) {
293: fileData = null;
294: } finally {
295: if (fis != null) {
296: try {
297: fis.close();
298: } catch (IOException e) {
299: // ignore
300: }
301: }
302: }
303:
304: return fileData;
305: }
306:
307: /**
308: * Returns the contents of the file as a String, using the specified
309: * encoding. This method uses {@link #get()} to retrieve the
310: * contents of the file.
311: *
312: * @param charset The charset to use.
313: *
314: * @return The contents of the file, as a string.
315: *
316: * @throws UnsupportedEncodingException if the requested character
317: * encoding is not available.
318: */
319: public String getString(final String charset)
320: throws UnsupportedEncodingException {
321: return new String(get(), charset);
322: }
323:
324: /**
325: * Returns the contents of the file as a String, using the default
326: * character encoding. This method uses {@link #get()} to retrieve the
327: * contents of the file.
328: *
329: * @return The contents of the file, as a string.
330: *
331: * @todo Consider making this method throw UnsupportedEncodingException.
332: */
333: public String getString() {
334: byte[] rawdata = get();
335: String charset = getCharSet();
336: if (charset == null) {
337: charset = DEFAULT_CHARSET;
338: }
339: try {
340: return new String(rawdata, charset);
341: } catch (UnsupportedEncodingException e) {
342: return new String(rawdata);
343: }
344: }
345:
346: /**
347: * A convenience method to write an uploaded item to disk. The client code
348: * is not concerned with whether or not the item is stored in memory, or on
349: * disk in a temporary location. They just want to write the uploaded item
350: * to a file.
351: * <p>
352: * This implementation first attempts to rename the uploaded item to the
353: * specified destination file, if the item was originally written to disk.
354: * Otherwise, the data will be copied to the specified file.
355: * <p>
356: * This method is only guaranteed to work <em>once</em>, the first time it
357: * is invoked for a particular item. This is because, in the event that the
358: * method renames a temporary file, that file will no longer be available
359: * to copy or rename again at a later time.
360: *
361: * @param file The <code>File</code> into which the uploaded item should
362: * be stored.
363: *
364: * @throws Exception if an error occurs.
365: */
366: public void write(File file) throws Exception {
367: if (isInMemory()) {
368: FileOutputStream fout = null;
369: try {
370: fout = new FileOutputStream(file);
371: fout.write(get());
372: } finally {
373: if (fout != null) {
374: fout.close();
375: }
376: }
377: } else {
378: File outputFile = getStoreLocation();
379: if (outputFile != null) {
380: // Save the length of the file
381: size = outputFile.length();
382: /*
383: * The uploaded file is being stored on disk
384: * in a temporary location so move it to the
385: * desired file.
386: */
387: if (!outputFile.renameTo(file)) {
388: BufferedInputStream in = null;
389: BufferedOutputStream out = null;
390: try {
391: in = new BufferedInputStream(
392: new FileInputStream(outputFile));
393: out = new BufferedOutputStream(
394: new FileOutputStream(file));
395: IOUtils.copy(in, out);
396: } finally {
397: if (in != null) {
398: try {
399: in.close();
400: } catch (IOException e) {
401: // ignore
402: }
403: }
404: if (out != null) {
405: try {
406: out.close();
407: } catch (IOException e) {
408: // ignore
409: }
410: }
411: }
412: }
413: } else {
414: /*
415: * For whatever reason we cannot write the
416: * file to disk.
417: */
418: throw new FileUploadException(
419: "Cannot write uploaded file to disk!");
420: }
421: }
422: }
423:
424: /**
425: * Deletes the underlying storage for a file item, including deleting any
426: * associated temporary disk file. Although this storage will be deleted
427: * automatically when the <code>FileItem</code> instance is garbage
428: * collected, this method can be used to ensure that this is done at an
429: * earlier time, thus preserving system resources.
430: */
431: public void delete() {
432: cachedContent = null;
433: File outputFile = getStoreLocation();
434: if (outputFile != null && outputFile.exists()) {
435: outputFile.delete();
436: }
437: }
438:
439: /**
440: * Returns the name of the field in the multipart form corresponding to
441: * this file item.
442: *
443: * @return The name of the form field.
444: *
445: * @see #setFieldName(java.lang.String)
446: *
447: */
448: public String getFieldName() {
449: return fieldName;
450: }
451:
452: /**
453: * Sets the field name used to reference this file item.
454: *
455: * @param fieldName The name of the form field.
456: *
457: * @see #getFieldName()
458: *
459: */
460: public void setFieldName(String fieldName) {
461: this .fieldName = fieldName;
462: }
463:
464: /**
465: * Determines whether or not a <code>FileItem</code> instance represents
466: * a simple form field.
467: *
468: * @return <code>true</code> if the instance represents a simple form
469: * field; <code>false</code> if it represents an uploaded file.
470: *
471: * @see #setFormField(boolean)
472: *
473: */
474: public boolean isFormField() {
475: return isFormField;
476: }
477:
478: /**
479: * Specifies whether or not a <code>FileItem</code> instance represents
480: * a simple form field.
481: *
482: * @param state <code>true</code> if the instance represents a simple form
483: * field; <code>false</code> if it represents an uploaded file.
484: *
485: * @see #isFormField()
486: *
487: */
488: public void setFormField(boolean state) {
489: isFormField = state;
490: }
491:
492: /**
493: * Returns an {@link java.io.OutputStream OutputStream} that can
494: * be used for storing the contents of the file.
495: *
496: * @return An {@link java.io.OutputStream OutputStream} that can be used
497: * for storing the contensts of the file.
498: *
499: * @throws IOException if an error occurs.
500: */
501: public OutputStream getOutputStream() throws IOException {
502: if (dfos == null) {
503: File outputFile = getTempFile();
504: dfos = new DeferredFileOutputStream(sizeThreshold,
505: outputFile);
506: }
507: return dfos;
508: }
509:
510: // --------------------------------------------------------- Public methods
511:
512: /**
513: * Returns the {@link java.io.File} object for the <code>FileItem</code>'s
514: * data's temporary location on the disk. Note that for
515: * <code>FileItem</code>s that have their data stored in memory,
516: * this method will return <code>null</code>. When handling large
517: * files, you can use {@link java.io.File#renameTo(java.io.File)} to
518: * move the file to new location without copying the data, if the
519: * source and destination locations reside within the same logical
520: * volume.
521: *
522: * @return The data file, or <code>null</code> if the data is stored in
523: * memory.
524: */
525: public File getStoreLocation() {
526: return dfos.getFile();
527: }
528:
529: // ------------------------------------------------------ Protected methods
530:
531: /**
532: * Removes the file contents from the temporary storage.
533: */
534: protected void finalize() {
535: File outputFile = dfos.getFile();
536:
537: if (outputFile != null && outputFile.exists()) {
538: outputFile.delete();
539: }
540: }
541:
542: /**
543: * Creates and returns a {@link java.io.File File} representing a uniquely
544: * named temporary file in the configured repository path. The lifetime of
545: * the file is tied to the lifetime of the <code>FileItem</code> instance;
546: * the file will be deleted when the instance is garbage collected.
547: *
548: * @return The {@link java.io.File File} to be used for temporary storage.
549: */
550: protected File getTempFile() {
551: File tempDir = repository;
552: if (tempDir == null) {
553: tempDir = new File(System.getProperty("java.io.tmpdir"));
554: }
555:
556: String tempFileName = "upload_" + UID + "_" + getUniqueId()
557: + ".tmp";
558:
559: File f = new File(tempDir, tempFileName);
560: FileCleaner.track(f, this );
561: return f;
562: }
563:
564: // -------------------------------------------------------- Private methods
565:
566: /**
567: * Returns an identifier that is unique within the class loader used to
568: * load this class, but does not have random-like apearance.
569: *
570: * @return A String with the non-random looking instance identifier.
571: */
572: private static String getUniqueId() {
573: final int limit = 100000000;
574: int current;
575: synchronized (DiskFileItem.class) {
576: current = counter++;
577: }
578: String id = Integer.toString(current);
579:
580: // If you manage to get more than 100 million of ids, you'll
581: // start getting ids longer than 8 characters.
582: if (current < limit) {
583: id = ("00000000" + id).substring(id.length());
584: }
585: return id;
586: }
587:
588: /**
589: * Returns a string representation of this object.
590: *
591: * @return a string representation of this object.
592: */
593: public String toString() {
594: return "name=" + this .getName() + ", StoreLocation="
595: + String.valueOf(this .getStoreLocation()) + ", size="
596: + this .getSize() + "bytes, " + "isFormField="
597: + isFormField() + ", FieldName=" + this .getFieldName();
598: }
599:
600: // -------------------------------------------------- Serialization methods
601:
602: /**
603: * Writes the state of this object during serialization.
604: *
605: * @param out The stream to which the state should be written.
606: *
607: * @throws IOException if an error occurs.
608: */
609: private void writeObject(ObjectOutputStream out) throws IOException {
610: // Read the data
611: if (dfos.isInMemory()) {
612: cachedContent = get();
613: } else {
614: cachedContent = null;
615: dfosFile = dfos.getFile();
616: }
617:
618: // write out values
619: out.defaultWriteObject();
620: }
621:
622: /**
623: * Reads the state of this object during deserialization.
624: *
625: * @param in The stream from which the state should be read.
626: *
627: * @throws IOException if an error occurs.
628: * @throws ClassNotFoundException if class cannot be found.
629: */
630: private void readObject(ObjectInputStream in) throws IOException,
631: ClassNotFoundException {
632: // read values
633: in.defaultReadObject();
634:
635: OutputStream output = getOutputStream();
636: if (cachedContent != null) {
637: output.write(cachedContent);
638: } else {
639: FileInputStream input = new FileInputStream(dfosFile);
640:
641: IOUtils.copy(input, output);
642: dfosFile.delete();
643: dfosFile = null;
644: }
645: output.close();
646:
647: cachedContent = null;
648: }
649:
650: }
|