View Javadoc

    1   /*
    2    * $Id: FileUploadInterceptor.java 495094 2007-01-11 02:51:40Z mrdon $
    3    *
    4    * Licensed to the Apache Software Foundation (ASF) under one
    5    * or more contributor license agreements.  See the NOTICE file
    6    * distributed with this work for additional information
    7    * regarding copyright ownership.  The ASF licenses this file
    8    * to you under the Apache License, Version 2.0 (the
    9    * "License"); you may not use this file except in compliance
    10   * with the License.  You may obtain a copy of the License at
    11   *
    12   *  http://www.apache.org/licenses/LICENSE-2.0
    13   *
    14   * Unless required by applicable law or agreed to in writing,
    15   * software distributed under the License is distributed on an
    16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    17   * KIND, either express or implied.  See the License for the
    18   * specific language governing permissions and limitations
    19   * under the License.
    20   */
    21  package org.apache.struts2.interceptor;
    22
    23  import java.io.File;
    24  import java.util.Collection;
    25  import java.util.Collections;
    26  import java.util.Enumeration;
    27  import java.util.HashSet;
    28  import java.util.Iterator;
    29  import java.util.Locale;
    30  import java.util.Map;
    31  import java.util.Set;
    32  import java.util.StringTokenizer;
    33
    34  import javax.servlet.http.HttpServletRequest;
    35
    36  import org.apache.commons.logging.Log;
    37  import org.apache.commons.logging.LogFactory;
    38  import org.apache.struts2.ServletActionContext;
    39  import org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper;
    40
    41  import com.opensymphony.xwork2.ActionContext;
    42  import com.opensymphony.xwork2.ActionInvocation;
    43  import com.opensymphony.xwork2.ActionProxy;
    44  import com.opensymphony.xwork2.ValidationAware;
    45  import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
    46  import com.opensymphony.xwork2.util.LocalizedTextUtil;
    47
    48  /***
    49   * <!-- START SNIPPET: description -->
    50   *
    51   * Interceptor that is based off of {@link MultiPartRequestWrapper}, which is automatically applied for any request that
    52   * includes a file. It adds the following parameters, where [File Name] is the name given to the file uploaded by the
    53   * HTML form:
    54   *
    55   * <ul>
    56   *
    57   * <li>[File Name] : File - the actual File</li>
    58   *
    59   * <li>[File Name]ContentType : String - the content type of the file</li>
    60   *
    61   * <li>[File Name]FileName : String - the actual name of the file uploaded (not the HTML name)</li>
    62   *
    63   * </ul>
    64   *
    65   * <p/> You can get access to these files by merely providing setters in your action that correspond to any of the three
    66   * patterns above, such as setDocument(File document), setDocumentContentType(String contentType), etc.
    67   * <br/>See the example code section.
    68   *
    69   * <p/> This interceptor will add several field errors, assuming that the action implements {@link ValidationAware}.
    70   * These error messages are based on several i18n values stored in struts-messages.properties, a default i18n file
    71   * processed for all i18n requests. You can override the text of these messages by providing text for the following
    72   * keys:
    73   *
    74   * <ul>
    75   *
    76   * <li>struts.messages.error.uploading - a general error that occurs when the file could not be uploaded</li>
    77   *
    78   * <li>struts.messages.error.file.too.large - occurs when the uploaded file is too large</li>
    79   *
    80   * <li>struts.messages.error.content.type.not.allowed - occurs when the uploaded file does not match the expected
    81   * content types specified</li>
    82   *
    83   * </ul>
    84   *
    85   * <!-- END SNIPPET: description -->
    86   *
    87   * <p/> <u>Interceptor parameters:</u>
    88   *
    89   * <!-- START SNIPPET: parameters -->
    90   *
    91   * <ul>
    92   *
    93   * <li>maximumSize (optional) - the maximum size (in bytes) that the interceptor will allow a file reference to be set
    94   * on the action. Note, this is <b>not</b> related to the various properties found in struts.properties.
    95   * Default to approximately 2MB.</li>
    96   *
    97   * <li>allowedTypes (optional) - a comma separated list of content types (ie: text/html) that the interceptor will allow
    98   * a file reference to be set on the action. If none is specified allow all types to be uploaded.</li>
    99   *
    100  * </ul>
    101  *
    102  * <!-- END SNIPPET: parameters -->
    103  *
    104  * <p/> <u>Extending the interceptor:</u>
    105  *
    106  * <p/>
    107  *
    108  * <!-- START SNIPPET: extending -->
    109  *
    110  * You can extend this interceptor and override the {@link #acceptFile} method to provide more control over which files
    111  * are supported and which are not.
    112  *
    113  * <!-- END SNIPPET: extending -->
    114  *
    115  * <p/> <u>Example code:</u>
    116  *
    117  * <pre>
    118  * <!-- START SNIPPET: example -->
    119  * &lt;action name="doUpload" class="com.examples.UploadAction"&gt;
    120  *     &lt;interceptor-ref name="fileUpload"/&gt;
    121  *     &lt;interceptor-ref name="basicStack"/&gt;
    122  *     &lt;result name="success"&gt;good_result.ftl&lt;/result&gt;
    123  * &lt;/action&gt;
    124  * </pre>
    125  *
    126  * And then you need to set encoding <code>multipart/form-data</code> in the form where the user selects the file to upload.
    127  * <pre>
    128  *   &lt;a:form action="doUpload" method="post" enctype="multipart/form-data"&gt;
    129  *       &lt;a:file name="upload" label="File"/&gt;
    130  *       &lt;a:submit/&gt;
    131  *   &lt;/a:form&gt;
    132  * </pre>
    133  *
    134  * And then in your action code you'll have access to the File object if you provide setters according to the
    135  * naming convention documented in the start.
    136  *
    137  * <pre>
    138  *    public com.examples.UploadAction implemements Action {
    139  *       private File file;
    140  *       private String contentType;
    141  *       private String filename;
    142  *
    143  *       public void setUpload(File file) {
    144  *          this.file = file;
    145  *       }
    146  *
    147  *       public void setUploadContentType(String contentType) {
    148  *          this.contentType = contentType;
    149  *       }
    150  *
    151  *       public void setUploadFileName(String filename) {
    152  *          this.filename = filename;
    153  *       }
    154  *
    155  *       ...
    156  *  }
    157  * </pre>
    158  * <!-- END SNIPPET: example -->
    159  *
    160  */
    161 public class FileUploadInterceptor extends AbstractInterceptor {
    162
    163     private static final long serialVersionUID = -4764627478894962478L;
    164
    165     protected static final Log log = LogFactory.getLog(FileUploadInterceptor.class);
    166     private static final String DEFAULT_DELIMITER = ",";
    167     private static final String DEFAULT_MESSAGE = "no.message.found";
    168
    169     protected Long maximumSize;
    170     protected String allowedTypes;
    171     protected Set allowedTypesSet = Collections.EMPTY_SET;
    172
    173     /***
    174      * Sets the allowed mimetypes
    175      *
    176      * @param allowedTypes A comma-delimited list of types
    177      */
    178     public void setAllowedTypes(String allowedTypes) {
    179         this.allowedTypes = allowedTypes;
    180
    181         // set the allowedTypes as a collection for easier access later
    182         allowedTypesSet = getDelimitedValues(allowedTypes);
    183     }
    184
    185     /***
    186      * Sets the maximum size of an uploaded file
    187      *
    188      * @param maximumSize The maximum size in bytes
    189      */
    190     public void setMaximumSize(Long maximumSize) {
    191         this.maximumSize = maximumSize;
    192     }
    193
    194     /* (non-Javadoc)
    195      * @see com.opensymphony.xwork2.interceptor.Interceptor#intercept(com.opensymphony.xwork2.ActionInvocation)
    196      */
    197     public String intercept(ActionInvocation invocation) throws Exception {
    198         ActionContext ac = invocation.getInvocationContext();
    199         HttpServletRequest request = (HttpServletRequest) ac.get(ServletActionContext.HTTP_REQUEST);
    200
    201         if (!(request instanceof MultiPartRequestWrapper)) {
    202             if (log.isDebugEnabled()) {
    203                 ActionProxy proxy = invocation.getProxy();
    204                 log.debug(getTextMessage("struts.messages.bypass.request", new Object[]{proxy.getNamespace(), proxy.getActionName()}, ActionContext.getContext().getLocale()));
    205             }
    206
    207             return invocation.invoke();
    208         }
    209
    210         final Object action = invocation.getAction();
    211         ValidationAware validation = null;
    212
    213         if (action instanceof ValidationAware) {
    214             validation = (ValidationAware) action;
    215         }
    216
    217         MultiPartRequestWrapper multiWrapper = (MultiPartRequestWrapper) request;
    218
    219         if (multiWrapper.hasErrors()) {
    220             for (Iterator errorIter = multiWrapper.getErrors().iterator(); errorIter.hasNext();) {
    221                 String error = (String) errorIter.next();
    222
    223                 if (validation != null) {
    224                     validation.addActionError(error);
    225                 }
    226
    227                 log.error(error);
    228             }
    229         }
    230
    231         Map parameters = ac.getParameters();
    232
    233         // Bind allowed Files
    234         Enumeration fileParameterNames = multiWrapper.getFileParameterNames();
    235         while (fileParameterNames != null && fileParameterNames.hasMoreElements()) {
    236             // get the value of this input tag
    237             String inputName = (String) fileParameterNames.nextElement();
    238
    239             // get the content type
    240             String[] contentType = multiWrapper.getContentTypes(inputName);
    241
    242             if (isNonEmpty(contentType)) {
    243                 // get the name of the file from the input tag
    244                 String[] fileName = multiWrapper.getFileNames(inputName);
    245
    246                 if (isNonEmpty(fileName)) {
    247                     // Get a File object for the uploaded File
    248                     File[] files = multiWrapper.getFiles(inputName);
    249                     if (files != null) {
    250                         for (int index = 0; index < files.length; index++) {
    251
    252                             if (acceptFile(files[index], contentType[index], inputName, validation, ac.getLocale())) {
    253                                 parameters.put(inputName, files);
    254                                 parameters.put(inputName + "ContentType", contentType);
    255                                 parameters.put(inputName + "FileName", fileName);
    256                             }
    257                         }
    258                     }
    259                 } else {
    260                     log.error(getTextMessage("struts.messages.invalid.file", new Object[]{inputName}, ActionContext.getContext().getLocale()));
    261                 }
    262             } else {
    263                 log.error(getTextMessage("struts.messages.invalid.content.type", new Object[]{inputName}, ActionContext.getContext().getLocale()));
    264             }
    265         }
    266
    267         // invoke action
    268         String result = invocation.invoke();
    269
    270         // cleanup
    271         fileParameterNames = multiWrapper.getFileParameterNames();
    272         while (fileParameterNames != null && fileParameterNames.hasMoreElements()) {
    273             String inputValue = (String) fileParameterNames.nextElement();
    274             File[] file = multiWrapper.getFiles(inputValue);
    275             for (int index = 0; index < file.length; index++) {
    276                 File currentFile = file[index];
    277                 log.info(getTextMessage("struts.messages.removing.file", new Object[]{inputValue, currentFile}, ActionContext.getContext().getLocale()));
    278
    279                 if ((currentFile != null) && currentFile.isFile()) {
    280                     currentFile.delete();
    281                 }
    282             }
    283         }
    284
    285         return result;
    286     }
    287
    288     /***
    289      * Override for added functionality. Checks if the proposed file is acceptable based on contentType and size.
    290      *
    291      * @param file        - proposed upload file.
    292      * @param contentType - contentType of the file.
    293      * @param inputName   - inputName of the file.
    294      * @param validation  - Non-null ValidationAware if the action implements ValidationAware, allowing for better
    295      *                    logging.
    296      * @param locale
    297      * @return true if the proposed file is acceptable by contentType and size.
    298      */
    299     protected boolean acceptFile(File file, String contentType, String inputName, ValidationAware validation, Locale locale) {
    300         boolean fileIsAcceptable = false;
    301
    302         // If it's null the upload failed
    303         if (file == null) {
    304             String errMsg = getTextMessage("struts.messages.error.uploading", new Object[]{inputName}, locale);
    305             if (validation != null) {
    306                 validation.addFieldError(inputName, errMsg);
    307             }
    308
    309             log.error(errMsg);
    310         } else if (maximumSize != null && maximumSize.longValue() < file.length()) {
    311             String errMsg = getTextMessage("struts.messages.error.file.too.large", new Object[]{inputName, file.getName(), "" + file.length()}, locale);
    312             if (validation != null) {
    313                 validation.addFieldError(inputName, errMsg);
    314             }
    315
    316             log.error(errMsg);
    317         } else if ((! allowedTypesSet.isEmpty()) && (!containsItem(allowedTypesSet, contentType))) {
    318             String errMsg = getTextMessage("struts.messages.error.content.type.not.allowed", new Object[]{inputName, file.getName(), contentType}, locale);
    319             if (validation != null) {
    320                 validation.addFieldError(inputName, errMsg);
    321             }
    322
    323             log.error(errMsg);
    324         } else {
    325             fileIsAcceptable = true;
    326         }
    327
    328         return fileIsAcceptable;
    329     }
    330
    331     /***
    332      * @param itemCollection - Collection of string items (all lowercase).
    333      * @param key            - Key to search for.
    334      * @return true if itemCollection contains the key, false otherwise.
    335      */
    336     private static boolean containsItem(Collection itemCollection, String key) {
    337         return itemCollection.contains(key.toLowerCase());
    338     }
    339
    340     private static Set getDelimitedValues(String delimitedString) {
    341         Set<String> delimitedValues = new HashSet<String>();
    342         if (delimitedString != null) {
    343             StringTokenizer stringTokenizer = new StringTokenizer(delimitedString, DEFAULT_DELIMITER);
    344             while (stringTokenizer.hasMoreTokens()) {
    345                 String nextToken = stringTokenizer.nextToken().toLowerCase().trim();
    346                 if (nextToken.length() > 0) {
    347                     delimitedValues.add(nextToken);
    348                 }
    349             }
    350         }
    351         return delimitedValues;
    352     }
    353
    354     private static boolean isNonEmpty(Object[] objArray) {
    355         boolean result = false;
    356         for (int index = 0; index < objArray.length && !result; index++) {
    357             if (objArray[index] != null) {
    358                 result = true;
    359             }
    360         }
    361         return result;
    362     }
    363
    364     private String getTextMessage(String messageKey, Object[] args, Locale locale) {
    365         if (args == null || args.length == 0) {
    366             return LocalizedTextUtil.findText(this.getClass(), messageKey, locale);
    367         } else {
    368             return LocalizedTextUtil.findText(this.getClass(), messageKey, locale, DEFAULT_MESSAGE, args);
    369         }
    370     }
    371 }