org.wings.SFileChooser Maven / Gradle / Ivy
/*
* Copyright 2000,2005 wingS development team.
*
* This file is part of wingS (http://wingsframework.org).
*
* wingS is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2.1
* of the License, or (at your option) any later version.
*
* Please see COPYING for the complete licence.
*/
package org.wings;
import java.io.File;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wings.event.SParentFrameEvent;
import org.wings.event.SParentFrameListener;
import org.wings.plaf.FileChooserCG;
import org.wings.util.LocaleCharSet;
/**
* Shows a textfield with a browse-button to enter a filename. The file is uploaded via HTTP and made accessible to the WingS
* application.
*
* The uploaded file is stored temporarily in the filesystem of the server with a unique name, so that uploaded files with the
* same filename do not clash. You can access this internal name with the {@link #getFileDir()} and {@link #getFileId()} methods.
* The user provided filename can be queried with the {@link #getFileName()} method.
*
* Since the file is stored temporarily in the filesystem, you should {@link File#delete()} it, when you are done with it.
* However, if you don't delete the file yourself, it is eventually being removed by the Java garbage collector, if you haven't
* renamed it (see {@link #getFile()}).
*
* The form, you add this SFileChooser to, needs to have the encoding type multipart/form-data
set (form.{@link
* SForm#setEncodingType(String) setEncodingType("multipart/form-data")}). This is handled by the form. You can explicitly set it
* via the above method, though, in order to increase speed.
*
* You can limit the size of files to be uploaded, so it is hard to make a denial-of-service (harddisk, bandwidth) attack from
* outside to your server. You can modify the maximum content length to be posted in {@link
* org.wings.session.Session#setMaxContentLength(int)}. This is 64 kByte by default, so you might want to change this in your
* application.
*
*
* The SFileChooser notifies the form if something has gone wrong with uploading a file.
*
* Szenario Files that are too big to be uploaded are blocked very early in the upload-process (if you are curious: this is
* done in {@link org.wings.session.MultipartRequest}). At that time, only a partial input is read, the rest is discarded to
* thwart denial of service attacks. Since we read only part of the input, we cannot make sure, that all parameters are
* gathered from the input, thus we cannot just deliver the events contained, since they might be incomplete. However, the file
* chooser needs to be informed, that something went wrong as to present an error message to the user. So in that case, only
* one event is delivered to the enclosing form, that contains this SFileChooser.
*
* Note, that in this case, this will not trigger the action listener that you might have added to the submit-button.
* This means, that you always should add your action listener to the {@link SForm} ({@link
* SForm#addActionListener(java.awt.event.ActionListener)}), not the submit button.
*
* @author Holger Engels
* @author Henner Zeller
*/
public class SFileChooser
extends SComponent
implements LowLevelEventListener, SParentFrameListener {
private final transient static Logger log = LoggerFactory.getLogger(SFileChooser.class);
/** maximum visible amount of characters in the file chooser. */
protected int columns = 16;
protected String fileNameFilter = null;
protected Class filter = null;
protected String fileDir = null;
protected String fileName = null;
protected String fileId = null;
protected String fileType = null;
/** the temporary file created on upload. This file is automatically removed if and when it is not accessible anymore. */
protected TempFile currentFile = null;
/** the temporary file created on upload. This file is automatically removed if and when it is not accessible anymore. */
protected IOException exception = null;
private SForm parentForm;
/** Creates a new FileChooser. */
public SFileChooser() {
addParentFrameListener(this);
}
/** Find the form, this FileChooser is embedded in. */
protected final SForm getParentForm() {
SComponent parent = getParent();
while (parent != null && !(parent instanceof SForm)) {
parent = parent.getParent();
}
return (SForm) parent;
}
/**
* notifies the parent form, to fire action performed. This is necessary, if an exception in parsing a MultiPartRequest
* occurs, e.g. upload file is too big.
*/
protected final void notifyParentForm() {
SForm form = getParentForm();
if (form != null) {
SForm.addArmedComponent(form);
}
}
@Override
public void parentFrameAdded(SParentFrameEvent e) {
parentForm = getParentForm();
if (parentForm != null) {
parentForm.registerFileChooser(this);
} else {
log.warn("file chooser not in a form");
}
}
@Override
public void parentFrameRemoved(SParentFrameEvent e) {
if (parentForm != null) {
parentForm.unregisterFileChooser(this);
} else {
log.warn("file chooser not in a form");
}
parentForm = null;
}
/**
* Set the visible amount of columns in the textfield.
*
* @param c columns; '-1' sets the default that is browser dependent.
*/
public void setColumns(int c) {
int oldColumns = columns;
columns = c;
if (columns != oldColumns) {
reload();
}
propertyChangeSupport.firePropertyChange("columns", oldColumns, this.columns);
}
/**
* returns the number of visible columns.
*
* @return number of visible columns.
*/
public int getColumns() {
return columns;
}
/**
* Unlike the swing filechooser that allows to match certain file-suffices, this sets the mimetype to be accepted.
* This filter may be fully qualified like text/html
or can contain a wildcard in the subtype like text/
* *
. Some browsers may as well accept a file-suffix wildcard as well.
*
* In any case, you hould check the result, since you cannot assume, that the browser actually does filter. Worse, browsers
* may not guess the correct type so users cannot upload a file even if it has the correct type. So, bottomline, it is
* generally a good idea to let the file name filter untouched, unless you know bugs of the browser at the other end of the
* wire...
*
* @param mimeFilter the mime type to be filtered.
*/
public void setFileNameFilter(String mimeFilter) {
String oldVal = this.fileNameFilter;
fileNameFilter = mimeFilter;
propertyChangeSupport.firePropertyChange("fileNameFilter", oldVal, this.fileNameFilter);
}
/**
* returns the current filename filter. This is a mimetype-filter
*
* @return the current filename filter or 'null', if no filename filter is provided.
* @see #setFileNameFilter(String)
*/
public String getFileNameFilter() {
return fileNameFilter;
}
/**
* Returns the filename, that has been given by the user in the upload text-field.
*
* @return the filename, given by the user.
* @throws IOException if something went wrong with the upload (most likely, the maximum allowed filesize is exceeded, see
* {@link org.wings.session.Session#setMaxContentLength(int)})
*/
public String getFileName() throws IOException {
if (exception != null) {
throw exception;
}
return fileName;
}
/**
* Returns the name of the system directory, the file has been stored temporarily in. You won't need this, unless you want to
* access the file directly. Don't store the value you receive here for use later, since the SFileChooser does its own
* garbage collecting of unused files.
*
* @return the pathname of the system directory, the file is stored in.
* @throws IOException if something went wrong with the upload (most likely, the maximum allowed filesize is exceeded, see
* {@link org.wings.session.Session#setMaxContentLength(int)})
*/
public String getFileDir() throws IOException {
if (exception != null) {
throw exception;
}
return fileDir;
}
/**
* Returns the internal ID of this file, that has been assigned at upload time. This ID is unique to prevent clashes with
* other uploaded files. You won't need this, unless you want to access the file directly. Don't store the value you receive
* here for later use, since the SFileChooser does its own garbage collecting of unused files.
*
* @return the internal, unique file id given to the uploaded file.
* @throws IOException if something went wrong with the upload (most likely, the maximum allowed filesize is exceeded, see
* {@link org.wings.session.Session#setMaxContentLength(int)})
*/
public String getFileId() throws IOException {
if (exception != null) {
throw exception;
}
return fileId;
}
/**
* Returns the mime type of this file, if known.
*
* @return the mime type of this file.
* @throws IOException if something went wrong with the upload (most likely, the maximum allowed filesize is exceeded, see
* {@link org.wings.session.Session#setMaxContentLength(int)})
*/
public String getFileType() throws IOException {
if (exception != null) {
throw exception;
}
return fileType;
}
/**
* returns the file, that has been uploaded. Use this, to open and read from the file uploaded by the user. Don't use this
* method to query the actual filename given by the user, since this file wraps a system generated file with a different
* (unique) name. Use {@link #getFileName()} instead.
*
* The file returned here will delete itself if you loose the reference to it and it is garbage collected to avoid filling
* up the filesystem (This doesn't mean, that you shouldn't be a good programmer and delete the file yourself, if you don't
* need it anymore :-). If you rename() the file to use it somewhere else, it is regarded not temporary anymore and thus will
* not be removed from the filesystem.
*
* @return a File to access the content of the uploaded file.
* @throws IOException if something went wrong with the upload (most likely, the maximum allowed filesize is exceeded, see
* {@link org.wings.session.Session#setMaxContentLength(int)})
*/
public File getSelectedFile() throws IOException {
if (exception != null) {
throw exception;
}
return currentFile;
}
protected void setSelectedFile(TempFile file) {
currentFile = file;
}
/**
* resets this FileChooser (no file selected). It does not remove an upload filter!. reset() will not remove a
* previously selected file from the local tmp disk space, so as long as you have a reference to such a file, you can still
* access it. If you don't have a reference to the file, it will automatically be removed when the file object is garbage
* collected.
*/
public void reset() {
currentFile = null;
fileId = null;
fileDir = null;
fileType = null;
fileName = null;
exception = null;
}
/**
* returns the file, that has been uploaded. Use this, to open and read from the file uploaded by the user. Don't use this
* method to query the actual filename given by the user, since this file wraps a system generated file with a different
* (unique) name. Use {@link #getFileName()} instead.
*
* The file returned here will delete itself if you loose the reference to it and it is garbage collected to avoid filling
* up the filesystem (This doesn't mean, that you shouldn't be a good programmer and delete the file yourself, if you don't
* need it anymore :-). If you rename() the file to use it somewhere else, it is regarded not temporary anymore and thus will
* not be removed from the filesystem.
*
* @return a File to access the content of the uploaded file.
* @throws IOException if something went wrong with the upload (most likely, the maximum allowed filesize is exceeded, see
* {@link org.wings.session.Session#setMaxContentLength(int)})
* @deprecated use {@link org.wings.SFileChooser#getSelectedFile()} instead.
*/
public File getFile() throws IOException {
return getSelectedFile();
}
/**
* An FilterOutputStream, that filters incoming files. You can use UploadFilters to inspect the stream or rewrite it to some
* own format.
*
* @param filter the Class that is instanciated to filter incoming files.
*/
public void setUploadFilter(Class filter) {
if (!FilterOutputStream.class.isAssignableFrom(filter)) {
throw new IllegalArgumentException(filter.getName() + " is not a FilterOutputStream!");
}
Class oldVal = this.filter;
UploadFilterManager.registerFilter(getLowLevelEventId(), filter);
this.filter = filter;
propertyChangeSupport.firePropertyChange("uploadFilter", oldVal, this.filter);
}
/** Returns the upload filter set in {@link #setUploadFilter(Class)} */
public Class getUploadFilter() {
return filter;
}
public FilterOutputStream getUploadFilterInstance() {
return UploadFilterManager.getFilterInstance(getLowLevelEventId());
}
public void setCG(FileChooserCG cg) {
super.setCG(cg);
}
// -- Implementation of LowLevelEventListener
@Override
public void processLowLevelEvent(String action, String... values) {
processKeyEvents(values);
if (action.endsWith("_keystroke")) {
return;
}
assert values != null && values[0] != null;
exception = null;
final String encoding = getSession().getCharacterEncoding() != null ? getSession().getCharacterEncoding() : LocaleCharSet.DEFAULT_ENCODING;
try {
String[] splittedValues = values[0].split("&");
Map params = new HashMap<>();
for (String splittedValue : splittedValues) {
final int seperatorPos = splittedValue.indexOf('=');
if (seperatorPos > 0 && seperatorPos < splittedValue.length()) {
String key = splittedValue.substring(0, seperatorPos);
String value = splittedValue.substring(seperatorPos + 1, splittedValue.length());
value = URLDecoder.decode(value, encoding);
params.put(key, value);
}
}
// parse Elements
this.fileDir = params.get("dir");
this.fileName = params.get("name");
this.fileId = params.get("id");
this.fileType = params.get("type");
if (fileDir != null && fileId != null) {
currentFile = new TempFile(fileDir, fileId);
}
} catch (UnsupportedEncodingException e) {
log.warn("Failed to url-decode '" + values[0] + "'.");
exception = e;
} catch (Exception ex) {
log.warn("Unknown Exception during URL decoding '" + values[0] + "'.");
exception = new IOException(ex.getMessage());
}
if (exception != null) {
notifyParentForm();
}
// clear the input field
reload();
}
@Override
public void fireIntermediateEvents() {
}
/** @see LowLevelEventListener#isEpochCheckEnabled() */
private boolean epochCheckEnabled = true;
/** @see LowLevelEventListener#isEpochCheckEnabled() */
@Override
public boolean isEpochCheckEnabled() {
return epochCheckEnabled;
}
/** @see LowLevelEventListener#isEpochCheckEnabled() */
public void setEpochCheckEnabled(boolean epochCheckEnabled) {
boolean oldVal = this.epochCheckEnabled;
this.epochCheckEnabled = epochCheckEnabled;
propertyChangeSupport.firePropertyChange("epochCheckEnabled", oldVal, this.epochCheckEnabled);
}
/**
* A temporary file. This file removes its representation in the filesysten, when there are no references to it (i.e. it is
* garbage collected)
*/
protected static class TempFile extends File {
private boolean isTemp;
public TempFile(String parent, String child) {
super(parent, child);
deleteOnExit();
isTemp = true;
}
/** when this file is renamed, then it is not temporary anymore, thus will not be removed on cleanup. */
@Override
public boolean renameTo(File newfile) {
boolean success = super.renameTo(newfile);
isTemp &= !success; // we are not temporary anymore on success.
return success;
}
/** removes the file in the filesystem, if it is still temporary. */
private void cleanup() {
if (isTemp) {
delete();
}
}
/** do a cleanup, if this temporary file is garbage collected. */
@Override
protected void finalize() throws Throwable {
super.finalize();
if (isTemp) {
log.debug("garbage collect file " + getName());
}
cleanup();
}
}
}