From f1d07c89844bbd3d46281eb95166f609bd4191e8 Mon Sep 17 00:00:00 2001 From: Matti Tahvonen Date: Fri, 12 Mar 2010 14:57:49 +0000 Subject: [PATCH] A proper (FF36 only) file drag support, enhanced test case. Should be forward compatible. svn changeset:11835/svn branch:6.3 --- .../gwt/client/ui/VDragAndDropWrapper.java | 132 +++++++++++++----- .../gwt/client/ui/dd/VHtml5DragEvent.java | 2 +- .../terminal/gwt/client/ui/dd/VHtml5File.java | 10 +- .../server/AbstractApplicationServlet.java | 2 + .../server/AbstractCommunicationManager.java | 75 ++++++---- src/com/vaadin/ui/DragAndDropWrapper.java | 84 ++++++++--- tests/src/com/vaadin/tests/dd/DDTest6.java | 94 ++++++++++++- 7 files changed, 309 insertions(+), 90 deletions(-) diff --git a/src/com/vaadin/terminal/gwt/client/ui/VDragAndDropWrapper.java b/src/com/vaadin/terminal/gwt/client/ui/VDragAndDropWrapper.java index 2beef216f2..606b382c26 100644 --- a/src/com/vaadin/terminal/gwt/client/ui/VDragAndDropWrapper.java +++ b/src/com/vaadin/terminal/gwt/client/ui/VDragAndDropWrapper.java @@ -13,6 +13,7 @@ import com.google.gwt.user.client.Command; import com.google.gwt.user.client.DeferredCommand; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.ui.Widget; +import com.google.gwt.xhr.client.XMLHttpRequest; import com.vaadin.terminal.gwt.client.ApplicationConnection; import com.vaadin.terminal.gwt.client.MouseEventDetails; import com.vaadin.terminal.gwt.client.Paintable; @@ -193,44 +194,16 @@ public class VDragAndDropWrapper extends VCustomComponent implements transferable.setData("filecount", fileCount); for (int i = 0; i < fileCount; i++) { final int fileId = filecounter++; - final VHtml5File file = event.getFile(fileCount); - transferable.setData("fn" + fileId, file.getName()); - transferable.setData("ft" + fileId, file.getType()); - transferable.setData("fs" + fileId, file.getSize()); - DeferredCommand.addCommand(new Command() { - public void execute() { - /* - * File contents is sent deferred to allow quick - * reaction on GUI although file upload may last long. - * TODO make this use apache file upload instead of our - * variable post like in upload. Currently stalls the - * GUI during upload. Also need to use dataurl to - * support all possible bytes in file content - */ - file.readAsDataUrl(new Callback() { - public void handleFile(JavaScriptObject object) { - client.updateVariable(client - .getPid(VDragAndDropWrapper.this), - "file" + fileId, object.toString(), - true); - - } - }); - - } - }); + 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); } } - // TODO remove this when above cleaner and more standard compliance - // system works - String fileAsString = event.getFileAsString(0); - if (fileAsString != null) { - ApplicationConnection.getConsole().log(fileAsString); - transferable.setData("fileContents", fileAsString); - } - VDragAndDropManager.get().endDrag(); vaadinDragEvent = null; event.preventDefault(); @@ -239,6 +212,97 @@ public class VDragAndDropWrapper extends VCustomComponent implements return false; } + static class ExtendedXHR extends XMLHttpRequest { + + protected ExtendedXHR() { + } + + public final native void sendBinary(JavaScriptObject data) + /*-{ + //this.overrideMimeType('text/plain; charset=x-user-defined-binary'); + this.sendAsBinary(data); + }-*/; + + } + + /** + * + * Currently supports only FF36 as no other browser supprots natively File + * api. + * + * @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. TODO make this use apache + * file upload instead of our variable post like in upload. + * Currently stalls the GUI during upload. Also need to use + * dataurl to support all possible bytes in file content + */ + file.readAsBinary(new Callback() { + public void handleFile(final JavaScriptObject object) { + + DeferredCommand.addCommand(new Command() { + + public void execute() { + + ExtendedXHR extendedXHR = (ExtendedXHR) ExtendedXHR + .create(); + extendedXHR.open("POST", client.getAppUri()); + extendedXHR + .setRequestHeader( + "PaintableId", + client + .getPid(VDragAndDropWrapper.this)); + extendedXHR.setRequestHeader("FileId", "" + + fileId); + + // extendedXHR.setRequestHeader("Connection", + // "close"); + + multipartSend( + extendedXHR, + object, + "XHRFILE" + + client + .getPid(VDragAndDropWrapper.this) + + "." + fileId); + + } + }); + } + }); + + } + }); + + } + + private native void multipartSend(JavaScriptObject xhr, + JavaScriptObject data, String name) + /*-{ + + var boundaryString = "------------------------------------------VAADINXHRFILEUPLOAD"; + var boundary = "--" + boundaryString; + var CRLF = "\r\n"; + xhr.setRequestHeader("Content-type", "multipart/form-data; boundary=\"" + boundaryString + "\""); + var requestBody = boundary + + CRLF + + "Content-Disposition: form-data; name=\""+name+"\"; filename=\"file\"" + + CRLF + + "Content-Type: application/octet-stream" // hard coded, type sent separately + + CRLF + CRLF + data.target.result + CRLF + boundary + "--" + CRLF; + xhr.setRequestHeader("Content-Length", requestBody.length); + + + xhr.sendAsBinary(requestBody); + + }-*/; + public VDropHandler getDropHandler() { return dropHandler; } diff --git a/src/com/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent.java b/src/com/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent.java index 9d5bafc2b7..47b1ba81ed 100644 --- a/src/com/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent.java +++ b/src/com/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent.java @@ -59,7 +59,7 @@ public class VHtml5DragEvent extends NativeEvent { public final native VHtml5File getFile(int fileIndex) /*-{ - return this.dataTransfer.files[i]; + return this.dataTransfer.files[fileIndex]; }-*/; } diff --git a/src/com/vaadin/terminal/gwt/client/ui/dd/VHtml5File.java b/src/com/vaadin/terminal/gwt/client/ui/dd/VHtml5File.java index ead0ee6ceb..1a1db2910d 100644 --- a/src/com/vaadin/terminal/gwt/client/ui/dd/VHtml5File.java +++ b/src/com/vaadin/terminal/gwt/client/ui/dd/VHtml5File.java @@ -14,17 +14,17 @@ public class VHtml5File extends JavaScriptObject { public native final String getName() /*-{ - return name; + return this.name; }-*/; public native final String getType() /*-{ - return type; + return this.type; }-*/; public native final int getSize() /*-{ - return size; + return this.size; }-*/; public native final void readAsBinary(final Callback callback) @@ -33,7 +33,9 @@ public class VHtml5File extends JavaScriptObject { r.onloadend = function(content) { callback.@com.vaadin.terminal.gwt.client.ui.dd.VHtml5File.Callback::handleFile(Lcom/google/gwt/core/client/JavaScriptObject;)(content); }; - r.readAsBinary(this); + r.readAsBinaryString(this); + var j = 0; + }-*/; public native final void readAsDataUrl(final Callback callback) diff --git a/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java b/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java index d1b8ec7671..d3243fab73 100644 --- a/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java +++ b/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java @@ -1252,6 +1252,8 @@ public abstract class AbstractApplicationServlet extends HttpServlet implements return RequestType.STATIC_FILE; } else if (isApplicationRequest(request)) { return RequestType.APPLICATION_RESOURCE; + } else if (request.getHeader("FileId") != null) { + return RequestType.FILE_UPLOAD; } return RequestType.OTHER; diff --git a/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java b/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java index 481e42c0e9..ada3869269 100644 --- a/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java +++ b/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java @@ -62,6 +62,7 @@ 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.Window; import com.vaadin.ui.Upload.UploadException; @@ -359,6 +360,7 @@ public abstract class AbstractCommunicationManager implements */ protected void doHandleFileUpload(Request request, Response response) throws IOException, FileUploadException { + // Create a new file upload handler final FileUpload upload = createFileUpload(); @@ -386,22 +388,6 @@ public abstract class AbstractCommunicationManager implements if (item.isFormField()) { // ignored, upload requests contains only files } else { - 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(); - } final UploadStream upstream = new UploadStream() { public String getContentName() { @@ -422,22 +408,55 @@ public abstract class AbstractCommunicationManager implements }; - // tell UploadProgressListener which component is receiving - // file - pl.setUpload(uploadComponent); + if (name.startsWith("XHRFILE")) { + String[] split = item.getFieldName().substring(7) + .split("\\."); + DragAndDropWrapper ddw = (DragAndDropWrapper) idPaintableMap + .get(split[0]); + + ddw.receiveFile(upstream, split[1]); - 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. + String debugId = ddw.getDebugId(); + + } else { + + 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) { - handleChangeVariablesError(application, - uploadComponent, e, - new HashMap()); + // put upload component into receiving state + uploadComponent.startUpload(); + } + + // tell UploadProgressListener which component is + // receiving + // file + pl.setUpload(uploadComponent); + + 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()); + } } } + } } } catch (final FileUploadException e) { diff --git a/src/com/vaadin/ui/DragAndDropWrapper.java b/src/com/vaadin/ui/DragAndDropWrapper.java index 3274b60b47..a848d7af40 100644 --- a/src/com/vaadin/ui/DragAndDropWrapper.java +++ b/src/com/vaadin/ui/DragAndDropWrapper.java @@ -3,8 +3,11 @@ */ package com.vaadin.ui; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; import java.util.Map; -import java.util.Set; import com.vaadin.event.Transferable; import com.vaadin.event.TransferableImpl; @@ -15,10 +18,13 @@ import com.vaadin.event.dd.DropTargetDetails; import com.vaadin.event.dd.DropTargetDetailsImpl; import com.vaadin.terminal.PaintException; import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.UploadStream; 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; @ClientWidget(VDragAndDropWrapper.class) @@ -27,9 +33,24 @@ public class DragAndDropWrapper extends CustomComponent implements DropTarget, public class WrapperTransferable extends TransferableImpl { + private Html5File[] files; + public WrapperTransferable(Component sourceComponent, Map rawVariables) { super(sourceComponent, rawVariables); + Integer fc = (Integer) rawVariables.get("filecount"); + if (fc != null) { + files = new Html5File[fc]; + for (int i = 0; i < fc; i++) { + Html5File file = new Html5File(); + 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); + } + } } /** @@ -51,21 +72,28 @@ public class DragAndDropWrapper extends CustomComponent implements DropTarget, } public Html5File[] getFiles() { - // TODO Auto-generated method stub - return null; + return files; } public class Html5File { + public String name; + private String id; + private int size; + private Receiver receiver; + private String type; + public String getFileName() { - // TODO Auto-generated method stub - return null; + return name; + } + + public int getFileSize() { + return size; } - // public int getFileSize() { - // // TODO Auto-generated method stub - // return 0; - // } + public String getType() { + return type; + } /** * HTML5 drags are read from client disk with a callback. This and @@ -77,14 +105,15 @@ public class DragAndDropWrapper extends CustomComponent implements DropTarget, * implementation writes the file contents as it arrives. */ public void receive(Receiver receiver) { - // TODO Auto-generated method stub - + this.receiver = receiver; } } } + private Map receivers = new HashMap(); + public class WrapperDropDetails extends DropTargetDetailsImpl { /** @@ -195,13 +224,34 @@ public class DragAndDropWrapper extends CustomComponent implements DropTarget, return dragStartMode; } - @Override - public void changeVariables(Object source, Map variables) { - super.changeVariables(source, variables); + /** + * 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 + * + * @param upstream + * @param fileId + */ + public void receiveFile(UploadStream upstream, String fileId) { + 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); + } + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } - Set keySet = variables.keySet(); - for (String string : keySet) { - // TODO get files } + } } diff --git a/tests/src/com/vaadin/tests/dd/DDTest6.java b/tests/src/com/vaadin/tests/dd/DDTest6.java index 6df55917fa..771a3b536f 100644 --- a/tests/src/com/vaadin/tests/dd/DDTest6.java +++ b/tests/src/com/vaadin/tests/dd/DDTest6.java @@ -1,5 +1,9 @@ package com.vaadin.tests.dd; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; @@ -22,7 +26,9 @@ import com.vaadin.event.dd.acceptCriteria.AcceptCriterion; import com.vaadin.event.dd.acceptCriteria.IsSameSourceAndTarget; import com.vaadin.event.dd.acceptCriteria.Not; import com.vaadin.terminal.Resource; +import com.vaadin.terminal.StreamResource; import com.vaadin.terminal.ThemeResource; +import com.vaadin.terminal.StreamResource.StreamSource; import com.vaadin.terminal.gwt.client.MouseEventDetails; import com.vaadin.tests.components.TestBase; import com.vaadin.tests.util.TestUtils; @@ -34,9 +40,12 @@ import com.vaadin.ui.Embedded; import com.vaadin.ui.Label; import com.vaadin.ui.SplitPanel; import com.vaadin.ui.Tree; +import com.vaadin.ui.Window; import com.vaadin.ui.AbsoluteLayout.ComponentPosition; +import com.vaadin.ui.DragAndDropWrapper.WrapperTransferable.Html5File; import com.vaadin.ui.Tree.TreeDragMode; import com.vaadin.ui.Tree.TreeDropTargetDetails; +import com.vaadin.ui.Upload.Receiver; public class DDTest6 extends TestBase { @@ -52,6 +61,8 @@ public class DDTest6 extends TestBase { private SplitPanel sp; + private BeanItemContainer fs1; + private static int count; private static DDTest6 instance; @@ -70,7 +81,7 @@ public class DDTest6 extends TestBase { tree1 = new Tree("Volume 1"); tree1.setImmediate(true); - BeanItemContainer fs1 = new BeanItemContainer(File.class); + fs1 = new BeanItemContainer(File.class); tree1.setContainerDataSource(fs1); for (int i = 0; i < files.length; i++) { fs1.addBean(files[i]); @@ -169,11 +180,18 @@ public class DDTest6 extends TestBase { public static class File { private Resource icon = DOC; private String name; + private ByteArrayOutputStream bas; + private String type; public File(String fileName) { name = fileName; } + public File(String fileName, ByteArrayOutputStream bas) { + this(fileName); + this.bas = bas; + } + public void setIcon(Resource icon) { this.icon = icon; } @@ -189,6 +207,28 @@ public class DDTest6 extends TestBase { public String getName() { return name; } + + public void setType(String type) { + this.type = type; + } + + public String getType() { + return type; + } + + public Resource getResource() { + StreamSource streamSource = new StreamSource() { + public InputStream getStream() { + if (bas != null) { + byte[] byteArray = bas.toByteArray(); + return new ByteArrayInputStream(byteArray); + } + // TODO Auto-generated method stub + return null; + } + }; + return new StreamResource(streamSource, getName(), DDTest6.get()); + } } public static class Folder extends File { @@ -202,7 +242,7 @@ public class DDTest6 extends TestBase { @Override protected String getDescription() { - return "dd: tree and web desktop tests. TODO add traditional icon area on right side with DragAndDropWrapper and absolutelayouts + more files, auto-opening folders"; + return "dd: tree and web desktop tests. FF36 supports draggin files from client side. (try dragging png image + double click) TODO more files, auto-opening folders"; } @Override @@ -210,6 +250,16 @@ public class DDTest6 extends TestBase { return 119; } + private void openFile(File file) { + // ATM supports only images. + + Embedded embedded = new Embedded(file.getName(), file.getResource()); + Window w = new Window(file.getName()); + w.addComponent(embedded); + getMainWindow().addWindow(w); + + } + static class FolderView extends DragAndDropWrapper implements DropHandler { static final HashMap views = new HashMap(); @@ -313,6 +363,35 @@ public class DDTest6 extends TestBase { File draggedFile = (File) ((DataBoundTransferable) dropEvent .getTransferable()).getItemId(); DDTest6.get().setParent(draggedFile, folder); + } else { + // expecting this to be an html5 drag + WrapperTransferable tr = (WrapperTransferable) dropEvent + .getTransferable(); + Html5File[] files2 = tr.getFiles(); + if (files2 != null) { + for (Html5File html5File : files2) { + String fileName = html5File.getFileName(); + // int bytes = html5File.getFileSize(); + final ByteArrayOutputStream bas = new ByteArrayOutputStream(); + + Receiver receiver = new Receiver() { + public OutputStream receiveUpload(String filename, + String MIMEType) { + return bas; + } + }; + + html5File.receive(receiver); + + File file = new File(fileName, bas); + file.setType(html5File.getType()); + DDTest6.get().fs1.addBean(file); + DDTest6.get().tree1.setChildrenAllowed(file, false); + DDTest6.get().setParent(file, folder); + } + + } + } } @@ -342,11 +421,15 @@ public class DDTest6 extends TestBase { l.addListener(new LayoutClickListener() { public void layoutClick(LayoutClickEvent event) { - if (file instanceof Folder) { - if (event.isDoubleClick()) { + if (event.isDoubleClick()) { + if (file instanceof Folder) { get().tree1.setValue(file); + } else { + String type = file.getType(); + if (type != null && type.equals("image/png")) { + DDTest6.get().openFile(file); + } } - } } @@ -372,7 +455,6 @@ public class DDTest6 extends TestBase { f = (File) ((DataBoundTransferable) dropEvent .getTransferable()).getItemId(); } - // TODO accept drags from Tree too if (f != null) { get().setParent(f, (Folder) FileIcon.this.file); -- 2.39.5