io.gravitee.exchange.api.websocket.channel.AbstractWebSocketChannel Maven / Gradle / Ivy
/*
* Copyright © 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.exchange.api.websocket.channel;
import static io.gravitee.exchange.api.command.CommandStatus.ERROR;
import io.gravitee.exchange.api.channel.Channel;
import io.gravitee.exchange.api.channel.exception.ChannelClosedException;
import io.gravitee.exchange.api.channel.exception.ChannelInactiveException;
import io.gravitee.exchange.api.channel.exception.ChannelInitializationException;
import io.gravitee.exchange.api.channel.exception.ChannelNoReplyException;
import io.gravitee.exchange.api.channel.exception.ChannelReplyException;
import io.gravitee.exchange.api.channel.exception.ChannelTimeoutException;
import io.gravitee.exchange.api.channel.exception.ChannelUnknownCommandException;
import io.gravitee.exchange.api.command.Command;
import io.gravitee.exchange.api.command.CommandAdapter;
import io.gravitee.exchange.api.command.CommandHandler;
import io.gravitee.exchange.api.command.CommandStatus;
import io.gravitee.exchange.api.command.Payload;
import io.gravitee.exchange.api.command.Reply;
import io.gravitee.exchange.api.command.ReplyAdapter;
import io.gravitee.exchange.api.command.goodbye.GoodByeCommand;
import io.gravitee.exchange.api.command.goodbye.GoodByeCommandPayload;
import io.gravitee.exchange.api.command.hello.HelloCommand;
import io.gravitee.exchange.api.command.hello.HelloReply;
import io.gravitee.exchange.api.command.hello.HelloReplyPayload;
import io.gravitee.exchange.api.command.noreply.NoReply;
import io.gravitee.exchange.api.command.unknown.UnknownCommandHandler;
import io.gravitee.exchange.api.command.unknown.UnknownReply;
import io.gravitee.exchange.api.websocket.protocol.ProtocolAdapter;
import io.gravitee.exchange.api.websocket.protocol.ProtocolExchange;
import io.gravitee.exchange.api.websocket.protocol.legacy.ignored.IgnoredReply;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.CompletableEmitter;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.core.SingleEmitter;
import io.vertx.rxjava3.core.Vertx;
import io.vertx.rxjava3.core.buffer.Buffer;
import io.vertx.rxjava3.core.http.WebSocketBase;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
/**
* @author Guillaume LAMIRAND (guillaume.lamirand at graviteesource.com)
* @author GraviteeSource Team
*/
@Slf4j
public abstract class AbstractWebSocketChannel implements Channel {
private static final int PING_DELAY = 5_000;
protected final String id = UUID.randomUUID().toString();
protected final Map, ? extends Reply>>> commandHandlers = new ConcurrentHashMap<>();
protected final Map, ? extends Command>, ? extends Reply>>> commandAdapters =
new ConcurrentHashMap<>();
protected final Map, ? extends Reply>>> replyAdapters = new ConcurrentHashMap<>();
protected final Vertx vertx;
protected final WebSocketBase webSocket;
protected final ProtocolAdapter protocolAdapter;
protected String targetId;
protected final Map>> resultEmitters = new ConcurrentHashMap<>();
protected boolean active;
private long pingTaskId = -1;
protected AbstractWebSocketChannel(
final List, ? extends Reply>>> commandHandlers,
final List, ? extends Command>, ? extends Reply>>> commandAdapters,
final List, ? extends Reply>>> replyAdapters,
final Vertx vertx,
final WebSocketBase webSocket,
final ProtocolAdapter protocolAdapter
) {
this.addCommandHandlers(commandHandlers);
this.addCommandHandlers(List.of(new UnknownCommandHandler()));
this.addCommandHandlers(protocolAdapter.commandHandlers());
this.addCommandAdapters(commandAdapters);
this.addCommandAdapters(protocolAdapter.commandAdapters());
this.addReplyAdapters(replyAdapters);
this.addReplyAdapters(protocolAdapter.replyAdapters());
this.vertx = vertx;
this.webSocket = webSocket;
this.protocolAdapter = protocolAdapter;
}
@Override
public String id() {
return id;
}
@Override
public String targetId() {
return targetId;
}
@Override
public boolean isActive() {
return this.active;
}
@Override
public boolean hasPendingCommands() {
return !resultEmitters.isEmpty();
}
@Override
public Completable initialize() {
return Completable
.create(emitter -> {
webSocket.closeHandler(v -> {
log.warn("Channel '{}' for target '{}' is closing", id, targetId);
active = false;
cleanChannel();
});
webSocket.pongHandler(buffer -> log.debug("Receiving pong frame from channel '{}' for target '{}'", id, targetId));
webSocket.textMessageHandler(buffer -> webSocket.close((short) 1003, "Unsupported text frame").subscribe());
webSocket.binaryMessageHandler(buffer -> {
if (buffer.length() > 0) {
ProtocolExchange websocketExchange = protocolAdapter.read(buffer);
try {
if (ProtocolExchange.Type.COMMAND == websocketExchange.type()) {
receiveCommand(emitter, websocketExchange.asCommand());
} else if (ProtocolExchange.Type.REPLY == websocketExchange.type()) {
receiveReply(websocketExchange.asReply());
} else {
webSocket.close((short) 1002, "Exchange message unknown").subscribe();
}
} catch (Exception e) {
log.warn(
String.format(
"An error occurred when trying to decode incoming websocket exchange [%s]. Closing Socket.",
websocketExchange
),
e
);
webSocket.close((short) 1011, "Unexpected error while handling incoming websocket exchange").subscribe();
}
}
});
if (!expectHelloCommand()) {
this.active = true;
emitter.onComplete();
}
})
.doOnComplete(() -> log.debug("Channel '{}' for target '{}' has been successfully initialized", id, targetId))
.doOnError(throwable -> log.error("Unable to initialize channel '{}' for target '{}'", id, targetId));
}
private > void receiveCommand(final CompletableEmitter emitter, final C command) {
if (command == null) {
webSocket.close((short) 1002, "Unrecognized incoming exchange").subscribe();
emitter.onError(new ChannelUnknownCommandException("Unrecognized incoming exchange"));
return;
}
Single extends Command>> commandObs;
CommandAdapter, Command>, Reply>> commandAdapter =
(CommandAdapter, Command>, Reply>>) commandAdapters.get(command.getType());
if (commandAdapter != null) {
commandObs = commandAdapter.adapt(targetId, command);
} else {
commandObs = Single.just(command);
}
commandObs
.flatMapCompletable(adaptedCommand -> {
CommandHandler, Reply>> commandHandler = (CommandHandler, Reply>>) commandHandlers.get(
adaptedCommand.getType()
);
if (expectHelloCommand() && !active && !Objects.equals(adaptedCommand.getType(), HelloCommand.COMMAND_TYPE)) {
webSocket.close((short) 1002, "Hello Command is first expected to initialize the exchange channel").subscribe();
emitter.onError(new ChannelInitializationException("Hello Command is first expected to initialize the channel"));
} else if (Objects.equals(adaptedCommand.getType(), HelloCommand.COMMAND_TYPE)) {
return handleHelloCommand(emitter, adaptedCommand, commandHandler);
} else if (Objects.equals(adaptedCommand.getType(), GoodByeCommand.COMMAND_TYPE)) {
return handleGoodByeCommand(adaptedCommand, commandHandler);
} else if (commandHandler != null) {
return handleCommandAsync(adaptedCommand, commandHandler);
} else {
log.info("No handler found for command type {}. Ignoring", adaptedCommand.getType());
return writeReply(
new NoReply(
adaptedCommand.getId(),
"No handler found for command type %s. Ignoring".formatted(adaptedCommand.getType())
)
);
}
return Completable.complete();
})
.onErrorResumeNext(throwable -> {
log.warn("Unexpected internal error occurred when handling command type %s".formatted(command.getType()), throwable);
return writeReply(new NoReply(command.getId(), "Unexpected internal error occurred"));
})
.subscribe();
}
protected abstract boolean expectHelloCommand();
private void receiveReply(final Reply> reply) {
SingleEmitter extends Reply>> replyEmitter = resultEmitters.remove(reply.getCommandId());
if (replyEmitter != null) {
Single extends Reply>> replyObs;
ReplyAdapter, Reply>> replyAdapter = (ReplyAdapter, Reply>>) replyAdapters.get(reply.getType());
if (replyAdapter != null) {
replyObs = replyAdapter.adapt(targetId, reply);
} else {
replyObs = Single.just(reply);
}
replyObs
.doOnSuccess(adaptedReply -> {
if (adaptedReply instanceof UnknownReply) {
replyEmitter.onError(new ChannelUnknownCommandException(adaptedReply.getErrorDetails()));
} else if (adaptedReply instanceof NoReply || adaptedReply instanceof IgnoredReply) {
replyEmitter.onError(new ChannelNoReplyException(adaptedReply.getErrorDetails()));
} else {
((SingleEmitter>) replyEmitter).onSuccess(adaptedReply);
}
if (adaptedReply.stopOnErrorStatus() && adaptedReply.getCommandStatus() == ERROR) {
webSocket.close().subscribe();
}
})
.doOnError(throwable -> {
log.warn("Unable to handle reply [{}, {}]", reply.getType(), reply.getCommandId());
replyEmitter.onError(new ChannelReplyException(throwable));
})
.subscribe();
}
}
@Override
public Completable close() {
return Completable.fromRunnable(() -> {
webSocket.close((short) 1000).subscribe();
this.cleanChannel();
});
}
protected void cleanChannel() {
this.active = false;
this.resultEmitters.forEach((type, emitter) -> {
if (!emitter.isDisposed()) {
emitter.onError(new ChannelClosedException());
}
});
this.resultEmitters.clear();
if (pingTaskId != -1) {
this.vertx.cancelTimer(this.pingTaskId);
this.pingTaskId = -1;
}
if (webSocket != null && !webSocket.isClosed()) {
this.webSocket.close((short) 1011).subscribe();
}
}
/**
* Method call to handle initialize command type
*/
protected Completable handleHelloCommand(
final CompletableEmitter emitter,
final Command> command,
final CommandHandler, Reply>> commandHandler
) {
if (commandHandler != null) {
return handleCommand(command, commandHandler, false)
.doOnSuccess(reply -> {
if (reply.getCommandStatus() == CommandStatus.SUCCEEDED) {
Payload payload = reply.getPayload();
if (payload instanceof HelloReplyPayload helloReplyPayload) {
this.targetId = helloReplyPayload.getTargetId();
this.active = true;
startPingTask();
emitter.onComplete();
} else {
emitter.onError(new ChannelInitializationException("Unable to parse hello reply payload"));
}
}
})
.ignoreElement();
} else {
return Completable.fromRunnable(() -> {
startPingTask();
emitter.onComplete();
});
}
}
private void startPingTask() {
this.pingTaskId =
this.vertx.setPeriodic(
PING_DELAY,
timerId -> {
if (!this.webSocket.isClosed()) {
this.webSocket.writePing(Buffer.buffer()).subscribe();
}
}
);
}
/**
* Method call to handle custom command type
*/
protected Completable handleGoodByeCommand(final Command> command, final CommandHandler, Reply>> commandHandler) {
if (commandHandler != null) {
return handleCommand(command, commandHandler, true)
.doOnSuccess(reply -> {
if (reply.getCommandStatus() == CommandStatus.SUCCEEDED) {
Payload payload = command.getPayload();
if (payload instanceof GoodByeCommandPayload goodByeCommandPayload && goodByeCommandPayload.isReconnect()) {
webSocket.close((short) 1013, "GoodBye Command with reconnection requested.").subscribe();
} else {
webSocket.close((short) 1000, "GoodBye Command without reconnection.").subscribe();
}
}
})
.doFinally(this::cleanChannel)
.ignoreElement();
} else {
return Completable.fromRunnable(() -> {
webSocket.close((short) 1013).subscribe();
this.cleanChannel();
});
}
}
protected Completable handleCommandAsync(final Command> command, final CommandHandler, Reply>> commandHandler) {
return handleCommand(command, commandHandler, false).ignoreElement();
}
protected Single> handleCommand(
final Command> command,
final CommandHandler, Reply>> commandHandler,
boolean dontReply
) {
return commandHandler
.handle(command)
.flatMap(reply -> {
if (!dontReply) {
return writeReply(reply).andThen(Single.just(reply));
}
return Single.just(reply);
})
.doOnError(throwable -> {
log.warn("Unable to handle command [{}, {}]", command.getType(), command.getId());
webSocket.close((short) 1011, "Unexpected error").subscribe();
});
}
@Override
public , R extends Reply>> Single send(final C command) {
return send(command, false);
}
protected Single sendHelloCommand(final HelloCommand helloCommand) {
return send(helloCommand, true);
}
protected , R extends Reply>> Single send(final C command, final boolean ignoreActiveStatus) {
return Single
.defer(() -> {
if (!ignoreActiveStatus && !active) {
return Single.error(new ChannelInactiveException());
}
CommandAdapter, R> commandAdapter = (CommandAdapter, R>) commandAdapters.get(command.getType());
if (commandAdapter != null) {
return commandAdapter.adapt(targetId, command);
} else {
return Single.just(command);
}
})
.flatMap(adaptedCommand ->
Single
.create(emitter -> {
resultEmitters.put(adaptedCommand.getId(), emitter);
writeCommand(adaptedCommand).doOnError(emitter::onError).onErrorComplete().subscribe();
})
.timeout(
adaptedCommand.getReplyTimeoutMs(),
TimeUnit.MILLISECONDS,
Single.error(() -> {
if (adaptedCommand.getReplyTimeoutMs() > 0) {
log.warn(
"No reply received in time for command [{}, {}]",
adaptedCommand.getType(),
adaptedCommand.getId()
);
}
throw new ChannelTimeoutException();
})
)
)
.onErrorResumeNext(throwable -> {
CommandAdapter, R> commandAdapter = (CommandAdapter, R>) commandAdapters.get(command.getType());
if (commandAdapter != null) {
return commandAdapter.onError(command, throwable);
} else {
return Single.error(throwable);
}
})
// Cleanup result emitters list if cancelled by the upstream.
.doOnDispose(() -> resultEmitters.remove(command.getId()));
}
protected > Completable writeCommand(C command) {
ProtocolExchange protocolExchange = ProtocolExchange
.builder()
.type(ProtocolExchange.Type.COMMAND)
.exchangeType(command.getType())
.exchange(command)
.build();
return writeToSocket(command.getId(), protocolExchange);
}
protected > Completable writeReply(R reply) {
return Single
.defer(() -> {
ReplyAdapter> replyAdapter = (ReplyAdapter>) replyAdapters.get(reply.getType());
if (replyAdapter != null) {
return replyAdapter.adapt(targetId, reply);
} else {
return Single.just(reply);
}
})
.flatMapCompletable(adaptedReply -> {
ProtocolExchange protocolExchange = ProtocolExchange
.builder()
.type(ProtocolExchange.Type.REPLY)
.exchangeType(adaptedReply.getType())
.exchange(adaptedReply)
.build();
return writeToSocket(adaptedReply.getCommandId(), protocolExchange);
});
}
private Completable writeToSocket(final String commandId, final ProtocolExchange websocketExchange) {
if (!webSocket.isClosed()) {
return webSocket
.writeBinaryMessage(protocolAdapter.write(websocketExchange))
.doOnComplete(() ->
log.debug("Write command/reply [{}, {}] to websocket successfully", websocketExchange.exchangeType(), commandId)
)
.onErrorResumeNext(throwable -> {
log.error("An error occurred when trying to send command/reply [{}, {}]", websocketExchange.exchangeType(), commandId);
return Completable.error(new Exception("Write to socket failed"));
});
} else {
return Completable.error(new ChannelClosedException());
}
}
@Override
public void addCommandHandlers(final List, ? extends Reply>>> commandHandlers) {
if (commandHandlers != null) {
commandHandlers.forEach(commandHandler -> this.commandHandlers.putIfAbsent(commandHandler.supportType(), commandHandler));
}
}
public void addCommandAdapters(
final List, ? extends Command>, ? extends Reply>>> commandAdapters
) {
if (commandAdapters != null) {
commandAdapters.forEach(commandAdapter -> this.commandAdapters.putIfAbsent(commandAdapter.supportType(), commandAdapter));
}
}
public void addReplyAdapters(final List, ? extends Reply>>> replyAdapters) {
if (replyAdapters != null) {
replyAdapters.forEach(replyAdapter -> this.replyAdapters.putIfAbsent(replyAdapter.supportType(), replyAdapter));
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy