All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.mcollovati.vertx.vaadin.communication.StreamReceiverHandler Maven / Gradle / Ivy

There is a newer version: 24.5.0-alpha1
Show newest version
/*
 * The MIT License
 * Copyright © 2016-2020 Marco Collovati ([email protected])
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.github.mcollovati.vertx.vaadin.communication;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Serializable;
import java.util.Set;

import com.github.mcollovati.vertx.support.BufferInputStreamAdapter;
import com.github.mcollovati.vertx.vaadin.VertxVaadinRequest;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.internal.StateNode;
import com.vaadin.flow.server.ErrorEvent;
import com.vaadin.flow.server.NoInputStreamException;
import com.vaadin.flow.server.NoOutputStreamException;
import com.vaadin.flow.server.StreamReceiver;
import com.vaadin.flow.server.StreamResource;
import com.vaadin.flow.server.StreamVariable;
import com.vaadin.flow.server.UploadException;
import com.vaadin.flow.server.VaadinRequest;
import com.vaadin.flow.server.VaadinResponse;
import com.vaadin.flow.server.VaadinSession;
import com.vaadin.flow.server.communication.streaming.StreamingEndEventImpl;
import com.vaadin.flow.server.communication.streaming.StreamingErrorEventImpl;
import com.vaadin.flow.server.communication.streaming.StreamingProgressEventImpl;
import com.vaadin.flow.server.communication.streaming.StreamingStartEventImpl;
import com.vaadin.flow.shared.ApplicationConstants;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.file.FileSystem;
import io.vertx.ext.web.FileUpload;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * Handles {@link StreamResource} instances registered in {@link VaadinSession}.
 *
 * Code adapted from the original {@link com.vaadin.flow.server.communication.StreamReceiverHandler}
 */
public class StreamReceiverHandler implements Serializable {

    private static final int MAX_UPLOAD_BUFFER_SIZE = 4 * 1024;

    /* Minimum interval which will be used for streaming progress events. */
    public static final int DEFAULT_STREAMING_PROGRESS_EVENT_INTERVAL_MS = 500;

    /**
     * An UploadInterruptedException will be thrown by an ongoing upload if
     * {@link StreamVariable#isInterrupted()} returns true.
     *
     * By checking the exception of an
     * {@link StreamVariable.StreamingErrorEvent} or {link FailedEvent} against
     * this class, it is possible to determine if an upload was interrupted by
     * code or aborted due to any other exception.
     */
    public static class UploadInterruptedException extends Exception {

        /**
         * Constructs an instance of UploadInterruptedException.
         */
        public UploadInterruptedException() {
            super("Upload interrupted by other thread");
        }
    }

    /**
     * Handle reception of incoming stream from the client.
     *
     * @param session        The session for the request
     * @param request        The request to handle
     * @param response       The response object to which a response can be written.
     * @param streamReceiver the receiver containing the destination stream variable
     * @param uiId           id of the targeted ui
     * @param securityKey    security from the request that should match registered stream
     *                       receiver id
     * @throws IOException if an IO error occurred
     */
    public void handleRequest(VaadinSession session, VaadinRequest request,
                              VaadinResponse response, StreamReceiver streamReceiver, String uiId,
                              String securityKey) throws IOException {
        StateNode source;

        session.lock();
        try {
            String secKey = streamReceiver.getId();
            if (secKey == null || !secKey.equals(securityKey)) {
                getLogger().warn(
                    "Received incoming stream with faulty security key.");
                return;
            }

            UI ui = session.getUIById(Integer.parseInt(uiId));
            UI.setCurrent(ui);

            source = streamReceiver.getNode();

        } finally {
            session.unlock();
        }

        try {
            Set fileUploads = ((VertxVaadinRequest) request).getRoutingContext().fileUploads();
            if (!fileUploads.isEmpty()) {
                doHandleMultipartFileUpload(session, request, response, fileUploads, streamReceiver, source);
            } else {
                // if boundary string does not exist, the posted file is from
                // XHR2.post(File)
                doHandleXhrFilePost(session, request, response, streamReceiver,
                    source, getContentLength(request));
            }
        } finally {
            UI.setCurrent(null);
        }
    }

    /**
     * Method used to stream content from a multipart request to given
     * StreamVariable.
     * 

* This method takes care of locking the session as needed and does not * assume the caller has locked the session. This allows the session to be * locked only when needed and not when handling the upload data. * * @param session The session containing the stream variable * @param request The upload request * @param response The upload response * @param streamReceiver the receiver containing the destination stream variable * @param owner The owner of the stream * @throws IOException If there is a problem reading the request or writing the * response */ protected void doHandleMultipartFileUpload(VaadinSession session, VaadinRequest request, VaadinResponse response, Set uploads, StreamReceiver streamReceiver, StateNode owner) throws IOException { long contentLength = getContentLength(request); FileSystem fileSystem = ((VertxVaadinRequest) request).getService().getVertx().fileSystem(); try { uploads.forEach(item -> handleStream(session, fileSystem, streamReceiver, owner, contentLength, item)); } catch (Exception e) { getLogger().warn("File upload failed.", e); } // Create a new file upload handler ServletFileUpload upload = new ServletFileUpload(); sendUploadResponse(response); } private void handleStream(VaadinSession session, FileSystem fileSystem, StreamReceiver streamReceiver, StateNode owner, long contentLength, FileUpload item) { String name = item.name(); Buffer buffer = fileSystem.readFileBlocking(item.uploadedFileName()); InputStream stream = new BufferInputStreamAdapter(buffer); try { handleFileUploadValidationAndData(session, stream, streamReceiver, name, item.contentType(), contentLength, owner); } catch (UploadException e) { session.getErrorHandler().error(new ErrorEvent(e)); } } /** * Used to stream plain file post (aka XHR2.post(File)) *

* This method takes care of locking the session as needed and does not * assume the caller has locked the session. This allows the session to be * locked only when needed and not when handling the upload data. *

* * @param session The session containing the stream variable * @param request The upload request * @param response The upload response * @param streamReceiver the receiver containing the destination stream variable * @param owner The owner of the stream * @param contentLength The length of the request content * @throws IOException If there is a problem reading the request or writing the * response */ protected void doHandleXhrFilePost(VaadinSession session, VaadinRequest request, VaadinResponse response, StreamReceiver streamReceiver, StateNode owner, long contentLength) throws IOException { // These are unknown in filexhr ATM, maybe add to Accept header that // is accessible in portlets final String filename = "unknown"; final String mimeType = filename; final InputStream stream = request.getInputStream(); try { handleFileUploadValidationAndData(session, stream, streamReceiver, filename, mimeType, contentLength, owner); } catch (UploadException e) { session.getErrorHandler().error(new ErrorEvent(e)); } sendUploadResponse(response); } private void handleFileUploadValidationAndData(VaadinSession session, InputStream inputStream, StreamReceiver streamReceiver, String filename, String mimeType, long contentLength, StateNode node) throws UploadException { session.lock(); try { if (node == null) { throw new UploadException( "File upload ignored because the node for the stream variable was not found"); } if (!node.isAttached()) { throw new UploadException("Warning: file upload ignored for " + node.getId() + " because the component was disabled"); } } finally { session.unlock(); } try { // Store ui reference so we can do cleanup even if node is // detached in some event handler boolean forgetVariable = streamToReceiver(session, inputStream, streamReceiver, filename, mimeType, contentLength); if (forgetVariable) { cleanStreamVariable(session, streamReceiver); } } catch (Exception e) { session.lock(); try { session.getErrorHandler().error(new ErrorEvent(e)); } finally { session.unlock(); } } } /** * To prevent event storming, streaming progress events are sent in this * interval rather than every time the buffer is filled. This fixes #13155. * To adjust this value override the method, and register your own handler * in VaadinService.createRequestHandlers(). The default is 500ms, and * setting it to 0 effectively restores the old behavior. * * @return the minimum interval to be used for streaming progress events */ protected int getProgressEventInterval() { return DEFAULT_STREAMING_PROGRESS_EVENT_INTERVAL_MS; } static void tryToCloseStream(OutputStream out) { try { // try to close output stream (e.g. file handle) if (out != null) { out.close(); } } catch (IOException ioe) { getLogger().debug("Exception closing stream", ioe); } } /** * Build response for handled download. * * @param response response to write to * @throws IOException exception when writing to stream */ private void sendUploadResponse(VaadinResponse response) throws IOException { response.setContentType( ApplicationConstants.CONTENT_TYPE_TEXT_HTML_UTF_8); try (OutputStream out = response.getOutputStream()) { final PrintWriter outWriter = new PrintWriter( new BufferedWriter(new OutputStreamWriter(out, UTF_8))); try { outWriter.print("download handled"); } finally { outWriter.flush(); } } } private void cleanStreamVariable(VaadinSession session, StreamReceiver streamReceiver) { session.lock(); try { session.getResourceRegistry().unregisterResource(streamReceiver); } finally { session.unlock(); } } private final boolean streamToReceiver(VaadinSession session, final InputStream in, StreamReceiver streamReceiver, String filename, String type, long contentLength) throws UploadException { StreamVariable streamVariable = streamReceiver.getStreamVariable(); if (streamVariable == null) { throw new IllegalStateException( "StreamVariable for the post not found"); } OutputStream out = null; long totalBytes = 0; StreamingStartEventImpl startedEvent = new StreamingStartEventImpl( filename, type, contentLength); try { boolean listenProgress; session.lock(); try { streamVariable.streamingStarted(startedEvent); out = streamVariable.getOutputStream(); listenProgress = streamVariable.listenProgress(); } finally { session.unlock(); } // Gets the output target stream if (out == null) { throw new NoOutputStreamException(); } if (null == in) { // No file, for instance non-existent filename in html upload throw new NoInputStreamException(); } final byte[] buffer = new byte[MAX_UPLOAD_BUFFER_SIZE]; long lastStreamingEvent = 0; int bytesReadToBuffer; do { bytesReadToBuffer = in.read(buffer); if (bytesReadToBuffer > 0) { out.write(buffer, 0, bytesReadToBuffer); totalBytes += bytesReadToBuffer; } if (listenProgress) { StreamingProgressEventImpl progressEvent = new StreamingProgressEventImpl( filename, type, contentLength, totalBytes); lastStreamingEvent = updateProgress(session, streamVariable, progressEvent, lastStreamingEvent, bytesReadToBuffer); } if (streamVariable.isInterrupted()) { throw new UploadInterruptedException(); } } while (bytesReadToBuffer > 0); // upload successful out.close(); StreamVariable.StreamingEndEvent event = new StreamingEndEventImpl( filename, type, totalBytes); session.lock(); try { streamVariable.streamingFinished(event); } finally { session.unlock(); } } catch (UploadInterruptedException e) { // Download interrupted by application code tryToCloseStream(out); StreamVariable.StreamingErrorEvent event = new StreamingErrorEventImpl( filename, type, contentLength, totalBytes, e); session.lock(); try { streamVariable.streamingFailed(event); } finally { session.unlock(); } // Note, we are not throwing interrupted exception forward as it is // not a terminal level error like all other exception. } catch (final Exception e) { tryToCloseStream(out); session.lock(); try { StreamVariable.StreamingErrorEvent event = new StreamingErrorEventImpl( filename, type, contentLength, totalBytes, e); streamVariable.streamingFailed(event); // throw exception for terminal to be handled (to be passed to // terminalErrorHandler) throw new UploadException(e); } finally { session.unlock(); } } return startedEvent.isDisposed(); } private long updateProgress(VaadinSession session, StreamVariable streamVariable, StreamingProgressEventImpl progressEvent, long lastStreamingEvent, int bytesReadToBuffer) { long now = System.currentTimeMillis(); // to avoid excessive session locking and event storms, // events are sent in intervals, or at the end of the file. if (lastStreamingEvent + getProgressEventInterval() <= now || bytesReadToBuffer <= 0) { session.lock(); try { streamVariable.onProgress(progressEvent); } finally { session.unlock(); } } return now; } /** * The request.getContentLength() is limited to "int" by the Servlet * specification. To support larger file uploads manually evaluate the * Content-Length header which can contain long values. */ private long getContentLength(VaadinRequest request) { try { return Long.parseLong(request.getHeader("Content-Length")); } catch (NumberFormatException e) { return -1l; } } private static Logger getLogger() { return LoggerFactory.getLogger(StreamReceiverHandler.class.getName()); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy