1 package com.instantbank.lettertemplate.control.web.handlers; 2 3 4 import java.io.*; 5 import java.util.*; 6 import javax.servlet.*; 7 import javax.servlet.http.*; 8 9 /** 10 * A utility class to handle <tt>multipart/form-data</tt> requests, the kind of 11 * requests that support file uploads. This class can receive arbitrarily large 12 * files (up to an artificial limit you can set), and fairly efficiently too. 13 * It cannot handle nested data (multipart content within multipart content) or 14 * internationalized content (such as non Latin-1 filenames). <p> 15 * 16 * It's used like this: <blockquote><pre> 17 * MultipartRequest multi = new MultipartRequest(req, "."); 18 * 19 * out.println("Params:"); 20 * Enumeration params = multi.getParameterNames(); 21 * while (params.hasMoreElements()) { 22 * String name = (String)params.nextElement(); 23 * String value = multi.getParameter(name); 24 * out.println(name + " = " + value); 25 * } 26 * out.println(); 27 * 28 * out.println("Files:"); 29 * Enumeration files = multi.getFileNames(); 30 * while (files.hasMoreElements()) { 31 * String name = (String)files.nextElement(); 32 * String filename = multi.getFilesystemName(name); 33 * String type = multi.getContentType(name); 34 * File f = multi.getFile(name); 35 * out.println("name: " + name); 36 * out.println("filename: " + filename); 37 * out.println("type: " + type); 38 * if (f != null) { 39 * out.println("f.toString(): " + f.toString()); 40 * out.println("f.getName(): " + f.getName()); 41 * out.println("f.exists(): " + f.exists()); 42 * out.println("f.length(): " + f.length()); 43 * out.println(); 44 * } 45 * } 46 * </pre></blockquote> A client can upload files using an HTML form with the 47 * following structure. Note that not all browsers support file uploads. 48 * <blockquote><pre> 49 * <FORM ACTION="/servlet/Handler" METHOD=POST 50 * ENCTYPE="multipart/form-data"> 51 * What is your name? <INPUT TYPE=TEXT NAME=submitter> <BR> 52 * Which file to upload? <INPUT TYPE=FILE NAME=file> <BR> 53 * <INPUT TYPE=SUBMIT> 54 * </FORM> 55 * </pre></blockquote> <p> 56 * 57 * The full file upload specification is contained in experimental RFC 1867, 58 * available at <a href="http://www.ietf.org/rfc/rfc1867.txt"> 59 * http://www.ietf.org/rfc/rfc1867.txt</a> . 60 * 61 * @author <b>Jason Hunter</b> , Copyright © 1998-1999 62 * @created September 2002 63 * @version 1.6, 00/03/19, better WebSphere 2.x/3.x content type workaround 64 * @version 1.5, 00/02/04, added auto MacBinary decoding for IE on Mac 65 * @version 1.4, 00/01/05, added getParameterValues(), WebSphere 2.x 66 * getContentType() workaround, stopped writing empty "unknown" file 67 * @version 1.3, 99/12/28, IE4 on Win98 lastIndexOf("boundary=") workaround 68 * @version 1.2, 99/12/20, IE4 on Mac readNextPart() workaround 69 * @version 1.1, 99/01/15, JSDK readLine() bug workaround 70 * @version 1.0, 98/09/18 71 */ 72 public class MultipartRequest { 73 74 private static final int DEFAULT_MAX_POST_SIZE = 1024 * 1024; 75 // 1 Meg 76 private static final String NO_FILE = "unknown"; 77 78 private HttpServletRequest req; 79 private File dir; 80 private int maxSize; 81 82 private Hashtable parameters = new Hashtable(); 83 // name - Vector of values 84 private Hashtable files = new Hashtable(); 85 // name - UploadedFile 86 87 88 /** 89 * Constructs a new MultipartRequest to handle the specified request, saving 90 * any uploaded files to the given directory, and limiting the upload size to 91 * 1 Megabyte. If the content is too large, an IOException is thrown. This 92 * constructor actually parses the <tt>multipart/form-data</tt> and throws an 93 * IOException if there's any problem reading or parsing the request. 94 * 95 * @param request the servlet request 96 * @param saveDirectory the directory in which to save any uploaded files 97 * @throws IOException Description of the Exception 98 */ 99 public MultipartRequest(HttpServletRequest request, 100 String saveDirectory) throws IOException { 101 this(request, saveDirectory, DEFAULT_MAX_POST_SIZE); 102 } 103 104 105 /** 106 * Constructs a new MultipartRequest to handle the specified request, saving 107 * any uploaded files to the given directory, and limiting the upload size to 108 * the specified length. If the content is too large, an IOException is 109 * thrown. This constructor actually parses the <tt>multipart/form-data</tt> 110 * and throws an IOException if there's any problem reading or parsing the 111 * request. 112 * 113 * @param request the servlet request 114 * @param saveDirectory the directory in which to save any uploaded files 115 * @param maxPostSize the maximum size of the POST content 116 * @throws IOException Description of the Exception 117 */ 118 public MultipartRequest(HttpServletRequest request, 119 String saveDirectory, 120 int maxPostSize) throws IOException { 121 // Sanity check values 122 if(request == null) { 123 throw new IllegalArgumentException("request cannot be null"); 124 } 125 if(saveDirectory == null) { 126 throw new IllegalArgumentException("saveDirectory cannot be null"); 127 } 128 if(maxPostSize <= 0) { 129 throw new IllegalArgumentException("maxPostSize must be positive"); 130 } 131 132 // Save the request, dir, and max size 133 req = request; 134 dir = new File(saveDirectory); 135 maxSize = maxPostSize; 136 137 // Check saveDirectory is truly a directory 138 if(!dir.isDirectory()) { 139 throw new IllegalArgumentException("Not a directory: " + saveDirectory); 140 } 141 142 // Check saveDirectory is writable 143 if(!dir.canWrite()) { 144 throw new IllegalArgumentException("Not writable: " + saveDirectory); 145 } 146 147 // Now parse the request saving data to "parameters" and "files"; 148 // write the file contents to the saveDirectory 149 readRequest(); 150 } 151 152 153 /** 154 * Constructor with an old signature, kept for backward compatibility. 155 * Without this constructor, a servlet compiled against a previous version of 156 * this class (pre 1.4) would have to be recompiled to link with this 157 * version. This constructor supports the linking via the old signature. 158 * Callers must simply be careful to pass in an HttpServletRequest. 159 * 160 * @param request Description of the Parameter 161 * @param saveDirectory Description of the Parameter 162 * @throws IOException Description of the Exception 163 */ 164 public MultipartRequest(ServletRequest request, 165 String saveDirectory) throws IOException { 166 this((HttpServletRequest)request, saveDirectory); 167 } 168 169 170 /** 171 * Constructor with an old signature, kept for backward compatibility. 172 * Without this constructor, a servlet compiled against a previous version of 173 * this class (pre 1.4) would have to be recompiled to link with this 174 * version. This constructor supports the linking via the old signature. 175 * Callers must simply be careful to pass in an HttpServletRequest. 176 * 177 * @param request Description of the Parameter 178 * @param saveDirectory Description of the Parameter 179 * @param maxPostSize Description of the Parameter 180 * @throws IOException Description of the Exception 181 */ 182 public MultipartRequest(ServletRequest request, 183 String saveDirectory, 184 int maxPostSize) throws IOException { 185 this((HttpServletRequest)request, saveDirectory, maxPostSize); 186 } 187 188 189 /** 190 * Returns the names of all the parameters as an Enumeration of Strings. It 191 * returns an empty Enumeration if there are no parameters. 192 * 193 * @return the names of all the parameters as an Enumeration of Strings 194 */ 195 public Enumeration getParameterNames() { 196 return parameters.keys(); 197 } 198 199 200 /** 201 * Returns the names of all the uploaded files as an Enumeration of Strings. 202 * It returns an empty Enumeration if there are no uploaded files. Each file 203 * name is the name specified by the form, not by the user. 204 * 205 * @return the names of all the uploaded files as an Enumeration of Strings 206 */ 207 public Enumeration getFileNames() { 208 return files.keys(); 209 } 210 211 212 /** 213 * Returns the value of the named parameter as a String, or null if the 214 * parameter was not sent or was sent without a value. The value is 215 * guaranteed to be in its normal, decoded form. If the parameter has 216 * multiple values, only the last one is returned (for backward 217 * compatibility). For parameters with multiple values, it's possible the 218 * last "value" may be null. 219 * 220 * @param name the parameter name 221 * @return the parameter value 222 */ 223 public String getParameter(String name) { 224 try { 225 Vector values = (Vector)parameters.get(name); 226 if(values == null || values.size() == 0) { 227 return null; 228 } 229 String value = (String)values.elementAt(values.size() - 1); 230 return value; 231 } 232 catch(Exception e) { 233 return null; 234 } 235 } 236 237 238 /** 239 * Returns the values of the named parameter as a String array, or null if 240 * the parameter was not sent. The array has one entry for each parameter 241 * field sent. If any field was sent without a value that entry is stored in 242 * the array as a null. The values are guaranteed to be in their normal, 243 * decoded form. A single value is returned as a one-element array. 244 * 245 * @param name the parameter name 246 * @return the parameter values 247 */ 248 public String[] getParameterValues(String name) { 249 try { 250 Vector values = (Vector)parameters.get(name); 251 if(values == null || values.size() == 0) { 252 return null; 253 } 254 String[] valuesArray = new String[values.size()]; 255 values.copyInto(valuesArray); 256 return valuesArray; 257 } 258 catch(Exception e) { 259 return null; 260 } 261 } 262 263 264 /** 265 * Returns the filesystem name of the specified file, or null if the file was 266 * not included in the upload. A filesystem name is the name specified by the 267 * user. It is also the name under which the file is actually saved. 268 * 269 * @param name the file name 270 * @return the filesystem name of the file 271 */ 272 public String getFilesystemName(String name) { 273 try { 274 UploadedFile file = (UploadedFile)files.get(name); 275 return file.getFilesystemName(); 276 // may be null 277 } 278 catch(Exception e) { 279 return null; 280 } 281 } 282 283 284 /** 285 * Returns the content type of the specified file (as supplied by the client 286 * browser), or null if the file was not included in the upload. 287 * 288 * @param name the file name 289 * @return the content type of the file 290 */ 291 public String getContentType(String name) { 292 try { 293 UploadedFile file = (UploadedFile)files.get(name); 294 return file.getContentType(); 295 // may be null 296 } 297 catch(Exception e) { 298 return null; 299 } 300 } 301 302 303 /** 304 * Returns a File object for the specified file saved on the server's 305 * filesystem, or null if the file was not included in the upload. 306 * 307 * @param name the file name 308 * @return a File object for the named file 309 */ 310 public File getFile(String name) { 311 try { 312 UploadedFile file = (UploadedFile)files.get(name); 313 return file.getFile(); 314 // may be null 315 } 316 catch(Exception e) { 317 return null; 318 } 319 } 320 321 322 /** 323 * The workhorse method that actually parses the request. A subclass can 324 * override this method for a better optimized or differently behaved 325 * implementation. 326 * 327 * @exception IOException if the uploaded content is larger than <tt>maxSize 328 * </tt> or there's a problem parsing the request 329 */ 330 protected void readRequest() throws IOException { 331 // Check the content length to prevent denial of service attacks 332 int length = req.getContentLength(); 333 if(length > maxSize) { 334 throw new IOException("Posted content length of " + length + 335 " exceeds limit of " + maxSize); 336 } 337 338 // Check the content type to make sure it's "multipart/form-data" 339 // Access header two ways to work around WebSphere oddities 340 String type = null; 341 String type1 = req.getHeader("Content-Type"); 342 String type2 = req.getContentType(); 343 // If one value is null, choose the other value 344 if(type1 == null && type2 != null) { 345 type = type2; 346 } 347 else if(type2 == null && type1 != null) { 348 type = type1; 349 } 350 // If neither value is null, choose the longer value 351 else if(type1 != null && type2 != null) { 352 type = (type1.length() > type2.length() ? type1 : type2); 353 } 354 355 if(type == null || 356 !type.toLowerCase().startsWith("multipart/form-data")) { 357 throw new IOException("Posted content type isn't multipart/form-data"); 358 } 359 360 // Get the boundary string; it's included in the content type. 361 // Should look something like "------------------------12012133613061" 362 String boundary = extractBoundary(type); 363 if(boundary == null) { 364 throw new IOException("Separation boundary was not specified"); 365 } 366 367 // Construct the special input stream we'll read from 368 MultipartInputStreamHandler in = 369 new MultipartInputStreamHandler(req.getInputStream(), length); 370 371 // Read the first line, should be the first boundary 372 String line = in.readLine(); 373 if(line == null) { 374 throw new IOException("Corrupt form data: premature ending"); 375 } 376 377 // Verify that the line is the boundary 378 if(!line.startsWith(boundary)) { 379 throw new IOException("Corrupt form data: no leading boundary"); 380 } 381 382 // Now that we're just beyond the first boundary, loop over each part 383 boolean done = false; 384 while(!done) { 385 done = readNextPart(in, boundary); 386 } 387 } 388 389 390 /** 391 * A utility method that reads an individual part. Dispatches to {@link 392 * #readParameter(MultipartInputStreamHandler, String) readParameter()} and 393 * {@link #readAndSaveFile(MultipartInputStreamHandler, String, String, 394 * String) readAndSaveFile()} methods to do the actual work. <p> 395 * 396 * A subclass can override this method for a better optimized or differently 397 * behaved implementation. 398 * 399 * @param in the stream from which to read the part 400 * @param boundary the boundary separating parts 401 * @return a flag indicating whether this is the last part 402 * @exception IOException if there's a problem reading or parsing the 403 * request. 404 */ 405 protected boolean readNextPart(MultipartInputStreamHandler in, 406 String boundary) throws IOException { 407 // Read the first line, should look like this: 408 // content-disposition: form-data; name="field1"; filename="file1.txt" 409 String line = in.readLine(); 410 if(line == null) { 411 // No parts left, we're done 412 return true; 413 } 414 else if(line.length() == 0) { 415 // IE4 on Mac sends an empty line at the end; treat that as the end. 416 // Thanks to Daniel Lemire and Henri Tourigny for this fix. 417 return true; 418 } 419 420 // Parse the content-disposition line 421 String[] dispInfo = extractDispositionInfo(line); 422 String disposition = dispInfo[0]; 423 String name = dispInfo[1]; 424 String filename = dispInfo[2]; 425 426 // Now onto the next line. This will either be empty 427 // or contain a Content-Type and then an empty line. 428 line = in.readLine(); 429 if(line == null) { 430 // No parts left, we're done 431 return true; 432 } 433 434 // Get the content type, or null if none specified 435 String contentType = extractContentType(line); 436 if(contentType != null) { 437 // Eat the empty line 438 line = in.readLine(); 439 if(line == null || line.length() > 0) { 440 // line should be empty 441 throw new 442 IOException("Malformed line after content type: " + line); 443 } 444 } 445 else { 446 // Assume a default content type 447 contentType = "application/octet-stream"; 448 } 449 450 // Now, finally, we read the content (end after reading the boundary) 451 if(filename == null) { 452 // This is a parameter, add it to the vector of values 453 String value = readParameter(in, boundary); 454 if(value.equals("")) { 455 value = null; 456 // treat empty strings like nulls 457 } 458 Vector existingValues = (Vector)parameters.get(name); 459 if(existingValues == null) { 460 existingValues = new Vector(); 461 parameters.put(name, existingValues); 462 } 463 existingValues.addElement(value); 464 } 465 else { 466 // This is a file 467 readAndSaveFile(in, boundary, filename, contentType); 468 if(filename.equals(NO_FILE)) { 469 files.put(name, new UploadedFile(null, null, null)); 470 } 471 else { 472 files.put(name, 473 new UploadedFile(dir.toString(), filename, contentType)); 474 } 475 } 476 return false; 477 // there's more to read 478 } 479 480 481 /** 482 * A utility method that reads a single part of the multipart request that 483 * represents a parameter. A subclass can override this method for a better 484 * optimized or differently behaved implementation. 485 * 486 * @param in the stream from which to read the parameter 487 * information 488 * @param boundary the boundary signifying the end of this part 489 * @return the parameter value 490 * @exception IOException if there's a problem reading or parsing the request 491 */ 492 protected String readParameter(MultipartInputStreamHandler in, 493 String boundary) throws IOException { 494 StringBuffer sbuf = new StringBuffer(); 495 String line; 496 497 while((line = in.readLine()) != null) { 498 if(line.startsWith(boundary)) { 499 break; 500 } 501 sbuf.append(line + "\r\n"); 502 // add the \r\n in case there are many lines 503 } 504 505 if(sbuf.length() == 0) { 506 return null; 507 // nothing read 508 } 509 510 sbuf.setLength(sbuf.length() - 2); 511 // cut off the last line's \r\n 512 return sbuf.toString(); 513 // no URL decoding needed 514 } 515 516 517 /** 518 * A utility method that reads a single part of the multipart request that 519 * represents a file, and saves the file to the given directory. A subclass 520 * can override this method for a better optimized or differently behaved 521 * implementation. 522 * 523 * @param in the stream from which to read the file 524 * @param boundary the boundary signifying the end of this part 525 * @param filename the name under which to save the uploaded file 526 * @param contentType Description of the Parameter 527 * @exception IOException if there's a problem reading or parsing the request 528 */ 529 protected void readAndSaveFile(MultipartInputStreamHandler in, 530 String boundary, 531 String filename, 532 String contentType) throws IOException { 533 OutputStream out = null; 534 // A filename of NO_FILE means no file was sent, so just read to the 535 // next boundary and ignore the empty contents 536 if(filename.equals(NO_FILE)) { 537 out = new ByteArrayOutputStream(); 538 // write to nowhere 539 } 540 // A MacBinary file goes through a decoder 541 else if(contentType.equals("application/x-macbinary")) { 542 File f = new File(dir + File.separator + filename); 543 out = new MacBinaryDecoderOutputStream( 544 new BufferedOutputStream( 545 new FileOutputStream(f), 8 * 1024)); 546 } 547 // A real file's contents are written to disk 548 else { 549 File f = new File(dir + File.separator + filename); 550 out = new BufferedOutputStream(new FileOutputStream(f), 8 * 1024); 551 } 552 553 byte[] bbuf = new byte[100 * 1024]; 554 // 100K 555 int result; 556 String line; 557 558 // ServletInputStream.readLine() has the annoying habit of 559 // adding a \r\n to the end of the last line. 560 // Since we want a byte-for-byte transfer, we have to cut those chars. 561 boolean rnflag = false; 562 while((result = in.readLine(bbuf, 0, bbuf.length)) != -1) { 563 // Check for boundary 564 if(result > 2 && bbuf[0] == '-' && bbuf[1] == '-') { 565 // quick pre-check 566 line = new String(bbuf, 0, result, "ISO-8859-1"); 567 if(line.startsWith(boundary)) { 568 break; 569 } 570 } 571 // Are we supposed to write \r\n for the last iteration? 572 if(rnflag) { 573 out.write('\r'); 574 out.write('\n'); 575 rnflag = false; 576 } 577 // Write the buffer, postpone any ending \r\n 578 if(result >= 2 && 579 bbuf[result - 2] == '\r' && 580 bbuf[result - 1] == '\n') { 581 out.write(bbuf, 0, result - 2); 582 // skip the last 2 chars 583 rnflag = true; 584 // make a note to write them on the next iteration 585 } 586 else { 587 out.write(bbuf, 0, result); 588 } 589 } 590 out.flush(); 591 out.close(); 592 } 593 594 595 /** 596 * Extracts and returns the boundary token from a line. 597 * 598 * @param line Description of the Parameter 599 * @return Description of the Return Value 600 */ 601 private String extractBoundary(String line) { 602 // Use lastIndexOf() because IE 4.01 on Win98 has been known to send the 603 // "boundary=" string multiple times. Thanks to David Wall for this fix. 604 int index = line.lastIndexOf("boundary="); 605 if(index == -1) { 606 return null; 607 } 608 String boundary = line.substring(index + 9); 609 // 9 for "boundary=" 610 611 // The real boundary is always preceeded by an extra "--" 612 boundary = "--" + boundary; 613 614 return boundary; 615 } 616 617 618 /** 619 * Extracts and returns disposition info from a line, as a String array 620 * with elements: disposition, name, filename. Throws an IOException 621 * if the line is malformatted. 622 * 623 * @param line Description of the Parameter 624 * @return Description of the Return Value 625 * @throws IOException Description of the Exception 626 */ 627 628 private String[] extractDispositionInfo(String line) throws IOException { 629 // Return the line's data as an array: disposition, name, filename 630 String[] retval = new String[3]; 631 632 // Convert the line to a lowercase string without the ending \r\n 633 // Keep the original line for error messages and for variable names. 634 String origline = line; 635 line = origline.toLowerCase(); 636 637 // Get the content disposition, should be "form-data" 638 int start = line.indexOf("content-disposition: "); 639 int end = line.indexOf(";"); 640 if(start == -1 || end == -1) { 641 throw new IOException("Content disposition corrupt: " + origline); 642 } 643 String disposition = line.substring(start + 21, end); 644 if(!disposition.equals("form-data")) { 645 throw new IOException("Invalid content disposition: " + disposition); 646 } 647 648 // Get the field name 649 start = line.indexOf("name=\"", end); 650 // start at last semicolon 651 end = line.indexOf("\"", start + 7); 652 // skip name=\" 653 if(start == -1 || end == -1) { 654 throw new IOException("Content disposition corrupt: " + origline); 655 } 656 String name = origline.substring(start + 6, end); 657 658 // Get the filename, if given 659 String filename = null; 660 start = line.indexOf("filename=\"", end + 2); 661 // start after name 662 end = line.indexOf("\"", start + 10); 663 // skip filename=\" 664 if(start != -1 && end != -1) { 665 // note the != 666 filename = origline.substring(start + 10, end); 667 // The filename may contain a full path. Cut to just the filename. 668 int slash = 669 Math.max(filename.lastIndexOf('/'), filename.lastIndexOf('\\')); 670 if(slash > -1) { 671 filename = filename.substring(slash + 1); 672 // past last slash 673 } 674 if(filename.equals("")) { 675 filename = NO_FILE; 676 } 677 // sanity check 678 } 679 680 // Return a String array: disposition, name, filename 681 retval[0] = disposition; 682 retval[1] = name; 683 retval[2] = filename; 684 return retval; 685 } 686 687 688 /** 689 * Extracts and returns the content type from a line, or null if the 690 * line was empty. Throws an IOException if the line is malformatted. 691 * 692 * @param line Description of the Parameter 693 * @return Description of the Return Value 694 * @throws IOException Description of the Exception 695 */ 696 697 private String extractContentType(String line) throws IOException { 698 String contentType = null; 699 700 // Convert the line to a lowercase string 701 String origline = line; 702 line = origline.toLowerCase(); 703 704 // Get the content type, if any 705 if(line.startsWith("content-type")) { 706 int start = line.indexOf(" "); 707 if(start == -1) { 708 throw new IOException("Content type corrupt: " + origline); 709 } 710 contentType = line.substring(start + 1); 711 } 712 else if(line.length() != 0) { 713 // no content type, so should be empty 714 throw new IOException("Malformed line after disposition: " + origline); 715 } 716 717 return contentType; 718 } 719 } 720 721 /** 722 * A class to hold information about an uploaded file. 723 */ 724 class UploadedFile { 725 726 727 private String dir; 728 private String filename; 729 private String type; 730 731 732 /** 733 * Constructor for the UploadedFile object 734 * 735 * @param dir Description of the Parameter 736 * @param filename Description of the Parameter 737 * @param type Description of the Parameter 738 */ 739 UploadedFile(String dir, String filename, String type) { 740 this.dir = dir; 741 this.filename = filename; 742 this.type = type; 743 } 744 745 746 /** 747 * Gets the contentType attribute of the UploadedFile object 748 * 749 * @return The contentType value 750 */ 751 public String getContentType() { 752 return type; 753 } 754 755 756 /** 757 * Gets the filesystemName attribute of the UploadedFile object 758 * 759 * @return The filesystemName value 760 */ 761 public String getFilesystemName() { 762 return filename; 763 } 764 765 766 /** 767 * Gets the file attribute of the UploadedFile object 768 * 769 * @return The file value 770 */ 771 public File getFile() { 772 if(dir == null || filename == null) { 773 return null; 774 } 775 else { 776 return new File(dir + File.separator + filename); 777 } 778 } 779 } 780 781 /** 782 * A class to aid in reading multipart/form-data from a ServletInputStream. 783 * It keeps track of how many bytes have been read and detects when the 784 * Content-Length limit has been reached. This is necessary since some 785 * servlet engines are slow to notice the end of stream. 786 * 787 * Mac users: The Mac doesn't like class names which exceed 32 characters 788 * (including the ".class") so while this class is usable from a JAR 789 * anywhere, it won't compile on a Mac. 790 */ 791 class MultipartInputStreamHandler { 792 793 794 ServletInputStream in; 795 int totalExpected; 796 int totalRead = 0; 797 byte[] buf = new byte[8 * 1024]; 798 799 800 /** 801 * Constructor for the MultipartInputStreamHandler object 802 * 803 * @param in Description of the Parameter 804 * @param totalExpected Description of the Parameter 805 */ 806 public MultipartInputStreamHandler(ServletInputStream in, 807 int totalExpected) { 808 this.in = in; 809 this.totalExpected = totalExpected; 810 } 811 812 813 /** 814 * Reads the next line of input. Returns null to indicate the end 815 * of stream. 816 * 817 * @return Description of the Return Value 818 * @throws IOException Description of the Exception 819 */ 820 821 public String readLine() throws IOException { 822 StringBuffer sbuf = new StringBuffer(); 823 int result; 824 String line; 825 826 do { 827 result = this.readLine(buf, 0, buf.length); 828 // this.readLine() does += 829 if(result != -1) { 830 sbuf.append(new String(buf, 0, result, "ISO-8859-1")); 831 } 832 }while (result == buf.length); 833 // loop only if the buffer was filled 834 835 if(sbuf.length() == 0) { 836 return null; 837 // nothing read, must be at the end of stream 838 } 839 840 sbuf.setLength(sbuf.length() - 2); 841 // cut off the trailing \r\n 842 return sbuf.toString(); 843 } 844 845 846 /** 847 * A pass-through to ServletInputStream.readLine() that keeps track 848 * of how many bytes have been read and stops reading when the 849 * Content-Length limit has been reached. 850 * 851 * @param b Description of the Parameter 852 * @param off Description of the Parameter 853 * @param len Description of the Parameter 854 * @return Description of the Return Value 855 * @throws IOException Description of the Exception 856 */ 857 858 public int readLine(byte b[], int off, int len) throws IOException { 859 if(totalRead >= totalExpected) { 860 return -1; 861 } 862 else { 863 if(len > (totalExpected - totalRead)) { 864 len = totalExpected - totalRead; 865 // keep from reading off end 866 } 867 int result = in.readLine(b, off, len); 868 if(result > 0) { 869 totalRead += result; 870 } 871 return result; 872 } 873 } 874 } 875 876 /** 877 * Class to filters MacBinary files to normal files on the fly 878 * Optimized for speed more than readability 879 */ 880 class MacBinaryDecoderOutputStream extends FilterOutputStream { 881 882 883 int bytesFiltered = 0; 884 int dataForkLength = 0; 885 886 887 /** 888 * Constructor for the MacBinaryDecoderOutputStream object 889 * 890 * @param out Description of the Parameter 891 */ 892 public MacBinaryDecoderOutputStream(OutputStream out) { 893 super(out); 894 } 895 896 897 /** 898 * Description of the Method 899 * 900 * @param b Description of the Parameter 901 * @exception IOException Description of the Exception 902 */ 903 public void write(int b) throws IOException { 904 // Bytes 83 through 86 are a long representing the data fork length 905 // Check <= 86 first to short circuit early in the common case 906 if(bytesFiltered <= 86 && bytesFiltered >= 83) { 907 int leftShift = (86 - bytesFiltered) * 8; 908 dataForkLength = dataForkLength | (b & 0xff) << leftShift; 909 } 910 // Bytes 128 up to (128 + dataForkLength - 1) are the data fork 911 else if(bytesFiltered < (128 + dataForkLength) && bytesFiltered >= 128) { 912 out.write(b); 913 } 914 bytesFiltered++; 915 } 916 917 918 /** 919 * Description of the Method 920 * 921 * @param b Description of the Parameter 922 * @exception IOException Description of the Exception 923 */ 924 public void write(byte b[]) throws IOException { 925 write(b, 0, b.length); 926 } 927 928 929 /** 930 * Description of the Method 931 * 932 * @param b Description of the Parameter 933 * @param off Description of the Parameter 934 * @param len Description of the Parameter 935 * @exception IOException Description of the Exception 936 */ 937 public void write(byte b[], int off, int len) throws IOException { 938 // If the write is for content past the end of the data fork, ignore 939 if(bytesFiltered >= (128 + dataForkLength)) { 940 bytesFiltered += len; 941 } 942 // If the write is entirely within the data fork, write it directly 943 else if(bytesFiltered >= 128 && 944 (bytesFiltered + len) <= (128 + dataForkLength)) { 945 out.write(b, off, len); 946 bytesFiltered += len; 947 } 948 // Otherwise, do the write a byte at a time to get the logic above 949 else { 950 for(int i = 0; i < len; i++) { 951 write(b[off + i]); 952 } 953 } 954 } 955 } 956 957