From b84bf1a1bd398d99de027f1468b8c5d07dd7dc50 Mon Sep 17 00:00:00 2001 From: Matti Tahvonen Date: Wed, 5 Aug 2009 07:48:43 +0000 Subject: [PATCH] multiple upload related enhancements - implemented interruption of upload #963 - added generics - changed deprecated gwt methods to 1.6 handlers - immediate mode now works (also styled as "one button upload") svn changeset:8442/svn branch:6.0 --- WebContent/VAADIN/themes/base/styles.css | 24 ++ .../VAADIN/themes/base/upload/upload.css | 22 ++ WebContent/VAADIN/themes/reindeer/styles.css | 24 ++ WebContent/VAADIN/themes/runo/styles.css | 24 ++ .../terminal/gwt/client/ui/VUpload.java | 155 +++++++--- src/com/vaadin/tests/TestForStyledUpload.java | 288 ++++++++++++++++++ src/com/vaadin/ui/Upload.java | 56 +++- 7 files changed, 546 insertions(+), 47 deletions(-) create mode 100644 WebContent/VAADIN/themes/base/upload/upload.css create mode 100644 src/com/vaadin/tests/TestForStyledUpload.java diff --git a/WebContent/VAADIN/themes/base/styles.css b/WebContent/VAADIN/themes/base/styles.css index a75762c0f8..e98bef219c 100644 --- a/WebContent/VAADIN/themes/base/styles.css +++ b/WebContent/VAADIN/themes/base/styles.css @@ -1436,6 +1436,30 @@ div.v-tree-node-leaf { clear: left; } +/* ./WebContent/VAADIN/themes/base/upload/upload.css */ +.v-upload-immediate { + position: relative; + width: 10em; + margin:0; +} + +.v-upload-immediate input { + opacity: 0; + filter: alpha(opacity=0); + z-index:2; + position: absolute; + right:0; + height:21px; + text-align: right; +} +.v-upload-immediate button { + position:relative; + left:0; + top:0; + width:100%; +} + + /* ./WebContent/VAADIN/themes/base/window/window.css */ .v-window { background: #fff; diff --git a/WebContent/VAADIN/themes/base/upload/upload.css b/WebContent/VAADIN/themes/base/upload/upload.css new file mode 100644 index 0000000000..ba96537ddb --- /dev/null +++ b/WebContent/VAADIN/themes/base/upload/upload.css @@ -0,0 +1,22 @@ +.v-upload-immediate { + position: relative; + width: 10em; + margin:0; +} + +.v-upload-immediate input { + opacity: 0; + filter: alpha(opacity=0); + z-index:2; + position: absolute; + right:0; + height:21px; + text-align: right; +} +.v-upload-immediate button { + position:relative; + left:0; + top:0; + width:100%; +} + \ No newline at end of file diff --git a/WebContent/VAADIN/themes/reindeer/styles.css b/WebContent/VAADIN/themes/reindeer/styles.css index 4df54dd3ed..b5bf3402e5 100644 --- a/WebContent/VAADIN/themes/reindeer/styles.css +++ b/WebContent/VAADIN/themes/reindeer/styles.css @@ -1436,6 +1436,30 @@ div.v-tree-node-leaf { clear: left; } +/* ./WebContent/VAADIN/themes/base/upload/upload.css */ +.v-upload-immediate { + position: relative; + width: 10em; + margin:0; +} + +.v-upload-immediate input { + opacity: 0; + filter: alpha(opacity=0); + z-index:2; + position: absolute; + right:0; + height:21px; + text-align: right; +} +.v-upload-immediate button { + position:relative; + left:0; + top:0; + width:100%; +} + + /* ./WebContent/VAADIN/themes/base/window/window.css */ .v-window { background: #fff; diff --git a/WebContent/VAADIN/themes/runo/styles.css b/WebContent/VAADIN/themes/runo/styles.css index a1633d5522..639e4cc992 100644 --- a/WebContent/VAADIN/themes/runo/styles.css +++ b/WebContent/VAADIN/themes/runo/styles.css @@ -1436,6 +1436,30 @@ div.v-tree-node-leaf { clear: left; } +/* ./WebContent/VAADIN/themes/base/upload/upload.css */ +.v-upload-immediate { + position: relative; + width: 10em; + margin:0; +} + +.v-upload-immediate input { + opacity: 0; + filter: alpha(opacity=0); + z-index:2; + position: absolute; + right:0; + height:21px; + text-align: right; +} +.v-upload-immediate button { + position:relative; + left:0; + top:0; + width:100%; +} + + /* ./WebContent/VAADIN/themes/base/window/window.css */ .v-window { background: #fff; diff --git a/src/com/vaadin/terminal/gwt/client/ui/VUpload.java b/src/com/vaadin/terminal/gwt/client/ui/VUpload.java index 666652a15a..117d3ea267 100644 --- a/src/com/vaadin/terminal/gwt/client/ui/VUpload.java +++ b/src/com/vaadin/terminal/gwt/client/ui/VUpload.java @@ -4,30 +4,53 @@ package com.vaadin.terminal.gwt.client.ui; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.ui.Button; -import com.google.gwt.user.client.ui.ClickListener; import com.google.gwt.user.client.ui.FileUpload; import com.google.gwt.user.client.ui.FlowPanel; -import com.google.gwt.user.client.ui.FormHandler; import com.google.gwt.user.client.ui.FormPanel; -import com.google.gwt.user.client.ui.FormSubmitCompleteEvent; -import com.google.gwt.user.client.ui.FormSubmitEvent; +import com.google.gwt.user.client.ui.Hidden; import com.google.gwt.user.client.ui.Panel; -import com.google.gwt.user.client.ui.Widget; +import com.google.gwt.user.client.ui.FormPanel.SubmitCompleteHandler; +import com.google.gwt.user.client.ui.FormPanel.SubmitHandler; import com.vaadin.terminal.gwt.client.ApplicationConnection; import com.vaadin.terminal.gwt.client.Paintable; import com.vaadin.terminal.gwt.client.UIDL; -public class VUpload extends FormPanel implements Paintable, ClickListener, - FormHandler { +public class VUpload extends FormPanel implements Paintable, + SubmitCompleteHandler, SubmitHandler { + + private final class MyFileUpload extends FileUpload { + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + if (event.getTypeInt() == Event.ONCHANGE) { + if (immediate && fu.getFilename() != null + && !"".equals(fu.getFilename())) { + submit(); + } + } else if (event.getTypeInt() == Event.ONFOCUS) { + // IE and user has clicked on hidden textarea part of upload + // field. Manually open file selector, other browsers do it by + // default. + fireNativeClick(fu.getElement()); + // also remove focus to enable hack if user presses cancel + // button + fireNativeBlur(fu.getElement()); + } + } + } public static final String CLASSNAME = "v-upload"; /** * FileUpload component that opens native OS dialog to select file. */ - FileUpload fu = new FileUpload(); + FileUpload fu = new MyFileUpload(); Panel panel = new FlowPanel(); @@ -56,18 +79,34 @@ public class VUpload extends FormPanel implements Paintable, ClickListener, private boolean enabled = true; + private boolean immediate; + + private Hidden maxfilesize = new Hidden(); + public VUpload() { super(); setEncoding(FormPanel.ENCODING_MULTIPART); setMethod(FormPanel.METHOD_POST); setWidget(panel); + panel.add(maxfilesize); panel.add(fu); submitButton = new Button(); - submitButton.addClickListener(this); + submitButton.setStyleName("v-button"); + submitButton.addClickHandler(new ClickHandler() { + public void onClick(ClickEvent event) { + if (immediate) { + // fire click on upload (eg. focused button and hit space) + fireNativeClick(fu.getElement()); + } else { + submit(); + } + } + }); panel.add(submitButton); - addFormHandler(this); + addSubmitCompleteHandler(this); + addSubmitHandler(this); setStyleName(CLASSNAME); } @@ -76,10 +115,12 @@ public class VUpload extends FormPanel implements Paintable, ClickListener, if (client.updateComponent(this, uidl, true)) { return; } + setImmediate(uidl.getBooleanAttribute("immediate")); this.client = client; paintableId = uidl.getId(); setAction(client.getAppUri()); - submitButton.setText(uidl.getStringAttribute("buttoncaption")); + submitButton.setHTML("" + + uidl.getStringAttribute("buttoncaption") + ""); fu.setName(paintableId + "_file"); if (uidl.hasAttribute("disabled") || uidl.hasAttribute("readonly")) { @@ -87,16 +128,76 @@ public class VUpload extends FormPanel implements Paintable, ClickListener, } else if (uidl.getBooleanAttribute("state")) { enableUploaod(); } + } + private void setImmediate(boolean booleanAttribute) { + if (immediate != booleanAttribute) { + immediate = booleanAttribute; + if (immediate) { + fu.sinkEvents(Event.ONCHANGE); + fu.sinkEvents(Event.ONFOCUS); + } + } + setStyleName(getElement(), CLASSNAME + "-immediate", immediate); } - public void onClick(Widget sender) { - submit(); + private static native void fireNativeClick(Element element) + /*-{ + element.click(); + }-*/; + + private static native void fireNativeBlur(Element element) + /*-{ + element.blur(); + }-*/; + + protected void disableUpload() { + submitButton.setEnabled(false); + fu.setVisible(false); + enabled = false; } - public void onSubmit(FormSubmitEvent event) { + protected void enableUploaod() { + submitButton.setEnabled(true); + fu.setVisible(true); + enabled = true; + } + + /** + * Re-creates file input field and populates panel. This is needed as we + * want to clear existing values from our current file input field. + */ + private void rebuildPanel() { + panel.remove(submitButton); + panel.remove(fu); + fu = new MyFileUpload(); + fu.setName(paintableId + "_file"); + fu.setVisible(enabled); + panel.add(fu); + panel.add(submitButton); + if (immediate) { + fu.sinkEvents(Event.ONCHANGE); + } + } + + public void onSubmitComplete(SubmitCompleteEvent event) { + if (client != null) { + if (t != null) { + t.cancel(); + } + ApplicationConnection.getConsole().log("Submit complete"); + client.sendPendingVariableChanges(); + } + + rebuildPanel(); + + submitted = false; + enableUploaod(); + } + + public void onSubmit(SubmitEvent event) { if (fu.getFilename().length() == 0 || submitted || !enabled) { - event.setCancelled(true); + event.cancel(); ApplicationConnection .getConsole() .log( @@ -125,28 +226,4 @@ public class VUpload extends FormPanel implements Paintable, ClickListener, t.schedule(800); } - protected void disableUpload() { - submitButton.setEnabled(false); - fu.setVisible(false); - enabled = false; - } - - protected void enableUploaod() { - submitButton.setEnabled(true); - fu.setVisible(true); - enabled = true; - } - - public void onSubmitComplete(FormSubmitCompleteEvent event) { - if (client != null) { - if (t != null) { - t.cancel(); - } - ApplicationConnection.getConsole().log("Submit complete"); - client.sendPendingVariableChanges(); - } - submitted = false; - enableUploaod(); - } - } diff --git a/src/com/vaadin/tests/TestForStyledUpload.java b/src/com/vaadin/tests/TestForStyledUpload.java new file mode 100644 index 0000000000..0e4933d2b9 --- /dev/null +++ b/src/com/vaadin/tests/TestForStyledUpload.java @@ -0,0 +1,288 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.tests; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; + +import com.vaadin.Application; +import com.vaadin.terminal.StreamResource; +import com.vaadin.ui.Button; +import com.vaadin.ui.Label; +import com.vaadin.ui.Layout; +import com.vaadin.ui.Link; +import com.vaadin.ui.Panel; +import com.vaadin.ui.ProgressIndicator; +import com.vaadin.ui.Upload; +import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.Window; +import com.vaadin.ui.Button.ClickEvent; +import com.vaadin.ui.Upload.FailedEvent; +import com.vaadin.ui.Upload.FailedListener; +import com.vaadin.ui.Upload.FinishedEvent; +import com.vaadin.ui.Upload.FinishedListener; +import com.vaadin.ui.Upload.StartedEvent; +import com.vaadin.ui.Upload.StartedListener; +import com.vaadin.ui.Upload.SucceededEvent; +import com.vaadin.ui.Upload.SucceededListener; + +public class TestForStyledUpload extends Application implements + Upload.FinishedListener, FailedListener, SucceededListener, + StartedListener { + + Layout main = new VerticalLayout(); + + TmpFileBuffer buffer = new TmpFileBuffer(); + + Panel status = new Panel("Uploaded file:"); + + private final Upload up; + + private final Label l; + + private final Label transferred = new Label(""); + + private final ProgressIndicator pi = new ProgressIndicator(); + + private final Label memoryStatus; + + public TestForStyledUpload() { + main + .addComponent(new Label( + "Clicking on button b updates information about upload components status or same with garbage collector.")); + + up = new Upload(null, buffer); + up.setButtonCaption("Select file"); + up.setImmediate(true); + up.addListener((FinishedListener) this); + up.addListener((FailedListener) this); + up.addListener((SucceededListener) this); + up.addListener((StartedListener) this); + + up.addListener(new Upload.ProgressListener() { + + public void updateProgress(long readBytes, long contentLenght) { + pi.setValue(new Float(readBytes / (float) contentLenght)); + + refreshMemUsage(); + + transferred.setValue("Transferred " + readBytes + " of " + + contentLenght); + } + + }); + + final Button b = new Button("Update status", this, "readState"); + + final Button c = new Button("Update status with gc", this, "gc"); + + main.addComponent(up); + l = new Label("Idle"); + main.addComponent(l); + + pi.setVisible(false); + pi.setPollingInterval(300); + main.addComponent(pi); + main.addComponent(transferred); + + memoryStatus = new Label(); + main.addComponent(memoryStatus); + + status.setVisible(false); + main.addComponent(status); + + Button cancel = new Button("Cancel current upload"); + cancel.addListener(new Button.ClickListener() { + public void buttonClick(ClickEvent event) { + buffer.cancel(); + } + }); + + main.addComponent(cancel); + + final Button restart = new Button("Restart demo application"); + restart.addListener(new Button.ClickListener() { + + public void buttonClick(ClickEvent event) { + TestForStyledUpload.this.close(); + } + }); + main.addComponent(restart); + main.addComponent(b); + main.addComponent(c); + + } + + public void gc() { + Runtime.getRuntime().gc(); + readState(); + } + + public void readState() { + final StringBuffer sb = new StringBuffer(); + + if (up.isUploading()) { + sb.append("Uploading..."); + sb.append(up.getBytesRead()); + sb.append("/"); + sb.append(up.getUploadSize()); + sb.append(" "); + sb.append(Math.round(100 * up.getBytesRead() + / (double) up.getUploadSize())); + sb.append("%"); + } else { + sb.append("Idle"); + } + l.setValue(sb.toString()); + refreshMemUsage(); + } + + public void uploadFinished(FinishedEvent event) { + status.removeAllComponents(); + final InputStream stream = buffer.getStream(); + if (stream == null) { + status.addComponent(new Label( + "Upload finished, but output buffer is null!!")); + } else { + status + .addComponent(new Label("Name: " + + event.getFilename(), Label.CONTENT_XHTML)); + status.addComponent(new Label("Mimetype: " + + event.getMIMEType(), Label.CONTENT_XHTML)); + status.addComponent(new Label("Size: " + event.getLength() + + " bytes.", Label.CONTENT_XHTML)); + + status.addComponent(new Link("Download " + buffer.getFileName(), + new StreamResource(buffer, buffer.getFileName(), this))); + + status.setVisible(true); + } + } + + public interface Buffer extends StreamResource.StreamSource, + Upload.Receiver { + + String getFileName(); + } + + public class TmpFileBuffer implements Buffer { + String mimeType; + + String fileName; + + private File file; + + private FileInputStream stream; + + public TmpFileBuffer() { + final String tempFileName = "upload_tmpfile_" + + System.currentTimeMillis(); + try { + file = File.createTempFile(tempFileName, null); + } catch (final IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + } + + public void cancel() { + up.interruptUpload(); + } + + public InputStream getStream() { + if (file == null) { + return null; + } + try { + stream = new FileInputStream(file); + return stream; + } catch (final FileNotFoundException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return null; + } + + /** + * @see com.vaadin.ui.Upload.Receiver#receiveUpload(String, String) + */ + public OutputStream receiveUpload(String filename, String MIMEType) { + fileName = filename; + mimeType = MIMEType; + try { + return new FileOutputStream(file); + } catch (final FileNotFoundException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return null; + } + + /** + * Returns the fileName. + * + * @return String + */ + public String getFileName() { + return fileName; + } + + /** + * Returns the mimeType. + * + * @return String + */ + public String getMimeType() { + return mimeType; + } + + } + + public void uploadFailed(FailedEvent event) { + pi.setVisible(false); + l.setValue("Upload was interrupted"); + } + + public void uploadSucceeded(SucceededEvent event) { + pi.setVisible(false); + l.setValue("Finished upload, idle"); + System.out.println(event); + } + + private void refreshMemUsage() { + // memoryStatus.setValue("Not available in Java 1.4"); + StringBuffer mem = new StringBuffer(); + MemoryMXBean mmBean = ManagementFactory.getMemoryMXBean(); + mem.append("Heap (M):"); + mem.append(mmBean.getHeapMemoryUsage().getUsed() / 1048576); + mem.append(" | Non-Heap (M):"); + mem.append(mmBean.getNonHeapMemoryUsage().getUsed() / 1048576); + memoryStatus.setValue(mem.toString()); + + } + + public void uploadStarted(StartedEvent event) { + pi.setVisible(true); + l.setValue("Started uploading file " + event.getFilename()); + } + + @Override + public void init() { + Window w = new Window(); + w.setTheme("runo"); + w.addComponent(main); + setMainWindow(w); + + } + +} diff --git a/src/com/vaadin/ui/Upload.java b/src/com/vaadin/ui/Upload.java index 70e16e1ded..cdea1e6ce3 100644 --- a/src/com/vaadin/ui/Upload.java +++ b/src/com/vaadin/ui/Upload.java @@ -4,6 +4,7 @@ package com.vaadin.ui; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Serializable; @@ -20,13 +21,16 @@ import com.vaadin.terminal.UploadStream; /** * Component for uploading files from client to server. * + *

* The visible component consists of a file name input box and a browse button * and an upload submit button to start uploading. * + *

* The Upload component needs a java.io.OutputStream to write the uploaded data. * You need to implement the Upload.Receiver interface and return the output * stream in the receiveUpload() method. * + *

* You can get an event regarding starting (StartedEvent), progress * (ProgressEvent), and finishing (FinishedEvent) of upload by implementing * StartedListener, ProgressListener, and FinishedListener, respectively. The @@ -34,10 +38,16 @@ import com.vaadin.terminal.UploadStream; * to separate between these two cases, you can use SucceededListener * (SucceededEvenet) and FailedListener (FailedEvent). * + *

* The upload component does not itself show upload progress, but you can use * the ProgressIndicator for providing progress feedback by implementing * ProgressListener and updating the indicator in updateProgress(). * + *

+ * Setting upload component immediate initiates the upload as soon as a file is + * selected, instead of the common pattern of file selection field and upload + * button. + * * @author IT Mill Ltd. * @version * @VERSION@ @@ -81,7 +91,9 @@ public class Upload extends AbstractComponent implements Component.Focusable { * upload */ private ProgressListener progressListener; - private LinkedHashSet progressListeners; + private LinkedHashSet progressListeners; + + private boolean interrupted = false; /* TODO: Add a default constructor, receive to temp file. */ @@ -169,6 +181,9 @@ public class Upload extends AbstractComponent implements Component.Focusable { fireUpdateProgress(totalBytes, contentLength); } } + if (interrupted) { + throw new Exception("Upload interrupted by other thread"); + } } // upload successful @@ -182,8 +197,15 @@ public class Upload extends AbstractComponent implements Component.Focusable { } catch (final Exception e) { synchronized (application) { // Download interrupted + try { + // still try to close output stream + out.close(); + } catch (IOException e1) { + // NOP + } fireUploadInterrupted(filename, type, totalBytes, e); endUpload(); + interrupted = false; } } } @@ -697,7 +719,7 @@ public class Upload extends AbstractComponent implements Component.Focusable { */ public void addListener(ProgressListener listener) { if (progressListeners == null) { - progressListeners = new LinkedHashSet(); + progressListeners = new LinkedHashSet(); } progressListeners.add(listener); } @@ -792,8 +814,9 @@ public class Upload extends AbstractComponent implements Component.Focusable { // this is implemented differently than other listeners to maintain // backwards compatibility if (progressListeners != null) { - for (Iterator it = progressListeners.iterator(); it.hasNext();) { - ProgressListener l = (ProgressListener) it.next(); + for (Iterator it = progressListeners.iterator(); it + .hasNext();) { + ProgressListener l = it.next(); l.updateProgress(totalBytes, contentLength); } } @@ -880,13 +903,24 @@ public class Upload extends AbstractComponent implements Component.Focusable { isUploading = true; } + /** + * Interrupts the upload currently being received. The interruption will be + * done by the receiving tread so this method will return immediately and + * the actual interrupt will happen a bit later. + */ + public void interruptUpload() { + if (isUploading) { + interrupted = true; + } + } + /** * Go into state where new uploading can begin. * * Warning: this is an internal method used by the framework and should not * be used by user of the Upload component. */ - public void endUpload() { + private void endUpload() { isUploading = false; contentLength = -1; } @@ -960,11 +994,17 @@ public class Upload extends AbstractComponent implements Component.Focusable { } /** - * File uploads usually have button that starts actual upload progress. This - * method is used to set text in that button. + * In addition to the actual file chooser, upload components have button + * that starts actual upload progress. This method is used to set text in + * that button. + * + *

+ * Note the string given is set as is to the button. HTML + * formatting is not stripped. Be sure to properly validate your value + * according to your needs. * * @param buttonCaption - * text for uploads button. + * text for upload components button. */ public void setButtonCaption(String buttonCaption) { this.buttonCaption = buttonCaption; -- 2.39.5