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