View Javadoc
1   /**
2    * This file Copyright (c) 2013-2016 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.ui.form.field.upload;
35  
36  import info.magnolia.cms.beans.config.MIMEMapping;
37  
38  import java.io.OutputStream;
39  import java.util.Locale;
40  
41  import org.apache.commons.lang3.StringEscapeUtils;
42  import org.apache.commons.lang3.StringUtils;
43  import org.slf4j.Logger;
44  import org.slf4j.LoggerFactory;
45  
46  import com.vaadin.data.Property;
47  import com.vaadin.event.dd.DragAndDropEvent;
48  import com.vaadin.event.dd.DropHandler;
49  import com.vaadin.event.dd.acceptcriteria.AcceptAll;
50  import com.vaadin.event.dd.acceptcriteria.AcceptCriterion;
51  import com.vaadin.server.StreamVariable;
52  import com.vaadin.server.UploadException;
53  import com.vaadin.ui.Component;
54  import com.vaadin.ui.CustomField;
55  import com.vaadin.ui.DragAndDropWrapper;
56  import com.vaadin.ui.DragAndDropWrapper.WrapperTransferable;
57  import com.vaadin.ui.HasComponents;
58  import com.vaadin.ui.Html5File;
59  import com.vaadin.ui.UI;
60  import com.vaadin.ui.Upload;
61  import com.vaadin.ui.Upload.FailedEvent;
62  import com.vaadin.ui.Upload.FailedListener;
63  import com.vaadin.ui.Upload.FinishedEvent;
64  import com.vaadin.ui.Upload.FinishedListener;
65  import com.vaadin.ui.Upload.ProgressListener;
66  import com.vaadin.ui.Upload.StartedEvent;
67  import com.vaadin.ui.Upload.StartedListener;
68  
69  /**
70   * Main implementation of the UploadFile field. This implementation used some
71   * features of {@link org.vaadin.easyuploads.UploadField} and associated
72   * classes.
73   * <p>
74   * This class handles Upload events and expose functions that allows to customize the 3 main upload states (link to a view Component):
75   * <ul>
76   * <li>Empty: Display the initial view (No Upload was performed)</li>
77   * <li>InProgress: Display the progress view (Progress Bar) <br>
78   * This view is triggered by a {@link StartedEvent}.</li>
79   * <li>Completed: Display the File detail and Icon <br>
80   * This event is triggered by <br>
81   * {@link FinishedEvent} <br>
82   * {@link FailedEvent}</li>
83   * </ul>
84   * <b>Important exposed method</b><br>
85   * {@link Upload} getUpload() : Return the Vaadin Upload Component responsible for the Uploading a File based on a folder. <br>
86   * createDropZone(Component c) : Give the Drop ability to the passed Component.<br>
87   * 
88   * @param <T> {@link UploadReceiver} implemented class.
89   */
90  public abstract class AbstractUploadField<T extends UploadReceiver> extends CustomField<T> implements StartedListener, FinishedListener, ProgressListener, FailedListener, DropHandler, UploadField {
91  
92      private static final Logger log = LoggerFactory.getLogger(AbstractUploadField.class);
93  
94      private long maxUploadSize = Long.MAX_VALUE;
95  
96      private String allowedMimeTypePattern = ".*";
97  
98      // Used to handle Cancel / Interrupted upload in the DragAndDrop
99      // implementation.
100     private boolean interruptedDragAndDropUpload = false;
101 
102     private Upload upload;
103 
104     private DragAndDropWrapper dropZone;
105 
106     private HasComponents root;
107 
108     /**
109      * Build the Empty Layout.<br>
110      * Use the fileWrapper to display file information and Status.
111      */
112     protected abstract void buildEmptyLayout();
113 
114     /**
115      * Build the in Progress Layout.<br>
116      * Generally display a progress bar {@link UploadProgressIndicator} and some file information.<br>
117      * Refresh of the action bar is handled by refreshInProgressLayout(...)<br>
118      * Use the fileWrapper to display file information and Status.
119      */
120     protected abstract void buildInProgressLayout(String uploadedFileMimeType);
121 
122     /**
123      * Update the in Progress Layout.<br>
124      */
125     protected abstract void refreshInProgressLayout(long readBytes, long contentLength, String fileName);
126 
127     /**
128      * Build the Completed Layout.<br>
129      * Use the fileWrapper to display file information and Status.
130      */
131     protected abstract void buildCompletedLayout();
132 
133     protected abstract void displayUploadInterruptNote(InterruptionReason reason);
134 
135     protected abstract void displayUploadFinishedNote(String fileName);
136 
137     protected abstract void displayUploadFailedNote(String fileName);
138 
139     @Override
140     public void setLocale(Locale locale) {
141         if (root != null) {
142             updateDisplay();
143         }
144     }
145 
146     @Override
147     public void setPropertyDataSource(Property newDataSource) {
148         super.setPropertyDataSource(newDataSource);
149         createUpload();
150     }
151 
152     /**
153      * Call the correct layout.
154      * <ul>
155      * <li>- Empty: --> buildEmptyLayout()
156      * <li>- Completed: --> buildCompletedLayout()
157      * </ul>
158      */
159     protected void updateDisplay() {
160         if (getValue() != null) {
161             if (this.getValue().isEmpty()) {
162                 buildEmptyLayout();
163             } else {
164                 buildCompletedLayout();
165             }
166             markAsDirty();
167         }
168     }
169 
170     /**
171      * Interrupt upload based on a user Action.
172      * An {@link com.vaadin.server.communication.FileUploadHandler.UploadInterruptedException} will be thrown by the underlying Vaadin classes.
173      */
174     protected void interruptUpload(InterruptionReason reason) {
175         displayUploadInterruptNote(reason);
176         if (upload.isUploading()) {
177             upload.interruptUpload();
178         } else {
179             setDragAndDropUploadInterrupted(true);
180         }
181     }
182 
183     private void setDragAndDropUploadInterrupted(boolean isInterrupted) {
184         interruptedDragAndDropUpload = isInterrupted;
185     }
186 
187     private boolean isDragAndDropUploadInterrupted() {
188         return interruptedDragAndDropUpload;
189     }
190 
191     /**
192      * Simple Enumeration listing all available Interruption reason.
193      */
194     protected enum InterruptionReason {
195         USER, FILE_NOT_ALLOWED, FILE_SIZE;
196     }
197 
198     /**
199      * Define the acceptance Upload Image criteria.
200      * The current implementation only check if the MimeType match the desired regExp.
201      */
202     protected boolean isValidFile(StartedEvent event) {
203         log.debug("Evaluate following regExp: {} against {}", allowedMimeTypePattern, event.getMIMEType());
204         return event.getMIMEType().matches(allowedMimeTypePattern);
205     }
206 
207     /**
208      * Perform a post validation based on the File MimeType.
209      */
210     private boolean postFileValidation() {
211         // TODO check for the best solution
212         // http://www.rgagnon.com/javadetails/java-0487.html
213         return true;
214     }
215 
216     /**
217      * Create the Upload component.
218      */
219     private void createUpload() {
220         this.upload = new Upload(null, getValue());
221         this.upload.addStartedListener(this);
222         this.upload.addFinishedListener(this);
223         this.upload.addProgressListener(this);
224         this.upload.setImmediate(true);
225     }
226 
227     /**
228      * @return the initialized Upload component.
229      */
230     protected Upload getUpload() {
231         return this.upload;
232     }
233 
234     /**
235      * @return the initialized DragAndDropWrapper.
236      */
237     protected DragAndDropWrapper getDropZone() {
238         return this.dropZone;
239     }
240 
241     /**
242      * The dropZone is a wrapper around a Component.
243      */
244     protected DragAndDropWrapper createDropZone(Component c) {
245         dropZone = new DragAndDropWrapper(c) {
246 
247         };
248         dropZone.setDropHandler(this);
249         return this.dropZone;
250     }
251 
252     /**
253      * Drop zone Handler.
254      */
255     @Override
256     public void drop(DragAndDropEvent event) {
257         final DragAndDropWrapper.WrapperTransferable transferable = (WrapperTransferable) event.getTransferable();
258         final Html5File[] files = transferable.getFiles();
259         if (files == null) {
260             return;
261         }
262 
263         // start polling immediately on drop
264         startPolling();
265 
266         for (final Html5File html5File : files) {
267             html5File.setStreamVariable(new StreamVariable() {
268 
269                 private String name;
270                 private String mime;
271 
272                 @Override
273                 public OutputStream getOutputStream() {
274                     return getValue().receiveUpload(name, mime);
275                 }
276 
277                 @Override
278                 public boolean listenProgress() {
279                     return true;
280                 }
281 
282                 @Override
283                 public void onProgress(StreamingProgressEvent event) {
284                     updateProgress(event.getBytesReceived(), event.getContentLength());
285                 }
286 
287                 @Override
288                 public void streamingStarted(StreamingStartEvent event) {
289                     setDragAndDropUploadInterrupted(false);
290                     name = event.getFileName();
291                     mime = event.getMimeType();
292                     if (StringUtils.isEmpty(mime)) {
293                         String extension = StringUtils.substringAfterLast(name, ".");
294                         mime = MIMEMapping.getMIMEType(extension);
295                         if(StringUtils.isEmpty(mime)) {
296                             log.warn("Couldn't find mimeType in MIMEMappings for file extension: {}", extension);
297                         }
298                     }
299                     StartedEvent startEvent = new StartedEvent(upload, name, mime, event.getContentLength());
300                     uploadStarted(startEvent);
301                 }
302 
303                 @Override
304                 public void streamingFinished(StreamingEndEvent event) {
305                     FinishedEvent uploadEvent = new FinishedEvent(upload, event.getFileName(), event.getMimeType(), event.getContentLength());
306                     uploadFinished(uploadEvent);
307                 }
308 
309                 @Override
310                 public void streamingFailed(StreamingErrorEvent event) {
311                     FailedEvent failedEvent = new FailedEvent(upload, event.getFileName(), event.getMimeType(), event.getContentLength());
312                     uploadFailed(failedEvent);
313                     setDragAndDropUploadInterrupted(false);
314                 }
315 
316                 @Override
317                 public synchronized boolean isInterrupted() {
318                     return isDragAndDropUploadInterrupted();
319                 }
320 
321             });
322         }
323     }
324 
325     /**
326      * Handled by isValidFile().
327      */
328     @Override
329     public AcceptCriterion getAcceptCriterion() {
330         return AcceptAll.get();
331     }
332 
333     /**
334      * Start Upload if the file is supported. <br>
335      * In case of not a supported file, interrupt the Upload.
336      */
337     @Override
338     public void uploadStarted(StartedEvent event) {
339         if (isValidFile(event)) {
340             buildInProgressLayout(event.getMIMEType());
341 
342             // start polling here in case of upload through file input
343             startPolling();
344         } else {
345             interruptUpload(InterruptionReason.FILE_NOT_ALLOWED);
346         }
347     }
348 
349     /**
350      * Update the Progress Component. At the same time, check if the uploaded
351      * File is not bigger as expected. Interrupt the Upload in this case.
352      */
353     @Override
354     public void updateProgress(long readBytes, long contentLength) {
355         if (readBytes > this.maxUploadSize || contentLength > this.maxUploadSize) {
356             this.interruptUpload(InterruptionReason.FILE_SIZE);
357             return;
358         }
359         refreshInProgressLayout(readBytes, contentLength, getValue().getLastFileName());
360     }
361 
362     /**
363      * Handle the {@link FinishedEvent}. In case of success: <br>
364      * - Populate the Uploaded Information to the fileWrapper. <br>
365      * - Build the Completed Layout. <br>
366      * In case of {@link FailedEvent} (this event
367      * is send on a Cancel upload) <br>
368      * - Do not populate data and call uploadFailed().
369      */
370     @Override
371     public void uploadFinished(FinishedEvent event) {
372 
373         stopPolling();
374 
375         if (event instanceof FailedEvent) {
376             uploadFailed((FailedEvent) event);
377             return;
378         }
379         // Post check Upload.
380         if (!postFileValidation()) {
381             FailedEvent newEvent = new FailedEvent(upload, getValue().getFileName(), getValue().getMimeType(), getValue().getFileSize());
382             uploadFailed(newEvent);
383             return;
384         }
385         displayUploadFinishedNote(StringEscapeUtils.escapeHtml4(event.getFilename()));
386         this.getPropertyDataSource().setValue(event.getUpload().getReceiver());
387         buildCompletedLayout();
388         fireValueChange(false);
389     }
390 
391     @Override
392     public void uploadFailed(FailedEvent event) {
393 
394         stopPolling();
395 
396         if (event.getReason() instanceof UploadException) {
397             displayUploadFailedNote(event.getFilename());
398         }
399         resetDataSource();
400         updateDisplay();
401         log.warn("Upload failed for file {} ", event.getFilename());
402     }
403 
404     @Override
405     public Class getType() {
406         return UploadReceiver.class;
407     }
408 
409     protected void resetDataSource() {
410         getValue().setValue(null);
411         getPropertyDataSource().setValue(getValue());
412     }
413 
414     protected HasComponents getRootLayout() {
415         return this.root;
416     }
417 
418     protected void setRootLayout(HasComponents root) {
419         this.root = root;
420     }
421 
422     @Override
423     public void setAllowedMimeTypePattern(String allowedMimeTypePattern) {
424         this.allowedMimeTypePattern = allowedMimeTypePattern;
425 
426     }
427 
428     @Override
429     public void setMaxUploadSize(long maxUploadSize) {
430         this.maxUploadSize = maxUploadSize;
431     }
432 
433     @Override
434     public void detach() {
435         if (upload.isUploading()) {
436             upload.interruptUpload();
437         } else {
438             setDragAndDropUploadInterrupted(true);
439         }
440         super.detach();
441     }
442 
443     private void startPolling() {
444         UI ui = UI.getCurrent();
445         if (ui != null) {
446             ui.access(new Runnable() {
447 
448                 @Override
449                 public void run() {
450                     UI.getCurrent().setPollInterval(200);
451                 }
452             });
453         }
454     }
455 
456     private void stopPolling() {
457         UI ui = UI.getCurrent();
458         if (ui != null) {
459             ui.access(new Runnable() {
460 
461                 @Override
462                 public void run() {
463                     UI.getCurrent().setPollInterval(-1);
464                 }
465             });
466         }
467     }
468 
469 }