Docjar: A Java Source and Docuemnt Enginecom.*    java.*    javax.*    org.*    all    new    plug-in

Quick Search    Search Deep

Source code: gsoft/xervlet/MultipartRequest.java


1   /*************************************************************************
2    Copyright (C) 2003  Steve Gee
3    stevesgee@cox.net
4   
5    This program is free software; you can redistribute it and/or
6    modify it under the terms of the GNU General Public License
7    as published by the Free Software Foundation; either version 2
8    of the License, or (at your option) any later version.
9   
10   This program is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   GNU General Public License for more details.
14  
15   You should have received a copy of the GNU General Public License
16   along with this program; if not, write to the Free Software
17   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
18   *************************************************************************/
19  
20  
21  package gsoft.xervlet;
22  
23  import java.io.*;
24  import java.util.*;
25  import javax.servlet.*;
26  
27  /**
28   * A utility class to handle <tt>multipart/form-data</tt> requests,
29   * the kind of requests that support file uploads.  This class can
30   * receive arbitrarily large files (up to an artificial limit you can set),
31   * and fairly efficiently too.
32   * It cannot handle nested data (multipart content within multipart content)
33   * or internationalized content (such as non Latin-1 filenames).
34   * <p>
35   * It's used like this:
36   * <blockquote><pre>
37   * MultipartRequest multi = new MultipartRequest(req, ".");
38   * &nbsp;
39   * out.println("Params:");
40   * Enumeration params = multi.getParameterNames();
41   * while (params.hasMoreElements()) {
42   *   String name = (String)params.nextElement();
43   *   String value = multi.getParameter(name);
44   *   out.println(name + " = " + value);
45   * }
46   * out.println();
47   * &nbsp;
48   * out.println("Files:");
49   * Enumeration files = multi.getFileNames();
50   * while (files.hasMoreElements()) {
51   *   String name = (String)files.nextElement();
52   *   String filename = multi.getFilesystemName(name);
53   *   String type = multi.getContentType(name);
54   *   File f = multi.getFile(name);
55   *   out.println("name: " + name);
56   *   out.println("filename: " + filename);
57   *   out.println("type: " + type);
58   *   if (f != null) {
59   *     out.println("f.toString(): " + f.toString());
60   *     out.println("f.getName(): " + f.getName());
61   *     out.println("f.exists(): " + f.exists());
62   *     out.println("f.length(): " + f.length());
63   *     out.println();
64   *   }
65   * }
66   * </pre></blockquote>
67   *
68   * A client can upload files using an HTML form with the following structure.
69   * Note that not all browsers support file uploads.
70   * <blockquote><pre>
71   * &lt;FORM ACTION="/servlet/Handler" METHOD=POST
72   *          ENCTYPE="multipart/form-data"&gt;
73   * What is your name? &lt;INPUT TYPE=TEXT NAME=submitter&gt; &lt;BR&gt;
74   * Which file to upload? &lt;INPUT TYPE=FILE NAME=file&gt; &lt;BR&gt;
75   * &lt;INPUT TYPE=SUBMIT&GT;
76   * &lt;/FORM&gt;
77   * </pre></blockquote>
78   * <p>
79   * The full file upload specification is contained in experimental RFC 1867,
80   * available at <a href="http://ds.internic.net/rfc/rfc1867.txt">
81   * http://ds.internic.net/rfc/rfc1867.txt</a>.
82   *
83   */
84  public class MultipartRequest {
85  
86      private static final int DEFAULT_MAX_POST_SIZE = 1024 * 1024;  // 1 Meg
87  
88      private ServletRequest req;
89      private File dir;
90      private int maxSize;
91  
92      private Hashtable parameters = new Hashtable();  // name - value
93      private Hashtable files = new Hashtable();       // name - UploadedFile
94  
95      public MultipartRequest(ServletRequest request,
96                              String saveDirectory) throws IOException {
97          this(request, saveDirectory, DEFAULT_MAX_POST_SIZE);
98      }
99  
100     public MultipartRequest(ServletRequest request,
101                             String saveDirectory,
102                             int maxPostSize) throws IOException {
103 
104         // Sanity check values
105         if (request == null)
106             throw new IllegalArgumentException("request cannot be null");
107         if (saveDirectory == null)
108             throw new IllegalArgumentException("saveDirectory cannot be null");
109         if (maxPostSize <= 0) {
110             throw new IllegalArgumentException("maxPostSize must be positive");
111         }
112 
113         // Save the request, dir, and max size
114         req = request;
115         dir = new File(saveDirectory);
116         maxSize = maxPostSize;
117 
118         // Check saveDirectory is truly a directory
119         if (!dir.isDirectory())
120             throw new IllegalArgumentException("Not a directory: " + saveDirectory);
121 
122         // Check saveDirectory is writable
123         if (!dir.canWrite())
124             throw new IllegalArgumentException("Not writable: " + saveDirectory);
125 
126         // Now parse the request saving data to "parameters" and "files";
127         // write the file contents to the saveDirectory
128         readRequest();
129     }
130 
131     public Enumeration getParameterNames() {
132         return parameters.keys();
133     }
134 
135     public Enumeration getFileNames() {
136         return files.keys();
137     }
138 
139     public String getParameter(String name) {
140         try {
141             String param = (String) parameters.get(name);
142             if (param.equals("")) return null;
143             return param;
144         } catch (Exception e) {
145             return null;
146         }
147     }
148 
149     public String getFilesystemName(String name) {
150         try {
151             UploadedFile file = (UploadedFile) files.get(name);
152             return file.getFilesystemName();  // may be null
153         } catch (Exception e) {
154             return null;
155         }
156     }
157 
158     public String getContentType(String name) {
159         try {
160             UploadedFile file = (UploadedFile) files.get(name);
161             return file.getContentType();  // may be null
162         } catch (Exception e) {
163             return null;
164         }
165     }
166 
167     public File getFile(String name) {
168         try {
169             UploadedFile file = (UploadedFile) files.get(name);
170             return file.getFile();  // may be null
171         } catch (Exception e) {
172             return null;
173         }
174     }
175 
176     protected void readRequest() throws IOException {
177         // Check the content type to make sure it's "multipart/form-data"
178         String type = req.getContentType();
179         if (type == null ||
180                 !type.toLowerCase().startsWith("multipart/form-data")) {
181             throw new IOException("Posted content type isn't multipart/form-data");
182         }
183 
184         // Check the content length to prevent denial of service attacks
185         int length = req.getContentLength();
186         if (length > maxSize) {
187             throw new IOException("Posted content length of " + length +
188                     " exceeds limit of " + maxSize);
189         }
190 
191         // Get the boundary string; it's included in the content type.
192         // Should look something like "------------------------12012133613061"
193         String boundary = extractBoundary(type);
194         if (boundary == null) {
195             throw new IOException("Separation boundary was not specified");
196         }
197 
198         // Construct the special input stream we'll read from
199         MultipartInputStreamHandler in =
200                 new MultipartInputStreamHandler(req.getInputStream(), boundary, length);
201 
202         // Read the first line, should be the first boundary
203         String line = in.readLine();
204         if (line == null) {
205             throw new IOException("Corrupt form data: premature ending");
206         }
207 
208         // Verify that the line is the boundary
209         if (!line.startsWith(boundary)) {
210             throw new IOException("Corrupt form data: no leading boundary");
211         }
212 
213         // Now that we're just beyond the first boundary, loop over each part
214         boolean done = false;
215         while (!done) {
216             done = readNextPart(in, boundary);
217         }
218     }
219 
220     protected boolean readNextPart(MultipartInputStreamHandler in,
221                                    String boundary) throws IOException {
222         // Read the first line, should look like this:
223         // content-disposition: form-data; name="field1"; filename="file1.txt"
224         String line = in.readLine();
225         if (line == null) {
226             // No parts left, we're done
227             return true;
228         }
229 
230         // Parse the content-disposition line
231         String[] dispInfo = extractDispositionInfo(line);
232         String disposition = dispInfo[0];
233         String name = dispInfo[1];
234         String filename = dispInfo[2];
235 
236         // Now onto the next line.  This will either be empty
237         // or contain a Content-Type and then an empty line.
238         line = in.readLine();
239         if (line == null) {
240             // No parts left, we're done
241             return true;
242         }
243 
244         // Get the content type, or null if none specified
245         String contentType = extractContentType(line);
246         if (contentType != null) {
247             // Eat the empty line
248             line = in.readLine();
249             if (line == null || line.length() > 0) {  // line should be empty
250                 throw new
251                         IOException("Malformed line after content type: " + line);
252             }
253         } else {
254             // Assume a default content type
255             contentType = "application/octet-stream";
256         }
257 
258         // Now, finally, we read the content (end after reading the boundary)
259         if (filename == null) {
260             // This is a parameter
261             String value = readParameter(in, boundary);
262             parameters.put(name, value);
263         } else {
264             // This is a file
265             readAndSaveFile(in, boundary, filename);
266             if (filename.equals("unknown")) {
267                 files.put(name, new UploadedFile(null, null, null));
268             } else {
269                 files.put(name,
270                         new UploadedFile(dir.toString(), filename, contentType));
271             }
272         }
273         return false;  // there's more to read
274     }
275 
276     protected String readParameter(MultipartInputStreamHandler in,
277                                    String boundary) throws IOException {
278         StringBuffer sbuf = new StringBuffer();
279         String line;
280 
281         while ((line = in.readLine()) != null) {
282             if (line.startsWith(boundary)) break;
283             sbuf.append(line + "\r\n");  // add the \r\n in case there are many lines
284         }
285 
286         if (sbuf.length() == 0) {
287             return null;  // nothing read
288         }
289 
290         sbuf.setLength(sbuf.length() - 2);  // cut off the last line's \r\n
291         return sbuf.toString();  // no URL decoding needed
292     }
293 
294     protected void readAndSaveFile(MultipartInputStreamHandler in,
295                                    String boundary,
296                                    String filename) throws IOException {
297         File f = new File(dir + File.separator + filename);
298         FileOutputStream fos = new FileOutputStream(f);
299         BufferedOutputStream out = new BufferedOutputStream(fos, 8 * 1024); // 8K
300 
301         byte[] bbuf = new byte[100 * 1024];  // 100K
302         int result;
303         String line;
304 
305         // ServletInputStream.readLine() has the annoying habit of
306         // adding a \r\n to the end of the last line.
307         // Since we want a byte-for-byte transfer, we have to cut those chars.
308         boolean rnflag = false;
309         while ((result = in.readLine(bbuf, 0, bbuf.length)) != -1) {
310             // Check for boundary
311             if (result > 2 && bbuf[0] == '-' && bbuf[1] == '-') { // quick pre-check
312                 line = new String(bbuf, 0, result, "ISO-8859-1");
313                 if (line.startsWith(boundary)) break;
314             }
315             // Are we supposed to write \r\n for the last iteration?
316             if (rnflag) {
317                 out.write('\r');
318                 out.write('\n');
319                 rnflag = false;
320             }
321             // Write the buffer, postpone any ending \r\n
322             if (result >= 2 &&
323                     bbuf[result - 2] == '\r' &&
324                     bbuf[result - 1] == '\n') {
325                 out.write(bbuf, 0, result - 2);  // skip the last 2 chars
326                 rnflag = true;  // make a note to write them on the next iteration
327             } else {
328                 out.write(bbuf, 0, result);
329             }
330         }
331         out.flush();
332         out.close();
333         fos.close();
334     }
335 
336     // Extracts and returns the boundary token from a line.
337     //
338     private String extractBoundary(String line) {
339         int index = line.indexOf("boundary=");
340         if (index == -1) {
341             return null;
342         }
343         String boundary = line.substring(index + 9);  // 9 for "boundary="
344 
345         // The real boundary is always preceeded by an extra "--"
346         boundary = "--" + boundary;
347 
348         return boundary;
349     }
350 
351     // Extracts and returns disposition info from a line, as a String array
352     // with elements: disposition, name, filename.  Throws an IOException
353     // if the line is malformatted.
354     //
355     private String[] extractDispositionInfo(String line) throws IOException {
356         // Return the line's data as an array: disposition, name, filename
357         String[] retval = new String[3];
358 
359         // Convert the line to a lowercase string without the ending \r\n
360         // Keep the original line for error messages and for variable names.
361         String origline = line;
362         line = origline.toLowerCase();
363 
364         // Get the content disposition, should be "form-data"
365         int start = line.indexOf("content-disposition: ");
366         int end = line.indexOf(";");
367         if (start == -1 || end == -1) {
368             throw new IOException("Content disposition corrupt: " + origline);
369         }
370         String disposition = line.substring(start + 21, end);
371         if (!disposition.equals("form-data")) {
372             throw new IOException("Invalid content disposition: " + disposition);
373         }
374 
375         // Get the field name
376         start = line.indexOf("name=\"", end);  // start at last semicolon
377         end = line.indexOf("\"", start + 7);   // skip name=\"
378         if (start == -1 || end == -1) {
379             throw new IOException("Content disposition corrupt: " + origline);
380         }
381         String name = origline.substring(start + 6, end);
382 
383         // Get the filename, if given
384         String filename = null;
385         start = line.indexOf("filename=\"", end + 2);  // start after name
386         end = line.indexOf("\"", start + 10);          // skip filename=\"
387         if (start != -1 && end != -1) {                // note the !=
388             filename = origline.substring(start + 10, end);
389             // The filename may contain a full path.  Cut to just the filename.
390             int slash =
391                     Math.max(filename.lastIndexOf('/'), filename.lastIndexOf('\\'));
392             if (slash > -1) {
393                 filename = filename.substring(slash + 1);  // past last slash
394             }
395             if (filename.equals("")) filename = "unknown"; // sanity check
396         }
397 
398         // Return a String array: disposition, name, filename
399         retval[0] = disposition;
400         retval[1] = name;
401         retval[2] = filename;
402         return retval;
403     }
404 
405     // Extracts and returns the content type from a line, or null if the
406     // line was empty.  Throws an IOException if the line is malformatted.
407     //
408     private String extractContentType(String line) throws IOException {
409         String contentType = null;
410 
411         // Convert the line to a lowercase string
412         String origline = line;
413         line = origline.toLowerCase();
414 
415         // Get the content type, if any
416         if (line.startsWith("content-type")) {
417             int start = line.indexOf(" ");
418             if (start == -1) {
419                 throw new IOException("Content type corrupt: " + origline);
420             }
421             contentType = line.substring(start + 1);
422         } else if (line.length() != 0) {  // no content type, so should be empty
423             throw new IOException("Malformed line after disposition: " + origline);
424         }
425 
426         return contentType;
427     }
428 }
429 
430 
431 // A class to hold information about an uploaded file.
432 //
433 
434 class UploadedFile {
435 
436     private String dir;
437     private String filename;
438     private String type;
439 
440     UploadedFile(String dir, String filename, String type) {
441         this.dir = dir;
442         this.filename = filename;
443         this.type = type;
444     }
445 
446     public String getContentType() {
447         return type;
448     }
449 
450     public String getFilesystemName() {
451         return filename;
452     }
453 
454     public File getFile() {
455         if (dir == null || filename == null) {
456             return null;
457         } else {
458             return new File(dir + File.separator + filename);
459         }
460     }
461 
462 }
463 
464 
465