001: /*
002: * $Id: CommonsMultipartRequestHandler.java 471754 2006-11-06 14:55:09Z husted $
003: *
004: * Licensed to the Apache Software Foundation (ASF) under one
005: * or more contributor license agreements. See the NOTICE file
006: * distributed with this work for additional information
007: * regarding copyright ownership. The ASF licenses this file
008: * to you under the Apache License, Version 2.0 (the
009: * "License"); you may not use this file except in compliance
010: * with the License. You may obtain a copy of the License at
011: *
012: * http://www.apache.org/licenses/LICENSE-2.0
013: *
014: * Unless required by applicable law or agreed to in writing,
015: * software distributed under the License is distributed on an
016: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017: * KIND, either express or implied. See the License for the
018: * specific language governing permissions and limitations
019: * under the License.
020: */
021: package org.apache.struts.upload;
022:
023: import org.apache.commons.fileupload.DiskFileUpload;
024: import org.apache.commons.fileupload.disk.DiskFileItem;
025: import org.apache.commons.fileupload.FileItem;
026: import org.apache.commons.fileupload.FileUploadException;
027: import org.apache.commons.logging.Log;
028: import org.apache.commons.logging.LogFactory;
029: import org.apache.struts.Globals;
030: import org.apache.struts.action.ActionMapping;
031: import org.apache.struts.action.ActionServlet;
032: import org.apache.struts.config.ModuleConfig;
033:
034: import javax.servlet.ServletContext;
035: import javax.servlet.ServletException;
036: import javax.servlet.http.HttpServletRequest;
037:
038: import java.io.File;
039: import java.io.FileNotFoundException;
040: import java.io.IOException;
041: import java.io.InputStream;
042: import java.io.Serializable;
043:
044: import java.util.Hashtable;
045: import java.util.Iterator;
046: import java.util.List;
047:
048: /**
049: * <p> This class implements the <code>MultipartRequestHandler</code>
050: * interface by providing a wrapper around the Jakarta Commons FileUpload
051: * library. </p>
052: *
053: * @version $Rev: 471754 $ $Date: 2006-11-06 08:55:09 -0600 (Mon, 06 Nov 2006) $
054: * @since Struts 1.1
055: */
056: public class CommonsMultipartRequestHandler implements
057: MultipartRequestHandler {
058: // ----------------------------------------------------- Manifest Constants
059:
060: /**
061: * <p> The default value for the maximum allowable size, in bytes, of an
062: * uploaded file. The value is equivalent to 250MB. </p>
063: */
064: public static final long DEFAULT_SIZE_MAX = 250 * 1024 * 1024;
065:
066: /**
067: * <p> The default value for the threshold which determines whether an
068: * uploaded file will be written to disk or cached in memory. The value is
069: * equivalent to 250KB. </p>
070: */
071: public static final int DEFAULT_SIZE_THRESHOLD = 256 * 1024;
072:
073: // ----------------------------------------------------- Instance Variables
074:
075: /**
076: * <p> Commons Logging instance. </p>
077: */
078: protected static Log log = LogFactory
079: .getLog(CommonsMultipartRequestHandler.class);
080:
081: /**
082: * <p> The combined text and file request parameters. </p>
083: */
084: private Hashtable elementsAll;
085:
086: /**
087: * <p> The file request parameters. </p>
088: */
089: private Hashtable elementsFile;
090:
091: /**
092: * <p> The text request parameters. </p>
093: */
094: private Hashtable elementsText;
095:
096: /**
097: * <p> The action mapping with which this handler is associated. </p>
098: */
099: private ActionMapping mapping;
100:
101: /**
102: * <p> The servlet with which this handler is associated. </p>
103: */
104: private ActionServlet servlet;
105:
106: // ---------------------------------------- MultipartRequestHandler Methods
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: // Get the app config for the current request.
158: ModuleConfig ac = (ModuleConfig) request
159: .getAttribute(Globals.MODULE_KEY);
160:
161: // Create and configure a DIskFileUpload instance.
162: DiskFileUpload upload = new DiskFileUpload();
163:
164: // The following line is to support an "EncodingFilter"
165: // see http://issues.apache.org/bugzilla/show_bug.cgi?id=23255
166: upload.setHeaderEncoding(request.getCharacterEncoding());
167:
168: // Set the maximum size before a FileUploadException will be thrown.
169: upload.setSizeMax(getSizeMax(ac));
170:
171: // Set the maximum size that will be stored in memory.
172: upload.setSizeThreshold((int) getSizeThreshold(ac));
173:
174: // Set the the location for saving data on disk.
175: upload.setRepositoryPath(getRepositoryPath(ac));
176:
177: // Create the hash tables to be populated.
178: elementsText = new Hashtable();
179: elementsFile = new Hashtable();
180: elementsAll = new Hashtable();
181:
182: // Parse the request into file items.
183: List items = null;
184:
185: try {
186: items = upload.parseRequest(request);
187: } catch (DiskFileUpload.SizeLimitExceededException e) {
188: // Special handling for uploads that are too big.
189: request
190: .setAttribute(
191: MultipartRequestHandler.ATTRIBUTE_MAX_LENGTH_EXCEEDED,
192: Boolean.TRUE);
193:
194: return;
195: } catch (FileUploadException e) {
196: log.error("Failed to parse multipart request", e);
197: throw new ServletException(e);
198: }
199:
200: // Partition the items into form fields and files.
201: Iterator iter = items.iterator();
202:
203: while (iter.hasNext()) {
204: FileItem item = (FileItem) iter.next();
205:
206: if (item.isFormField()) {
207: addTextParameter(request, item);
208: } else {
209: addFileParameter(item);
210: }
211: }
212: }
213:
214: /**
215: * <p> Returns a hash table containing the text (that is, non-file)
216: * request parameters. </p>
217: *
218: * @return The text request parameters.
219: */
220: public Hashtable getTextElements() {
221: return this .elementsText;
222: }
223:
224: /**
225: * <p> Returns a hash table containing the file (that is, non-text)
226: * request parameters. </p>
227: *
228: * @return The file request parameters.
229: */
230: public Hashtable getFileElements() {
231: return this .elementsFile;
232: }
233:
234: /**
235: * <p> Returns a hash table containing both text and file request
236: * parameters. </p>
237: *
238: * @return The text and file request parameters.
239: */
240: public Hashtable getAllElements() {
241: return this .elementsAll;
242: }
243:
244: /**
245: * <p> Cleans up when a problem occurs during request processing. </p>
246: */
247: public void rollback() {
248: Iterator iter = elementsFile.values().iterator();
249:
250: while (iter.hasNext()) {
251: FormFile formFile = (FormFile) iter.next();
252:
253: formFile.destroy();
254: }
255: }
256:
257: /**
258: * <p> Cleans up at the end of a request. </p>
259: */
260: public void finish() {
261: rollback();
262: }
263:
264: // -------------------------------------------------------- Support Methods
265:
266: /**
267: * <p> Returns the maximum allowable size, in bytes, of an uploaded file.
268: * The value is obtained from the current module's controller
269: * configuration. </p>
270: *
271: * @param mc The current module's configuration.
272: * @return The maximum allowable file size, in bytes.
273: */
274: protected long getSizeMax(ModuleConfig mc) {
275: return convertSizeToBytes(mc.getControllerConfig()
276: .getMaxFileSize(), DEFAULT_SIZE_MAX);
277: }
278:
279: /**
280: * <p> Returns the size threshold which determines whether an uploaded
281: * file will be written to disk or cached in memory. </p>
282: *
283: * @param mc The current module's configuration.
284: * @return The size threshold, in bytes.
285: */
286: protected long getSizeThreshold(ModuleConfig mc) {
287: return convertSizeToBytes(mc.getControllerConfig()
288: .getMemFileSize(), DEFAULT_SIZE_THRESHOLD);
289: }
290:
291: /**
292: * <p> Converts a size value from a string representation to its numeric
293: * value. The string must be of the form nnnm, where nnn is an arbitrary
294: * decimal value, and m is a multiplier. The multiplier must be one of
295: * 'K', 'M' and 'G', representing kilobytes, megabytes and gigabytes
296: * respectively. </p><p> If the size value cannot be converted, for
297: * example due to invalid syntax, the supplied default is returned
298: * instead. </p>
299: *
300: * @param sizeString The string representation of the size to be
301: * converted.
302: * @param defaultSize The value to be returned if the string is invalid.
303: * @return The actual size in bytes.
304: */
305: protected long convertSizeToBytes(String sizeString,
306: long defaultSize) {
307: int multiplier = 1;
308:
309: if (sizeString.endsWith("K")) {
310: multiplier = 1024;
311: } else if (sizeString.endsWith("M")) {
312: multiplier = 1024 * 1024;
313: } else if (sizeString.endsWith("G")) {
314: multiplier = 1024 * 1024 * 1024;
315: }
316:
317: if (multiplier != 1) {
318: sizeString = sizeString.substring(0,
319: sizeString.length() - 1);
320: }
321:
322: long size = 0;
323:
324: try {
325: size = Long.parseLong(sizeString);
326: } catch (NumberFormatException nfe) {
327: log.warn("Invalid format for file size ('" + sizeString
328: + "'). Using default.");
329: size = defaultSize;
330: multiplier = 1;
331: }
332:
333: return (size * multiplier);
334: }
335:
336: /**
337: * <p> Returns the path to the temporary directory to be used for uploaded
338: * files which are written to disk. The directory used is determined from
339: * the first of the following to be non-empty. <ol> <li>A temp dir
340: * explicitly defined either using the <code>tempDir</code> servlet init
341: * param, or the <code>tempDir</code> attribute of the <controller>
342: * element in the Struts config file.</li> <li>The container-specified
343: * temp dir, obtained from the <code>javax.servlet.context.tempdir</code>
344: * servlet context attribute.</li> <li>The temp dir specified by the
345: * <code>java.io.tmpdir</code> system property.</li> (/ol> </p>
346: *
347: * @param mc The module config instance for which the path should be
348: * determined.
349: * @return The path to the directory to be used to store uploaded files.
350: */
351: protected String getRepositoryPath(ModuleConfig mc) {
352: // First, look for an explicitly defined temp dir.
353: String tempDir = mc.getControllerConfig().getTempDir();
354:
355: // If none, look for a container specified temp dir.
356: if ((tempDir == null) || (tempDir.length() == 0)) {
357: if (servlet != null) {
358: ServletContext context = servlet.getServletContext();
359: File tempDirFile = (File) context
360: .getAttribute("javax.servlet.context.tempdir");
361:
362: tempDir = tempDirFile.getAbsolutePath();
363: }
364:
365: // If none, pick up the system temp dir.
366: if ((tempDir == null) || (tempDir.length() == 0)) {
367: tempDir = System.getProperty("java.io.tmpdir");
368: }
369: }
370:
371: if (log.isTraceEnabled()) {
372: log.trace("File upload temp dir: " + tempDir);
373: }
374:
375: return tempDir;
376: }
377:
378: /**
379: * <p> Adds a regular text parameter to the set of text parameters for
380: * this request and also to the list of all parameters. Handles the case
381: * of multiple values for the same parameter by using an array for the
382: * parameter value. </p>
383: *
384: * @param request The request in which the parameter was specified.
385: * @param item The file item for the parameter to add.
386: */
387: protected void addTextParameter(HttpServletRequest request,
388: FileItem item) {
389: String name = item.getFieldName();
390: String value = null;
391: boolean haveValue = false;
392: String encoding = null;
393:
394: if (item instanceof DiskFileItem) {
395: encoding = ((DiskFileItem) item).getCharSet();
396: if (log.isDebugEnabled()) {
397: log.debug("DiskFileItem.getCharSet=[" + encoding + "]");
398: }
399: }
400:
401: if (encoding == null) {
402: encoding = request.getCharacterEncoding();
403: if (log.isDebugEnabled()) {
404: log.debug("request.getCharacterEncoding=[" + encoding
405: + "]");
406: }
407: }
408:
409: if (encoding != null) {
410: try {
411: value = item.getString(encoding);
412: haveValue = true;
413: } catch (Exception e) {
414: // Handled below, since haveValue is false.
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: elementsFile.put(item.getFieldName(), formFile);
459: elementsAll.put(item.getFieldName(), formFile);
460: }
461:
462: // ---------------------------------------------------------- Inner Classes
463:
464: /**
465: * <p> This class implements the Struts <code>FormFile</code> interface by
466: * wrapping the Commons FileUpload <code>FileItem</code> interface. This
467: * implementation is <i>read-only</i>; any attempt to modify an instance
468: * of this class will result in an <code>UnsupportedOperationException</code>.
469: * </p>
470: */
471: static class CommonsFormFile implements FormFile, Serializable {
472: /**
473: * <p> The <code>FileItem</code> instance wrapped by this object.
474: * </p>
475: */
476: FileItem fileItem;
477:
478: /**
479: * Constructs an instance of this class which wraps the supplied file
480: * item. </p>
481: *
482: * @param fileItem The Commons file item to be wrapped.
483: */
484: public CommonsFormFile(FileItem fileItem) {
485: this .fileItem = fileItem;
486: }
487:
488: /**
489: * <p> Returns the content type for this file. </p>
490: *
491: * @return A String representing content type.
492: */
493: public String getContentType() {
494: return fileItem.getContentType();
495: }
496:
497: /**
498: * <p> Sets the content type for this file. <p> NOTE: This method is
499: * not supported in this implementation. </p>
500: *
501: * @param contentType A string representing the content type.
502: */
503: public void setContentType(String contentType) {
504: throw new UnsupportedOperationException(
505: "The setContentType() method is not supported.");
506: }
507:
508: /**
509: * <p> Returns the size, in bytes, of this file. </p>
510: *
511: * @return The size of the file, in bytes.
512: */
513: public int getFileSize() {
514: return (int) fileItem.getSize();
515: }
516:
517: /**
518: * <p> Sets the size, in bytes, for this file. <p> NOTE: This method
519: * is not supported in this implementation. </p>
520: *
521: * @param filesize The size of the file, in bytes.
522: */
523: public void setFileSize(int filesize) {
524: throw new UnsupportedOperationException(
525: "The setFileSize() method is not supported.");
526: }
527:
528: /**
529: * <p> Returns the (client-side) file name for this file. </p>
530: *
531: * @return The client-size file name.
532: */
533: public String getFileName() {
534: return getBaseFileName(fileItem.getName());
535: }
536:
537: /**
538: * <p> Sets the (client-side) file name for this file. <p> NOTE: This
539: * method is not supported in this implementation. </p>
540: *
541: * @param fileName The client-side name for the file.
542: */
543: public void setFileName(String fileName) {
544: throw new UnsupportedOperationException(
545: "The setFileName() method is not supported.");
546: }
547:
548: /**
549: * <p> Returns the data for this file as a byte array. Note that this
550: * may result in excessive memory usage for large uploads. The use of
551: * the {@link #getInputStream() getInputStream} method is encouraged
552: * as an alternative. </p>
553: *
554: * @return An array of bytes representing the data contained in this
555: * form file.
556: * @throws FileNotFoundException If some sort of file representation
557: * cannot be found for the FormFile
558: * @throws IOException If there is some sort of IOException
559: */
560: public byte[] getFileData() throws FileNotFoundException,
561: IOException {
562: return fileItem.get();
563: }
564:
565: /**
566: * <p> Get an InputStream that represents this file. This is the
567: * preferred method of getting file data. </p>
568: *
569: * @throws FileNotFoundException If some sort of file representation
570: * cannot be found for the FormFile
571: * @throws IOException If there is some sort of IOException
572: */
573: public InputStream getInputStream()
574: throws FileNotFoundException, IOException {
575: return fileItem.getInputStream();
576: }
577:
578: /**
579: * <p> Destroy all content for this form file. Implementations should
580: * remove any temporary files or any temporary file data stored
581: * somewhere </p>
582: */
583: public void destroy() {
584: fileItem.delete();
585: }
586:
587: /**
588: * <p> Returns the base file name from the supplied file path. On the
589: * surface, this would appear to be a trivial task. Apparently,
590: * however, some Linux JDKs do not implement <code>File.getName()</code>
591: * correctly for Windows paths, so we attempt to take care of that
592: * here. </p>
593: *
594: * @param filePath The full path to the file.
595: * @return The base file name, from the end of the path.
596: */
597: protected String getBaseFileName(String filePath) {
598: // First, ask the JDK for the base file name.
599: String fileName = new File(filePath).getName();
600:
601: // Now check for a Windows file name parsed incorrectly.
602: int colonIndex = fileName.indexOf(":");
603:
604: if (colonIndex == -1) {
605: // Check for a Windows SMB file path.
606: colonIndex = fileName.indexOf("\\\\");
607: }
608:
609: int backslashIndex = fileName.lastIndexOf("\\");
610:
611: if ((colonIndex > -1) && (backslashIndex > -1)) {
612: // Consider this filename to be a full Windows path, and parse it
613: // accordingly to retrieve just the base file name.
614: fileName = fileName.substring(backslashIndex + 1);
615: }
616:
617: return fileName;
618: }
619:
620: /**
621: * <p> Returns the (client-side) file name for this file. </p>
622: *
623: * @return The client-size file name.
624: */
625: public String toString() {
626: return getFileName();
627: }
628: }
629: }
|