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.
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.
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.
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, a (P)IDL 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 - 14 Feb 2015