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.basic;
35  
36  import info.magnolia.i18nsystem.SimpleTranslator;
37  import info.magnolia.ui.api.context.UiContext;
38  import info.magnolia.ui.form.field.definition.BasicUploadFieldDefinition;
39  import info.magnolia.ui.form.field.upload.AbstractUploadField;
40  import info.magnolia.ui.form.field.upload.UploadProgressIndicator;
41  import info.magnolia.ui.form.field.upload.UploadReceiver;
42  import info.magnolia.ui.imageprovider.ImageProvider;
43  import info.magnolia.ui.vaadin.overlay.MessageStyleTypeEnum;
44  
45  import org.apache.commons.io.FileUtils;
46  import org.apache.commons.lang3.StringUtils;
47  import org.slf4j.Logger;
48  import org.slf4j.LoggerFactory;
49  
50  import com.vaadin.ui.Alignment;
51  import com.vaadin.ui.Button;
52  import com.vaadin.ui.Button.ClickEvent;
53  import com.vaadin.ui.Component;
54  import com.vaadin.ui.CssLayout;
55  import com.vaadin.ui.FormLayout;
56  import com.vaadin.ui.Layout;
57  import com.vaadin.ui.NativeButton;
58  import com.vaadin.v7.data.Property;
59  import com.vaadin.v7.shared.ui.label.ContentMode;
60  import com.vaadin.v7.ui.HorizontalLayout;
61  import com.vaadin.v7.ui.Label;
62  import com.vaadin.v7.ui.TextField;
63  
64  /**
65   * Basic implementation of {@link AbstractUploadField}.<br>
66   * Define the Layout components for
67   * <ul>
68   * <li>EmptyLayout (no File are yet uploaded)
69   * <li>InProgressLayout (ProgressBar / Cancel Button...)
70   * <li>CompletedLayout (File Detail / Preview ...)
71   * </ul>
72   * 
73   * @param <T> {@link UploadReceiver} implemented class.
74   */
75  public class BasicUploadField<T extends UploadReceiver> extends AbstractUploadField<T> {
76      private static final long serialVersionUID = 1L;
77      private static final Logger log = LoggerFactory.getLogger(BasicUploadField.class);
78  
79      private static final String PREFIX_MEDIA_KEY = "field.upload.media";
80      private static final String MEDIA = "media";
81  
82      // Root layout
83      private final CssLayout layout;
84      private UploadProgressIndicator progress;
85      protected final ImageProvider imageProvider;
86      private boolean editFileName = false;
87      private boolean editFileFormat = false;
88      protected UiContext uiContext;
89      private final SimpleTranslator i18n;
90  
91      public BasicUploadField(ImageProvider imageProvider, UiContext uiContext, BasicUploadFieldDefinition definition, SimpleTranslator i18n) {
92          super();
93          // Propagate definition.
94          populateFromDefinition(definition);
95  
96          this.imageProvider = imageProvider;
97          this.layout = new CssLayout();
98          this.layout.setSizeUndefined();
99          this.uiContext = uiContext;
100         this.i18n = i18n;
101 
102         setRootLayout(createDropZone(layout));
103         // Update Style Name
104         addStyleName("upload-image-field");
105         addStyleName("no-horizontal-drag-hints");
106         addStyleName("no-vertical-drag-hints");
107     }
108 
109     /**
110      * Initialize the root component.
111      * Build the initial layout:
112      * - Empty Layout if the incoming Item is empty.
113      * - Completed Layout with the Item Informations if this Item is not empty.
114      */
115     @Override
116     public void attach() {
117         super.attach();
118         updateDisplay();
119         log.debug("Component was attached ...");
120     }
121 
122     /**
123      * Main entry point to create the Empty Layout.
124      * This Basic implementation of Empty layout is composed of <br>
125      * - An Upload button <br>
126      * - A Label inviting to Drag files <br>
127      */
128     @Override
129     protected void buildEmptyLayout() {
130         layout.removeAllComponents();
131         if (isReadOnly()) {
132             return;
133         }
134         // Add Upload Button
135         getUpload().setButtonCaption(getCaption(selectNewCaption, null));
136         layout.addComponent(getUpload());
137         // Add DropZone Label
138         Label uploadText = new Label(getCaption(dropZoneCaption, null), ContentMode.HTML);
139         uploadText.addStyleName("upload-text");
140         layout.addComponent(uploadText);
141 
142         // Update Style Name
143         getRootLayout().removeStyleName("start");
144         getRootLayout().removeStyleName("done");
145         getRootLayout().removeStyleName("in-progress");
146         getRootLayout().addStyleName("upload");
147         getRootLayout().addStyleName("initial");
148 
149         log.debug("buildEmptyLayout() called ...");
150     }
151 
152     /**
153      * Main entry point to create the In Progress Layout.
154      * This Basic implementation of In Progress is composed of <br>
155      * - A Progress Bar <br>
156      * - A Cancel Button <br>
157      */
158     @Override
159     protected void buildInProgressLayout(String uploadedFileMimeType) {
160         layout.removeAllComponents();
161         // Update the caption Extension
162         setCaptionExtension(uploadedFileMimeType);
163         // Create the process Indigator
164         progress = new BasicUploadProgressIndicator(inProgressCaption, inProgressRatioCaption, i18n);
165         progress.setVisible(true);
166         progress.setProgress(0);
167         layout.addComponent(progress.asVaadinComponent());
168 
169         // Add the Cancel Button
170         layout.addComponent(createCancelButton());
171 
172         // Update Style Name
173         getRootLayout().removeStyleName("done");
174         getRootLayout().addStyleName("upload");
175         getRootLayout().addStyleName("initial");
176         getRootLayout().addStyleName("in-progress");
177 
178         log.debug("buildInProgressLayout() called ...");
179     }
180 
181     @Override
182     protected void refreshInProgressLayout(long readBytes, long contentLength, String fileName) {
183         if (progress != null) {
184             progress.refreshLayout(readBytes, contentLength, fileName);
185         }
186     }
187 
188     /**
189      * Main entry point to create the Completed Layout.
190      * This Basic implementation of Completed Layout is composed of <br>
191      * - An Icon representing the File type <br>
192      * - A Detail text information <br>
193      * - An Action bar: UploadNew and Delete Actions <br>
194      * <b>Override</b><br>
195      * getFileInfo() In order to change the Displayed Text <br>
196      * createIconStyleName() In order to change the Icon Style displayed.
197      */
198     @Override
199     protected void buildCompletedLayout() {
200         layout.removeAllComponents();
201         // Update the caption Extension
202         setCaptionExtension(null);
203         // Create the File Detail Component
204         layout.addComponent(createFileInfoComponent());
205         // Create the Action Layout
206         layout.addComponent(createCompletedActionLayout());
207 
208         // Create Preview
209         layout.addComponent(createThumbnailComponent());
210 
211         // Update Style Name
212         getRootLayout().addStyleName("upload");
213         getRootLayout().removeStyleName("in-progress");
214         getRootLayout().removeStyleName("initial");
215         getRootLayout().addStyleName("done");
216 
217         log.debug("buildCompletedLayout() called ...");
218     }
219 
220     /**
221      * Build the Completed Action Layout.
222      */
223     protected Layout createCompletedActionLayout() {
224         // Action Button (Upload new or delete). Default is always Upload
225         HorizontalLayout actionLayout = new HorizontalLayout();
226         actionLayout.setSizeUndefined();
227         actionLayout.addStyleName("buttons");
228         actionLayout.setSpacing(true);
229         // Add Upload Button
230         getUpload().setButtonCaption(getCaption(selectAnotherCaption, null));
231         actionLayout.addComponent(getUpload());
232         // Add Remove Button if a file is present.
233         if (!getValue().isEmpty()) {
234             Button delete = createDeleteButton();
235             actionLayout.addComponent(delete);
236             actionLayout.setComponentAlignment(delete, Alignment.MIDDLE_RIGHT);
237         }
238         return actionLayout;
239     }
240 
241     /**
242      * Create the Cancel Button.
243      * Used to cancel an ongoing Upload.
244      */
245     private Button createCancelButton() {
246         Button cancelButton = new NativeButton(null, new Button.ClickListener() {
247             private static final long serialVersionUID = 1L;
248 
249             @Override
250             public void buttonClick(ClickEvent event) {
251                 interruptUpload(InterruptionReason.USER);
252             }
253         });
254         cancelButton.addStyleName("cancel");
255         return cancelButton;
256     }
257 
258     /**
259      * Create Delete button.
260      */
261     protected Button createDeleteButton() {
262         Button deleteButton = new Button();
263         deleteButton.setHtmlContentAllowed(true);
264         deleteButton.setCaption("<span class=\"" + "icon-trash" + "\"></span>");
265         deleteButton.addStyleName("inline");
266         deleteButton.setDescription(i18n.translate(deleteCaption));
267 
268         deleteButton.addClickListener(new Button.ClickListener() {
269             private static final long serialVersionUID = 1L;
270 
271             @Override
272             public void buttonClick(ClickEvent event) {
273                 resetDataSource();
274                 updateDisplay();
275             }
276         });
277         return deleteButton;
278     }
279 
280     /**
281      * Initialize a Component displaying some File Informations.
282      * Override getFileDetail...() in order to display custom info's you may want to display.
283      *
284      * @return A file Info Component. Generally a {@link FormLayout}.
285      */
286     private Component createFileInfoComponent() {
287         FormLayout fileInfo = new FormLayout();
288         fileInfo.setSizeUndefined();
289         fileInfo.addStyleName("file-details");
290         fileInfo.addComponent(getFileDetailHeader());
291         fileInfo.addComponent(getFileDetailFileName());
292         fileInfo.addComponent(getFileDetailSize());
293         fileInfo.addComponent(getFileDetailFileFormat());
294         return fileInfo;
295     }
296 
297     /**
298      * Add Title.
299      */
300     protected Component getFileDetailHeader() {
301         Label label = new Label();
302         label.setValue(getCaption(fileDetailHeaderCaption, null));
303         return label;
304     }
305 
306     /**
307      * Add File Name.<br>
308      * If editFileName is true, display an Input Text Field. <br>
309      * Else display a simple label.
310      */
311     protected Component getFileDetailFileName() {
312         // Build the file name without the extension
313         final boolean hasExtension = StringUtils.isNotBlank(getValue().getExtension());
314         final String extension = hasExtension ? "." + getValue().getExtension() : "";
315         String fileName = StringUtils.removeEnd(getValue().getFileName(), extension);
316 
317         if (this.editFileName && !isReadOnly()) {
318             TextField textField = new TextField(i18n.translate(fileDetailNameCaption), fileName);
319             textField.setNullRepresentation("");
320             textField.setCaption(i18n.translate(fileDetailNameCaption));
321             textField.addValueChangeListener(new Property.ValueChangeListener() {
322                 @Override
323                 public void valueChange(Property.ValueChangeEvent event) {
324                     Object newFileNameObject = event.getProperty().getValue();
325                     String newFileName = (newFileNameObject != null && StringUtils.isNotBlank(newFileNameObject.toString())) ? newFileNameObject.toString() : UploadReceiver.INVALID_FILE_NAME;
326                     getValue().setFileName(newFileName + extension);
327                     getPropertyDataSource().setValue(getValue());
328                 }
329             });
330             return textField;
331         } else {
332             Label label = new Label();
333             label.setCaption(i18n.translate(fileDetailNameCaption));
334             label.setValue(fileName);
335             return label;
336         }
337     }
338 
339     /**
340      * Add File Info.
341      */
342     protected Component getFileDetailSize() {
343         Label label = new Label();
344         label.setCaption(i18n.translate(fileDetailSizeCaption));
345         label.setValue(FileUtils.byteCountToDisplaySize(getValue().getFileSize()));
346         return label;
347     }
348 
349     /**
350      * Add File Format.<br>
351      * If editFileFormat is true, display an Input Text Field. <br>
352      * Else display a simple label.
353      */
354     protected Component getFileDetailFileFormat() {
355         if (this.editFileFormat && !isReadOnly()) {
356             TextField textField = new TextField(i18n.translate(fileDetailFormatCaption), getValue().getExtension());
357             textField.setNullRepresentation("");
358             textField.setCaption(i18n.translate(fileDetailFormatCaption));
359             return textField;
360         } else {
361             Label label = new Label();
362             label.setValue(getValue().getExtension());
363             label.setCaption(i18n.translate(fileDetailFormatCaption));
364             return label;
365         }
366     }
367 
368     /**
369      * @return Thumbnail Component.
370      */
371     protected Component createThumbnailComponent() {
372         Label thumbnail = new Label();
373         thumbnail.setSizeUndefined();
374         thumbnail.addStyleName("preview-image");
375         thumbnail.addStyleName("file-preview");
376         thumbnail.addStyleName(createIconStyleName());
377         return thumbnail;
378     }
379 
380     /**
381      * Create the Icon related to a File. <br>
382      * <b>Override this method in order to change the Displayed Icon .</b>
383      *
384      * @return
385      */
386     protected String createIconStyleName() {
387         return "icon-" + imageProvider.resolveIconClassName(getValue().getMimeType());
388     }
389 
390     @Override
391     protected Component initContent() {
392         return getRootLayout();
393     }
394 
395     /**
396      * Configure Field based on the definition.
397      */
398     protected void populateFromDefinition(BasicUploadFieldDefinition definition) {
399         this.setMaxUploadSize(definition.getMaxUploadSize());
400         this.setAllowedMimeTypePattern(definition.getAllowedMimeTypePattern());
401 
402         this.setSelectNewCaption(definition.getSelectNewCaption());
403         this.setSelectAnotherCaption(definition.getSelectAnotherCaption());
404         this.setDropZoneCaption(definition.getDropZoneCaption());
405         this.setInProgressCaption(definition.getInProgressCaption());
406         this.setInProgressRatioCaption(definition.getInProgressRatioCaption());
407         this.setFileDetailHeaderCaption(definition.getFileDetailHeaderCaption());
408         this.setFileDetailNameCaption(definition.getFileDetailNameCaption());
409         this.setFileDetailSizeCaption(definition.getFileDetailSizeCaption());
410         this.setFileDetailFormatCaption(definition.getFileDetailFormatCaption());
411         this.setFileDetailSourceCaption(definition.getFileDetailSourceCaption());
412         this.setSuccessNoteCaption(definition.getSuccessNoteCaption());
413         this.setWarningNoteCaption(definition.getWarningNoteCaption());
414         this.setErrorNoteCaption(definition.getErrorNoteCaption());
415         this.setDeteteCaption(definition.getDeleteCaption());
416         this.setEditFileFormat(definition.isEditFileFormat());
417         this.setEditFileName(definition.isEditFileName());
418         this.setUserInterruption(definition.getUserInterruption());
419         this.setTypeInterruption(definition.getTypeInterruption());
420         this.setSizeInterruption(definition.getSizeInterruption());
421     }
422 
423     /**
424      * Caption section.
425      */
426     protected String captionExtension;
427 
428     protected void setCaptionExtension(String mimeType) {
429         captionExtension = "";
430     }
431 
432     protected String getCaption(String caption, String[] args) {
433         if (StringUtils.isEmpty(caption)) {
434             return "";
435         }
436         if (StringUtils.isNotBlank(captionExtension)) {
437             String mediaName = i18n.translate(PREFIX_MEDIA_KEY + '.' + captionExtension);
438             String[] paras;
439             if (args != null && args.length > 0) {
440                 paras = new String[args.length + 1];
441                 paras[0] = mediaName;
442                 System.arraycopy(args, 0, paras, 1, args.length);
443             } else {
444                 paras = new String[] { mediaName };
445             }
446             return i18n.translate(caption + '.' + MEDIA, paras);
447         }
448         if (args != null && args.length > 0) {
449             return i18n.translate(caption, args);
450         } else {
451             return i18n.translate(caption);
452         }
453     }
454 
455     protected String selectNewCaption;
456     protected String selectAnotherCaption;
457     protected String deleteCaption;
458     protected String dropZoneCaption;
459     protected String inProgressCaption;
460     protected String inProgressRatioCaption;
461     protected String fileDetailHeaderCaption;
462     protected String fileDetailNameCaption;
463     protected String fileDetailSizeCaption;
464     protected String fileDetailFormatCaption;
465     protected String fileDetailSourceCaption;
466     protected String successNoteCaption;
467     protected String warningNoteCaption;
468     protected String errorNoteCaption;
469     private String sizeInterruption;
470     private String typeInterruption;
471     private String userInterruption;
472 
473     public void setSelectNewCaption(String selectNewCaption) {
474         this.selectNewCaption = selectNewCaption;
475     }
476 
477     public void setSelectAnotherCaption(String selectAnotherCaption) {
478         this.selectAnotherCaption = selectAnotherCaption;
479     }
480 
481     public void setDropZoneCaption(String dropZoneCaption) {
482         this.dropZoneCaption = dropZoneCaption;
483     }
484 
485     public void setInProgressCaption(String inProgressCaption) {
486         this.inProgressCaption = inProgressCaption;
487     }
488 
489     public void setInProgressRatioCaption(String inProgressRatioCaption) {
490         this.inProgressRatioCaption = inProgressRatioCaption;
491     }
492 
493     public void setFileDetailHeaderCaption(String fileDetailHeaderCaption) {
494         this.fileDetailHeaderCaption = fileDetailHeaderCaption;
495     }
496 
497     public void setFileDetailNameCaption(String fileDetailNameCaption) {
498         this.fileDetailNameCaption = fileDetailNameCaption;
499     }
500 
501     public void setFileDetailSizeCaption(String fileDetailSizeCaption) {
502         this.fileDetailSizeCaption = fileDetailSizeCaption;
503     }
504 
505     public void setFileDetailFormatCaption(String fileDetailFormatCaption) {
506         this.fileDetailFormatCaption = fileDetailFormatCaption;
507     }
508 
509     public void setFileDetailSourceCaption(String fileDetailSourceCaption) {
510         this.fileDetailSourceCaption = fileDetailSourceCaption;
511     }
512 
513     public void setSuccessNoteCaption(String successNoteCaption) {
514         this.successNoteCaption = successNoteCaption;
515     }
516 
517     public void setWarningNoteCaption(String warningNoteCaption) {
518         this.warningNoteCaption = warningNoteCaption;
519     }
520 
521     public void setErrorNoteCaption(String errorNoteCaption) {
522         this.errorNoteCaption = errorNoteCaption;
523     }
524 
525     public void setDeteteCaption(String deleteCaption) {
526         this.deleteCaption = deleteCaption;
527     }
528 
529     public void setSizeInterruption(String sizeInterruption) {
530         this.sizeInterruption = sizeInterruption;
531     }
532 
533     public void setTypeInterruption(String typeInterruption) {
534         this.typeInterruption = typeInterruption;
535     }
536 
537     public void setUserInterruption(String userInterruption) {
538         this.userInterruption = userInterruption;
539     }
540 
541     @Override
542     protected void displayUploadInterruptNote(InterruptionReason reason) {
543         String caption = "";
544         if (reason.equals(InterruptionReason.USER)) {
545             caption = userInterruption;
546         } else if (reason.equals(InterruptionReason.FILE_SIZE)) {
547             caption = sizeInterruption;
548         } else {
549             caption = typeInterruption;
550         }
551         uiContext.openNotification(MessageStyleTypeEnum.WARNING, true, getCaption(warningNoteCaption, new String[] { i18n.translate(caption) }));
552     }
553 
554     @Override
555     protected void displayUploadFinishedNote(String fileName) {
556         uiContext.openNotification(MessageStyleTypeEnum.INFO, true, getCaption(successNoteCaption, new String[] { fileName }));
557     }
558 
559     @Override
560     protected void displayUploadFailedNote(String fileName) {
561         uiContext.openAlert(MessageStyleTypeEnum.ERROR, "ERROR", getCaption(errorNoteCaption, new String[] { fileName }), i18n.translate("button.ok"), null);
562     }
563 
564     public void setEditFileName(boolean editFileName) {
565         this.editFileName = editFileName;
566     }
567 
568     public void setEditFileFormat(boolean editFileFormat) {
569         this.editFileFormat = editFileFormat;
570     }
571 
572     /**
573      * For test cases.
574      */
575     public CssLayout getCssLayout() {
576         return this.layout;
577     }
578 
579     @Override
580     public boolean isEmpty() {
581         return getValue().isEmpty();
582     }
583 
584     @Override
585     public void setReadOnly(boolean readOnly) {
586         super.setReadOnly(readOnly);
587         if (readOnly) {
588             // Remove drop zone
589             if (getDropZone() != null) {
590                 getDropZone().setDropHandler(null);
591             }
592             if (getUpload() != null) {
593                 getUpload().removeStartedListener(this);
594                 getUpload().removeFinishedListener(this);
595                 getUpload().removeProgressListener(this);
596             }
597             if (getValue().isEmpty()) {
598                 buildEmptyLayout();
599             }
600         }
601 
602     }
603 }