View Javadoc
1   /*
2    * Copyright 2000-2014 Vaadin Ltd.
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5    * use this file except in compliance with the License. You may obtain a copy of
6    * the License at
7    * 
8    * http://www.apache.org/licenses/LICENSE-2.0
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations under
14   * the License.
15   */
16  package com.vaadin.ui;
17  
18  import java.io.OutputStream;
19  import java.util.HashMap;
20  import java.util.HashSet;
21  import java.util.Iterator;
22  import java.util.LinkedHashMap;
23  import java.util.Map;
24  import java.util.Map.Entry;
25  import java.util.Set;
26  
27  import org.jsoup.nodes.Element;
28  
29  import com.vaadin.event.Transferable;
30  import com.vaadin.event.TransferableImpl;
31  import com.vaadin.event.dd.DragSource;
32  import com.vaadin.event.dd.DropHandler;
33  import com.vaadin.event.dd.DropTarget;
34  import com.vaadin.event.dd.TargetDetails;
35  import com.vaadin.event.dd.TargetDetailsImpl;
36  import com.vaadin.server.PaintException;
37  import com.vaadin.server.PaintTarget;
38  import com.vaadin.server.StreamVariable;
39  import com.vaadin.shared.MouseEventDetails;
40  import com.vaadin.shared.ui.dd.HorizontalDropLocation;
41  import com.vaadin.shared.ui.dd.VerticalDropLocation;
42  import com.vaadin.shared.ui.draganddropwrapper.DragAndDropWrapperConstants;
43  import com.vaadin.shared.ui.draganddropwrapper.DragAndDropWrapperServerRpc;
44  import com.vaadin.ui.declarative.DesignContext;
45  
46  @SuppressWarnings("serial")
47  public class DragAndDropWrapper extends CustomComponent implements DropTarget,
48          DragSource, LegacyComponent {
49  
50      public class WrapperTransferable extends TransferableImpl {
51  
52          private Html5File[] files;
53  
54          public WrapperTransferable(Component sourceComponent,
55                  Map<String, Object> rawVariables) {
56              super(sourceComponent, rawVariables);
57              Integer fc = (Integer) rawVariables.get("filecount");
58              if (fc != null) {
59                  files = new Html5File[fc];
60                  for (int i = 0; i < fc; i++) {
61                      Html5File file = new Html5File(
62                              (String) rawVariables.get("fn" + i), // name
63                              ((Double) rawVariables.get("fs" + i)).longValue(), // size
64                              (String) rawVariables.get("ft" + i)); // mime
65                      String id = (String) rawVariables.get("fi" + i);
66                      files[i] = file;
67                      receivers.put(id, new ProxyReceiver(id, file));
68                      markAsDirty(); // paint Receivers
69                  }
70              }
71          }
72  
73          /**
74           * The component in wrapper that is being dragged or null if the
75           * transferable is not a component (most likely an html5 drag).
76           * 
77           * @return
78           */
79          public Component getDraggedComponent() {
80              Component object = (Component) getData("component");
81              return object;
82          }
83  
84          /**
85           * @return the mouse down event that started the drag and drop operation
86           */
87          public MouseEventDetails getMouseDownEvent() {
88              return MouseEventDetails.deSerialize((String) getData("mouseDown"));
89          }
90  
91          public Html5File[] getFiles() {
92              return files;
93          }
94  
95          public String getText() {
96              String data = (String) getData("Text"); // IE, html5
97              if (data == null) {
98                  // check for "text/plain" (webkit)
99                  data = (String) getData("text/plain");
100             }
101             return data;
102         }
103 
104         public String getHtml() {
105             String data = (String) getData("Html"); // IE, html5
106             if (data == null) {
107                 // check for "text/plain" (webkit)
108                 data = (String) getData("text/html");
109             }
110             return data;
111         }
112 
113     }
114 
115     private final DragAndDropWrapperServerRpc rpc = new DragAndDropWrapperServerRpc() {
116 
117         @Override
118         public void poll() {
119             // #19616 RPC to poll the server for changes
120         }
121     };
122 
123     private Map<String, ProxyReceiver> receivers = new HashMap<String, ProxyReceiver>();
124 
125     public class WrapperTargetDetails extends TargetDetailsImpl {
126 
127         public WrapperTargetDetails(Map<String, Object> rawDropData) {
128             super(rawDropData, DragAndDropWrapper.this);
129         }
130 
131         /**
132          * @return the absolute position of wrapper on the page
133          */
134         public Integer getAbsoluteLeft() {
135             return (Integer) getData("absoluteLeft");
136         }
137 
138         /**
139          * 
140          * @return the absolute position of wrapper on the page
141          */
142         public Integer getAbsoluteTop() {
143             return (Integer) getData("absoluteTop");
144         }
145 
146         /**
147          * @return a detail about the drags vertical position over the wrapper.
148          */
149         public VerticalDropLocation getVerticalDropLocation() {
150             return VerticalDropLocation
151                     .valueOf((String) getData("verticalLocation"));
152         }
153 
154         /**
155          * @return a detail about the drags horizontal position over the
156          *         wrapper.
157          */
158         public HorizontalDropLocation getHorizontalDropLocation() {
159             return HorizontalDropLocation
160                     .valueOf((String) getData("horizontalLocation"));
161         }
162 
163     }
164 
165     public enum DragStartMode {
166         /**
167          * {@link DragAndDropWrapper} does not start drag events at all
168          */
169         NONE,
170         /**
171          * The component on which the drag started will be shown as drag image.
172          */
173         COMPONENT,
174         /**
175          * The whole wrapper is used as a drag image when dragging.
176          */
177         WRAPPER,
178         /**
179          * The whole wrapper is used to start an HTML5 drag.
180          * 
181          * NOTE: In Internet Explorer 6 to 8, this prevents user interactions
182          * with the wrapper's contents. For example, clicking a button inside
183          * the wrapper will no longer work.
184          */
185         HTML5,
186 
187         /**
188          * Uses the component defined in
189          * {@link #setDragImageComponent(Component)} as the drag image.
190          */
191         COMPONENT_OTHER,
192     }
193 
194     private final Map<String, Object> html5DataFlavors = new LinkedHashMap<String, Object>();
195     private DragStartMode dragStartMode = DragStartMode.NONE;
196     private Component dragImageComponent = null;
197 
198     private Set<String> sentIds = new HashSet<String>();
199 
200     /**
201      * This is an internal constructor. Use
202      * {@link DragAndDropWrapper#DragAndDropWrapper(Component)} instead.
203      * 
204      * @since 7.5.0
205      */
206     @Deprecated
207     public DragAndDropWrapper() {
208         super();
209         registerRpc(rpc);
210     }
211 
212     /**
213      * Wraps given component in a {@link DragAndDropWrapper}.
214      * 
215      * @param root
216      *            the component to be wrapped
217      */
218     public DragAndDropWrapper(Component root) {
219         this();
220         setCompositionRoot(root);
221     }
222 
223     /**
224      * Sets data flavors available in the DragAndDropWrapper is used to start an
225      * HTML5 style drags. Most commonly the "Text" flavor should be set.
226      * Multiple data types can be set.
227      * 
228      * @param type
229      *            the string identifier of the drag "payload". E.g. "Text" or
230      *            "text/html"
231      * @param value
232      *            the value
233      */
234     public void setHTML5DataFlavor(String type, Object value) {
235         html5DataFlavors.put(type, value);
236         markAsDirty();
237     }
238 
239     @Override
240     public void changeVariables(Object source, Map<String, Object> variables) {
241         // TODO Remove once LegacyComponent is no longer implemented
242     }
243 
244     @Override
245     public void paintContent(PaintTarget target) throws PaintException {
246         target.addAttribute(DragAndDropWrapperConstants.DRAG_START_MODE,
247                 dragStartMode.ordinal());
248 
249         if (dragStartMode.equals(DragStartMode.COMPONENT_OTHER)) {
250             if (dragImageComponent != null) {
251                 target.addAttribute(
252                         DragAndDropWrapperConstants.DRAG_START_COMPONENT_ATTRIBUTE,
253                         dragImageComponent.getConnectorId());
254             } else {
255                 throw new IllegalArgumentException(
256                         "DragStartMode.COMPONENT_OTHER set but no component "
257                                 + "was defined. Please set a component using DragAnd"
258                                 + "DropWrapper.setDragStartComponent(Component).");
259             }
260         }
261         if (getDropHandler() != null) {
262             getDropHandler().getAcceptCriterion().paint(target);
263         }
264         if (receivers != null && receivers.size() > 0) {
265             for (Iterator<Entry<String, ProxyReceiver>> it = receivers
266                     .entrySet().iterator(); it.hasNext();) {
267                 Entry<String, ProxyReceiver> entry = it.next();
268                 String id = entry.getKey();
269                 ProxyReceiver proxyReceiver = entry.getValue();
270                 Html5File html5File = proxyReceiver.file;
271                 if (html5File.getStreamVariable() != null) {
272                     if (!sentIds.contains(id)) {
273                         target.addVariable(this, "rec-" + id,
274                                 new ProxyReceiver(id, html5File));
275 
276                         /*
277                          * if a new batch is requested to be uploaded before the
278                          * last one is done, any remaining ids will be replayed.
279                          * We want to avoid a new ProxyReceiver to be made since
280                          * it'll get a new URL, so we need to keep extra track
281                          * on what has been sent.
282                          * 
283                          * See #12330.
284                          */
285                         sentIds.add(id);
286 
287                         // these are cleaned from receivers once the upload has
288                         // started
289                     }
290                 } else {
291                     // instructs the client side not to send the file
292                     target.addVariable(this, "rec-" + id, (String) null);
293                     // forget the file from subsequent paints
294                     it.remove();
295                 }
296             }
297         }
298         target.addAttribute(DragAndDropWrapperConstants.HTML5_DATA_FLAVORS,
299                 html5DataFlavors);
300     }
301 
302     private DropHandler dropHandler;
303 
304     @Override
305     public DropHandler getDropHandler() {
306         return dropHandler;
307     }
308 
309     public void setDropHandler(DropHandler dropHandler) {
310         this.dropHandler = dropHandler;
311         markAsDirty();
312     }
313 
314     @Override
315     public TargetDetails translateDropTargetDetails(
316             Map<String, Object> clientVariables) {
317         return new WrapperTargetDetails(clientVariables);
318     }
319 
320     @Override
321     public Transferable getTransferable(final Map<String, Object> rawVariables) {
322         return new WrapperTransferable(this, rawVariables);
323     }
324 
325     public void setDragStartMode(DragStartMode dragStartMode) {
326         this.dragStartMode = dragStartMode;
327         markAsDirty();
328     }
329 
330     public DragStartMode getDragStartMode() {
331         return dragStartMode;
332     }
333 
334     /**
335      * Sets the component that will be used as the drag image. Only used when
336      * wrapper is set to {@link DragStartMode#COMPONENT_OTHER}
337      * 
338      * @param dragImageComponent
339      */
340     public void setDragImageComponent(Component dragImageComponent) {
341         this.dragImageComponent = dragImageComponent;
342         markAsDirty();
343     }
344 
345     /**
346      * Gets the component that will be used as the drag image. Only used when
347      * wrapper is set to {@link DragStartMode#COMPONENT_OTHER}
348      * 
349      * @return <code>null</code> if no component is set.
350      */
351     public Component getDragImageComponent() {
352         return dragImageComponent;
353     }
354 
355     final class ProxyReceiver implements StreamVariable {
356 
357         private String id;
358         private Html5File file;
359 
360         public ProxyReceiver(String id, Html5File file) {
361             this.id = id;
362             this.file = file;
363         }
364 
365         private boolean listenProgressOfUploadedFile;
366 
367         @Override
368         public OutputStream getOutputStream() {
369             if (file.getStreamVariable() == null) {
370                 return null;
371             }
372             return file.getStreamVariable().getOutputStream();
373         }
374 
375         @Override
376         public boolean listenProgress() {
377             return file.getStreamVariable().listenProgress();
378         }
379 
380         @Override
381         public void onProgress(StreamingProgressEvent event) {
382             file.getStreamVariable().onProgress(
383                     new ReceivingEventWrapper(event));
384         }
385 
386         @Override
387         public void streamingStarted(StreamingStartEvent event) {
388             listenProgressOfUploadedFile = file.getStreamVariable() != null;
389             if (listenProgressOfUploadedFile) {
390                 file.getStreamVariable().streamingStarted(
391                         new ReceivingEventWrapper(event));
392             }
393             // no need tell to the client about this receiver on next paint
394             receivers.remove(id);
395             sentIds.remove(id);
396         }
397 
398         @Override
399         public void streamingFinished(StreamingEndEvent event) {
400             if (listenProgressOfUploadedFile) {
401                 file.getStreamVariable().streamingFinished(
402                         new ReceivingEventWrapper(event));
403             }
404         }
405 
406         @Override
407         public void streamingFailed(final StreamingErrorEvent event) {
408             if (listenProgressOfUploadedFile) {
409                 file.getStreamVariable().streamingFailed(
410                         new ReceivingEventWrapper(event));
411             }
412         }
413 
414         @Override
415         public boolean isInterrupted() {
416             return file.getStreamVariable().isInterrupted();
417         }
418 
419         /*
420          * With XHR2 file posts we can't provide as much information from the
421          * terminal as with multipart request. This helper class wraps the
422          * terminal event and provides the lacking information from the
423          * Html5File.
424          */
425         class ReceivingEventWrapper implements StreamingErrorEvent,
426                 StreamingEndEvent, StreamingStartEvent, StreamingProgressEvent {
427 
428             private StreamingEvent wrappedEvent;
429 
430             ReceivingEventWrapper(StreamingEvent e) {
431                 wrappedEvent = e;
432             }
433 
434             @Override
435             public String getMimeType() {
436                 return file.getType();
437             }
438 
439             @Override
440             public String getFileName() {
441                 return file.getFileName();
442             }
443 
444             @Override
445             public long getContentLength() {
446                 return file.getFileSize();
447             }
448 
449             public StreamVariable getReceiver() {
450                 return ProxyReceiver.this;
451             }
452 
453             @Override
454             public Exception getException() {
455                 if (wrappedEvent instanceof StreamingErrorEvent) {
456                     return ((StreamingErrorEvent) wrappedEvent).getException();
457                 }
458                 return null;
459             }
460 
461             @Override
462             public long getBytesReceived() {
463                 return wrappedEvent.getBytesReceived();
464             }
465 
466             /**
467              * Calling this method has no effect. DD files are receive only once
468              * anyway.
469              */
470             @Override
471             public void disposeStreamVariable() {
472 
473             }
474         }
475 
476     }
477 
478     @Override
479     public void readDesign(Element design, DesignContext designContext) {
480         super.readDesign(design, designContext);
481 
482         for (Element child : design.children()) {
483             Component component = designContext.readDesign(child);
484             if (getDragStartMode() == DragStartMode.COMPONENT_OTHER
485                     && child.hasAttr(":drag-image")) {
486                 setDragImageComponent(component);
487             } else if (getCompositionRoot() == null) {
488                 setCompositionRoot(component);
489             }
490         }
491     }
492 
493     @Override
494     public void writeDesign(Element design, DesignContext designContext) {
495         super.writeDesign(design, designContext);
496 
497         design.appendChild(designContext.createElement(getCompositionRoot()));
498         if (getDragStartMode() == DragStartMode.COMPONENT_OTHER) {
499             Element child = designContext
500                     .createElement(getDragImageComponent());
501             child.attr(":drag-image", true);
502             design.appendChild(child);
503         }
504     }
505 }