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