Source: impl/listener-endpoint.js

/**
 * This endpoint listens for incoming connections. It must map incoming
 * connections to existing endpoints, if so available. This mapping must also
 * key the transport implementation to send future reconnects to the same
 * endpoint, if still available
 * 
 * @author Vladimir Katardjiev
 * @constructor
 */
Trap.ListenerEndpoint = function() {
//public class ListenerTrapEndpoint extends TrapEndpointImpl implements ListenerTrapTransportDelegate, TrapListener
	
	Trap.Endpoint.prototype.constructor.call(this);
	// currently live spawned endpoints
	// Type <String, WeakReference<ServerTrapEndpoint>>
	this.endpoints					= new Trap.Map();
	// newly born transports
	// Type <TrapTransport>
	this.sTransports				= new Trap.List();
	this.sDelegate					= null;
	this.sContext					= null;
	
	/*
	 * It is possible for an endpoint to attempt to multi-connect transports (i.e. connect multiple transports simultaneously).
	 * With no further coordination, these transports would spawn separate endpoints on the server end. But the server cannot
	 * coordinate them as the client doesn't know which will first-succeed.
	 *
	 * The solution is simple; each client will generate a unique identifier to map transports from the same client together.
	 * This identifier must be long, include a client-specific part, a temporal part and a random part, and is only valid for 30 seconds.
	 * There is a risk that if someone guesses this identifier during the 30-second window, he or she can attach to a Trap session
	 * without permission (if the session is unauthenticated), but the risk of guessing such a short-lived and high-entropy passphrase is
	 * minimal.
	 *
	 * After 30 seconds, this timer expires. The timer is configurable via the configuration parameter trap.concurrent-connection-window
	 */
	
	this.concurrentConnectionWindow	= 30000;
	
	// Type <String, ServerTrapEndpoint>
	this.ccEndpoints				= new Trap.Map();
	

	// Load the appropriate transports
	for (var tName in Trap.Transports)
	{
		var t = Trap.Transports[tName];

		if (t.prototype && typeof(t.prototype.setTransportPriority) == "function" ) // is a transport 
		{
			var transport = new t();
			this.logger.trace("Initialising new Transport for server: {}", transport.getTransportName());

			if (!transport.canListen())
			{
				this.logger.trace("Skipping it; it cannot listen");
				continue;
			}
			
			if (this.useBinary && !transport.supportsBinary)
			{
				this.logger.info("Skipping it; Trap Binary Mode requested, but transport only supports text");
				continue;
			}
			
			transport.useBinary = this.useBinary;
			
			// Unlike Java, TrapEndpoint only defines one addTransport, and that one sets
			// this object as delegate. Thus, we're done.
			this.addTransport(transport);
		}
	}
	
	if (this.transports.size() == 0)
		throw "No transports could be initialised; either no transports could connect, or transports did not support binary mode (if requested)";

	
};

Trap.ListenerEndpoint.prototype = new Trap.Endpoint;
Trap.ListenerEndpoint.prototype.constructor = Trap.ListenerEndpoint;
																												
/**
 * Called when the state of this listener's transport is changed.
 * @param {Trap.Transport.State} newState
 * @param {Trap.Transport.State} oldState
 * @param {Trap.Transport} transport
 * @param {Object} context
 * @return null
 */
Trap.ListenerEndpoint.prototype.ttStateChanged = function(newState, oldState, transport, context)
	{
		if ((newState == Trap.Transport.State.DISCONNECTED) || (newState == Trap.Transport.State.ERROR))
			this.sTransports.remove(transport);
	};
	
	/**
 * @param {Trap.Message} message
 * @param {Trap.Transport} transport
 * @param {Object} context
 * @return null
 */
Trap.ListenerEndpoint.prototype.ttMessageReceived = function(message, transport, context)
{
	if (message.getOp() != Trap.Message.Operation.OPEN)
	{
		try
		{
			transport.send(new Trap.Message().setOp(Trap.Message.Operation.ERROR), false);
		}
		catch (e)
		{
		}
		transport.disconnect();
		this.logger.info("Disconnecting transport with ID [{}] due to the first message operation not being open; it was [{}]", transport, message.getOp());
		return;
	}
	
	this.logger.debug("New OPEN message. Body length is {}", (message.getData() != null ? message.getData().length : 0));
	
	// Parse body. We'll need some of the information here...
	
	var cfg = new Trap.Configuration(message.getDataAsString());
	var trapId = cfg.getOption(Trap.Constants.ENDPOINT_ID);
	
	if (trapId != Trap.Constants.ENDPOINT_ID_CLIENT)
	{
		var e = this.endpoints.get(trapId);
		
		if (e == null)
		{
			try
			{
				transport.send(new Trap.Message().setOp(Trap.Message.Operation.ERROR), false);
			}
			catch (e1)
			{
			}
			transport.disconnect();
			this.logger.info("Disconnecting transport with ID [{}] due to it trying to connect to non-existent TrapEndpoint session; id [{}]", transport, trapId);
			return;
		}
		
		this.logger.debug("Adding new transport to TrapEndpoint ID {}", e.getTrapID());
		e.addTransport(transport, message);
		
	}
	else
	{
		try
		{
			
			
			var token = cfg.getOption(Trap.Constants.CONNECTION_TOKEN);
			
			var e = this.ccEndpoints.get(token);
			
			if (e == null)
			{
				this.logger.debug("Creating new TrapEndpoint in response to new transport");
				e = new Trap.ServerEndpoint(this);
				
				// Propagate all settings to the new endpoint
				e.configure(this.getConfiguration());
				e.setAuthentication(this.getAuthentication());
				e.setQueueType(this.getQueueType());
				e.setTrapID(Trap._uuid());
				e.setFormat(message.getFormat());
				
				// Store it for future connections
				this.endpoints.put(e.getTrapID(), e);
				
				// Attach transport to endpoint, continuing auth
				e.addTransport(transport, message);
				
				// Remove the transport from us for Garbage Collection
				this.sTransports.remove(transport);
				
				// Notify of new endpoint. NOTE: This means OPEN will never be triggered on server endpoints
				// This is quite a departure from the previous mechanics.
				this.sDelegate.incomingTrapConnection(e, this, this.sContext);
				
				if (token != null)
				{
					
					// Add the endpoint to the cached ones.
					this.ccEndpoints.put(token, e);
					
					// Schedule a task to remove the endpoint from the cached ones
					var mt = this;
					setTimeout(function() {
						mt.ccEndpoints.remove(token);
					}, this.concurrentConnectionWindow);
				}
			}
			else
			{
				// Add the transport, preventing duplicates
				e.addTransport(transport, message);
				this.sTransports.remove(transport);
			}
			
		}
		catch (ex)
		{
			this.logger.warn(ex);
		}
	}
};

/**
 * On an incoming connection, we need to perform two things. First, we need to add the listener as a delegate of the transport. It will wait for an incoming message, and, once it arrives,
 * dispatch the transport to the appropriate recipient (or a new endpoint, if applicable).
 *
 * (non-Javadoc)
 * @see com.ericsson.research.trap.spi.ListenerTrapTransportDelegate#ttsIncomingConnection(com.ericsson.research.trap.spi.TrapTransport, com.ericsson.research.trap.spi.ListenerTrapTransport, java.lang.Object)
 * @param {Trap.Transport} connection
 * @param {Trap.ListenerTransport} server
 * @param {Object} context
 */
Trap.ListenerEndpoint.prototype.ttsIncomingConnection = function(connection, server, context)
{
	connection.setTransportDelegate(this, null);
	this.sTransports.add(connection);
	
	/*
	 * Add a timeout (30 secs) for a transport to get out of sTransports or be forcibly removed.
	 */
	
	var mt = this;
	setTimeout(function() {
		
		mt.sTransports.remove(connection);
		
	}, 30000);
};

/**
 * @param {Trap.ListenerDelegate} delegate
 * @param {Object} context
 */
Trap.ListenerEndpoint.prototype.listen = function(delegate, context)
{
	this.sDelegate = delegate;
	this.sContext = context;
	
	// Now enable listening
	for (var i = 0; i < this.transports.size(); i++)
	{
		var t = this.transports.get(i);
		if (t.isEnabled())
			t.listen(this, null);
	}
	
	this.setState(Trap.Endpoint.State.OPEN);
};

Trap.ListenerEndpoint.prototype.getClientConfiguration = function()
{
	return this.getClientConfiguration(false);
};

/**
 * @param {Boolean} failOnUnreachable
 * @return {String}
 */
Trap.ListenerEndpoint.prototype.getClientConfiguration = function(failOnUnreachable)
{
	try
	{
		var out = new Trap.Configuration();
		for (var i = 0; i < this.transports.size(); i++)
		{
			var t = this.transports.get(i);
			if (t.isEnabled())
				t.getClientConfiguration(out, failOnUnreachable);
		}
		return out.toString();
	}
	catch (e)
	{
		this.logger.debug("Auto configuration disabled due to improperly configured transport; {}", e);
		return "";
	}
};

Trap.ListenerEndpoint.prototype.reconnect = function(timeout)
{
	throw "Cannot reconnect a listener. What you're doing is strange.";
};

/**
 * @param {Trap.Authentication} authentication
 */
Trap.ListenerEndpoint.prototype.setAuthentication = function(authentication)
{
	this.authentication = authentication;
};

Trap.ListenerEndpoint.prototype.close = function()
{
	this.setState(Trap.State.CLOSING);
	Trap.Endpoint.prototype.onEnd.call(this, null, null);
};

/**
 * @param {String} configuration
 */
Trap.ListenerEndpoint.prototype.configure = function(configuration)
{
	Trap.Endpoint.prototype.configure.call(this, configuration);
	this.concurrentConnectionWindow = this.config.getIntOption("trap.concurrent-connection-window", this.concurrentConnectionWindow);
};