1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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.objectfactory.Components;
39 import info.magnolia.ui.api.message.Message;
40 import info.magnolia.ui.api.message.MessageType;
41 import info.magnolia.ui.framework.message.MessagesManager;
42 import info.magnolia.ui.framework.util.TempFilesManager;
43 import info.magnolia.ui.theme.ResurfaceTheme;
44 import info.magnolia.ui.vaadin.server.DownloadStreamResource;
45
46 import java.io.ByteArrayInputStream;
47 import java.io.File;
48 import java.io.FileInputStream;
49 import java.io.FileOutputStream;
50 import java.io.IOException;
51 import java.io.OutputStream;
52 import java.util.Collection;
53 import java.util.Optional;
54 import java.util.stream.Collectors;
55 import java.util.stream.Stream;
56
57 import javax.inject.Inject;
58
59 import org.apache.commons.io.FileUtils;
60 import org.apache.commons.lang3.StringUtils;
61 import org.apache.tika.Tika;
62 import org.devlib.schmidt.imageinfo.ImageInfo;
63 import org.slf4j.Logger;
64 import org.slf4j.LoggerFactory;
65
66 import com.google.common.net.MediaType;
67 import com.machinezoo.noexception.Exceptions;
68 import com.vaadin.server.FileResource;
69 import com.vaadin.server.Page;
70 import com.vaadin.server.Resource;
71 import com.vaadin.server.StreamResource;
72 import com.vaadin.server.StreamVariable;
73 import com.vaadin.ui.Button;
74 import com.vaadin.ui.Component;
75 import com.vaadin.ui.CssLayout;
76 import com.vaadin.ui.CustomField;
77 import com.vaadin.ui.HorizontalLayout;
78 import com.vaadin.ui.Html5File;
79 import com.vaadin.ui.Image;
80 import com.vaadin.ui.Label;
81 import com.vaadin.ui.Notification;
82 import com.vaadin.ui.Upload;
83 import com.vaadin.ui.VerticalLayout;
84 import com.vaadin.ui.dnd.FileDropTarget;
85
86 import lombok.Data;
87
88
89
90
91
92 public class UploadField extends CustomField<File> {
93
94 private static final Tika TIKA = new Tika();
95
96 private static final Resource DEFAULT_REVIEW_IMG = MagnoliaIcons.FILE;
97 private static final Collection<String> IMG_MIME_TYPES = Stream.of(MediaType.JPEG,
98 MediaType.PNG, MediaType.BMP, MediaType.GIF).map(MediaType::toString).collect(Collectors.toList());
99 private static final Logger log = LoggerFactory.getLogger(UploadField.class);
100
101 private final VerticalLayout rootLayout;
102 private final UploadFieldDetailComponent detailComponent;
103
104 private final FileInfo fileInfo = new FileInfo();
105 private File currentTempFile;
106
107 private Upload uploadBtn;
108
109 private Button removeUploadBtn;
110 private Button downloadBtn;
111
112 private Label dragLabel;
113 private Image thumbnail;
114 private CssLayout uploadPanel;
115
116 private final TempFilesManager tempFilesManager;
117 private final UploadFieldDefinition definition;
118 private final SimpleTranslator translator;
119 private final MessagesManager messagesManager;
120
121
122
123
124 @Deprecated
125 public UploadField(TempFilesManager tempFilesManager,
126 UploadFieldDefinition definition,
127 SimpleTranslator translator) {
128 this(tempFilesManager, definition, translator, Components.getComponent(MessagesManager.class));
129 }
130
131 @Inject
132 public UploadField(TempFilesManager tempFilesManager,
133 UploadFieldDefinition definition,
134 SimpleTranslator translator, MessagesManager messagesManager) {
135 this.tempFilesManager = tempFilesManager;
136 this.definition = definition;
137 this.translator = translator;
138 this.messagesManager = messagesManager;
139
140 this.rootLayout = new VerticalLayout();
141 this.rootLayout.setSizeFull();
142 this.rootLayout.setMargin(false);
143 this.rootLayout.setSpacing(true);
144
145 this.detailComponent = new UploadFieldDetailComponent(translator);
146 }
147
148 @Override
149 public File getValue() {
150 return currentTempFile;
151 }
152
153 public Optional<FileInfo> getFileInfo() {
154 return getValue() == null ? Optional.empty() : Optional.of(this.fileInfo);
155 }
156
157 @Override
158 protected void doSetValue(File value) {
159 currentTempFile = value;
160 fileInfo.mimeType = detectMediaType(value);
161 fileInfo.name = tempFilesManager.toStandardFileName(value.getName());
162 fileInfo.path = value.getPath();
163 fileInfo.sizeInBytes = FileUtils.sizeOf(value);
164 tempFilesManager.register(value);
165 }
166
167 @Override
168 protected Component initContent() {
169
170 HorizontalLayout content = new HorizontalLayout();
171 content.setSizeFull();
172 content.setSpacing(false);
173
174 uploadPanel = new CssLayout();
175 uploadPanel.setSizeFull();
176 uploadPanel.setStyleName("upload-file-panel");
177 uploadPanel.setHeightUndefined();
178
179 CssLayout controlButtonPanel = new CssLayout();
180 controlButtonPanel.addStyleName("control-button-panel");
181 controlButtonPanel.setWidth(30, Unit.PIXELS);
182
183 removeUploadBtn = createControlPanelButton(MagnoliaIcons.TRASH);
184 removeUploadBtn.addClickListener(event -> {
185 FileUtils.deleteQuietly(new File(fileInfo.path));
186 this.currentTempFile = null;
187 this.fileInfo.clear();
188 updateControlVisibilities();
189 });
190
191 removeUploadBtn.setDescription(translator.translate("fields.uploadField.upload.removeFile"));
192 controlButtonPanel.addComponent(removeUploadBtn);
193 additionalUploadButtons(controlButtonPanel);
194
195 downloadBtn = createControlPanelButton(MagnoliaIcons.DOWNLOAD);
196 downloadBtn.addClickListener(event -> openDownload());
197 downloadBtn.setDescription(translator.translate("fields.uploadField.upload.download"));
198 controlButtonPanel.addComponent(downloadBtn);
199
200 thumbnail = new Image(StringUtils.EMPTY, DEFAULT_REVIEW_IMG);
201 thumbnail.addStyleName("file-preview-thumbnail");
202
203 dragLabel = createDragFileUploadLabel();
204
205 Upload.Receiver receiver = (Upload.Receiver) (filename, mimeType) -> handleUpload(filename);
206 uploadBtn = new Upload(StringUtils.EMPTY, receiver);
207 uploadBtn.addSucceededListener(event -> handleUploadSuccess(event.getMIMEType(), event.getFilename(), event.getLength()));
208
209 uploadBtn.addStyleName("upload-button");
210
211 uploadPanel.addComponents(controlButtonPanel, dragLabel, thumbnail, uploadBtn);
212 if (definition.isReadOnly()) {
213 uploadPanel.setEnabled(false);
214 }
215
216 new FileDropTarget<>(uploadPanel, event -> event.getFiles().forEach(file -> file.setStreamVariable(new UploadStream(file))));
217
218 content.addComponents(uploadPanel);
219 content.setExpandRatio(uploadPanel, 1);
220 rootLayout.addComponents(content);
221
222 updateControlVisibilities();
223
224 return rootLayout;
225 }
226
227 private void handleUploadSuccess(String mimeType, String fileName, long fileSize) {
228 if (mimeType.matches(definition.getAllowedMimeTypePattern())) {
229 fileInfo.name = fileName;
230 fileInfo.sizeInBytes = fileSize;
231 fileInfo.mimeType = mimeType;
232
233 updateControlVisibilities();
234 fireEvent(createValueChange(null, false));
235 Notification.show(translator.translate("fields.uploadField.upload.success", fileName));
236 } else {
237 Notification.show(translator.translate("fields.uploadField.upload.abort"), Notification.Type.WARNING_MESSAGE);
238 }
239 }
240
241 protected TempFilesManager getTempFilesManager() {
242 return tempFilesManager;
243 }
244
245 protected void additionalUploadButtons(CssLayout controlButtonPanel) {
246 }
247
248 protected Button createControlPanelButton(Resource icon) {
249 Button button = new Button(icon);
250 button.addStyleNames(ResurfaceTheme.BUTTON_ICON, "control-button");
251 return button;
252 }
253
254 protected void updateControlVisibilities() {
255 boolean hasValue = getValue() != null;
256
257 removeUploadBtn.setVisible(hasValue);
258 downloadBtn.setVisible(hasValue);
259
260 updatePreviewThumbnail();
261 dragLabel.setVisible(!hasValue);
262 thumbnail.setVisible(hasValue);
263
264 if (hasValue) {
265 detailComponent.updateFileDetail(this.fileInfo);
266 rootLayout.addComponent(detailComponent);
267 } else {
268 rootLayout.removeComponent(detailComponent);
269 }
270
271 String uploadButtonCaption = StringUtils.isEmpty(fileInfo.name)
272 ? translator.translate("fields.uploadField.upload.uploadFile")
273 : translator.translate("fields.uploadField.upload.uploadAnotherFile");
274
275 uploadBtn.setButtonCaption(uploadButtonCaption);
276 }
277
278 private Label createDragFileUploadLabel() {
279 Label dragLabel = new Label(translator.translate("fields.uploadField.upload.dragNDrop"));
280 dragLabel.setStyleName("drag-to-upload-label");
281 return dragLabel;
282 }
283
284
285 void openDownload() {
286 try (ByteArrayInputStream inputStream = new ByteArrayInputStream(FileUtils.readFileToByteArray(getValue()))) {
287 StreamResource.StreamSource streamSource = () -> inputStream;
288 StreamResource streamResource = new StreamResource(streamSource, "");
289 getFileInfo().map(FileInfo::getMimeType).ifPresent(streamResource::setMIMEType);
290 String fileName = getFileInfo()
291 .map(FileInfo::getName)
292 .orElse("default");
293 DownloadStreamResourceesource.html#DownloadStreamResource">DownloadStreamResource resource = new DownloadStreamResource(streamSource, fileName);
294
295
296
297 resource.setCacheTime(-1);
298 resource.getStream().setParameter("Content-Disposition", String.format("attachment; filename=\"%s\"", fileName));
299 getFileInfo().map(FileInfo::getMimeType).ifPresent(resource::setMIMEType);
300 Page.getCurrent().open(resource, null, false);
301 } catch (IOException e) {
302 Messageessage.html#Message">Message message = new Message(MessageType.ERROR,
303 translator.translate("fields.uploadField.download.error.subject"),
304 translator.translate("fields.uploadField.download.error.message"));
305
306 messagesManager.sendLocalMessage(message);
307 log.warn(e.getMessage());
308 }
309 }
310
311 private OutputStream handleUpload(String filename) {
312 try {
313 currentTempFile = tempFilesManager.createTempFile(filename);
314 final FileOutputStream fos = FileUtils.openOutputStream(currentTempFile);
315 fileInfo.path = currentTempFile.getPath();
316 return fos;
317 } catch (IOException e) {
318 throw new RuntimeException(e);
319 }
320 }
321
322 private String detectMediaType(File value) {
323
324 try {
325 return TIKA.detect(value);
326 } catch (IOException e) {
327 throw new RuntimeException(e);
328 }
329 }
330
331 protected void updatePreviewThumbnail() {
332 if (getValue() != null) {
333 if (IMG_MIME_TYPES.contains(this.fileInfo.mimeType)) {
334 thumbnail.setIcon(null);
335 thumbnail.setSource(new FileResource(currentTempFile));
336 uploadPanel.addStyleName("upload-file-panel-large");
337 } else {
338 thumbnail.setIcon(MagnoliaIcons.FILE);
339 thumbnail.setSource(null);
340 uploadPanel.removeStyleName("upload-file-panel-large");
341 }
342 } else {
343 thumbnail.setIcon(null);
344 thumbnail.setSource(null);
345 uploadPanel.setStyleName("upload-file-panel");
346 }
347 }
348
349 @Data
350 public class FileInfo {
351 private String name;
352 private String path;
353 private String mimeType;
354 private long sizeInBytes = 0;
355
356 public FileInfo() {
357 clear();
358 }
359
360 public void clear() {
361 name = StringUtils.EMPTY;
362 path = StringUtils.EMPTY;
363 mimeType = StringUtils.EMPTY;
364 sizeInBytes = 0;
365 }
366
367 public Optional<ImageInfo> getImageInfo() {
368 if (IMG_MIME_TYPES.contains(this.mimeType)) {
369 ImageInfo imageInfo = new ImageInfo();
370 imageInfo.setInput(Exceptions.wrap().get(() -> new FileInputStream(getValue())));
371 return Optional.of(imageInfo).filter(ImageInfo::check);
372 }
373 return Optional.empty();
374 }
375 }
376
377 private class UploadStream implements StreamVariable {
378 private final Html5File html5File;
379
380 private UploadStream(Html5File html5File) {
381 this.html5File = html5File;
382 }
383
384 @Override
385 public OutputStream getOutputStream() {
386 return handleUpload(html5File.getFileName());
387 }
388
389 @Override
390 public boolean listenProgress() {
391 return true;
392 }
393
394 @Override
395 public boolean isInterrupted() {
396 return false;
397 }
398
399 @Override
400 public void onProgress(StreamingProgressEvent event) {
401 Notification.show(translator.translate("fields.uploadField.upload.progress", event.getBytesReceived()));
402 }
403
404 @Override
405 public void streamingStarted(StreamingStartEvent event) {
406 Notification.show(translator.translate("fields.uploadField.upload.started", event.getFileName()));
407 }
408
409 @Override
410 public void streamingFailed(StreamingErrorEvent event) {
411 Notification.show(translator.translate("fields.uploadField.upload.failure", event.getFileName()),
412 Notification.Type.ERROR_MESSAGE);
413 }
414
415 @Override
416 public void streamingFinished(StreamingEndEvent event) {
417 handleUploadSuccess(event.getMimeType(), event.getFileName(), event.getBytesReceived());
418 }
419 }
420 }