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