001: /*
002: * $Id: FileUploadInterceptor.java 495094 2007-01-11 02:51:40Z mrdon $
003: *
004: * Licensed to the Apache Software Foundation (ASF) under one
005: * or more contributor license agreements. See the NOTICE file
006: * distributed with this work for additional information
007: * regarding copyright ownership. The ASF licenses this file
008: * to you under the Apache License, Version 2.0 (the
009: * "License"); you may not use this file except in compliance
010: * with the License. You may obtain a copy of the License at
011: *
012: * http://www.apache.org/licenses/LICENSE-2.0
013: *
014: * Unless required by applicable law or agreed to in writing,
015: * software distributed under the License is distributed on an
016: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017: * KIND, either express or implied. See the License for the
018: * specific language governing permissions and limitations
019: * under the License.
020: */
021: package org.apache.struts2.interceptor;
022:
023: import java.io.File;
024: import java.util.Collection;
025: import java.util.Collections;
026: import java.util.Enumeration;
027: import java.util.HashSet;
028: import java.util.Iterator;
029: import java.util.Locale;
030: import java.util.Map;
031: import java.util.Set;
032: import java.util.StringTokenizer;
033:
034: import javax.servlet.http.HttpServletRequest;
035:
036: import org.apache.commons.logging.Log;
037: import org.apache.commons.logging.LogFactory;
038: import org.apache.struts2.ServletActionContext;
039: import org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper;
040:
041: import com.opensymphony.xwork2.ActionContext;
042: import com.opensymphony.xwork2.ActionInvocation;
043: import com.opensymphony.xwork2.ActionProxy;
044: import com.opensymphony.xwork2.ValidationAware;
045: import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
046: import com.opensymphony.xwork2.util.LocalizedTextUtil;
047:
048: /**
049: * <!-- START SNIPPET: description -->
050: *
051: * Interceptor that is based off of {@link MultiPartRequestWrapper}, which is automatically applied for any request that
052: * includes a file. It adds the following parameters, where [File Name] is the name given to the file uploaded by the
053: * HTML form:
054: *
055: * <ul>
056: *
057: * <li>[File Name] : File - the actual File</li>
058: *
059: * <li>[File Name]ContentType : String - the content type of the file</li>
060: *
061: * <li>[File Name]FileName : String - the actual name of the file uploaded (not the HTML name)</li>
062: *
063: * </ul>
064: *
065: * <p/> You can get access to these files by merely providing setters in your action that correspond to any of the three
066: * patterns above, such as setDocument(File document), setDocumentContentType(String contentType), etc.
067: * <br/>See the example code section.
068: *
069: * <p/> This interceptor will add several field errors, assuming that the action implements {@link ValidationAware}.
070: * These error messages are based on several i18n values stored in struts-messages.properties, a default i18n file
071: * processed for all i18n requests. You can override the text of these messages by providing text for the following
072: * keys:
073: *
074: * <ul>
075: *
076: * <li>struts.messages.error.uploading - a general error that occurs when the file could not be uploaded</li>
077: *
078: * <li>struts.messages.error.file.too.large - occurs when the uploaded file is too large</li>
079: *
080: * <li>struts.messages.error.content.type.not.allowed - occurs when the uploaded file does not match the expected
081: * content types specified</li>
082: *
083: * </ul>
084: *
085: * <!-- END SNIPPET: description -->
086: *
087: * <p/> <u>Interceptor parameters:</u>
088: *
089: * <!-- START SNIPPET: parameters -->
090: *
091: * <ul>
092: *
093: * <li>maximumSize (optional) - the maximum size (in bytes) that the interceptor will allow a file reference to be set
094: * on the action. Note, this is <b>not</b> related to the various properties found in struts.properties.
095: * Default to approximately 2MB.</li>
096: *
097: * <li>allowedTypes (optional) - a comma separated list of content types (ie: text/html) that the interceptor will allow
098: * a file reference to be set on the action. If none is specified allow all types to be uploaded.</li>
099: *
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
166: .getLog(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: // set the allowedTypes as a collection for easier access later
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: /* (non-Javadoc)
196: * @see com.opensymphony.xwork2.interceptor.Interceptor#intercept(com.opensymphony.xwork2.ActionInvocation)
197: */
198: public String intercept(ActionInvocation invocation)
199: throws Exception {
200: ActionContext ac = invocation.getInvocationContext();
201: HttpServletRequest request = (HttpServletRequest) ac
202: .get(ServletActionContext.HTTP_REQUEST);
203:
204: if (!(request instanceof MultiPartRequestWrapper)) {
205: if (log.isDebugEnabled()) {
206: ActionProxy proxy = invocation.getProxy();
207: log.debug(getTextMessage(
208: "struts.messages.bypass.request", new Object[] {
209: proxy.getNamespace(),
210: proxy.getActionName() }, ActionContext
211: .getContext().getLocale()));
212: }
213:
214: return invocation.invoke();
215: }
216:
217: final Object action = invocation.getAction();
218: ValidationAware validation = null;
219:
220: if (action instanceof ValidationAware) {
221: validation = (ValidationAware) action;
222: }
223:
224: MultiPartRequestWrapper multiWrapper = (MultiPartRequestWrapper) request;
225:
226: if (multiWrapper.hasErrors()) {
227: for (Iterator errorIter = multiWrapper.getErrors()
228: .iterator(); errorIter.hasNext();) {
229: String error = (String) errorIter.next();
230:
231: if (validation != null) {
232: validation.addActionError(error);
233: }
234:
235: log.error(error);
236: }
237: }
238:
239: Map parameters = ac.getParameters();
240:
241: // Bind allowed Files
242: Enumeration fileParameterNames = multiWrapper
243: .getFileParameterNames();
244: while (fileParameterNames != null
245: && fileParameterNames.hasMoreElements()) {
246: // get the value of this input tag
247: String inputName = (String) fileParameterNames
248: .nextElement();
249:
250: // get the content type
251: String[] contentType = multiWrapper
252: .getContentTypes(inputName);
253:
254: if (isNonEmpty(contentType)) {
255: // get the name of the file from the input tag
256: String[] fileName = multiWrapper
257: .getFileNames(inputName);
258:
259: if (isNonEmpty(fileName)) {
260: // Get a File object for the uploaded File
261: File[] files = multiWrapper.getFiles(inputName);
262: if (files != null) {
263: for (int index = 0; index < files.length; index++) {
264:
265: if (acceptFile(files[index],
266: contentType[index], inputName,
267: validation, ac.getLocale())) {
268: parameters.put(inputName, files);
269: parameters.put(inputName
270: + "ContentType", contentType);
271: parameters.put(inputName + "FileName",
272: fileName);
273: }
274: }
275: }
276: } else {
277: log.error(getTextMessage(
278: "struts.messages.invalid.file",
279: new Object[] { inputName }, ActionContext
280: .getContext().getLocale()));
281: }
282: } else {
283: log.error(getTextMessage(
284: "struts.messages.invalid.content.type",
285: new Object[] { inputName }, ActionContext
286: .getContext().getLocale()));
287: }
288: }
289:
290: // invoke action
291: String result = invocation.invoke();
292:
293: // cleanup
294: fileParameterNames = multiWrapper.getFileParameterNames();
295: while (fileParameterNames != null
296: && fileParameterNames.hasMoreElements()) {
297: String inputValue = (String) fileParameterNames
298: .nextElement();
299: File[] file = multiWrapper.getFiles(inputValue);
300: for (int index = 0; index < file.length; index++) {
301: File currentFile = file[index];
302: log.info(getTextMessage(
303: "struts.messages.removing.file", new Object[] {
304: inputValue, currentFile },
305: ActionContext.getContext().getLocale()));
306:
307: if ((currentFile != null) && currentFile.isFile()) {
308: currentFile.delete();
309: }
310: }
311: }
312:
313: return result;
314: }
315:
316: /**
317: * Override for added functionality. Checks if the proposed file is acceptable based on contentType and size.
318: *
319: * @param file - proposed upload file.
320: * @param contentType - contentType of the file.
321: * @param inputName - inputName of the file.
322: * @param validation - Non-null ValidationAware if the action implements ValidationAware, allowing for better
323: * logging.
324: * @param locale
325: * @return true if the proposed file is acceptable by contentType and size.
326: */
327: protected boolean acceptFile(File file, String contentType,
328: String inputName, ValidationAware validation, Locale locale) {
329: boolean fileIsAcceptable = false;
330:
331: // If it's null the upload failed
332: if (file == null) {
333: String errMsg = getTextMessage(
334: "struts.messages.error.uploading",
335: new Object[] { inputName }, locale);
336: if (validation != null) {
337: validation.addFieldError(inputName, errMsg);
338: }
339:
340: log.error(errMsg);
341: } else if (maximumSize != null
342: && maximumSize.longValue() < file.length()) {
343: String errMsg = getTextMessage(
344: "struts.messages.error.file.too.large",
345: new Object[] { inputName, file.getName(),
346: "" + file.length() }, locale);
347: if (validation != null) {
348: validation.addFieldError(inputName, errMsg);
349: }
350:
351: log.error(errMsg);
352: } else if ((!allowedTypesSet.isEmpty())
353: && (!containsItem(allowedTypesSet, contentType))) {
354: String errMsg = getTextMessage(
355: "struts.messages.error.content.type.not.allowed",
356: new Object[] { inputName, file.getName(),
357: contentType }, locale);
358: if (validation != null) {
359: validation.addFieldError(inputName, errMsg);
360: }
361:
362: log.error(errMsg);
363: } else {
364: fileIsAcceptable = true;
365: }
366:
367: return fileIsAcceptable;
368: }
369:
370: /**
371: * @param itemCollection - Collection of string items (all lowercase).
372: * @param key - Key to search for.
373: * @return true if itemCollection contains the key, false otherwise.
374: */
375: private static boolean containsItem(Collection itemCollection,
376: String key) {
377: return itemCollection.contains(key.toLowerCase());
378: }
379:
380: private static Set getDelimitedValues(String delimitedString) {
381: Set<String> delimitedValues = new HashSet<String>();
382: if (delimitedString != null) {
383: StringTokenizer stringTokenizer = new StringTokenizer(
384: delimitedString, DEFAULT_DELIMITER);
385: while (stringTokenizer.hasMoreTokens()) {
386: String nextToken = stringTokenizer.nextToken()
387: .toLowerCase().trim();
388: if (nextToken.length() > 0) {
389: delimitedValues.add(nextToken);
390: }
391: }
392: }
393: return delimitedValues;
394: }
395:
396: private static boolean isNonEmpty(Object[] objArray) {
397: boolean result = false;
398: for (int index = 0; index < objArray.length && !result; index++) {
399: if (objArray[index] != null) {
400: result = true;
401: }
402: }
403: return result;
404: }
405:
406: private String getTextMessage(String messageKey, Object[] args,
407: Locale locale) {
408: if (args == null || args.length == 0) {
409: return LocalizedTextUtil.findText(this.getClass(),
410: messageKey, locale);
411: } else {
412: return LocalizedTextUtil.findText(this.getClass(),
413: messageKey, locale, DEFAULT_MESSAGE, args);
414: }
415: }
416: }
|