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

io.quarkus.websockets.next.runtime.BasicWebSocketConnectorImpl Maven / Gradle / Ivy

There is a newer version: 3.17.2
Show newest version
package io.quarkus.websockets.next.runtime;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.inject.Typed;

import org.jboss.logging.Logger;

import io.quarkus.tls.TlsConfigurationRegistry;
import io.quarkus.virtual.threads.VirtualThreadsRecorder;
import io.quarkus.websockets.next.BasicWebSocketConnector;
import io.quarkus.websockets.next.CloseReason;
import io.quarkus.websockets.next.WebSocketClientConnection;
import io.quarkus.websockets.next.WebSocketClientException;
import io.quarkus.websockets.next.WebSocketsClientRuntimeConfig;
import io.smallrye.mutiny.Uni;
import io.vertx.core.AsyncResult;
import io.vertx.core.Context;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.WebSocket;
import io.vertx.core.http.WebSocketClient;
import io.vertx.core.http.WebSocketConnectOptions;
import io.vertx.core.impl.ContextImpl;
import io.vertx.core.impl.VertxImpl;

@Typed(BasicWebSocketConnector.class)
@Dependent
public class BasicWebSocketConnectorImpl extends WebSocketConnectorBase
        implements BasicWebSocketConnector {

    private static final Logger LOG = Logger.getLogger(BasicWebSocketConnectorImpl.class);

    // mutable state

    private ExecutionModel executionModel = ExecutionModel.BLOCKING;

    private Consumer openHandler;

    private BiConsumer textMessageHandler;

    private BiConsumer binaryMessageHandler;

    private BiConsumer pongMessageHandler;

    private BiConsumer closeHandler;

    private BiConsumer errorHandler;

    BasicWebSocketConnectorImpl(Vertx vertx, Codecs codecs, ClientConnectionManager connectionManager,
            WebSocketsClientRuntimeConfig config, TlsConfigurationRegistry tlsConfigurationRegistry) {
        super(vertx, codecs, connectionManager, config, tlsConfigurationRegistry);
    }

    @Override
    public BasicWebSocketConnector executionModel(ExecutionModel model) {
        this.executionModel = Objects.requireNonNull(model);
        return self();
    }

    @Override
    public BasicWebSocketConnector path(String path) {
        setPath(Objects.requireNonNull(path));
        return self();
    }

    @Override
    public BasicWebSocketConnector onOpen(Consumer consumer) {
        this.openHandler = Objects.requireNonNull(consumer);
        return self();
    }

    @Override
    public BasicWebSocketConnector onTextMessage(BiConsumer consumer) {
        this.textMessageHandler = Objects.requireNonNull(consumer);
        return self();
    }

    @Override
    public BasicWebSocketConnector onBinaryMessage(BiConsumer consumer) {
        this.binaryMessageHandler = Objects.requireNonNull(consumer);
        return self();
    }

    @Override
    public BasicWebSocketConnector onPong(BiConsumer consumer) {
        this.pongMessageHandler = Objects.requireNonNull(consumer);
        return self();
    }

    @Override
    public BasicWebSocketConnector onClose(BiConsumer consumer) {
        this.closeHandler = Objects.requireNonNull(consumer);
        return self();
    }

    @Override
    public BasicWebSocketConnector onError(BiConsumer consumer) {
        this.errorHandler = Objects.requireNonNull(consumer);
        return self();
    }

    @Override
    public Uni connect() {
        if (baseUri == null) {
            throw new WebSocketClientException("Endpoint URI not set!");
        }

        // A new client is created for each connection
        // The client is created when the returned Uni is subscribed
        // The client is closed when the connection is closed
        AtomicReference client = new AtomicReference<>();

        WebSocketConnectOptions connectOptions = newConnectOptions(baseUri);
        StringBuilder requestUri = new StringBuilder();
        String mergedPath = mergePath(baseUri.getPath(), replacePathParameters(path));
        requestUri.append(mergedPath);
        if (baseUri.getQuery() != null) {
            requestUri.append("?").append(baseUri.getQuery());
        }
        connectOptions.setURI(requestUri.toString());
        for (Entry> e : headers.entrySet()) {
            for (String val : e.getValue()) {
                connectOptions.addHeader(e.getKey(), val);
            }
        }
        subprotocols.forEach(connectOptions::addSubProtocol);

        URI serverEndpointUri;
        try {
            serverEndpointUri = new URI(baseUri.getScheme(), baseUri.getUserInfo(), baseUri.getHost(), baseUri.getPort(),
                    mergedPath,
                    baseUri.getQuery(), baseUri.getFragment());
        } catch (URISyntaxException e) {
            throw new WebSocketClientException(e);
        }

        Uni websocket = Uni.createFrom(). emitter(e -> {
            // Create a new event loop context for each client, otherwise the current context is used
            // We want to avoid a situation where if multiple clients/connections are created in a row,
            // the same event loop is used and so writing/receiving messages is de-facto serialized
            // Get rid of this workaround once https://github.com/eclipse-vertx/vert.x/issues/5366 is resolved
            ContextImpl context = ((VertxImpl) vertx).createEventLoopContext();
            context.dispatch(new Handler() {
                @Override
                public void handle(Void event) {
                    WebSocketClient c = vertx.createWebSocketClient(populateClientOptions());
                    client.setPlain(c);
                    c.connect(connectOptions, new Handler>() {
                        @Override
                        public void handle(AsyncResult r) {
                            if (r.succeeded()) {
                                e.complete(r.result());
                            } else {
                                e.fail(r.cause());
                            }
                        }
                    });
                }
            });
        });
        return websocket.map(ws -> {
            String clientId = BasicWebSocketConnector.class.getName();
            TrafficLogger trafficLogger = TrafficLogger.forClient(config);
            WebSocketClientConnectionImpl connection = new WebSocketClientConnectionImpl(clientId, ws,
                    codecs,
                    pathParams,
                    serverEndpointUri,
                    headers, trafficLogger);
            if (trafficLogger != null) {
                trafficLogger.connectionOpened(connection);
            }
            connectionManager.add(BasicWebSocketConnectorImpl.class.getName(), connection);

            if (openHandler != null) {
                doExecute(connection, null, (c, ignored) -> openHandler.accept(c));
            }

            if (textMessageHandler != null) {
                ws.textMessageHandler(new Handler() {
                    @Override
                    public void handle(String message) {
                        if (trafficLogger != null) {
                            trafficLogger.textMessageReceived(connection, message);
                        }
                        doExecute(connection, message, textMessageHandler);
                    }
                });
            }

            if (binaryMessageHandler != null) {
                ws.binaryMessageHandler(new Handler() {

                    @Override
                    public void handle(Buffer message) {
                        if (trafficLogger != null) {
                            trafficLogger.binaryMessageReceived(connection, message);
                        }
                        doExecute(connection, message, binaryMessageHandler);
                    }
                });
            }

            if (pongMessageHandler != null) {
                ws.pongHandler(new Handler() {

                    @Override
                    public void handle(Buffer event) {
                        doExecute(connection, event, pongMessageHandler);
                    }
                });
            }

            if (errorHandler != null) {
                ws.exceptionHandler(new Handler() {

                    @Override
                    public void handle(Throwable event) {
                        doExecute(connection, event, errorHandler);
                    }
                });
            }

            ws.closeHandler(new Handler() {

                @Override
                public void handle(Void event) {
                    if (trafficLogger != null) {
                        trafficLogger.connectionClosed(connection);
                    }
                    if (closeHandler != null) {
                        CloseReason reason = CloseReason.INTERNAL_SERVER_ERROR;
                        if (ws.closeStatusCode() != null) {
                            reason = new CloseReason(ws.closeStatusCode(), ws.closeReason());
                        }
                        doExecute(connection, reason, closeHandler);
                    }
                    connectionManager.remove(BasicWebSocketConnectorImpl.class.getName(), connection);
                    client.get().close();
                }

            });

            return connection;
        });
    }

    private  void doExecute(WebSocketClientConnectionImpl connection, MESSAGE message,
            BiConsumer consumer) {
        // We always invoke callbacks on a new duplicated context and offload if blocking/virtualThread is needed
        Context context = vertx.getOrCreateContext();
        ContextSupport.createNewDuplicatedContext(context, connection).runOnContext(new Handler() {
            @Override
            public void handle(Void event) {
                if (executionModel == ExecutionModel.VIRTUAL_THREAD) {
                    VirtualThreadsRecorder.getCurrent().execute(new Runnable() {
                        public void run() {
                            try {
                                consumer.accept(connection, message);
                            } catch (Exception e) {
                                LOG.errorf(e, "Unable to call handler: " + connection);
                            }
                        }
                    });
                } else if (executionModel == ExecutionModel.BLOCKING) {
                    vertx.executeBlocking(new Callable() {
                        @Override
                        public Void call() {
                            try {
                                consumer.accept(connection, message);
                            } catch (Exception e) {
                                LOG.errorf(e, "Unable to call handler: " + connection);
                            }
                            return null;
                        }
                    }, false);
                } else {
                    // Non-blocking -> event loop
                    try {
                        consumer.accept(connection, message);
                    } catch (Exception e) {
                        LOG.errorf(e, "Unable to call handler: " + connection);
                    }
                }
            }
        });
    }

    private String mergePath(String path1, String path2) {
        StringBuilder ret = new StringBuilder();
        if (path1 != null) {
            ret.append(path1);
        }
        if (path2 != null) {
            if (path1.endsWith("/")) {
                if (path2.startsWith("/")) {
                    ret.append(path2.substring(1));
                } else {
                    ret.append(path2);
                }
            } else {
                if (path2.startsWith("/")) {
                    ret.append(path2);
                } else {
                    ret.append("/").append(path2);
                }
            }
        }
        return ret.toString();
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy