package com.saffrontech.vertx; import io.vertx.core.AsyncResult; import io.vertx.core.Handler; import io.vertx.core.MultiMap; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; import io.vertx.core.eventbus.DeliveryOptions; import io.vertx.core.eventbus.Message; import io.vertx.core.http.HttpClientOptions; import io.vertx.core.http.HttpClientRequest; import io.vertx.core.http.WebSocket; import io.vertx.core.json.JsonObject; import java.net.URI; import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** * Simple event bus bridge using Vert.x websockets. * Note: Only Message<JsonObject> and Message<lt;String> is supported * * Created by beders on 6/23/15. */ public class EventBusBridge { Vertx vertx; WebSocket webSocket; long pingTimerID; ConcurrentHashMap<String, List<DefaultHandler<?>>> handlers = new ConcurrentHashMap<>(); ConcurrentHashMap<String, DefaultHandler<?>> replyHandlers = new ConcurrentHashMap<>(); static final int MAX_SOCKET_FRAME_SIZE = 2*(int)Math.pow(2,18); // 512K max payload /** Create an event bus bridge using an absolute URL and default socket frame size (512K). */ public static EventBusBridge connect(URI endPoint, io.vertx.core.Handler<EventBusBridge> onOpenHandler) { return connect(-1, null, endPoint, onOpenHandler, null, null); } /** Create an event bus bridge using an absolute or relative URL with additional options. * Note: If a relative URL is provided, defaultPort and defaultHost from options will be used! * @param endPoint * @param onOpenHandler * @param options http options. If you are connecting through SSL to a server with a self-signed certificate, use setTrustAll and verifyHost options. * @return */ public static EventBusBridge connect(URI endPoint, io.vertx.core.Handler<EventBusBridge> onOpenHandler, HttpClientOptions options) { return connect(-1, null, endPoint, onOpenHandler, options, null); } /** * Provide your own Vertx instance. * @see EventBusBridge#connect(URI, Handler) * */ public static EventBusBridge connect(URI endPoint, io.vertx.core.Handler<EventBusBridge> onOpenHandler, Vertx vertx) { return connect(-1, null, endPoint, onOpenHandler, null, vertx); } /** * Use this static method to connect through a proxy. * @param port port to use * @param host the host to connect to * @param endPoint the actual endpoint. Use an absolute URL when connecting through a proxy. * @param onOpenHandler * @param vertx a vertx instance (optional) * @return */ public static EventBusBridge connect(int port, String host, URI endPoint, io.vertx.core.Handler<EventBusBridge> onOpenHandler, Vertx vertx) { return connect(port, host, endPoint, onOpenHandler, null, vertx); } /** * Use this static method to connect through a proxy. * @param port port to use * @param host the host to connect to * @param endPoint the actual endpoint. Use an absolute URL when connecting through a proxy. * @param onOpenHandler * @param options * @return */ public static EventBusBridge connect(int port, String host, URI endPoint, io.vertx.core.Handler<EventBusBridge> onOpenHandler, HttpClientOptions options) { return connect(port, host, endPoint, onOpenHandler, options, null); } public static EventBusBridge connect(URI endPoint, io.vertx.core.Handler<EventBusBridge> onOpenHandler, int maxSocketFrameSize) { return connect(endPoint, onOpenHandler, maxSocketFrameSize, null); } public static EventBusBridge connect(URI endPoint, io.vertx.core.Handler<EventBusBridge> onOpenHandler, int maxSocketFrameSize, Vertx vertx) { return connect(-1, null, endPoint, onOpenHandler, new HttpClientOptions().setMaxWebsocketFrameSize(maxSocketFrameSize), vertx); } public static EventBusBridge connect(int port, String host, URI endPoint, io.vertx.core.Handler<EventBusBridge> onOpenHandler, HttpClientOptions options, Vertx vertx) { HttpClientOptions actualOptions = options == null ? new HttpClientOptions().setMaxWebsocketFrameSize(MAX_SOCKET_FRAME_SIZE) : options; int actualPort = guessPort(port, endPoint, actualOptions); String actualHost = guessHost(host, endPoint, actualOptions); actualOptions.setSsl(guessSsl(endPoint, options)); return new EventBusBridge(actualPort, actualHost, endPoint, onOpenHandler, actualOptions, Optional.ofNullable(vertx)); } /** Guess port: It is either set explicitly, taken from the absolute URL or taken from the default options */ private static int guessPort(int port, URI endPoint, HttpClientOptions options) { if (port != -1) return port; if (endPoint.isAbsolute()) { if (endPoint.getPort() == -1) { return portFromScheme(endPoint.getScheme()); } else { return endPoint.getPort(); } } else { return options.getDefaultPort(); } } /** Guess host: It is either set explicitly, taken from the absolute URL or taken from the default options */ private static String guessHost(String host, URI endPoint, HttpClientOptions actualOptions) { if (host == null || host.isEmpty()) { if (endPoint.isAbsolute()) { return endPoint.getHost(); } else { return actualOptions.getDefaultHost(); } } else { return host; } } /** Guess SSL: Either taken from scheme or set directly in options */ private static boolean guessSsl(URI endPoint, HttpClientOptions options) { if (endPoint.isAbsolute()) { return endPoint.getScheme().equals("https"); } else { return options.isSsl(); } } private static int portFromScheme(String scheme) { return scheme.equals("https") ? 443 : 80; } private EventBusBridge(int port, String host, URI endPoint, io.vertx.core.Handler<EventBusBridge> onOpenHandler, HttpClientOptions options, Optional<Vertx> aVertx) { vertx = aVertx.orElse(Vertx.vertx()); vertx.createHttpClient(options).websocket(port, host, endPoint.toString() + "/websocket", ws -> { webSocket = ws; ws.handler(this::bufferReceived); ws.closeHandler(it -> { if (pingTimerID != 0) { vertx.cancelTimer(pingTimerID); } handlers.clear(); replyHandlers.clear(); }); onOpenHandler.handle(EventBusBridge.this); sendPing(); pingTimerID = vertx.setPeriodic(5000L, time -> sendPing()); }); } protected void sendPing() { //System.out.println("Sending ping"); if (webSocket != null) { JsonObject msg = new JsonObject().put("type", "ping"); try { webSocket.write(Buffer.buffer(msg.toString())); } catch (IllegalStateException ise) { vertx.cancelTimer(pingTimerID); } } else if (pingTimerID != 0) { vertx.cancelTimer(pingTimerID); } } public EventBusBridge send(String address, String message) { Objects.requireNonNull(webSocket); sendMessage("send", address, message, null); return this; } public EventBusBridge publish(String address, String message) { Objects.requireNonNull(webSocket); sendMessage("publish", address, message, null); return this; } public EventBusBridge send(String address, String message, EventHandler<?> replyHandler) { Objects.requireNonNull(webSocket); sendMessage("send", address, message, replyHandler); return this; } public EventBusBridge publish(String address, String message, EventHandler<?> replyHandler) { Objects.requireNonNull(webSocket); sendMessage("publish", address, message, replyHandler); return this; } public EventBusBridge send(String address, String message, MessageHandler<?> replyHandler) { Objects.requireNonNull(webSocket); sendMessage("send", address, message, replyHandler); return this; } public EventBusBridge publish(String address, String message, MessageHandler<?> replyHandler) { Objects.requireNonNull(webSocket); sendMessage("publish", address, message, replyHandler); return this; } public EventBusBridge send(String address, JsonObject message) { Objects.requireNonNull(webSocket); sendMessage("send", address, message, null); return this; } public EventBusBridge publish(String address, JsonObject message) { Objects.requireNonNull(webSocket); sendMessage("publish", address, message, null); return this; } public EventBusBridge send(String address, JsonObject message, EventHandler<?> replyHandler) { Objects.requireNonNull(webSocket); sendMessage("send", address, message, replyHandler); return this; } public EventBusBridge publish(String address, JsonObject message, EventHandler<?> replyHandler) { Objects.requireNonNull(webSocket); sendMessage("publish", address, message, replyHandler); return this; } public EventBusBridge send(String address, JsonObject message, MessageHandler<?> replyHandler) { Objects.requireNonNull(webSocket); sendMessage("send", address, message, replyHandler); return this; } public EventBusBridge publish(String address, JsonObject message, MessageHandler<?> replyHandler) { Objects.requireNonNull(webSocket); sendMessage("publish", address, message, replyHandler); return this; } public EventBusBridge registerHandler(String address, MessageHandler<?> messageHandler) { return registerHandlerInternal(address, messageHandler); } public EventBusBridge registerHandler(String address, EventHandler<?> eventHandler) { return registerHandlerInternal(address, eventHandler); } protected EventBusBridge registerHandlerInternal(String address, DefaultHandler<?> eventHandler) { handlers.computeIfAbsent(address, key -> { webSocket.write(Buffer.buffer(new JsonObject().put("type", "register").put("address", address).toString())); return Collections.synchronizedList(new ArrayList<>()); }).add(eventHandler); return this; } public EventBusBridge unregisterHandler(String address, MessageHandler<?> messageHandler) { return unregisterHandlerInternal(address, messageHandler); } public EventBusBridge unregisterHandler(String address, EventHandler<?> eventHandler) { return unregisterHandlerInternal(address, eventHandler); } protected EventBusBridge unregisterHandlerInternal(String address, DefaultHandler<?> eventHandler) { List<DefaultHandler<?>> handlers = this.handlers.getOrDefault(address, Collections.emptyList()); handlers.remove(eventHandler); if (handlers.isEmpty()) { String unregisterMsg = new JsonObject().put("type","unregister").put("address", address).toString(); webSocket.write(Buffer.buffer(unregisterMsg)); } return this; } public void close() { if (webSocket != null) { webSocket.close(); webSocket = null; } } private void sendMessage(String sendOrPublish, String address, Object message, DefaultHandler<?> replyHandler) { JsonObject msg = new JsonObject().put("type", sendOrPublish).put("address", address).put("body", message); if (replyHandler != null) { String replyAddress = UUID.randomUUID().toString(); replyHandlers.put(replyAddress, replyHandler); msg.put("replyAddress", replyAddress); } webSocket.write(Buffer.buffer(msg.toString())); } protected void bufferReceived(Buffer buffer) { //System.out.println("Buffer Received"); //System.out.println(buffer.toString()); JsonObject msg = new JsonObject(buffer.toString()); String type = msg.getString("type"); if ("err".equals(type)) { // TODO invoke error handler System.err.println("Error message from the event bus bridge:" + msg.toString()); return; } String address = msg.getString("address"); EventBusMessage result = new EventBusMessage(msg); for (DefaultHandler<?> h : handlers.getOrDefault(address, Collections.emptyList())) { result.deliverTo(h); } if (replyHandlers.containsKey(address)) { DefaultHandler<?> replyHandler = replyHandlers.remove(address); result.deliverTo(replyHandler); } } public boolean isOpen() { return webSocket != null; } public class EventBusMessage<T> implements Message<T> { String address; String replyAddress; T body; DefaultHandler<T> handler; EventBusMessage(JsonObject json) { address = json.getString("address"); replyAddress = json.getString("replyAddress", null); body = (T)json.getValue("body"); } EventBusMessage(Message<T> result) { address = result.address(); replyAddress = result.replyAddress(); body = result.body(); } @Override public String address() { return null; } @Override public MultiMap headers() { throw new UnsupportedOperationException("No headers supported"); } @Override public T body() { return body; } @Override public String replyAddress() { return replyAddress; } @Override public void reply(Object message) { this.reply(message, new DeliveryOptions(), null); } @Override public <R> void reply(Object message, Handler<AsyncResult<Message<R>>> replyHandler) { this.reply(message, new DeliveryOptions(), replyHandler); } @Override public void reply(Object message, DeliveryOptions options) { this.reply(message, options, null); } @Override public <R> void reply(Object message, DeliveryOptions deliveryOptions, Handler<AsyncResult<Message<R>>> replyHandler) { if (this.replyAddress != null) { EventBusBridge.this.send(replyAddress, message.toString(), (result,eb) -> { if (replyHandler != null) { replyHandler.handle(new AsyncResult<Message<R>>() { @Override public Message<R> result() { return new EventBusMessage(result); } @Override public Throwable cause() { return null; } @Override public boolean succeeded() { return true; } @Override public boolean failed() { return false; } }); } }); } } @Override public void fail(int i, String s) { throw new UnsupportedOperationException("Failure is not an option"); } public void unregister() { if (handler != null) { handler.unregister(address, EventBusBridge.this); } } private void deliverTo(DefaultHandler<T> handler) { this.handler = handler; // give handler a chance to un-register handler.invoke(this, EventBusBridge.this); } public Message<JsonObject> asJson() { return (Message<JsonObject>) this; } } }