View Javadoc
1   /*
2    * Copyright 2000-2013 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                              (Integer) rawVariables.get("fs" + i), // 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 details about the actual event that caused the event details.
136          *         Practically mouse move or mouse up.
137          */
138         public MouseEventDetails getMouseEvent() {
139             return MouseEventDetails
140                     .deSerialize((String) getData("mouseEvent"));
141         }
142 
143         /**
144          * @return a detail about the drags vertical position over the wrapper.
145          */
146         public VerticalDropLocation getVerticalDropLocation() {
147             return VerticalDropLocation
148                     .valueOf((String) getData("verticalLocation"));
149         }
150 
151         /**
152          * @return a detail about the drags horizontal position over the
153          *         wrapper.
154          */
155         public HorizontalDropLocation getHorizontalDropLocation() {
156             return HorizontalDropLocation
157                     .valueOf((String) getData("horizontalLocation"));
158         }
159 
160     }
161 
162     public enum DragStartMode {
163         /**
164          * {@link DragAndDropWrapper} does not start drag events at all
165          */
166         NONE,
167         /**
168          * The component on which the drag started will be shown as drag image.
169          */
170         COMPONENT,
171         /**
172          * The whole wrapper is used as a drag image when dragging.
173          */
174         WRAPPER,
175         /**
176          * The whole wrapper is used to start an HTML5 drag.
177          * 
178          * NOTE: In Internet Explorer 6 to 8, this prevents user interactions
179          * with the wrapper's contents. For example, clicking a button inside
180          * the wrapper will no longer work.
181          */
182         HTML5,
183     }
184 
185     private final Map<String, Object> html5DataFlavors = new LinkedHashMap<String, Object>();
186     private DragStartMode dragStartMode = DragStartMode.NONE;
187 
188     private Set<String> sentIds = new HashSet<String>();
189 
190     /**
191      * Wraps given component in a {@link DragAndDropWrapper}.
192      * 
193      * @param root
194      *            the component to be wrapped
195      */
196     public DragAndDropWrapper(Component root) {
197         super(root);
198     }
199 
200     /**
201      * Sets data flavors available in the DragAndDropWrapper is used to start an
202      * HTML5 style drags. Most commonly the "Text" flavor should be set.
203      * Multiple data types can be set.
204      * 
205      * @param type
206      *            the string identifier of the drag "payload". E.g. "Text" or
207      *            "text/html"
208      * @param value
209      *            the value
210      */
211     public void setHTML5DataFlavor(String type, Object value) {
212         html5DataFlavors.put(type, value);
213         markAsDirty();
214     }
215 
216     @Override
217     public void changeVariables(Object source, Map<String, Object> variables) {
218         // TODO Remove once LegacyComponent is no longer implemented
219     }
220 
221     @Override
222     public void paintContent(PaintTarget target) throws PaintException {
223         target.addAttribute(DragAndDropWrapperConstants.DRAG_START_MODE,
224                 dragStartMode.ordinal());
225         if (getDropHandler() != null) {
226             getDropHandler().getAcceptCriterion().paint(target);
227         }
228         if (receivers != null && receivers.size() > 0) {
229             for (Iterator<Entry<String, ProxyReceiver>> it = receivers
230                     .entrySet().iterator(); it.hasNext();) {
231                 Entry<String, ProxyReceiver> entry = it.next();
232                 String id = entry.getKey();
233                 ProxyReceiver proxyReceiver = entry.getValue();
234                 Html5File html5File = proxyReceiver.file;
235                 if (html5File.getStreamVariable() != null) {
236                     if (!sentIds.contains(id)) {
237                         target.addVariable(this, "rec-" + id,
238                                 new ProxyReceiver(id, html5File));
239 
240                         /*
241                          * if a new batch is requested to be uploaded before the
242                          * last one is done, any remaining ids will be replayed.
243                          * We want to avoid a new ProxyReceiver to be made since
244                          * it'll get a new URL, so we need to keep extra track
245                          * on what has been sent.
246                          * 
247                          * See #12330.
248                          */
249                         sentIds.add(id);
250 
251                         // these are cleaned from receivers once the upload has
252                         // started
253                     }
254                 } else {
255                     // instructs the client side not to send the file
256                     target.addVariable(this, "rec-" + id, (String) null);
257                     // forget the file from subsequent paints
258                     it.remove();
259                 }
260             }
261         }
262         target.addAttribute(DragAndDropWrapperConstants.HTML5_DATA_FLAVORS,
263                 html5DataFlavors);
264     }
265 
266     private DropHandler dropHandler;
267 
268     @Override
269     public DropHandler getDropHandler() {
270         return dropHandler;
271     }
272 
273     public void setDropHandler(DropHandler dropHandler) {
274         this.dropHandler = dropHandler;
275         markAsDirty();
276     }
277 
278     @Override
279     public TargetDetails translateDropTargetDetails(
280             Map<String, Object> clientVariables) {
281         return new WrapperTargetDetails(clientVariables);
282     }
283 
284     @Override
285     public Transferable getTransferable(final Map<String, Object> rawVariables) {
286         return new WrapperTransferable(this, rawVariables);
287     }
288 
289     public void setDragStartMode(DragStartMode dragStartMode) {
290         this.dragStartMode = dragStartMode;
291         markAsDirty();
292     }
293 
294     public DragStartMode getDragStartMode() {
295         return dragStartMode;
296     }
297 
298     final class ProxyReceiver implements StreamVariable {
299 
300         private String id;
301         private Html5File file;
302 
303         public ProxyReceiver(String id, Html5File file) {
304             this.id = id;
305             this.file = file;
306         }
307 
308         private boolean listenProgressOfUploadedFile;
309 
310         @Override
311         public OutputStream getOutputStream() {
312             if (file.getStreamVariable() == null) {
313                 return null;
314             }
315             return file.getStreamVariable().getOutputStream();
316         }
317 
318         @Override
319         public boolean listenProgress() {
320             return file.getStreamVariable().listenProgress();
321         }
322 
323         @Override
324         public void onProgress(StreamingProgressEvent event) {
325             file.getStreamVariable().onProgress(
326                     new ReceivingEventWrapper(event));
327         }
328 
329         @Override
330         public void streamingStarted(StreamingStartEvent event) {
331             listenProgressOfUploadedFile = file.getStreamVariable() != null;
332             if (listenProgressOfUploadedFile) {
333                 file.getStreamVariable().streamingStarted(
334                         new ReceivingEventWrapper(event));
335             }
336             // no need tell to the client about this receiver on next paint
337             receivers.remove(id);
338             sentIds.remove(id);
339             // let the terminal GC the streamvariable and not to accept other
340             // file uploads to this variable
341             event.disposeStreamVariable();
342         }
343 
344         @Override
345         public void streamingFinished(StreamingEndEvent event) {
346             if (listenProgressOfUploadedFile) {
347                 file.getStreamVariable().streamingFinished(
348                         new ReceivingEventWrapper(event));
349             }
350         }
351 
352         @Override
353         public void streamingFailed(final StreamingErrorEvent event) {
354             if (listenProgressOfUploadedFile) {
355                 file.getStreamVariable().streamingFailed(
356                         new ReceivingEventWrapper(event));
357             }
358         }
359 
360         @Override
361         public boolean isInterrupted() {
362             return file.getStreamVariable().isInterrupted();
363         }
364 
365         /*
366          * With XHR2 file posts we can't provide as much information from the
367          * terminal as with multipart request. This helper class wraps the
368          * terminal event and provides the lacking information from the
369          * Html5File.
370          */
371         class ReceivingEventWrapper implements StreamingErrorEvent,
372                 StreamingEndEvent, StreamingStartEvent, StreamingProgressEvent {
373 
374             private StreamingEvent wrappedEvent;
375 
376             ReceivingEventWrapper(StreamingEvent e) {
377                 wrappedEvent = e;
378             }
379 
380             @Override
381             public String getMimeType() {
382                 return file.getType();
383             }
384 
385             @Override
386             public String getFileName() {
387                 return file.getFileName();
388             }
389 
390             @Override
391             public long getContentLength() {
392                 return file.getFileSize();
393             }
394 
395             public StreamVariable getReceiver() {
396                 return ProxyReceiver.this;
397             }
398 
399             @Override
400             public Exception getException() {
401                 if (wrappedEvent instanceof StreamingErrorEvent) {
402                     return ((StreamingErrorEvent) wrappedEvent).getException();
403                 }
404                 return null;
405             }
406 
407             @Override
408             public long getBytesReceived() {
409                 return wrappedEvent.getBytesReceived();
410             }
411 
412             /**
413              * Calling this method has no effect. DD files are receive only once
414              * anyway.
415              */
416             @Override
417             public void disposeStreamVariable() {
418 
419             }
420         }
421 
422     }
423 
424 }