001: /*
002: * Copyright 2000,2005 wingS development team.
003: *
004: * This file is part of wingS (http://wingsframework.org).
005: *
006: * wingS is free software; you can redistribute it and/or modify
007: * it under the terms of the GNU Lesser General Public License
008: * as published by the Free Software Foundation; either version 2.1
009: * of the License, or (at your option) any later version.
010: *
011: * Please see COPYING for the complete licence.
012: */
013: package org.wings;
014:
015: import org.apache.commons.logging.Log;
016: import org.apache.commons.logging.LogFactory;
017: import org.wings.plaf.FileChooserCG;
018: import org.wings.event.SParentFrameListener;
019: import org.wings.event.SParentFrameEvent;
020: import org.wings.util.LocaleCharSet;
021:
022: import javax.servlet.http.HttpUtils;
023: import java.io.File;
024: import java.io.FilterOutputStream;
025: import java.io.IOException;
026: import java.io.UnsupportedEncodingException;
027: import java.net.URLDecoder;
028: import java.util.Hashtable;
029:
030: /**
031: * Shows a textfield with a browse-button to enter a filename.
032: * The file is uploaded via HTTP and made accessible to the WingS application.
033: * <p/>
034: * <p>The uploaded file is stored temporarily in the filesystem of the
035: * server with a unique name, so that uploaded files with the same
036: * filename do not clash. You can access this internal name with
037: * the {@link #getFileDir()} and {@link #getFileId()} methods.
038: * The user provided filename can be queried with the
039: * {@link #getFileName()} method.
040: * <p/>
041: * Since the file is stored temporarily in the filesystem, you should
042: * {@link File#delete()} it, when you are done with it.
043: * However, if you don't delete the file yourself, it is eventually
044: * being removed by the Java garbage collector, if you haven't renamed it
045: * (see {@link #getFile()}).
046: * <p/>
047: * <p>The form, you add this SFileChooser to, needs to have the encoding type
048: * <code>multipart/form-data</code> set
049: * (form.{@link SForm#setEncodingType(String) setEncodingType("multipart/form-data")}).
050: * This is handled by the form. You can explicitly set it via the above
051: * method, though, in order to increase speed.
052: * <p/>
053: * <p>You can limit the size of files to be uploaded, so it is hard to make
054: * a denial-of-service (harddisk, bandwidth) attack from outside to your
055: * server. You can modify the maximum content length to be posted in
056: * {@link org.wings.session.Session#setMaxContentLength(int)}. This is
057: * 64 kByte by default, so you might want to change this in your application.
058: * <p/>
059: * <p/>
060: * The SFileChooser notifies the form if something has gone
061: * wrong with uploading a file.
062: * <p/>
063: * <b>Szenario</b>
064: * Files that are too big to be uploaded are blocked
065: * very early in the upload-process (if you are curious: this is done in
066: * {@link org.wings.session.MultipartRequest}).
067: * At that time, only a partial input is
068: * read, the rest is discarded to thwart denial of service attacks. Since we
069: * read only part of the input, we cannot make sure, that <em>all</em>
070: * parameters are gathered from the input, thus we cannot just deliver the
071: * events contained, since they might be incomplete. However, the file
072: * chooser needs to be informed, that something went wrong as to present
073: * an error message to the user. So in that case, only <em>one</em> event
074: * is delivered to the enclosing <em>form</em>, that contains this
075: * SFileChooser.
076: * <p/>
077: * <p>Note, that in this case, this will <em>not</em> trigger the action
078: * listener that you might have added to the submit-button.
079: * This means, that you <em>always</em> should add your action listener
080: * to the {@link SForm} ({@link SForm#addActionListener(java.awt.event.ActionListener)}),
081: * <em>not</em> the submit button.
082: *
083: * @author Holger Engels
084: * @author <a href="mailto:H.Zeller@acm.org">Henner Zeller</a>
085: */
086: public class SFileChooser extends SComponent implements
087: LowLevelEventListener, SParentFrameListener {
088: private final transient static Log log = LogFactory
089: .getLog(SFileChooser.class);
090:
091: /**
092: * maximum visible amount of characters in the file chooser.
093: */
094: protected int columns = 16;
095:
096: protected String fileNameFilter = null;
097:
098: protected Class filter = null;
099: protected String fileDir = null;
100: protected String fileName = null;
101: protected String fileId = null;
102: protected String fileType = null;
103:
104: /**
105: * the temporary file created on upload. This file is automatically
106: * removed if and when it is not accessible anymore.
107: */
108: protected TempFile currentFile = null;
109:
110: /**
111: * the temporary file created on upload. This file is automatically
112: * removed if and when it is not accessible anymore.
113: */
114: protected IOException exception = null;
115: private SForm parentForm;
116:
117: /**
118: * Creates a new FileChooser.
119: */
120: public SFileChooser() {
121: addParentFrameListener(this );
122: }
123:
124: /**
125: * Find the form, this FileChooser is embedded in.
126: */
127: protected final SForm getParentForm() {
128: SComponent parent = getParent();
129:
130: while (parent != null && !(parent instanceof SForm)) {
131: parent = parent.getParent();
132: }
133:
134: return (SForm) parent;
135: }
136:
137: /**
138: * notifies the parent form, to fire action performed. This is necessary,
139: * if an exception in parsing a MultiPartRequest occurs, e.g. upload
140: * file is too big.
141: */
142: protected final void notifyParentForm() {
143: SForm form = getParentForm();
144:
145: if (form != null) {
146: SForm.addArmedComponent(form);
147: }
148: }
149:
150: public void parentFrameAdded(SParentFrameEvent e) {
151: parentForm = getParentForm();
152: if (parentForm != null)
153: parentForm.registerFileChooser(this );
154: else
155: log.warn("file chooser not in a form");
156: }
157:
158: public void parentFrameRemoved(SParentFrameEvent e) {
159: if (parentForm != null)
160: parentForm.unregisterFileChooser(this );
161: else
162: log.warn("file chooser not in a form");
163: parentForm = null;
164: }
165:
166: /**
167: * Set the visible amount of columns in the textfield.
168: *
169: * @param c columns; '-1' sets the default that is browser dependent.
170: */
171: public void setColumns(int c) {
172: int oldColumns = columns;
173: columns = c;
174: if (columns != oldColumns)
175: reload();
176: }
177:
178: /**
179: * returns the number of visible columns.
180: *
181: * @return number of visible columns.
182: */
183: public int getColumns() {
184: return columns;
185: }
186:
187: /**
188: * Unlike the swing filechooser that allows to match certain file-suffices,
189: * this sets the <em>mimetype</em> to be accepted. This filter may be fully
190: * qualified like <code>text/html</code> or can contain
191: * a wildcard in the subtype like <code>text/ *</code>. Some browsers
192: * may as well accept a file-suffix wildcard as well.
193: * <p/>
194: * <p>In any case, you hould check the result, since you cannot assume,
195: * that the browser
196: * actually does filter. Worse, browsers may not guess the
197: * correct type so users cannot upload a file even if it has the correct
198: * type. So, bottomline, it is generally a good idea to let the file
199: * name filter untouched, unless you know bugs of the browser at the
200: * other end of the wire...
201: *
202: * @param mimeFilter the mime type to be filtered.
203: */
204: public void setFileNameFilter(String mimeFilter) {
205: fileNameFilter = mimeFilter;
206: }
207:
208: /**
209: * returns the current filename filter. This is a mimetype-filter
210: *
211: * @return the current filename filter or 'null', if no filename filter
212: * is provided.
213: * @see #setFileNameFilter(String)
214: */
215: public String getFileNameFilter() {
216: return fileNameFilter;
217: }
218:
219: /**
220: * Returns the filename, that has been given by the user in the
221: * upload text-field.
222: *
223: * @return the filename, given by the user.
224: * @throws IOException if something went wrong with the upload (most
225: * likely, the maximum allowed filesize is exceeded, see
226: * {@link org.wings.session.Session#setMaxContentLength(int)})
227: */
228: public String getFileName() throws IOException {
229: if (exception != null)
230: throw exception;
231:
232: return fileName;
233: }
234:
235: /**
236: * Returns the name of the system directory, the file has been stored
237: * temporarily in. You won't need this, unless you want to access the
238: * file directly. Don't store the value you receive here for use later,
239: * since the SFileChooser does its own garbage collecting of unused files.
240: *
241: * @return the pathname of the system directory, the file is stored in.
242: * @throws IOException if something went wrong with the upload (most
243: * likely, the maximum allowed filesize is exceeded, see
244: * {@link org.wings.session.Session#setMaxContentLength(int)})
245: */
246: public String getFileDir() throws IOException {
247: if (exception != null)
248: throw exception;
249:
250: return fileDir;
251: }
252:
253: /**
254: * Returns the internal ID of this file, that has been assigned at upload
255: * time. This ID is unique to prevent clashes with other uploaded files.
256: * You won't need this, unless you want to access the file directly. Don't
257: * store the value you receive here for later use, since the SFileChooser
258: * does its own garbage collecting of unused files.
259: *
260: * @return the internal, unique file id given to the uploaded file.
261: * @throws IOException if something went wrong with the upload (most
262: * likely, the maximum allowed filesize is exceeded, see
263: * {@link org.wings.session.Session#setMaxContentLength(int)})
264: */
265: public String getFileId() throws IOException {
266: if (exception != null)
267: throw exception;
268:
269: return fileId;
270: }
271:
272: /**
273: * Returns the mime type of this file, if known.
274: *
275: * @return the mime type of this file.
276: * @throws IOException if something went wrong with the upload (most
277: * likely, the maximum allowed filesize is exceeded, see
278: * {@link org.wings.session.Session#setMaxContentLength(int)})
279: */
280: public String getFileType() throws IOException {
281: if (exception != null)
282: throw exception;
283:
284: return fileType;
285: }
286:
287: /**
288: * returns the file, that has been uploaded. Use this, to open and
289: * read from the file uploaded by the user. Don't use this method
290: * to query the actual filename given by the user, since this file
291: * wraps a system generated file with a different (unique) name.
292: * Use {@link #getFileName()} instead.
293: * <p/>
294: * <p>The file returned here
295: * will delete itself if you loose the reference to it and it is
296: * garbage collected to avoid filling up the filesystem (This doesn't
297: * mean, that you shouldn't be a good programmer and delete the
298: * file yourself, if you don't need it anymore :-).
299: * If you rename() the file to use it somewhere else,
300: * it is regarded not temporary anymore and thus will <em>not</em>
301: * be removed from the filesystem.
302: *
303: * @return a File to access the content of the uploaded file.
304: * @throws IOException if something went wrong with the upload (most
305: * likely, the maximum allowed filesize is exceeded, see
306: * {@link org.wings.session.Session#setMaxContentLength(int)})
307: *
308: */
309: public File getSelectedFile() throws IOException {
310: if (exception != null)
311: throw exception;
312:
313: return currentFile;
314: }
315:
316: /**
317: * resets this FileChooser (no file selected). It does not remove an
318: * upload filter!.
319: * reset() will <em>not</em> remove a previously selected file from
320: * the local tmp disk space, so as long as you have a reference to
321: * such a file, you can still access it. If you don't have a reference
322: * to the file, it will automatically be removed when the file object
323: * is garbage collected.
324: */
325: public void reset() {
326: currentFile = null;
327: fileId = null;
328: fileDir = null;
329: fileType = null;
330: fileName = null;
331: exception = null;
332: }
333:
334: /**
335: * returns the file, that has been uploaded. Use this, to open and
336: * read from the file uploaded by the user. Don't use this method
337: * to query the actual filename given by the user, since this file
338: * wraps a system generated file with a different (unique) name.
339: * Use {@link #getFileName()} instead.
340: * <p/>
341: * <p>The file returned here
342: * will delete itself if you loose the reference to it and it is
343: * garbage collected to avoid filling up the filesystem (This doesn't
344: * mean, that you shouldn't be a good programmer and delete the
345: * file yourself, if you don't need it anymore :-).
346: * If you rename() the file to use it somewhere else,
347: * it is regarded not temporary anymore and thus will <em>not</em>
348: * be removed from the filesystem.
349: *
350: * @return a File to access the content of the uploaded file.
351: * @throws IOException if something went wrong with the upload (most
352: * likely, the maximum allowed filesize is exceeded, see
353: * {@link org.wings.session.Session#setMaxContentLength(int)})
354: * @deprecated use {@link org.wings.SFileChooser#getSelectedFile()} instead.
355: */
356: public File getFile() throws IOException {
357: return getSelectedFile();
358: }
359:
360: /**
361: * An FilterOutputStream, that filters incoming files. You can use
362: * UploadFilters to inspect the stream or rewrite it to some own
363: * format.
364: *
365: * @param filter the Class that is instanciated to filter incoming
366: * files.
367: */
368: public void setUploadFilter(Class filter) {
369: if (!FilterOutputStream.class.isAssignableFrom(filter))
370: throw new IllegalArgumentException(filter.getName()
371: + " is not a FilterOutputStream!");
372:
373: UploadFilterManager
374: .registerFilter(getLowLevelEventId(), filter);
375: this .filter = filter;
376: }
377:
378: /**
379: * Returns the upload filter set in {@link #setUploadFilter(Class)}
380: */
381: public Class getUploadFilter() {
382: return filter;
383: }
384:
385: public FilterOutputStream getUploadFilterInstance() {
386: return UploadFilterManager
387: .getFilterInstance(getLowLevelEventId());
388: }
389:
390: public void setCG(FileChooserCG cg) {
391: super .setCG(cg);
392: }
393:
394: // -- Implementation of LowLevelEventListener
395: public void processLowLevelEvent(String action, String[] values) {
396: processKeyEvents(values);
397: if (action.endsWith("_keystroke"))
398: return;
399:
400: exception = null;
401:
402: /* Analogous to the other end in UploadedFile.java ! */
403: final String encoding = getSession().getCharacterEncoding() != null ? getSession()
404: .getCharacterEncoding()
405: : LocaleCharSet.DEFAULT_ENCODING;
406: String value;
407: try {
408: value = URLDecoder.decode(values[0], encoding);
409: } catch (UnsupportedEncodingException e) {
410: log.warn("Failed to url-decode '" + values[0] + "'.");
411: value = values[0];
412: }
413:
414: if ("exception".equals(value)) {
415: exception = new IOException(values[1]);
416:
417: notifyParentForm();
418: } else {
419: try {
420: Hashtable params = HttpUtils.parseQueryString(value);
421: String[] arr;
422: arr = (String[]) params.get("dir");
423: this .fileDir = (arr != null) ? arr[0] : null;
424: arr = (String[]) params.get("name");
425: this .fileName = (arr != null) ? arr[0] : null;
426: arr = (String[]) params.get("id");
427: this .fileId = (arr != null) ? arr[0] : null;
428: arr = (String[]) params.get("type");
429: this .fileType = (arr != null) ? arr[0] : null;
430: if (fileDir != null && fileId != null) {
431: currentFile = new TempFile(fileDir, fileId);
432: }
433: } catch (Exception e) {
434: log.fatal(null, e);
435: }
436: }
437:
438: // clear the input field
439: reload();
440: }
441:
442: public void fireIntermediateEvents() {
443: }
444:
445: /** @see LowLevelEventListener#isEpochCheckEnabled() */
446: private boolean epochCheckEnabled = true;
447:
448: /** @see LowLevelEventListener#isEpochCheckEnabled() */
449: public boolean isEpochCheckEnabled() {
450: return epochCheckEnabled;
451: }
452:
453: /** @see LowLevelEventListener#isEpochCheckEnabled() */
454: public void setEpochCheckEnabled(boolean epochCheckEnabled) {
455: this .epochCheckEnabled = epochCheckEnabled;
456: }
457:
458: /**
459: * A temporary file. This file removes its representation in the
460: * filesysten, when there are no references to it (i.e. it is garbage
461: * collected)
462: */
463: private static class TempFile extends File {
464: private boolean isTemp;
465:
466: public TempFile(String parent, String child) {
467: super (parent, child);
468: deleteOnExit();
469: isTemp = true;
470: }
471:
472: /**
473: * when this file is renamed, then it is not temporary anymore,
474: * thus will not be removed on cleanup.
475: */
476: public boolean renameTo(File newfile) {
477: boolean success;
478: success = super .renameTo(newfile);
479: isTemp &= !success; // we are not temporary anymore on success.
480: return success;
481: }
482:
483: /**
484: * removes the file in the filesystem, if it is still temporary.
485: */
486: private void cleanup() {
487: if (isTemp) {
488: delete();
489: }
490: }
491:
492: /**
493: * do a cleanup, if this temporary file is garbage collected.
494: */
495: protected void finalize() throws Throwable {
496: super .finalize();
497: if (isTemp)
498: log.debug("garbage collect file " + getName());
499: cleanup();
500: }
501: }
502: }
|