View Javadoc

1   /**
2    * $Source: /usr/cvsroot/melati/melati/src/main/java/org/melati/servlet/MultipartFormDataDecoder.java,v $
3    * $Revision: 1.3 $
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    private static int MAX_SIZE = 2048;
64    private int maxSize;
65    private Melati melati = null;
66    DelimitedBufferedInputStream in;
67    String contentType;
68    FormDataAdaptorFactory factory;
69    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    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      this(melati,in, contentType, factory, MAX_SIZE);
92    }
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                               int maxSize) {
109     this.melati = melati;
110     this.in = new DelimitedBufferedInputStream(in, maxSize);
111     this.contentType = contentType;
112     this.factory = factory;
113     this.maxSize = maxSize;
114   }
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       return parseData(in, contentType, maxSize);
126     }
127     catch (IOException e) {
128       throw e;
129     }
130     finally {
131       in.close();
132       in = null;
133     }
134   }
135 
136   private Hashtable<String,MultipartFormField> parseData(DelimitedBufferedInputStream inP,
137                               String contentTypeP,
138                               int maxSizeP)
139       throws IOException {
140     String boundary = getBoundary(contentTypeP);
141     String line;
142     String header = "";
143     MultipartFormField field = null;
144     byte[] CRLF = {13,10};
145     byte[] buff = new byte[maxSizeP];
146     int count;
147 
148     while (state != STOP) {
149 
150        // Look for the start of a field (a boundary)
151       if (state == FIELD_START) {
152         count = inP.readToDelimiter(buff, 0, buff.length, boundary.getBytes());
153         if (count == buff.length) {
154           throw new IOException(
155               "Didn't find a boundary in the first " 
156               + buff.length + " bytes");
157         }
158         count = inP.readToDelimiter(buff, 0, buff.length, CRLF);
159         line = new String(buff, 0, count);
160 
161         if (line.equals(boundary)) {
162           state = IN_FIELD_HEADER;
163           header = "";
164           if (inP.read(buff, 0, 2) != 2) // snarf the crlf
165             throw new IOException(
166                 "Boundary wasn't followed by 2 bytes (\\r\\n)");
167         }
168         else if (line.equals(boundary+"--")) {
169           state = STOP;
170         }
171         else
172           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       if (state == IN_FIELD_HEADER) {
178         count = inP.readToDelimiter(buff, 0, buff.length, CRLF);
179         if (count != 0)     // a header line
180           header += new String(buff, 0, count) + "\r\n";
181         else {              // end of headers
182           state = IN_FIELD_DATA;
183           field = new MultipartFormField();
184           readField(field, header);
185           fields.put(field.getFieldName(), field);
186         }
187         if (inP.read(buff, 0, 2) != 2) // snarf the crlf
188           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       if (state == IN_FIELD_DATA) {
194         String dataBoundary = "\r\n" + boundary;
195 
196         // get an adaptor to save the field data 
197         FormDataAdaptor adaptor = null;
198         // Field should never be null but eclipse doesn't know that 
199         if (field != null && field.getUploadedFileName().equals("")) { // no file uploaded
200           adaptor = new MemoryFormDataAdaptor(); // store data in memory
201         }
202         else {
203           adaptor = factory.get(melati, field); 
204         }
205         adaptor.readData(field, inP, dataBoundary.getBytes());
206         // Field should never be null but eclipse doesn't know that 
207         if (field != null) field.setFormDataAdaptor(adaptor);
208         state = FIELD_START;
209       }
210 
211     }  // end of while (state != STOP)
212     return fields;
213   }
214 
215   private void readField(MultipartFormField field, String header) {
216     field.setContentDisposition(extractField(header, 
217                                              "content-disposition:", ";"));
218     String fieldName = extractField(header, "name=",";");
219     if (fieldName.length() != 0) {
220       if(fieldName.charAt(0) == '\"')
221         fieldName = fieldName.substring(1, fieldName.length() - 1);
222       field.setFieldName(fieldName);
223     }
224     String fileName = extractField(header, "filename=", ";");
225     if(fileName.length() != 0) {
226       if(fileName.charAt(0) == '\"')
227         fileName = fileName.substring(1, fileName.length()-1);
228       field.setUploadedFilePath(fileName);
229     }
230     field.setContentType(extractField(header, "content-type:", ";"));
231   }
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     String lheader = header.toLowerCase();
246     int begin = 0, end = 0;
247     begin = lheader.indexOf(lBound);
248     if(begin == -1)
249       return "";
250     begin = begin + lBound.length();
251     end = lheader.indexOf(rBound, begin);
252     if(end == -1)
253       end = lheader.indexOf("\r\n", begin);
254     if(end == -1)
255       end = lheader.length();
256     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     String boundary="";
267     int index;
268     // 9 - number of symbols in "boundary="
269     if ((index = header.lastIndexOf("boundary=")) != -1) {
270        boundary = header.substring(index + 9);
271        // as since real boundary two chars '-' longer 
272        // than written in CONTENT_TYPE header
273        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       throw new IOException("Boundary not found in header");
296     }
297      return boundary;
298   }
299 
300 }
301 
302 
303 
304     
305 
306 
307