1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.apache.struts.upload;
19
20 import org.apache.commons.fileupload.DiskFileUpload;
21 import org.apache.commons.fileupload.disk.DiskFileItem;
22 import org.apache.commons.fileupload.FileItem;
23 import org.apache.commons.fileupload.FileUploadException;
24 import org.apache.commons.logging.Log;
25 import org.apache.commons.logging.LogFactory;
26 import org.apache.struts.Globals;
27 import org.apache.struts.action.ActionMapping;
28 import org.apache.struts.action.ActionServlet;
29 import org.apache.struts.config.ModuleConfig;
30
31 import javax.servlet.ServletContext;
32 import javax.servlet.ServletException;
33 import javax.servlet.http.HttpServletRequest;
34
35 import java.io.File;
36 import java.io.FileNotFoundException;
37 import java.io.IOException;
38 import java.io.InputStream;
39 import java.io.Serializable;
40
41 import java.util.Hashtable;
42 import java.util.Iterator;
43 import java.util.List;
44
45 /***
46 * <p> This class implements the <code>MultipartRequestHandler</code>
47 * interface by providing a wrapper around the Jakarta Commons FileUpload
48 * library. </p>
49 *
50 * @version $Rev: 421119 $ $Date: 2006-07-11 21:49:11 -0700 (Tue, 11 Jul 2006) $
51 * @since Struts 1.1
52 */
53 public class CommonsMultipartRequestHandler implements MultipartRequestHandler {
54
55
56 /***
57 * <p> The default value for the maximum allowable size, in bytes, of an
58 * uploaded file. The value is equivalent to 250MB. </p>
59 */
60 public static final long DEFAULT_SIZE_MAX = 250 * 1024 * 1024;
61
62 /***
63 * <p> The default value for the threshold which determines whether an
64 * uploaded file will be written to disk or cached in memory. The value is
65 * equivalent to 250KB. </p>
66 */
67 public static final int DEFAULT_SIZE_THRESHOLD = 256 * 1024;
68
69
70
71 /***
72 * <p> Commons Logging instance. </p>
73 */
74 protected static Log log =
75 LogFactory.getLog(CommonsMultipartRequestHandler.class);
76
77 /***
78 * <p> The combined text and file request parameters. </p>
79 */
80 private Hashtable elementsAll;
81
82 /***
83 * <p> The file request parameters. </p>
84 */
85 private Hashtable elementsFile;
86
87 /***
88 * <p> The text request parameters. </p>
89 */
90 private Hashtable elementsText;
91
92 /***
93 * <p> The action mapping with which this handler is associated. </p>
94 */
95 private ActionMapping mapping;
96
97 /***
98 * <p> The servlet with which this handler is associated. </p>
99 */
100 private ActionServlet servlet;
101
102
103
104 /***
105 * <p> Retrieves the servlet with which this handler is associated. </p>
106 *
107 * @return The associated servlet.
108 */
109 public ActionServlet getServlet() {
110 return this.servlet;
111 }
112
113 /***
114 * <p> Sets the servlet with which this handler is associated. </p>
115 *
116 * @param servlet The associated servlet.
117 */
118 public void setServlet(ActionServlet servlet) {
119 this.servlet = servlet;
120 }
121
122 /***
123 * <p> Retrieves the action mapping with which this handler is associated.
124 * </p>
125 *
126 * @return The associated action mapping.
127 */
128 public ActionMapping getMapping() {
129 return this.mapping;
130 }
131
132 /***
133 * <p> Sets the action mapping with which this handler is associated.
134 * </p>
135 *
136 * @param mapping The associated action mapping.
137 */
138 public void setMapping(ActionMapping mapping) {
139 this.mapping = mapping;
140 }
141
142 /***
143 * <p> Parses the input stream and partitions the parsed items into a set
144 * of form fields and a set of file items. In the process, the parsed
145 * items are translated from Commons FileUpload <code>FileItem</code>
146 * instances to Struts <code>FormFile</code> instances. </p>
147 *
148 * @param request The multipart request to be processed.
149 * @throws ServletException if an unrecoverable error occurs.
150 */
151 public void handleRequest(HttpServletRequest request)
152 throws ServletException {
153
154 ModuleConfig ac =
155 (ModuleConfig) request.getAttribute(Globals.MODULE_KEY);
156
157
158 DiskFileUpload upload = new DiskFileUpload();
159
160
161
162 upload.setHeaderEncoding(request.getCharacterEncoding());
163
164
165 upload.setSizeMax(getSizeMax(ac));
166
167
168 upload.setSizeThreshold((int) getSizeThreshold(ac));
169
170
171 upload.setRepositoryPath(getRepositoryPath(ac));
172
173
174 elementsText = new Hashtable();
175 elementsFile = new Hashtable();
176 elementsAll = new Hashtable();
177
178
179 List items = null;
180
181 try {
182 items = upload.parseRequest(request);
183 } catch (DiskFileUpload.SizeLimitExceededException e) {
184
185 request.setAttribute(MultipartRequestHandler.ATTRIBUTE_MAX_LENGTH_EXCEEDED,
186 Boolean.TRUE);
187
188 return;
189 } catch (FileUploadException e) {
190 log.error("Failed to parse multipart request", e);
191 throw new ServletException(e);
192 }
193
194
195 Iterator iter = items.iterator();
196
197 while (iter.hasNext()) {
198 FileItem item = (FileItem) iter.next();
199
200 if (item.isFormField()) {
201 addTextParameter(request, item);
202 } else {
203 addFileParameter(item);
204 }
205 }
206 }
207
208 /***
209 * <p> Returns a hash table containing the text (that is, non-file)
210 * request parameters. </p>
211 *
212 * @return The text request parameters.
213 */
214 public Hashtable getTextElements() {
215 return this.elementsText;
216 }
217
218 /***
219 * <p> Returns a hash table containing the file (that is, non-text)
220 * request parameters. </p>
221 *
222 * @return The file request parameters.
223 */
224 public Hashtable getFileElements() {
225 return this.elementsFile;
226 }
227
228 /***
229 * <p> Returns a hash table containing both text and file request
230 * parameters. </p>
231 *
232 * @return The text and file request parameters.
233 */
234 public Hashtable getAllElements() {
235 return this.elementsAll;
236 }
237
238 /***
239 * <p> Cleans up when a problem occurs during request processing. </p>
240 */
241 public void rollback() {
242 Iterator iter = elementsFile.values().iterator();
243
244 while (iter.hasNext()) {
245 FormFile formFile = (FormFile) iter.next();
246
247 formFile.destroy();
248 }
249 }
250
251 /***
252 * <p> Cleans up at the end of a request. </p>
253 */
254 public void finish() {
255 rollback();
256 }
257
258
259
260 /***
261 * <p> Returns the maximum allowable size, in bytes, of an uploaded file.
262 * The value is obtained from the current module's controller
263 * configuration. </p>
264 *
265 * @param mc The current module's configuration.
266 * @return The maximum allowable file size, in bytes.
267 */
268 protected long getSizeMax(ModuleConfig mc) {
269 return convertSizeToBytes(mc.getControllerConfig().getMaxFileSize(),
270 DEFAULT_SIZE_MAX);
271 }
272
273 /***
274 * <p> Returns the size threshold which determines whether an uploaded
275 * file will be written to disk or cached in memory. </p>
276 *
277 * @param mc The current module's configuration.
278 * @return The size threshold, in bytes.
279 */
280 protected long getSizeThreshold(ModuleConfig mc) {
281 return convertSizeToBytes(mc.getControllerConfig().getMemFileSize(),
282 DEFAULT_SIZE_THRESHOLD);
283 }
284
285 /***
286 * <p> Converts a size value from a string representation to its numeric
287 * value. The string must be of the form nnnm, where nnn is an arbitrary
288 * decimal value, and m is a multiplier. The multiplier must be one of
289 * 'K', 'M' and 'G', representing kilobytes, megabytes and gigabytes
290 * respectively. </p><p> If the size value cannot be converted, for
291 * example due to invalid syntax, the supplied default is returned
292 * instead. </p>
293 *
294 * @param sizeString The string representation of the size to be
295 * converted.
296 * @param defaultSize The value to be returned if the string is invalid.
297 * @return The actual size in bytes.
298 */
299 protected long convertSizeToBytes(String sizeString, long defaultSize) {
300 int multiplier = 1;
301
302 if (sizeString.endsWith("K")) {
303 multiplier = 1024;
304 } else if (sizeString.endsWith("M")) {
305 multiplier = 1024 * 1024;
306 } else if (sizeString.endsWith("G")) {
307 multiplier = 1024 * 1024 * 1024;
308 }
309
310 if (multiplier != 1) {
311 sizeString = sizeString.substring(0, sizeString.length() - 1);
312 }
313
314 long size = 0;
315
316 try {
317 size = Long.parseLong(sizeString);
318 } catch (NumberFormatException nfe) {
319 log.warn("Invalid format for file size ('" + sizeString
320 + "'). Using default.");
321 size = defaultSize;
322 multiplier = 1;
323 }
324
325 return (size * multiplier);
326 }
327
328 /***
329 * <p> Returns the path to the temporary directory to be used for uploaded
330 * files which are written to disk. The directory used is determined from
331 * the first of the following to be non-empty. <ol> <li>A temp dir
332 * explicitly defined either using the <code>tempDir</code> servlet init
333 * param, or the <code>tempDir</code> attribute of the <controller>
334 * element in the Struts config file.</li> <li>The container-specified
335 * temp dir, obtained from the <code>javax.servlet.context.tempdir</code>
336 * servlet context attribute.</li> <li>The temp dir specified by the
337 * <code>java.io.tmpdir</code> system property.</li> (/ol> </p>
338 *
339 * @param mc The module config instance for which the path should be
340 * determined.
341 * @return The path to the directory to be used to store uploaded files.
342 */
343 protected String getRepositoryPath(ModuleConfig mc) {
344
345 String tempDir = mc.getControllerConfig().getTempDir();
346
347
348 if ((tempDir == null) || (tempDir.length() == 0)) {
349 if (servlet != null) {
350 ServletContext context = servlet.getServletContext();
351 File tempDirFile =
352 (File) context.getAttribute("javax.servlet.context.tempdir");
353
354 tempDir = tempDirFile.getAbsolutePath();
355 }
356
357
358 if ((tempDir == null) || (tempDir.length() == 0)) {
359 tempDir = System.getProperty("java.io.tmpdir");
360 }
361 }
362
363 if (log.isTraceEnabled()) {
364 log.trace("File upload temp dir: " + tempDir);
365 }
366
367 return tempDir;
368 }
369
370 /***
371 * <p> Adds a regular text parameter to the set of text parameters for
372 * this request and also to the list of all parameters. Handles the case
373 * of multiple values for the same parameter by using an array for the
374 * parameter value. </p>
375 *
376 * @param request The request in which the parameter was specified.
377 * @param item The file item for the parameter to add.
378 */
379 protected void addTextParameter(HttpServletRequest request, FileItem item) {
380 String name = item.getFieldName();
381 String value = null;
382 boolean haveValue = false;
383 String encoding = null;
384
385 if (item instanceof DiskFileItem) {
386 encoding = ((DiskFileItem)item).getCharSet();
387 if (log.isDebugEnabled()) {
388 log.debug("DiskFileItem.getCharSet=[" + encoding + "]");
389 }
390 }
391
392 if (encoding == null) {
393 encoding = request.getCharacterEncoding();
394 if (log.isDebugEnabled()) {
395 log.debug("request.getCharacterEncoding=[" + encoding + "]");
396 }
397 }
398
399 if (encoding != null) {
400 try {
401 value = item.getString(encoding);
402 haveValue = true;
403 } catch (Exception e) {
404
405 }
406 }
407
408 if (!haveValue) {
409 try {
410 value = item.getString("ISO-8859-1");
411 } catch (java.io.UnsupportedEncodingException uee) {
412 value = item.getString();
413 }
414
415 haveValue = true;
416 }
417
418 if (request instanceof MultipartRequestWrapper) {
419 MultipartRequestWrapper wrapper = (MultipartRequestWrapper) request;
420
421 wrapper.setParameter(name, value);
422 }
423
424 String[] oldArray = (String[]) elementsText.get(name);
425 String[] newArray;
426
427 if (oldArray != null) {
428 newArray = new String[oldArray.length + 1];
429 System.arraycopy(oldArray, 0, newArray, 0, oldArray.length);
430 newArray[oldArray.length] = value;
431 } else {
432 newArray = new String[] { value };
433 }
434
435 elementsText.put(name, newArray);
436 elementsAll.put(name, newArray);
437 }
438
439 /***
440 * <p> Adds a file parameter to the set of file parameters for this
441 * request and also to the list of all parameters. </p>
442 *
443 * @param item The file item for the parameter to add.
444 */
445 protected void addFileParameter(FileItem item) {
446 FormFile formFile = new CommonsFormFile(item);
447
448 elementsFile.put(item.getFieldName(), formFile);
449 elementsAll.put(item.getFieldName(), formFile);
450 }
451
452
453
454 /***
455 * <p> This class implements the Struts <code>FormFile</code> interface by
456 * wrapping the Commons FileUpload <code>FileItem</code> interface. This
457 * implementation is <i>read-only</i>; any attempt to modify an instance
458 * of this class will result in an <code>UnsupportedOperationException</code>.
459 * </p>
460 */
461 static class CommonsFormFile implements FormFile, Serializable {
462 /***
463 * <p> The <code>FileItem</code> instance wrapped by this object.
464 * </p>
465 */
466 FileItem fileItem;
467
468 /***
469 * Constructs an instance of this class which wraps the supplied file
470 * item. </p>
471 *
472 * @param fileItem The Commons file item to be wrapped.
473 */
474 public CommonsFormFile(FileItem fileItem) {
475 this.fileItem = fileItem;
476 }
477
478 /***
479 * <p> Returns the content type for this file. </p>
480 *
481 * @return A String representing content type.
482 */
483 public String getContentType() {
484 return fileItem.getContentType();
485 }
486
487 /***
488 * <p> Sets the content type for this file. <p> NOTE: This method is
489 * not supported in this implementation. </p>
490 *
491 * @param contentType A string representing the content type.
492 */
493 public void setContentType(String contentType) {
494 throw new UnsupportedOperationException(
495 "The setContentType() method is not supported.");
496 }
497
498 /***
499 * <p> Returns the size, in bytes, of this file. </p>
500 *
501 * @return The size of the file, in bytes.
502 */
503 public int getFileSize() {
504 return (int) fileItem.getSize();
505 }
506
507 /***
508 * <p> Sets the size, in bytes, for this file. <p> NOTE: This method
509 * is not supported in this implementation. </p>
510 *
511 * @param filesize The size of the file, in bytes.
512 */
513 public void setFileSize(int filesize) {
514 throw new UnsupportedOperationException(
515 "The setFileSize() method is not supported.");
516 }
517
518 /***
519 * <p> Returns the (client-side) file name for this file. </p>
520 *
521 * @return The client-size file name.
522 */
523 public String getFileName() {
524 return getBaseFileName(fileItem.getName());
525 }
526
527 /***
528 * <p> Sets the (client-side) file name for this file. <p> NOTE: This
529 * method is not supported in this implementation. </p>
530 *
531 * @param fileName The client-side name for the file.
532 */
533 public void setFileName(String fileName) {
534 throw new UnsupportedOperationException(
535 "The setFileName() method is not supported.");
536 }
537
538 /***
539 * <p> Returns the data for this file as a byte array. Note that this
540 * may result in excessive memory usage for large uploads. The use of
541 * the {@link #getInputStream() getInputStream} method is encouraged
542 * as an alternative. </p>
543 *
544 * @return An array of bytes representing the data contained in this
545 * form file.
546 * @throws FileNotFoundException If some sort of file representation
547 * cannot be found for the FormFile
548 * @throws IOException If there is some sort of IOException
549 */
550 public byte[] getFileData()
551 throws FileNotFoundException, IOException {
552 return fileItem.get();
553 }
554
555 /***
556 * <p> Get an InputStream that represents this file. This is the
557 * preferred method of getting file data. </p>
558 *
559 * @throws FileNotFoundException If some sort of file representation
560 * cannot be found for the FormFile
561 * @throws IOException If there is some sort of IOException
562 */
563 public InputStream getInputStream()
564 throws FileNotFoundException, IOException {
565 return fileItem.getInputStream();
566 }
567
568 /***
569 * <p> Destroy all content for this form file. Implementations should
570 * remove any temporary files or any temporary file data stored
571 * somewhere </p>
572 */
573 public void destroy() {
574 fileItem.delete();
575 }
576
577 /***
578 * <p> Returns the base file name from the supplied file path. On the
579 * surface, this would appear to be a trivial task. Apparently,
580 * however, some Linux JDKs do not implement <code>File.getName()</code>
581 * correctly for Windows paths, so we attempt to take care of that
582 * here. </p>
583 *
584 * @param filePath The full path to the file.
585 * @return The base file name, from the end of the path.
586 */
587 protected String getBaseFileName(String filePath) {
588
589 String fileName = new File(filePath).getName();
590
591
592 int colonIndex = fileName.indexOf(":");
593
594 if (colonIndex == -1) {
595
596 colonIndex = fileName.indexOf("////");
597 }
598
599 int backslashIndex = fileName.lastIndexOf("//");
600
601 if ((colonIndex > -1) && (backslashIndex > -1)) {
602
603
604 fileName = fileName.substring(backslashIndex + 1);
605 }
606
607 return fileName;
608 }
609
610 /***
611 * <p> Returns the (client-side) file name for this file. </p>
612 *
613 * @return The client-size file name.
614 */
615 public String toString() {
616 return getFileName();
617 }
618 }
619 }