001// Copyright (C) 1998-2001 by Jason Hunter <jhunter_AT_acm_DOT_org>. 002// All rights reserved. Use of this class is limited. 003// Please see the LICENSE for more information. 004 005package com.oreilly.servlet; 006 007import java.io.*; 008import java.util.*; 009import javax.servlet.*; 010import javax.servlet.http.*; 011 012import com.oreilly.servlet.multipart.MultipartParser; 013import com.oreilly.servlet.multipart.Part; 014import com.oreilly.servlet.multipart.FilePart; 015import com.oreilly.servlet.multipart.ParamPart; 016import com.oreilly.servlet.multipart.FileRenamePolicy; 017 018/** 019 * A utility class to handle <code>multipart/form-data</code> requests, 020 * the kind of requests that support file uploads. This class emulates the 021 * interface of <code>HttpServletRequest</code>, making it familiar to use. 022 * It uses a "push" model where any incoming files are read and saved directly 023 * to disk in the constructor. If you wish to have more flexibility, e.g. 024 * write the files to a database, use the "pull" model 025 * <code>MultipartParser</code> instead. 026 * <p> 027 * This class can receive arbitrarily large files (up to an artificial limit 028 * you can set), and fairly efficiently too. 029 * It cannot handle nested data (multipart content within multipart content). 030 * It <b>can</b> now with the latest release handle internationalized content 031 * (such as non Latin-1 filenames). 032 * <p> 033 * To avoid collisions and have fine control over file placement, there's a 034 * constructor variety that takes a pluggable FileRenamePolicy implementation. 035 * A particular policy can choose to rename or change the location of the file 036 * before it's written. 037 * <p> 038 * See the included upload.war for an example of how to use this class. 039 * <p> 040 * The full file upload specification is contained in experimental RFC 1867, 041 * available at <a href="http://www.ietf.org/rfc/rfc1867.txt"> 042 * http://www.ietf.org/rfc/rfc1867.txt</a>. 043 * 044 * @see MultipartParser 045 * 046 * @author Jason Hunter 047 * @author Geoff Soutter 048 * @version 1.11, 2002/11/01, combine query string params in param list<br> 049 * @version 1.10, 2002/05/27, added access to the original file names<br> 050 * @version 1.9, 2002/04/30, added support for file renaming, thanks to 051 * Changshin Lee<br> 052 * @version 1.8, 2002/04/30, added support for internationalization, thanks to 053 * Changshin Lee<br> 054 * @version 1.7, 2001/02/07, made fields protected to increase user flexibility<br> 055 * @version 1.6, 2000/07/21, redid internals to use MultipartParser, 056 * thanks to Geoff Soutter<br> 057 * @version 1.5, 2000/02/04, added auto MacBinary decoding for IE on Mac<br> 058 * @version 1.4, 2000/01/05, added getParameterValues(), 059 * WebSphere 2.x getContentType() workaround, 060 * stopped writing empty "unknown" file<br> 061 * @version 1.3, 1999/12/28, IE4 on Win98 lastIndexOf("boundary=") 062 * workaround<br> 063 * @version 1.2, 1999/12/20, IE4 on Mac readNextPart() workaround<br> 064 * @version 1.1, 1999/01/15, JSDK readLine() bug workaround<br> 065 * @version 1.0, 1998/09/18<br> 066 */ 067public class MultipartRequest { 068 069 private static final int DEFAULT_MAX_POST_SIZE = 1024 * 1024; // 1 Meg 070 071 protected Hashtable parameters = new Hashtable(); // name - Vector of values 072 protected Hashtable files = new Hashtable(); // name - UploadedFile 073 074 /** 075 * Constructs a new MultipartRequest to handle the specified request, 076 * saving any uploaded files to the given directory, and limiting the 077 * upload size to 1 Megabyte. If the content is too large, an 078 * IOException is thrown. This constructor actually parses the 079 * <tt>multipart/form-data</tt> and throws an IOException if there's any 080 * problem reading or parsing the request. 081 * 082 * @param request the servlet request. 083 * @param saveDirectory the directory in which to save any uploaded files. 084 * @exception IOException if the uploaded content is larger than 1 Megabyte 085 * or there's a problem reading or parsing the request. 086 */ 087 public MultipartRequest(HttpServletRequest request, 088 String saveDirectory) throws IOException { 089 this(request, saveDirectory, DEFAULT_MAX_POST_SIZE); 090 } 091 092 /** 093 * Constructs a new MultipartRequest to handle the specified request, 094 * saving any uploaded files to the given directory, and limiting the 095 * upload size to the specified length. If the content is too large, an 096 * IOException is thrown. This constructor actually parses the 097 * <tt>multipart/form-data</tt> and throws an IOException if there's any 098 * problem reading or parsing the request. 099 * 100 * @param request the servlet request. 101 * @param saveDirectory the directory in which to save any uploaded files. 102 * @param maxPostSize the maximum size of the POST content. 103 * @exception IOException if the uploaded content is larger than 104 * <tt>maxPostSize</tt> or there's a problem reading or parsing the request. 105 */ 106 public MultipartRequest(HttpServletRequest request, 107 String saveDirectory, 108 int maxPostSize) throws IOException { 109 this(request, saveDirectory, maxPostSize, null, null); 110 } 111 112 /** 113 * Constructs a new MultipartRequest to handle the specified request, 114 * saving any uploaded files to the given directory, and limiting the 115 * upload size to the specified length. If the content is too large, an 116 * IOException is thrown. This constructor actually parses the 117 * <tt>multipart/form-data</tt> and throws an IOException if there's any 118 * problem reading or parsing the request. 119 * 120 * @param request the servlet request. 121 * @param saveDirectory the directory in which to save any uploaded files. 122 * @param encoding the encoding of the response, such as ISO-8859-1 123 * @exception IOException if the uploaded content is larger than 124 * 1 Megabyte or there's a problem reading or parsing the request. 125 */ 126 public MultipartRequest(HttpServletRequest request, 127 String saveDirectory, 128 String encoding) throws IOException { 129 this(request, saveDirectory, DEFAULT_MAX_POST_SIZE, encoding, null); 130 } 131 132 /** 133 * Constructs a new MultipartRequest to handle the specified request, 134 * saving any uploaded files to the given directory, and limiting the 135 * upload size to the specified length. If the content is too large, an 136 * IOException is thrown. This constructor actually parses the 137 * <tt>multipart/form-data</tt> and throws an IOException if there's any 138 * problem reading or parsing the request. 139 * 140 * @param request the servlet request. 141 * @param saveDirectory the directory in which to save any uploaded files. 142 * @param maxPostSize the maximum size of the POST content. 143 * @param encoding the encoding of the response, such as ISO-8859-1 144 * @exception IOException if the uploaded content is larger than 145 * <tt>maxPostSize</tt> or there's a problem reading or parsing the request. 146 */ 147 public MultipartRequest(HttpServletRequest request, 148 String saveDirectory, 149 int maxPostSize, 150 FileRenamePolicy policy) throws IOException { 151 this(request, saveDirectory, maxPostSize, null, policy); 152 } 153 154 /** 155 * Constructs a new MultipartRequest to handle the specified request, 156 * saving any uploaded files to the given directory, and limiting the 157 * upload size to the specified length. If the content is too large, an 158 * IOException is thrown. This constructor actually parses the 159 * <tt>multipart/form-data</tt> and throws an IOException if there's any 160 * problem reading or parsing the request. 161 * 162 * @param request the servlet request. 163 * @param saveDirectory the directory in which to save any uploaded files. 164 * @param maxPostSize the maximum size of the POST content. 165 * @param encoding the encoding of the response, such as ISO-8859-1 166 * @exception IOException if the uploaded content is larger than 167 * <tt>maxPostSize</tt> or there's a problem reading or parsing the request. 168 */ 169 public MultipartRequest(HttpServletRequest request, 170 String saveDirectory, 171 int maxPostSize, 172 String encoding) throws IOException { 173 this(request, saveDirectory, maxPostSize, encoding, null); 174 } 175 176 /** 177 * Constructs a new MultipartRequest to handle the specified request, 178 * saving any uploaded files to the given directory, and limiting the 179 * upload size to the specified length. If the content is too large, an 180 * IOException is thrown. This constructor actually parses the 181 * <tt>multipart/form-data</tt> and throws an IOException if there's any 182 * problem reading or parsing the request. 183 * 184 * To avoid file collisions, this constructor takes an implementation of the 185 * FileRenamePolicy interface to allow a pluggable rename policy. 186 * 187 * @param request the servlet request. 188 * @param saveDirectory the directory in which to save any uploaded files. 189 * @param maxPostSize the maximum size of the POST content. 190 * @param encoding the encoding of the response, such as ISO-8859-1 191 * @param policy a pluggable file rename policy 192 * @exception IOException if the uploaded content is larger than 193 * <tt>maxPostSize</tt> or there's a problem reading or parsing the request. 194 */ 195 public MultipartRequest(HttpServletRequest request, 196 String saveDirectory, 197 int maxPostSize, 198 String encoding, 199 FileRenamePolicy policy) throws IOException { 200 // Sanity check values 201 if (request == null) 202 throw new IllegalArgumentException("request cannot be null"); 203 if (saveDirectory == null) 204 throw new IllegalArgumentException("saveDirectory cannot be null"); 205 if (maxPostSize <= 0) { 206 throw new IllegalArgumentException("maxPostSize must be positive"); 207 } 208 209 // Save the dir 210 File dir = new File(saveDirectory); 211 212 // Check saveDirectory is truly a directory 213 if (!dir.isDirectory()) 214 throw new IllegalArgumentException("Not a directory: " + saveDirectory); 215 216 // Check saveDirectory is writable 217 if (!dir.canWrite()) 218 throw new IllegalArgumentException("Not writable: " + saveDirectory); 219 220 // Parse the incoming multipart, storing files in the dir provided, 221 // and populate the meta objects which describe what we found 222 MultipartParser parser = 223 new MultipartParser(request, maxPostSize, true, true, encoding); 224 225 // Some people like to fetch query string parameters from 226 // MultipartRequest, so here we make that possible. Thanks to 227 // Ben Johnson, ben.johnson@merrillcorp.com, for the idea. 228 if (request.getQueryString() != null) { 229 // Let HttpUtils create a name->String[] structure 230 Hashtable queryParameters = 231 HttpUtils.parseQueryString(request.getQueryString()); 232 // For our own use, name it a name->Vector structure 233 Enumeration queryParameterNames = queryParameters.keys(); 234 while (queryParameterNames.hasMoreElements()) { 235 Object paramName = queryParameterNames.nextElement(); 236 String[] values = (String[])queryParameters.get(paramName); 237 Vector newValues = new Vector(); 238 for (int i = 0; i < values.length; i++) { 239 newValues.add(values[i]); 240 } 241 parameters.put(paramName, newValues); 242 } 243 } 244 245 Part part; 246 while ((part = parser.readNextPart()) != null) { 247 String name = part.getName(); 248 if (part.isParam()) { 249 // It's a parameter part, add it to the vector of values 250 ParamPart paramPart = (ParamPart) part; 251 String value = paramPart.getStringValue(); 252 Vector existingValues = (Vector)parameters.get(name); 253 if (existingValues == null) { 254 existingValues = new Vector(); 255 parameters.put(name, existingValues); 256 } 257 existingValues.addElement(value); 258 } 259 else if (part.isFile()) { 260 // It's a file part 261 FilePart filePart = (FilePart) part; 262 String fileName = filePart.getFileName(); 263 if (fileName != null) { 264 filePart.setRenamePolicy(policy); // null policy is OK 265 // The part actually contained a file 266 filePart.writeTo(dir); 267 files.put(name, new UploadedFile(dir.toString(), 268 filePart.getFileName(), 269 fileName, 270 filePart.getContentType())); 271 } 272 else { 273 // The field did not contain a file 274 files.put(name, new UploadedFile(null, null, null, null)); 275 } 276 } 277 } 278 } 279 280 /** 281 * Constructor with an old signature, kept for backward compatibility. 282 * Without this constructor, a servlet compiled against a previous version 283 * of this class (pre 1.4) would have to be recompiled to link with this 284 * version. This constructor supports the linking via the old signature. 285 * Callers must simply be careful to pass in an HttpServletRequest. 286 * 287 */ 288 public MultipartRequest(ServletRequest request, 289 String saveDirectory) throws IOException { 290 this((HttpServletRequest)request, saveDirectory); 291 } 292 293 /** 294 * Constructor with an old signature, kept for backward compatibility. 295 * Without this constructor, a servlet compiled against a previous version 296 * of this class (pre 1.4) would have to be recompiled to link with this 297 * version. This constructor supports the linking via the old signature. 298 * Callers must simply be careful to pass in an HttpServletRequest. 299 * 300 */ 301 public MultipartRequest(ServletRequest request, 302 String saveDirectory, 303 int maxPostSize) throws IOException { 304 this((HttpServletRequest)request, saveDirectory, maxPostSize); 305 } 306 307 /** 308 * Returns the names of all the parameters as an Enumeration of 309 * Strings. It returns an empty Enumeration if there are no parameters. 310 * 311 * @return the names of all the parameters as an Enumeration of Strings. 312 */ 313 public Enumeration getParameterNames() { 314 return parameters.keys(); 315 } 316 317 /** 318 * Returns the names of all the uploaded files as an Enumeration of 319 * Strings. It returns an empty Enumeration if there are no uploaded 320 * files. Each file name is the name specified by the form, not by 321 * the user. 322 * 323 * @return the names of all the uploaded files as an Enumeration of Strings. 324 */ 325 public Enumeration getFileNames() { 326 return files.keys(); 327 } 328 329 /** 330 * Returns the value of the named parameter as a String, or null if 331 * the parameter was not sent or was sent without a value. The value 332 * is guaranteed to be in its normal, decoded form. If the parameter 333 * has multiple values, only the last one is returned (for backward 334 * compatibility). For parameters with multiple values, it's possible 335 * the last "value" may be null. 336 * 337 * @param name the parameter name. 338 * @return the parameter value. 339 */ 340 public String getParameter(String name) { 341 try { 342 Vector values = (Vector)parameters.get(name); 343 if (values == null || values.size() == 0) { 344 return null; 345 } 346 String value = (String)values.elementAt(values.size() - 1); 347 return value; 348 } 349 catch (Exception e) { 350 return null; 351 } 352 } 353 354 /** 355 * Returns the values of the named parameter as a String array, or null if 356 * the parameter was not sent. The array has one entry for each parameter 357 * field sent. If any field was sent without a value that entry is stored 358 * in the array as a null. The values are guaranteed to be in their 359 * normal, decoded form. A single value is returned as a one-element array. 360 * 361 * @param name the parameter name. 362 * @return the parameter values. 363 */ 364 public String[] getParameterValues(String name) { 365 try { 366 Vector values = (Vector)parameters.get(name); 367 if (values == null || values.size() == 0) { 368 return null; 369 } 370 String[] valuesArray = new String[values.size()]; 371 values.copyInto(valuesArray); 372 return valuesArray; 373 } 374 catch (Exception e) { 375 return null; 376 } 377 } 378 379 /** 380 * Returns the filesystem name of the specified file, or null if the 381 * file was not included in the upload. A filesystem name is the name 382 * specified by the user. It is also the name under which the file is 383 * actually saved. 384 * 385 * @param name the file name. 386 * @return the filesystem name of the file. 387 */ 388 public String getFilesystemName(String name) { 389 try { 390 UploadedFile file = (UploadedFile)files.get(name); 391 return file.getFilesystemName(); // may be null 392 } 393 catch (Exception e) { 394 return null; 395 } 396 } 397 398 /** 399 * Returns the original filesystem name of the specified file (before any 400 * renaming policy was applied), or null if the file was not included in 401 * the upload. A filesystem name is the name specified by the user. 402 * 403 * @param name the file name. 404 * @return the original file name of the file. 405 */ 406 public String getOriginalFileName(String name) { 407 try { 408 UploadedFile file = (UploadedFile)files.get(name); 409 return file.getOriginalFileName(); // may be null 410 } 411 catch (Exception e) { 412 return null; 413 } 414 } 415 416 /** 417 * Returns the content type of the specified file (as supplied by the 418 * client browser), or null if the file was not included in the upload. 419 * 420 * @param name the file name. 421 * @return the content type of the file. 422 */ 423 public String getContentType(String name) { 424 try { 425 UploadedFile file = (UploadedFile)files.get(name); 426 return file.getContentType(); // may be null 427 } 428 catch (Exception e) { 429 return null; 430 } 431 } 432 433 /** 434 * Returns a File object for the specified file saved on the server's 435 * filesystem, or null if the file was not included in the upload. 436 * 437 * @param name the file name. 438 * @return a File object for the named file. 439 */ 440 public File getFile(String name) { 441 try { 442 UploadedFile file = (UploadedFile)files.get(name); 443 return file.getFile(); // may be null 444 } 445 catch (Exception e) { 446 return null; 447 } 448 } 449} 450 451 452// A class to hold information about an uploaded file. 453// 454class UploadedFile { 455 456 private String dir; 457 private String filename; 458 private String original; 459 private String type; 460 461 UploadedFile(String dir, String filename, String original, String type) { 462 this.dir = dir; 463 this.filename = filename; 464 this.original = original; 465 this.type = type; 466 } 467 468 public String getContentType() { 469 return type; 470 } 471 472 public String getFilesystemName() { 473 return filename; 474 } 475 476 public String getOriginalFileName() { 477 return original; 478 } 479 480 public File getFile() { 481 if (dir == null || filename == null) { 482 return null; 483 } 484 else { 485 return new File(dir + File.separator + filename); 486 } 487 } 488} 489