package io.github.centrifugal.centrifuge; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.ByteArrayOutputStream; import java.io.InputStream; import okhttp3.Headers; import okhttp3.OkHttpClient; import okhttp3.WebSocket; import okhttp3.Request; import okhttp3.Response; import okhttp3.WebSocketListener; import okio.ByteString; import io.github.centrifugal.centrifuge.internal.backoff.Backoff; import io.github.centrifugal.centrifuge.internal.protocol.Protocol; import com.google.gson.JsonParseException; import com.google.gson.JsonParser; import com.google.gson.JsonObject; import com.google.protobuf.InvalidProtocolBufferException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java8.util.concurrent.CompletableFuture; public class Client { private WebSocket ws; private String url; Options getOpts() { return opts; } private Options opts; private String token = ""; private EventListener listener; private String client; private Map<Integer, CompletableFuture<Protocol.Reply>> futures = new ConcurrentHashMap<>(); private Map<Integer, Protocol.Command> connectCommands = new ConcurrentHashMap<>(); private Map<Integer, Protocol.Command> connectAsyncCommands = new ConcurrentHashMap<>(); ConnectionState getState() { return state; } private ConnectionState state = ConnectionState.NEW; private final Map<String, Subscription> subs = new ConcurrentHashMap<>(); private Boolean connecting = false; private Boolean disconnecting = false; private Backoff backoff; private Boolean needReconnect = true; ExecutorService getExecutor() { return executor; } private ExecutorService executor = Executors.newSingleThreadExecutor(); private ExecutorService reconnectExecutor = Executors.newSingleThreadExecutor(); private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); private ScheduledFuture pingTask; private ScheduledFuture refreshTask; private String disconnectReason = ""; private static final int NORMAL_CLOSURE_STATUS = 1000; /** * Set connection JWT. This is a token you have to receive from your application backend. * @param token */ public void setToken(String token) { this.executor.submit(() -> { Client.this.token = token; }); } /** * Creates a new instance of Client. Client allows to allocate new Subscriptions to channels, * automatically manages reconnects. * @param url * @param opts * @param listener */ public Client(final String url, final Options opts, final EventListener listener) { this.url = url; this.opts = opts; this.listener = listener; this.backoff = new Backoff(); } private int _id = 0; private int getNextId() { return ++_id; } public void connect() { this.executor.submit(() -> { if (Client.this.state == ConnectionState.CONNECTED || Client.this.connecting) { return; } Client.this._connect(); }); } private void _connect() { this.connecting = true; Headers.Builder headers = new Headers.Builder(); if (this.opts.getHeaders() != null) { for (Map.Entry<String,String> entry : this.opts.getHeaders().entrySet()) { headers.add(entry.getKey(), entry.getValue()); } } Request request = new Request.Builder() .url(this.url) .headers(headers.build()) .build(); if (this.ws != null) { this.ws.cancel(); } this.ws = (new OkHttpClient()).newWebSocket(request, new WebSocketListener() { @Override public void onOpen(WebSocket webSocket, Response response) { super.onOpen(webSocket, response); Client.this.executor.submit(Client.this::handleConnectionOpen); } @Override public void onMessage(WebSocket webSocket, ByteString bytes) { super.onMessage(webSocket, bytes); Client.this.executor.submit(() -> Client.this.handleConnectionMessage(bytes.toByteArray())); } @Override public void onClosing(WebSocket webSocket, int code, String reason) { super.onClosing(webSocket, code, reason); webSocket.close(NORMAL_CLOSURE_STATUS, null); System.out.println("Closing : " + code + " / " + reason); } @Override public void onClosed(WebSocket webSocket, int code, String reason) { super.onClosed(webSocket, code, reason); Client.this.executor.submit(() -> { /* TODO: refactor this. */ if (!reason.equals("")) { try { JsonObject jsonObject = new JsonParser().parse(reason).getAsJsonObject(); String disconnectReason = jsonObject.get("reason").getAsString(); Boolean shouldReconnect = jsonObject.get("reconnect").getAsBoolean(); Client.this.handleConnectionClose(disconnectReason, shouldReconnect); return; } catch (JsonParseException e) { Client.this.handleConnectionClose("connection closed", true); } } if (!Client.this.disconnectReason.equals("")) { JsonObject jsonObject = new JsonParser().parse(Client.this.disconnectReason).getAsJsonObject(); String disconnectReason = jsonObject.get("reason").getAsString(); Boolean shouldReconnect = jsonObject.get("reconnect").getAsBoolean(); Client.this.disconnectReason = ""; Client.this.handleConnectionClose(disconnectReason, shouldReconnect); return; } Client.this.handleConnectionClose("connection closed", true); }); } @Override public void onFailure(WebSocket webSocket, Throwable t, Response response) { super.onFailure(webSocket, t, response); Client.this.executor.submit(Client.this::handleConnectionError); } }); } private void handleConnectionOpen() { this.sendConnect(); } private void handleConnectionMessage(byte[] bytes) { if (this.disconnecting) { return; } InputStream stream = new ByteArrayInputStream(bytes); try { while (stream.available() > 0) { Protocol.Reply reply = Protocol.Reply.parseDelimitedFrom(stream); this.processReply(reply); } } catch (IOException e) { e.printStackTrace(); } } /** * Disconnect from server and do not reconnect. */ public void disconnect() { this.executor.submit(() -> { String disconnectReason = "{\"reason\": \"clean disconnect\", \"reconnect\": false}"; Client.this._disconnect(disconnectReason, false); }); } private void _disconnect(String disconnectReason, Boolean needReconnect) { this.disconnecting = true; this.needReconnect = needReconnect; this.disconnectReason = disconnectReason; this.ws.close(NORMAL_CLOSURE_STATUS, "cya"); } private void handleConnectionClose(String reason, Boolean shouldReconnect) { this.needReconnect = shouldReconnect; ConnectionState previousState = this.state; if (this.pingTask != null) { this.pingTask.cancel(true); } if (this.refreshTask != null) { this.refreshTask.cancel(true); } this.state = ConnectionState.DISCONNECTED; this.disconnecting = false; synchronized (this.subs) { for (Map.Entry<String, Subscription> entry : this.subs.entrySet()) { Subscription sub = entry.getValue(); SubscriptionState previousSubState = sub.getState(); sub.moveToUnsubscribed(); if (previousSubState == SubscriptionState.SUBSCRIBED) { sub.getListener().onUnsubscribe(sub, new UnsubscribeEvent()); } } } if (previousState != ConnectionState.DISCONNECTED) { DisconnectEvent event = new DisconnectEvent(); event.setReason(reason); event.setReconnect(shouldReconnect); for(Map.Entry<Integer, CompletableFuture<Protocol.Reply>> entry: this.futures.entrySet()) { CompletableFuture f = entry.getValue(); f.completeExceptionally(new IOException()); } this.listener.onDisconnect(this, event); } if (this.needReconnect) { this.scheduleReconnect(); } } private void handleConnectionError() { this.listener.onError(this, new ErrorEvent()); this.handleConnectionClose("connection error", true); } private void scheduleReconnect() { this.reconnectExecutor.submit(() -> { try { Thread.sleep(Client.this.backoff.duration()); } catch(InterruptedException ex) { Thread.currentThread().interrupt(); } Client.this.executor.submit(() -> { if (!Client.this.needReconnect) { return; } Client.this._connect(); }); }); } private void sendSubscribeSynchronized(String channel, String token) { Protocol.SubscribeRequest req = Protocol.SubscribeRequest.newBuilder() .setChannel(channel) .setToken(token) .build(); Protocol.Command cmd = Protocol.Command.newBuilder() .setId(this.getNextId()) .setMethod(Protocol.MethodType.SUBSCRIBE) .setParams(req.toByteString()) .build(); CompletableFuture<Protocol.Reply> f = new CompletableFuture<>(); this.futures.put(cmd.getId(), f); f.thenAccept(reply -> { this.handleSubscribeReply(channel, reply); this.futures.remove(cmd.getId()); }).orTimeout(this.opts.getTimeout(), TimeUnit.MILLISECONDS).exceptionally(e -> { this.executor.submit(() -> { Client.this.futures.remove(cmd.getId()); String disconnectReason = "{\"reason\": \"timeout\", \"reconnect\": true}"; Client.this._disconnect(disconnectReason, true); }); return null; }); this.ws.send(ByteString.of(this.serializeCommand(cmd))); } private void sendSubscribe(Subscription sub) { String channel = sub.getChannel(); if (sub.getChannel().startsWith(this.opts.getPrivateChannelPrefix())) { PrivateSubEvent privateSubEvent = new PrivateSubEvent(); privateSubEvent.setChannel(sub.getChannel()); privateSubEvent.setClient(this.client); this.listener.onPrivateSub(this, privateSubEvent, new TokenCallback() { @Override public void Fail(Throwable e) { Client.this.executor.submit(() -> { if (!Client.this.client.equals(privateSubEvent.getClient())) { return; } String disconnectReason = "{\"reason\": \"private subscribe error\", \"reconnect\": true}"; Client.this._disconnect(disconnectReason, true); }); } @Override public void Done(String token) { if (Client.this.state != ConnectionState.CONNECTED) { return; } Client.this.sendSubscribeSynchronized(channel, token); } }); } else { this.sendSubscribeSynchronized(channel, ""); } } void sendUnsubscribe(Subscription sub) { this.executor.submit(() -> Client.this.sendUnsubscribeSynchronized(sub)); } private void sendUnsubscribeSynchronized(Subscription sub) { String channel = sub.getChannel(); Protocol.UnsubscribeRequest req = Protocol.UnsubscribeRequest.newBuilder() .setChannel(channel) .build(); Protocol.Command cmd = Protocol.Command.newBuilder() .setId(this.getNextId()) .setMethod(Protocol.MethodType.UNSUBSCRIBE) .setParams(req.toByteString()) .build(); CompletableFuture<Protocol.Reply> f = new CompletableFuture<>(); this.futures.put(cmd.getId(), f); f.thenAccept(reply -> { this.handleUnsubscribeReply(channel, reply); this.futures.remove(cmd.getId()); }).orTimeout(this.opts.getTimeout(), TimeUnit.MILLISECONDS).exceptionally(e -> { this.futures.remove(cmd.getId()); e.printStackTrace(); return null; }); this.ws.send(ByteString.of(this.serializeCommand(cmd))); } private byte[] serializeCommand(Protocol.Command cmd) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); try { cmd.writeDelimitedTo(stream); } catch (IOException e) { e.printStackTrace(); } return stream.toByteArray(); } private Subscription getSub(String channel) { return this.subs.get(channel); } /** * Create new subscription to channel with certain SubscriptionEventListener * @param channel * @param listener * @return * @throws DuplicateSubscriptionException */ public Subscription newSubscription(String channel, SubscriptionEventListener listener) throws DuplicateSubscriptionException { Subscription sub; synchronized (this.subs) { if (this.subs.get(channel) != null) { throw new DuplicateSubscriptionException(); } sub = new Subscription(Client.this, channel, listener); this.subs.put(channel, sub); } return sub; } /** * Try to get Subscription from internal client registry. Can return null if Subscription * does not exist yet. * @param channel * @return */ public Subscription getSubscription(String channel) { Subscription sub; synchronized (this.subs) { sub = this.getSub(channel); } return sub; } /** * Say Client that Subscription should be removed from internal registry. Subscription will be * automatically unsubscribed before removing. * @param sub */ public void removeSubscription(Subscription sub) { synchronized (this.subs) { sub.unsubscribe(); if (this.subs.get(sub.getChannel()) != null) { this.subs.remove(sub.getChannel()); } } } void subscribe(Subscription sub) { this.executor.submit(() -> { if (Client.this.state != ConnectionState.CONNECTED) { // Subscription registered and will start subscribing as soon as // client will be connected. return; } Client.this.sendSubscribe(sub); }); } private void handleSubscribeReply(String channel, Protocol.Reply reply) { Subscription sub = this.getSub(channel); if (reply.getError().getCode() != 0) { if (sub != null) { ReplyError err = new ReplyError(); err.setCode(reply.getError().getCode()); err.setMessage(reply.getError().getMessage()); sub.moveToSubscribeError(err); } return; } try { if (sub != null) { Protocol.SubscribeResult result = Protocol.SubscribeResult.parseFrom(reply.getResult().toByteArray()); sub.moveToSubscribeSuccess(result); } } catch (InvalidProtocolBufferException e) { e.printStackTrace(); } } private void handleUnsubscribeReply(String channel, Protocol.Reply reply) { } private void _sendPing() { if (this.state != ConnectionState.CONNECTED) { return; } Protocol.PingRequest req = Protocol.PingRequest.newBuilder().build(); Protocol.Command cmd = Protocol.Command.newBuilder() .setId(this.getNextId()) .setMethod(Protocol.MethodType.PING) .setParams(req.toByteString()) .build(); CompletableFuture<Protocol.Reply> f = new CompletableFuture<>(); this.futures.put(cmd.getId(), f); f.thenAccept(reply -> { this.futures.remove(cmd.getId()); }).orTimeout(this.opts.getTimeout(), TimeUnit.MILLISECONDS).exceptionally(e -> { this.executor.submit(() -> { Client.this.futures.remove(cmd.getId()); String disconnectReason = "{\"reason\": \"no ping\", \"reconnect\": true}"; Client.this._disconnect(disconnectReason, true); }); return null; }); boolean sent = this.ws.send(ByteString.of(this.serializeCommand(cmd))); if (!sent) { f.completeExceptionally(new IOException()); } } private void sendPing() { this.executor.submit(Client.this::_sendPing); } private void handleConnectReply(Protocol.Reply reply) { if (reply.getError().getCode() != 0) { // TODO: handle error. return; } try { Protocol.ConnectResult result = Protocol.ConnectResult.parseFrom(reply.getResult().toByteArray()); ConnectEvent event = new ConnectEvent(); event.setClient(result.getClient()); event.setData(result.getData().toByteArray()); this.state = ConnectionState.CONNECTED; this.connecting = false; this.client = result.getClient(); this.listener.onConnect(this, event); synchronized (this.subs) { for (Map.Entry<String, Subscription> entry : this.subs.entrySet()) { Subscription sub = entry.getValue(); if (sub.getNeedResubscribe()) { this.sendSubscribe(sub); } } } this.backoff.reset(); for(Map.Entry<Integer, Protocol.Command> entry: this.connectCommands.entrySet()) { // TODO: send in one frame? Protocol.Command cmd = entry.getValue(); boolean sent = this.ws.send(ByteString.of(this.serializeCommand(cmd))); if (!sent) { CompletableFuture<Protocol.Reply> f = this.futures.get(cmd.getId()); if (f != null) { f.completeExceptionally(new IOException()); } } } this.connectCommands.clear(); for(Map.Entry<Integer, Protocol.Command> entry: this.connectAsyncCommands.entrySet()) { // TODO: send in one frame? Protocol.Command cmd = entry.getValue(); CompletableFuture<Protocol.Reply> f = this.futures.get(cmd.getId()); boolean sent = this.ws.send(ByteString.of(this.serializeCommand(cmd))); if (!sent) { if (f != null) { f.completeExceptionally(new IOException()); } } else { if (f != null) { f.complete(null); } } } this.connectAsyncCommands.clear(); this.pingTask = this.scheduler.scheduleAtFixedRate(Client.this::sendPing, this.opts.getPingInterval(), this.opts.getPingInterval(), TimeUnit.MILLISECONDS); if (result.getExpires()) { int ttl = result.getTtl(); this.refreshTask = this.scheduler.schedule(Client.this::sendRefresh, ttl, TimeUnit.SECONDS); } } catch (InvalidProtocolBufferException e) { e.printStackTrace(); } } private void sendRefresh() { this.executor.submit(() -> { Client.this.listener.onRefresh(Client.this, new RefreshEvent(), new TokenCallback() { @Override public void Fail(Throwable e) { // TODO: handle error. } @Override public void Done(String token) { Client.this.executor.submit(() -> { if (token.equals("")) { return; } if (Client.this.state != ConnectionState.CONNECTED) { return; } refreshSynchronized(token, new ReplyCallback<Protocol.RefreshResult>() { @Override public void onFailure(Throwable e) { // TODO: handle error. } @Override public void onDone(ReplyError error, Protocol.RefreshResult result) { if (error != null) { // TODO: handle error. return; } if (result.getExpires()) { int ttl = result.getTtl(); Client.this.refreshTask = Client.this.scheduler.schedule(Client.this::sendRefresh, ttl, TimeUnit.SECONDS); } } }); }); } }); }); } private void sendConnect() { Protocol.ConnectRequest req = Protocol.ConnectRequest.newBuilder() .setToken(this.token) .build(); Protocol.Command cmd = Protocol.Command.newBuilder() .setId(this.getNextId()) .setMethod(Protocol.MethodType.CONNECT) .setParams(req.toByteString()) .build(); CompletableFuture<Protocol.Reply> f = new CompletableFuture<>(); this.futures.put(cmd.getId(), f); f.thenAccept(reply -> { this.handleConnectReply(reply); this.futures.remove(cmd.getId()); }).orTimeout(this.opts.getTimeout(), TimeUnit.MILLISECONDS).exceptionally(e -> { this.futures.remove(cmd.getId()); String disconnectReason = "{\"reason\": \"connect error\", \"reconnect\": true}"; Client.this._disconnect(disconnectReason, true); return null; }); this.ws.send(ByteString.of(this.serializeCommand(cmd))); } private void processReply(Protocol.Reply reply) { if (reply.getId() > 0) { CompletableFuture<Protocol.Reply> cf = this.futures.get(reply.getId()); if (cf != null) cf.complete(reply); } else { this.handleAsyncReply(reply); } } private void handleAsyncReply(Protocol.Reply reply) { try { Protocol.Push push = Protocol.Push.parseFrom(reply.getResult()); String channel = push.getChannel(); if (push.getType() == Protocol.PushType.PUBLICATION) { Protocol.Publication pub = Protocol.Publication.parseFrom(push.getData()); Subscription sub = this.getSub(channel); if (sub != null) { PublishEvent event = new PublishEvent(); event.setData(pub.getData().toByteArray()); sub.getListener().onPublish(sub, event); } } else if (push.getType() == Protocol.PushType.JOIN) { Protocol.Join join = Protocol.Join.parseFrom(push.getData()); Subscription sub = this.getSub(channel); if (sub != null) { JoinEvent event = new JoinEvent(); ClientInfo info = new ClientInfo(); info.setClient(join.getInfo().getClient()); info.setUser(join.getInfo().getUser()); info.setConnInfo(join.getInfo().getConnInfo().toByteArray()); info.setChanInfo(join.getInfo().getChanInfo().toByteArray()); event.setInfo(info); sub.getListener().onJoin(sub, event); } } else if (push.getType() == Protocol.PushType.LEAVE) { Protocol.Leave leave = Protocol.Leave.parseFrom(push.getData()); Subscription sub = this.getSub(channel); if (sub != null) { LeaveEvent event = new LeaveEvent(); ClientInfo info = new ClientInfo(); info.setClient(leave.getInfo().getClient()); info.setUser(leave.getInfo().getUser()); info.setConnInfo(leave.getInfo().getConnInfo().toByteArray()); info.setChanInfo(leave.getInfo().getChanInfo().toByteArray()); event.setInfo(info); sub.getListener().onLeave(sub, event); } } else if (push.getType() == Protocol.PushType.UNSUB) { Protocol.Unsub.parseFrom(push.getData()); Subscription sub = this.getSub(channel); if (sub != null) { sub.unsubscribeNoResubscribe(); } } else if (push.getType() == Protocol.PushType.MESSAGE) { Protocol.Message msg = Protocol.Message.parseFrom(push.getData()); MessageEvent event = new MessageEvent(); event.setData(msg.getData().toByteArray()); this.listener.onMessage(this, event); } } catch (InvalidProtocolBufferException e) { e.printStackTrace(); } } /** * Send asynchronous message with data to server. Callback successfully completes if data * written to connection. No reply from server expected in this case. * @param data * @param cb */ public void send(byte[] data, CompletionCallback cb) { this.executor.submit(() -> Client.this.sendSynchronized(data, cb)); } private void sendSynchronized(byte[] data, CompletionCallback cb) { Protocol.SendRequest req = Protocol.SendRequest.newBuilder() .setData(com.google.protobuf.ByteString.copyFrom(data)) .build(); Protocol.Command cmd = Protocol.Command.newBuilder() .setId(this.getNextId()) .setMethod(Protocol.MethodType.SEND) .setParams(req.toByteString()) .build(); CompletableFuture<Protocol.Reply> f = new CompletableFuture<>(); this.futures.put(cmd.getId(), f); f.thenAccept(reply -> { this.cleanCommandFuture(cmd); cb.onDone(); }).orTimeout(this.opts.getTimeout(), TimeUnit.MILLISECONDS).exceptionally(e -> { this.executor.submit(() -> { this.cleanCommandFuture(cmd); cb.onFailure(e); }); return null; }); if (this.state != ConnectionState.CONNECTED) { this.connectAsyncCommands.put(cmd.getId(), cmd); } else { boolean sent = this.ws.send(ByteString.of(this.serializeCommand(cmd))); if (!sent) { f.completeExceptionally(new IOException()); } else { f.complete(null); } } } private void enqueueCommandFuture(Protocol.Command cmd, CompletableFuture<Protocol.Reply> f) { this.futures.put(cmd.getId(), f); if (this.state != ConnectionState.CONNECTED) { this.connectCommands.put(cmd.getId(), cmd); } else { boolean sent = this.ws.send(ByteString.of(this.serializeCommand(cmd))); if (!sent) { f.completeExceptionally(new IOException()); } } } private ReplyError getReplyError(Protocol.Reply reply) { ReplyError err = new ReplyError(); err.setCode(reply.getError().getCode()); err.setMessage(reply.getError().getMessage()); return err; } private void cleanCommandFuture(Protocol.Command cmd) { this.futures.remove(cmd.getId()); if (this.connectCommands.get(cmd.getId()) != null) { this.connectCommands.remove(cmd.getId()); } if (Client.this.connectAsyncCommands.get(cmd.getId()) != null) { Client.this.connectAsyncCommands.remove(cmd.getId()); } } /** * Send RPC to server, process result in callback. * @param data * @param cb */ public void rpc(byte[] data, ReplyCallback<RPCResult> cb) { this.executor.submit(() -> Client.this.rpcSynchronized(null, data, cb)); } /** * Send RPC with method to server, process result in callback. * @param method * @param data * @param cb */ public void rpc(String method, byte[] data, ReplyCallback<RPCResult> cb) { this.executor.submit(() -> Client.this.rpcSynchronized(method, data, cb)); } private void rpcSynchronized(String method, byte[] data, ReplyCallback<RPCResult> cb) { Protocol.RPCRequest.Builder builder = Protocol.RPCRequest.newBuilder() .setData(com.google.protobuf.ByteString.copyFrom(data)); if(method != null){ builder.setMethod(method); } Protocol.RPCRequest req = builder.build(); Protocol.Command cmd = Protocol.Command.newBuilder() .setId(this.getNextId()) .setMethod(Protocol.MethodType.RPC) .setParams(req.toByteString()) .build(); CompletableFuture<Protocol.Reply> f = new CompletableFuture<>(); f.thenAccept(reply -> { this.cleanCommandFuture(cmd); if (reply.getError().getCode() != 0) { cb.onDone(getReplyError(reply), null); } else { try { Protocol.RPCResult rpcResult = Protocol.RPCResult.parseFrom(reply.getResult().toByteArray()); RPCResult result = new RPCResult(); result.setData(rpcResult.getData().toByteArray()); cb.onDone(null, result); } catch (InvalidProtocolBufferException e) { e.printStackTrace(); } } }).orTimeout(this.opts.getTimeout(), TimeUnit.MILLISECONDS).exceptionally(e -> { this.executor.submit(() -> { Client.this.cleanCommandFuture(cmd); cb.onFailure(e); }); return null; }); this.enqueueCommandFuture(cmd, f); } /** * Publish data to channel without being subscribed to it. Publish option should be * enabled in Centrifuge/Centrifugo server configuration. * @param channel * @param data * @param cb */ public void publish(String channel, byte[] data, ReplyCallback<PublishResult> cb) { this.executor.submit(() -> Client.this.publishSynchronized(channel, data, cb)); } private void publishSynchronized(String channel, byte[] data, ReplyCallback<PublishResult> cb) { Protocol.PublishRequest req = Protocol.PublishRequest.newBuilder() .setChannel(channel) .setData(com.google.protobuf.ByteString.copyFrom(data)) .build(); Protocol.Command cmd = Protocol.Command.newBuilder() .setId(this.getNextId()) .setMethod(Protocol.MethodType.PUBLISH) .setParams(req.toByteString()) .build(); CompletableFuture<Protocol.Reply> f = new CompletableFuture<>(); f.thenAccept(reply -> { this.cleanCommandFuture(cmd); if (reply.getError().getCode() != 0) { cb.onDone(getReplyError(reply), null); } else { PublishResult result = new PublishResult(); cb.onDone(null, result); } }).orTimeout(this.opts.getTimeout(), TimeUnit.MILLISECONDS).exceptionally(e -> { this.executor.submit(() -> { Client.this.cleanCommandFuture(cmd); cb.onFailure(e); }); return null; }); this.enqueueCommandFuture(cmd, f); } void history(String channel, ReplyCallback<HistoryResult> cb) { this.executor.submit(() -> Client.this.historySynchronized(channel, cb)); } private void historySynchronized(String channel, ReplyCallback<HistoryResult> cb) { Protocol.HistoryRequest req = Protocol.HistoryRequest.newBuilder() .setChannel(channel) .build(); Protocol.Command cmd = Protocol.Command.newBuilder() .setId(this.getNextId()) .setMethod(Protocol.MethodType.HISTORY) .setParams(req.toByteString()) .build(); CompletableFuture<Protocol.Reply> f = new CompletableFuture<>(); f.thenAccept(reply -> { this.cleanCommandFuture(cmd); if (reply.getError().getCode() != 0) { cb.onDone(getReplyError(reply), null); } else { try { Protocol.HistoryResult replyResult = Protocol.HistoryResult.parseFrom(reply.getResult().toByteArray()); HistoryResult result = new HistoryResult(); List<Protocol.Publication> protoPubs = replyResult.getPublicationsList(); List<Publication> pubs = new ArrayList<>(); for (int i=0; i<protoPubs.size(); i++) { Protocol.Publication protoPub = protoPubs.get(i); Publication pub = new Publication(); pub.setData(protoPub.getData().toByteArray()); pubs.add(pub); } result.setPublications(pubs); cb.onDone(null, result); } catch (InvalidProtocolBufferException e) { e.printStackTrace(); } } }).orTimeout(this.opts.getTimeout(), TimeUnit.MILLISECONDS).exceptionally(e -> { this.executor.submit(() -> { Client.this.cleanCommandFuture(cmd); cb.onFailure(e); }); return null; }); this.enqueueCommandFuture(cmd, f); } void presence(String channel, ReplyCallback<PresenceResult> cb) { this.executor.submit(() -> Client.this.presenceSynchronized(channel, cb)); } private void presenceSynchronized(String channel, ReplyCallback<PresenceResult> cb) { Protocol.PresenceRequest req = Protocol.PresenceRequest.newBuilder() .setChannel(channel) .build(); Protocol.Command cmd = Protocol.Command.newBuilder() .setId(this.getNextId()) .setMethod(Protocol.MethodType.PRESENCE) .setParams(req.toByteString()) .build(); CompletableFuture<Protocol.Reply> f = new CompletableFuture<>(); f.thenAccept(reply -> { this.cleanCommandFuture(cmd); if (reply.getError().getCode() != 0) { cb.onDone(getReplyError(reply), null); } else { try { Protocol.PresenceResult replyResult = Protocol.PresenceResult.parseFrom(reply.getResult().toByteArray()); PresenceResult result = new PresenceResult(); Map<String, Protocol.ClientInfo> protoPresence = replyResult.getPresenceMap(); Map<String, ClientInfo> presence = new HashMap<>(); for (Map.Entry<String, Protocol.ClientInfo> entry : protoPresence.entrySet()) { Protocol.ClientInfo protoClientInfo = entry.getValue(); presence.put(entry.getKey(), ClientInfo.fromProtocolClientInfo(protoClientInfo)); } result.setPresence(presence); cb.onDone(null, result); } catch (InvalidProtocolBufferException e) { e.printStackTrace(); } } }).orTimeout(this.opts.getTimeout(), TimeUnit.MILLISECONDS).exceptionally(e -> { this.executor.submit(() -> { Client.this.cleanCommandFuture(cmd); cb.onFailure(e); }); return null; }); this.enqueueCommandFuture(cmd, f); } void presenceStats(String channel, ReplyCallback<PresenceStatsResult> cb) { this.executor.submit(() -> Client.this.presenceStatsSynchronized(channel, cb)); } private void presenceStatsSynchronized(String channel, ReplyCallback<PresenceStatsResult> cb) { Protocol.PresenceStatsRequest req = Protocol.PresenceStatsRequest.newBuilder() .setChannel(channel) .build(); Protocol.Command cmd = Protocol.Command.newBuilder() .setId(this.getNextId()) .setMethod(Protocol.MethodType.PRESENCE_STATS) .setParams(req.toByteString()) .build(); CompletableFuture<Protocol.Reply> f = new CompletableFuture<>(); f.thenAccept(reply -> { this.cleanCommandFuture(cmd); if (reply.getError().getCode() != 0) { cb.onDone(getReplyError(reply), null); } else { try { Protocol.PresenceStatsResult replyResult = Protocol.PresenceStatsResult.parseFrom(reply.getResult().toByteArray()); PresenceStatsResult result = new PresenceStatsResult(); result.setNumClients(replyResult.getNumClients()); result.setNumUsers(replyResult.getNumUsers()); cb.onDone(null, result); } catch (InvalidProtocolBufferException e) { e.printStackTrace(); } } }).orTimeout(this.opts.getTimeout(), TimeUnit.MILLISECONDS).exceptionally(e -> { this.executor.submit(() -> { Client.this.cleanCommandFuture(cmd); cb.onFailure(e); }); return null; }); this.enqueueCommandFuture(cmd, f); } private void refreshSynchronized(String token, ReplyCallback<Protocol.RefreshResult> cb) { Protocol.RefreshRequest req = Protocol.RefreshRequest.newBuilder() .setToken(token) .build(); Protocol.Command cmd = Protocol.Command.newBuilder() .setId(this.getNextId()) .setMethod(Protocol.MethodType.REFRESH) .setParams(req.toByteString()) .build(); CompletableFuture<Protocol.Reply> f = new CompletableFuture<>(); f.thenAccept(reply -> { this.cleanCommandFuture(cmd); if (reply.getError().getCode() != 0) { cb.onDone(getReplyError(reply), null); } else { try { Protocol.RefreshResult replyResult = Protocol.RefreshResult.parseFrom(reply.getResult().toByteArray()); cb.onDone(null, replyResult); } catch (InvalidProtocolBufferException e) { e.printStackTrace(); } } }).orTimeout(this.opts.getTimeout(), TimeUnit.MILLISECONDS).exceptionally(e -> { this.executor.submit(() -> { Client.this.cleanCommandFuture(cmd); cb.onFailure(e); }); return null; }); this.enqueueCommandFuture(cmd, f); } }