/** * The MIT License * Copyright (c) 2010 Tad Glines * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.glines.socketio.sample.eventbus; import com.glines.socketio.common.DisconnectReason; import com.glines.socketio.common.SocketIOException; import com.glines.socketio.server.SocketIOInbound; import com.glines.socketio.server.SocketIOOutbound; import com.glines.socketio.server.SocketIOServlet; import org.codehaus.jettison.json.JSONArray; import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; import org.eclipse.jetty.util.log.JavaUtilLog; import org.eclipse.jetty.util.log.Log; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; public class EventBusServlet extends SocketIOServlet { static { try { Log.setLog(new JavaUtilLog()); LogManager.getLogManager().reset(); LogManager.getLogManager().readConfiguration(Thread.currentThread().getContextClassLoader().getResourceAsStream("logging.properties")); } catch (IOException e) { throw new RuntimeException(e.getMessage(), e); } } private static final Logger LOGGER = Logger.getLogger(EventBusServlet.class.getName()); private final ConcurrentMap<String, Endpoints> subscriptions = new ConcurrentHashMap<String, Endpoints>(); @Override protected SocketIOInbound doSocketIOConnect(HttpServletRequest request) { return new Endpoint(request.getSession().getId(), request.getRemoteHost(), request.getRemotePort()); } private final class Endpoint implements SocketIOInbound { private final String remoteHost; private final int remotePort; private final String id; private volatile SocketIOOutbound outbound; private Endpoint(String id, String remoteHost, int remotePort) { this.remoteHost = remoteHost; this.remotePort = remotePort; this.id = id; } String getId() { return id; } @Override public String toString() { return "Endpoint " + id + " (" + remoteHost + ":" + remotePort + ")"; } @Override public int hashCode() { return 31 * id.hashCode(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Endpoint endpoint = (Endpoint) o; return id.equals(endpoint.id); } @Override public void onConnect(SocketIOOutbound outbound) { if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine(this + " connected."); this.outbound = outbound; try { send(new JSONObject().put("type", MessageType.ACK)); } catch (JSONException e) { throw new RuntimeException(e.getMessage(), e); } } @Override public void onDisconnect(DisconnectReason reason, String errorMessage) { if (LOGGER.isLoggable(Level.FINE)) LOGGER.log(Level.FINE, this + " disconnected: reason=" + reason); this.outbound = null; for (Endpoints ee : subscriptions.values()) ee.remove(this); } @Override public void onMessage(int messageType, String message) { if (outbound == null) throw new NullPointerException(); if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine(this + " received message: " + message); try { JSONArray array = new JSONArray(message); for (int i = 0; i < array.length() && outbound != null; i++) { JSONObject json = array.getJSONObject(i); MessageType type = MessageType.valueOf(json.getInt("type")); switch (type) { case SUBSCRIBE: { String topic = json.getString("topic"); if (LOGGER.isLoggable(Level.FINE)) LOGGER.log(Level.FINE, this + " subscribes to topic: " + topic); subscriptions.putIfAbsent(topic, new Endpoints(topic)); subscriptions.get(topic).add(this); break; } case UNSUBSCRIBE: { String topic = json.getString("topic"); if (LOGGER.isLoggable(Level.FINE)) LOGGER.log(Level.FINE, this + " unsubscribes from topic: " + topic); Endpoints ee = subscriptions.get(topic); if (ee != null) ee.remove(this); return; } case PUBLISH: { String topic = json.getString("topic"); String data = json.getString("data"); if (LOGGER.isLoggable(Level.FINE)) LOGGER.log(Level.FINE, this + " publishes to topic " + topic + " message: " + data); Endpoints ee = subscriptions.get(topic); if (ee != null) ee.fire(topic, data); break; } default: { close(); throw new IllegalArgumentException("Illegal message: " + message); } } } } catch (JSONException e) { throw new RuntimeException(e.getMessage(), e); } } void close() { if (LOGGER.isLoggable(Level.FINE)) LOGGER.log(Level.FINE, this + " closing."); if (outbound != null) { outbound.close(); this.outbound = null; } } void send(JSONObject data) { if (outbound != null) { String str = data.toString(); if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine("Sending to " + this + " message: " + str); try { outbound.sendMessage(str); } catch (SocketIOException e) { LOGGER.log(Level.SEVERE, "Error sending message to " + this + " => disconnecting. Error: " + e.getMessage(), e); close(); } } } } private final class Endpoints { final String topic; final ConcurrentMap<String, Endpoint> endpoints = new ConcurrentHashMap<String, Endpoint>(); Endpoints(String topic) { this.topic = topic; } @Override public String toString() { return "Endpoints for " + topic + ": " + endpoints.size(); } void add(Endpoint endpoint) { if (LOGGER.isLoggable(Level.FINE)) LOGGER.log(Level.FINE, "Subscribing " + endpoint + " to " + this); Endpoint old = endpoints.get(endpoint.getId()); if (old == null) { endpoints.putIfAbsent(endpoint.getId(), endpoint); } else { endpoints.replace(endpoint.getId(), old, endpoint); } if (old != null) old.close(); } void remove(Endpoint endpoint) { if (endpoints.remove(endpoint.getId(), endpoint)) { if (LOGGER.isLoggable(Level.FINE)) LOGGER.log(Level.FINE, endpoint + " unsubscribed from " + this); if (endpoints.isEmpty()) { subscriptions.remove(topic, this); if (LOGGER.isLoggable(Level.FINE)) LOGGER.log(Level.FINE, this + " removed from subscriptions."); } } } void fire(String topic, String data) { for (Endpoint endpoint : endpoints.values()) try { endpoint.send(new JSONObject().put("type", MessageType.PUBLISH).put("topic", topic).put("data", data)); } catch (JSONException e) { throw new RuntimeException(e.getMessage(), e); } } } private static enum MessageType { ACK(4), SUBSCRIBE(1), UNSUBSCRIBE(2), PUBLISH(3), UNKNOWN(0); final int code; MessageType(int code) { this.code = code; } static MessageType valueOf(int code) { for (MessageType type : values()) if (type.code == code) return type; return UNKNOWN; } @Override public String toString() { return "" + code; } } }