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.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
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 }