View Javadoc
1   /**
2    * This file Copyright (c) 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.field;
35  
36  import info.magnolia.i18nsystem.SimpleTranslator;
37  import info.magnolia.icons.MagnoliaIcons;
38  import info.magnolia.ui.framework.util.TempFilesManager;
39  import info.magnolia.ui.theme.ResurfaceTheme;
40  
41  import java.io.File;
42  import java.io.FileInputStream;
43  import java.io.FileOutputStream;
44  import java.io.IOException;
45  import java.io.InputStream;
46  import java.io.OutputStream;
47  import java.util.Collection;
48  import java.util.Optional;
49  import java.util.stream.Collectors;
50  import java.util.stream.Stream;
51  
52  import org.apache.commons.io.FileUtils;
53  import org.apache.commons.lang3.StringUtils;
54  import org.apache.tika.Tika;
55  import org.devlib.schmidt.imageinfo.ImageInfo;
56  
57  import com.google.common.net.MediaType;
58  import com.machinezoo.noexception.Exceptions;
59  import com.vaadin.server.FileResource;
60  import com.vaadin.server.Resource;
61  import com.vaadin.server.StreamVariable;
62  import com.vaadin.ui.Button;
63  import com.vaadin.ui.Component;
64  import com.vaadin.ui.CssLayout;
65  import com.vaadin.ui.CustomField;
66  import com.vaadin.ui.HorizontalLayout;
67  import com.vaadin.ui.Html5File;
68  import com.vaadin.ui.Image;
69  import com.vaadin.ui.Label;
70  import com.vaadin.ui.Notification;
71  import com.vaadin.ui.Upload;
72  import com.vaadin.ui.VerticalLayout;
73  import com.vaadin.ui.dnd.FileDropTarget;
74  
75  import lombok.Data;
76  
77  /**
78   * Base Vaadin 8 implementation of the Magnolia Upload field.
79   *
80   */
81  public class UploadField extends CustomField<File> {
82  
83      private static final Tika TIKA = new Tika();
84  
85      private static final Resource DEFAULT_REVIEW_IMG = MagnoliaIcons.FILE;
86      private static final Collection<String> IMG_MIME_TYPES = Stream.of(MediaType.JPEG,
87              MediaType.PNG, MediaType.BMP, MediaType.GIF).map(MediaType::toString).collect(Collectors.toList());
88  
89      private final VerticalLayout rootLayout;
90      private final UploadFieldDetailComponent detailComponent;
91  
92      private FileInfo fileInfo = new FileInfo();
93      private File currentTempFile;
94  
95      private Upload uploadBtn;
96  
97      private Button removeUploadBtn;
98  
99      private Label dragLabel;
100     private Image thumbnail;
101     private CssLayout uploadPanel;
102 
103     private final TempFilesManager tempFilesManager;
104     private final UploadFieldDefinition definition;
105     private final SimpleTranslator translator;
106 
107     public UploadField(TempFilesManager tempFilesManager,
108                        UploadFieldDefinition definition,
109                        SimpleTranslator translator) {
110         this.tempFilesManager = tempFilesManager;
111         this.definition = definition;
112         this.translator = translator;
113 
114         this.rootLayout = new VerticalLayout();
115         this.rootLayout.setSizeFull();
116         this.rootLayout.setMargin(false);
117         this.rootLayout.setSpacing(true);
118 
119         this.detailComponent = new UploadFieldDetailComponent(translator);
120     }
121 
122     @Override
123     public File getValue() {
124         return currentTempFile;
125     }
126 
127     public Optional<FileInfo> getFileInfo() {
128         return getValue() == null ? Optional.empty() : Optional.of(this.fileInfo);
129     }
130 
131     @Override
132     protected void doSetValue(File value) {
133         try (InputStream inputStream = new FileInputStream(value)) {
134             currentTempFile = value;
135             fileInfo.mimeType = TIKA.detect(inputStream);
136             fileInfo.name = tempFilesManager.toStandardFileName(value.getName());
137             fileInfo.path = value.getPath();
138             fileInfo.sizeInBytes = FileUtils.sizeOf(value);
139             tempFilesManager.register(value);
140         } catch (IOException e) {
141             throw new RuntimeException(e);
142         }
143     }
144 
145     @Override
146     protected Component initContent() {
147 
148         HorizontalLayout content = new HorizontalLayout();
149         content.setSizeFull();
150         content.setSpacing(false);
151 
152         uploadPanel = new CssLayout();
153         uploadPanel.setSizeFull();
154         uploadPanel.setStyleName("upload-file-panel");
155         uploadPanel.setHeightUndefined();
156 
157         CssLayout controlButtonPanel = new CssLayout();
158         controlButtonPanel.addStyleName("control-button-panel");
159         controlButtonPanel.setWidth(30, Unit.PIXELS);
160 
161         removeUploadBtn = createControlPanelButton(MagnoliaIcons.TRASH);
162         removeUploadBtn.addClickListener(event -> {
163             FileUtils.deleteQuietly(new File(fileInfo.path));
164             this.currentTempFile = null;
165             this.fileInfo.clear();
166             updateControlVisibilities();
167         });
168 
169         removeUploadBtn.setDescription(translator.translate("fields.uploadField.upload.removeFile"));
170         controlButtonPanel.addComponent(removeUploadBtn);
171         additionalUploadButtons(controlButtonPanel);
172 
173         thumbnail = new Image(StringUtils.EMPTY, DEFAULT_REVIEW_IMG);
174         thumbnail.addStyleName("file-preview-thumbnail");
175 
176         dragLabel = createDragFileUploadLabel();
177 
178         Upload.Receiver receiver = (Upload.Receiver) (filename, mimeType) -> handleUpload(filename);
179         uploadBtn = new Upload(StringUtils.EMPTY, receiver);
180         uploadBtn.addSucceededListener(event -> handleUploadSuccess(event.getMIMEType(), event.getFilename(), event.getLength()));
181 
182         uploadBtn.addStyleName("upload-button");
183 
184         uploadPanel.addComponents(controlButtonPanel, dragLabel, thumbnail, uploadBtn);
185         if (definition.isReadOnly()) {
186             uploadPanel.setEnabled(false);
187         }
188 
189         new FileDropTarget<>(uploadPanel, event -> event.getFiles().forEach(file -> file.setStreamVariable(new UploadStream(file))));
190 
191         content.addComponents(uploadPanel);
192         content.setExpandRatio(uploadPanel, 1);
193         rootLayout.addComponents(content);
194 
195         updateControlVisibilities();
196 
197         return rootLayout;
198     }
199 
200     private void handleUploadSuccess(String mimeType, String fileName, long fileSize) {
201         if (mimeType.matches(definition.getAllowedMimeTypePattern())) {
202             fileInfo.name = fileName;
203             fileInfo.sizeInBytes = fileSize;
204             fileInfo.mimeType = mimeType;
205 
206             updateControlVisibilities();
207             fireEvent(createValueChange(null, false));
208             Notification.show(translator.translate("fields.uploadField.upload.success", fileName));
209         } else {
210             Notification.show(translator.translate("fields.uploadField.upload.abort"), Notification.Type.WARNING_MESSAGE);
211         }
212     }
213 
214     protected TempFilesManager getTempFilesManager() {
215         return tempFilesManager;
216     }
217 
218     protected void additionalUploadButtons(CssLayout controlButtonPanel) {
219     }
220 
221     protected Button createControlPanelButton(Resource icon) {
222         Button button = new Button(icon);
223         button.addStyleNames(ResurfaceTheme.BUTTON_ICON, "control-button");
224         return button;
225     }
226 
227     protected void updateControlVisibilities() {
228         boolean hasValue = getValue() != null;
229 
230         removeUploadBtn.setVisible(hasValue);
231 
232         updatePreviewThumbnail();
233         dragLabel.setVisible(!hasValue);
234         thumbnail.setVisible(hasValue);
235 
236         if (hasValue) {
237             detailComponent.updateFileDetail(this.fileInfo);
238             rootLayout.addComponent(detailComponent);
239         } else {
240             rootLayout.removeComponent(detailComponent);
241         }
242 
243         String uploadButtonCaption = StringUtils.isEmpty(fileInfo.name)
244                 ? translator.translate("fields.uploadField.upload.uploadFile")
245                 : translator.translate("fields.uploadField.upload.uploadAnotherFile");
246 
247         uploadBtn.setButtonCaption(uploadButtonCaption);
248     }
249 
250     private Label createDragFileUploadLabel() {
251         Label dragLabel = new Label(translator.translate("fields.uploadField.upload.dragNDrop"));
252         dragLabel.setStyleName("drag-to-upload-label");
253         return dragLabel;
254     }
255 
256     private OutputStream handleUpload(String filename) {
257         try {
258             currentTempFile = tempFilesManager.createTempFile(filename);
259             final FileOutputStream fos = FileUtils.openOutputStream(currentTempFile);
260             fileInfo.path = currentTempFile.getPath();
261             return fos;
262         } catch (IOException e) {
263             throw new RuntimeException(e);
264         }
265     }
266 
267     protected void updatePreviewThumbnail() {
268         if (getValue() != null) {
269             if (IMG_MIME_TYPES.contains(this.fileInfo.mimeType)) {
270                 thumbnail.setIcon(null);
271                 thumbnail.setSource(new FileResource(currentTempFile));
272                 uploadPanel.addStyleName("upload-file-panel-large");
273             } else {
274                 thumbnail.setIcon(MagnoliaIcons.FILE);
275                 thumbnail.setSource(null);
276                 uploadPanel.removeStyleName("upload-file-panel-large");
277             }
278         } else {
279             thumbnail.setIcon(null);
280             thumbnail.setSource(null);
281             uploadPanel.setStyleName("upload-file-panel");
282         }
283     }
284 
285     @Data
286     public class FileInfo {
287         private String name;
288         private String path;
289         private String mimeType;
290         private long sizeInBytes = 0;
291 
292         public FileInfo() {
293             clear();
294         }
295 
296         public void clear() {
297             name = StringUtils.EMPTY;
298             path = StringUtils.EMPTY;
299             mimeType = StringUtils.EMPTY;
300             sizeInBytes = 0;
301         }
302 
303         public Optional<ImageInfo> getImageInfo() {
304             if (IMG_MIME_TYPES.contains(this.mimeType)) {
305                 ImageInfo imageInfo = new ImageInfo();
306                 imageInfo.setInput(Exceptions.wrap().get(() -> new FileInputStream(getValue())));
307                 return Optional.of(imageInfo).filter(ImageInfo::check);
308             }
309             return Optional.empty();
310         }
311     }
312 
313     private class UploadStream implements StreamVariable {
314         private Html5File html5File;
315 
316         private UploadStream(Html5File html5File) {
317             this.html5File = html5File;
318         }
319 
320         @Override
321         public OutputStream getOutputStream() {
322             return handleUpload(html5File.getFileName());
323         }
324 
325         @Override
326         public boolean listenProgress() {
327             return true;
328         }
329 
330         @Override
331         public boolean isInterrupted() {
332             return false;
333         }
334 
335         @Override
336         public void onProgress(StreamingProgressEvent event) {
337             Notification.show(translator.translate("fields.uploadField.upload.progress", event.getBytesReceived()));
338         }
339 
340         @Override
341         public void streamingStarted(StreamingStartEvent event) {
342             Notification.show(translator.translate("fields.uploadField.upload.started", event.getFileName()));
343         }
344 
345         @Override
346         public void streamingFailed(StreamingErrorEvent event) {
347             Notification.show(translator.translate("fields.uploadField.upload.failure", event.getFileName()),
348                     Notification.Type.ERROR_MESSAGE);
349         }
350 
351         @Override
352         public void streamingFinished(StreamingEndEvent event) {
353             handleUploadSuccess(event.getMimeType(), event.getFileName(), event.getBytesReceived());
354         }
355     }
356 }