com.iheart.quickio.client.QuickIo.java Source code

Java tutorial

Introduction

Here is the source code for com.iheart.quickio.client.QuickIo.java

Source

/**
 * @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");
            }
        }
    }
}