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

org.jboss.resteasy.reactive.client.impl.SseEventSourceImpl Maven / Gradle / Ivy

There is a newer version: 3.17.5
Show newest version
package org.jboss.resteasy.reactive.client.impl;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

import jakarta.ws.rs.client.Invocation;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.sse.InboundSseEvent;
import jakarta.ws.rs.sse.SseEventSource;

import org.jboss.resteasy.reactive.common.util.CommonSseUtil;

import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpClientResponse;
import io.vertx.core.http.HttpConnection;
import io.vertx.core.net.impl.ConnectionBase;

public class SseEventSourceImpl implements SseEventSource, Handler {

    private TimeUnit reconnectUnit;
    private long reconnectDelay;

    private final WebTargetImpl webTarget;
    private final Invocation.Builder invocationBuilder;
    // this tracks user request to open/close
    private volatile boolean isOpen;
    // this tracks whether we have a connection open
    private volatile boolean isInProgress;

    private final List> consumers = new ArrayList<>();
    private final List> errorListeners = new ArrayList<>();
    private final List completionListeners = new ArrayList<>();
    private HttpConnection connection;
    private final SseParser sseParser;
    private long timerId = -1;
    private boolean receivedClientClose;

    public SseEventSourceImpl(WebTargetImpl webTarget, Invocation.Builder invocationBuilder,
            long reconnectDelay, TimeUnit reconnectUnit) {
        this(webTarget, invocationBuilder, reconnectDelay, reconnectUnit, null);
    }

    public SseEventSourceImpl(WebTargetImpl webTarget, Invocation.Builder invocationBuilder,
            long reconnectDelay, TimeUnit reconnectUnit, String defaultContentType) {
        // tests set a null endpoint
        Objects.requireNonNull(reconnectUnit);
        if (reconnectDelay <= 0)
            throw new IllegalArgumentException("Delay must be > 0: " + reconnectDelay);
        this.webTarget = webTarget;
        this.reconnectDelay = reconnectDelay;
        this.reconnectUnit = reconnectUnit;
        this.sseParser = new SseParser(this, defaultContentType);
        this.invocationBuilder = invocationBuilder;
    }

    WebTargetImpl getWebTarget() {
        return webTarget;
    }

    @Override
    public synchronized void register(Consumer onEvent) {
        consumers.add(onEvent);
    }

    @Override
    public synchronized void register(Consumer onEvent, Consumer onError) {
        consumers.add(onEvent);
        errorListeners.add(onError);
    }

    @Override
    public synchronized void register(Consumer onEvent, Consumer onError, Runnable onComplete) {
        consumers.add(onEvent);
        errorListeners.add(onError);
        completionListeners.add(onComplete);
    }

    @Override
    public synchronized void open() {
        if (isOpen)
            return;
        isOpen = true;
        connect();
    }

    // CALL WITH THE LOCK
    private void connect() {
        if (isInProgress)
            return;
        isInProgress = true;
        // ignore previous client closes
        receivedClientClose = false;
        AsyncInvokerImpl invoker = (AsyncInvokerImpl) invocationBuilder.rx();
        RestClientRequestContext restClientRequestContext = invoker.performRequestInternal("GET", null, null, false);
        restClientRequestContext.getResult().handle((response, throwable) -> {
            // errors during connection don't currently lead to a retry
            if (throwable != null) {
                receiveThrowable(throwable);
                notifyCompletion();
            } else if (Response.Status.Family.familyOf(response.getStatus()) != Response.Status.Family.SUCCESSFUL) {
                receiveThrowable(new RuntimeException("HTTP call unsuccessful: " + response.getStatus()));
                notifyCompletion();
            } else if (!MediaType.SERVER_SENT_EVENTS_TYPE.isCompatible(response.getMediaType())) {
                receiveThrowable(
                        new RuntimeException("HTTP call did not return an SSE media type: " + response.getMediaType()));
                notifyCompletion();
            } else {
                registerOnClient(restClientRequestContext.getVertxClientResponse());
            }
            return null;
        });
    }

    /**
     * Allows the HTTP client to register for SSE after it has made the request
     */
    synchronized void registerAfterRequest(HttpClientResponse vertxClientResponse) {
        if (isOpen)
            throw new IllegalStateException("Was already open");
        isOpen = true;
        registerOnClient(vertxClientResponse);
    }

    private void registerOnClient(HttpClientResponse vertxClientResponse) {
        // make sure we get exceptions on the response, like close events, otherwise they
        // will be logged as errors by vertx
        vertxClientResponse.exceptionHandler(t -> {
            if (t == ConnectionBase.CLOSED_EXCEPTION) {
                // we can ignore this one since we registered a closeHandler
            } else {
                receiveThrowable(t);
            }
        });
        // since we registered our exception handler, let's remove the request exception handler
        // that is set in ClientSendRequestHandler
        vertxClientResponse.request().exceptionHandler(null);
        connection = vertxClientResponse.request().connection();
        String sseContentTypeHeader = vertxClientResponse.getHeader(CommonSseUtil.SSE_CONTENT_TYPE);
        if ((sseContentTypeHeader != null) && !sseContentTypeHeader.isEmpty()) {
            sseParser.setSseContentTypeHeader(sseContentTypeHeader);
        }
        // we don't add a closeHandler handler on the connection as it can race with this handler
        // and close before the emitter emits anything
        // see: https://github.com/quarkusio/quarkus/pull/16438
        vertxClientResponse.handler(sseParser);
        vertxClientResponse.endHandler(v -> {
            close(true);
        });
        vertxClientResponse.resume();
    }

    private void receiveThrowable(Throwable throwable) {
        for (Consumer errorListener : errorListeners) {
            errorListener.accept(throwable);
        }
    }

    @Override
    public boolean isOpen() {
        return isOpen;
    }

    @Override
    public boolean close(long timeout, TimeUnit unit) {
        close(false);
        return true;
    }

    private synchronized void close(boolean clientClosed) {
        if (!isOpen) {
            return;
        }
        if (clientClosed) {
            // do not react more than once on client closing
            if (this.receivedClientClose) {
                return;
            }
        }
        // it's possible that the client closed our connection, then we registered a reconnect timer
        // and then the user is closing us, so we don't have a connection yet
        if (connection != null) {
            connection.close();
        }
        connection = null;
        isInProgress = false;
        boolean notifyCompletion = true;
        if (!clientClosed) {
            isOpen = false;
            if (receivedClientClose) {
                // do not notify completion if we already did as part of the client closing
                notifyCompletion = false;
            }
        } else {
            receivedClientClose = true;
        }
        if (notifyCompletion) {
            // notify completion before reconnecting
            notifyCompletion();
        }
        Vertx vertx = webTarget.getRestClient().getVertx();
        // did we already try to reconnect?
        if (timerId != -1) {
            // cancel any previous timer
            vertx.cancelTimer(timerId);
            timerId = -1;
        }
        // schedule a new reconnect if the client closed us
        if (clientClosed) {
            timerId = vertx.setTimer(TimeUnit.MILLISECONDS.convert(reconnectDelay, reconnectUnit), this);
        }
    }

    private synchronized void notifyCompletion() {
        for (Runnable runnable : completionListeners) {
            runnable.run();
        }
    }

    public synchronized void fireEvent(InboundSseEventImpl event) {
        // spec says to do this
        if (event.isReconnectDelaySet()) {
            reconnectDelay = event.getReconnectDelay();
            reconnectUnit = TimeUnit.MILLISECONDS;
        }
        for (Consumer consumer : consumers) {
            consumer.accept(event);
        }
    }

    @Override
    public synchronized void handle(Long event) {
        // ignore a timeout if it's not the last one we set
        if (timerId != event.longValue()) {
            return;
        }
        // also ignore a reconnect order if the user closed this
        if (!isOpen) {
            return;
        }
        connect();
    }

    // For tests
    public SseParser getSseParser() {
        return sseParser;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy