r20600: Web Application Framework
[kai/samba.git] / webapps / swat / source / class / swat / main / AbstractModuleFsm.js
1 /*
2  * Copyright:
3  *   (C) 2006 by Derrell Lipman
4  *       All rights reserved
5  *
6  * License:
7  *   LGPL 2.1: http://creativecommons.org/licenses/LGPL/2.1/
8  */
9
10 /**
11  * Common facilities for modules' finite state machines.  Each module's FSM
12  * should extend this class.
13  */
14 qx.OO.defineClass("swat.main.AbstractModuleFsm", qx.core.Object, function()
15 {
16   qx.core.Object.call(this);
17
18   // Create an array for pushing request objects
19   this._requests = [ ];
20 });
21
22
23 qx.Proto.buildFsm = function(module)
24 {
25   throw new Error("Module must overload buildFsm() " +
26                   "to build its custom finite state machine.");
27 };
28
29 qx.Proto.addAwaitRpcResultState = function(module)
30 {
31   var fsm = module.fsm;
32   var _this = this;
33
34   /*
35    * State: AwaitRpcResult
36    *
37    * Actions upon entry:
38    *  - enable any objects in group "swat.main.fsmUtils.enable_during_rpc"
39    *  - disable any objects in group "swat.main.fsmUtils.disable_during_rpc"
40    *
41    * Actions upon exit:
42    *   - disable any objects in group "swat.main.fsmUtils.enable_during_rpc"
43    *   - enable any objects in group "swat.main.fsmUtils.disable_during_rpc"
44    *
45    * Transition on:
46    *  "completed" (on RPC)
47    *  "failed" (on RPC)
48    *  "execute" on swat.main.fsmUtils.abort_rpc
49    */
50   var state = new qx.util.fsm.State(
51     "State_AwaitRpcResult",
52     {
53       "autoActionsBeforeOnentry" :
54       {
55         // The name of a function.
56         "setEnabled" :
57         [
58           {
59             // We want to enable objects in the group
60             // swat.main.fsmUtils.enable_during_rpc
61             "parameters" : [ true ],
62
63             // Call this.getObject(<object>).setEnabled(true) on
64             // state entry, for each <object> in the group called
65             // "swat.main.fsmUtils.enable_during_rpc".
66             "groups"      : [ "swat.main.fsmUtils.enable_during_rpc" ]
67           },
68
69           {
70             // We want to disable objects in the group
71             // swat.main.fsmUtils.disable_during_rpc
72             "parameters" : [ false ],
73
74             // Call this.getObject(<object>).setEnabled(false) on
75             // state entry, for each <object> in the group called
76             // "swat.main.fsmUtils.disable_during_rpc".
77             "groups"      : [ "swat.main.fsmUtils.disable_during_rpc" ]
78           }
79         ]
80       },
81
82       "autoActionsBeforeOnexit" :
83       {
84         // The name of a function.
85         "setEnabled" :
86         [
87           {
88             // We want to re-disable objects we had enabled, in the group
89             // swat.main.fsmUtils.enable_during_rpc
90             "parameters" : [ false ],
91
92             // Call this.getObject(<object>).setEnabled(false) on
93             // state entry, for each <object> in the group called
94             // "swat.main.fsmUtils.enable_during_rpc".
95             "groups"      : [ "swat.main.fsmUtils.enable_during_rpc" ]
96           },
97
98           {
99             // We want to re-enable objects we had disabled, in the group
100             // swat.main.fsmUtils.disable_during_rpc
101             "parameters" : [ true ],
102
103             // Call this.getObject(<object>).setEnabled(true) on
104             // state entry, for each <object> in the group called
105             // "swat.main.fsmUtils.disable_during_rpc".
106             "groups"      : [ "swat.main.fsmUtils.disable_during_rpc" ]
107           }
108         ]
109       },
110
111       "onentry" :
112         function(fsm, event)
113         {
114           var bAuthCompleted = false;
115
116           // See if we just completed an authentication
117           if (fsm.getPreviousState() == "State_Authenticate" &&
118               event.getType() == "complete")
119           {
120             bAuthCompleted = true;
121           }
122 _this.debug("bAuthCompleted=" + bAuthCompleted);
123
124           // If we didn't just complete an authentication and we're coming
125           // from some other state...
126           if (! bAuthCompleted &&
127               fsm.getPreviousState() != "State_AwaitRpcResult")
128           {
129             // ... then push the previous state onto the state stack
130 _this.warn("PUSHING STATE");
131             fsm.pushState(false);
132           }
133         },
134
135       "events" :
136       {
137         "execute"  :
138         {
139           "swat.main.fsmUtils.abort_rpc" :
140             "Transition_AwaitRpcResult_to_AwaitRpcResult_via_button_abort"
141         },
142
143         "completed" :
144           "Transition_AwaitRpcResult_to_PopStack_via_complete",
145
146         "failed" :
147           qx.util.fsm.FiniteStateMachine.EventHandling.PREDICATE
148       }
149     });
150   fsm.addState(state);
151
152   /*** Transitions that use a PREDICATE appear first ***/
153
154   /*
155    * Transition: AwaitRpcResult to GetAuthInfo
156    *
157    * Cause: "failed" (on RPC) where reason is PermissionDenied
158    */
159   var trans = new qx.util.fsm.Transition(
160     "Transition_AwaitRpcResult_to_Authenticate",
161     {
162       "nextState" :
163         "State_Authenticate",
164
165       "predicate" :
166         function(fsm, event)
167         {
168           var error = event.getData(); // retrieve the JSON-RPC error
169
170           // Did we get get origin=Server, and either
171           // code=NotLoggedIn or code=SessionExpired ? 
172           var origins = swat.main.AbstractModuleFsm.JsonRpc_Origin;
173           var serverErrors = swat.main.AbstractModuleFsm.JsonRpc_ServerError;
174           if (error.origin == origins.Server &&
175               (error.code == serverErrors.NotLoggedIn ||
176                error.code == serverErrors.SessionExpired))
177           {
178             return true;
179           }
180
181           // fall through to next transition, also for "failed"
182           return false;
183         },
184
185       "ontransition" :
186         function(fsm, event)
187         {
188           var caption;
189
190           var error = event.getData(); // retrieve the JSON-RPC error
191           var serverErrors = swat.main.AbstractModuleFsm.JsonRpc_ServerError;
192
193           switch(error.code)
194           {
195           case serverErrors.NotLoggedIn:
196             caption = "Please log in.";
197             break;
198
199           case serverErrors.SessionExpired:
200           default:
201             caption = "Session Expired.  Please log in.";
202             break;
203           }
204
205           // Retrieve the modal authentication window.
206
207           var loginWin = swat.main.Authenticate.getInstance(module);
208
209           // Set the caption
210           loginWin.setCaption(caption);
211
212           // Set the domain info
213           loginWin.setInfo(error.info);
214
215           // Open the authentication window
216           loginWin.open();
217         }
218     });
219   state.addTransition(trans);
220
221   /*
222    * Transition: AwaitRpcResult to PopStack
223    *
224    * Cause: "failed" (on RPC)
225    */
226   var trans = new qx.util.fsm.Transition(
227     "Transition_AwaitRpcResult_to_PopStack_via_failed",
228     {
229       "nextState" :
230         qx.util.fsm.FiniteStateMachine.StateChange.POP_STATE_STACK,
231
232       "ontransition" :
233         function(fsm, event)
234         {
235           // Get the request object
236           var rpcRequest = _this.getCurrentRpcRequest();
237           
238           // Generate the result for a completed request
239           rpcRequest.setUserData("result",
240                                   {
241                                       type : "failed",
242                                       data : event.getData()
243                                   });
244         }
245     });
246   state.addTransition(trans);
247
248   /*** Remaining transitions are accessed via the jump table ***/
249
250   /*
251    * Transition: AwaitRpcResult to AwaitRpcResult
252    *
253    * Cause: "execute" on swat.main.fsmUtils.abort_rpc
254    */
255   var trans = new qx.util.fsm.Transition(
256     "Transition_AwaitRpcResult_to_AwaitRpcResult_via_button_abort",
257     {
258       "nextState" :
259         "State_AwaitRpcResult",
260
261       "ontransition" :
262         function(fsm, event)
263         {
264           // Get the request object
265           var rpcRequest = _this.getCurrentRpcRequest();
266
267           // Issue an abort for the pending request
268           rpcRequest.request.abort();
269         }
270     });
271   state.addTransition(trans);
272
273   /*
274    * Transition: AwaitRpcResult to PopStack
275    *
276    * Cause: "complete" (on RPC)
277    */
278   var trans = new qx.util.fsm.Transition(
279     "Transition_AwaitRpcResult_to_PopStack_via_complete",
280     {
281       "nextState" :
282         qx.util.fsm.FiniteStateMachine.StateChange.POP_STATE_STACK,
283
284       "ontransition" :
285         function(fsm, event)
286         {
287           // Get the request object
288           var rpcRequest = _this.getCurrentRpcRequest();
289           
290           // Generate the result for a completed request
291           rpcRequest.setUserData("result",
292                                   {
293                                       type : "complete",
294                                       data : event.getData()
295                                   });
296         }
297     });
298   state.addTransition(trans);
299
300   /*
301    * State: Authenticate
302    *
303    * Transition on:
304    *  "execute" on login_button
305    */
306   var state = new qx.util.fsm.State(
307     "State_Authenticate",
308     {
309       "onentry" :
310         function(fsm, event)
311         {
312           // Retrieve the login window object
313           var win = module.fsm.getObject("login_window");
314
315           // Clear the password field
316           win.password.setValue("");
317
318           // If there's no value selected for domain...
319           if (win.domain.getValue() == null)
320           {
321             // ... then select the first value
322             win.domain.setSelected(win.domain.getList().getFirstChild());
323           }
324
325           // Retrieve the current RPC request
326           var rpcRequest = _this.getCurrentRpcRequest();
327
328           // Did we just return from an RPC request and was it a login request?
329           if (fsm.getPreviousState() == "State_AwaitRpcResult" &&
330               rpcRequest.service == "samba.system" &&
331               rpcRequest.params.length > 1 &&
332               rpcRequest.params[1] == "login")
333           {
334             // Yup.  Display the result.  Pop the old request off the stack
335             var loginRequest = _this.popRpcRequest();
336
337             // Retrieve the result
338             var result = loginRequest.getUserData("result");
339
340             // Did we succeed?
341             if (result.type == "failed")
342             {
343               // Nope.  Just reset the caption, and remain in this state.
344               win.setCaption("Login Failed.  Try again.");
345             }
346             else
347             {
348               // Login was successful.  Generate an event that will transition
349               // us back to the AwaitRpcResult state to again await the result
350               // of the original RPC request.
351               win.dispatchEvent(new qx.event.type.Event("complete"), true);
352
353               // Reissue the original request.  (We already popped the login
354               // request off the stack, so the current request is the original
355               // one.)
356               var origRequest = _this.getCurrentRpcRequest();
357               
358               // Retrieve the RPC object */
359               var rpc = fsm.getObject("swat.main.rpc");
360
361               // Set the service name
362               rpc.setServiceName(origRequest.service);
363
364               // Reissue the request
365               origRequest.request =
366                 qx.io.remote.Rpc.prototype.callAsyncListeners.apply(
367                   rpc,
368                   origRequest.params);
369
370               // Clear the password field, for good measure
371               win.password.setValue("");
372
373               // Close the login window
374               win.close();
375             }
376
377             // Dispose of the login request
378             loginRequest.request.dispose();
379             loginRequest.request = null;
380           }
381         },
382
383       "events" :
384       {
385         "execute"  :
386         {
387           "login_button" :
388             "Transition_Authenticate_to_AwaitRpcResult_via_button_login"
389         },
390
391         "complete"  :
392         {
393           "login_window" :
394             "Transition_Authenticate_to_AwaitRpcResult_via_complete"
395         }
396       }
397     });
398   fsm.addState(state);
399
400   /*
401    * Transition: Authenticate to AwaitRpcResult
402    *
403    * Cause: "execute" on login_button
404    */
405   var trans = new qx.util.fsm.Transition(
406     "Transition_Authenticate_to_AwaitRpcResult_via_button_login",
407     {
408       "nextState" :
409         "State_AwaitRpcResult",
410
411       "ontransition" :
412         function(fsm, event)
413         {
414           // Retrieve the login window object
415           var win = fsm.getObject("login_window");
416
417           // Issue a Login call
418           _this.callRpc(fsm,
419                         "samba.system",
420                         "login",
421                         [
422                           win.userName.getValue(),
423                           win.password.getValue(),
424                           win.domain.getValue()
425                         ]);
426         }
427     });
428   state.addTransition(trans);
429
430   /*
431    * Transition: Authenticate to AwaitRpcResult
432    *
433    * Cause: "complete" on login_window
434    *
435    * We've already re-issued the original request, so we have nothing to do
436    * here but transition back to the AwaitRpcResult state to again await the
437    * result of the original request.
438    */
439   var trans = new qx.util.fsm.Transition(
440     "Transition_Authenticate_to_AwaitRpcResult_via_complete",
441     {
442       "nextState" :
443         "State_AwaitRpcResult"
444     });
445   state.addTransition(trans);
446 };
447
448
449 /**
450  * Issue a remote procedure call.
451  *
452  * @param fsm {qx.util.fsm.FiniteStateMachine}
453  *   The finite state machine issuing this remote procedure call.
454  *
455  * @param service {string}
456  *   The name of the remote service which provides the specified method.
457  *
458  * @param method {string}
459  *   The name of the method within the specified service.
460  *
461  * @param params {Array}
462  *   The parameters to be passed to the specified method.
463  *
464  * @return {Object}
465  *   The request object for the just-issued RPC request.
466  */
467 qx.Proto.callRpc = function(fsm, service, method, params)
468 {
469   // Create an object to hold a copy of the parameters.  (We need a
470   // qx.core.Object() to be able to store this in the finite state machine.)
471   var rpcRequest = new qx.core.Object();
472
473   // Save the service name
474   rpcRequest.service = service;
475
476   // Copy the parameters; we'll prefix our copy with additional params
477   rpcRequest.params = params.slice(0);
478
479   // Prepend the method
480   rpcRequest.params.unshift(method);
481
482   // Prepend the flag indicating to coalesce failure events
483   rpcRequest.params.unshift(true);
484
485   // Retrieve the RPC object */
486   var rpc = fsm.getObject("swat.main.rpc");
487
488   // Set the service name
489   rpc.setServiceName(rpcRequest.service);
490
491   // Issue the request
492   rpcRequest.request =
493     qx.io.remote.Rpc.prototype.callAsyncListeners.apply(rpc,
494                                                         rpcRequest.params);
495
496   // Make the rpc request object available to the AwaitRpcResult state
497   this.pushRpcRequest(rpcRequest);
498
499   // Give 'em what they came for
500   return rpcRequest;
501 };
502
503
504 /**
505  * Push an RPC request onto the request stack.
506  *
507  * @param request {Object}
508  *   The just-issued rpc request object
509  */
510 qx.Proto.pushRpcRequest = function(rpcRequest)
511 {
512   this._requests.push(rpcRequest);
513 };
514
515
516 /**
517  * Retrieve the most recent RPC request from the request stack and pop the
518  * stack.
519  *
520  * @return {Object}
521  *   The rpc request object from the top of the request stack
522  */
523 qx.Proto.popRpcRequest = function()
524 {
525   if (this._requests.length == 0)
526   {
527     throw new Error("Attempt to pop an RPC request when list is empty.");
528   }
529
530   var rpcRequest = this._requests.pop();
531   return rpcRequest;
532 };
533
534
535 /**
536  * Retrieve the most recent RPC request.
537  *
538  * @return {Object}
539  *   The rpc request object at the top of the request stack
540  */
541 qx.Proto.getCurrentRpcRequest = function()
542 {
543   if (this._requests.length == 0)
544   {
545     throw new Error("Attempt to retrieve an RPC request when list is empty.");
546   }
547
548   return this._requests[this._requests.length - 1];
549 };
550
551
552 /**
553  * JSON-RPC error origins
554  */
555 qx.Class.JsonRpc_Origin =
556 {
557   Server              : 1,
558   Application         : 2,
559   Transport           : 3,
560   Client              : 4
561 };
562
563
564 /**
565  * JSON-RPC Errors for origin == Server
566  */
567 qx.Class.JsonRpc_ServerError =
568 {
569   /**
570    * Error code, value 0: Unknown Error
571    *
572    * The default error code, used only when no specific error code is passed
573    * to the JsonRpcError constructor.  This code should generally not be used.
574    */
575   Unknown               : 0,
576
577   /**
578    * Error code, value 1: Illegal Service
579    *
580    * The service name contains illegal characters or is otherwise deemed
581    * unacceptable to the JSON-RPC server.
582    */
583   IllegalService        : 1,
584
585   /**
586    * Error code, value 2: Service Not Found
587    *
588    * The requested service does not exist at the JSON-RPC server.
589    */
590   ServiceNotFound       : 2,
591
592   /**
593    * Error code, value 3: Class Not Found
594    *
595    * If the JSON-RPC server divides service methods into subsets (classes),
596    * this indicates that the specified class was not found.  This is slightly
597    * more detailed than "Method Not Found", but that error would always also
598    * be legal (and true) whenever this one is returned. (Not used in this
599    * implementation)
600    */
601   ClassNotFound         : 3, // not used in this implementation
602
603   /**
604    * Error code, value 4: Method Not Found
605    *
606    * The method specified in the request is not found in the requested
607    * service.
608    */
609   MethodNotFound        : 4,
610
611   /*
612    * Error code, value 5: Parameter Mismatch
613    *
614    * If a method discovers that the parameters (arguments) provided to it do
615    * not match the requisite types for the method's parameters, it should
616    * return this error code to indicate so to the caller.
617    *
618    * This error is also used to indicate an illegal parameter value, in server
619    * scripts.
620    */
621   ParameterMismatch     : 5,
622
623   /**
624    * Error code, value 6: Permission Denied
625    *
626    * A JSON-RPC service provider can require authentication, and that
627    * authentication can be implemented such the method takes authentication
628    * parameters, or such that a method or class of methods requires prior
629    * authentication.  If the caller has not properly authenticated to use the
630    * requested method, this error code is returned.
631    */
632   PermissionDenied      : 6,
633
634   /*** Errors generated by this server which are not qooxdoo-standard ***/
635
636   /*
637    * Error code, value 1000: Unexpected Output
638    *
639    * The called method illegally generated output to the browser, which would
640    * have preceeded the JSON-RPC data.
641    */
642   UnexpectedOutput      : 1000,
643
644   /*
645    * Error code, value 1001: Resource Error
646    *
647    * Too many resources were requested, a system limitation on the total number
648    * of resources has been reached, or a resource or resource id was misused.
649    */
650   ResourceError         : 1001,
651
652   /*
653    * Error code, value 1002: Not Logged In
654    *
655    * The user has logged out and must re-authenticate, or this is a brand new
656    * session and the user must log in.
657    *
658    */
659   NotLoggedIn           : 1002,
660
661   /*
662    * Error code, value 1003: Session Expired
663    *
664    * The session has expired and the user must re-authenticate.
665    *
666    */
667   SessionExpired        : 1003,
668
669   /*
670    * Error code, value 1004: Login Failed
671    *
672    * An attempt to log in failed.
673    *
674    */
675   LoginFailed           : 1004
676 };