View Javadoc
1   /**
2    * This file Copyright (c) 2013-2018 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.event.dd.DragAndDropEvent;
47  import com.vaadin.event.dd.DropHandler;
48  import com.vaadin.event.dd.acceptcriteria.AcceptAll;
49  import com.vaadin.event.dd.acceptcriteria.AcceptCriterion;
50  import com.vaadin.server.StreamVariable;
51  import com.vaadin.server.UploadException;
52  import com.vaadin.ui.Component;
53  import com.vaadin.ui.DragAndDropWrapper;
54  import com.vaadin.ui.DragAndDropWrapper.WrapperTransferable;
55  import com.vaadin.ui.HasComponents;
56  import com.vaadin.ui.Html5File;
57  import com.vaadin.ui.UI;
58  import com.vaadin.v7.data.Property;
59  import com.vaadin.v7.ui.CustomField;
60  import com.vaadin.v7.ui.Upload;
61  import com.vaadin.v7.ui.Upload.FailedEvent;
62  import com.vaadin.v7.ui.Upload.FailedListener;
63  import com.vaadin.v7.ui.Upload.FinishedEvent;
64  import com.vaadin.v7.ui.Upload.FinishedListener;
65  import com.vaadin.v7.ui.Upload.ProgressListener;
66  import com.vaadin.v7.ui.Upload.StartedEvent;
67  import com.vaadin.v7.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     private UploadStatus uploadStatus = UploadStatus.READY;
109 
110     /**
111      * Build the Empty Layout.<br>
112      * Use the fileWrapper to display file information and Status.
113      */
114     protected abstract void buildEmptyLayout();
115 
116     /**
117      * Build the in Progress Layout.<br>
118      * Generally display a progress bar {@link UploadProgressIndicator} and some file information.<br>
119      * Refresh of the action bar is handled by refreshInProgressLayout(...)<br>
120      * Use the fileWrapper to display file information and Status.
121      */
122     protected abstract void buildInProgressLayout(String uploadedFileMimeType);
123 
124     /**
125      * Update the in Progress Layout.<br>
126      */
127     protected abstract void refreshInProgressLayout(long readBytes, long contentLength, String fileName);
128 
129     /**
130      * Build the Completed Layout.<br>
131      * Use the fileWrapper to display file information and Status.
132      */
133     protected abstract void buildCompletedLayout();
134 
135     protected abstract void displayUploadInterruptNote(InterruptionReason reason);
136 
137     protected abstract void displayUploadFinishedNote(String fileName);
138 
139     protected abstract void displayUploadFailedNote(String fileName);
140 
141     @Override
142     public void setLocale(Locale locale) {
143         if (root != null) {
144             updateDisplay();
145         }
146     }
147 
148     @Override
149     public void setPropertyDataSource(Property newDataSource) {
150         super.setPropertyDataSource(newDataSource);
151         createUpload();
152     }
153 
154     /**
155      * Call the correct layout.
156      * <ul>
157      * <li>- Empty: --> buildEmptyLayout()
158      * <li>- Completed: --> buildCompletedLayout()
159      * </ul>
160      */
161     protected void updateDisplay() {
162         if (getValue() != null) {
163             if (this.getValue().isEmpty()) {
164                 buildEmptyLayout();
165             } else {
166                 buildCompletedLayout();
167             }
168             markAsDirty();
169         }
170     }
171 
172     /**
173      * Interrupt upload based on a user Action.
174      * An {@link com.vaadin.server.communication.FileUploadHandler.UploadInterruptedException} will be thrown by the underlying Vaadin classes.
175      */
176     protected void interruptUpload(InterruptionReason reason) {
177         displayUploadInterruptNote(reason);
178         if (upload.isUploading()) {
179             upload.interruptUpload();
180         } else {
181             setDragAndDropUploadInterrupted(true);
182         }
183         // current upload cancelled, should be ready for new upload session
184         uploadStatus = UploadStatus.READY;
185     }
186 
187     private void setDragAndDropUploadInterrupted(boolean isInterrupted) {
188         interruptedDragAndDropUpload = isInterrupted;
189     }
190 
191     private boolean isDragAndDropUploadInterrupted() {
192         return interruptedDragAndDropUpload;
193     }
194 
195     /**
196      * Simple Enumeration listing all available Interruption reason.
197      */
198     protected enum InterruptionReason {
199         USER, FILE_NOT_ALLOWED, FILE_SIZE;
200     }
201 
202     /**
203      * Simple Enumeration listing all available Interruption reason.
204      */
205     protected enum UploadStatus {
206         READY, UPLOADING, FINISHED
207     }
208 
209     /**
210      * Define the acceptance Upload Image criteria.
211      * The current implementation only check if the MimeType match the desired regExp.
212      */
213     protected boolean isValidFile(StartedEvent event) {
214         log.debug("Evaluate following regExp: {} against {}", allowedMimeTypePattern, event.getMIMEType());
215         return event.getMIMEType().matches(allowedMimeTypePattern);
216     }
217 
218     /**
219      * Perform a post validation based on the File MimeType.
220      */
221     private boolean postFileValidation() {
222         // TODO check for the best solution
223         // http://www.rgagnon.com/javadetails/java-0487.html
224         return true;
225     }
226 
227     /**
228      * Create the Upload component.
229      */
230     private void createUpload() {
231         this.upload = new Upload(null, getValue());
232         this.upload.addStartedListener(this);
233         this.upload.addFinishedListener(this);
234         this.upload.addProgressListener(this);
235         this.upload.setImmediate(true);
236     }
237 
238     /**
239      * @return the initialized Upload component.
240      */
241     protected Upload getUpload() {
242         return this.upload;
243     }
244 
245     /**
246      * @return the initialized DragAndDropWrapper.
247      */
248     protected DragAndDropWrapper getDropZone() {
249         return this.dropZone;
250     }
251 
252     /**
253      * The dropZone is a wrapper around a Component.
254      */
255     protected DragAndDropWrapper createDropZone(Component c) {
256         dropZone = new DragAndDropWrapper(c);
257         dropZone.setDropHandler(this);
258         return this.dropZone;
259     }
260 
261     /**
262      * Drop zone Handler.
263      */
264     @Override
265     public void drop(DragAndDropEvent event) {
266 
267         // If there is value -> uploading some file, new request should be skipped
268         if (!UploadStatus.READY.equals(uploadStatus)) {
269             log.debug("Not ready to upload new file, skipping, is there any other file is uploading?");
270             return;
271         }
272 
273         // Drop new file and job is free -> allow drop upload.
274         setDragAndDropUploadInterrupted(false);
275 
276         final DragAndDropWrapper.WrapperTransferable transferable = (WrapperTransferable) event.getTransferable();
277         final Html5File[] files = transferable.getFiles();
278         if (files == null) {
279             return;
280         }
281 
282         // start polling immediately on drop
283         startPolling();
284 
285         for (final Html5File html5File : files) {
286             html5File.setStreamVariable(new StreamVariable() {
287 
288                 private String name;
289                 private String mime;
290 
291                 @Override
292                 public OutputStream getOutputStream() {
293                     return getValue().receiveUpload(name, mime);
294                 }
295 
296                 @Override
297                 public boolean listenProgress() {
298                     return true;
299                 }
300 
301                 @Override
302                 public void onProgress(StreamingProgressEvent event) {
303                     updateProgress(event.getBytesReceived(), event.getContentLength());
304                 }
305 
306                 @Override
307                 public void streamingStarted(StreamingStartEvent event) {
308                     name = event.getFileName();
309                     mime = event.getMimeType();
310                     if (StringUtils.isEmpty(mime)) {
311                         String extension = StringUtils.substringAfterLast(name, ".");
312                         final String mimeType = MIMEMapping.getMIMEType(extension);
313                         mime = mimeType == null ? "" : mimeType;
314                         if(StringUtils.isEmpty(mime)) {
315                             log.warn("Couldn't find mimeType in MIMEMappings for file extension: {}", extension);
316                         }
317                     }
318                     StartedEvent startEvent = new StartedEvent(upload, name, mime, event.getContentLength());
319                     uploadStarted(startEvent);
320                 }
321 
322                 @Override
323                 public void streamingFinished(StreamingEndEvent event) {
324                     FinishedEvent uploadEvent = new FinishedEvent(upload, event.getFileName(), event.getMimeType(), event.getContentLength());
325                     uploadFinished(uploadEvent);
326                 }
327 
328                 @Override
329                 public void streamingFailed(StreamingErrorEvent event) {
330                     FailedEvent failedEvent = new FailedEvent(upload, event.getFileName(), event.getMimeType(), event.getContentLength());
331                     uploadFailed(failedEvent);
332                 }
333 
334                 @Override
335                 public synchronized boolean isInterrupted() {
336                     return isDragAndDropUploadInterrupted();
337                 }
338 
339 
340             });
341         }
342     }
343 
344     /**
345      * Handled by isValidFile().
346      */
347     @Override
348     public AcceptCriterion getAcceptCriterion() {
349         return AcceptAll.get();
350     }
351 
352     /**
353      * Start Upload if the file is supported. <br>
354      * In case of not a supported file, interrupt the Upload.
355      */
356     @Override
357     public void uploadStarted(StartedEvent event) {
358         if (isValidFile(event)) {
359             buildInProgressLayout(event.getMIMEType());
360 
361             // start polling here in case of upload through file input
362             startPolling();
363         } else {
364             interruptUpload(InterruptionReason.FILE_NOT_ALLOWED);
365         }
366         uploadStatus = UploadStatus.UPLOADING;
367     }
368 
369     /**
370      * Update the Progress Component. At the same time, check if the uploaded
371      * File is not bigger as expected. Interrupt the Upload in this case.
372      */
373     @Override
374     public void updateProgress(long readBytes, long contentLength) {
375         if (readBytes > this.maxUploadSize || contentLength > this.maxUploadSize) {
376             this.interruptUpload(InterruptionReason.FILE_SIZE);
377             return;
378         }
379         refreshInProgressLayout(readBytes, contentLength, getValue().getLastFileName());
380     }
381 
382     /**
383      * Handle the {@link FinishedEvent}. In case of success: <br>
384      * - Populate the Uploaded Information to the fileWrapper. <br>
385      * - Build the Completed Layout. <br>
386      * In case of {@link FailedEvent} (this event
387      * is send on a Cancel upload) <br>
388      * - Do not populate data and call uploadFailed().
389      */
390     @Override
391     public void uploadFinished(FinishedEvent event) {
392 
393         stopPolling();
394 
395         if (event instanceof FailedEvent) {
396             uploadFailed((FailedEvent) event);
397             return;
398         }
399         // Post check Upload.
400         if (!postFileValidation()) {
401             FailedEvent newEvent = new FailedEvent(upload, getValue().getFileName(), getValue().getMimeType(), getValue().getFileSize());
402             uploadFailed(newEvent);
403             return;
404         }
405         displayUploadFinishedNote(StringEscapeUtils.escapeHtml4(event.getFilename()));
406         this.getPropertyDataSource().setValue(event.getUpload().getReceiver());
407         buildCompletedLayout();
408         fireValueChange(false);
409         uploadStatus = UploadStatus.READY;
410     }
411 
412     @Override
413     public void uploadFailed(FailedEvent event) {
414 
415         stopPolling();
416 
417         if (event.getReason() instanceof UploadException) {
418             displayUploadFailedNote(event.getFilename());
419         }
420         resetDataSource();
421         updateDisplay();
422         log.warn("Upload failed for file {} ", event.getFilename());
423         uploadStatus = UploadStatus.READY;
424     }
425 
426     @Override
427     public Class getType() {
428         return UploadReceiver.class;
429     }
430 
431     protected void resetDataSource() {
432         getValue().setValue(null);
433         getPropertyDataSource().setValue(getValue());
434     }
435 
436     protected HasComponents getRootLayout() {
437         return this.root;
438     }
439 
440     protected void setRootLayout(HasComponents root) {
441         this.root = root;
442     }
443 
444     @Override
445     public void setAllowedMimeTypePattern(String allowedMimeTypePattern) {
446         this.allowedMimeTypePattern = allowedMimeTypePattern;
447 
448     }
449 
450     @Override
451     public void setMaxUploadSize(long maxUploadSize) {
452         this.maxUploadSize = maxUploadSize;
453     }
454 
455     @Override
456     public void detach() {
457         if (upload.isUploading()) {
458             upload.interruptUpload();
459         } else {
460             setDragAndDropUploadInterrupted(true);
461         }
462         super.detach();
463     }
464 
465     private void startPolling() {
466         UI ui = UI.getCurrent();
467         if (ui != null) {
468             ui.access(() -> UI.getCurrent().setPollInterval(200));
469         }
470     }
471 
472     private void stopPolling() {
473         UI ui = UI.getCurrent();
474         if (ui != null) {
475             ui.access(() -> UI.getCurrent().setPollInterval(-1));
476         }
477     }
478 
479 }