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