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