/* * Copyright 2000-2016 Vaadin Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.vaadin.ui.dnd; import java.io.OutputStream; import java.util.HashMap; import java.util.Map; import com.vaadin.server.ServletPortletHelper; import com.vaadin.server.StreamVariable; import com.vaadin.shared.ApplicationConstants; import com.vaadin.shared.ui.dnd.FileDropTargetClientRpc; import com.vaadin.shared.ui.dnd.FileDropTargetRpc; import com.vaadin.shared.ui.dnd.FileDropTargetState; import com.vaadin.shared.ui.dnd.FileParameters; import com.vaadin.ui.AbstractComponent; import com.vaadin.ui.ConnectorTracker; import com.vaadin.ui.Html5File; import com.vaadin.ui.UI; import com.vaadin.ui.dnd.event.FileDropEvent; /** * Extension to add drop target functionality to a widget for accepting and * uploading files. * <p> * Dropped files are handled in the {@link FileDropHandler} given in the * constructor. The file details are first sent to the handler, which can then * decide which files to upload to server by setting a {@link StreamVariable} * with {@link Html5File#setStreamVariable(StreamVariable)}. * * @param <T> * Type of the component to be extended. * @author Vaadin Ltd * @since 8.1 */ public class FileDropTarget<T extends AbstractComponent> extends DropTargetExtension<T> { /** * Handles the file drop event. */ private final FileDropHandler<T> fileDropHandler; /** * Extends {@code target} component and makes it a file drop target. A file * drop handler needs to be added to handle the file drop event. * * @param target * Component to be extended. * @param fileDropHandler * File drop handler that handles the file drop event. * @see FileDropEvent */ public FileDropTarget(T target, FileDropHandler<T> fileDropHandler) { super(target); this.fileDropHandler = fileDropHandler; registerRpc(new FileDropTargetRpc() { @Override public void drop(Map<String, FileParameters> fileParams) { onDrop(fileParams); } @Override public void poll() { // Polling server for changes after upload finished } }); } /** * Invoked when a file or files have been dropped on client side. Fires the * {@link FileDropEvent}. * * @param fileParams * map from file ids to actual file details */ protected void onDrop(Map<String, FileParameters> fileParams) { Map<String, Html5File> files = new HashMap<>(); Map<String, String> urls = new HashMap<>(); // Create a collection of html5 files fileParams.forEach((id, fileParameters) -> { Html5File html5File = new Html5File(fileParameters.getName(), fileParameters.getSize(), fileParameters.getMime()); files.put(id, html5File); }); // Call drop handler with the collection of dropped files FileDropEvent<T> event = new FileDropEvent<>(getParent(), files.values()); fileDropHandler.drop(event); // Create upload URLs for the files that the drop handler // attached stream variable to files.entrySet().stream() .filter(entry -> entry.getValue().getStreamVariable() != null) .forEach(entry -> { String id = entry.getKey(); Html5File file = entry.getValue(); String url = createUrl(file, id); urls.put(id, url); }); // Send upload URLs to the client if there are files to be // uploaded if (urls.size() > 0) { getRpcProxy(FileDropTargetClientRpc.class).sendUploadUrl(urls); } } /** * Creates an upload URL for the given file and file ID. * * @param file * File to be uploaded. * @param id * Generated ID for the file. * @return Upload URL for uploading the file to the server. */ private String createUrl(Html5File file, String id) { return getStreamVariableTargetUrl("rec-" + id, new FileReceiver(id, file)); } private String getStreamVariableTargetUrl(String name, StreamVariable value) { String connectorId = getConnectorId(); UI ui = getUI(); int uiId = ui.getUIId(); String key = uiId + "/" + connectorId + "/" + name; ConnectorTracker connectorTracker = ui.getConnectorTracker(); connectorTracker.addStreamVariable(connectorId, name, value); String secKey = connectorTracker.getSeckey(value); return ApplicationConstants.APP_PROTOCOL_PREFIX + ServletPortletHelper.UPLOAD_URL_PREFIX + key + "/" + secKey; } @Override protected FileDropTargetState getState() { return (FileDropTargetState) super.getState(); } @Override protected FileDropTargetState getState(boolean markAsDirty) { return (FileDropTargetState) super.getState(markAsDirty); } /** * Returns the component this extension is attached to. * * @return Extended component. */ @Override @SuppressWarnings("unchecked") public T getParent() { return super.getParent(); } private class FileReceiver implements StreamVariable { private final String id; private Html5File file; public FileReceiver(String id, Html5File file) { this.id = id; this.file = file; } private boolean listenProgressOfUploadedFile; @Override public OutputStream getOutputStream() { if (file.getStreamVariable() == null) { return null; } return file.getStreamVariable().getOutputStream(); } @Override public boolean listenProgress() { return file.getStreamVariable().listenProgress(); } @Override public void onProgress(StreamingProgressEvent event) { file.getStreamVariable() .onProgress(new ReceivingEventWrapper(event)); } @Override public void streamingStarted(StreamingStartEvent event) { listenProgressOfUploadedFile = file.getStreamVariable() != null; if (listenProgressOfUploadedFile) { file.getStreamVariable() .streamingStarted(new ReceivingEventWrapper(event)); } } @Override public void streamingFinished(StreamingEndEvent event) { if (listenProgressOfUploadedFile) { file.getStreamVariable() .streamingFinished(new ReceivingEventWrapper(event)); } } @Override public void streamingFailed(final StreamingErrorEvent event) { if (listenProgressOfUploadedFile) { file.getStreamVariable() .streamingFailed(new ReceivingEventWrapper(event)); } } @Override public boolean isInterrupted() { return file.getStreamVariable().isInterrupted(); } /* * With XHR2 file posts we can't provide as much information from the * terminal as with multipart request. This helper class wraps the * terminal event and provides the lacking information from the * FileParameters. */ class ReceivingEventWrapper implements StreamingErrorEvent, StreamingEndEvent, StreamingStartEvent, StreamingProgressEvent { private final StreamingEvent wrappedEvent; ReceivingEventWrapper(StreamingEvent e) { wrappedEvent = e; } @Override public String getMimeType() { return file.getType(); } @Override public String getFileName() { return file.getFileName(); } @Override public long getContentLength() { return file.getFileSize(); } public StreamVariable getReceiver() { return FileReceiver.this; } @Override public Exception getException() { if (wrappedEvent instanceof StreamingErrorEvent) { return ((StreamingErrorEvent) wrappedEvent).getException(); } return null; } @Override public long getBytesReceived() { return wrappedEvent.getBytesReceived(); } /** * Calling this method has no effect. DD files are receive only once * anyway. */ @Override public void disposeStreamVariable() { } } } }