All Manuals > Developing Component Software with CORBA® > 5 The Bank Client

5.4 Defining the interfaces

Note: This section assumes some basic familiarity with the CAPI library. See the CAPI Reference Manual for details.

In this section, we define three CAPI interface classes account-interface, checkingAccount-interface, and bank-interface. These classes are used to present graphical interfaces to CORBA objects with the IDL interfaces account, checkingAccount, and bank.

We begin by defining the interface class account-interface:

(capi:define-interface account-interface ()
  ((account-ref :initarg :account-ref)
   (account-name :initarg :account-name :accessor account-name)
   (bank-interface :initarg :owner))
  (:panes
   (balance-field capi:display-pane 
                  :title (:initarg :account-name)
                  :visible-min-width '(:character 10)
                  :visible-max-width nil)
   (button-panel capi:push-button-panel 
                 :callbacks '(credit debit)
                 :items '("Credit" "Debit")
                 :callback-type :interface))
  (:layouts 
   (account-layout capi:column-layout '(balance-field 
                                        button-panel)))
  (:default-initargs :auto-menus nil :max-width t))

This is how we use an instance of class account-interface. We store the name of the customer owning this account in the title of the display pane (using initarg :title).

The account-ref slot stores a CORBA object reference (of class BankingDemo:account) to the corresponding CORBA account object on the server. The bank-interface slot stores a pointer to the bank interface for this object.

The pane balance-field reports the state of the CORBA object's balance attribute as a readonly text field. We delegate the initialization of this field value to an initialize-instance after method specialized on
account-interface. The value needs to be updated after each invocation of a CORBA debit or credit operation.

The button panel button-panel defines buttons to activate callbacks
debit-callback and credit-callback. These callbacks prompt the user for amounts and then invoke the corresponding CORBA operations debit and credit on the object reference stored in the account-ref field. We will implement these callbacks in a moment.

The buttons are laid out in a column layout account-layout. Mirroring the fact that the IDL interface checkingAccount inherits from account, we define the Common Lisp frame class checking-account-interface as a subclass of account-interface:

(capi:define-interface checking-account-interface 
    (account-interface) ()
  (:panes
   (limit-field capi:display-pane 
                :visible-min-width '(:character 10)
                :visible-max-width nil))
  (:layouts
   (checking-account-layout capi:column-layout 
                            '(account-layout limit-field))))

The pane limit-field reports the state of the CORBA object's limit attribute as a readonly text field. Again, we can delegate the initialization of this field's value to an initialize instance after method specialized on
checking-account-interface.

The layout checking-account-layout simply lays out the inherited layout account-layout, containing the account's balance, together with the additional limit-field.

The definition of bank-interface class follows the same pattern:

(capi:define-interface bank-interface ()
  ((bank-ref :initarg :bank-ref))
  (:menu-bar open-actions)
  (:menus
   (open-actions
    "Action"
    (("Open Account" :callback 'open-account-callback)
     ("Open Checking Account" :callback 
                              'open-checking-account-callback)
     ("Retrieve Account" :callback 'retrieve-account-callback)
     ("Close Account" :callback 'close-account-callback))
    :callback-type :interface))
  (:layouts 
   (accounts-area capi:row-layout () 
                  :accessor accounts-area
                  :horizontal-scroll t))
  (:default-initargs :auto-menus nil :best-width 400))

The accounts-area layout keeps track of the account-interfaces created by the bank interface as the result of invoking operations on the CORBA bank object. This list is maintained to prevent the user from obtaining more than one interface to the same account. We need to update it whenever an account interface is exited.

The interface menu items Open Account, Open Checking Account, Retrieve Account, and Close Account activate callbacks open-Account-callback,
open-Checking-Account-callback, retrieve-Account-callback, and close-Account-callback.

These callbacks prompt the user for appropriate arguments and then invoke the corresponding CORBA operations openAccount, openCheckingAccount, retrieveAccount, and closeAccount on the object reference stored in the bank-ref slot. We will see the implementation of these callbacks in a moment.

5.4.1 Initializing and exiting account frames

Each time we make a new account interface we want to ensure two things:

An easy way to do this is to add an initialize-instance after method specialized on account-interface. (In Common Lisp, each call to make an instance of a given class is automatically followed by a call to initialize that instance; you are free to specialize the initialize-instance generic function on particular classes.)

(defmethod initialize-instance :after ((self account-interface)     &key)
  (with-slots (account-ref balance-field) self
    (when account-ref
      (setf (capi:display-pane-text balance-field)
            (format nil "~A" (op:balance account-ref))))))

Here, we encounter our first example of invoking a CORBA operation on a CORBA object. The Common Lisp variable account-ref, of class BankingDemo:account, contains a proxy for a CORBA account object on the server. The application (op:balance account-ref) invokes a stub method specialized on the proxy's class. The stub method forwards the request across the ORB to the actual object on the server.

The request is executed on the object in the server and the result passed back across the ORB to the stub method, which returns the value to the client as a corba:long. This value is then used to set the initial value of the balance field.

We can initialize the interface for a checking account in a similar way:

(defmethod initialize-instance :after (
     (self checking-account-interface)  &key)
  (with-slots (account-ref limit-field) self
    (when account-ref
      (setf (capi:display-pane-text limit-field)
            (format nil "~A" (op:limit account-ref))))))

Inheritance ensures that the method on account-interface is called which registers the interface and sets up its balance field; a call to the op:limit stub determines the initial value of its limit field.

For convenience, we define a generic function make-account-frame that makes the correct class of frame for a given account object reference:

(defmethod make-account-frame ((self BankingDemo:account) 
                                &key bank-interface title)
  (push-new-item bank-interface
                 title
                 (make-instance 'account-interface 
                                :account-ref self
                                :account-name title
                                :owner bank-interface)))
(defmethod make-account-frame ((self BankingDemo:checkingAccount)
                                &key bank-interface title)
  (push-new-item bank-interface
                 title
                 (make-instance 'checking-account-interface 
                                :account-ref self
                                :account-name title
                                :owner bank-interface)))

These methods simply dispatch on the class of the object reference to make an account-interface or checkingAccount-interface as appropriate.

5.4.2 Defining the callbacks

Defining the callbacks attached to each button is straightforward. Recall that in CAPI, because we stated that the button callback type was :interface, the argument passed to a callback is the interface whose activation triggered that callback.

The credit-callback is activated by the Credit button of some account interface:

(defun credit (self)
  (with-slots (balance-field account-ref) self
    (let ((amount (capi:prompt-for-integer "Amount?" :min 0)))
       (when amount
        (op:credit account-ref amount)
        (setf (capi:display-pane-text balance-field)
              (format nil "~A" (op:balance account-ref)))))))

The callback is passed the account interface. It then extracts the CORBA object reference stored in the frame's account-ref slot and prompts the user for an amount. The function capi:prompt-for-integer queries the user for an integer and returns nil if the user cancels the dialog. If the amount is valid, the callback invokes the stub method op:credit on the CORBA object reference with the specified absolute value of the amount (recall that the credit operation expects an unsigned long as its argument). Finally, it updates the balance field of the frame with the current value of the object's balance attribute, obtained by invoking the stub method op:balance.

The definition of debit-callback is very similar to the definition of credit-callback:

(defun debit (self)
  (with-slots (balance-field account-ref) self
    (let ((amount (capi:prompt-for-integer "Amount?" :min 0)))
      (when amount
        (handler-case
            (progn
              (op:debit account-ref amount)
              (setf (capi:display-pane-text balance-field)
                    (format nil "~A" (op:balance account-ref))))
          (BankingDemo:account/refusal 
           (xx)
           (capi:display-message "Debit returned refusal with 
                                 string: <~A>"
                                 (op:reason xx))))))))

The only difference is that debit-callback must deal with the additional possibility that the debit operation, when invoked on the target object, may fail, raising the IDL exception refusal. If the object raises this exception, the op:debit stub method signals it as a Common Lisp condition of class
BankingDemo:account/refusal.

The exception can then be caught and handled in any of the standard Common Lisp ways. Here, we simply place the invocation in the body of a handler-case statement with an appropriate exception clause to handle the condition.

The open-account-callback is activated by the openAccount-button of some bank frame:

(defun open-account-callback (self)
  (with-slots (bank-ref) self
    (let ((name (capi:prompt-for-string "Name?")))
      (when name
        (handler-case
            (let ((account-ref
                   (op:openaccount bank-ref name)))
              (make-account-frame account-ref 
               :bank-interface self :title name))
          (Bankingdemo:Bank/DuplicateAccount 
           ()
           (capi:display-message "Cannot create account for 
                                ~A" name)))))))

The callback extracts the CORBA object reference stored in the interface's bank-ref slot. The function capi:prompt-for-string queries the user for the new customer's name returning a string (or nil if the user cancels the dialog). If the dialog has not been cancelled, the callback invokes the stub method op:openAccount on the target object reference bank, passing the argument name. If successful, the invocation returns an object reference, of class BankingDemo:account, to an IDL account object, which is then used to make and start a new account-interface, via a call to make-account-frame.

Recall that the IDL operation openAccount may fail, raising the IDL user exception duplicateAccount. As in the definition of debit-callback, we cater for this eventuality by placing the invocation in the body of a
handler-case statement and install a handler on the corresponding Common Lisp condition of class BankingDemo:bank/duplicateAccount. This handler simply informs the user of the exception using the CAPI function
display-message to create and display a simple alert dialog box.

The definition of open-checking-account-callback is similar to the definition of openAccount-callback but prompts the user for an additional integer to pass as the overdraft limit of the new checking account:

(defun open-checking-account-callback (self)
  (with-slots (bank-ref) self
    (let ((name (capi:prompt-for-string "Name?")))
      (when name
        (let ((limit (capi:prompt-for-integer "Limit?")))
          (when limit
            (handler-case
                (let ((account-ref
                       (op:opencheckingaccount bank-ref 
                                              name limit)))
                  (make-account-frame account-ref 
                   :bank-interface self :title name))
              (Bankingdemo:Bank/DuplicateAccount 
               ()
               (capi:display-message "Cannot create another
                                    account for ~A" name)))))))))

While openAccount and openCheckingAccount create accounts for new customers, the retrieveAccount operation is simply meant to look up the account of an existing customer:

(defun retrieve-account-callback (self)
  (with-slots (bank-ref) self
    (let ((name (capi:prompt-for-string "Name?")))
      (when name
        (if (find-named-frame self name)
            (capi:display-message "Already viewing it...")
          (handler-case
              (let ((account-ref
                     (op:retrieveaccount bank-ref name)))
                (when (op:Is_a account-ref (op:id 
                   Bankingdemo:_Tc_Checkingaccount))
                  (setf account-ref 
                        (op:narrow 'Bankingdemo:Checkingaccount
                                    account-ref)))
                (make-account-frame account-ref 
                  :bank-interface self :title name))
            (Bankingdemo:Bank/NonExistentAccount 
             ()
             (capi:display-message "No account exists for 
                                   name ~A" name))))))))

This callback incorporates a test that prevents the user from being presented with more than one interface to the same account. It invokes the stub method op:retrieveAccount only if the account under that name is not already on display. Because of IDL inheritance, the server implementing the IDL retrieveAccount operation may return any object reference whose interface inherits from the IDL account interface.

In particular, the server may return an IDL checkingAccount as a special instance of an IDL account. In Common Lisp terms, this means that the stub method Op:retrieveAccount may return an object reference of class
BankingDemo:checkingAccount as a special instance of
BankingDemo:account. The call to make-account-frame dispatches on the actual, or most derived, class of the resulting object reference, making an account-interface or checking-account-interface as appropriate.

The definition of the close-account-callback is straightforward:

(defun close-account-callback (self)
  (with-slots (bank-ref) self
    (let ((name (capi:prompt-with-list (all-frame-names self)
                                       "Choose account")))
      (when name
        (op:closeaccount bank-ref 
                         (with-slots (account-ref)
                             (find-named-frame self name)
                           account-ref))
        (remove-account-frame self name)))))

The function prompt-with-list presents a dialog asking the user to select a name from the list of available account frames (indexed by their
account-name), returning nil if the user decides to cancel the dialog. Given a valid selection, the callback invokes the stub method op:closeAccount on the target object reference, bank-ref, passing the name of the selected account. Finally, the account interface is removed from the bank interface.

5.4.3 Initializing the ORB and obtaining the first object reference

A client can only communicate with a CORBA object if it possesses a reference to that object. This raises the question of how the client obtains its initial object reference. The fact that some IDL operation may return an object reference is of no help here: without a reference to specify as its target, there is no way to invoke this operation.

In more detail, before a client can enter the CORBA environment, it must first:

CORBA provides a standard set of operations, specified in pseudo IDL (PIDL), to initialize applications and obtain the appropriate object references.

Operations providing access to the ORB reside in the CORBA module. (Like an IDL interface declaration, an IDL or PIDL module declaration defines a new namespace for the body of declarations it encloses. What it does not do is define a new type of CORBA object.) Operations providing access to an Object Adapter, Interface Repository, Naming Service, and other Object Services reside in the ORB interface defined within the CORBA module.

To provide some flavor of PIDL, here is a fragment of the PIDL specification of CORBA that we rely on in our implementation of the bank client.

module CORBA { 
  interface Object { 
    boolean is_a (in string logical_type_id); 
  ... 
  }; 
  interface ORB { 
    string object_to_string (in Object obj); 
    Object string_to_object (in string str); 
  ... 
  }; 
...
  typedef string ORBid; 
  typedef sequence <string> arg_list;
  ORB ORB_init (inout arg_list argv, in ORBid orb_identifier);
}; 

The Object interface is implicitly inherited by all IDL interfaces, much as every Common Lisp class inherits from the class standard-object.

The is_a operation provides a test for inheritance (the logical_type_id is a string representation of an interface identifier). The operation returns true if the object is an instance of that interface, including if that interface is an ancestor of the most derived interface of that object.

The ORB operations object_to_string and string_to_object provide an invertible mapping from object references to their representations as strings.

Notice that the CORBA operation ORB_init is defined outside the scope of any interface, providing a means of bootstrapping into the CORBA world. Calling ORB_init initializes the ORB, returning an ORB pseudo-object that can be used as the target for further ORB operations.

Like most other language bindings, the Common Lisp binding adopts the pseudo-objects approach in which these CORBA and ORB operations are accessed by applying the binding's normal IDL mapping rules to the PIDL specification.

In this tutorial, we use a very simple technique to obtain the initial object reference. The client assumes that the server has published a reference to its implementation of the bank object, encoded as a string, in a shared file. After starting up, the client reads the file, decodes the string as an object reference, and then uses this reference as the target of further operations.

Here is the remaining Common Lisp code that completes the implementation of the client:

(defun bank-client ()
  (let ((orb (op:orb_init nil "LispWorks ORB")))
    (let ((bank-ref (op:narrow 'BankingDemo:bank
                               (file-to-object orb))))
      (capi:display (make-instance 'bank-interface 
                                   :bank-ref bank-ref
                                   :title "Corba Bank")))))

The defparameter *bank-ior-file* is the name of the shared file used to pass the reference of the bank object from the server to the client.

The method file-as-string reads a file's contents.

The top-level let statement first initializes The LispWorks ORB by calling the Common Lisp generic function op:ORB_init corresponding to the PIDL ORB_init operation. The first argument to this call is an empty list. Passing an empty sequence instructs the op:ORB_init function to ignore this argument and use the application's command line arguments (if any) instead. The value of the second argument, "LispWorks ORB", merely identifies the ORB to use.

Invoking op:string_to_object on this ORB, passing the string read from the shared file, reconstitutes the string as an unspecific object reference of class CORBA:Object. Calling the op:narrow method on this object reference narrows (that is, coerces) it to a more specific object reference of class
BankingDemo:bank. (The op:narrow method employs an implicit call to the object's is_a operation to check that the desired coercion is safe.)

Finally, the resulting object reference bank-ref, of class BankingDemo:bank, is used to make and start a new bank interface, displaying the initial GUI to the user. The implementation of the client is now complete.


Developing Component Software with CORBA® - 01 Dec 2021 19:38:36