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