Initial revision
[OpenSRF.git] / src / javascript / opensrf_app_session.js
1 /** @file oils_app_session.js
2   * @brief AppRequest and AppSession.
3   * The AppSession class does most of the communication work and the AppRequest 
4   * contains the top level client API.
5   */
6
7 /** The default wait time when a client calls recv. It
8   * may be overrided by passing in a value to the recv method
9   */
10 AppRequest.DEF_RECV_TIMEOUT = 10000;
11
12 /** Provide a pre-built AppSession object and the payload of the REQUEST
13   * message you wish to send
14   */
15 function AppRequest( session, payload ) {
16
17         /** Our controling session */
18         this.session = session;
19
20         /** This requests message thread trace */
21         this.thread_trace = null;
22
23         /** This request REQUEST payload */
24         this.payload = payload;
25
26         /** True if this request has completed is request cycle */
27         this.is_complete = false;
28
29         /** Stores responses received from requests */
30         this.recv_queue = new Array();
31 }
32
33 /** returns true if this AppRequest has completed its request cycle.  */
34 AppRequest.prototype.complete = function() {
35         if( this.is_complete ) { return true; }
36         this.session.queue_wait(0);
37         return this.is_complete;
38 }
39
40 /** When called on an AppRequest object, its status will be
41   * set to complete regardless of its previous status
42   */
43 AppRequest.prototype.set_complete = function() {
44         this.is_complete = true;
45 }
46
47 /** Returns the current thread trace */
48 AppRequest.prototype.get_thread_trace = function() {
49         return this.thread_trace;
50 }
51
52 /** Pushes some payload onto the recv queue */
53 AppRequest.prototype.push_queue = function( payload ) {
54         this.recv_queue.push( payload );
55 }
56
57 /** Returns the current payload of this request */
58 AppRequest.prototype.get_payload = function() {
59         return this.payload;
60 }
61
62 /** Removes this request from the our AppSession's request bucket 
63   * Call this method when you are finished with a particular request 
64   */
65 AppRequest.prototype.finish = function() {
66         this.session.remove_request( this );
67 }
68
69
70 /** Retrieves the current thread trace from the associated AppSession object,
71   * increments that session's thread trace, sets this AppRequest's thread trace
72   * to the new value.  The request is then sent.
73   */
74 AppRequest.prototype.make_request = function() {
75         var tt = this.session.get_thread_trace();
76         this.session.set_thread_trace( ++tt );
77         this.thread_trace = tt;
78         this.session.add_request( this );
79         this.session.send( oilsMessage.REQUEST, tt, this.payload );
80 }
81
82 /** Checks the receive queue for message payloads.  If any are found, the first 
83   * is returned.  Otherwise, this method will wait at most timeout seconds for
84   * a message to appear in the receive queue.  Once it arrives it is returned.
85   * If no messages arrive in the timeout provided, null is returned.
86
87   * NOTE: timeout is in * milliseconds *
88   */
89
90 AppRequest.prototype.recv = function( /*int*/ timeout ) {
91
92
93         if( this.recv_queue.length > 0 ) {
94                 return this.recv_queue.shift();
95         }
96
97         //if( this.complete ) { return null; }
98
99         if( timeout == null ) {
100                 timeout = AppRequest.DEF_RECV_TIMEOUT;
101         }
102
103         while( timeout > 0 ) {
104
105                 var start = new Date().getTime();
106                 this.session.queue_wait( timeout );
107
108                 if( this.recv_queue.length > 0 ) {
109                         return this.recv_queue.shift();
110                 }
111
112                 // shortcircuit the call if we're already complete
113                 if( this.complete() ) { return null; }
114
115                 new Logger().debug( "AppRequest looping in recv " 
116                                 + this.get_thread_trace() + " with timeout " + timeout, Logger.DEBUG );
117
118                 var end = new Date().getTime();
119                 timeout -= ( end - start );
120         }
121
122         return null;
123 }
124
125 /** Resend this AppRequest's REQUEST message, useful for recovering
126  * from disconnects, etc.
127  */
128 AppRequest.prototype.resend = function() {
129
130         new Logger().debug( "Resending msg with thread trace: " 
131                         + this.get_thread_trace(), Logger.DEBUG );
132         this.session.send( oilsMessage.REQUEST, this.get_thread_trace(), this.payload );
133 }
134
135
136
137         
138 // -----------------------------------------------------------------------------
139 // -----------------------------------------------------------------------------
140 // AppSession code
141 // -----------------------------------------------------------------------------
142
143 /** Global cach of AppSession objects */
144 AppSession.session_cache = new Array();
145
146 // -----------------------------------------------------------------------------
147 // Session states
148 // -----------------------------------------------------------------------------
149 /** True when we're attempting to connect to a remte service */
150 AppSession.CONNECTING   = 0; 
151 /** True when we have successfully connected to a remote service */
152 AppSession.CONNECTED            = 1;
153 /** True when we have been disconnected from a remote service */
154 AppSession.DISCONNECTED = 2;
155 /** The current default method protocol */
156 AppSession.PROTOCOL             = 1;
157
158 /** Our connection with the outside world */
159 AppSession.transport_handle = null;
160
161
162 /** Returns the session with the given session id */
163 AppSession.find_session = function(session_id) {
164         return AppSession.session_cache[session_id];
165 }
166
167 /** Adds the given session to the global cache */
168 AppSession.push_session = function(session) {
169         AppSession.session_cache[session.get_session_id()] = session;
170 }
171
172 /** Deletes the session with the given session id from the global cache */
173 AppSession.delete_session = function(session_id) {
174         AppSession.session_cache[session_id] = null;
175 }
176
177 /** Builds a new session.
178   * @param remote_service The remote service we want to make REQUEST's of
179   */
180 function AppSession( username, password, remote_service ) {
181
182
183         /** Our logger object */
184         this.logger = new Logger();
185
186         random_num = Math.random() + "";
187         random_num.replace( '.', '' );
188
189         /** Our session id */
190         this.session_id = new Date().getTime() + "" + random_num;
191
192         this.auth = new userAuth( username, password );
193
194         /** Our AppRequest queue */
195         this.request_queue = new Array();
196
197         /** Our connectivity state */
198         this.state = AppSession.DISCONNECTED;
199
200         var config = new Config();
201         
202         /** The remote ID of the service we are communicating with as retrieved 
203           * from the config file
204          */
205         this.orig_remote_id = config.get_value( "remote_service/" + remote_service );
206         if( ! this.orig_remote_id ) { 
207                 throw new oils_ex_config( "No remote service id for: " + remote_service );
208         }
209
210         /** The current remote ID of the service we are communicating with */
211         this.remote_id = this.orig_remote_id;
212
213         /** Our current request threadtrace, which is incremented with each 
214           * newly sent AppRequest */
215         this.thread_trace = 0;
216
217         /** Our queue of AppRequest objects */
218         this.req_queue = new Array();
219
220         /** Our queue of AppRequests that are awaiting a resend of their payload */
221         this.resend_queue = new Array();
222
223         // Build the transport_handle if if doesn't already exist
224         if( AppSession.transport_handle == null ) {
225                 this.build_transport();
226         }
227
228         AppSession.push_session( this );
229
230 }
231
232 /** The transport implementation is loaded from the config file and magically
233   * eval'ed into an object.  All connection settings come from the client 
234   * config.
235   * * This should only be called by the AppSession constructor and only when
236   * the transport_handle is null.
237   */
238 AppSession.prototype.build_transport = function() {
239
240         var config = new Config();
241         var transport_impl = config.get_value( "transport/transport_impl" );
242         if( ! transport_impl ) {
243                 throw new oils_ex_config( "No transport implementation defined in config file" );
244         }
245
246         var username    = config.get_value( "transport/username" );
247         var password    = config.get_value( "transport/password" );
248         var this_host   = config.get_value( "system/hostname" );
249         var resource    = this_host + "_" + new Date().getTime();
250         var server              = config.get_value( "transport/primary" );
251         var port                        = config.get_value( "transport/port" );
252         var tim                 = config.get_value( "transport/connect_timeout" );
253         var timeout             = tim * 1000;
254
255         var eval_string = 
256                 "AppSession.transport_handle = new " + transport_impl + "( username, password, resource );";
257
258         eval( eval_string );
259         
260         if( AppSession.transport_handle == null ) {
261                 throw new oils_ex_config( "Transport implementation defined in config file is not valid" );
262         }
263
264         if( !AppSession.transport_handle.connect( server, port, timeout ) ) {
265                 throw new oils_ex_transport( "Connection attempt to remote service timed out" );
266         }
267
268         if( ! AppSession.transport_handle.connected() ) {
269                 throw new oils_ex_transport( "AppSession is unable to connect to the remote service" );
270         }
271 }
272
273
274 /** Adds the given AppRequest object to this AppSession's request queue */
275 AppSession.prototype.add_request = function( req_obj ) {
276         new Logger().debug( "Adding AppRequest: " + req_obj.get_thread_trace(), Logger.DEBUG );
277         this.req_queue[req_obj.get_thread_trace()] = req_obj;
278 }
279
280 /** Removes the AppRequest object from this AppSession's request queue */
281 AppSession.prototype.remove_request = function( req_obj ) {
282         this.req_queue[req_obj.get_thread_trace()] = null;
283 }
284
285 /** Returns the AppRequest with the given thread_trace */
286 AppSession.prototype.get_request = function( thread_trace ) {
287         return this.req_queue[thread_trace];
288 }
289
290
291 /** Returns this AppSession's session id */
292 AppSession.prototype.get_session_id = function() { 
293         return this.session_id; 
294 }
295
296 /** Resets the remote_id for the transport to the original remote_id retrieved
297   * from the config file
298   */
299 AppSession.prototype.reset_remote = function() { 
300         this.remote_id = this.orig_remote_id; 
301 }
302
303 /** Returns the current message thread trace */
304 AppSession.prototype.get_thread_trace = function() {
305         return this.thread_trace;
306 }
307
308 /** Sets the current thread trace */
309 AppSession.prototype.set_thread_trace = function( tt ) {
310         this.thread_trace = tt;
311 }
312
313 /** Returns the state that this session is in (i.e. CONNECTED) */
314 AppSession.prototype.get_state = function() {
315         return this.state;
316 }
317
318 /** Sets the session state.  The state should be one of the predefined 
319   * session AppSession session states.
320   */
321 AppSession.prototype.set_state = function(state) {
322         this.state = state;
323 }
324
325 /** Returns the current remote_id for this session */
326 AppSession.prototype.get_remote_id = function() {
327         return this.remote_id;
328 }
329
330 /** Sets the current remote_id for this session */
331 AppSession.prototype.set_remote_id = function( id ) {
332         this.remote_id = id;
333 }
334
335 /** Pushes an AppRequest object onto the resend queue */
336 AppSession.prototype.push_resend = function( app_request ) {
337         this.resend_queue.push( app_request );
338 }
339
340 /** Destroys the current session.  This will disconnect from the
341   * remote service, remove all AppRequests from the request
342   * queue, and finally remove this session from the global cache
343   */
344 AppSession.prototype.destroy = function() {
345
346         new Logger().debug( "Destroying AppSession: " + this.get_session_id(), Logger.DEBUG );
347
348         // disconnect from the remote service
349         if( this.get_state() != AppSession.DISCONNECTED ) {
350                 this.disconnect();
351         }
352         // remove us from the global cache
353         AppSession.delete_session( this.get_session_id() );
354
355         // Remove app request references
356         for( var index in this.req_queue ) {
357                 this.req_queue[index] = null;
358         }
359 }
360
361 /** This forces a resend of all AppRequests that are currently 
362   * in the resend queue
363   */
364 AppSession.prototype.flush_resend = function() {
365
366         if( this.resend_queue.length > 0 ) {
367                 new Logger().debug( "Resending " 
368                         + this.resend_queue.length + " messages", Logger.INFO );
369         }
370
371         var req = this.resend_queue.shift();
372
373         while( req != null ) {
374                 req.resend();
375                 req = this.resend_queue.shift();
376         }
377 }
378
379 /** This method tracks down the AppRequest with the given thread_trace and 
380   * pushes the payload into that AppRequest's recieve queue.
381   */
382 AppSession.prototype.push_queue = function( dom_payload, thread_trace ) {
383
384         var req = this.get_request( thread_trace );
385         if( ! req ) {
386                 new Logger().debug( "No AppRequest exists for TT: " + thread_trace, Logger.ERROR );
387                 return;
388         }
389         req.push_queue( dom_payload );
390 }
391
392
393 /** Connect to the remote service.  The connect timeout is read from the config.
394   * This method returns null if the connection fails.  It returns a reference
395   * to this AppSession object otherwise.
396   */
397 AppSession.prototype.connect = function() {
398
399         if( this.get_state() == AppSession.CONNECTED ) { 
400                 return this;
401         }
402
403         var config = new Config();
404         var rem = config.get_value( "transport/connect_timeout" );
405         if( ! rem ) {
406                 throw new oils_ex_config( "Unable to retreive timeout value from config" );
407         }
408
409         var remaining = rem * 1000; // milliseconds
410
411         this.reset_remote();
412         this.set_state( AppSession.CONNECTING );
413         this.send( oilsMessage.CONNECT, 0, "" );
414
415         new Logger().debug( "CONNECTING with timeout: " + remaining, Logger.DEBUG );
416
417         while( this.get_state() != AppSession.CONNECTED && remaining > 0 ) {
418
419                 var starttime = new Date().getTime();
420                 this.queue_wait( remaining );
421                 var endtime = new Date().getTime();
422                 remaining -= (endtime - starttime);
423         }
424
425         if( ! this.get_state() == AppSession.CONNECTED ) {
426                 return null;
427         }
428
429         return this;
430 }
431
432 /** Disconnects from the remote service */
433 AppSession.prototype.disconnect = function() {
434
435         if( this.get_state() == AppSession.DISCONNECTED ) {
436                 return;
437         }
438
439         this.send( oilsMessage.DISCONNECT, this.get_thread_trace(), "" );
440         this.set_state( AppSession.DISCONNECTED );
441         this.reset_remote();
442 }
443
444
445 /** Builds a new message with the given type and thread_trace.  If the message
446   * is a REQUEST, then the payload is added as well.
447   * This method will verify that the session is in the CONNECTED state before
448   * sending any REQUEST's by attempting to do a connect.
449   *
450   * Note: msg_type and thread_trace must be provided.
451   */
452 AppSession.prototype.send = function( msg_type, thread_trace, payload ) {
453
454         if( msg_type == null || thread_trace == null ) {
455                 throw new oils_ex_args( "Not enough arguments provided to AppSession.send method" );
456         }
457
458         // go ahead and make sure there's nothing new to process
459         this.queue_wait(0);
460
461         var msg;
462         if( msg_type == oilsMessage.CONNECT ) {
463                 msg = new oilsMessage( msg_type, AppSession.PROTOCOL, this.auth );
464         } else {
465                 msg = new oilsMessage( msg_type, AppSession.PROTOCOL );
466         }
467
468         msg.setThreadTrace( thread_trace );
469
470         if( msg_type == oilsMessage.REQUEST ) {
471                 if( ! payload ) {
472                         throw new oils_ex_args( "No payload provided for REQUEST message in AppSession.send" );
473                 }
474                 msg.add( payload );
475         }
476
477
478         // Make sure we're connected
479         if( (msg_type != oilsMessage.DISCONNECT) && (msg_type != oilsMessage.CONNECT) &&
480                         (this.get_state() != AppSession.CONNECTED) ) {
481                 if( ! this.connect() ) {
482                         throw new oils_ex_session( this.get_session_id() + " | Unable to connect to remote service after redirect" );
483                 }
484         }
485
486         this.logger.debug( "AppSession sending tt: " 
487                         + thread_trace + " to " + this.get_remote_id() 
488                         + " " +  msg_type , Logger.INFO );
489
490         AppSession.transport_handle.send( this.get_remote_id(), this.get_session_id(), msg.toString(true) );
491
492 }
493
494
495 /** Waits up to 'timeout' milliseconds for some data to arrive.
496   * Any data that arrives will be process according to its
497   * payload and message type.  This method will return after
498   * any data has arrived.
499   * @param timeout How many milliseconds to wait or data to arrive
500   */
501 AppSession.prototype.queue_wait = function( timeout ) {
502         this.flush_resend(); // necessary if running parallel sessions 
503         new Logger().debug( "In queue_wait " + timeout, Logger.DEBUG );
504         var tran_msg = AppSession.transport_handle.process_msg( timeout );
505         this.flush_resend();
506 }
507
508
509