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

io.gravitee.cockpit.connectors.ws.channel.ClientChannel Maven / Gradle / Ivy

There is a newer version: 5.1.2
Show newest version
/**
 * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
 *
 * 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 io.gravitee.cockpit.connectors.ws.channel;

import static io.gravitee.cockpit.api.command.Command.*;
import static io.gravitee.cockpit.api.command.CommandStatus.ERROR;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.gravitee.cockpit.api.command.*;
import io.gravitee.cockpit.api.command.hello.HelloCommand;
import io.gravitee.cockpit.api.command.hello.HelloPayload;
import io.gravitee.cockpit.api.command.hello.HelloReply;
import io.gravitee.cockpit.api.command.ignored.IgnoredReply;
import io.gravitee.cockpit.connectors.ws.exceptions.ChannelClosedException;
import io.gravitee.common.util.Version;
import io.gravitee.node.api.Node;
import io.gravitee.plugin.core.api.PluginManifest;
import io.reactivex.*;
import io.reactivex.subjects.SingleSubject;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.WebSocket;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import lombok.extern.slf4j.Slf4j;

/**
 * @author Jeoffrey HAEYAERT (jeoffrey.haeyaert at graviteesource.com)
 * @author GraviteeSource Team
 */
@Slf4j
public class ClientChannel {

    private final WebSocket webSocket;
    private final Map> resultEmitters;
    private final Map, Reply>> commandHandlers;
    private final CommandProducer helloCommandProducer;

    private final Node node;
    private final PluginManifest pluginManifest;
    private final ObjectMapper objectMapper;
    private boolean goodbyeCommandReceived = false;
    private ClientChannelEventHandler closeHandler = () -> {};
    private ClientChannelEventHandler onPrimaryHandler = () -> {};
    private ClientChannelEventHandler onReplicaHandler = () -> {};

    public ClientChannel(
        WebSocket webSocket,
        Node node,
        CommandProducer helloCommandProducer,
        Map, Reply>> commandHandlers,
        PluginManifest pluginManifest,
        ObjectMapper objectMapper
    ) {
        this.webSocket = webSocket;
        this.node = node;
        this.pluginManifest = pluginManifest;
        this.objectMapper = objectMapper;
        this.resultEmitters = new ConcurrentHashMap<>();
        this.helloCommandProducer = helloCommandProducer;
        this.commandHandlers = commandHandlers;
    }

    public Single init() {
        // Start listening.
        listen();

        // Hello command handler is only used once after connection occurred.
        return handleHello(node);
    }

    public void cleanup() {
        resultEmitters.forEach((type, emitter) -> emitter.onError(new ChannelClosedException()));
        resultEmitters.clear();
    }

    Single handleHello(Node node) {
        HelloPayload payload = new HelloPayload();
        io.gravitee.cockpit.api.command.Node payloadNode = new io.gravitee.cockpit.api.command.Node();
        payloadNode.setApplication(node.application());
        payloadNode.setVersion(Version.RUNTIME_VERSION.MAJOR_VERSION);
        payloadNode.setConnectorVersion(pluginManifest.version());
        payloadNode.setHostname(node.hostname());
        payload.setNode(payloadNode);
        HelloCommand helloCommand = new HelloCommand(payload);

        Single helloCommandObs = Single.just(helloCommand);

        SingleSubject helloHandshakeDone = SingleSubject.create();

        helloCommandObs
            .flatMap(
                command -> {
                    if (helloCommandProducer != null) {
                        // Give to opportunity to complete the HelloCommand with custom information.
                        return helloCommandProducer.prepare(helloCommand);
                    }
                    return Single.just(command);
                }
            )
            .flatMap(this::send)
            .flatMap(
                reply -> {
                    if (helloCommandProducer != null) {
                        return helloCommandProducer.handleReply((HelloReply) reply);
                    }
                    return Single.just((HelloReply) reply);
                }
            )
            .subscribe(
                reply -> {
                    helloHandshakeDone.onSuccess(reply);
                    log.info(
                        "HelloCommand replied with status [{}]. Installation status is [{}]",
                        reply.getCommandStatus(),
                        reply.getInstallationStatus()
                    );

                    if (shouldCloseConnection(reply)) {
                        closeHandler.handle();
                    }
                },
                t -> {
                    helloHandshakeDone.onError(t);
                    log.error("Unable to send HelloCommand", t);
                }
            );

        return helloHandshakeDone;
    }

    void handleGoodbye() {
        goodbyeCommandReceived = true;
    }

    void listen() {
        webSocket.handler(
            buffer -> {
                String incoming = buffer.toString();

                if (incoming.startsWith(PING_PONG_PREFIX)) {
                    return;
                }

                if (incoming.startsWith(PRIMARY_MESSAGE)) {
                    log.warn("I am the PRIMARY");
                    this.onPrimaryHandler.handle();
                    return;
                }

                if (incoming.startsWith(REPLICA_MESSAGE)) {
                    log.warn("I am a replica");
                    this.onReplicaHandler.handle();
                    return;
                }

                try {
                    if (incoming.startsWith(COMMAND_PREFIX)) {
                        Command command = decodeJsonString(incoming.replace(COMMAND_PREFIX, ""), Command.class);
                        CommandHandler, Reply> commandHandler = commandHandlers.get(command.getType());

                        if (command.getType().equals(Type.GOODBYE_COMMAND)) {
                            handleGoodbye();
                        }

                        if (commandHandler != null) {
                            commandHandler
                                .handle(command)
                                .subscribe(
                                    reply -> {
                                        reply(reply);
                                        if (reply.stopOnErrorStatus() && reply.getCommandStatus() == ERROR) {
                                            webSocket.close();
                                        }
                                        if (goodbyeCommandReceived) {
                                            closeHandler.handle();
                                        }
                                    },
                                    e -> webSocket.close((short) 500, "Unexpected error")
                                );
                        } else {
                            log.info("No handler found for command type {}. Ignoring.", command.getType());
                            reply(new IgnoredReply(command.getId()));
                        }
                    } else if (incoming.startsWith(REPLY_PREFIX)) {
                        Reply reply = decodeJsonString(incoming.replace(REPLY_PREFIX, ""), Reply.class);
                        SingleEmitter emitter = resultEmitters.remove(reply.getCommandId());

                        if (emitter != null) {
                            emitter.onSuccess(reply);
                        }
                    } else {
                        webSocket.close((short) 400, "Bad incoming content");
                    }
                } catch (Exception e) {
                    log.info(String.format("An error occurred when trying to decode incoming content [%s]. Ignore message.", incoming), e);
                }
            }
        );
    }

    public Single send(Command command) {
        return Single
            .create(
                emitter -> {
                    resultEmitters.put(command.getId(), emitter);
                    write(command).doOnError(emitter::onError).subscribe();
                }
            )
            // Cleanup result emitters list if cancelled by the upstream.
            .doOnDispose(() -> resultEmitters.remove(command.getId()));
    }

    public void onClose(ClientChannelEventHandler closeHandler) {
        this.closeHandler = closeHandler;
    }

    public void onPrimary(ClientChannelEventHandler onPrimaryHandler) {
        this.onPrimaryHandler = onPrimaryHandler;
    }

    public void onReplica(ClientChannelEventHandler onReplicaHandler) {
        this.onReplicaHandler = onReplicaHandler;
    }

    void reply(Reply reply) throws JsonProcessingException {
        this.webSocket.write(
                Buffer.buffer(REPLY_PREFIX + toJsonString(reply)),
                avoid -> {
                    if (avoid.failed()) {
                        log.error("An error occurred when trying to reply [{}]. Closing socket.", reply);
                        webSocket.close();
                    } else {
                        log.debug("Write to socket succeeded");
                    }
                }
            );
    }

    private Completable write(Command command) {
        return Completable.create(
            emitter ->
                this.webSocket.write(
                        Buffer.buffer(COMMAND_PREFIX + toJsonString(command)),
                        avoid -> {
                            if (avoid.failed()) {
                                log.error("An error occurred when trying to send command [{}]", command);
                                emitter.onError(new Exception("Write to socket failed"));
                            } else {
                                log.debug("Write to socket succeeded: [{}]", command);
                                emitter.onComplete();
                            }
                        }
                    )
        );
    }

    private boolean shouldCloseConnection(HelloReply reply) {
        String installationStatus = reply.getInstallationStatus();
        return installationStatus.equals("DELETED") || installationStatus.equals("INCOMPATIBLE");
    }

    private String toJsonString(Object object) throws JsonProcessingException {
        return objectMapper.writeValueAsString(object);
    }

    private  T decodeJsonString(String content, Class clazz) throws JsonProcessingException {
        return objectMapper.readValue(content, clazz);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy