001: /*
002: * Copyright (c) 2002-2003 by OpenSymphony
003: * All rights reserved.
004: */
005: package com.opensymphony.webwork.interceptor;
006:
007: import com.opensymphony.webwork.ServletActionContext;
008: import com.opensymphony.webwork.dispatcher.multipart.MultiPartRequestWrapper;
009: import com.opensymphony.xwork.ActionContext;
010: import com.opensymphony.xwork.ActionInvocation;
011: import com.opensymphony.xwork.ActionProxy;
012: import com.opensymphony.xwork.ValidationAware;
013: import com.opensymphony.xwork.interceptor.Interceptor;
014: import com.opensymphony.xwork.util.LocalizedTextUtil;
015: import org.apache.commons.logging.Log;
016: import org.apache.commons.logging.LogFactory;
017:
018: import javax.servlet.http.HttpServletRequest;
019: import java.io.File;
020: import java.util.*;
021:
022: /**
023: * <!-- START SNIPPET: description -->
024: *
025: * Interceptor that is based off of {@link MultiPartRequestWrapper}, which is automatically applied for any request that
026: * includes a file. It adds the following parameters, where [File Name] is the name given to the file uploaded by the
027: * HTML form:
028: *
029: * <ul>
030: *
031: * <li>[File Name] : File - the actual File</li>
032: *
033: * <li>[File Name]ContentType : String - the content type of the file</li>
034: *
035: * <li>[File Name]FileName : String - the actual name of the file uploaded (not the HTML name)</li>
036: *
037: * </ul>
038: *
039: * <p/> You can get access to these files by merely providing setters in your action that correspond to any of the three
040: * patterns above, such as setDocument(File document), setDocumentContentType(String contentType), etc.
041: * <br/>See the example code section.
042: *
043: * <p/> This interceptor will add several field errors, assuming that the action implements {@link ValidationAware}.
044: * These error messages are based on several i18n values stored in webwork-messages.properties, a default i18n file
045: * processed for all i18n requests. You can override the text of these messages by providing text for the following
046: * keys:
047: *
048: * <ul>
049: *
050: * <li>webwork.messages.error.uploading - a general error that occurs when the file could not be uploaded</li>
051: *
052: * <li>webwork.messages.error.file.too.large - occurs when the uploaded file is too large</li>
053: *
054: * <li>webwork.messages.error.content.type.not.allowed - occurs when the uploaded file does not match the expected
055: * content types specified</li>
056: *
057: * </ul>
058: *
059: * <!-- END SNIPPET: description -->
060: *
061: * <p/> <u>Interceptor parameters:</u>
062: *
063: * <!-- START SNIPPET: parameters -->
064: *
065: * <ul>
066: *
067: * <li>maximumSize (optional) - the maximum size (in bytes) that the interceptor will allow a file reference to be set
068: * on the action. Note, this is <b>not</b> related to the various properties found in webwork.properties.
069: * Default to approximately 2MB.</li>
070: *
071: * <li>allowedTypes (optional) - a comma separated list of content types (ie: text/html) that the interceptor will allow
072: * a file reference to be set on the action. If none is specified allow all types to be uploaded.</li>
073: *
074: * </ul>
075: *
076: * <!-- END SNIPPET: parameters -->
077: *
078: * <p/> <u>Extending the interceptor:</u>
079: *
080: * <p/>
081: *
082: * <!-- START SNIPPET: extending -->
083: *
084: * You can extend this interceptor and override the {@link #acceptFile} method to provide more control over which files
085: * are supported and which are not.
086: *
087: * <!-- END SNIPPET: extending -->
088: *
089: * <p/> <u>Example code:</u>
090: *
091: * <pre>
092: * <!-- START SNIPPET: example -->
093: * <action name="doUpload" class="com.examples.UploadAction">
094: * <interceptor-ref name="fileUpload"/>
095: * <interceptor-ref name="basicStack"/>
096: * <result name="success">good_result.ftl</result>
097: * </action>
098: * </pre>
099: *
100: * And then you need to set encoding <code>multipart/form-data</code> in the form where the user selects the file to upload.
101: * <pre>
102: * <ww:form action="doUpload" method="post" enctype="multipart/form-data">
103: * <ww:file name="upload" label="File"/>
104: * <ww:submit/>
105: * </ww:form>
106: * </pre>
107: *
108: * And then in your action code you'll have access to the File object if you provide setters according to the
109: * naming convention documented in the start.
110: *
111: * <pre>
112: * public com.examples.UploadAction implemements Action {
113: * private File file;
114: * private String contentType;
115: * private String filename;
116: *
117: * public void setUpload(File file) {
118: * this.file = file;
119: * }
120: *
121: * public void setUploadContentType(String contentType) {
122: * this.contentType = contentType;
123: * }
124: *
125: * public void setUploadFileName(String filename) {
126: * this.filename = filename;
127: * }
128: *
129: * ...
130: * }
131: * </pre>
132: * <!-- END SNIPPET: example -->
133: *
134: */
135: public class FileUploadInterceptor implements Interceptor {
136:
137: private static final long serialVersionUID = 3475785630497636730L;
138:
139: protected static final Log log = LogFactory
140: .getLog(FileUploadInterceptor.class);
141: private static final String DEFAULT_DELIMITER = ",";
142: private static final String DEFAULT_MESSAGE = "no.message.found";
143:
144: protected Long maximumSize;
145: protected String allowedTypes;
146: protected Set allowedTypesSet = Collections.EMPTY_SET;
147:
148: public void setAllowedTypes(String allowedTypes) {
149: this .allowedTypes = allowedTypes;
150:
151: // set the allowedTypes as a collection for easier access later
152: allowedTypesSet = getDelimitedValues(allowedTypes);
153: }
154:
155: public void setMaximumSize(Long maximumSize) {
156: this .maximumSize = maximumSize;
157: }
158:
159: public void destroy() {
160: }
161:
162: public void init() {
163: }
164:
165: public String intercept(ActionInvocation invocation)
166: throws Exception {
167: ActionContext ac = invocation.getInvocationContext();
168: HttpServletRequest request = (HttpServletRequest) ac
169: .get(ServletActionContext.HTTP_REQUEST);
170:
171: if (!(request instanceof MultiPartRequestWrapper)) {
172: if (log.isDebugEnabled()) {
173: ActionProxy proxy = invocation.getProxy();
174: log.debug(getTextMessage(
175: "webwork.messages.bypass.request",
176: new Object[] { proxy.getNamespace(),
177: proxy.getActionName() }, ActionContext
178: .getContext().getLocale()));
179: }
180:
181: return invocation.invoke();
182: }
183:
184: final Object action = invocation.getAction();
185: ValidationAware validation = null;
186:
187: if (action instanceof ValidationAware) {
188: validation = (ValidationAware) action;
189: }
190:
191: MultiPartRequestWrapper multiWrapper = (MultiPartRequestWrapper) request;
192:
193: if (multiWrapper.hasErrors()) {
194: for (Iterator errorIter = multiWrapper.getErrors()
195: .iterator(); errorIter.hasNext();) {
196: String error = (String) errorIter.next();
197:
198: if (validation != null) {
199: validation.addActionError(error);
200: }
201:
202: log.error(error);
203: }
204: }
205:
206: Map parameters = ac.getParameters();
207:
208: // Bind allowed Files
209: Enumeration fileParameterNames = multiWrapper
210: .getFileParameterNames();
211: while (fileParameterNames != null
212: && fileParameterNames.hasMoreElements()) {
213: // get the value of this input tag
214: String inputName = (String) fileParameterNames
215: .nextElement();
216:
217: // get the content type
218: String[] contentType = multiWrapper
219: .getContentTypes(inputName);
220:
221: if (isNonEmpty(contentType)) {
222: // get the name of the file from the input tag
223: String[] fileName = multiWrapper
224: .getFileNames(inputName);
225:
226: if (isNonEmpty(fileName)) {
227: // Get a File object for the uploaded File
228: File[] files = multiWrapper.getFiles(inputName);
229: if (files != null) {
230: for (int index = 0; index < files.length; index++) {
231: if (log.isInfoEnabled()) {
232: log
233: .info(getTextMessage(
234: "webwork.messages.current.file",
235: new Object[] {
236: inputName,
237: contentType[index],
238: fileName[index],
239: files[index] },
240: ActionContext
241: .getContext()
242: .getLocale()));
243: }
244:
245: if (acceptFile(files[index],
246: contentType[index], inputName,
247: validation, ac.getLocale())) {
248: parameters.put(inputName, files);
249: parameters.put(inputName
250: + "ContentType", contentType);
251: parameters.put(inputName + "FileName",
252: fileName);
253: }
254: }
255: }
256: } else {
257: log.error(getTextMessage(
258: "webwork.messages.invalid.file",
259: new Object[] { inputName }, ActionContext
260: .getContext().getLocale()));
261: }
262: } else {
263: log.error(getTextMessage(
264: "webwork.messages.invalid.content.type",
265: new Object[] { inputName }, ActionContext
266: .getContext().getLocale()));
267: }
268: }
269:
270: // invoke action
271: String result = invocation.invoke();
272:
273: // cleanup
274: fileParameterNames = multiWrapper.getFileParameterNames();
275: while (fileParameterNames != null
276: && fileParameterNames.hasMoreElements()) {
277: String inputValue = (String) fileParameterNames
278: .nextElement();
279: File[] file = multiWrapper.getFiles(inputValue);
280: for (int index = 0; index < file.length; index++) {
281: File currentFile = file[index];
282: log.info(getTextMessage(
283: "webwork.messages.removing.file", new Object[] {
284: inputValue, currentFile },
285: ActionContext.getContext().getLocale()));
286:
287: if ((currentFile != null) && currentFile.isFile()) {
288: currentFile.delete();
289: }
290: }
291: }
292:
293: return result;
294: }
295:
296: /**
297: * Override for added functionality. Checks if the proposed file is acceptable based on contentType and size.
298: *
299: * @param file - proposed upload file.
300: * @param contentType - contentType of the file.
301: * @param inputName - inputName of the file.
302: * @param validation - Non-null ValidationAware if the action implements ValidationAware, allowing for better
303: * logging.
304: * @param locale
305: * @return true if the proposed file is acceptable by contentType and size.
306: */
307: protected boolean acceptFile(File file, String contentType,
308: String inputName, ValidationAware validation, Locale locale) {
309: boolean fileIsAcceptable = false;
310:
311: // If it's null the upload failed
312: if (file == null) {
313: String errMsg = getTextMessage(
314: "webwork.messages.error.uploading",
315: new Object[] { inputName }, locale);
316: if (validation != null) {
317: validation.addFieldError(inputName, errMsg);
318: }
319:
320: log.error(errMsg);
321: } else if (maximumSize != null
322: && maximumSize.longValue() < file.length()) {
323: String errMsg = getTextMessage(
324: "webwork.messages.error.file.too.large",
325: new Object[] { inputName, file.getName(),
326: "" + file.length() }, locale);
327: if (validation != null) {
328: validation.addFieldError(inputName, errMsg);
329: }
330:
331: log.error(errMsg);
332: } else if ((!allowedTypesSet.isEmpty())
333: && (!containsItem(allowedTypesSet, contentType))) {
334: String errMsg = getTextMessage(
335: "webwork.messages.error.content.type.not.allowed",
336: new Object[] { inputName, file.getName(),
337: contentType }, locale);
338: if (validation != null) {
339: validation.addFieldError(inputName, errMsg);
340: }
341:
342: log.error(errMsg);
343: } else {
344: fileIsAcceptable = true;
345: }
346:
347: return fileIsAcceptable;
348: }
349:
350: /**
351: * @param itemCollection - Collection of string items (all lowercase).
352: * @param key - Key to search for.
353: * @return true if itemCollection contains the key, false otherwise.
354: */
355: protected boolean containsItem(Collection itemCollection, String key) {
356: return itemCollection.contains(key.toLowerCase());
357: }
358:
359: protected Set getDelimitedValues(String delimitedString) {
360: Set delimitedValues = new HashSet();
361: if (delimitedString != null) {
362: StringTokenizer stringTokenizer = new StringTokenizer(
363: delimitedString, DEFAULT_DELIMITER);
364: while (stringTokenizer.hasMoreTokens()) {
365: String nextToken = stringTokenizer.nextToken()
366: .toLowerCase().trim();
367: if (nextToken.length() > 0) {
368: delimitedValues.add(nextToken);
369: }
370: }
371: }
372: return delimitedValues;
373: }
374:
375: protected boolean isNonEmpty(Object[] objArray) {
376: boolean result = false;
377: for (int index = 0; index < objArray.length && !result; index++) {
378: if (objArray[index] != null) {
379: result = true;
380: }
381: }
382: return result;
383: }
384:
385: protected String getTextMessage(String messageKey, Object[] args,
386: Locale locale) {
387: if (args == null || args.length == 0) {
388: return LocalizedTextUtil.findText(this.getClass(),
389: messageKey, locale);
390: } else {
391: return LocalizedTextUtil.findText(this.getClass(),
392: messageKey, locale, DEFAULT_MESSAGE, args);
393: }
394: }
395: }
|