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