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 }