1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 * <action name="doUpload" class="com.examples.UploadAction">
120 * <interceptor-ref name="fileUpload"/>
121 * <interceptor-ref name="basicStack"/>
122 * <result name="success">good_result.ftl</result>
123 * </action>
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 * <a:form action="doUpload" method="post" enctype="multipart/form-data">
129 * <a:file name="upload" label="File"/>
130 * <a:submit/>
131 * </a:form>
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
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
195
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
234 Enumeration fileParameterNames = multiWrapper.getFileParameterNames();
235 while (fileParameterNames != null && fileParameterNames.hasMoreElements()) {
236
237 String inputName = (String) fileParameterNames.nextElement();
238
239
240 String[] contentType = multiWrapper.getContentTypes(inputName);
241
242 if (isNonEmpty(contentType)) {
243
244 String[] fileName = multiWrapper.getFileNames(inputName);
245
246 if (isNonEmpty(fileName)) {
247
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
268 String result = invocation.invoke();
269
270
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
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 }