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