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 us to .
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 plase 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         * @todo Consider making this method throw UnsupportedEncodingException.
385         */
386        public String getString() {
387            byte[] rawdata = get();
388            String charset = getCharSet();
389            if (charset == null) {
390                charset = DEFAULT_CHARSET;
391            }
392            try {
393                return new String(rawdata, charset);
394            } catch (UnsupportedEncodingException e) {
395                return new String(rawdata);
396            }
397        }
398    
399    
400        /**
401         * A convenience method to write an uploaded item to disk. The client code
402         * is not concerned with whether or not the item is stored in memory, or on
403         * disk in a temporary location. They just want to write the uploaded item
404         * to a file.
405         * <p>
406         * This implementation first attempts to rename the uploaded item to the
407         * specified destination file, if the item was originally written to disk.
408         * Otherwise, the data will be copied to the specified file.
409         * <p>
410         * This method is only guaranteed to work <em>once</em>, the first time it
411         * is invoked for a particular item. This is because, in the event that the
412         * method renames a temporary file, that file will no longer be available
413         * to copy or rename again at a later time.
414         *
415         * @param file The <code>File</code> into which the uploaded item should
416         *             be stored.
417         *
418         * @throws Exception if an error occurs.
419         */
420        public void write(File file) throws Exception {
421          // PAG: we just can't write files
422          throw new FileUploadException(
423           "Cannot write uploaded file to disk!");
424        }
425    
426    
427        /**
428         * Deletes the underlying storage for a file item, including deleting any
429         * associated temporary disk file. Although this storage will be deleted
430         * automatically when the <code>FileItem</code> instance is garbage
431         * collected, this method can be used to ensure that this is done at an
432         * earlier time, thus preserving system resources.
433         */
434        @edu.umd.cs.findbugs.annotations.SuppressWarnings(value="RV_RETURN_VALUE_IGNORED_BAD_PRACTICE", justification="Do not want to modify code we have just pasted")
435        public void delete() {
436            cachedContent = null;
437            File outputFile = getStoreLocation();
438            if (outputFile != null && outputFile.exists()) {
439                outputFile.delete();
440            }
441        }
442    
443    
444        /**
445         * Returns the name of the field in the multipart form corresponding to
446         * this file item.
447         *
448         * @return The name of the form field.
449         *
450         * @see #setFieldName(java.lang.String)
451         *
452         */
453        public String getFieldName() {
454            return fieldName;
455        }
456    
457    
458        /**
459         * Sets the field name used to reference this file item.
460         *
461         * @param fieldName The name of the form field.
462         *
463         * @see #getFieldName()
464         *
465         */
466        public void setFieldName(String fieldName) {
467            this.fieldName = fieldName;
468        }
469    
470    
471        /**
472         * Determines whether or not a <code>FileItem</code> instance represents
473         * a simple form field.
474         *
475         * @return <code>true</code> if the instance represents a simple form
476         *         field; <code>false</code> if it represents an uploaded file.
477         *
478         * @see #setFormField(boolean)
479         *
480         */
481        public boolean isFormField() {
482            return isFormField;
483        }
484    
485    
486        /**
487         * Specifies whether or not a <code>FileItem</code> instance represents
488         * a simple form field.
489         *
490         * @param state <code>true</code> if the instance represents a simple form
491         *              field; <code>false</code> if it represents an uploaded file.
492         *
493         * @see #isFormField()
494         *
495         */
496        public void setFormField(boolean state) {
497            isFormField = state;
498        }
499    
500    
501        /**
502         * Returns an {@link java.io.OutputStream OutputStream} that can
503         * be used for storing the contents of the file.
504         *
505         * @return An {@link java.io.OutputStream OutputStream} that can be used
506         *         for storing the contensts of the file.
507         *
508         * @throws IOException if an error occurs.
509         */
510        public OutputStream getOutputStream()
511            throws IOException {      
512            if (dfos == null) {
513                File outputFile = getTempFile();
514                dfos = new DeferredFileOutputStream(sizeThreshold, outputFile);
515            }
516            return dfos;
517        }
518    
519    
520        // --------------------------------------------------------- Public methods
521    
522    
523        /**
524         * Returns the {@link java.io.File} object for the <code>FileItem</code>'s
525         * data's temporary location on the disk. Note that for
526         * <code>FileItem</code>s that have their data stored in memory,
527         * this method will return <code>null</code>. When handling large
528         * files, you can use {@link java.io.File#renameTo(java.io.File)} to
529         * move the file to new location without copying the data, if the
530         * source and destination locations reside within the same logical
531         * volume.
532         *
533         * @return The data file, or <code>null</code> if the data is stored in
534         *         memory.
535         */
536        public File getStoreLocation() {
537            return dfos.getFile();
538        }
539    
540    
541        // ------------------------------------------------------ Protected methods
542    
543    
544        /**
545         * Removes the file contents from the temporary storage.
546         */
547        @edu.umd.cs.findbugs.annotations.SuppressWarnings(value="RV_RETURN_VALUE_IGNORED_BAD_PRACTICE", justification="Do not want to modify code we have just pasted")
548        @Override
549        protected void finalize() {
550            File outputFile = dfos.getFile();
551    
552            if (outputFile != null && outputFile.exists()) {
553                outputFile.delete();
554            }
555        }
556    
557    
558        /**
559         * Creates and returns a {@link java.io.File File} representing a uniquely
560         * named temporary file in the configured repository path. The lifetime of
561         * the file is tied to the lifetime of the <code>FileItem</code> instance;
562         * the file will be deleted when the instance is garbage collected.
563         *
564         * @return The {@link java.io.File File} to be used for temporary storage.
565         */
566        protected File getTempFile() {
567            File tempDir = repository;
568            if (tempDir == null) {
569                tempDir = new File(System.getProperty("java.io.tmpdir"));
570            }
571    
572            String fileName = "upload_" + UID + "_" + getUniqueId() + ".tmp";
573    
574            File f = new File(tempDir, fileName);
575            // PAG: we just can't create new threads, which FileCleaner.track does!
576            // FileCleaner.track(f, this);
577            return f;
578        }
579    
580    
581        // -------------------------------------------------------- Private methods
582    
583    
584        /**
585         * Returns an identifier that is unique within the class loader used to
586         * load this class, but does not have random-like apearance.
587         *
588         * @return A String with the non-random looking instance identifier.
589         */
590        private static String getUniqueId() {
591            final int limit = 100000000;
592            int current;
593            synchronized (DiskFileItem2.class) {
594                current = counter++;
595            }
596            String id = Integer.toString(current);
597    
598            // If you manage to get more than 100 million of ids, you'll
599            // start getting ids longer than 8 characters.
600            if (current < limit) {
601                id = ("00000000" + id).substring(id.length());
602            }
603            return id;
604        }
605    
606    
607    
608    
609        /**
610         * Returns a string representation of this object.
611         *
612         * @return a string representation of this object.
613         */
614        @Override
615        public String toString() {
616            return "name=" + this.getName()
617                + ", StoreLocation="
618                + String.valueOf(this.getStoreLocation())
619                + ", size="
620                + this.getSize()
621                + "bytes, "
622                + "isFormField=" + isFormField()
623                + ", FieldName="
624                + this.getFieldName();
625        }
626    
627    
628        // -------------------------------------------------- Serialization methods
629    
630    
631        /**
632         * Writes the state of this object during serialization.
633         *
634         * @param out The stream to which the state should be written.
635         *
636         * @throws IOException if an error occurs.
637         */
638        private void writeObject(ObjectOutputStream out) throws IOException {
639            // Read the data
640            if (dfos.isInMemory()) {
641                cachedContent = get();
642            } else {
643                cachedContent = null;
644                dfosFile = dfos.getFile();
645            }
646    
647            // write out values
648            out.defaultWriteObject();
649        }
650    
651        /**
652         * Reads the state of this object during deserialization.
653         *
654         * @param in The stream from which the state should be read.
655         *
656         * @throws IOException if an error occurs.
657         * @throws ClassNotFoundException if class cannot be found.
658         */
659        @edu.umd.cs.findbugs.annotations.SuppressWarnings(value="RV_RETURN_VALUE_IGNORED_BAD_PRACTICE", justification="Do not want to modify code we have just pasted")
660        private void readObject(ObjectInputStream in)
661                throws IOException, ClassNotFoundException {
662            // read values
663            in.defaultReadObject();
664    
665            OutputStream output = getOutputStream();
666            if (cachedContent != null) {
667                output.write(cachedContent);
668            } else {
669                FileInputStream input = new FileInputStream(dfosFile);
670    
671                IOUtils.copy(input, output);
672                dfosFile.delete();
673                dfosFile = null;
674            }
675            output.close();
676    
677            cachedContent = null;
678        }
679    
680    }