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

net.oneandone.reactive.sse.servlet.SseWriteableChannel Maven / Gradle / Ivy

There is a newer version: 0.10
Show newest version
/*
 * Copyright 1&1 Internet AG, https://github.com/1and1/
 * 
 * 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 net.oneandone.reactive.sse.servlet;




import java.io.IOException;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;

import net.oneandone.reactive.sse.ServerSentEvent;

import com.google.common.collect.Lists;





class SseWriteableChannel  {
    private static final List> whenWritePossibles = Lists.newArrayList();
    
    private final ServletOutputStream out;
    private final Consumer errorConsumer;


    
    public SseWriteableChannel(ServletOutputStream out, Consumer errorConsumer) {
        this(out, errorConsumer, Duration.ZERO, null);
    }

    
    public SseWriteableChannel(ServletOutputStream out, Consumer errorConsumer, Duration keepAlivePeriod, ScheduledExecutorService executor) {
        this.errorConsumer = errorConsumer;
        this.out = out;
        out.setWriteListener(new ServletWriteListener());
        
        if (executor != null) {
            // start the keep alive emitter 
            new KeepAliveEmitter(this, keepAlivePeriod, executor).start();
        }
        
        // write http header, implicitly 
        requestWriteNotificationAsync().thenAccept(Void -> flush());
    }

    
    public CompletableFuture writeEventAsync(ServerSentEvent event) {       
        CompletableFuture writtenFuture = new CompletableFuture<>();
        requestWriteNotificationAsync().thenAccept(Void -> writeToWrite(event, writtenFuture));
        
        return writtenFuture;
    }   
    
    
    private void writeToWrite(ServerSentEvent event, CompletableFuture writtenSizeFuture) {       
        try {
            synchronized (out) {
                byte[] data = event.toWire().getBytes("UTF-8");
                out.write(data);
                out.flush();
                
                writtenSizeFuture.complete(data.length);
            }
        } catch (IOException | RuntimeException t) {
            errorConsumer.accept(t);
            writtenSizeFuture.completeExceptionally(t);
        }
    }   
    

    private CompletableFuture requestWriteNotificationAsync() {
        CompletableFuture whenWritePossible = new CompletableFuture<>();

        synchronized (whenWritePossibles) {
            if (isWritePossible()) {
                whenWritePossible.complete(true);
            } else {
                // if not the WriteListener#onWritePossible will be called by the servlet container later
                whenWritePossibles.add(whenWritePossible);
            }
        }
        
        return whenWritePossible;
    }
    
    
    private boolean isWritePossible() {
        
        // triggers that write listener's onWritePossible will be called, if is possible to write data
        // According to the Servlet 3.1 spec the onWritePossible will be invoked if and only if isReady() 
        // method has been called and has returned false.
        //
        // Unfortunately the Servlet 3.1 spec left it open how many bytes can be written
        // Jetty for instance keeps a reference to the passed byte array and essentially owns it until the write is complete
        
        try {
            return out.isReady();
        } catch (IllegalStateException ise)  {
            return false;
        }
    }

    
    private void flush() {
        try {
            out.flush();
        } catch (IOException ioe) {
            throw new RuntimeException(ioe);
        }
    }
    
    
    public void close() {
        try {
            out.close();
        } catch (IOException ignore) { }
    }
    
  
    
    
    private final class ServletWriteListener implements WriteListener {

        @Override
        public void onWritePossible() throws IOException {    
            synchronized (whenWritePossibles) {
                whenWritePossibles.forEach(whenWritePossible -> whenWritePossible.complete(null));
                whenWritePossibles.clear();
            }
        }

        @Override
        public void onError(Throwable t) {
            errorConsumer.accept(t);
        }
    }  
    
    
    
    /**
     * sents keep alive messages to keep the http connection alive in case of idling
     * @author grro
     */
    private static final class KeepAliveEmitter {
        private final SseWriteableChannel channel;
        private final Duration keepAlivePeriod;
        private final ScheduledExecutorService executor;

        
        public KeepAliveEmitter(SseWriteableChannel channel, Duration keepAlivePeriod, ScheduledExecutorService executor) {
            this.channel = channel;
            this.keepAlivePeriod = keepAlivePeriod;
            this.executor = executor;
        }
        
        public void start() {
            scheduleNextKeepAliveEvent();
        }
        
        private void scheduleNextKeepAliveEvent() {
            Runnable task = () -> channel.writeEventAsync(ServerSentEvent.newEvent().comment("keep alive"))
                                         .thenAccept(numWritten -> scheduleNextKeepAliveEvent());
            
            executor.schedule(task, keepAlivePeriod.getSeconds(), TimeUnit.SECONDS);
        }        
    } 
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy