Java tutorial
/******************************************************************************* * Copyright (c) 2014 Axel Baumgartner. All rights reserved. This program and * the accompanying materials are made available under the terms of the GNU * Public License v2.0 which accompanies this distribution, and is available at * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html Contributors: Axel * Baumgartner - initial API and implementation ******************************************************************************/ package at.ac.sbg.icts.spacebrew.client; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.HashMap; import java.util.SortedSet; import java.util.TreeSet; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.JSONValue; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A client implementation of the Spacebrew protocol. Connects to a Spacebrew * server via WebSocket, allows to send data via publishers * {@link #addPublisher(String, boolean)} and to receive data via subscribers * {@link #addSubscriber(String, String, String)}. * <p/> * Modified from <a * href="http://labatrockwell.github.io/spacebrew-processing-library/" * >spacebrew-processing-library</a> by <a * href="http://rockwellgroup.com/lab">Brett Renfer and Julio Terra<a/> . * <p/> * Dependencies: * <ul> * <li><a href="http://github.com/TooTallNate/Java-WebSocket">java_websocket</a> * </li> * <li><a href="http://code.google.com/p/json-simple/">json-simple-1.1.1</a></li> * <li><a href="http://www.slf4j.org">slfwj-api-1.7.2</a></li> * <li><a href="http://www.slf4j.org">slf4j-simple-1.7.2</a> (only required if * no other Logger is used)</li> * </ul> * * @author Axel Baumgartner */ public class SpacebrewClient implements WebSocketClientImplCallback { /** * Provides logging facilities. */ private final Logger log = LoggerFactory.getLogger(SpacebrewClient.class); /** * The object that implements the callback methods. */ private final SpacebrewClientCallback callback; /** * The WebSocket client used to communicate with the server. */ private WebSocketClientImpl webSocketClient; /** * The URI of the Spacebrew server to connect to. */ private String serverUri; /** * Name of your application as it will appear in the Spacebrew. * administration. */ private String name; /** * A description of what your application does as it appears in the * Spacebrew administration. */ private String description = ""; /** * The URI of the Spacebrew server currently connected to. */ private String currentServerUri; /** * True if this client is currently connected to a Spacebrew server. */ private boolean connected; /** * Holds all publishers this client offers (publisherName, (type, * messageTemplate)). */ private final HashMap<String, HashMap<String, SpacebrewMessage>> publishers = new HashMap<String, HashMap<String, SpacebrewMessage>>(); /** * Lists which subscribers this client offers (subscriberName, (type, * messageTemplate)). */ private final HashMap<String, HashMap<String, SpacebrewMessage>> subscribers = new HashMap<String, HashMap<String, SpacebrewMessage>>(); /** * Holds the callback methods for all subscribers this client offers * (subscriberName, (methodName, method)). */ private final HashMap<String, HashMap<String, Method>> subscriberMethods = new HashMap<String, HashMap<String, Method>>(); /** * Holds the callback objects for all subscribers this client offers * (subscriberName, callback). */ private final HashMap<String, HashMap<String, Object>> subscriberObjects = new HashMap<String, HashMap<String, Object>>(); /** * The time in milliseconds after which a lost connection is reopened. When * 0 no reconnect will happen. */ private long timeout = 1000; /** * True while the client is disconnecting from our side. */ private boolean disconnecting = false; /** * True while the client is trying to connect. */ private boolean connecting = false; /** * True while the client is reconnecting. */ private boolean reconnecting = false; /** * @param callback The object that will receive messages via callback * methods. * @param serverUri The complete URI of the Spacebrew server to connect to * @param name The name of your application as it will appear in the * Spacebrew administration. */ public SpacebrewClient(SpacebrewClientCallback callback, String serverUri, String name) { this.callback = callback; this.serverUri = serverUri; this.name = name; } /** * @param callback The object that will receive messages via callback * methods. * @param serverUri The complete URI of the Spacebrew server to connect to * @param name The name of your application as it will appear in the * Spacebrew administration. * @param description The description of your application as it will appear * in the Spacebrew administration */ public SpacebrewClient(SpacebrewClientCallback callback, String serverUri, String name, String description) { this.callback = callback; this.serverUri = serverUri; this.name = name; this.description = description; } /** * Sets the URI of the server to connect to. If the client is already * connected, this URI will be used on the next connection attempt. * * @param serverUri The complete URI of the server to connect to on the next * connection attempt */ public void setServerUri(String serverUri) { this.serverUri = serverUri; } /** * @return The complete URI of the server to connect to on the next * connection attempt */ public String getServerURI() { return serverUri; } /** * @return The URI of the server currently connected to */ public String getCurrentServerURI() { return currentServerUri; } /** * @param name The name of this client as it will appear in the Spacebrew * administration */ public void setName(String name) { this.name = name; } /** * @return The name of this client as it will appear in the Spacebrew * administration */ public String getName() { return name; } /** * @param description The description of this client as it will appear in * the Spacebrew administration */ public void setDescription(String description) { this.description = description; } /** * @return The description of this client as it will appear in the Spacebrew * administration */ public String getDescription() { return description; } /** * Sets the timeout the client should wait before reconnecting in * milliseconds. Set to 0 to never reconnect. * * @param timeout The timeout in milliseconds */ public void setTimeout(long timeout) { this.timeout = timeout; } /** * @return The time the client should wait before reconnecting in * milliseconds. 0 means it never reconnects. */ public long getTimeout() { return timeout; } /** * Connects to the Spacebrew server and updates the server with the * currently registered subscribers and publishers this client offers. */ public void connect() { if (!connected) { connecting = true; try { log.info("Connecting to server with URI: {}", serverUri); webSocketClient = new WebSocketClientImpl(this, serverUri); webSocketClient.connect(); currentServerUri = serverUri; } catch (Exception e) { log.error("Could not connect to server with URI: {}", serverUri); log.debug("Exception: " + e.getMessage()); } } } /** * Closes the connection to the Spacebrew server. */ public void disconnect() { if (connected) { log.info("Disconnecting from server with URI: {}", currentServerUri); disconnecting = true; webSocketClient.close(); webSocketClient = null; } } /** * Disconnects and immediately reconnects to the Spacebrew server. */ public void reconnect() { reconnecting = true; disconnect(); } /** * Updates the server about the current subscribers and publishers this * client offers. Called automatically when a connection was successfully * opened. */ @SuppressWarnings("unchecked") private void sendConfig() { if (!connected) { return; } JSONObject configPart = new JSONObject(); configPart.put("name", name); configPart.put("description", description); JSONArray publishes = new JSONArray(); HashMap<String, SpacebrewMessage> temp; SpacebrewMessage message; SortedSet<String> keys; SortedSet<String> typeKeys; keys = new TreeSet<String>(publishers.keySet()); for (String key : keys) { temp = publishers.get(key); typeKeys = new TreeSet<String>(temp.keySet()); for (String typeKey : typeKeys) { message = temp.get(typeKey); JSONObject publish = new JSONObject(); publish.put("name", message.name); publish.put("type", message.type); publish.put("default", message.defaultValue); publishes.add(publish); } } JSONObject publishPart = new JSONObject(); publishPart.put("messages", publishes); configPart.put("publish", publishPart); JSONArray subscribes = new JSONArray(); keys = new TreeSet<String>(subscribers.keySet()); for (String key : keys) { temp = subscribers.get(key); typeKeys = new TreeSet<String>(temp.keySet()); for (String typeKey : typeKeys) { message = temp.get(typeKey); JSONObject subscribe = new JSONObject(); subscribe.put("name", message.name); subscribe.put("type", message.type); subscribes.add(subscribe); } } JSONObject subscribePart = new JSONObject(); subscribePart.put("messages", subscribes); configPart.put("subscribe", subscribePart); JSONObject configMessage = new JSONObject(); configMessage.put("config", configPart); publish(configMessage); } /** * Creates a boolean publisher and adds it to <code>publishers</code>. * Updates the server about the new publisher if the client is connected. * * @param name The name of the publisher * @param defaultValue The default starting value */ public void addPublisher(String name, boolean defaultValue) { addPublisher(name, SpacebrewMessage.TYPE_BOOLEAN, defaultValue + ""); } /** * Creates a range publisher and adds it to <code>publishers</code>. Updates * the server about the new publisher if the client is connected. * * @param name The name of the publisher * @param defaultValue The default starting value */ public void addPublisher(String name, int defaultValue) { addPublisher(name, SpacebrewMessage.TYPE_RANGE, defaultValue + ""); } /** * Creates a String publisher and adds it to <code>publishers</code>. * Updates the server about the new publisher if the client is connected. * * @param name The name of the publisher * @param defaultValue The default starting value */ public void addPublisher(String name, String defaultValue) { addPublisher(name, SpacebrewMessage.TYPE_STRING, defaultValue); } /** * Creates a publisher and adds it to <code>publishers</code>. Updates the * server about the new publisher if the client is connected. * * @param name The name of the publisher * @param type The type of the publisher (i.e. * <code>SpacebrewMessage.TYPE_BOOLEAN</code>, * <code>SpacebrewMessage.TYPE_RANGE</code> or * <code>SpacebrewMessage.TYPE_STRING</code>) * @param defaultValue The default starting value */ public void addPublisher(String name, String type, String defaultValue) { SpacebrewMessage message = new SpacebrewMessage(); message.name = name; message.type = type; message.defaultValue = defaultValue; if (!publishers.containsKey(name)) { publishers.put(name, new HashMap<String, SpacebrewMessage>()); } publishers.get(name).put(type, message); sendConfig(); log.debug("Added publisher with name \"{}\", type \"{}\" and default value \"{}\".", name, type, defaultValue); } /** * Adds a subscriber that uses the generic callback object. Updates the * server about the new subscriber if the client is connected. * * @param name The name of the subscriber * @param type The type of the subscriber (i.e. * <code>SpacebrewMessage.TYPE_BOOLEAN</code>, * <code>SpacebrewMessage.TYPE_RANGE</code> or * <code>SpacebrewMessage.TYPE_STRING</code>) * @param methodName The name of the method in the callback object */ public void addSubscriber(String name, String type, String methodName) { SpacebrewMessage message = new SpacebrewMessage(); message.name = name; message.type = type.toLowerCase(); Method method = null; if (type.equals(SpacebrewMessage.TYPE_BOOLEAN)) { try { method = callback.getClass().getMethod(methodName, new Class[] { boolean.class }); } catch (Exception e) { log.error( "Could not add subscriber with name \"{}\" and type \"{}\", callback does not implement method \"{}\"!", name, type, methodName); } } else if (type.equals(SpacebrewMessage.TYPE_RANGE)) { try { method = callback.getClass().getMethod(methodName, new Class[] { int.class }); } catch (Exception e) { log.error( "Could not add subscriber with name \"{}\" and type \"{}\", callback does not implement method \"{}\"!", name, type, methodName); } } else if (type.equals(SpacebrewMessage.TYPE_STRING)) { try { method = callback.getClass().getMethod(methodName, new Class[] { String.class }); } catch (Exception e) { log.error( "Could not add subscriber with name \"{}\" and type \"{}\", callback does not implement method \"{}\"!", name, type, methodName); } } if (method != null) { if (!subscribers.containsKey(name)) { subscribers.put(name, new HashMap<String, SpacebrewMessage>()); } subscribers.get(name).put(type, message); if (!subscriberMethods.containsKey(name)) { subscriberMethods.put(name, new HashMap<String, Method>()); } subscriberMethods.get(name).put(type, method); sendConfig(); log.debug("Added subscriber with name \"{}\", type \"{}\" and callback method \"{}\".", name, type, method.getName()); } } /** * Adds a boolean subscriber that uses a specific callback object. Updates * the server about the new subscriber if the client is connected. * * @param name The name of the subscriber * @param callback The callback object for boolean messages */ public void addSubscriber(String name, BooleanSubscriber callback) { addSubscriber(name, SpacebrewMessage.TYPE_BOOLEAN, callback); } /** * Adds a range subscriber that uses a specific callback object. Updates the * server about the new subscriber if the client is connected. * * @param name The name of the subscriber * @param callback The callback object forrange messages */ public void addSubscriber(String name, RangeSubscriber callback) { addSubscriber(name, SpacebrewMessage.TYPE_RANGE, callback); } /** * Adds a string subscriber that uses a specific callback object. Updates * the server about the new subscriber if the client is connected. * * @param name The name of the subscriber * @param callback The callback object for string messages */ public void addSubscriber(String name, StringSubscriber callback) { addSubscriber(name, SpacebrewMessage.TYPE_STRING, callback); } /** * Adds a subscriber which uses its own callback object. * * @param name The name of the subscriber * @param type The type of the subscriber (i.e. * <code>SpacebrewMessage.TYPE_BOOLEAN</code>, * <code>SpacebrewMessage.TYPE_RANGE</code> or * <code>SpacebrewMessage.TYPE_STRING</code>) * @param subscriber The callback object */ private void addSubscriber(String name, String type, Object subscriber) { SpacebrewMessage message = new SpacebrewMessage(); message.name = name; message.type = type; if (!subscribers.containsKey(name)) { subscribers.put(name, new HashMap<String, SpacebrewMessage>()); } subscribers.get(name).put(type, message); if (!subscriberObjects.containsKey(name)) { subscriberObjects.put(name, new HashMap<String, Object>()); } subscriberObjects.get(name).put(type, subscriber); sendConfig(); log.debug("Added subscriber with name \"{}\" and type \"{}\".", name, type); } /** * Removes a specified publisher. * * @param name The name of the publisher to remove * @param type The type of the publisher to remove */ public void removePublisher(String name, String type) { if (publishers.containsKey(name)) { publishers.get(name).remove(type); sendConfig(); log.debug("Removed publisher with name \"{}\" and type \"{}\".", name, type); } } /** * Removes a specified subscriber. * * @param name The name of the subscriber to remove * @param type The type of the subscriber to remove */ public void removeSubscriber(String name, String type) { if (subscribers.containsKey(name)) { subscribers.get(name).remove(type); if (subscriberMethods.containsKey(name)) { subscriberMethods.get(name).remove(type); } if (subscriberObjects.containsKey(name)) { subscriberObjects.get(name).remove(type); sendConfig(); log.debug("Removed subscriber with name \"{}\", type \"{}\".", name, type); } } } /** * Publishes a boolean message from a specified publisher. * * @param name The name of the publisher * @param value The value of the message */ public void publish(String name, boolean value) { publish(name, SpacebrewMessage.TYPE_BOOLEAN, value + ""); } /** * Publishes a range message from a specified publisher. * * @param name The name of the publisher * @param value The value of the message */ public void publish(String name, int value) { publish(name, SpacebrewMessage.TYPE_RANGE, value + ""); } /** * Publishes a string message from a specified publisher. * * @param name The name of the publisher * @param value The value of the message */ public void publish(String name, String value) { publish(name, SpacebrewMessage.TYPE_STRING, value); } /** * Sends a message with a specified type from a specified publisher. * * @param name The name of the publisher * @param type The type of the subscriber (i.e. * <code>SpacebrewMessage.TYPE_BOOLEAN</code>, * <code>SpacebrewMessage.TYPE_RANGE</code> or * <code>SpacebrewMessage.TYPE_STRING</code>) * @param value The value of the message */ @SuppressWarnings("unchecked") public void publish(String name, String type, String value) { if (publishers.containsKey(name)) { JSONObject messagePart = new JSONObject(); messagePart.put("clientName", this.name); messagePart.put("name", name); messagePart.put("type", type); messagePart.put("value", value); JSONObject message = new JSONObject(); message.put("message", messagePart); publish(message); } else { log.error("Could not send message, no publisher with name \"{}\" and type \"{}\" has been added!", name, type); } } /** * Sends a JSON message to the server. * * @param message The message to send */ private void publish(JSONObject message) { if (connected) { webSocketClient.send(message.toString()); } else { log.warn("Could not send message, not connected!"); } } /** * Callback method for the <code>WebsocketClient</code> object. */ @Override public void onOpen() { connected = true; connecting = false; log.info("Connection opened to server with URI: {}", currentServerUri); sendConfig(); callback.onOpen(); } /** * Callback method for the <code>WebsocketClient</code> object. */ @Override public void onClose() { if (connected) { connected = false; log.info("Connection closed to server with URI: {}", currentServerUri); } if (reconnecting) { reconnecting = false; connect(); } if (disconnecting) { disconnecting = false; } else if (timeout > 0) { if (connecting) { log.error("Could not connect to server with URI: {}", currentServerUri); } try { Thread.sleep(timeout); connect(); } catch (InterruptedException ex) { // ignore } } } /** * Callback method for the <code>WebsocketClient</code> object. * * @param string The received message * @throws Throwable */ @Override public void onMessage(String string) { Object temp = JSONValue.parse(string); JSONObject container = (JSONObject) temp; JSONObject message = (JSONObject) container.get("message"); String name = (String) message.get("name"); String type = (String) message.get("type"); String value = (String) message.get("value"); if (subscriberMethods.containsKey(name)) { Throwable cause = null; try { Method method = subscriberMethods.get(name).get(type); if (type.equals(SpacebrewMessage.TYPE_BOOLEAN)) { method.invoke(callback, Boolean.parseBoolean(value)); } else if (type.equals(SpacebrewMessage.TYPE_RANGE)) { method.invoke(callback, sanitizeRangeMessage(value)); } else if (type.equals(SpacebrewMessage.TYPE_STRING)) { method.invoke(callback, value); } } catch (InvocationTargetException e) { cause = e.getCause(); } catch (IllegalAccessException e) { cause = e.getCause(); } if (cause != null) { log.error( "Could not pass incoming spacebrew message to callback, exception occurred while calling callback method for subscriber with name \"{}\" and type \"{}\"!", name, type); StringWriter errors = new StringWriter(); cause.printStackTrace(new PrintWriter(errors)); log.debug("Stacktrace: \n" + errors); } } if (subscriberObjects.containsKey(name)) { Object subscriber = null; try { subscriber = subscriberObjects.get(name).get(type); if (type.equals(SpacebrewMessage.TYPE_BOOLEAN)) { ((BooleanSubscriber) subscriber).receive(Boolean.parseBoolean(value)); } else if (type.equals(SpacebrewMessage.TYPE_RANGE)) { ((RangeSubscriber) subscriber).receive(sanitizeRangeMessage(value)); } else if (type.equals(SpacebrewMessage.TYPE_STRING)) { ((StringSubscriber) subscriber).receive(value); } } catch (Exception e) { log.error( "Could not pass message to callback, exception occured while passing message to subscriber with name \"{}\"", name); log.debug("Exception: {}", e); } } } /** * Callback method for <code>webSocketClient</code>. * * @param exception The <code>Exception</code> that caused the error */ @Override public void onError(Exception exception) { log.error("Connection error occured!"); log.debug("Exception: {}", exception); callback.onError(); } /** * @return True if this client is connected to a server */ public boolean isConnected() { return connected; } /** * Takes an incoming range message and produces an integer value in the * interval [0,1023]. The Spacebrew server does not check the messages for * the correct format, thus we check and sanitize it here. * * @param message The unsanitized incoming range message * @return The sanitized <code>int</code> value */ private int sanitizeRangeMessage(String message) { int value = 0; try { value = Integer.parseInt(message); } catch (NumberFormatException e) { // ignore } if (value > 1023) { value = 1023; } else if (value < 0) { value = 0; } return value; } }