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