From 011608a0a39784c3834aa1a4c42f6b7280f3240a Mon Sep 17 00:00:00 2001 From: Matti Tahvonen Date: Fri, 8 Oct 2010 11:21:50 +0000 Subject: [PATCH] refactored handling of Receiver's lots of other upload related improvements. fixes #5743 (removed dependency to commons fileupload), #5742 (drag and drop file uploads in webkits), #5741 (Receiver is upload specific), #4271 (uploads in GateIn). Also improves API for receiving files with drag and drop (rejection, canceling, tracking of the actual streaming) and transfers files in serial in DDW. svn changeset:15461/svn branch:6.5 --- build/build.xml | 12 +- src/com/vaadin/terminal/PaintTarget.java | 16 + src/com/vaadin/terminal/Receiver.java | 29 ++ src/com/vaadin/terminal/ReceiverOwner.java | 161 ++++++ .../gwt/client/ui/VDragAndDropWrapper.java | 258 ++++++---- .../terminal/gwt/client/ui/VUpload.java | 2 +- .../server/AbstractApplicationPortlet.java | 33 +- .../server/AbstractApplicationServlet.java | 15 +- .../server/AbstractCommunicationManager.java | 461 ++++++++++++------ .../gwt/server/AbstractReceivingEvent.java | 49 ++ .../gwt/server/CommunicationManager.java | 130 ++++- .../terminal/gwt/server/JsonPaintTarget.java | 9 + .../gwt/server/NoInputStreamException.java | 6 + .../gwt/server/NoOutputStreamException.java | 6 + .../server/PortletApplicationContext2.java | 1 + .../server/PortletCommunicationManager.java | 95 ++-- .../gwt/server/ReceivingEndedEventImpl.java | 15 + .../gwt/server/ReceivingFailedEventImpl.java | 22 + .../server/ReceivingProgressedEventImpl.java | 15 + .../gwt/server/ReceivingStartedEventImpl.java | 15 + .../terminal/gwt/server/UploadException.java | 12 + src/com/vaadin/ui/DragAndDropWrapper.java | 208 ++++---- src/com/vaadin/ui/Html5File.java | 137 ++++++ src/com/vaadin/ui/Upload.java | 251 ++++------ tests/src/com/vaadin/tests/dd/DDTest6.java | 2 +- .../com/vaadin/tests/dd/DragAndDropFiles.java | 129 +++++ .../src/com/vaadin/tests/dd/DragDropPane.java | 2 +- 27 files changed, 1502 insertions(+), 589 deletions(-) create mode 100644 src/com/vaadin/terminal/Receiver.java create mode 100644 src/com/vaadin/terminal/ReceiverOwner.java create mode 100644 src/com/vaadin/terminal/gwt/server/AbstractReceivingEvent.java create mode 100644 src/com/vaadin/terminal/gwt/server/NoInputStreamException.java create mode 100644 src/com/vaadin/terminal/gwt/server/NoOutputStreamException.java create mode 100644 src/com/vaadin/terminal/gwt/server/ReceivingEndedEventImpl.java create mode 100644 src/com/vaadin/terminal/gwt/server/ReceivingFailedEventImpl.java create mode 100644 src/com/vaadin/terminal/gwt/server/ReceivingProgressedEventImpl.java create mode 100644 src/com/vaadin/terminal/gwt/server/ReceivingStartedEventImpl.java create mode 100644 src/com/vaadin/terminal/gwt/server/UploadException.java create mode 100644 src/com/vaadin/ui/Html5File.java create mode 100644 tests/src/com/vaadin/tests/dd/DragAndDropFiles.java diff --git a/build/build.xml b/build/build.xml index ff98813857..e7cad185aa 100644 --- a/build/build.xml +++ b/build/build.xml @@ -180,7 +180,6 @@ - @@ -574,12 +573,7 @@ - - Compiling custom fileupload classes. - - - - + Compiling src (server-side) @@ -798,10 +792,6 @@ - - - - diff --git a/src/com/vaadin/terminal/PaintTarget.java b/src/com/vaadin/terminal/PaintTarget.java index 7bfc3555fc..52ac490863 100644 --- a/src/com/vaadin/terminal/PaintTarget.java +++ b/src/com/vaadin/terminal/PaintTarget.java @@ -151,6 +151,22 @@ public interface PaintTarget extends Serializable { */ public void addAttribute(String name, Resource value) throws PaintException; + /** + * Adds a Receiver attribute to component. Eg. in web terminals Receivers + * are typically URIs, where the client side can do an http post (multipart + * request). + * + * @param name + * the Attribute name + * @param value + * the Attribute value + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(ReceiverOwner owner, String name, Receiver value) + throws PaintException; + /** * Adds a long attribute to component. Atributes must be added before any * content is written. diff --git a/src/com/vaadin/terminal/Receiver.java b/src/com/vaadin/terminal/Receiver.java new file mode 100644 index 0000000000..6958cfdf0b --- /dev/null +++ b/src/com/vaadin/terminal/Receiver.java @@ -0,0 +1,29 @@ +package com.vaadin.terminal; + +import java.io.OutputStream; +import java.io.Serializable; + +/** + * + * Interface that must be implemented by the upload receivers to provide the + * Upload component an output stream to write the uploaded data. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 6.5 + */ +public interface Receiver extends Serializable { + + /** + * Invoked when a new upload arrives. + * + * @param filename + * the desired filename of the upload, usually as specified by + * the client. + * @param MIMEType + * the MIME type of the uploaded file. + * @return Stream to which the uploaded file should be written. + */ + public OutputStream receiveUpload(String filename, String MIMEType); +} diff --git a/src/com/vaadin/terminal/ReceiverOwner.java b/src/com/vaadin/terminal/ReceiverOwner.java new file mode 100644 index 0000000000..28ec83b067 --- /dev/null +++ b/src/com/vaadin/terminal/ReceiverOwner.java @@ -0,0 +1,161 @@ +package com.vaadin.terminal; + +import java.io.Serializable; + +import com.vaadin.Application; +import com.vaadin.terminal.ReceiverOwner.ReceivingController; + +/** + * Special kind of {@link VariableOwner} that can send and receive information + * with the terminal implementation about the progress of receiving data to its + * Receiver. The actual communication happens via {@link ReceivingController} + * which is fetched by the terminal when the Receiving is about to start. + */ +public interface ReceiverOwner extends VariableOwner { + + /* + * The monitor/control is passed to separate ReceivingContorller because: + * + * - possibly some component in the future may need support for streaming to + * multiple Receivers at the same time. + * + * - we don't want to bloat implementing ReceiverOwner's API. Now only one + * method is published and they can decide what event/methods to publish as + * their public API. + */ + + /** + * Returns a handle for the terminal via the ReceiverOwner can monitor and + * control the steaming of data to {@link Receiver}. + *

+ * Most commonly ReceiverOwner implementation wants to implement this method + * as final and reveal its own API for the end users. + * + * @param receiver + * the Receiver whose streaming is to be controlled + * @return a {@link ReceivingController} that will be used to control and + * monitor the progress of streaming + */ + ReceivingController getReceivingController(Receiver receiver); + + interface ReceivingEvent extends Serializable { + + /** + * @return the file name of the streamed file if known + */ + String getFileName(); + + /** + * @return the mime type of the streamed file if known + */ + String getMimeType(); + + /** + * @return the Receiver into which the content is being streamed + */ + Receiver getReceiver(); + + /** + * @return the length of the stream (in bytes) if known, else -1 + */ + long getContentLength(); + + /** + * @return then number of bytes streamed to Receiver + */ + long getBytesReceived(); + } + + /** + * Event passed to + * {@link ReceivingController#uploadStarted(ReceivingStartedEvent)} method + * before the streaming of the content to {@link Receiver} starts. + */ + public interface ReceivingStartedEvent extends ReceivingEvent { + } + + /** + * Event passed to + * {@link ReceivingController#onProgress(ReceivingProgressedEvent)} method + * during the streaming progresses. + */ + public interface ReceivingProgressedEvent extends ReceivingEvent { + } + + /** + * Event passed to + * {@link ReceivingController#uploadFinished(ReceivingEndedEvent)} method + * the contents have been streamed to Receiver successfully. + */ + public interface ReceivingEndedEvent extends ReceivingEvent { + } + + /** + * Event passed to + * {@link ReceivingController#uploadFailed(ReceivingFailedEvent)} method + * when the streaming ended before the end of the input. The streaming may + * fail due an interruption by {@link ReceivingController} or due an other + * unknown exception in communication. In the latter case the exception is + * also passed to + * {@link Application#terminalError(com.vaadin.terminal.Terminal.ErrorEvent)} + * . + */ + public interface ReceivingFailedEvent extends ReceivingEvent { + + /** + * @return the exception that caused the receiving not to finish cleanly + */ + Exception getException(); + + } + + public interface ReceivingController { + /** + * Whether the {@link #onProgress(long, long)} method should be called + * during the upload. + *

+ * {@link #onProgress(long, long)} is called in a synchronized block + * during the content is being received. This is potentially bit slow, + * so we are calling this method only if requested. The value is + * requested after the {@link #uploadStarted(ReceivingStartedEvent)} + * event, but not after each buffer reading. + * + * @return true if this ReceiverOwner wants to by notified during the + * upload of the progress of streaming. + * @see ReceiverOwner#onProgress(int, int) + */ + boolean listenProgress(); + + /** + * This method is called by the terminal if {@link #listenProgress()} + * returns true when the streaming starts. + */ + void onProgress(ReceivingProgressedEvent event); + + void uploadStarted(ReceivingStartedEvent event); + + void uploadFinished(ReceivingEndedEvent event); + + void uploadFailed(ReceivingFailedEvent event); + + /* + * Not synchronized to avoid stalls (caused by UIDL requests) while + * streaming the content. Implementations also most commonly atomic even + * without the restriction. + */ + /** + * ReceiverOwner can set this flag to true if it wants the Terminal to + * stop receiving current upload. + *

+ * Note, the usage of this method is not synchronized over the + * Application instance by the terminal like other methods. The + * implementation should only return a boolean field and especially not + * to modify UI or implement a synchronization by itself. + * + * @return true if the streaming should be interrupted as soon as + * possible. + */ + boolean isInterrupted(); + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/VDragAndDropWrapper.java b/src/com/vaadin/terminal/gwt/client/ui/VDragAndDropWrapper.java index ffbe25e9a7..702b324129 100644 --- a/src/com/vaadin/terminal/gwt/client/ui/VDragAndDropWrapper.java +++ b/src/com/vaadin/terminal/gwt/client/ui/VDragAndDropWrapper.java @@ -3,8 +3,13 @@ */ package com.vaadin.terminal.gwt.client.ui; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Set; +import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.JsArrayString; import com.google.gwt.event.dom.client.MouseDownEvent; @@ -33,7 +38,6 @@ import com.vaadin.terminal.gwt.client.ui.dd.VDropHandler; import com.vaadin.terminal.gwt.client.ui.dd.VHasDropHandler; import com.vaadin.terminal.gwt.client.ui.dd.VHtml5DragEvent; import com.vaadin.terminal.gwt.client.ui.dd.VHtml5File; -import com.vaadin.terminal.gwt.client.ui.dd.VHtml5File.Callback; import com.vaadin.terminal.gwt.client.ui.dd.VTransferable; import com.vaadin.terminal.gwt.client.ui.dd.VerticalDropLocation; @@ -97,45 +101,115 @@ public class VDragAndDropWrapper extends VCustomComponent implements private int dragStarMode; private int filecounter = 0; private boolean dragLeavPostponed; + private Map fileIdToReveiver; @Override public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { this.client = client; super.updateFromUIDL(uidl, client); if (!uidl.hasAttribute("cached") && !uidl.hasAttribute("hidden")) { - int childCount = uidl.getChildCount(); - if (childCount > 1) { - UIDL childUIDL = uidl.getChildUIDL(1); + UIDL acceptCrit = uidl.getChildByTagName("-ac"); + if (acceptCrit == null) { + dropHandler = null; + } else { if (dropHandler == null) { dropHandler = new CustomDropHandler(); } - dropHandler.updateAcceptRules(childUIDL); - } else { - dropHandler = null; + dropHandler.updateAcceptRules(acceptCrit); + } + + Set variableNames = uidl.getVariableNames(); + for (String fileId : variableNames) { + if (fileId.startsWith("rec-")) { + String receiverUrl = uidl.getStringVariable(fileId); + fileId = fileId.substring(4); + if (fileIdToReveiver == null) { + fileIdToReveiver = new HashMap(); + } + if ("".equals(receiverUrl)) { + Integer id = Integer.parseInt(fileId); + int indexOf = fileIds.indexOf(id); + if (indexOf != -1) { + files.remove(indexOf); + fileIds.remove(indexOf); + } + } else { + fileIdToReveiver.put(fileId, receiverUrl); + } + } } + startNextUpload(); dragStarMode = uidl.getIntAttribute("dragStartMode"); } } + private boolean uploading; + + private ReadyStateChangeHandler readyStateChangeHandler = new ReadyStateChangeHandler() { + public void onReadyStateChange(XMLHttpRequest xhr) { + if (xhr.getReadyState() == XMLHttpRequest.DONE) { + // visit server for possible + // variable changes + client.sendPendingVariableChanges(); + uploading = false; + startNextUpload(); + xhr.clearOnReadyStateChange(); + } + } + }; + + private void startNextUpload() { + DeferredCommand.addCommand(new Command() { + + public void execute() { + if (!uploading) { + if (fileIds.size() > 0) { + + uploading = true; + final Integer fileId = fileIds.remove(0); + VHtml5File file = files.remove(0); + final String receiverUrl = fileIdToReveiver + .remove(fileId.toString()); + ExtendedXHR extendedXHR = (ExtendedXHR) ExtendedXHR + .create(); + extendedXHR + .setOnReadyStateChange(readyStateChangeHandler); + extendedXHR.open("POST", receiverUrl); + extendedXHR.postFile(file); + + } + } + + } + }); + + } + public boolean html5DragEnter(VHtml5DragEvent event) { if (dropHandler == null) { return true; } - if (dragLeavPostponed) { - // returned quickly back to wrapper - dragLeavPostponed = false; + try { + + if (dragLeavPostponed) { + // returned quickly back to wrapper + dragLeavPostponed = false; + return false; + } + VTransferable transferable = new VTransferable(); + transferable.setDragSource(this); + + vaadinDragEvent = VDragAndDropManager.get().startDrag(transferable, + event, false); + VDragAndDropManager.get().setCurrentDropHandler(getDropHandler()); + event.preventDefault(); + event.stopPropagation(); return false; + } catch (Exception e) { + GWT.getUncaughtExceptionHandler().onUncaughtException(e); + return true; } - VTransferable transferable = new VTransferable(); - transferable.setDragSource(this); - - vaadinDragEvent = VDragAndDropManager.get().startDrag(transferable, - event, false); - VDragAndDropManager.get().setCurrentDropHandler(getDropHandler()); - event.preventDefault(); - event.stopPropagation(); - return false; } public boolean html5DragLeave(VHtml5DragEvent event) { @@ -143,24 +217,32 @@ public class VDragAndDropWrapper extends VCustomComponent implements return true; } - dragLeavPostponed = true; - DeferredCommand.addCommand(new Command() { - public void execute() { - // Yes, dragleave happens before drop. Makes no sense to me. - // IMO shouldn't fire leave at all if drop happens (I guess this - // is what IE does). - // In Vaadin we fire it only if drop did not happen. - if (dragLeavPostponed - && vaadinDragEvent != null - && VDragAndDropManager.get().getCurrentDropHandler() == getDropHandler()) { - VDragAndDropManager.get().interruptDrag(); + try { + + dragLeavPostponed = true; + DeferredCommand.addCommand(new Command() { + public void execute() { + // Yes, dragleave happens before drop. Makes no sense to me. + // IMO shouldn't fire leave at all if drop happens (I guess + // this + // is what IE does). + // In Vaadin we fire it only if drop did not happen. + if (dragLeavPostponed + && vaadinDragEvent != null + && VDragAndDropManager.get() + .getCurrentDropHandler() == getDropHandler()) { + VDragAndDropManager.get().interruptDrag(); + } + dragLeavPostponed = false; } - dragLeavPostponed = false; - } - }); - event.preventDefault(); - event.stopPropagation(); - return false; + }); + event.preventDefault(); + event.stopPropagation(); + return false; + } catch (Exception e) { + GWT.getUncaughtExceptionHandler().onUncaughtException(e); + return true; + } } public boolean html5DragOver(VHtml5DragEvent event) { @@ -188,41 +270,47 @@ public class VDragAndDropWrapper extends VCustomComponent implements if (dropHandler == null || !currentlyValid) { return true; } + try { - VTransferable transferable = vaadinDragEvent.getTransferable(); + VTransferable transferable = vaadinDragEvent.getTransferable(); - JsArrayString types = event.getTypes(); - for (int i = 0; i < types.length(); i++) { - String type = types.get(i); - if (isAcceptedType(type)) { - String data = event.getDataAsText(type); - if (data != null) { - transferable.setData(type, data); + JsArrayString types = event.getTypes(); + for (int i = 0; i < types.length(); i++) { + String type = types.get(i); + if (isAcceptedType(type)) { + String data = event.getDataAsText(type); + if (data != null) { + transferable.setData(type, data); + } } } - } - int fileCount = event.getFileCount(); - if (fileCount > 0) { - transferable.setData("filecount", fileCount); - for (int i = 0; i < fileCount; i++) { - final int fileId = filecounter++; - final VHtml5File file = event.getFile(i); - transferable.setData("fi" + i, "" + fileId); - transferable.setData("fn" + i, file.getName()); - transferable.setData("ft" + i, file.getType()); - transferable.setData("fs" + i, file.getSize()); - postFile(fileId, file); + int fileCount = event.getFileCount(); + if (fileCount > 0) { + transferable.setData("filecount", fileCount); + for (int i = 0; i < fileCount; i++) { + final int fileId = filecounter++; + final VHtml5File file = event.getFile(i); + transferable.setData("fi" + i, "" + fileId); + transferable.setData("fn" + i, file.getName()); + transferable.setData("ft" + i, file.getType()); + transferable.setData("fs" + i, file.getSize()); + queueFilePost(fileId, file); + } + } - } + VDragAndDropManager.get().endDrag(); + vaadinDragEvent = null; + event.preventDefault(); + event.stopPropagation(); - VDragAndDropManager.get().endDrag(); - vaadinDragEvent = null; - event.preventDefault(); - event.stopPropagation(); + return false; + } catch (Exception e) { + GWT.getUncaughtExceptionHandler().onUncaughtException(e); + return true; + } - return false; } protected String[] acceptedTypes = new String[] { "Text", "Url", @@ -242,10 +330,14 @@ public class VDragAndDropWrapper extends VCustomComponent implements protected ExtendedXHR() { } - public final native void sendBinary(JavaScriptObject data) + public final native void postFile(VHtml5File file) /*-{ - //this.overrideMimeType('text/plain; charset=x-user-defined-binary'); - this.sendAsBinary(data); + + // Accept header is readable in portlets resourceRequest + // TODO add filename and mime type too?? + this.setRequestHeader('Accept', 'text/html,vaadin/filexhr'); + this.setRequestHeader('Content-Type', 'multipart/form-data'); + this.send(file); }-*/; } @@ -257,38 +349,12 @@ public class VDragAndDropWrapper extends VCustomComponent implements * @param fileId * @param data */ - private void postFile(final int fileId, final VHtml5File file) { - DeferredCommand.addCommand(new Command() { - public void execute() { - /* - * File contents is sent deferred to allow quick reaction on GUI - * although file upload may last long. - */ - file.readAsBinary(new Callback() { - public void handleFile(final JavaScriptObject object) { - - ExtendedXHR extendedXHR = (ExtendedXHR) ExtendedXHR - .create(); - String name = "XHRFILE" + getPid() + "." + fileId; - extendedXHR - .setOnReadyStateChange(new ReadyStateChangeHandler() { - public void onReadyStateChange( - XMLHttpRequest xhr) { - if (xhr.getReadyState() == XMLHttpRequest.DONE) { - client.sendPendingVariableChanges(); - xhr.clearOnReadyStateChange(); - } - } - }); - extendedXHR.open("POST", client.getAppUri()); - multipartSend(extendedXHR, object, name); - - } - }); - - } - }); + private List fileIds = new ArrayList(); + private List files = new ArrayList(); + private void queueFilePost(final int fileId, final VHtml5File file) { + fileIds.add(fileId); + files.add(file); } private String getPid() { diff --git a/src/com/vaadin/terminal/gwt/client/ui/VUpload.java b/src/com/vaadin/terminal/gwt/client/ui/VUpload.java index 4cb7464a80..8b52434295 100644 --- a/src/com/vaadin/terminal/gwt/client/ui/VUpload.java +++ b/src/com/vaadin/terminal/gwt/client/ui/VUpload.java @@ -147,7 +147,7 @@ public class VUpload extends SimplePanel implements Paintable { this.client = client; paintableId = uidl.getId(); nextUploadId = uidl.getIntAttribute("nextid"); - element.setAction(client.getAppUri()); + element.setAction(uidl.getStringVariable("action")); submitButton.setText(uidl.getStringAttribute("buttoncaption")); fu.setName(paintableId + "_file"); diff --git a/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java b/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java index 8d5d328402..f6a88d9100 100644 --- a/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java +++ b/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java @@ -49,7 +49,6 @@ import com.liferay.portal.kernel.util.PortalClassInvoker; import com.liferay.portal.kernel.util.PropsUtil; import com.vaadin.Application; import com.vaadin.Application.SystemMessages; -import com.vaadin.external.org.apache.commons.fileupload.portlet.PortletFileUpload; import com.vaadin.terminal.DownloadStream; import com.vaadin.terminal.Terminal; import com.vaadin.terminal.gwt.client.ApplicationConfiguration; @@ -259,6 +258,8 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet } else if (request instanceof ResourceRequest) { if (isUIDLRequest((ResourceRequest) request)) { return RequestType.UIDL; + } else if (isFileUploadRequest((ResourceRequest) request)) { + return RequestType.FILE_UPLOAD; } else if (isApplicationResourceRequest((ResourceRequest) request)) { return RequestType.APPLICATION_RESOURCE; } else if (isDummyRequest((ResourceRequest) request)) { @@ -267,12 +268,7 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet return RequestType.STATIC_FILE; } } else if (request instanceof ActionRequest) { - if (isFileUploadRequest((ActionRequest) request)) { - return RequestType.FILE_UPLOAD; - } else { - // action other than upload - return RequestType.ACTION; - } + return RequestType.ACTION; } else if (request instanceof EventRequest) { return RequestType.EVENT; } @@ -294,8 +290,8 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet && request.getResourceID().equals("DUMMY"); } - private boolean isFileUploadRequest(ActionRequest request) { - return PortletFileUpload.isMultipartContent(request); + private boolean isFileUploadRequest(ResourceRequest request) { + return "UPLOAD".equals(request.getResourceID()); } /** @@ -356,19 +352,6 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet PortletCommunicationManager applicationManager = applicationContext .getApplicationManager(application); - if (response instanceof RenderResponse - && applicationManager.dummyURL == null) { - /* - * The application manager needs an URL to the dummy page. - * See the PortletCommunicationManager.sendUploadResponse - * method for more information. - */ - ResourceURL dummyURL = ((RenderResponse) response) - .createResourceURL(); - dummyURL.setResourceID("DUMMY"); - applicationManager.dummyURL = dummyURL.toString(); - } - /* Update browser information from request */ updateBrowserProperties(applicationContext.getBrowser(), request); @@ -438,7 +421,8 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet /* Handle the request */ if (requestType == RequestType.FILE_UPLOAD) { applicationManager.handleFileUpload( - (ActionRequest) request, (ActionResponse) response); + (ResourceRequest) request, + (ResourceResponse) response); return; } else if (requestType == RequestType.UIDL) { // Handles AJAX UIDL requests @@ -1054,7 +1038,8 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet Map config = new LinkedHashMap(); /* - * We need this in order to get uploads to work. + * We need this in order to get uploads to work. TODO this is not needed + * for uploads anymore, check if this is needed for some other things */ PortletURL appUri = response.createActionURL(); config.put("appUri", "'" + appUri.toString() + "'"); diff --git a/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java b/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java index f461fce404..aedfb2a204 100644 --- a/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java +++ b/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java @@ -34,7 +34,6 @@ import javax.servlet.http.HttpSession; import com.vaadin.Application; import com.vaadin.Application.SystemMessages; -import com.vaadin.external.org.apache.commons.fileupload.servlet.ServletFileUpload; import com.vaadin.terminal.DownloadStream; import com.vaadin.terminal.ParameterHandler; import com.vaadin.terminal.Terminal; @@ -177,6 +176,7 @@ public abstract class AbstractApplicationServlet extends HttpServlet implements private final String resourcePath = null; private int resourceCacheTime = 3600; + static final String UPLOAD_URL_PREFIX = "APP/UPLOAD/"; /** * Called by the servlet container to indicate to a servlet that the servlet @@ -1362,7 +1362,18 @@ public abstract class AbstractApplicationServlet extends HttpServlet implements } private boolean isFileUploadRequest(HttpServletRequest request) { - return ServletFileUpload.isMultipartContent(request); + String pathInfo = getRequestPathInfo(request); + + if (pathInfo == null) { + return false; + } + + if (pathInfo.startsWith("/" + UPLOAD_URL_PREFIX)) { + return true; + } + + return false; + } private boolean isOnUnloadRequest(HttpServletRequest request) { diff --git a/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java b/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java index 0773dca4ec..b9d6bf99fa 100644 --- a/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java +++ b/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java @@ -43,29 +43,26 @@ import javax.servlet.ServletResponse; import com.vaadin.Application; import com.vaadin.Application.SystemMessages; -import com.vaadin.external.org.apache.commons.fileupload.FileItemIterator; -import com.vaadin.external.org.apache.commons.fileupload.FileItemStream; -import com.vaadin.external.org.apache.commons.fileupload.FileUpload; -import com.vaadin.external.org.apache.commons.fileupload.FileUploadException; -import com.vaadin.external.org.apache.commons.fileupload.ProgressListener; import com.vaadin.terminal.ApplicationResource; import com.vaadin.terminal.DownloadStream; import com.vaadin.terminal.PaintException; import com.vaadin.terminal.PaintTarget; import com.vaadin.terminal.Paintable; import com.vaadin.terminal.Paintable.RepaintRequestEvent; +import com.vaadin.terminal.Receiver; +import com.vaadin.terminal.ReceiverOwner; +import com.vaadin.terminal.ReceiverOwner.ReceivingController; +import com.vaadin.terminal.ReceiverOwner.ReceivingEndedEvent; +import com.vaadin.terminal.ReceiverOwner.ReceivingFailedEvent; +import com.vaadin.terminal.ReceiverOwner.ReceivingStartedEvent; import com.vaadin.terminal.Terminal.ErrorEvent; import com.vaadin.terminal.Terminal.ErrorListener; import com.vaadin.terminal.URIHandler; -import com.vaadin.terminal.UploadStream; import com.vaadin.terminal.VariableOwner; import com.vaadin.terminal.gwt.client.ApplicationConnection; import com.vaadin.terminal.gwt.server.ComponentSizeValidator.InvalidLayout; import com.vaadin.ui.AbstractField; import com.vaadin.ui.Component; -import com.vaadin.ui.DragAndDropWrapper; -import com.vaadin.ui.Upload; -import com.vaadin.ui.Upload.UploadException; import com.vaadin.ui.Window; /** @@ -261,6 +258,12 @@ public abstract class AbstractCommunicationManager implements } + static class UploadInterruptedException extends Exception { + public UploadInterruptedException() { + super("Upload interrupted by other thread"); + } + } + private static String GET_PARAM_REPAINT_ALL = "repaintAll"; // flag used in the request to indicate that the security token should be @@ -296,6 +299,9 @@ public abstract class AbstractCommunicationManager implements private static final int MAX_BUFFER_SIZE = 64 * 1024; + /* Same as in apache commons file upload library that was previously used. */ + private static final int MAX_UPLOAD_BUFFER_SIZE = 4 * 1024; + private static final String GET_PARAM_ANALYZE_LAYOUTS = "analyzeLayouts"; private final ArrayList dirtyPaintables = new ArrayList(); @@ -332,146 +338,319 @@ public abstract class AbstractCommunicationManager implements requireLocale(application.getLocale().toString()); } - /** - * Create an upload handler that is appropriate to the context in which the - * application is being run (servlet or portlet). - * - * @return new {@link FileUpload} instance - */ - protected abstract FileUpload createFileUpload(); + protected Application getApplication() { + return application; + } - /** - * TODO New method - document me! - * - * @param upload - * @param request - * @return - * @throws IOException - * @throws FileUploadException - */ - protected abstract FileItemIterator getUploadItemIterator( - FileUpload upload, Request request) throws IOException, - FileUploadException; + private static final int LF = "\n".getBytes()[0]; + + private static final String CRLF = "\r\n"; + + private static String readLine(InputStream stream) throws IOException { + StringBuilder sb = new StringBuilder(); + int readByte = stream.read(); + while (readByte != LF) { + char c = (char) readByte; + sb.append(c); + readByte = stream.read(); + } + + return sb.substring(0, sb.length() - 1); + } /** - * TODO New method - document me! + * Method used to stream content from a multipart request (either from + * servlet or portlet request) to given Receiver + * * * @param request * @param response + * @param receiver + * @param owner + * @param boundary * @throws IOException - * @throws FileUploadException */ - protected void doHandleFileUpload(Request request, Response response) - throws IOException, FileUploadException { + protected void doHandleSimpleMultipartFileUpload(Request request, + Response response, Receiver receiver, ReceiverOwner owner, + String boundary) throws IOException { + boundary = CRLF + "--" + boundary + "--"; + + // multipart parsing, supports only one file for request, but that is + // fine for our current terminal - // Create a new file upload handler - final FileUpload upload = createFileUpload(); + final InputStream inputStream = request.getInputStream(); - final UploadProgressListener pl = new UploadProgressListener(); + int contentLength = request.getContentLength(); - upload.setProgressListener(pl); + boolean atStart = false; - // Parse the request - FileItemIterator iter; + String rawfilename = "unknown"; + String rawMimeType = "application/octet-stream"; - try { - iter = getUploadItemIterator(upload, request); - /* - * ATM this loop is run only once as we are uploading one file per - * request. + /* + * Read the stream until the actual file starts (empty line). Read + * filename and content type from multipart headers. + */ + while (!atStart) { + String readLine = readLine(inputStream); + contentLength -= (readLine.length() + 2); + if (readLine.startsWith("Content-Disposition:") + && readLine.indexOf("filename=") > 0) { + rawfilename = readLine.replaceAll(".*filename=", ""); + String parenthesis = rawfilename.substring(0, 1); + rawfilename = rawfilename.substring(1); + rawfilename = rawfilename.substring(0, + rawfilename.indexOf(parenthesis)); + } else if (readLine.equals("")) { + atStart = true; + } else if (readLine.startsWith("Content-Type")) { + rawMimeType = readLine.split(": ")[1]; + } + } + + contentLength -= (boundary.length() + 2); // 2 == CRLF + + final char[] charArray = boundary.toCharArray(); + + /* + * Reads bytes from the underlaying stream. Compares the read bytes to + * the boundary string and returns -1 if met. + * + * The maching happens so that if the read byte equals to the first char + * of boundary string, the stream goes to "buffering mode". In buffering + * mode bytes are read until the character does not match the + * corresponding from boundary string or the full boundary string is + * found. + */ + InputStream simpleMultiPartReader = new InputStream() { + int matchedCount = 0; + int curBoundaryIndex = 0; + /** + * The byte found after a "promising start for boundary" */ - while (iter.hasNext()) { - final FileItemStream item = iter.next(); - final String name = item.getFieldName(); - // Should report only the filename even if the browser sends the - // path - final String filename = removePath(item.getName()); - final String mimeType = item.getContentType(); - final InputStream stream = item.openStream(); - if (item.isFormField()) { - // ignored, upload requests contains only files + private int bufferedByte = -1; + private boolean atTheEnd = false; + + @Override + public int read() throws IOException { + if (atTheEnd) { + return -1; + } else if (bufferedByte >= 0) { + return getBuffered(); } else { - final UploadStream upstream = new UploadStream() { - - public String getContentName() { - return filename; + int fromActualStream = inputStream.read(); + if (fromActualStream == -1) { + // unexpected end of stream + throw new IOException( + "The multipart stream ended unexpectedly"); + } + if (charArray[matchedCount] == fromActualStream) { + while (true) { + matchedCount++; + if (matchedCount == charArray.length) { + // reached the end of file + atTheEnd = true; + return -1; + } + fromActualStream = inputStream.read(); + if (fromActualStream != charArray[matchedCount]) { + // Did not found full boundary, cache the last + // byte + bufferedByte = fromActualStream; + return getBuffered(); + } } + } + return fromActualStream; + } + } - public String getContentType() { - return mimeType; - } + private int getBuffered() throws IOException { + int b; + if (matchedCount == 0) { + b = bufferedByte; + bufferedByte = -1; + } else { + b = charArray[curBoundaryIndex++]; + if (curBoundaryIndex == matchedCount) { + matchedCount = 0; + curBoundaryIndex = 0; + // next call for getBuffered will return the + // bufferedByte, not from the char array. + } + } + if (b == -1) { + throw new IOException( + "The multipart stream ended unexpectedly"); + } + return b; + } + }; - public InputStream getStream() { - return stream; - } + /* + * Should report only the filename even if the browser sends the path + */ + final String filename = removePath(rawfilename); + final String mimeType = rawMimeType; - public String getStreamName() { - return "stream"; - } + try { + /* + * safe cast as in GWT terminal all variable owners are expected to + * be components. + */ + Component component = (Component) owner; + if (component.isReadOnly()) { + throw new UploadException( + "Warning: file upload ignored because the componente was read-only"); + } + streamToReceiver(simpleMultiPartReader, receiver, owner, filename, + mimeType, contentLength); + } catch (Exception e) { + synchronized (application) { + handleChangeVariablesError(application, (Component) owner, e, + new HashMap()); + } + } + sendUploadResponse(request, response); - }; + } - if (name.startsWith("XHRFILE")) { - String[] split = item.getFieldName().substring(7) - .split("\\."); - DragAndDropWrapper ddw = (DragAndDropWrapper) idPaintableMap - .get(split[0]); + /** + * Used to stream plain file post (aka XHR2.post(File)) + * + * @param request + * @param response + * @param receiver + * @param owner + * @param contentLength + * @throws IOException + */ + protected void doHandleXhrFilePost(Request request, Response response, + Receiver receiver, ReceiverOwner owner, int contentLength) + throws IOException { - try { - ddw.receiveFile(upstream, split[1]); - } catch (UploadException e) { - synchronized (application) { - handleChangeVariablesError(application, ddw, e, - new HashMap()); - } - } + // These are unknown in filexhr ATM, maybe add to Accept header that + // is accessible in portlets + final String filename = "unknown"; + final String mimeType = filename; + final InputStream stream = request.getInputStream(); + try { + /* + * safe cast as in GWT terminal all variable owners are expected to + * be components. + */ + Component component = (Component) owner; + if (component.isReadOnly()) { + throw new UploadException( + "Warning: file upload ignored because the componente was read-only"); + } + streamToReceiver(stream, receiver, owner, filename, mimeType, + contentLength); + } catch (Exception e) { + synchronized (application) { + handleChangeVariablesError(application, (Component) owner, e, + new HashMap()); + } + } + sendUploadResponse(request, response); + } - } else { + protected final void streamToReceiver(final InputStream in, + Receiver receiver, ReceiverOwner source, String filename, + String type, int contentLength) throws UploadException { + if (receiver == null) { + throw new IllegalStateException("Receiver for the post not found"); + } - int separatorPos = name.lastIndexOf("_"); - final String pid = name.substring(0, separatorPos); - final Upload uploadComponent = (Upload) idPaintableMap - .get(pid); - if (uploadComponent == null) { - throw new FileUploadException( - "Upload component not found"); - } - if (uploadComponent.isReadOnly()) { - throw new FileUploadException( - "Warning: ignored file upload because upload component is set as read-only"); - } - synchronized (application) { - // put upload component into receiving state - uploadComponent.startUpload(); - } + ReceivingController controller = source + .getReceivingController(receiver); - // tell UploadProgressListener which component is - // receiving - // file - pl.setUpload(uploadComponent); + final Application application = getApplication(); - try { - uploadComponent.receiveUpload(upstream); - } catch (UploadException e) { - // error happened while receiving file. Handle the - // error in the same manner as it would have - // happened in - // variable change. - synchronized (application) { - handleChangeVariablesError(application, - uploadComponent, e, - new HashMap()); - } - } + OutputStream out = null; + try { + boolean listenProgress; + synchronized (application) { + ReceivingStartedEvent startedEvent = new ReceivingStartedEventImpl( + receiver, filename, type, contentLength); + controller.uploadStarted(startedEvent); + out = receiver.receiveUpload(filename, type); + listenProgress = controller.listenProgress(); + } + + // Gets the output target stream + if (out == null) { + throw new NoOutputStreamException(); + } + + if (null == in) { + // No file, for instance non-existent filename in html upload + throw new NoInputStreamException(); + } + + final byte buffer[] = new byte[MAX_UPLOAD_BUFFER_SIZE]; + int bytesReadToBuffer = 0; + int totalBytes = 0; + ReceivingProgressedEventImpl progressEvent = new ReceivingProgressedEventImpl( + receiver, filename, type, contentLength); + + while ((bytesReadToBuffer = in.read(buffer)) > 0) { + out.write(buffer, 0, bytesReadToBuffer); + totalBytes += bytesReadToBuffer; + if (listenProgress) { + // update progress if listener set and contentLength + // received + synchronized (application) { + /* + * Note, we are reusing the same progress event, not to + * pollute VM with lots of objects. This might not help + * though if the end user aggressively updates UI on + * each onProgress. + */ + progressEvent.setBytesReceived(totalBytes); + controller.onProgress(progressEvent); } + } + if (controller.isInterrupted()) { + throw new UploadInterruptedException(); + } + } + + // upload successful + out.close(); + ReceivingEndedEvent event = new ReceivingEndedEventImpl(receiver, + filename, type, totalBytes); + synchronized (application) { + controller.uploadFinished(event); + } + } catch (UploadInterruptedException e) { + // Download interrupted by application code + try { + // still try to close output stream (e.g. file handle) + out.close(); + } catch (IOException e1) { + // NOP + } + ReceivingFailedEvent event = new ReceivingFailedEventImpl(receiver, + filename, type, contentLength, e); + synchronized (application) { + controller.uploadFailed(event); + } + // Note, we are not throwing interrupted exception forward as it is + // not a terminal level erro like all other exception. + } catch (final Exception e) { + synchronized (application) { + ReceivingFailedEvent event = new ReceivingFailedEventImpl( + receiver, filename, type, contentLength, e); + synchronized (application) { + controller.uploadFailed(event); } + // throw exception for terminal to be handled (to be passed to + // terminalErrorHandler) + throw new UploadException(e); } - } catch (final FileUploadException e) { - throw e; } - - sendUploadResponse(request, response); } /** @@ -722,14 +901,19 @@ public abstract class AbstractCommunicationManager implements } else { // remove detached components from paintableIdMap so they // can be GC'ed + /* + * TODO figure out if we could move this beyond the painting + * phase, "respond as fast as possible, then do the cleanup". + * Beware of painting the dirty detatched components. + */ for (Iterator it = paintableIdMap.keySet() .iterator(); it.hasNext();) { Component p = (Component) it.next(); if (p.getApplication() == null) { + unregisterPaintable(p); idPaintableMap.remove(paintableIdMap.get(p)); it.remove(); dirtyPaintables.remove(p); - p.removeListener(this); } } paintables = getDirtyVisibleComponents(window); @@ -977,6 +1161,16 @@ public abstract class AbstractCommunicationManager implements } + /** + * Called when communication manager stops listening for repaints for given + * component. + * + * @param p + */ + protected void unregisterPaintable(Component p) { + p.removeListener(this); + } + /** * TODO document * @@ -1148,7 +1342,7 @@ public abstract class AbstractCommunicationManager implements return success; } - private VariableOwner getVariableOwner(String string) { + protected VariableOwner getVariableOwner(String string) { VariableOwner owner = (VariableOwner) idPaintableMap.get(string); if (owner == null && string.startsWith("DD")) { return getDragAndDropService(); @@ -1807,30 +2001,6 @@ public abstract class AbstractCommunicationManager implements } } - /* - * Upload progress listener notifies upload component once when Jakarta - * FileUpload can determine content length. Used to detect files total size, - * uploads progress can be tracked inside upload. - */ - private class UploadProgressListener implements ProgressListener, - Serializable { - - Upload uploadComponent; - - boolean updated = false; - - public void setUpload(Upload u) { - uploadComponent = u; - } - - public void update(long bytesRead, long contentLength, int items) { - if (!updated && uploadComponent != null) { - uploadComponent.setUploadSize(contentLength); - updated = true; - } - } - } - /** * Helper method to test if a component contains another * @@ -1964,4 +2134,7 @@ public abstract class AbstractCommunicationManager implements } } + + abstract String createReceiverUrl(ReceiverOwner owner, String name, + Receiver value); } diff --git a/src/com/vaadin/terminal/gwt/server/AbstractReceivingEvent.java b/src/com/vaadin/terminal/gwt/server/AbstractReceivingEvent.java new file mode 100644 index 0000000000..2e84538e92 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/server/AbstractReceivingEvent.java @@ -0,0 +1,49 @@ +package com.vaadin.terminal.gwt.server; + +import com.vaadin.terminal.Receiver; +import com.vaadin.terminal.ReceiverOwner.ReceivingEvent; + +/** + * Abstract base class for ReceivingEvent implementations. + */ +@SuppressWarnings("serial") +abstract class AbstractReceivingEvent implements ReceivingEvent { + private final String type; + private final String filename; + private Receiver receiver; + private long contentLength; + private long bytesReceived; + + public String getFileName() { + return filename; + } + + public String getMimeType() { + return type; + } + + public AbstractReceivingEvent(Receiver receiver, String filename, + String type, long length) { + this.receiver = receiver; + this.filename = filename; + this.type = type; + contentLength = length; + } + + public Receiver getReceiver() { + return receiver; + } + + public long getContentLength() { + return contentLength; + } + + public long getBytesReceived() { + return bytesReceived; + } + + void setBytesReceived(long bytesReceived) { + this.bytesReceived = bytesReceived; + } + +} diff --git a/src/com/vaadin/terminal/gwt/server/CommunicationManager.java b/src/com/vaadin/terminal/gwt/server/CommunicationManager.java index c48af80305..b257559231 100644 --- a/src/com/vaadin/terminal/gwt/server/CommunicationManager.java +++ b/src/com/vaadin/terminal/gwt/server/CommunicationManager.java @@ -7,6 +7,9 @@ package com.vaadin.terminal.gwt.server; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -14,12 +17,12 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import com.vaadin.Application; -import com.vaadin.external.org.apache.commons.fileupload.FileItemIterator; -import com.vaadin.external.org.apache.commons.fileupload.FileUpload; -import com.vaadin.external.org.apache.commons.fileupload.FileUploadException; -import com.vaadin.external.org.apache.commons.fileupload.servlet.ServletFileUpload; import com.vaadin.terminal.ApplicationResource; import com.vaadin.terminal.DownloadStream; +import com.vaadin.terminal.Paintable; +import com.vaadin.terminal.Receiver; +import com.vaadin.terminal.ReceiverOwner; +import com.vaadin.ui.Component; import com.vaadin.ui.Window; /** @@ -202,34 +205,56 @@ public class CommunicationManager extends AbstractCommunicationManager { super(application); } - @Override - protected FileUpload createFileUpload() { - return new ServletFileUpload(); - } - - @Override - protected FileItemIterator getUploadItemIterator(FileUpload upload, - Request request) throws IOException, FileUploadException { - return ((ServletFileUpload) upload) - .getItemIterator((HttpServletRequest) request - .getWrappedRequest()); - } - /** * Handles file upload request submitted via Upload component. * - * TODO document + * @see #createReceiverUrl(ReceiverOwner, String, Receiver) * * @param request * @param response * @throws IOException - * @throws FileUploadException + * @throws InvalidUIDLSecurityKeyException */ public void handleFileUpload(HttpServletRequest request, HttpServletResponse response) throws IOException, - FileUploadException { - doHandleFileUpload(new HttpServletRequestWrapper(request), - new HttpServletResponseWrapper(response)); + InvalidUIDLSecurityKeyException { + + /* + * URI pattern: APP/UPPLOAD/[PID]/[NAME]/[SECKEY] See #createReceiverUrl + */ + + String pathInfo = request.getPathInfo(); + // strip away part until the data we are interested starts + int startOfData = pathInfo + .indexOf(AbstractApplicationServlet.UPLOAD_URL_PREFIX) + + AbstractApplicationServlet.UPLOAD_URL_PREFIX.length(); + String uppUri = pathInfo.substring(startOfData); + String[] parts = uppUri.split("/", 3); // 0 = pid, 1= name, 2 = sec key + + Receiver receiver = pidToNameToReceiver.get(parts[0]).remove(parts[1]); + String secKey = receiverToSeckey.remove(receiver); + if (secKey.equals(parts[2])) { + + ReceiverOwner source = (ReceiverOwner) getVariableOwner(parts[0]); + String contentType = request.getContentType(); + if (request.getContentType().contains("boundary")) { + // Multipart requests contain boundary string + doHandleSimpleMultipartFileUpload( + new HttpServletRequestWrapper(request), + new HttpServletResponseWrapper(response), receiver, + source, contentType.split("boundary=")[1]); + } else { + // if boundary string does not exist, the posted file is from + // XHR2.post(File) + doHandleXhrFilePost(new HttpServletRequestWrapper(request), + new HttpServletResponseWrapper(response), receiver, + source, request.getContentLength()); + } + } else { + throw new InvalidUIDLSecurityKeyException( + "Security key in upload post did not match!"); + } + } /** @@ -308,4 +333,65 @@ public class CommunicationManager extends AbstractCommunicationManager { new AbstractApplicationServletWrapper(applicationServlet)); } + @Override + protected void unregisterPaintable(Component p) { + /* Cleanup possible receivers */ + if (pidToNameToReceiver != null && p instanceof ReceiverOwner) { + Map removed = pidToNameToReceiver + .remove(getPaintableId(p)); + if (removed != null) { + for (String key : removed.keySet()) { + receiverToSeckey.remove(removed.get(key)); + } + } + } + super.unregisterPaintable(p); + + } + + private Map> pidToNameToReceiver; + + private Map receiverToSeckey; + + @Override + String createReceiverUrl(ReceiverOwner owner, String name, Receiver value) { + + /* + * We will use the same APP/* URI space as ApplicationResources but + * prefix url with UPLOAD + * + * eg. APP/UPPLOAD/[PID]/[NAME]/[SECKEY] + * + * SECKEY is created on each paint to make URL's unpredictable (to + * prevent CSRF attacks). + * + * NAME and PID from URI forms a key to fetch Receiver when handling + * post + */ + String paintableId = getPaintableId((Paintable) owner); + String key = paintableId + "/" + name; + + if (pidToNameToReceiver == null) { + pidToNameToReceiver = new HashMap>(); + } + Map nameToReceiver = pidToNameToReceiver + .get(paintableId); + if (nameToReceiver == null) { + nameToReceiver = new HashMap(); + pidToNameToReceiver.put(paintableId, nameToReceiver); + } + nameToReceiver.put(name, value); + + if (receiverToSeckey == null) { + receiverToSeckey = new HashMap(); + } + String seckey = UUID.randomUUID().toString(); + receiverToSeckey.put(value, seckey); + + return getApplication().getURL() + + AbstractApplicationServlet.UPLOAD_URL_PREFIX + key + "/" + + seckey; + + } + } diff --git a/src/com/vaadin/terminal/gwt/server/JsonPaintTarget.java b/src/com/vaadin/terminal/gwt/server/JsonPaintTarget.java index c94fd68473..b50fc0a8a1 100644 --- a/src/com/vaadin/terminal/gwt/server/JsonPaintTarget.java +++ b/src/com/vaadin/terminal/gwt/server/JsonPaintTarget.java @@ -27,6 +27,8 @@ import com.vaadin.terminal.ExternalResource; import com.vaadin.terminal.PaintException; import com.vaadin.terminal.PaintTarget; import com.vaadin.terminal.Paintable; +import com.vaadin.terminal.Receiver; +import com.vaadin.terminal.ReceiverOwner; import com.vaadin.terminal.Resource; import com.vaadin.terminal.ThemeResource; import com.vaadin.terminal.VariableOwner; @@ -1102,4 +1104,11 @@ public class JsonPaintTarget implements PaintTarget { Collection> getUsedPaintableTypes() { return usedPaintableTypes; } + + public void addVariable(ReceiverOwner owner, String name, Receiver value) + throws PaintException { + String url = manager.createReceiverUrl(owner, name, value); + addVariable(owner, name, url); + } + } diff --git a/src/com/vaadin/terminal/gwt/server/NoInputStreamException.java b/src/com/vaadin/terminal/gwt/server/NoInputStreamException.java new file mode 100644 index 0000000000..72b99342cf --- /dev/null +++ b/src/com/vaadin/terminal/gwt/server/NoInputStreamException.java @@ -0,0 +1,6 @@ +package com.vaadin.terminal.gwt.server; + +@SuppressWarnings("serial") +public class NoInputStreamException extends Exception { + +} diff --git a/src/com/vaadin/terminal/gwt/server/NoOutputStreamException.java b/src/com/vaadin/terminal/gwt/server/NoOutputStreamException.java new file mode 100644 index 0000000000..6c33dab33e --- /dev/null +++ b/src/com/vaadin/terminal/gwt/server/NoOutputStreamException.java @@ -0,0 +1,6 @@ +package com.vaadin.terminal.gwt.server; + +@SuppressWarnings("serial") +public class NoOutputStreamException extends Exception { + +} diff --git a/src/com/vaadin/terminal/gwt/server/PortletApplicationContext2.java b/src/com/vaadin/terminal/gwt/server/PortletApplicationContext2.java index ceb26d1fb1..1b3585cb80 100644 --- a/src/com/vaadin/terminal/gwt/server/PortletApplicationContext2.java +++ b/src/com/vaadin/terminal/gwt/server/PortletApplicationContext2.java @@ -250,6 +250,7 @@ public class PortletApplicationContext2 extends AbstractWebApplicationContext { return resourceURL.toString(); } else { // in a background thread or otherwise outside a request + // TODO exception ?? return null; } } diff --git a/src/com/vaadin/terminal/gwt/server/PortletCommunicationManager.java b/src/com/vaadin/terminal/gwt/server/PortletCommunicationManager.java index 57226588f9..37beafa172 100644 --- a/src/com/vaadin/terminal/gwt/server/PortletCommunicationManager.java +++ b/src/com/vaadin/terminal/gwt/server/PortletCommunicationManager.java @@ -7,9 +7,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; -import javax.portlet.ActionRequest; -import javax.portlet.ActionResponse; import javax.portlet.ClientDataRequest; import javax.portlet.MimeResponse; import javax.portlet.PortletRequest; @@ -17,15 +17,16 @@ import javax.portlet.PortletResponse; import javax.portlet.PortletSession; import javax.portlet.ResourceRequest; import javax.portlet.ResourceResponse; +import javax.portlet.ResourceURL; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequestWrapper; import com.vaadin.Application; -import com.vaadin.external.org.apache.commons.fileupload.FileItemIterator; -import com.vaadin.external.org.apache.commons.fileupload.FileUpload; -import com.vaadin.external.org.apache.commons.fileupload.FileUploadException; -import com.vaadin.external.org.apache.commons.fileupload.portlet.PortletFileUpload; import com.vaadin.terminal.DownloadStream; +import com.vaadin.terminal.Paintable; +import com.vaadin.terminal.Receiver; +import com.vaadin.terminal.ReceiverOwner; +import com.vaadin.ui.Component; import com.vaadin.ui.Window; /** @@ -37,7 +38,7 @@ import com.vaadin.ui.Window; @SuppressWarnings("serial") public class PortletCommunicationManager extends AbstractCommunicationManager { - protected String dummyURL; + private ResourceResponse currentUidlResponse; private static class PortletRequestWrapper implements Request { @@ -191,39 +192,36 @@ public class PortletCommunicationManager extends AbstractCommunicationManager { super(application); } - @Override - protected FileUpload createFileUpload() { - return new PortletFileUpload(); - } - - @Override - protected FileItemIterator getUploadItemIterator(FileUpload upload, - Request request) throws IOException, FileUploadException { - return ((PortletFileUpload) upload) - .getItemIterator((ActionRequest) request.getWrappedRequest()); - } + public void handleFileUpload(ResourceRequest request, + ResourceResponse response) throws IOException { + String contentType = request.getContentType(); + String name = request.getParameter("name"); + String ownerId = request.getParameter("rec-owner"); + ReceiverOwner variableOwner = (ReceiverOwner) getVariableOwner(ownerId); + Receiver receiver = ownerToNameToReceiver.get(variableOwner).remove( + name); + + // clean up, may be re added on next paint + ownerToNameToReceiver.get(variableOwner).remove(name); + + if (contentType.contains("boundary")) { + doHandleSimpleMultipartFileUpload( + new PortletRequestWrapper(request), + new PortletResponseWrapper(response), receiver, + variableOwner, contentType.split("boundary=")[1]); + } else { + doHandleXhrFilePost(new PortletRequestWrapper(request), + new PortletResponseWrapper(response), receiver, + variableOwner, request.getContentLength()); + } - public void handleFileUpload(ActionRequest request, ActionResponse response) - throws FileUploadException, IOException { - doHandleFileUpload(new PortletRequestWrapper(request), - new PortletResponseWrapper(response)); } @Override - protected void sendUploadResponse(Request request, Response response) - throws IOException { - if (response.getWrappedResponse() instanceof ActionResponse) { - /* - * If we do not redirect to some other page, the entire portal page - * will be re-printed into the target of the upload request (an - * IFRAME), which in turn will cause very strange side effects. - */ - System.out.println("Redirecting to dummyURL: " + dummyURL); - ((ActionResponse) response.getWrappedResponse()) - .sendRedirect(dummyURL == null ? "http://www.google.com" - : dummyURL); - } else { - super.sendUploadResponse(request, response); + protected void unregisterPaintable(Component p) { + super.unregisterPaintable(p); + if (ownerToNameToReceiver != null) { + ownerToNameToReceiver.remove(p); } } @@ -231,10 +229,12 @@ public class PortletCommunicationManager extends AbstractCommunicationManager { ResourceResponse response, AbstractApplicationPortlet applicationPortlet, Window window) throws InvalidUIDLSecurityKeyException, IOException { + currentUidlResponse = response; doHandleUidlRequest(new PortletRequestWrapper(request), new PortletResponseWrapper(response), new AbstractApplicationPortletWrapper(applicationPortlet), window); + currentUidlResponse = null; } DownloadStream handleURI(Window window, ResourceRequest request, @@ -265,9 +265,32 @@ public class PortletCommunicationManager extends AbstractCommunicationManager { Window getApplicationWindow(PortletRequest request, AbstractApplicationPortlet applicationPortlet, Application application, Window assumedWindow) { + return doGetApplicationWindow(new PortletRequestWrapper(request), new AbstractApplicationPortletWrapper(applicationPortlet), application, assumedWindow); } + private Map> ownerToNameToReceiver; + + @Override + String createReceiverUrl(ReceiverOwner owner, String name, Receiver value) { + if (ownerToNameToReceiver == null) { + ownerToNameToReceiver = new HashMap>(); + } + Map nameToReceiver = ownerToNameToReceiver.get(owner); + if (nameToReceiver == null) { + nameToReceiver = new HashMap(); + ownerToNameToReceiver.put(owner, nameToReceiver); + } + nameToReceiver.put(name, value); + ResourceURL resurl = currentUidlResponse.createResourceURL(); + resurl.setResourceID("UPLOAD"); + resurl.setParameter("name", name); + resurl.setParameter("rec-owner", getPaintableId((Paintable) owner)); + resurl.setProperty("name", name); + resurl.setProperty("rec-owner", getPaintableId((Paintable) owner)); + return resurl.toString(); + } + } diff --git a/src/com/vaadin/terminal/gwt/server/ReceivingEndedEventImpl.java b/src/com/vaadin/terminal/gwt/server/ReceivingEndedEventImpl.java new file mode 100644 index 0000000000..e5519abf56 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/server/ReceivingEndedEventImpl.java @@ -0,0 +1,15 @@ +package com.vaadin.terminal.gwt.server; + +import com.vaadin.terminal.Receiver; +import com.vaadin.terminal.ReceiverOwner.ReceivingEndedEvent; + +@SuppressWarnings("serial") +class ReceivingEndedEventImpl extends AbstractReceivingEvent implements + ReceivingEndedEvent { + + public ReceivingEndedEventImpl(Receiver receiver, String filename, + String type, long totalBytes) { + super(receiver, filename, type, totalBytes); + } + +} diff --git a/src/com/vaadin/terminal/gwt/server/ReceivingFailedEventImpl.java b/src/com/vaadin/terminal/gwt/server/ReceivingFailedEventImpl.java new file mode 100644 index 0000000000..f57675af65 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/server/ReceivingFailedEventImpl.java @@ -0,0 +1,22 @@ +package com.vaadin.terminal.gwt.server; + +import com.vaadin.terminal.Receiver; +import com.vaadin.terminal.ReceiverOwner.ReceivingFailedEvent; + +@SuppressWarnings("serial") +class ReceivingFailedEventImpl extends AbstractReceivingEvent implements + ReceivingFailedEvent { + + private final Exception exception; + + public ReceivingFailedEventImpl(Receiver receiver, final String filename, + final String type, long contentLength, final Exception exception) { + super(receiver, filename, type, contentLength); + this.exception = exception; + } + + public Exception getException() { + return exception; + } + +} diff --git a/src/com/vaadin/terminal/gwt/server/ReceivingProgressedEventImpl.java b/src/com/vaadin/terminal/gwt/server/ReceivingProgressedEventImpl.java new file mode 100644 index 0000000000..a098cf0be9 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/server/ReceivingProgressedEventImpl.java @@ -0,0 +1,15 @@ +package com.vaadin.terminal.gwt.server; + +import com.vaadin.terminal.Receiver; +import com.vaadin.terminal.ReceiverOwner.ReceivingProgressedEvent; + +@SuppressWarnings("serial") +class ReceivingProgressedEventImpl extends AbstractReceivingEvent implements + ReceivingProgressedEvent { + + public ReceivingProgressedEventImpl(Receiver receiver, + final String filename, final String type, long contentLength) { + super(receiver, filename, type, contentLength); + } + +} diff --git a/src/com/vaadin/terminal/gwt/server/ReceivingStartedEventImpl.java b/src/com/vaadin/terminal/gwt/server/ReceivingStartedEventImpl.java new file mode 100644 index 0000000000..119d169bec --- /dev/null +++ b/src/com/vaadin/terminal/gwt/server/ReceivingStartedEventImpl.java @@ -0,0 +1,15 @@ +package com.vaadin.terminal.gwt.server; + +import com.vaadin.terminal.Receiver; +import com.vaadin.terminal.ReceiverOwner.ReceivingStartedEvent; + +@SuppressWarnings("serial") +class ReceivingStartedEventImpl extends AbstractReceivingEvent implements + ReceivingStartedEvent { + + public ReceivingStartedEventImpl(Receiver receiver, final String filename, + final String type, long contentLength) { + super(receiver, filename, type, contentLength); + } + +} diff --git a/src/com/vaadin/terminal/gwt/server/UploadException.java b/src/com/vaadin/terminal/gwt/server/UploadException.java new file mode 100644 index 0000000000..ef3b2554d2 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/server/UploadException.java @@ -0,0 +1,12 @@ +package com.vaadin.terminal.gwt.server; + +@SuppressWarnings("serial") +public class UploadException extends Exception { + public UploadException(Exception e) { + super("Upload failed", e); + } + + public UploadException(String msg) { + super(msg); + } +} diff --git a/src/com/vaadin/ui/DragAndDropWrapper.java b/src/com/vaadin/ui/DragAndDropWrapper.java index 87ff571f36..afcb70506b 100644 --- a/src/com/vaadin/ui/DragAndDropWrapper.java +++ b/src/com/vaadin/ui/DragAndDropWrapper.java @@ -3,10 +3,6 @@ */ package com.vaadin.ui; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.Serializable; import java.util.HashMap; import java.util.Map; @@ -19,23 +15,34 @@ import com.vaadin.event.dd.TargetDetails; import com.vaadin.event.dd.TargetDetailsImpl; import com.vaadin.terminal.PaintException; import com.vaadin.terminal.PaintTarget; -import com.vaadin.terminal.UploadStream; +import com.vaadin.terminal.Receiver; +import com.vaadin.terminal.ReceiverOwner; import com.vaadin.terminal.gwt.client.MouseEventDetails; import com.vaadin.terminal.gwt.client.ui.VDragAndDropWrapper; import com.vaadin.terminal.gwt.client.ui.dd.HorizontalDropLocation; import com.vaadin.terminal.gwt.client.ui.dd.VerticalDropLocation; -import com.vaadin.terminal.gwt.server.AbstractApplicationServlet; -import com.vaadin.ui.DragAndDropWrapper.WrapperTransferable.Html5File; -import com.vaadin.ui.Upload.Receiver; -import com.vaadin.ui.Upload.UploadException; +import com.vaadin.ui.Html5File.ProxyReceiver; @SuppressWarnings("serial") @ClientWidget(VDragAndDropWrapper.class) public class DragAndDropWrapper extends CustomComponent implements DropTarget, - DragSource { + DragSource, ReceiverOwner { public class WrapperTransferable extends TransferableImpl { + /** + * @deprecated this class is made top level in recent version. Use + * com.vaadin.ui.Html5File instead + */ + @Deprecated + private class Html5File extends com.vaadin.ui.Html5File { + + Html5File(String name, long size, String mimeType) { + super(name, size, mimeType); + } + + } + private Html5File[] files; public WrapperTransferable(Component sourceComponent, @@ -45,13 +52,14 @@ public class DragAndDropWrapper extends CustomComponent implements DropTarget, if (fc != null) { files = new Html5File[fc]; for (int i = 0; i < fc; i++) { - Html5File file = new Html5File(); + Html5File file = new Html5File( + (String) rawVariables.get("fn" + i), // name + (Integer) rawVariables.get("fs" + i), // size + (String) rawVariables.get("ft" + i)); // mime String id = (String) rawVariables.get("fi" + i); - file.name = (String) rawVariables.get("fn" + i); - file.size = (Integer) rawVariables.get("fs" + i); - file.type = (String) rawVariables.get("ft" + i); files[i] = file; receivers.put(id, file); + requestRepaint(); // paint Receivers } } } @@ -96,50 +104,6 @@ public class DragAndDropWrapper extends CustomComponent implements DropTarget, return data; } - /** - * {@link DragAndDropWrapper} can receive also files from client - * computer if appropriate HTML 5 features are supported on client side. - * This class wraps information about dragged file on server side. - */ - public class Html5File implements Serializable { - - public String name; - private int size; - private Receiver receiver; - private String type; - - public String getFileName() { - return name; - } - - public int getFileSize() { - return size; - } - - public String getType() { - return type; - } - - /** - * Sets the {@link Receiver} that into which the file contents will - * be written. Usage of Reveiver is similar to {@link Upload} - * component. - * - *

- * Note! receiving file contents is experimental feature - * depending on HTML 5 API's. It is supported only by Firefox 3.6 at - * this time. - * - * @param receiver - * the callback that returns stream where the - * implementation writes the file contents as it arrives. - */ - public void setReceiver(Receiver receiver) { - this.receiver = receiver; - } - - } - } private Map receivers = new HashMap(); @@ -220,9 +184,23 @@ public class DragAndDropWrapper extends CustomComponent implements DropTarget, if (getDropHandler() != null) { getDropHandler().getAcceptCriterion().paint(target); } + if (receivers != null && receivers.size() > 0) { + for (String id : receivers.keySet()) { + Html5File html5File = receivers.get(id); + if (html5File.getReceiver() != null) { + target.addVariable(this, "rec-" + id, + html5File.getProxyReceiver()); + } else { + // instructs the client side not to send the file + target.addVariable(this, "rec-" + id, (String) null); + } + } + } } private DropHandler dropHandler; + private Html5File currentlyUploadedFile; + private boolean listenProgressOfUploadedFile; public DropHandler getDropHandler() { return dropHandler; @@ -251,38 +229,96 @@ public class DragAndDropWrapper extends CustomComponent implements DropTarget, return dragStartMode; } - /** - * This method should only be used by Vaadin terminal implementation. This - * is not end user api. - * - * TODO should fire progress events + end/succes events like upload. Not - * critical until we have a wider browser support for HTML5 File API - * - * @param upstream - * @param fileId - * @throws UploadException + /* + * Single controller is enough for atm as files are transferred in serial. + * If parallel transfer is needed, this logic needs to go to Html5File */ - public void receiveFile(UploadStream upstream, String fileId) - throws UploadException { - Html5File file = receivers.get(fileId); - if (file != null && file.receiver != null) { - OutputStream receiveUpload = file.receiver.receiveUpload( - file.getFileName(), "TODO"); - - InputStream stream = upstream.getStream(); - byte[] buf = new byte[AbstractApplicationServlet.MAX_BUFFER_SIZE]; - int bytesRead; - try { - while ((bytesRead = stream.read(buf)) != -1) { - receiveUpload.write(buf, 0, bytesRead); + private ReceivingController controller = new ReceivingController() { + /* + * With XHR2 file posts we can't provide as much information from the + * terminal as with multipart request. This helper class wraps the + * terminal event and provides the lacking information from the + * Html5File. + */ + class ReceivingEventWrapper implements ReceivingFailedEvent, + ReceivingEndedEvent, ReceivingStartedEvent, + ReceivingProgressedEvent { + private ReceivingEvent wrappedEvent; + + ReceivingEventWrapper(ReceivingEvent e) { + wrappedEvent = e; + } + + public String getMimeType() { + return currentlyUploadedFile.getType(); + } + + public String getFileName() { + return currentlyUploadedFile.getFileName(); + } + + public long getContentLength() { + return currentlyUploadedFile.getFileSize(); + } + + public Receiver getReceiver() { + return currentlyUploadedFile.getReceiver(); + } + + public Exception getException() { + if (wrappedEvent instanceof ReceivingFailedEvent) { + return ((ReceivingFailedEvent) wrappedEvent).getException(); } - receiveUpload.close(); - } catch (IOException e) { - throw new UploadException(e); + return null; + } + + public long getBytesReceived() { + return wrappedEvent.getBytesReceived(); + } + } + + public boolean listenProgress() { + return listenProgressOfUploadedFile; + } + + public void onProgress(ReceivingProgressedEvent event) { + currentlyUploadedFile.getUploadListener().onProgress( + new ReceivingEventWrapper(event)); + } + + public void uploadStarted(ReceivingStartedEvent event) { + currentlyUploadedFile = ((ProxyReceiver) event.getReceiver()) + .getFile(); + listenProgressOfUploadedFile = currentlyUploadedFile + .getUploadListener() != null; + if (listenProgressOfUploadedFile) { + currentlyUploadedFile.getUploadListener().uploadStarted( + new ReceivingEventWrapper(event)); + } + } + + public void uploadFinished(ReceivingEndedEvent event) { + if (listenProgressOfUploadedFile) { + currentlyUploadedFile.getUploadListener().uploadFinished( + new ReceivingEventWrapper(event)); } - // clean up the reference when file is downloaded - receivers.remove(fileId); } + public void uploadFailed(final ReceivingFailedEvent event) { + if (listenProgressOfUploadedFile) { + currentlyUploadedFile.getUploadListener().uploadFailed( + new ReceivingEventWrapper(event)); + } + } + + public boolean isInterrupted() { + return currentlyUploadedFile.isInterrupted(); + } + + }; + + public ReceivingController getReceivingController(Receiver receiver) { + return controller; } + } diff --git a/src/com/vaadin/ui/Html5File.java b/src/com/vaadin/ui/Html5File.java new file mode 100644 index 0000000000..988bf534b9 --- /dev/null +++ b/src/com/vaadin/ui/Html5File.java @@ -0,0 +1,137 @@ +package com.vaadin.ui; + +import java.io.OutputStream; +import java.io.Serializable; + +import com.vaadin.event.dd.DropHandler; +import com.vaadin.terminal.Receiver; +import com.vaadin.terminal.ReceiverOwner.ReceivingEndedEvent; +import com.vaadin.terminal.ReceiverOwner.ReceivingFailedEvent; +import com.vaadin.terminal.ReceiverOwner.ReceivingProgressedEvent; +import com.vaadin.terminal.ReceiverOwner.ReceivingStartedEvent; + +/** + * {@link DragAndDropWrapper} can receive also files from client computer if + * appropriate HTML 5 features are supported on client side. This class wraps + * information about dragged file on server side. + */ +public class Html5File implements Serializable { + + final class ProxyReceiver implements Receiver { + public OutputStream receiveUpload(String filename, String MIMEType) { + if (receiver == null) { + return null; + } + return receiver.receiveUpload(filename, MIMEType); + } + + Html5File getFile() { + return Html5File.this; + } + } + + private String name; + private long size; + private Receiver receiver; + private String type; + + Html5File(String name, long size, String mimeType) { + this.name = name; + this.size = size; + type = mimeType; + } + + /** + * The receiver that is registered to the terminal. Wraps the actual + * Receiver set later by Html5File user. + */ + private ProxyReceiver proxyReceiver = new ProxyReceiver(); + private boolean interrupted = false; + private Html5FileUploadListener listener;; + + public String getFileName() { + return name; + } + + public long getFileSize() { + return size; + } + + public String getType() { + return type; + } + + /** + * Sets the {@link Receiver} that into which the file contents will be + * written. Usage of Reveiver is similar to {@link Upload} component. + *

+ * If the {@link Receiver} is not set in the {@link DropHandler} the file + * contents will not be sent to server. + *

+ * Note! receiving file contents is experimental feature depending + * on HTML 5 API's. It is supported only by modern web brosers like Firefox + * 3.6 and above and recent webkit based browsers (Safari 5, Chrome 6) at + * this time. + * + * @param receiver + * the callback that returns stream where the implementation + * writes the file contents as it arrives. + */ + public void setReceiver(Receiver receiver) { + this.receiver = receiver; + } + + public Receiver getReceiver() { + return receiver; + } + + ProxyReceiver getProxyReceiver() { + return proxyReceiver; + } + + /** + * Gets the {@link Html5FileUploadListener} that is used to track the progress of + * streaming the file contents to given {@link Receiver}. + * + * @return + */ + public Html5FileUploadListener getUploadListener() { + return listener; + } + + /** + * Sets the {@link Html5FileUploadListener} that can be used to track the progress of + * streaming the file contents to given {@link Receiver}. + * + * @param listener + * @see #setReceiver(Receiver) + */ + public void setUploadListener(Html5FileUploadListener listener) { + this.listener = listener; + } + + public boolean isInterrupted() { + return interrupted; + } + + /** + * Interrupts uploading this file. + * + * @param interrupted + */ + public void setInterrupted(boolean interrupted) { + this.interrupted = interrupted; + } + + public interface Html5FileUploadListener { + + void onProgress(ReceivingProgressedEvent event); + + void uploadStarted(ReceivingStartedEvent event); + + void uploadFinished(ReceivingEndedEvent event); + + void uploadFailed(ReceivingFailedEvent event); + } + +} \ No newline at end of file diff --git a/src/com/vaadin/ui/Upload.java b/src/com/vaadin/ui/Upload.java index b281d90b09..1acc91031e 100644 --- a/src/com/vaadin/ui/Upload.java +++ b/src/com/vaadin/ui/Upload.java @@ -4,20 +4,18 @@ package com.vaadin.ui; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.io.Serializable; import java.lang.reflect.Method; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.Map; -import com.vaadin.Application; import com.vaadin.terminal.PaintException; import com.vaadin.terminal.PaintTarget; -import com.vaadin.terminal.UploadStream; +import com.vaadin.terminal.ReceiverOwner; import com.vaadin.terminal.gwt.client.ui.VUpload; +import com.vaadin.terminal.gwt.server.NoInputStreamException; +import com.vaadin.terminal.gwt.server.NoOutputStreamException; import com.vaadin.ui.ClientWidget.LoadStyle; /** @@ -62,12 +60,8 @@ import com.vaadin.ui.ClientWidget.LoadStyle; */ @SuppressWarnings("serial") @ClientWidget(value = VUpload.class, loadStyle = LoadStyle.LAZY) -public class Upload extends AbstractComponent implements Component.Focusable { - - /** - * Upload buffer size. - */ - private static final int BUFFER_SIZE = 64 * 1024; // 64k +public class Upload extends AbstractComponent implements Component.Focusable, + ReceiverOwner { /** * Should the field be focused on next repaint? @@ -130,105 +124,6 @@ public class Upload extends AbstractComponent implements Component.Focusable { receiver = uploadReceiver; } - /** - * This method is called by terminal when upload is received. - * - * Note, this method is called outside synchronized (Application) block, so - * overriding this may be dangerous. - * - * @param upload - */ - public void receiveUpload(UploadStream upload) throws UploadException { - if (receiver == null) { - throw new IllegalStateException( - "Receiver not set for the Upload component"); - } - - if (!isUploading) { - throw new IllegalStateException("uploading not started"); - } - - // Gets file properties - final String filename = upload.getContentName(); - final String type = upload.getContentType(); - - final Application application = getApplication(); - - synchronized (application) { - fireStarted(filename, type); - } - - // Gets the output target stream - final OutputStream out = receiver.receiveUpload(filename, type); - if (out == null) { - synchronized (application) { - fireNoOutputStream(filename, type, 0); - endUpload(); - } - return; - } - - final InputStream in = upload.getStream(); - - if (null == in) { - // No file, for instance non-existent filename in html upload - synchronized (application) { - fireNoInputStream(filename, type, 0); - endUpload(); - } - return; - } - - final byte buffer[] = new byte[BUFFER_SIZE]; - int bytesRead = 0; - totalBytes = 0; - try { - while ((bytesRead = in.read(buffer)) > 0) { - out.write(buffer, 0, bytesRead); - totalBytes += bytesRead; - if (contentLength > 0 - && (progressListeners != null || progressListener != null)) { - // update progress if listener set and contentLength - // received - synchronized (application) { - fireUpdateProgress(totalBytes, contentLength); - } - } - if (interrupted) { - throw new UploadInterruptedException(); - } - } - - // upload successful - out.close(); - synchronized (application) { - fireUploadSuccess(filename, type, totalBytes); - endUpload(); - requestRepaint(); - } - - } catch (final Exception e) { - synchronized (application) { - if (e instanceof UploadInterruptedException) { - // Download interrupted - try { - // still try to close output stream - out.close(); - } catch (IOException e1) { - // NOP - } - } - fireUploadInterrupted(filename, type, totalBytes, e); - endUpload(); - interrupted = false; - if (!(e instanceof UploadInterruptedException)) { - // throw exception for terminal to be handled - throw new UploadException(e); - } - } - } - } - /** * Invoked when the value of a variable has changed. * @@ -278,6 +173,9 @@ public class Upload extends AbstractComponent implements Component.Focusable { target.addAttribute("nextid", nextid); + // Post file to this receiver + target.addVariable(this, "action", receiver); + } /** @@ -288,20 +186,11 @@ public class Upload extends AbstractComponent implements Component.Focusable { * @version * @VERSION@ * @since 3.0 + * @deprecated use {@link com.vaadin.terminal.Receiver} instead. A "copy" + * here is kept for backwards compatibility. */ - public interface Receiver extends Serializable { - - /** - * Invoked when a new upload arrives. - * - * @param filename - * the desired filename of the upload, usually as specified - * by the client. - * @param MIMEType - * the MIME type of the uploaded file. - * @return Stream to which the uploaded file should be written. - */ - public OutputStream receiveUpload(String filename, String MIMEType); + @Deprecated + public interface Receiver extends com.vaadin.terminal.Receiver { } /* Upload events */ @@ -332,19 +221,6 @@ public class Upload extends AbstractComponent implements Component.Focusable { } } - private class UploadInterruptedException extends Exception { - public UploadInterruptedException() { - super("Upload interrupted by other thread"); - } - - } - - public static class UploadException extends Exception { - public UploadException(Exception e) { - super("Upload failed", e); - } - } - /** * Upload.Received event is sent when the upload receives a file, regardless * of whether the reception was successful or failed. If you wish to @@ -356,7 +232,7 @@ public class Upload extends AbstractComponent implements Component.Focusable { * @VERSION@ * @since 3.0 */ - public class FinishedEvent extends Component.Event { + public static class FinishedEvent extends Component.Event { /** * Length of the received file. @@ -439,7 +315,7 @@ public class Upload extends AbstractComponent implements Component.Focusable { * @VERSION@ * @since 3.0 */ - public class FailedEvent extends FinishedEvent { + public static class FailedEvent extends FinishedEvent { private Exception reason = null; @@ -484,7 +360,7 @@ public class Upload extends AbstractComponent implements Component.Focusable { /** * FailedEvent that indicates that an output stream could not be obtained. */ - public class NoOutputStreamEvent extends FailedEvent { + public static class NoOutputStreamEvent extends FailedEvent { /** * @@ -502,7 +378,7 @@ public class Upload extends AbstractComponent implements Component.Focusable { /** * FailedEvent that indicates that an input stream could not be obtained. */ - public class NoInputStreamEvent extends FailedEvent { + public static class NoInputStreamEvent extends FailedEvent { /** * @@ -526,7 +402,7 @@ public class Upload extends AbstractComponent implements Component.Focusable { * @VERSION@ * @since 3.0 */ - public class SucceededEvent extends FinishedEvent { + public static class SucceededEvent extends FinishedEvent { /** * @@ -550,10 +426,14 @@ public class Upload extends AbstractComponent implements Component.Focusable { * @VERSION@ * @since 5.0 */ - public class StartedEvent extends Component.Event { + public static class StartedEvent extends Component.Event { private final String filename; private final String type; + /** + * Length of the received file. + */ + private final long length; /** * @@ -562,10 +442,12 @@ public class Upload extends AbstractComponent implements Component.Focusable { * @param MIMEType * @param length */ - public StartedEvent(Upload source, String filename, String MIMEType) { + public StartedEvent(Upload source, String filename, String MIMEType, + long contentLength) { super(source); this.filename = filename; type = MIMEType; + length = contentLength; } /** @@ -595,6 +477,13 @@ public class Upload extends AbstractComponent implements Component.Focusable { return type; } + /** + * @return the length of the file that is being uploaded + */ + public long getContentLength() { + return length; + } + } /** @@ -786,19 +675,8 @@ public class Upload extends AbstractComponent implements Component.Focusable { * @param length */ protected void fireStarted(String filename, String MIMEType) { - fireEvent(new Upload.StartedEvent(this, filename, MIMEType)); - } - - /** - * Emit upload finished event. - * - * @param filename - * @param MIMEType - * @param length - */ - protected void fireUploadReceived(String filename, String MIMEType, - long length) { - fireEvent(new Upload.FinishedEvent(this, filename, MIMEType, length)); + fireEvent(new Upload.StartedEvent(this, filename, MIMEType, + contentLength)); } /** @@ -913,15 +791,6 @@ public class Upload extends AbstractComponent implements Component.Focusable { this.tabIndex = tabIndex; } - /** - * Sets the size of the file currently being uploaded. - * - * @param contentLength - */ - public void setUploadSize(long contentLength) { - this.contentLength = contentLength; - } - /** * Go into upload state. This is to prevent double uploading on same * component. @@ -959,6 +828,8 @@ public class Upload extends AbstractComponent implements Component.Focusable { private void endUpload() { isUploading = false; contentLength = -1; + interrupted = false; + requestRepaint(); } public boolean isUploading() { @@ -1046,4 +917,54 @@ public class Upload extends AbstractComponent implements Component.Focusable { this.buttonCaption = buttonCaption; } + /* + * Handle to terminal via Upload monitors and controls the upload during it + * is being streamed. + */ + private final ReceivingController controller = new ReceivingController() { + public boolean listenProgress() { + return (progressListeners != null || progressListener != null); + } + + public void onProgress(ReceivingProgressedEvent event) { + fireUpdateProgress(event.getBytesReceived(), + event.getContentLength()); + } + + public void uploadStarted(ReceivingStartedEvent event) { + startUpload(); + contentLength = event.getContentLength(); + fireStarted(event.getFileName(), event.getMimeType()); + } + + public void uploadFinished(ReceivingEndedEvent event) { + fireUploadSuccess(event.getFileName(), event.getMimeType(), + event.getContentLength()); + endUpload(); + requestRepaint(); + } + + public void uploadFailed(ReceivingFailedEvent event) { + Exception exception = event.getException(); + if (exception instanceof NoInputStreamException) { + fireNoInputStream(event.getFileName(), event.getMimeType(), 0); + } else if (exception instanceof NoOutputStreamException) { + fireNoOutputStream(event.getFileName(), event.getMimeType(), 0); + } else { + fireUploadInterrupted(event.getFileName(), event.getMimeType(), + 0, exception); + } + endUpload(); + } + + public boolean isInterrupted() { + return interrupted; + } + }; + + public final ReceivingController getReceivingController( + com.vaadin.terminal.Receiver receiver) { + return controller; + } + } diff --git a/tests/src/com/vaadin/tests/dd/DDTest6.java b/tests/src/com/vaadin/tests/dd/DDTest6.java index 8c86b93513..38c48ae44c 100644 --- a/tests/src/com/vaadin/tests/dd/DDTest6.java +++ b/tests/src/com/vaadin/tests/dd/DDTest6.java @@ -38,8 +38,8 @@ import com.vaadin.ui.AbsoluteLayout.ComponentPosition; import com.vaadin.ui.Component; import com.vaadin.ui.CssLayout; import com.vaadin.ui.DragAndDropWrapper; -import com.vaadin.ui.DragAndDropWrapper.WrapperTransferable.Html5File; import com.vaadin.ui.Embedded; +import com.vaadin.ui.Html5File; import com.vaadin.ui.Label; import com.vaadin.ui.SplitPanel; import com.vaadin.ui.Table; diff --git a/tests/src/com/vaadin/tests/dd/DragAndDropFiles.java b/tests/src/com/vaadin/tests/dd/DragAndDropFiles.java new file mode 100644 index 0000000000..a22be5c440 --- /dev/null +++ b/tests/src/com/vaadin/tests/dd/DragAndDropFiles.java @@ -0,0 +1,129 @@ +package com.vaadin.tests.dd; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; + +import org.apache.commons.io.output.NullOutputStream; + +import com.vaadin.event.dd.DragAndDropEvent; +import com.vaadin.event.dd.DropHandler; +import com.vaadin.event.dd.acceptcriteria.AcceptAll; +import com.vaadin.event.dd.acceptcriteria.AcceptCriterion; +import com.vaadin.terminal.Receiver; +import com.vaadin.terminal.ReceiverOwner.ReceivingEndedEvent; +import com.vaadin.terminal.ReceiverOwner.ReceivingFailedEvent; +import com.vaadin.terminal.ReceiverOwner.ReceivingProgressedEvent; +import com.vaadin.terminal.ReceiverOwner.ReceivingStartedEvent; +import com.vaadin.tests.components.TestBase; +import com.vaadin.ui.Component; +import com.vaadin.ui.CssLayout; +import com.vaadin.ui.DragAndDropWrapper; +import com.vaadin.ui.DragAndDropWrapper.WrapperTransferable; +import com.vaadin.ui.Html5File; +import com.vaadin.ui.Html5File.Html5FileUploadListener; +import com.vaadin.ui.Label; + +public class DragAndDropFiles extends TestBase { + + @Override + protected void setup() { + CssLayout cssLayout = new CssLayout() { + @Override + protected String getCss(Component c) { + return "display: block; padding:20px; border: 2px dotted black; background: #aaa;"; + } + }; + Component l = new Label("Drag file on me"); + l.setSizeUndefined(); + cssLayout.addComponent(l); + DragAndDropWrapper dragAndDropWrapper = new DragAndDropWrapper( + cssLayout); + dragAndDropWrapper.setSizeUndefined(); + dragAndDropWrapper.setDropHandler(new DropHandler() { + + public AcceptCriterion getAcceptCriterion() { + return AcceptAll.get(); + } + + public void drop(DragAndDropEvent event) { + WrapperTransferable transferable = (WrapperTransferable) event + .getTransferable(); + Html5File[] files = transferable.getFiles(); + if (files != null) { + + for (int i = 0; i < files.length; i++) { + Html5File file = files[i]; + // Max 1 MB files are uploaded + if (file.getFileSize() > 1024 * 1024) { + getMainWindow() + .showNotification( + "File " + + file.getFileName() + + " was too large, not transferred to the server side."); + continue; + } + + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + + Receiver receiver = new Receiver() { + public OutputStream receiveUpload(String filename, + String MIMEType) { + System.err.println("receiveUpload " + filename); + + return new NullOutputStream(); + } + }; + file.setReceiver(receiver); + Html5FileUploadListener listener = new Html5FileUploadListener() { + + public void uploadStarted( + ReceivingStartedEvent event) { + getMainWindow().showNotification( + "Started uploading " + + event.getFileName()); + + } + + public void uploadFinished(ReceivingEndedEvent event) { + getMainWindow().showNotification( + "Finished uploading " + + event.getFileName()); + } + + public void uploadFailed(ReceivingFailedEvent event) { + getMainWindow().showNotification( + "Failed uploading " + + event.getFileName()); + + } + + public void onProgress( + ReceivingProgressedEvent event) { + System.err.println("Progress" + + event.getBytesReceived()); + } + }; + file.setUploadListener(listener); + } + } + + } + }); + + addComponent(dragAndDropWrapper); + } + + /* + * TODO implement 'handbrake' for testing, progresss listener, interrupting. + */ + @Override + protected String getDescription() { + return "Should work. Over 1 MB files will not be posted. TODO implement 'handbrake' for testing, progresss listener, interrupting."; + } + + @Override + protected Integer getTicketNumber() { + return null; + } + +} diff --git a/tests/src/com/vaadin/tests/dd/DragDropPane.java b/tests/src/com/vaadin/tests/dd/DragDropPane.java index a3552b05b5..1bfc66aa7e 100644 --- a/tests/src/com/vaadin/tests/dd/DragDropPane.java +++ b/tests/src/com/vaadin/tests/dd/DragDropPane.java @@ -15,7 +15,7 @@ import com.vaadin.ui.AbsoluteLayout; import com.vaadin.ui.AbsoluteLayout.ComponentPosition; import com.vaadin.ui.Component; import com.vaadin.ui.DragAndDropWrapper; -import com.vaadin.ui.DragAndDropWrapper.WrapperTransferable.Html5File; +import com.vaadin.ui.Html5File; import com.vaadin.ui.Label; import com.vaadin.ui.Upload.Receiver; -- 2.39.5