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