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