Coverage Report - org.melati.servlet.MultipartFormDataDecoder
 
Classes in this File Line Coverage Branch Coverage Complexity
MultipartFormDataDecoder
91%
82/90
73%
31/42
5.286
 
 1  
 /**
 2  
  * $Source$
 3  
  * $Revision$
 4  
  *
 5  
  * Copyright (C) 2000 Myles Chippendale
 6  
  *
 7  
  * Part of Melati (http://melati.org), a framework for the rapid
 8  
  * development of clean, maintainable web applications.
 9  
  *
 10  
  * Melati is free software; Permission is granted to copy, distribute
 11  
  * and/or modify this software under the terms either:
 12  
  *
 13  
  * a) the GNU General Public License as published by the Free Software
 14  
  *    Foundation; either version 2 of the License, or (at your option)
 15  
  *    any later version,
 16  
  *
 17  
  *    or
 18  
  *
 19  
  * b) any version of the Melati Software License, as published
 20  
  *    at http://melati.org
 21  
  *
 22  
  * You should have received a copy of the GNU General Public License and
 23  
  * the Melati Software License along with this program;
 24  
  * if not, write to the Free Software Foundation, Inc.,
 25  
  * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA to obtain the
 26  
  * GNU General Public License and visit http://melati.org to obtain the
 27  
  * Melati Software License.
 28  
  *
 29  
  * Feel free to contact the Developers of Melati (http://melati.org),
 30  
  * if you would like to work out a different arrangement than the options
 31  
  * outlined here.  It is our intention to allow Melati to be used by as
 32  
  * wide an audience as possible.
 33  
  *
 34  
  * This program is distributed in the hope that it will be useful,
 35  
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 36  
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 37  
  * GNU General Public License for more details.
 38  
  *
 39  
  * Contact details for copyright holder:
 40  
  *
 41  
  *     Myles Chippendale <mylesc@paneris.org>
 42  
  *     http://paneris.org/
 43  
  *     29 Stanley Road, Oxford, OX4 1QY, UK
 44  
  *
 45  
  * Based on code by
 46  
  *   Vasily Pozhidaev <voodoo@knastu.ru; vpozhidaev@mail.ru>
 47  
  * */
 48  
 
 49  
 package org.melati.servlet;
 50  
 
 51  
 import java.io.IOException;
 52  
 import java.io.InputStream;
 53  
 import java.util.Hashtable;
 54  
 import org.melati.Melati;
 55  
 import org.melati.util.DelimitedBufferedInputStream;
 56  
 
 57  
 /**
 58  
  * Parses a multipart/form-data request into its different
 59  
  * fields, saving any files it finds along the way.
 60  
  */
 61  
 public class MultipartFormDataDecoder {
 62  
 
 63  1
   private static int MAX_SIZE = 2048;
 64  
   private int maxSize;
 65  19
   private Melati melati = null;
 66  
   DelimitedBufferedInputStream in;
 67  
   String contentType;
 68  
   FormDataAdaptorFactory factory;
 69  19
   Hashtable<String,MultipartFormField> fields = new Hashtable<String,MultipartFormField>();
 70  
 
 71  
   private static final int FIELD_START = 0;
 72  
   private static final int IN_FIELD_HEADER = 1;
 73  
   private static final int IN_FIELD_DATA = 2;
 74  
   private static final int STOP = 3;
 75  
 
 76  19
   private int state = FIELD_START;
 77  
 
 78  
   /**
 79  
    * Constructor with default maximum size.
 80  
    *
 81  
    * @param melati      The {@link Melati}
 82  
    * @param in          An {@link InputStream} from which to read the data
 83  
    * @param contentType A valid MIME type
 84  
    * @param factory     A {@link FormDataAdaptorFactory} to determine how to 
 85  
    *                    store the object's data
 86  
    */
 87  
   public MultipartFormDataDecoder(Melati melati,
 88  
                               InputStream in,
 89  
                               String contentType,
 90  
                               FormDataAdaptorFactory factory) {
 91  19
     this(melati,in, contentType, factory, MAX_SIZE);
 92  19
   }
 93  
 
 94  
   /**
 95  
    * Constructor specifying maximum size.
 96  
    *
 97  
    * @param melati      The {@link Melati}
 98  
    * @param in          An {@link InputStream} from which to read the data
 99  
    * @param contentType A valid MIME type
 100  
    * @param factory     A {@link FormDataAdaptorFactory} to determine how to 
 101  
    *                    store the object's data
 102  
    * @param maxSize     The maximum size of the data
 103  
    */
 104  
   public MultipartFormDataDecoder(Melati melati,
 105  
                               InputStream in,
 106  
                               String contentType,
 107  
                               FormDataAdaptorFactory factory,
 108  19
                               int maxSize) {
 109  19
     this.melati = melati;
 110  19
     this.in = new DelimitedBufferedInputStream(in, maxSize);
 111  19
     this.contentType = contentType;
 112  19
     this.factory = factory;
 113  19
     this.maxSize = maxSize;
 114  19
   }
 115  
 
 116  
  /**
 117  
   * Parse the uploaded data into its constituents. 
 118  
   * 
 119  
   * @return a <code>Hashtable</code> of the constituents
 120  
   * @throws IOException
 121  
   *         if an error occurs reading the input stream
 122  
   */
 123  
   public Hashtable<String,MultipartFormField> parseData() throws IOException {
 124  
     try {
 125  19
       return parseData(in, contentType, maxSize);
 126  
     }
 127  0
     catch (IOException e) {
 128  0
       throw e;
 129  
     }
 130  
     finally {
 131  19
       in.close();
 132  19
       in = null;
 133  
     }
 134  
   }
 135  
 
 136  
   private Hashtable<String,MultipartFormField> parseData(DelimitedBufferedInputStream inP,
 137  
                               String contentTypeP,
 138  
                               int maxSizeP)
 139  
       throws IOException {
 140  19
     String boundary = getBoundary(contentTypeP);
 141  
     String line;
 142  19
     String header = "";
 143  19
     MultipartFormField field = null;
 144  19
     byte[] CRLF = {13,10};
 145  19
     byte[] buff = new byte[maxSizeP];
 146  
     int count;
 147  
 
 148  187
     while (state != STOP) {
 149  
 
 150  
        // Look for the start of a field (a boundary)
 151  168
       if (state == FIELD_START) {
 152  83
         count = inP.readToDelimiter(buff, 0, buff.length, boundary.getBytes());
 153  83
         if (count == buff.length) {
 154  0
           throw new IOException(
 155  
               "Didn't find a boundary in the first " 
 156  
               + buff.length + " bytes");
 157  
         }
 158  83
         count = inP.readToDelimiter(buff, 0, buff.length, CRLF);
 159  83
         line = new String(buff, 0, count);
 160  
 
 161  83
         if (line.equals(boundary)) {
 162  64
           state = IN_FIELD_HEADER;
 163  64
           header = "";
 164  64
           if (inP.read(buff, 0, 2) != 2) // snarf the crlf
 165  0
             throw new IOException(
 166  
                 "Boundary wasn't followed by 2 bytes (\\r\\n)");
 167  
         }
 168  19
         else if (line.equals(boundary+"--")) {
 169  19
           state = STOP;
 170  
         }
 171  
         else
 172  0
           throw new IOException(
 173  
               "Didn't find the boundary I was expecting before a field");
 174  
       }
 175  
       
 176  
        // Read headers (i.e. until the first blank line)
 177  168
       if (state == IN_FIELD_HEADER) {
 178  149
         count = inP.readToDelimiter(buff, 0, buff.length, CRLF);
 179  149
         if (count != 0)     // a header line
 180  85
           header += new String(buff, 0, count) + "\r\n";
 181  
         else {              // end of headers
 182  64
           state = IN_FIELD_DATA;
 183  64
           field = new MultipartFormField();
 184  64
           readField(field, header);
 185  64
           fields.put(field.getFieldName(), field);
 186  
         }
 187  149
         if (inP.read(buff, 0, 2) != 2) // snarf the crlf
 188  0
           throw new IOException(
 189  
               "Header line wasn't followed by 2 bytes (\\r\\n)");
 190  
       } 
 191  
 
 192  
        // Read data (i.e. until the next boundary);
 193  168
       if (state == IN_FIELD_DATA) {
 194  64
         String dataBoundary = "\r\n" + boundary;
 195  
 
 196  
         // get an adaptor to save the field data 
 197  64
         FormDataAdaptor adaptor = null;
 198  
         // Field should never be null but eclipse doesn't know that 
 199  64
         if (field != null && field.getUploadedFileName().equals("")) { // no file uploaded
 200  51
           adaptor = new MemoryFormDataAdaptor(); // store data in memory
 201  
         }
 202  
         else {
 203  13
           adaptor = factory.get(melati, field); 
 204  
         }
 205  64
         adaptor.readData(field, inP, dataBoundary.getBytes());
 206  
         // Field should never be null but eclipse doesn't know that 
 207  64
         if (field != null) field.setFormDataAdaptor(adaptor);
 208  64
         state = FIELD_START;
 209  64
       }
 210  
 
 211  
     }  // end of while (state != STOP)
 212  19
     return fields;
 213  
   }
 214  
 
 215  
   private void readField(MultipartFormField field, String header) {
 216  64
     field.setContentDisposition(extractField(header, 
 217  
                                              "content-disposition:", ";"));
 218  64
     String fieldName = extractField(header, "name=",";");
 219  64
     if (fieldName.length() != 0) {
 220  64
       if(fieldName.charAt(0) == '\"')
 221  64
         fieldName = fieldName.substring(1, fieldName.length() - 1);
 222  64
       field.setFieldName(fieldName);
 223  
     }
 224  64
     String fileName = extractField(header, "filename=", ";");
 225  64
     if(fileName.length() != 0) {
 226  21
       if(fileName.charAt(0) == '\"')
 227  21
         fileName = fileName.substring(1, fileName.length()-1);
 228  21
       field.setUploadedFilePath(fileName);
 229  
     }
 230  64
     field.setContentType(extractField(header, "content-type:", ";"));
 231  64
   }
 232  
 
 233  
  /**
 234  
   * Extract a String from header bounded by lBound and either: 
 235  
   * rBound or a "\r\n"
 236  
   * or the end of the String.
 237  
   *
 238  
   * @param header The field metadata to read
 239  
   * @param lBound Where to start reading from 
 240  
   * @param rBound where to stop reading
 241  
   * @return The substring required
 242  
   */
 243  
   protected String extractField(String header, String lBound, 
 244  
                                 String rBound) {
 245  256
     String lheader = header.toLowerCase();
 246  256
     int begin = 0, end = 0;
 247  256
     begin = lheader.indexOf(lBound);
 248  256
     if(begin == -1)
 249  86
       return "";
 250  170
     begin = begin + lBound.length();
 251  170
     end = lheader.indexOf(rBound, begin);
 252  170
     if(end == -1)
 253  85
       end = lheader.indexOf("\r\n", begin);
 254  170
     if(end == -1)
 255  0
       end = lheader.length();
 256  170
     return header.substring(begin, end).trim();
 257  
   }
 258  
 
 259  
  /**
 260  
   * Extract boundary from the header.
 261  
   * No longer attempts to get it from the data.
 262  
   * 
 263  
   */
 264  
   private String getBoundary(/*byte[] data, */ String header) 
 265  
       throws IOException {
 266  19
     String boundary="";
 267  
     int index;
 268  
     // 9 - number of symbols in "boundary="
 269  19
     if ((index = header.lastIndexOf("boundary=")) != -1) {
 270  19
        boundary = header.substring(index + 9);
 271  
        // as since real boundary two chars '-' longer 
 272  
        // than written in CONTENT_TYPE header
 273  19
        boundary = "--" + boundary;
 274  
     } else {
 275  
 
 276  
 /*
 277  
        // HotJava does not send boundary within CONTENT_TYPE header:
 278  
        // and as I seen HotJava has errors with sending binary data.
 279  
        int begin = 0, end = 0;
 280  
        byte[] str1 = {45, 45}, str2 = {13, 10};
 281  
        begin = indexOf(data, str1, 0);
 282  
        end = indexOf(data, str2, begin);
 283  
 
 284  
        // Boundary length should be in reasonable limits
 285  
        if (begin != -1 && end != -1 && 
 286  
            ((end - begin) > 5 && 
 287  
            (end - begin) < 100)) {
 288  
          byte[] buf = new byte[end - begin];
 289  
          System.arraycopy(data, begin, buf, 0, end - begin);
 290  
          boundary = new String(buf);
 291  
        } else {
 292  
          throw new IOException("Boundary not found");
 293  
        }
 294  
 */
 295  0
       throw new IOException("Boundary not found in header");
 296  
     }
 297  19
      return boundary;
 298  
   }
 299  
 
 300  
 }
 301  
 
 302  
 
 303  
 
 304  
     
 305  
 
 306  
 
 307