Java tutorial
/* General AI - WAMP Server and Client * Copyright (C) 2013 Tuna Oezer, General AI. * See license.txt for copyright information. */ package ai.general.net.wamp; import ai.general.common.RandomString; import ai.general.common.Strings; import ai.general.directory.Directory; import ai.general.directory.Handler; import ai.general.directory.Request; import ai.general.directory.Result; import ai.general.net.Connection; import ai.general.net.OutputSender; import ai.general.net.RelayHandler; import ai.general.net.RpcCallback; import ai.general.net.Uri; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; /** * Represents a connection that uses the WebSocket Application Messaging Protocol (WAMP) as the * communication protocol. * * For details on the WAMP protocol see the <a href="http://wamp.ws">WAMP Specification.</a>. * * This class implements the entire WAMP specification both for WAMP servers and clients. * * WampConnection can function both as a server and client at the same time. This implementation * is compatible with standard WAMP but extends the standard protocol in the following * ways: * <p><ul> * <li>WampConnection allows symmetric communication between client and server. Both the server * and client can make RPC calls to each other and can subscribe to messages at either endpoint. * </li> * <li>The * wildcard can be used to subscribe to or unsubscribe from a set of topics.</li> * <li>WampConnection treats publish and event calls in the same way. Whether a publish or event * message is sent, depends on whether the connection is on the server or client side, regardless * of the method that is called.</li> * <li>WampConnection allows that multiple methods execute an RPC call and combines the * results of the methods into a single return.</li> * <li>WampConnection supports the specification of request parameters as URI query parameters. * </li> * <li>WampConnection supports user information specified in URI's.</li> * <li>WampConnection can map from one user's URI structure to another users's URI structure * allowing communication between assymetric clients.</li> * <li>WampConnection can forward requests to another server allowing a network of servers that * route message.</li> * </ul></p> * * WampConnection must be closed by calling the {@link #close()} method in order to properly * remove all Node handlers. * * WampConnection is thread-safe. The {@link #process(String)} method can be executed by a * thread pool. */ public class WampConnection extends Connection { // The URI port. private static final int kPort = -1; // The URI protocol. private static final String kProtocol = "wamp"; // Length of session ID strings. private static final int kSessionIdLength = 16; // Server ID string sent in welcome messages. private static final String kServerId = "general.ai-Intercom/2014.01.15"; // Required by protocol. private static final int kWampVersion = 1; // WAMP type ID's. private static final int kWelcome = 0; private static final int kPrefix = 1; private static final int kCall = 2; private static final int kCallResult = 3; private static final int kCallError = 4; private static final int kSubscribe = 5; private static final int kUnsubscribe = 6; private static final int kPublish = 7; private static final int kEvent = 8; /** * By default the WampConnection starts in client mode. To switch the connection to server * mode, the {@link #welcome()} method needs to be called. * * All URI's are interpreted with respect to the home path of the user account associated * with this connection. WampConnection does not allow access to resources that are not * reachable via the home path of the user account. * * If access to external resources is needed, the {@link Directory} class can be used to * create links from a node under the home path of the user to a resource outside the home path * to grant temporary access to the user. This allows a more scalable and secure authentication * architecture. * * Requests are always relativized with respect to the home path of a user account. * Requests from one user account to another user account (e.g., with a publish request) are * mapped from the source URI structure to the destination URI structure. This allows * communication between users without the need to use the same URI structures. * * @param uri The URI of the server endpoint associated with this connection. * @param user_account Account name associated with this connection or null if none. * @param home_path Home directory path for user account. Must be absolute. * @param sender OutputSender to be used to send output to the remote endpoint. */ public WampConnection(Uri uri, String user_account, String home_path, OutputSender sender) { super(uri, user_account, home_path); this.sender_ = sender; is_server_ = false; setSessionId("0"); json_mapper_ = new ObjectMapper(); object_array_type_ = json_mapper_.getTypeFactory().constructArrayType(Object.class); string_array_list_type_ = json_mapper_.getTypeFactory().constructCollectionType(ArrayList.class, String.class); client_subscribed_uris_ = new ArrayList<Uri>(); server_subscribed_paths_ = new ArrayList<String>(); pending_rpc_calls_ = new HashMap<String, RpcCallback>(); rpc_call_counter_ = 0; prefix_ = new HashMap<String, String>(); } /** * Makes an RPC call to the remote endpoint at the specified method path. * This method creates an appropriate URI for the call. * * The RPC is executed asynchronously. When completed the provided RpcCallback will be called. * * @param method_path Method URI path of the RPC method to call. * @param callback The callback to invoke when the RPC returns. * @param arguments RPC method arguments. * @return True if the call was sent. */ @Override public boolean call(String method_path, RpcCallback callback, Object... arguments) { if (method_path.length() == 0) { return false; } if (method_path.charAt(0) != '/') { method_path = "/" + method_path; } try { return call(createUriFromPath(method_path), callback, arguments); } catch (IllegalArgumentException e) { return false; } } /** * Makes an RPC call to the remote endpoint at the specified method URI. * * The RPC is executed asynchronously. When completed the provided RpcCallback will be called. * * @param method_uri Method URI of the RPC method to call. * @param callback The callback to invoke when the RPC returns. * @param arguments RPC method arguments. * @return True if the call was sent. */ @Override public boolean call(Uri method_uri, RpcCallback callback, Object... arguments) { String call_id; synchronized (pending_rpc_calls_) { rpc_call_counter_++; call_id = getSessionId() + ":" + rpc_call_counter_ + ":" + System.currentTimeMillis(); pending_rpc_calls_.put(call_id, callback); } ArrayNode request = json_mapper_.createArrayNode(); request.add(kCall); request.add(call_id); request.add(method_uri.toString()); for (Object argument : arguments) { request.addPOJO(argument); } try { return sender_.sendText(json_mapper_.writeValueAsString(request)); } catch (JsonProcessingException e) { return false; } } /** * Resets the session ID. This may be necessary to reconnect to a server using the same * WampConnection instance. */ public void clearSessionId() { setSessionId("0"); } /** * This method must be called in order to properly clean up when the WampConnection is * closed. * Removes all handlers added by this instance. */ @Override public void close() { super.close(); unsubscribeAll(); } /** * Sends an event message to the remote endpoint for the specified topic path with the provided * data. * * The WAMP protocol distinguishes between a publish from a client and a server. This method * produces a WAMP event on the server side and a WAMP publish on the client side. * * This method generates the appropriate URI for the request based on information provided * during construction of this object. * The path must be absolute. * * The data object must be a POJO, e.g. a Java Bean object. * * @param topic_path The topic path to which to send the event message. * @param data The data associated with the event message. Must be a POJO. * @return True if the request was sent. */ public boolean event(String topic_path, Object data) { return publish(topic_path, data, false, null, null); } /** * Sends an event message to the remote endpoint for the specified topic URI with the provided * data in JSON format. * * The WAMP protocol distinguishes between a publish from a client and a server. This method * produces a WAMP event on the server side and a WAMP publish on the client side. * * @param topic_uri The topic URI to which to send the event message. * @param data The data associated with the event message. * @return True if the request was sent. */ public boolean event(Uri topic_uri, Object data) { return publish(topic_uri, data, false, null, null); } /** * Whether this connection acts as a WAMP server or WAMP client. * * @return True if this connection uses the WAMP server protocol. */ public boolean isServer() { return is_server_; } /** * Sends a prefix request with the specified prefix and URI. * * @param prefix The desired prefix for the URI. * @param uri The URI to be replaced by the prefix. * @return True if the message was sent. */ public boolean prefix(String prefix, Uri uri) { ArrayNode request = json_mapper_.createArrayNode(); request.add(kPrefix); request.add(prefix); request.add(uri.toString()); try { return sender_.sendText(json_mapper_.writeValueAsString(request)); } catch (JsonProcessingException e) { return false; } } /** * Processes an incoming WAMP message. This method processes both server and client messages. * Any response is sent to the caller via the output sender. * * This method returns true if the input conforms to the protocol and could successfully be * interpreted. The method does return true even if the processing of the request has resulted * in a logic error as long as the input conforms to the protocol. * * @param input Message received from remote endpoint. * @return True if the input was successfully interpreted. */ @Override public boolean process(String input) { if (input == null) { return false; } try { Object[] request = json_mapper_.readValue(input, object_array_type_); if (request.length < 1) { log.trace("invalid request"); return false; } int type_id = (Integer) request[0]; switch (type_id) { case kWelcome: return processWelcome(request); case kPrefix: return processPrefix(request); case kCall: return processCall(request); case kCallResult: return processCallResult(request); case kCallError: return processCallError(request); case kSubscribe: return processSubscribe(request); case kUnsubscribe: return processUnsubscribe(request); case kPublish: return processPublish(request); case kEvent: return processEvent(request); default: return false; } } catch (Exception e) { log.catching(Level.TRACE, e); return false; } } /** * Sends a publish request to the remote endpoint for the specified topic path with the provided * data. This method creates the appropriate URI. * * The WAMP protocol distinguishes between a publish from a client and a server. This method * produces a WAMP event on the server side and a WAMP publish on the client side. * * @param topic_path The topic URI path to publish to. * @param data The data to publish. * @return True if the request was sent. */ @Override public boolean publish(String topic_path, Object data) { return publish(topic_path, data, false, null, null); } /** * Sends a publish request to the remote endpoint for the specified topic URI with the provided * data. * * The WAMP protocol distinguishes between a publish from a client and a server. This method * produces a WAMP event on the server side and a WAMP publish on the client side. * * Any additional publish parameters, such as exclude or eligible lists, must be specified * as URI query parameters. Note that URI parameters are not a standard WAMP feature. * * @param topic_uri The topic URI to publish to. * @param data The data to publish. * @return True if the request was sent. */ @Override public boolean publish(Uri topic_uri, Object data) { return publish(topic_uri, data, false, null, null); } /** * Sends a publish request to the remote endpoint for the specified topic path with the provided * data. * * This method allows explicit specification of the exclude_me argument. * * The WAMP protocol distinguishes between a publish from a client and a server. This method * produces a WAMP event on the server side and a WAMP publish on the client side. Note that * the exclude_me parameter is not supported on the server in the standard WAMP protocol. * * @param topic_path The topic URI path to publish to. * @param data The data to publish. * @param exclude_me Value of exclude_me argument. * @return True if the request was sent. */ @Override public boolean publish(String topic_path, Object data, boolean exclude_me) { return publish(topic_path, data, exclude_me, null, null); } /** * Sends a publish request to the remote endpoint for the specified topic path with the provided * data. * * This method allows explicit specification or exclude or eligible lists. * * The WAMP protocol distinguishes between a publish from a client and a server. This method * produces a WAMP event on the server side and a WAMP publish on the client side. Note that * the exclude and eligible parameters are not supported on the server in the standard WAMP * protocol. * * @param topic_path The topic URI path to publish to. * @param data The data to publish. * @param exclude List of target session ID's to exclude from the publish request. * @param eligible Explicit list of target session ID's to include in the publish request. * @return True if the request was sent. */ @Override public boolean publish(String topic_path, Object data, String[] exclude, String[] eligible) { return publish(topic_path, data, false, exclude, eligible); } /** * Sends a subscribe request to the remote endpoint for the specified topic path. * This method generates the appropriate URI for the request based on information provided * during construction of this object. * The path must be absolute. * * @param topic_path The topic path to subscribe to. * @return True if the request was sent. */ @Override public boolean subscribe(String topic_path) { if (topic_path.length() == 0) { return false; } if (topic_path.charAt(0) != '/') { topic_path = "/" + topic_path; } try { return subscribe(createUriFromPath(topic_path)); } catch (IllegalArgumentException e) { return false; } } /** * Sends a subscribe request to the remote endpoint for the specified topic URI. * * @param topic_uri The topic URI to subscribe to. * @return True if the request was sent. */ @Override public boolean subscribe(Uri topic_uri) { ArrayNode request = json_mapper_.createArrayNode(); request.add(kSubscribe); request.add(topic_uri.toString()); try { if (sender_.sendText(json_mapper_.writeValueAsString(request))) { synchronized (client_subscribed_uris_) { client_subscribed_uris_.add(topic_uri); } return true; } else { return false; } } catch (JsonProcessingException e) { // Exception will not be thrown due to construction. return false; } } /** * Sends an unsubscribe request to the remote endpoint for the specified topic path. * This method generates the appropriate URI for the request based on information provided * during construction of this object. * This method is the counterpart of {@link #subscribe(String)}. * * @param topic_path The topic path to unsubscribe from. * @return True if the request was sent. */ @Override public boolean unsubscribe(String topic_path) { if (topic_path.length() == 0) { return false; } if (topic_path.charAt(0) != '/') { topic_path = "/" + topic_path; } try { return unsubscribe(createUriFromPath(topic_path)); } catch (IllegalArgumentException e) { return false; } } /** * Sends an unsubscribe request to the remote endpoint for the specified topic URI. * * @param topic_uri The topic URI to unsubscribe from. * @return True if the request was sent. */ @Override public boolean unsubscribe(Uri topic_uri) { ArrayNode request = json_mapper_.createArrayNode(); request.add(kUnsubscribe); request.add(topic_uri.toString()); try { if (sender_.sendText(json_mapper_.writeValueAsString(request))) { synchronized (client_subscribed_uris_) { client_subscribed_uris_.remove(topic_uri); } return true; } else { return false; } } catch (JsonProcessingException e) { // Exception will not be thrown due to construction. return false; } } /** * Starts a new session with a WAMP client and sends a welcome message to the client using * a randomly generated session ID. * * This message must be sent by a WAMP server in order to initiate communication with a client. * * After this method is called the WampConnection will use the WAMP server protocol in future * messages. * * @return True if the message was sent. */ public boolean welcome() { return welcome(RandomString.nextString(kSessionIdLength)); } /** * Starts a new session with a WAMP client and sends a welcome message to the client using the * specified session ID. * * This message must be sent by a WAMP server in order to initiate communication with a client. * * After this method is called the WampConnection will use the WAMP server protocol in future * messages. * * @param session_id Unique Session ID. * @return True if the message was sent. */ public boolean welcome(String session_id) { setSessionId(session_id); is_server_ = true; ArrayNode response = json_mapper_.createArrayNode(); response.add(kWelcome); response.add(session_id); response.add(kWampVersion); response.add(kServerId); try { if (sender_.sendText(json_mapper_.writeValueAsString(response))) { setIsReady(true); log.trace("connected as server with session ID '{}'", getSessionId()); return true; } } catch (JsonProcessingException e) { /* Exception will not be thrown due to construction. */ } return false; } /** * Creates a Uri object from the given URI string. Verifies that the URI string conforms to the * expected format and normalizes the URI string as necessary. * * This method also expands any WAMP CURIE to a full URI if an applicable CURIE prefix has * been defined via a previous prefix request. * * @param uri_string The unprocessed URI as a string. * @return The URI if the uri_string is valid or null if it is invalid. */ private Uri createUri(String uri_string) { if (!prefix_.isEmpty()) { int index = uri_string.indexOf(':'); if (index > 0) { String prefix_replacement = prefix_.get(uri_string.substring(0, index)); if (prefix_replacement != null) { uri_string = prefix_replacement + uri_string.substring(index + 1); } } } try { return new Uri(new URI(uri_string).normalize()); } catch (URISyntaxException e) { return null; } catch (IllegalArgumentException e) { return null; } } /** * Creates a Uri object from a path. This method uses the protocol, user, hostname, and port * information from this connection object. * * @param path The path for which to create the URI object. * @return A full URI for the specified path using parameters from this connection. * @throws IllegalArgumentException If the URI cannot be created for the specified path. */ private Uri createUriFromPath(String path) throws IllegalArgumentException { Uri uri = new Uri(kProtocol, getHostname(), path); uri.setPort(kPort); uri.setUser(getUserAccount()); return uri; } /** * Creates a call error message that can be sent to the caller of an RPC method. * * If any error_details are provided, they are included in the message. * The call URI is modified by setting the error code as its fragment. * * @param uri The URI of the original RPC method call. * @param call_id The call ID supplied by the original caller. * @param error_code An error code that is added as a fragment to the error URI. * @param error_description The description of the error (mandatory). * @param error_detals Additional details about the error (optional). May be null. * @return The call error message to be returned to the caller. */ private String makeCallError(Uri uri, String call_id, String error_code, String error_description, Object error_details) { try { uri.setFragment(error_code); ArrayNode response = json_mapper_.createArrayNode(); response.add(kCallError); response.add(call_id); response.add(uri.toUri().toString()); response.add(error_description); if (error_details != null) { response.addPOJO(error_details); } return json_mapper_.writeValueAsString(response); } catch (JsonProcessingException e) { // handled below // Revert to manual message generation if there is a secondary exception during exception // handling. return "[" + kCallError + ", " + call_id + ", \"wamp://" + getHostname() + "/error#runtime_error\", \"runtime error\"]"; } } /** * Creates a call result message that can be sent to the caller of an RPC method. * * If the result array has only one item, that item will be returned as the RPC result. * If the result array has multiple items, the entire array is returned as a JSON array. * If the result array has no items, null is returned. * If there is an error while generating the call result, a call error will be returned. * * @param uri The URI of the original RPC method call. * @param call_id The call ID supplied by the original caller. * @param result The RPC method results. * @return The call result message to be returned to the caller. */ private String makeCallResult(Uri uri, String call_id, Collection<Object> result) { try { ArrayNode response = json_mapper_.createArrayNode(); response.add(kCallResult); response.add(call_id); switch (result.size()) { case 0: response.addNull(); break; case 1: response.addPOJO(result.iterator().next()); break; default: response.addPOJO(result); break; } return json_mapper_.writeValueAsString(response); } catch (JsonProcessingException e) { return makeCallError(uri, call_id, "runtime_error", "runtime error", null); } } /** * Processes an incoming call request. * * Once the call has completed, sends the call result or call error message via the output * sender. * * The method handler is run synchronously on the calling thread. This method does not return * until the method handler returns. * * wamp_request[1] = call ID * wamp_request[2] = method URI * wamp_request[3..] = arguments * * @param wamp_request The WAMP request arguments. * @return True if the request has been successfully processed. * @throws ClassCastException if the request arguments are invalid. */ private boolean processCall(Object[] wamp_request) { final int kIndexCallId = 1; final int kIndexMethodUri = 2; if (wamp_request.length < 3) { // Protocol violation, do not respond. log.trace("invalid call request"); return false; } String call_id = (String) wamp_request[kIndexCallId]; Uri uri = createUri((String) wamp_request[kIndexMethodUri]); if (uri == null) { log.trace("invalid method uri: {}", wamp_request[kIndexMethodUri]); return sender_.sendText( makeCallError(createUriFromPath("/error"), call_id, "rpc_error", "undefined method", null)); } Request request = new Request(uri, Request.RequestType.Call); for (int i = 3; i < wamp_request.length; i++) { request.addArgument(wamp_request[i]); } if (Directory.Instance.handle(getHomePath(), request) > 0) { Result result = request.getResult(); if (!result.hasErrors()) { sender_.sendText(makeCallResult(uri, call_id, result.getValues())); log.trace("processed RPC call with success: '{}'", wamp_request[kIndexMethodUri]); } else { // WAMP supports returning only one error. Thus, only the first error is returned to the // caller. Result.Error error = result.getError(0); sender_.sendText( makeCallError(uri, call_id, "logic_error", error.getDescription(), error.getDetails())); log.trace("processed RPC call with error: '{}'", wamp_request[kIndexMethodUri]); } } else { sender_.sendText(makeCallError(uri, call_id, "rpc_error", "undefined method", null)); log.trace("call to undefined method: '{}'", wamp_request[kIndexMethodUri]); } return true; } /** * Processes an incoming call error. * * The call error callback is called on the calling thread. * * wamp_request[1] = call ID * wamp_request[2] = error URI * wamp_request[3] = error description * wamp_request[4] = error details (optional) * * @param wamp_request The WAMP request arguments. * @return True if the request has been successfully processed. * @throws ClassCastException if the request arguments are invalid. */ private boolean processCallError(Object[] wamp_request) { final int kIndexCallId = 1; final int kIndexErrorUri = 2; final int kIndexErrorDescription = 3; final int kIndexErrorDetails = 4; if (wamp_request.length < 4) { log.trace("invalid call error request"); return false; } String call_id = (String) wamp_request[kIndexCallId]; Object error_details = null; if (wamp_request.length > 4) { error_details = wamp_request[kIndexErrorDetails]; } RpcCallback callback = pending_rpc_calls_.get(call_id); if (callback == null) { log.trace("call error with no callback"); return true; } synchronized (pending_rpc_calls_) { pending_rpc_calls_.remove(call_id); } try { callback.onError(new Uri((String) wamp_request[kIndexErrorUri]), (String) wamp_request[kIndexErrorDescription], error_details); } catch (IllegalArgumentException e) { // On URI error make the callback without the URI. callback.onError(null, (String) wamp_request[kIndexErrorDescription], error_details); } log.trace("processed call error"); return true; } /** * Processes an incoming call result. * * The call result is processed on the calling thread. * * wamp_request[1] = call ID * wamp_request[2] = call result * * @param wamp_request The WAMP request arguments. * @return True if the request has been successfully processed. * @throws ClassCastException if the request arguments are invalid. */ private boolean processCallResult(Object[] wamp_request) { final int kIndexCallId = 1; final int kIndexCallResult = 2; if (wamp_request.length < 3) { log.trace("invalid call result request"); return false; } String call_id = (String) wamp_request[kIndexCallId]; RpcCallback callback = pending_rpc_calls_.get(call_id); if (callback == null) { log.trace("call result with no callback"); return true; } synchronized (pending_rpc_calls_) { pending_rpc_calls_.remove(call_id); } callback.onSuccess(wamp_request[kIndexCallResult]); log.trace("processed call result"); return true; } /** * Processes an incoming event request. * * This method allows events for unsubscribed URI's. This is necessary since a subscription * requests may contain wildcards. * * wamp_request[1] = topic URI * wamp_request[2] = event data * * @param wamp_request The WAMP request arguments. * @return True if the request has been successfully processed. * @throws ClassCastException if the request arguments are invalid. */ private boolean processEvent(Object[] wamp_request) { final int kIndexTopicUri = 1; final int kIndexEventData = 2; if (wamp_request.length < 3) { log.trace("invalid event request"); return false; } Uri uri = createUri((String) wamp_request[kIndexTopicUri]); if (uri == null) { log.trace("invalid topic uri: {}", wamp_request[kIndexTopicUri]); return false; } Request request = new Request(uri, Request.RequestType.Publish, wamp_request[kIndexEventData]); Directory.Instance.handle(getHomePath(), request); log.trace("processed event '{}'", wamp_request[kIndexTopicUri]); return true; } /** * Processes an incoming prefix request. * * wamp_request[1] = prefix * wamp_request[2] = URI to be prefixed * * @param wamp_request The WAMP request arguments. * @return True if the request has been successfully procesed. * @throws ClassCastException if request arguments are invalid. */ private boolean processPrefix(Object[] wamp_request) { final int kIndexPrefix = 1; final int kIndexUri = 2; if (wamp_request.length < 3) { log.trace("invalid prefix request"); return false; } synchronized (prefix_) { prefix_.put((String) wamp_request[kIndexPrefix], (String) wamp_request[kIndexUri]); } log.trace("processed prefix '{}' -> '{}'", wamp_request[kIndexPrefix], wamp_request[kIndexUri]); return true; } /** * Processes an incoming publish request. * * wamp_request[1] = topic URI * wamp_request[2] = event data * wamp_request[3] = exclude_me or exclude list (optional) * wamp_request[4] = eligible list (optional) * * @param wamp_request The WAMP request arguments. * @return True if the request has been successfully procesed. * @throws ClassCastException if request arguments are invalid. */ private boolean processPublish(Object[] wamp_request) { final int kIndexTopicUri = 1; final int kIndexEventData = 2; final int kIndexExclude = 3; final int kIndexElligible = 4; if (wamp_request.length < 3) { log.trace("invalid publish request"); return false; } Uri uri = createUri((String) wamp_request[kIndexTopicUri]); if (uri == null) { log.trace("invalid topic uri: {}", wamp_request[kIndexTopicUri]); return false; } Request request = new Request(uri, Request.RequestType.Publish, wamp_request[kIndexEventData]); if (wamp_request.length > kIndexExclude) { if (wamp_request[kIndexExclude] instanceof Boolean) { uri.setParameter("exclude", getSessionId()); } else if (wamp_request[kIndexExclude] instanceof ArrayList) { ArrayList<String> exclude = json_mapper_.convertValue(wamp_request[kIndexExclude], string_array_list_type_); if (exclude.size() > 0) { uri.setParameter("exclude", Strings.join(exclude, ",")); } } if (wamp_request.length > kIndexElligible) { if (wamp_request[kIndexElligible] instanceof ArrayList) { ArrayList<String> eligible = json_mapper_.convertValue(wamp_request[kIndexElligible], string_array_list_type_); if (eligible.size() > 0) { uri.setParameter("eligible", Strings.join(eligible, ",")); } } } } Directory.Instance.handle(getHomePath(), request); log.trace("processed publish '{}'", wamp_request[kIndexTopicUri]); return true; } /** * Processes an incoming subscribe request. * * wamp_request[1] = topic URI * * @param wamp_request The WAMP request arguments. * @return True if the request has been successfully procesed. * @throws ClassCastException if request arguments are invalid. */ private boolean processSubscribe(Object[] wamp_request) { final int kIndexTopicUri = 1; if (wamp_request.length < 2) { log.trace("invalid subscribe request"); return false; } Uri uri = createUri((String) wamp_request[kIndexTopicUri]); if (uri == null) { log.trace("invalid topic uri: {}", wamp_request[kIndexTopicUri]); return false; } String path = getHomePath() + uri.getPath(); synchronized (server_subscribed_paths_) { if (!server_subscribed_paths_.contains(path)) { Handler handler = new RelayHandler(relayHandlerName(path), this, uri); if (Directory.Instance.addHandler(path, handler)) { server_subscribed_paths_.add(path); } } } log.trace("Processed subscribe '{}'", path); return true; } /** * Processes an incoming unsubscribe request. * * wamp_request[1] = topic URI * * @param wamp_request The WAMP request arguments. * @return True if the request has been successfully procesed. * @throws ClassCastException if request arguments are invalid. */ private boolean processUnsubscribe(Object[] wamp_request) { final int kIndexTopicUri = 1; if (wamp_request.length < 2) { log.trace("invalid unsubscribe request"); return false; } Uri uri = createUri((String) wamp_request[kIndexTopicUri]); if (uri == null) { log.trace("invalid topic uri: {}", wamp_request[kIndexTopicUri]); return false; } String path = getHomePath() + uri.getPath(); Directory.Instance.removeHandler(path, relayHandlerName(path)); synchronized (server_subscribed_paths_) { server_subscribed_paths_.remove(path); } log.trace("Processed unsubscribe '{}'", path); return true; } /** * Processes an incoming welcome request. * * wamp_request[1] = session ID * wamp_request[2] = protocol version * wamp_request[3] = server ID * * @param wamp_request The WAMP request arguments. * @return True if the request has been successfully procesed. * @throws ClassCastException if request arguments are invalid. */ private boolean processWelcome(Object[] wamp_request) { final int kIndexSessionId = 1; // final int kProtocolVersion = 2; // ignored final int kIndexServerId = 3; if (wamp_request.length < 4) { log.trace("invalid welcome request"); return false; } setSessionId((String) wamp_request[kIndexSessionId]); setServerId((String) wamp_request[kIndexServerId]); setIsReady(true); log.trace("received server welcome from {} for session {}", getServerId(), getSessionId()); return true; } /** * Internal unified publish method that accepts a string topic path. * * See {@link #publish(int, URI, Object, boolean, String[], String[])} for detailed comments. * * @param topic_path The topic URI path to publish to. * @param data The data to publish. * @param exclude_me Optional value of exclude_me argument (only included if true). * @param exclude List of session ID's to exclude from the publish request or null. * @param eligible Explicit list of session ID's to include in the publish request or null. * @return True if the request was sent. */ private boolean publish(String topic_path, Object data, boolean exclude_me, String[] exclude, String[] eligible) { if (topic_path.length() == 0) { return false; } if (topic_path.charAt(0) != '/') { topic_path = "/" + topic_path; } try { return publish(createUriFromPath(topic_path), data, exclude_me, exclude, eligible); } catch (IllegalArgumentException e) { return false; } } /** * Internal unified publish method that accepts a URI topic path. * * The WAMP protocol distinguishes between a publish from a client and a server. Server messages * are published as WAMP events and client messages as WAMP publish messages. This method * combines the two mechanism by using the right WAMP type ID depending on connection context. * * This method adds the optional exclude_me, exclude and eligible parameters to the message. * The exclude_me parameter is only added if it is true. The exclude parameter is only added * if it is not null and exclude_me is false. The eligible parameter is only added if it is * not not. If the eligible parameter is specified, but both exclude_me is false and exclude * is null, this method adds an empty exclude array to the message to conform to the protocol. * * While this method does not strictly enforce it, the WAMP protocol does not support the * exclude_me, exclude and eligible parameters on the server side. So, on the server side * these values should be always set to false and null. * * @param topic_uri The topic URI to publish to. * @param data The data to publish. * @param exclude_me Optional value of exclude_me argument (only included if true). * @param exclude List of session ID's to exclude from the publish request or null. * @param eligible Explicit list of session ID's to include in the publish request or null. * @return True if the request was sent. */ private boolean publish(Uri topic_uri, Object data, boolean exclude_me, String[] exclude, String[] eligible) { ArrayNode request = json_mapper_.createArrayNode(); request.add(is_server_ ? kEvent : kPublish); request.add(topic_uri.toString()); request.addPOJO(data); if (exclude_me) { request.add(exclude_me); } else if (exclude != null) { request.addPOJO(exclude); } if (eligible != null) { if (request.size() < 4) { request.addPOJO(new String[] {}); } request.addPOJO(eligible); } try { return sender_.sendText(json_mapper_.writeValueAsString(request)); } catch (JsonProcessingException e) { return false; } } /** * Creates and returns a unique name for the relay handler for the specified path. * * @return A unique name for the relay handler for the specified path. */ private String relayHandlerName(String path) { if (getUserAccount() == null) { return path + "->@" + getSessionId(); } else { return path + "->" + getUserAccount() + "@" + getSessionId(); } } /** * Executes both client and server unsubscription. * As client, unsubscribes from all subscribed topics. * As server, unsubscribes all topics subscribed to by clients. */ private void unsubscribeAll() { // client unsubscription synchronized (client_subscribed_uris_) { Uri[] topic_uris = client_subscribed_uris_.toArray(new Uri[] {}); for (Uri uri : topic_uris) { unsubscribe(uri); } client_subscribed_uris_.clear(); } // server unsubscription synchronized (server_subscribed_paths_) { for (String path : server_subscribed_paths_) { Directory.Instance.removeHandler(path, relayHandlerName(path)); } server_subscribed_paths_.clear(); } } private static Logger log = LogManager.getLogger(); private ArrayList<Uri> client_subscribed_uris_; // All URI's subscribed to as client. private boolean is_server_; // If true, use server protocol. private ObjectMapper json_mapper_; // JSON parser and generator. private JavaType object_array_type_; // Object[] type used in JSON parsing. private HashMap<String, RpcCallback> pending_rpc_calls_; // RPC calls in progress. private HashMap<String, String> prefix_; // WAMP prefix directory. private long rpc_call_counter_; // Counter used to keep track of RPC calls. private OutputSender sender_; // Used to send messages to the remote endpoint. private ArrayList<String> server_subscribed_paths_; // All paths subscribed to by clients. private JavaType string_array_list_type_; // ArrayList<String> type used in JSON parsing. }