1 /*
  2  ****************************************************************
  3  * Licensed Materials - Property of IBM
  4  * 5725-F96 IBM MessageSight
  5  * (C) Copyright IBM Corp. 2013, 2014.  All Rights Reserved.
  6  *
  7  * US Government Users Restricted Rights - Use, duplication or
  8  * disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
  9  ****************************************************************
 10  */
 11 MessagingJSON = (function (global) {
 12 
 13 	var version = "1.0";
 14 
 15 	/** 
 16 	 * Unique message type identifiers, with associated
 17 	 * associated integer values.
 18 	 * @private 
 19 	 */
 20 	var MESSAGE_TYPE = {
 21 			Connect: 1, 
 22 			Connected: 2, 
 23 			Send: 3,
 24 			Ack: 4,
 25 			Subscribe: 5, 
 26 			CloseSubscription: 6,
 27 			DestroySubscription: 7,
 28 			Ping: 8,
 29 			Pong: 9,
 30 			Close: 10
 31 	};
 32 
 33 	/**
 34 	 * Validate an object's parameter names to ensure they 
 35 	 * match a list of expected variables name for this option
 36 	 * type. Used to ensure option object passed into the API don't
 37 	 * contain erroneous parameters.
 38 	 * @param {Object} obj - User options object
 39 	 * @param {Object} keys - valid keys and types that may exist in obj. 
 40 	 * @throws {Error} Invalid option parameter found. 
 41 	 * @private 
 42 	 */
 43 	var validate = function(obj, keys) {
 44 		for(key in obj) {
 45 			if (obj.hasOwnProperty(key)) {       		
 46 				if (keys.hasOwnProperty(key)) {
 47 					if (typeof obj[key] !== keys[key])
 48 					   throw new Error(format(ERROR.INVALID_TYPE, [typeof obj[key], key]));
 49 				} else {	
 50 					var errorStr = "Unknown property, " + key + ". Valid properties are:";
 51 					for (key in keys)
 52 						if (keys.hasOwnProperty(key))
 53 							errorStr = errorStr+" "+key;
 54 					throw new Error(errorStr);
 55 				}
 56 			}
 57 		}
 58 	};
 59 
 60 	/**
 61 	 * Return a new function which runs the user function bound
 62 	 * to a fixed scope. 
 63 	 * @param {function} User function
 64 	 * @param {object} Function scope  
 65 	 * @return {function} User function bound to another scope
 66 	 * @private 
 67 	 */
 68 	var scope = function (f, scope) {
 69 		return function () {
 70 			return f.apply(scope, arguments);
 71 		};
 72 	};
 73 	
 74 	/** 
 75 	 * Unique message type identifiers, with associated
 76 	 * associated integer values.
 77 	 * @private 
 78 	 */
 79 	var ERROR = {
 80 		OK: {code:0, text:"OK."},
 81 		CONNECT_TIMEOUT: {code:1, text:"Connect timed out."},
 82 		SUBSCRIBE_TIMEOUT: {code:2, text:"Subscribe timed out."}, 
 83 		UNSUBSCRIBE_TIMEOUT: {code:3, text:"Unsubscribe timed out."},
 84 		PING_TIMEOUT: {code:4, text:"Ping timed out."},
 85 		INTERNAL_ERROR: {code:5, text:"Internal error."},
 86 		CONNACK_RETURNCODE: {code:6, text:"Bad Connack return code:{0} {1}."},
 87 		SOCKET_ERROR: {code:7, text:"Socket error: {0}."},
 88 		SOCKET_CLOSE: {code:8, text:"Socket closed."},
 89 		MALFORMED_UTF: {code:9, text:"Malformed UTF data:{0} {1} {2}."},
 90 		UNSUPPORTED: {code:10, text:"{0} is not supported by this browser."},
 91 		INVALID_STATE: {code:11, text:"Invalid state {0}."},
 92 		INVALID_TYPE: {code:12, text:"Invalid type {0} for {1}."},
 93 		INVALID_ARGUMENT: {code:13, text:"Invalid argument {0} for {1}."},
 94 		UNSUPPORTED_OPERATION: {code:14, text:"Unsupported operation."},
 95 		INVALID_STORED_DATA: {code:15, text:"Invalid data in local storage key={0} value={1}."},
 96 		INVALID_JSON_MESSAGE_TYPE: {code:16, text:"Invalid JSON message type {0}."},
 97 		MALFORMED_UNICODE: {code:17, text:"Malformed Unicode string:{0} {1}."},
 98 		SUBSCRIPTION_NOT_FOUND: {code:18, text:"A subscription for the topic {0} with the name {1} does not exist."},
 99 	};
100 	
101 	/**
102 	 * Format an error message text.
103 	 * @private
104 	 * @param {error} ERROR.KEY value above.
105 	 * @param {substitutions} [array] substituted into the text.
106 	 * @return the text with the substitutions made.
107 	 */
108 	var format = function(error, substitutions) {
109 		var text = error.text;
110 		if (substitutions) {
111 		  for (var i=0; i<substitutions.length; i++) {
112 			field = "{"+i+"}";
113 			start = text.indexOf(field);
114 			if(start > 0) {
115 				var part1 = text.substring(0,start);
116 				var part2 = text.substring(start+field.length);
117 				text = part1+substitutions[i]+part2;
118 			}
119 		  }
120 		}
121 		return text;
122 	};
123 
124 	
125 	/** 
126 	 * Repeat keepalive requests, monitor responses.
127 	 * @ignore
128 	 */
129 	var Pinger = function(client, window, keepAliveInterval) { 
130 		this._client = client;        	
131 		this._window = window;
132 		this._keepAliveInterval = keepAliveInterval*1000;     	
133 		this.isReset = false;
134 		
135 		var pingReq =  { Action: "Ping" };
136 		
137 		this.pingReqMsg = JSON.stringify(pingReq);
138 		
139 		var doTimeout = function (pinger) {
140 			return function () {
141 				return doPing.apply(pinger);
142 			};
143 		};
144 		
145 		/** @ignore */
146 		var doPing = function() { 
147 			if (!this.isReset) {
148 				this._client._trace("Pinger.doPing", "Timed out");
149 				this._client._disconnected( ERROR.PING_TIMEOUT.code , format(ERROR.PING_TIMEOUT));
150 			} else {
151 				this.isReset = false;
152 				this._client._trace("Pinger.doPing", this.pingReqMsg);
153 				this._client.socket.send(this.pingReqMsg); 
154 				this.timeout = this._window.setTimeout(doTimeout(this), this._keepAliveInterval);
155 			}
156 		}
157 
158 		this.reset = function() {
159 			this.isReset = true;
160 			this._window.clearTimeout(this.timeout);
161 			if (this._keepAliveInterval > 0)
162 				this.timeout = setTimeout(doTimeout(this), this._keepAliveInterval);
163 		}
164 
165 		this.cancel = function() {
166 			this._window.clearTimeout(this.timeout);
167 		}
168 	 }; 
169 	
170 	/**
171 	 * Monitor request completion.
172 	 * @ignore
173 	 */
174 	var Timeout = function(client, window, timeoutSeconds, action, args) {
175 		this._window = window;
176 		if (!timeoutSeconds)
177 			timeoutSeconds = 30;
178 		
179 		var doTimeout = function (action, client, args) {
180 			return function () {
181 				return action.apply(client, args);
182 			};
183 		};
184 		this.timeout = setTimeout(doTimeout(action, client, args), timeoutSeconds * 1000);
185 		
186 		this.cancel = function() {
187 			this._window.clearTimeout(this.timeout);
188 		}
189 	};
190 
191 	/*
192 	 * Internal implementation of the Websockets jsJSON V1.0 client.
193 	 * 
194 	 * @name Messaging.ClientImpl @constructor 
195 	 * @param {String} host the DNS nameof the webSocket host. 
196 	 * @param {Number} port the port number for that host.
197 	 * @param {String} clientId the MQ client identifier.
198 	 */
199 	var ClientImpl = function (uri, host, port, path, clientId) {
200 		// Check dependencies are satisfied in this browser.
201 		if (!("WebSocket" in global && global["WebSocket"] !== null)) {
202 			throw new Error(format(ERROR.UNSUPPORTED, ["WebSocket"]));
203 		}
204 
205 		this.host = host;
206 		this.port = port;
207 		this.path = path;
208 		this.uri = uri;
209 		this.clientId = clientId;
210 
211 	};
212 
213 	// Messaging Client public instance members. 
214 	ClientImpl.prototype.host;
215 	ClientImpl.prototype.port;
216 	ClientImpl.prototype.path;
217 	ClientImpl.prototype.uri;
218 	ClientImpl.prototype.clientId;
219 
220 	// Messaging Client private instance members.
221 	ClientImpl.prototype.socket;
222 	/* true once we have received an acknowledgment to a CONNECT packet. */
223 	ClientImpl.prototype.connected = false;
224 	ClientImpl.prototype._connectTimeout;
225 	ClientImpl.prototype.onMessageArrived;
226 	ClientImpl.prototype.connectOptions;
227 	ClientImpl.prototype._acksId = 1;
228 
229 	ClientImpl.prototype.receiveBuffer = null;
230 	ClientImpl.prototype.connPinger = null;
231 	ClientImpl.prototype.acks = null;
232 	
233 	ClientImpl.prototype._traceBuffer = null;
234 	ClientImpl.prototype._MAX_TRACE_ENTRIES = 100;
235 
236 	ClientImpl.prototype.connect = function (connectOptions) {
237 		
238 		this._trace("Client.connect", connectOptions, this.connected);
239 		if (this.connected) 
240 			throw new Error(format(ERROR.INVALID_STATE, ["connected state initialized"]));
241 		if (this.socket)
242 			throw new Error(format(ERROR.INVALID_STATE, ["socket already connected"]));
243 		
244 		this.connectOptions = connectOptions;
245 		this._doConnect(this.uri);  		
246 		
247 	};
248 	
249 	ClientImpl.prototype.createTopicSubscription = function (filter, options) {
250 		this._trace("Client.createTopicSubscription", filter, options);
251 			  
252 		if (!this.connected)
253 			throw new Error(format(ERROR.INVALID_STATE, ["not connected"]));
254 		
255 		// get a unique Ack ID
256 		var id = this._acksId++;
257 		this.acks[id] = {Type:MESSAGE_TYPE.Subscribe, Name:options.Name, Topic:filter, ID:id};
258 		// if there is an onFailure reference add it to the Ack entry
259 		if (options.onFailure) {
260 			this.acks[id].onFailure = options.onFailure;
261 		}
262 		
263 		if (options.onSuccess) {
264 			this.acks[id].onSuccess = options.onSuccess;
265 		}
266 		
267 		// create the command object
268 		var cmdObj = { Action: "Subscribe", Name:options.Name, Topic:filter, ID:id};
269 		var cmdString = JSON.stringify(cmdObj);
270 		
271 		this._trace("sending JSON subscribe message...", cmdString);
272 		this.socket.send(cmdString);
273 		
274 	};
275 	
276 	ClientImpl.prototype.closeTopicSubscription = function (subscriptionName, options) {
277 		this._trace("Client.closeSubscription", subscriptionName, options);
278 			  
279 		if (!this.connected)
280 			throw new Error(format(ERROR.INVALID_STATE, ["not connected"]));
281 		
282 		var id = this._acksId++;
283 		
284 		// create the Ack entry...
285 		this.acks[id] =  {Type:MESSAGE_TYPE.CloseSubscription,  Name:subscriptionName, ID:id};
286 		
287 		// if there is an onFailure reference add it to the Ack entry
288 		if (options.onFailure) {
289 			this.acks[id].onFailure = options.onFailure;
290 		}
291 		
292 		if (options.onSuccess) {
293 			this.acks[id].onSuccess = options.onSuccess;
294 		}
295 		
296 		// create the cmd action...
297 		var cmdObj = { Action: "CloseSubscription", ID:id, Name:subscriptionName};		
298 		var cmdString = JSON.stringify(cmdObj);
299 		this._trace("sending JSON close subscription message...", cmdString);
300 		this.socket.send(cmdString);
301 	
302 	};
303 	
304 	ClientImpl.prototype.send = function (message, options) {
305 		this._trace("Client.send");
306 
307 		if (!this.connected)
308 		   throw new Error(format(ERROR.INVALID_STATE, ["not connected"]));
309 		
310 		var id = this._acksId++;
311 		
312 		// create the Ack entry...
313 		this.acks[id] =  {Type:MESSAGE_TYPE.Send, Topic:message.destinationName, ID:id};
314 		
315 		// if there is an onFailure reference add it to the Ack entry
316 		if (options.onFailure) {
317 			this.acks[id].onFailure = options.onFailure;
318 		}
319 		
320 		if (options.onSuccess) {
321 			this.acks[id].onSuccess = options.onSuccess;
322 		}
323 		
324 		var sendObj = {Action: "Send", Topic:message.destinationName, QoS:message.qos, ID:id, Body:message.payloadString };
325 		var msgString = JSON.stringify(sendObj);
326 
327 		this._trace("sending JSON publish", msgString);
328 		this.socket.send(msgString);
329 		
330 
331 	};
332 	
333 	ClientImpl.prototype._processAck = function (ackObj) {
334 		
335 		this._trace("Client._processAck");
336 		
337 		if (!this.acks[ackObj.ID]) {
338 			this._trace("no ack entry found for the id: ", ackObj.ID);
339 			return;
340 		}
341 		
342 		// look up the ack entry
343 		var ackEntry = this.acks[ackObj.ID];
344 		var type = ackEntry.Type;
345 		
346 		switch(type) {
347 			case MESSAGE_TYPE.Subscribe:
348 				if (ackObj.RC != 0) {
349 					if (ackEntry.onFailure) {
350 						ackEntry.onFailure(ackEntry.Name, ackEntry.Topic, ackObj);
351 					} 
352 				} else if (ackEntry.onSuccess) {
353 					ackEntry.onSuccess(ackEntry.Name, ackEntry.Topic);
354 				} 
355 				break;
356 			case MESSAGE_TYPE.CloseSubscription:
357 				if (ackObj.RC != 0) {
358 					if (ackEntry.onFailure) {
359 						ackEntry.onFailure( ackEntry.Name, ackObj);
360 					}
361 				} else if (ackEntry.onSuccess) {
362 					ackEntry.onSuccess(ackEntry.Name);
363 				} 
364 				break;
365 			case MESSAGE_TYPE.Send:
366 				if (ackObj.RC != 0) {
367 					if (ackEntry.onFailure) {
368 						ackEntry.onFailure( ackEntry.Topic, ackObj);
369 					}
370 				} else if (ackEntry.onSuccess) {
371 					ackEntry.onSuccess(ackEntry.Topic);
372 				} 
373 				break;
374 			default:
375 		};
376 			
377 		// ack reference is no longer needed
378 		delete this.acks[ackObj.ID];
379 		
380 	};
381 	
382 	ClientImpl.prototype.disconnect = function () {
383 		this._trace("Client.disconnect");
384 
385 		if (!this.socket)
386 			throw new Error(format(ERROR.INVALID_STATE, ["not connecting or connected"]));
387 		
388 		// try to close the connection gracefully 
389 		var msg = { Action: "Close" };
390 
391 		var msgString = JSON.stringify(msg);
392 		this._trace("sending JSON close message...", msgString);
393 		this.socket.send(msgString);
394 		
395 		if (this.socket) {
396 			// Cancel all socket callbacks so that they cannot be driven again by this socket.
397 			this.socket.onopen = null;
398 			this.socket.onmessage = null;
399 			this.socket.onerror = null;
400 			this.socket.onclose = null;
401 			if (this.socket.readyState === 1)
402 				this.socket.close();
403 			delete this.socket;           
404 		}
405 		this.connected = false;
406 		
407 		if (this.onConnectionLost)
408 			this.onConnectionLost();   
409 		
410 	};
411 	
412 	ClientImpl.prototype.getTraceLog = function () {
413 		if ( this._traceBuffer !== null ) {
414 			return this._traceBuffer;
415 		}
416 	};
417 	
418 	ClientImpl.prototype.startTrace = function () {
419 		if (this._traceBuffer) {
420 			delete this._traceBuffer;
421 		}
422 		this._traceBuffer = [];
423 		this._trace("Initializing trace...", new Date());
424 	};
425 	
426 	ClientImpl.prototype.stopTrace = function () {
427 		delete this._traceBuffer;
428 	};
429 
430 	ClientImpl.prototype._doConnect = function (wsurl) { 
431 		
432 		this._trace("Client._doConnect", wsurl);
433 		this.socket = new WebSocket(wsurl, "json-msg");
434 		this.socket.onopen = scope(this._on_socket_open, this);
435 		this.socket.onmessage = scope(this._on_socket_message, this);
436 		this.socket.onerror = scope(this._on_socket_error, this);
437 		this.socket.onclose = scope(this._on_socket_close, this);
438 		
439 		this.acks = new Object();
440 		
441 		// start a pinger for keep alive timeout
442 		this.connPinger = new Pinger(this, window, this.connectOptions.keepAliveInterval);
443 		
444 		this._connectTimeout = new Timeout(this, window, this.connectOptions.timeout, this._disconnected,  [ERROR.CONNECT_TIMEOUT.code, format(ERROR.CONNECT_TIMEOUT)]);
445 
446 	};
447 
448 	/** 
449 	 * Called when the underlying websocket has been opened.
450 	 * @ignore
451 	 */
452 	ClientImpl.prototype._on_socket_open = function () {
453 		console.log("SOCKET OPEN!!!");
454 		this._trace("Client._on_socket_open");
455 		
456 		// we have an open socket to the server - try to connect
457 		var msg = { Action: "Connect", ClientID:this.clientId };
458 		if (this.connectOptions.userName) {
459 			msg.User = this.connectOptions.userName;
460 		}
461 		if (this.connectOptions.password) {
462 			msg.Password = this.connectOptions.password;
463 		}
464 		//if (this.connectOptions.keepAliveTimeout) {
465 			msg.KeepAliveTimeout = 60;
466 		//}
467 		var msgString = JSON.stringify(msg);
468 		this._trace("Sending connection request...", msgString);
469 		this.socket.send(msgString);
470 
471 	};
472 
473 	/** 
474 	 * Called when the underlying websocket has received a complete packet.
475 	 * @ignore
476 	 */
477 	ClientImpl.prototype._on_socket_message = function (event) {
478 		
479 		this._trace("Client._on_socket_message", event.data);
480 		
481 		// create an object from the JSON string
482 		var msgObj = JSON.parse(event.data); 
483 		var type = MESSAGE_TYPE[msgObj.Action];
484 		
485 		try {
486 			switch(type) {
487 				case MESSAGE_TYPE.Connected:
488 					this.connected = true;
489 					this._connectTimeout.cancel();
490 					if (this.connectOptions.onSuccess) {
491 						this.connectOptions.onSuccess();
492 					}
493 					break;
494 				case MESSAGE_TYPE.Send:
495 					if(this.onMessageArrived) {
496 						this.onMessageArrived(msgObj);
497 					}
498 					break;
499 				case MESSAGE_TYPE.Ack:
500 					this._processAck(msgObj);
501 					break;
502 				case MESSAGE_TYPE.Close:
503 					if (msgObj.RC != 0) {
504 						if (this.onConnectionLost) {
505 							this.onConnectionLost(msgObj.RC, msgObj.Reason);
506 						}
507 					}
508 					break;
509 				case MESSAGE_TYPE.Ping:
510 					var msg = { Action: "Pong" };
511 					var msgString = JSON.stringify(msg);
512 					this._trace("sending Pong message to server...", msgString);
513 					this.socket.send(msgString);
514 					break;
515 				case MESSAGE_TYPE.Pong:
516 					break;
517 				default:
518 					this._disconnected(ERROR.INVALID_JSON_MESSAGE_TYPE.code , format(ERROR.INVALID_JSON_MESSAGE_TYPE, [msgObj.Action]));
519 			};
520 		
521 		} catch (error) {
522 				this._disconnected(ERROR.INTERNAL_ERROR.code , format(ERROR.INTERNAL_ERROR, [error.message]));
523 				return;
524 		}
525 		
526 		// Reset the receive ping timer, we now have evidence the server is alive.
527 		this.connPinger.reset();		
528 
529 	};
530 
531 
532 	/** @ignore */
533 	ClientImpl.prototype._on_socket_error = function (error) {
534 		
535 		var data = "";
536 		if (!error || !error.data) {
537 			data = "Unknown connection error";
538 		} else {
539 			data = error.data;
540 		}
541 		
542 		this._trace("Client._on_socket_error", data);
543 		this._disconnected(ERROR.SOCKET_ERROR.code , format(ERROR.SOCKET_ERROR, [data]));
544 		
545 	};
546 
547 	/** @ignore */
548 	ClientImpl.prototype._on_socket_close = function () {
549 		this._trace("Client._on_socket_close");
550 		this._disconnected(ERROR.SOCKET_CLOSE.code , format(ERROR.SOCKET_CLOSE));
551 	};
552 	
553 
554 	/**
555 	 * Client has disconnected either at its own request or because the server
556 	 * or network disconnected it. Remove all non-durable state.
557 	 * @param {errorCode} [number] the error number.
558 	 * @param {errorText} [string] the error text.
559 	 * @ignore
560 	 */
561 	ClientImpl.prototype._disconnected = function (errorCode, errorText) {
562 		this._trace("Client._disconnected", errorCode, errorText);
563 		
564 		this.connPinger.cancel();
565 		if (this._connectTimeout)
566 			this._connectTimeout.cancel();
567 
568 	   
569 		if (this.socket) {
570 			
571 			try {
572 				// try and disconnect gracefully... 
573 				var msg = { Action: "Close" };
574 
575 				var msgString = JSON.stringify(msg);
576 				this._trace("sending JSON close message...", msgString);
577 				this.socket.send(msgString);
578 				
579 			} catch (error) {
580 				// nothing to do here..
581 			}
582 			
583 			// Cancel all socket callbacks so that they cannot be driven again by this socket.
584 			this.socket.onopen = null;
585 			this.socket.onmessage = null;
586 			this.socket.onerror = null;
587 			this.socket.onclose = null;
588 			if (this.socket.readyState === 1)
589 				this.socket.close();
590 			delete this.socket; 
591 			
592 		}
593 		
594 		if (errorCode === undefined) {
595 			errorCode = ERROR.OK.code;
596 			errorText = format(ERROR.OK);
597 		}
598 			
599 		// Run any application callbacks last as they may attempt to reconnect and hence create a new socket.
600 		if (this.connected) {
601 			this.connected = false;
602 			// Execute the connectionLostCallback if there is one, and we were connected.       
603 			if (this.onConnectionLost)
604 				this.onConnectionLost(errorCode, errorText);      	
605 		} else {
606 			// Otherwise we never had a connection, so indicate that the connect has failed.
607 			if(this.connectOptions.onFailure)
608 				this.connectOptions.onFailure({errorCode:errorCode, errorMessage:errorText});
609 		}
610 	
611 	};
612 	
613 	/** @ignore */
614 	ClientImpl.prototype._trace = function () {
615 		if ( this._traceBuffer !== null ) {  
616 			for (var i = 0, max = arguments.length; i < max; i++) {
617 				if ( this._traceBuffer.length == this._MAX_TRACE_ENTRIES ) {    
618 					this._traceBuffer.shift();              
619 				}
620 				if (i === 0) this._traceBuffer.push(arguments[i]);
621 				else if (typeof arguments[i] === "undefined" ) this._traceBuffer.push(arguments[i]);
622 				else this._traceBuffer.push("  "+JSON.stringify(arguments[i]));
623 		   };
624 		};
625 	};
626 	
627 
628 	// ------------------------------------------------------------------------
629 	// Public Programming interface.
630 	// ------------------------------------------------------------------------
631 	
632 	/** 
633 	 * The JavaScript application communicates with IBM MessageSight via the json_msg plug-in using a {@link MessagingJSON.Client} object. 
634 	 * 
635 	 * The send, subscribe and unsubscribe methods are implemented as asynchronous JavaScript methods 
636 	 * (even though the underlying protocol exchange might be synchronous in nature). 
637 	 * This means they signal their completion by calling back to the application 
638 	 * via Success or Failure callback functions provided by the application on the method in question. 
639 	 * These callbacks are called at most once per method invocation and do not persist beyond the lifetime 
640 	 * of the script that made the invocation.
641 	 * <p>
642 	 * In contrast there are some callback functions, most notably <i>onMessageArrived</i>, 
643 	 * that are defined on the {@link MessagingJSON.Client} object.  
644 	 * These callbacks might be called multiple times, and are not directly related to specific method invocations made by the client. 
645 	 *
646 	 * @name MessagingJSON.Client    
647 	 * 
648 	 * @constructor
649 	 *  
650 	 * @param {string} host - the address of the IBM MessageSight host, as a fully qualified WebSocket URI, as a DNS name or dotted decimal IP address.
651 	 * @param {number} port - the port number to connect to - only required if host is not a URI
652 	 * @param {string} path - the path on the host to connect to - only used if host is not a URI. Default: '/json-msg'.
653 	 * @param {string} clientId - the MessagingJSON client identifier, between 0 and 65535 characters in length.
654 	 * 
655 	 * @property {string} host - <i>read only</i> the IBM MessageSight DNS hostname or dotted decimal IP address.
656 	 * @property {number} port - <i>read only</i> the IBM MessageSight port.
657 	 * @property {string} path - <i>read only</i> the IBM MessageSight path.
658 	 * @property {string} clientId - <i>read only</i> used when connecting to IBM MessageSight.   
659 	 */
660 	var Client = function (host, port, path, clientId) {
661 		
662 		var uri;
663 	    
664 		if (typeof host !== "string")
665 			throw new Error(format(ERROR.INVALID_TYPE, [typeof host, "host"]));
666 	    
667 	    if (arguments.length == 2) {
668 	        clientId = port;
669 	        uri = host;
670 	        var match = uri.match(/^(wss?):\/\/((\[(.+)\])|([^\/]+?))(:(\d+))?(\/.*)$/);
671 	        if (match) {
672 	            host = match[4]||match[2];
673 	            port = parseInt(match[7]);
674 	            path = match[8];
675 	        } else {
676 	            throw new Error(format(ERROR.INVALID_ARGUMENT,[host,"host"]));
677 	        }
678 	    } else {
679 	        if (arguments.length == 3) {
680 				clientId = path;
681 				path = "/json-msg";
682 			}
683 			if (typeof port !== "number" || port < 0)
684 				throw new Error(format(ERROR.INVALID_TYPE, [typeof port, "port"]));
685 			if (typeof path !== "string")
686 				throw new Error(format(ERROR.INVALID_TYPE, [typeof path, "path"]));
687 			
688 			var ipv6AddSBracket = (host.indexOf(":") != -1 && host.slice(0,1) != "[" && host.slice(-1) != "]");
689 			uri = "ws://"+(ipv6AddSBracket?"["+host+"]":host)+":"+port+path;
690 			console.log("URI is " + uri);
691 		}
692 
693 		var clientIdLength = 0;
694 		for (var i = 0; i<clientId.length; i++) {
695 			var charCode = clientId.charCodeAt(i);                   
696 			if (0xD800 <= charCode && charCode <= 0xDBFF)  {    			
697 				 i++; // Surrogate pair.
698 			}   		   
699 			clientIdLength++;
700 		}     	   	
701 		if (typeof clientId !== "string" || clientIdLength > 65535)
702 			throw new Error(format(ERROR.INVALID_ARGUMENT, [clientId, "clientId"])); 
703 		
704 		var client = new ClientImpl(uri, host, port, path, clientId);
705 		this._getHost =  function() { return host; };
706 		this._setHost = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); };
707 			
708 		this._getPort = function() { return port; };
709 		this._setPort = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); };
710 
711 		this._getPath = function() { return path; };
712 		this._setPath = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); };
713 
714 		this._getURI = function() { return uri; };
715 		this._setURI = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); };
716 		
717 		this._getClientId = function() { return client.clientId; };
718 		this._setClientId = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); };
719 		
720 		this._getOnConnectionLost = function() { return client.onConnectionLost; };
721 		this._setOnConnectionLost = function(newOnConnectionLost) { 
722 			if (typeof newOnConnectionLost === "function")
723 				client.onConnectionLost = newOnConnectionLost;
724 			else 
725 				throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnConnectionLost, "onConnectionLost"]));
726 		};
727 			
728 		this._getOnMessageArrived = function() { return client.onMessageArrived; };
729 		this._setOnMessageArrived = function(newOnMessageArrived) { 
730 			if (typeof newOnMessageArrived === "function")
731 				client.onMessageArrived = newOnMessageArrived;
732 			else 
733 				throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnMessageArrived, "onMessageArrived"]));			
734 		};
735 		
736 		/** 
737 		 * Connect this MessagingJSON client to IBM MessageSight via the json_msg plug-in. 
738 		 * 
739 		 * @name MessagingJSON.Client#connect
740 		 * @function
741 		 * @param {Object} connectOptions - attributes used with the connection. 
742 		 * @param {number} connectOptions.timeout - If the connect has not succeeded within this 
743 		 *                    number of seconds, it is deemed to have failed.
744 		 *                    The default is 30 seconds.
745 		 * @param {string} connectOptions.userName - Authentication username for this connection.
746 		 * @param {string} connectOptions.password - Authentication password for this connection.
747 		 * @param {Number} connectOptions.keepAliveInterval - the interval in seconds to check if this
748 		 *                     client is still connected to IBM MessageSight - and that MessageSight can 
749 		 *                     respond.
750 		 * @param {Number} connectOptions.keepAliveTimeout - the json_msg plug-in disconnects this client if
751 		 *                    there is no activity for this number of seconds. If not specified the 
752 		 *                    server assumes 60 seconds. 
753 		 * @param {boolean} connectOptions.useSSL - if present and true, use an SSL Websocket connection.
754 		 * @param {function} connectOptions.onSuccess - called when the connect acknowledgement 
755 		 *                    has been received from IBM MessageSight.
756 		 * @config {function} [onFailure] called when the connect request has failed or timed out.
757 		 *                         A single response object parameter is passed to the onFailure callback 
758 		 *                         containing the following fields:
759 		 * 							<ol>     
760 		 * 							<li>errorCode a number indicating the nature of the error.
761 		 * 							<li>errorMessage text describing the error.      
762 		 * 							</ol>
763 		 * @throws {InvalidState} if the client is not in disconnected state. The client must have received connectionLost
764 		 * or disconnected before calling connect for a second or subsequent time.
765 		 */
766 		this.connect = function (connectOptions) {
767 			connectOptions = connectOptions || {} ;
768 			validate(connectOptions,  {timeout:"number",
769 									   userName:"string", 
770 									   password:"string",  
771 									   keepAliveInterval:"number",
772 									   keepAliveTimeout: "number",
773 									   useSSL:"boolean",
774 									   onSuccess:"function", 
775 									   onFailure:"function"});
776 			
777 			// If no keep alive interval is set, assume 60 seconds.
778 			if (connectOptions.keepAliveInterval === undefined)
779 				connectOptions.keepAliveInterval = 60;
780 			
781 			client.connect(connectOptions);
782 		
783 		};
784 		
785 		/** 
786 		 * Normal disconnect of this MessagingJSON client from IBM MessageSight.
787 		 * 
788 		 * @name MessagingJSON.Client#disconnect
789 		 * @function
790 		 * @throws {InvalidState} if the client is already disconnected.     
791 		 */
792 		this.disconnect = function () {
793 			client.disconnect();
794 		};
795 		
796 		/** 
797 		 * Subscribe for messages, request receipt of a copy of messages sent to the destinations described by the filter.
798 		 * 
799 		 * @name MessagingJSON.Client#createTopicSubscription
800 		 * @function
801 		 * @param {string} filter describing the destinations to receive messages from.
802 		 * <br>
803 		 * @param {object} subscribeOptions - used to control the subscription
804 		 *
805 		 * @param {number} subscribeOptions.qos - the maiximum reliability of any publications sent 
806 		 *                                  as a result of making this subscription. Valid settings are:
807 		 *                                  <dl>
808 		 *                                  <dt>0 - at most once.
809 		 *                                  <dt>1 - at least once.
810 		 *                                  <dt>2 - exactly once.       
811 		 *                                  </dl>
812 		 * @param {function} subscribeOptions.onSuccess - called when the subscribe acknowledgement
813 		 *                                  has been received from IBM MessageSight.
814 		 * @param {function} subscribeOptions.onFailure - called when the subscribe request has failed or timed out.
815 		 *                                  A single response object parameter is passed to the onFailure callback containing the following fields:
816 		 *                                  <ol>   
817 		 *                                  <li>errorCode - a number indicating the nature of the error.
818 		 *                                  <li>errorMessage - text describing the error.      
819 		 *                                  </ol>
820 		 * @param {number} subscribeOptions.timeout - which, if present, determines the number of
821 		 *                                  seconds after which the onFailure calback is called.
822 		 *                                  The presence of a timeout does not prevent the onSuccess
823 		 *                                  callback from being called when the subscribe completes.         
824 		 * @throws {InvalidState} if the client is not in connected state.
825 		 */
826 		this.createTopicSubscription = function (filter, subscribeOptions) {
827 			if (typeof filter !== "string")
828 				throw new Error("Invalid argument:"+filter);
829 			subscribeOptions = subscribeOptions || {} ;
830 			validate(subscribeOptions,  {QoS:"number",
831 										 Name: "string",
832 										 onSuccess:"function", 
833 										 onFailure:"function",
834 										 timeout:"number"
835 										});
836 			if (subscribeOptions.timeout && !subscribeOptions.onFailure)
837 				throw new Error("subscribeOptions.timeout specified with no onFailure callback.");
838 			if (typeof subscribeOptions.qos !== "undefined" 
839 				&& !(subscribeOptions.qos === 0 || subscribeOptions.qos === 1 || subscribeOptions.qos === 2 ))
840 				throw new Error(format(ERROR.INVALID_ARGUMENT, [subscribeOptions.qos, "subscribeOptions.qos"]));
841 			client.createTopicSubscription(filter, subscribeOptions);
842 		};
843 		
844 		/** 
845 		 * Close a subscription identified by a subscrption name.
846 		 * 
847 		 * @name MessagingJSON.Client#closeTopicSubscription
848 		 * @function
849 		 * @param {string} subscriptionName The name of the subscription to close.
850 		 * <br>
851 		 * @param {object} options - used to control the subscription
852 		 *
853 		 * @param {function} options.onSuccess - called when the subscribe acknowledgement
854 		 *                                  has been received from IBM MessageSight.
855 		 * @param {function} options.onFailure - called when the subscribe request has failed or timed out.
856 		 *                                  A single response object parameter is passed to the onFailure callback containing the following fields:
857 		 *                                  <ol>     
858 		 *                                  <li>errorCode - a number indicating the nature of the error.
859 		 *                                  <li>errorMessage - text describing the error.      
860 		 *                                  </ol>
861 		 * @param {number} options.timeout - which, if present, determines the number of
862 		 *                                  seconds after which the onFailure calback is called.
863 		 *                                  The presence of a timeout does not prevent the onSuccess
864 		 *                                  callback from being called when the subscribe completes.         
865 		 * @throws {InvalidState} if the client is not in connected state.
866 		 */
867 		this.closeTopicSubscription = function (subscriptionName, options) {
868 			if (typeof subscriptionName !== "string")
869 				throw new Error("Invalid argument:"+subscriptionName);
870 			options = options || {} ;
871 			validate(options,  {onSuccess:"function", 
872 								onFailure:"function",
873 								timeout:"number"
874 							});
875 			if (options.timeout && !options.onFailure)
876 				throw new Error("options.timeout specified with no onFailure callback.");
877 			client.closeTopicSubscription(subscriptionName, options);
878 		};
879 		
880 		/**
881 		 * Send a message to the consumers of the destination in the Message.
882 		 * 
883 		 * @name MessagingJSON.Client#send
884 		 * @function 
885 		 * @param {MessagingJSON.Message} message to send.
886 		 * @param {object} options - used to control the subscription
887 		 *
888 		 * @param {function} options.onSuccess - called when the subscribe acknowledgement
889 		 *                                  has been received from IBM MessageSight.
890 		 * @param {function} options.onFailure - called when the subscribe request has failed or timed out.
891 		 *                                  A single response object parameter is passed to the onFailure callback containing the following fields:
892 		 *                                  <ol>     
893 		 *                                  <li>errorCode - a number indicating the nature of the error.
894 		 *                                  <li>errorMessage - text describing the error.      
895 		 *                                  </ol>
896 		 * @param {number} options.timeout - which, if present, determines the number of
897 		 *                                  seconds after which the onFailure calback is called.
898 		 *                                  The presence of a timeout does not prevent the onSuccess
899 		 *                                  callback from being called when the subscribe completes.  
900 		 
901 		 * @throws {InvalidState} if the client is not connected.
902 		 */   
903 		this.send = function (message, options) {       	
904 			if (!(message instanceof Message))
905 				throw new Error("Invalid argument:"+typeof message);
906 			if (typeof message.destinationName === "undefined")
907 				throw new Error("Invalid parameter Message.destinationName:"+message.destinationName);
908 			options = options || {} ;
909 			validate(options,  {onSuccess:"function", 
910 								onFailure:"function",
911 								timeout:"number"
912 							});
913 			if (options.timeout && !options.onFailure)
914 				throw new Error("options.timeout specified with no onFailure callback.");
915 		   
916 			client.send(message, options);   
917 		};
918 		
919 		/** 
920 		 * Get the contents of the trace log.
921 		 * 
922 		 * @name MessagingJSON.Client#getTraceLog
923 		 * @function
924 		 * @return {Object[]} tracebuffer containing the time ordered trace records.
925 		 */
926 		this.getTraceLog = function () {
927 			return client.getTraceLog();
928 		}
929 		
930 		/** 
931 		 * Start tracing.
932 		 * 
933 		 * @name MessagingJSON.Client#startTrace
934 		 * @function
935 		 */
936 		this.startTrace = function () {
937 			client.startTrace();
938 		};
939 		
940 		/** 
941 		 * Stop tracing.
942 		 * 
943 		 * @name MessagingJSON.Client#stopTrace
944 		 * @function
945 		 */
946 		this.stopTrace = function () {
947 			client.stopTrace();
948 		};
949 
950 	};
951 	
952 	Client.prototype = {
953 			
954 		get host() { return this._getHost(); },
955 		set host(newHost) { this._setHost(newHost); },
956 				
957 		get port() { return this._getPort(); },
958 		set port(newPort) { this._setPort(newPort); },
959 
960 		get path() { return this._getPath(); },
961 		set path(newPath) { this._setPath(newPath); },
962 				
963 		get clientId() { return this._getClientId(); },
964 		set clientId(newClientId) { this._setClientId(newClientId); },
965 		
966 		get onConnectionLost() { return this._getOnConnectionLost(); },
967 		set onConnectionLost(newOnConnectionLost) { this._setOnConnectionLost(newOnConnectionLost); },
968 		
969 		get onMessageArrived() { return this._getOnMessageArrived(); },
970 		set onMessageArrived(newOnMessageArrived) { this._setOnMessageArrived(newOnMessageArrived); }
971 
972 	};
973 	
974 	/** 
975 	 * An application message, sent or received.
976 	 * <p>
977 	 * All attributes may be null, which implies the default values.
978 	 * 
979 	 * @name MessagingJSON.Message
980 	 * @constructor
981 	 * @param {String|ArrayBuffer} payload The message data to be sent.
982 	 * <p>
983 	 * @property {string} payloadString <i>read only</i> The payload as a string if the payload consists of valid UTF-8 characters.
984 	 * @property {ArrayBuffer} payloadBytes <i>read only</i> The payload as an ArrayBuffer.
985 	 * <p>
986 	 * @property {string} destinationName <b>mandatory</b> The name of the destination to which the message is to be sent
987 	 *                    (for messages about to be sent) or the name of the destination from which the message has been received.
988 	 *                    (for messages received by the onMessageArrived function).
989 	 * <p>
990 	 * @property {number} qos The reliability setting used to deliver the message. Applicable for destinations of type Topic only.
991 	 * <dl>
992 	 *     <dt>0 Best effort (default).
993 	 *     <dt>1 At least once.
994 	 *     <dt>2 Exactly once.     
995 	 * </dl>
996 	 * <p>
997 	 *                     
998 	 */
999 	var Message = function (newPayload, payloadEncoded) {  
1000 		
1001 		var payload;
1002 		if (   typeof newPayload === "string" ) {
1003 			payload = newPayload;
1004 		} else {
1005 			throw (format(ERROR.INVALID_ARGUMENT, [newPayload, "newPayload"]));
1006 		}
1007 
1008 		this._getPayloadString = function () {
1009 				return payload;
1010 		};
1011 		
1012 		var encoded = false;
1013 		if (payloadEncoded) {
1014 			encoded = payloadEncoded;
1015 		}
1016 		this._getIsEncoded = function() {
1017 			return encoded;
1018 		};
1019 
1020 		var destinationName = undefined;
1021 		this._getDestinationName = function() { return destinationName; };
1022 		this._setDestinationName = function(newDestinationName) { 
1023 			if (typeof newDestinationName === "string")
1024 				destinationName = newDestinationName;
1025 			else 
1026 				throw new Error(format(ERROR.INVALID_ARGUMENT, [newDestinationName, "newDestinationName"]));
1027 		};
1028 				
1029 		var qos = 0;
1030 		this._getQos = function() { return qos; };
1031 		this._setQos = function(newQos) { 
1032 			if (newQos === 0 || newQos === 1 || newQos === 2 )
1033 				qos = newQos;
1034 			else 
1035 				throw new Error("Invalid argument:"+newQos);
1036 		};
1037 		
1038 		var destType = undefined;
1039 		this._getDestinationType = function() { return destType; };
1040 		this._setDestinationType = function(newDestType) { 
1041 			if (newDestType === "Topic")
1042 				destType = newDestType;
1043 			else 
1044 				throw new Error("Invalid destination type: " + newDestType);
1045 		};
1046 
1047 	};
1048 	
1049 	Message.prototype = {
1050 		get payloadString() { return this._getPayloadString(); },
1051 		
1052 		get isEncoded() { return this._getIsEncoded(); },
1053 		
1054 		TOPIC : "Topic",
1055 		
1056 		get destinationName() { return this._getDestinationName(); },
1057 		set destinationName(newDestinationName) { this._setDestinationName(newDestinationName); },
1058 		
1059 		get qos() { return this._getQos(); },
1060 		set qos(newQos) { this._setQos(newQos); },
1061 		
1062 		get destinationType() { return this._getDestinationType(); },
1063 		set destinationType(newDestType) { this._setDestinationType(newDestType); },
1064 		
1065 	};
1066 	
1067 
1068 	// Module contents.
1069 	return {
1070 		Client: Client,
1071 		Message: Message
1072 	};
1073 
1074 })(window);
1075