Source: org/terraswarm/accessor/accessors/web/dashboard/RoutingWebSocketServer.js

// Copyright (c) 2016-2017 The Regents of the University of California.
// All rights reserved.
//
// Permission is hereby granted, without written agreement and without
// license or royalty fees, to use, copy, modify, and distribute this
// software and its documentation for any purpose, provided that the above
// copyright notice and the following two paragraphs appear in all copies
// of this software.
//
// IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
// ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
// THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.
//
// THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
// PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
// CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
// ENHANCEMENTS, OR MODIFICATIONS.
//

/** This accessor is an extension of the WebSocketServer, which wraps
 *  the functionality of the base class with the additional feature of
 *  tracking the ID attribute of JSON messages received from the network
 *  and automatically directing messages outgoing from the swarmlet with
 *  corresponding message ID parameters for the matching socketID number.
 *
 *  This accessor assumes messages sent over the web socket have the form
 *  of stringified JSON, where the JSON has a string attribute named "ID"
 *  (a message ID). When a message is received from the network, this 
 *  accessor updates an internal mapping of message IDs to socketIDs
 *  to reflect the socket on which the message ID was received. When a
 *  JSON message is received on this accessor's routingSend input,
 *  it will be stringified and sent over the web socket to the socketID
 *  matching the outgoing message's ID attribute. Note: Outgoing messages
 *  without an ID attribute or without an established mapping to a socketID
 *  will be treated as errors and discarded! 
 *
 *  To use this mode of communication, outgoing messages from the swarmlet
 *  should be directed to the "routingSend" input instead of the base class's
 *  "toSend" input.
 *
 *  Refer to the documentation of WebSocketServer for a more complete
 *  description of the extended accessor.
 *
 *  @accessor dashboard/RoutingWebSocketServer
 *  @parameter {string} hostInterface The IP address or domain name of the
 *    network interface to listen to.
 *  @parameter {int} port The port to listen to for connections.
 *  @parameter {string} pfxKeyCertPassword If sslTls is set to true, then this option needs
 *   to specify the password for the pfx key-cert file specified by pfxKeyCertPath.
 *  @parameter {string} pfxKeyCertPath If sslTls is set to true, then this option needs to
 *   specify the fully qualified filename for the file that stores the private key and certificate
 *   that this server will use to identify itself. This path can be any of those understood by the
 *   Ptolemy host, e.g. paths beginning with $CLASSPATH/.
 *  @parameter {string} receiveType The MIME type for incoming messages,
 *    which defaults to 'text/plain'.
 *  @parameter {string} sendType The MIME type for outgoing messages,
 *    which defaults to 'text/plain'.
 *  @parameter {boolean} sslTls Whether SSL/TLS is enabled. This defaults to false.
 *  @input routingSend JSON data with an 'ID' attribute to be stringified and sent
 *    over the open socket upon which this accessor has prior received a network message with a matching
 *    'ID' attribute.
 *  @input toSend The data to be sent to open sockets.
 *    If this is an object with 'socketID' field and a 'message' field,
 *    then send the value of the message field to the socket identified
 *    by the socketID field. If the input has any other form, then the
 *    message is broadcast to all open socket connections.
 *  @output {int} listening When the server is listening for connections, this output
 *    will produce the port number that the server is listening on
 *  @output connection An output produced when a connection opens or closes.
 *    The output is an object with two fields, a 'socketID',
 *    which is a unique ID for this client connection, and a 'status' field,
 *    which is the string 'open' or 'closed'.
 *  @output received A message received a client in the form of an object with two fields,
 *    a 'socketID', which is a unique ID for this client connection, and a 'message' field,
 *    which is the message received from the client.
 *  @author Hokeun Kim, Edward Lee
 *  @version $$Id$$
 */

// Stop extra messages from jslint and jshint.  Note that there should
// be no space between the / and the * and global. See
// https://chess.eecs.berkeley.edu/ptexternal/wiki/Main/JSHint */
/*globals console, error, exports, require */
/*jshint globalstrict: true*/
'use strict';
/*jslint plusplus: true */

/** Sets up the accessor by defining inputs and outputs. */
exports.setup = function () {
    this.extend('net/WebSocketServer');
    this.parameter('receiveType', {
        value: 'text/plain'
    });
    this.parameter('sendType', {
        value: 'text/plain'
    });
    //Port defaults to 8095
    this.parameter('port',{
        value: 8095
    });
    this.input('routingSend');
};

/** Adds an input handler on routingSend that converts the input into a message delivered
 * to the toSend input of the superclass.
 */
exports.initialize = function () {
    this.routingTable = {};
    exports.ssuper.initialize.call(this);

    //Find the matching socketID for the messageID in the routing table, construct a toSend input
    //to WebSocketServer, and send it.
    this.addInputHandler('routingSend', function(){
        var routingIn = this.get('routingSend');
        if(routingIn && routingIn.id){
            var wrappedMsg = {
                "message": JSON.stringify(routingIn),
                "socketID": this.routingTable[routingIn.id]
            };
            this.send('toSend', wrappedMsg);
        } else {
            error('Input to RoutingSend must be JSON with an id property');
        }
    });
};

//Override
//Find the messageID corresponding to the closed socketID and delete it from the mapping 
exports.notifyClose = function(socketID){
    for(var messageID in this.routingTable){
        if(this.routingTable.hasOwnProperty(messageID) && this.routingTable[messageID] == socketID){
            delete this.routingTable[messageID];
        }
    }
    return;
};

//Override
//Establish a mapping between the received messageID and socketID.
//TODO Perhaps this function should close sockets when the messageID
//is reassigned to a new socket?
exports.filterReceived = function (message, socketID) {
    var messageID;
    try{
        var parsed = JSON.parse(message);
        if(parsed && parsed.id){
            this.routingTable[parsed.id] = socketID;
        } else {
            console.log("WARNING: RoutingWebSocketServer expects messages to be stringified JSON with an ID attribute.");
            console.log("The received message is stringified JSON but does not have an ID attribute.");
            return message;
        }
    } catch (err) {
        console.log("WARNING: RoutingWebSocketServer expects messages to be stringified JSON with an ID attribute.");
        console.log("The received message is not stringified JSON.");
        return message;
    }
    return message;
};

exports.wrapup = function() {
    exports.ssuper.wrapup.call(this);
};