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 }