Java tutorial
/** * @file QuickIo.java * * @author Andrew Stone <andrew@clovar.com> * @copyright 2012-2014 Clear Channel Inc. * * @internal * This file is part of QuickIoClient and is released under * the MIT License: http://opensource.org/licenses/MIT */ package com.iheart.quickio.client; import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import java.net.URI; import java.net.URISyntaxException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.concurrent.atomic.AtomicLong; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; public class QuickIo { public static final int CODE_DISCONNECTED = -1; public static final int CODE_OK = 200; public static final int CODE_IN_PROGRESS = 202; public static final int CODE_BAD = 400; public static final int CODE_NOT_FOUND = 404; public static final String EV_OPEN = "/open"; public static final String EV_CLOSE = "/close"; public static final String EV_ERROR = "/error"; public static final String EV_CALLBACK = "/qio/callback/"; public static final String EV_HEARTBEAT = "/qio/heartbeat"; public static final String EV_MOVE = "/qio/move"; /** * Address to connect to */ private final String addr; /** * Single instance of Gson to create JSON trees and such */ public static final Gson gson = new Gson(); /** * All of the events we're subscribed to */ protected final Map<String, Event> evs = new HashMap<String, Event>(); /** * Everything needed to manage callbacks */ private final AtomicLong cbId = new AtomicLong(0); private Map<Long, QuickIoCallback> cbs = new HashMap<Long, QuickIoCallback>(); /** * Messages waiting to be sent */ private List<ToSend> toSend = new LinkedList<ToSend>(); /** * If there is an active connection to the server */ private volatile boolean connected = false; /** * For tracking callback connection assocation */ private volatile long connId = 0; /** * The only thread used for all connections */ protected PollThread thread; public QuickIo(final String addr) { this.addr = addr; this.evs.put(QuickIo.EV_OPEN, new Event(QuickIo.EV_OPEN, true)); this.evs.put(QuickIo.EV_CLOSE, new Event(QuickIo.EV_CLOSE, true)); this.evs.put(QuickIo.EV_ERROR, new Event(QuickIo.EV_ERROR, true)); } public void close() { if (this.thread != null) { this.thread.close(); } } /** * Connect to the QuickIo cluster, or trigger an immediate reconnect. */ public void reconnect() { this.reconnect(null); } private void reconnect(final String to) { this.createThread(); if (this.thread != null) { this.thread.reconnect(to); } } protected boolean onMessage(final long connId, final String msg) { final Message m = Parser.parseMessage(msg); if (m == null) { return false; } QuickIoCallback cb = null; if (m.cbId != 0) { cb = new QuickIoCallback(this, connId, m.cbId) { @Override public void onCallback(final JsonElement json, final QuickIoCallback cb, final int code, final String errMsg) { } }; } this.route(m.evPath, cb, m.json); return true; } private void createThread() { URI uri; if (this.thread != null) { return; } try { uri = new URI(this.addr); } catch (URISyntaxException e) { final String msg = "Invalid URL: " + addr; QuickIo.this.trigger(QuickIo.EV_ERROR, QuickIo.gson.toJsonTree(msg)); return; } this.thread = new PollThread(uri.getHost(), uri.getPort(), uri.getScheme() == "wss", new ConnectionEvents() { public void onOpen() { synchronized (QuickIo.this) { QuickIo.this.connected = true; QuickIo.this.connId++; Map<Long, QuickIoCallback> cbs = QuickIo.this.cbs; QuickIo.this.cbs = new HashMap<Long, QuickIoCallback>(); for (final QuickIoCallback cb : cbs.values()) { cb.onCallback(null, null, QuickIo.CODE_DISCONNECTED, "disconnected"); } for (final Event ev : QuickIo.this.evs.values()) { if (!ev.isInternal) { QuickIo.this.subscribe(ev.evPath); } } List<ToSend> toSend = QuickIo.this.toSend; QuickIo.this.toSend = new LinkedList<ToSend>(); for (final ToSend ts : toSend) { QuickIo.this.send(ts.evPath, ts.jsonStr, ts.cb, true, QuickIo.this.connId); } QuickIo.this.trigger(QuickIo.EV_OPEN, null); } } public void onClose() { synchronized (QuickIo.this) { QuickIo.this.connected = false; QuickIo.this.trigger(QuickIo.EV_CLOSE, null); } } public void onError(final String msg) { QuickIo.this.trigger(QuickIo.EV_ERROR, QuickIo.gson.toJsonTree(msg)); } public boolean onMessage(final String msg) { return QuickIo.this.onMessage(QuickIo.this.connId, msg); } }); this.thread.start(); } private void route(final String evPath, final QuickIoCallback cb, final JsonElement json) { if (evPath.startsWith(QuickIo.EV_CALLBACK)) { this.routeCallback(evPath, cb, json); } else if (evPath.startsWith(QuickIo.EV_HEARTBEAT)) { if (cb != null) { cb.send(null, null); } } else if (evPath.startsWith(QuickIo.EV_MOVE)) { this.move(json); } else { this.trigger(evPath, json); } } private void routeCallback(final String evPath, final QuickIoCallback cb, final JsonElement json) { if (!json.isJsonObject()) { return; } long cbId = Long.parseLong(evPath.substring(QuickIo.EV_CALLBACK.length())); QuickIoCallback evCb = null; synchronized (this) { evCb = this.cbs.remove(cbId); } if (evCb == null) { return; } JsonObject obj = json.getAsJsonObject(); int code = obj.get("code").getAsInt(); String errMsg = ""; if (obj.has("err_msg")) { JsonElement errObj = obj.get("err_msg"); errMsg = errObj.isJsonNull() ? null : errObj.getAsString(); } evCb.onCallback(obj.get("data"), cb, code, errMsg); } private void trigger(final String evPath, final JsonElement json) { Event ev = null; synchronized (this) { ev = this.evs.get(evPath); } if (ev == null) { return; } ev.trigger(json); } private void subscribe(final String evPath) { this.send("/qio/on", QuickIo.gson.toJsonTree(evPath), new QuickIoCallback() { @Override public void onCallback(final JsonElement json, final QuickIoCallback cb, final int code, final String errMsg) { switch (code) { case QuickIo.CODE_OK: case QuickIo.CODE_IN_PROGRESS: case QuickIo.CODE_DISCONNECTED: break; default: Event ev = null; synchronized (this) { ev = QuickIo.this.evs.remove(evPath); } if (ev != null) { ev.trigger(code, errMsg); } } } }, false); } private void move(final JsonElement json) { if (!json.isJsonPrimitive()) { return; } JsonPrimitive pr = json.getAsJsonPrimitive(); if (!pr.isString()) { return; } this.reconnect(pr.getAsString()); } public synchronized void on(String evPath, final QuickIoEventCallback cb) { if (cb == null) { return; } evPath = Util.cleanEvPath(evPath); if (!this.evs.containsKey(evPath)) { this.evs.put(evPath, new Event(evPath)); this.subscribe(evPath); } this.evs.get(evPath).add(cb); } public synchronized void off(String evPath, final QuickIoEventCallback cb) { evPath = Util.cleanEvPath(evPath); Event ev = this.evs.get(evPath); if (ev != null) { ev.remove(cb); if (ev.shouldRemove()) { this.evs.remove(evPath); this.send("/qio/off", QuickIo.gson.toJsonTree(evPath), null, false); } } } public void one(final String evPath, final QuickIoEventCallback cb) { this.on(evPath, new QuickIoEventCallback() { @Override public void onSubscribeError(final int code, final String errMsg) { cb.onSubscribeError(code, errMsg); } @Override public void onCallback(final JsonElement json) { QuickIo.this.off(evPath, this); cb.onCallback(evPath, json); } }); } public void send(final String evPath, final JsonElement json, final QuickIoCallback cb) { this.send(evPath, json, cb, true); } protected void send(final String evPath, final JsonElement json, final QuickIoCallback cb, final boolean shouldEnqueue) { this.send(evPath, json, cb, shouldEnqueue, -1); } protected void send(final String evPath, final JsonElement json, final QuickIoCallback cb, final boolean shouldEnqueue, final long connId) { String jsonStr = QuickIo.gson.toJson(json); this.send(evPath, jsonStr, cb, shouldEnqueue, connId); } protected synchronized void send(String evPath, final String jsonStr, final QuickIoCallback cb, boolean shouldEnqueue, final long connId) { boolean sent = false; if (connId != -1 && connId != this.connId) { shouldEnqueue = false; } else { evPath = Util.cleanEvPath(evPath); if (this.connected) { long cbId = 0; if (cb != null) { cbId = this.cbId.incrementAndGet(); this.cbs.put(cbId, cb); } final String msg = String.format("%s:%d=%s", evPath, cbId, jsonStr); sent = this.thread.send(msg); if (sent) { if (cb != null) { cb.attach(this, this.connId); } } else { this.cbs.remove(cbId); } } } if (!sent) { if (shouldEnqueue) { this.toSend.add(new ToSend(evPath, jsonStr, cb)); } else if (cb != null) { cb.onCallback(null, null, QuickIo.CODE_DISCONNECTED, "disconnected"); } } } }