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