The Bank Server

The server

In this chapter, we design and implement a CORBA server, using Dylan and the Open Dylan ORB.

Our server presents an object-oriented interface to a bank object and its accounts. Because we want the bank’s account records to persist beyond the lifetime of the server, we store the account records in a Microsoft Access™ relational database. The server manipulates this database using Open Dylan’s SQL-ODBC library.

Since the primary motivation for this tutorial is to illustrate the use of CORBA, we will not spend too much time on the use of the SQL-ODBC library. Instead, we concentrate on how to provide implementations of CORBA objects, and how to make those implementation available to the CORBA environment.

A very small amount of SQL knowledge is assumed in early sections of this chapter.

The ODBC database

The folder Examplescorbabankbank-server contains a prepared Microsoft Access database file bankDB.mdb that the server uses to record account details.

This database contains a single SQL table called Accounts:

Name

Balance

Limit

Jack

0

NULL

Jill

-100

200

The Accounts table has three columns: Name, Balance, and Limit. Each record in the table represents an account.

  • The Name column contains values of SQL type string that are used to uniquely identify account records.

  • The Balance column contains values of SQL type long, reflecting the current balance of each account.

  • The Limit column contains either the distinguished SQL value NULL that indicates an absent value, or a value of SQL type long, reflecting the overdraft limit of a checking account.

Note that both accounts and checking accounts are stored as records in the same table. By convention, we interpret a record with a NULL Limit value an ordinary account. We interpret a record with a non-NULL, and thus integral, Limit value as a checking account with the given overdraft limit.

For instance, the Accounts table above contains two records, one for Jack’s account and one for Jill’s checking account. (Recall that checking accounts can have negative balances while ordinary accounts cannot.)

Overview of the Open Dylan SQL-ODBC library

The SQL-ODBC library is built on top of a generic SQL library. The SQL library does not include the low-level code necessary to communicate with any particular database management system (DBMS): it simply provides a convenient high-level mechanism for integrating database operations into Dylan applications.

The SQL library is designed to form the high-level part of implementation libraries that contain lower-level code to support a particular DBMS protocol, such as ODBC. The SQL-ODBC library is one such implementation library.

The SQL library defines the following abstract classes:

  • The abstract class <dbms> is used to identify a database management system.

    The SQL-ODBC library defines an instantiable subclass <odbc-dbms> of <user> for identifying the ODBC DBMS.

  • The abstract class <user> identifies users to a DBMS.

    Exactly what a user means depends on the DBMS. The make method on <user> takes two keyword arguments, user: and password:. The user: init-keyword takes an instance of <string> that should be a valid user name for the DBMS in question. The password: init-keyword should be the password that accompanies the user name.

    The SQL-ODBC library defines the instantiable subclass <odbc-user> of <user> for identifying a user to an ODBC DBMS.

  • The abstract class <database> identifies a database to a DBMS.

    Exactly what a database is depends on the DBMS in question. The SQL-ODBC library defines the class <odbc-database>, a subclass of <database>, whose instances identify databases to an ODBC DBMS. In particular, the make method on <odbc-database> accepts the datasource-name: keyword argument to specify the name of the ODBC datasource, as a <string>.

  • The abstract class <connection> represents database connections.

    Instances of this class identify an execution context for executing SQL statements. The exact composition of a connection depends on the DBMS.

    The SQL-ODBC library defines the class <odbc-connection>, a subclass of <connection>. Instances of this class are created upon making a connection an ODBC database.

The <sql-statement> class represents SQL statements and their state. This class has the init-keywords text:, input-indicator:, and output-indicator:.

The required keyword text: expects a <string> that contains the text of an SQL statement. Host variables can be included in the statement by placing a question mark (?) at the point in the string at which the value of the host variable should be substituted. The optional keyword argument output-indicator: expects an instance of <object>. The output indicator is a substitution value to be used whenever the column of a retrieved record contains the SQL NULL value. The optional keyword input-indicator: expects an instance of <object>. The input indicator is a marker value used to identify SQL NULL values in host variables.

The SQL library defines two convenient macros that we use in this tutorial: with-dbms and with-connection. Here is the form of a with-dbms call:

with-dbms (dbms)
  // body
end with-dbms;

The with-dbms statement macro considers dbms, which must be a general instance of class <dbms>, to be the DBMS in use throughout body. For example, if dbms is an instance of <odbc-dbms> and body contains a call to connect, then the call actually returns an <odbc-connection>.

Here is the form of a with-connection call:

with-connection (connection)
  // body
end with-connection;

The with-connection statement macro considers connection, which must be an instance of class <connection>, to be the default database connection in use throughout body . For instance, each call to execute an SQL statement within body uses connection by default, so that the call’s connection: keyword argument need not be supplied.

A call to the generic function connect returns a new connection of class <connection> to the specified database database as the user user . The connection can be closed by a call to disconnect.

A call to the generic function execute(sql-statement, parameter: vector) executes the SQL statement on the default connection. The (optional) parameter: argument supplies a vector of values to be substituted for any host variables appearing in the statement’s text. The n th entry of this vector determines the value of the n th host variable. Vector entries that equal the value of the statement’s input-indicator: keyword argument are sent as SQL NULL values.

If the SQL statement is a SELECT statement, then the result of executing the statement (with execute) is a value of class <result-set>, which is itself a subclass of Dylan’s built in <sequence> class. Each element of a result set is a record and each element of a record is a value. The various Dylan collection protocols and functions work as you would expect on a result set. For the purpose of this tutorial, it suffices to think of a result set as a sequence of vectors.

Just to illustrate the use of the SQL-ODBC library without worrying about the implementation of our CORBA server, here is a code fragment that might be used to extract the entries in the Name and Balance columns of the bankDB.mdb database:

begin
  // choose the DBMS
  let odbc-dbms = make(<odbc-dbms>);
  with-dbms (odbc-dbms)
    // identify the database
    let database = make(<database>, datasource-name: "bankDB");

    // identify the user
    let user = make(<user>, user-name: "", password: "");

    // establish a connection for this database and user
    let connection = connect(database, user);
    with-connection (connection) // make it the default
      let query1 =               // construct the query...
        make(<sql-statement>,
             text: "select (Name, Balance) from Accounts");

       // ... and execute it on the default connection
       let result-set = execute(query);

       // extract the first record
       let first-record = result-set[0];

       // extract the first field of the first record.
       let first-name = result-set[0][0];
       let first-balance = result-set[0][1];
       let second-record = result-set[1];

       // ...
    end with-connection;
    disconnect(connection); // disconnect from the database
  end with-dbms;
end;

Implementing CORBA objects in a server

A CORBA server has to provide an implementation object, called a servant, for each of the proxy objects that might be manipulated by a client. Our server needs to implement the initial bank servant, and then create new servants for each of the account objects created in response to openAccount, openCheckingAccount and retrieveAccount requests. Each of these servants needs to be registered in the CORBA environment and assigned an object reference, so that the ORB can direct incoming requests to the appropriate servant.

In CORBA, the primary means for an object implementation to access ORB services such as object reference generation is via an object adapter.

Object adapters

An object adapter is responsible for the following functions:

  • Generation and interpretation of object references

  • Registration of servants

  • Mapping object references to the corresponding servants

  • IDL method invocations, mediated by skeleton methods

  • Servant activation and deactivation

The Open Dylan ORB library provides an implementation of the Portable Object Adapter (POA). This object adapter forms part of the CORBA standard and, like the ORB, has an interface that is specified in pseudo IDL (PIDL). The Open Dylan interface to the POA conforms closely to the interface obtained by applying the Dylan mapping rules to the POA’s PIDL specification.

A POA object manages the implementation of a collection of objects, associating object references with specific servants. While the ORB is an abstraction visible to both the client and server, POA objects are visible only to the server. User-supplied object implementations are registered with a POA and assigned object references. When a client issues a request to perform an operation on such an object reference, the ORB and POA cooperate to determine which servant the operation should be invoked on, and to perform the invocation as an upcall through a skeleton method.

The POA allows several ways of using servants although it does not deal with the issue of starting the server process. Once started, however, there can be a servant started and ended for a single method call, a separate servant for each object, or a shared servant for all instances of the object type. It allows for groups of objects to be associated by means of being registered with different instances of the POA object and allows implementations to specify their own activation techniques. If the implementation is not active when an invocation is performed, the POA will start one.

Unfortunately, the flexibility afforded by the POA means that its interface is complex and somewhat difficult to use. The example in this tutorial makes only elementary use of the POA.

Here is the PIDL specification of the facilities of the POA that are used in this tutorial:

module PortableServer {
  native Servant;

  interface POAManager {
    exception AdapterInactive{};
    void activate() raises (...);
    ...
  };

  interface POA {
    exception WrongAdapter {};
    readonly attribute POAManager the_POAManager;
    Object servant_to_reference(in Servant p_servant)
      raises (...);
    Servant reference_to_servant(in Object reference)
      raises (WrongAdapter, ...);
    ...
  };
};

The POA-related interfaces are defined in a module separate from the CORBA module, called PortableServer . That module declares several interfaces, of which only the POA and POAManager are shown here.

The PortableServer module specifies the type Servant. Values of type Servant represent language-specific implementations of CORBA interfaces. Since this type can only be determined by the programming language in question, it is merely declared as a native type in the PIDL.

In the Dylan mapping, the Servant type maps to the abstract class PortableServer/<Servant>. User-defined Dylan classes that are meant to implement CORBA objects and be registered with a POA must inherit from this abstract class.

Each POA object has an associated POAManager object. A POA manager encapsulates the processing state of the POA it is associated with. Using operations on the POA manager, an application can make requests for a POA to be queued or discarded, and can have the POA deactivated.

A POA manager has two main processing states, holding and active , that determine the capabilities of the associated POA and the handling of ORB requests received by that POA. Both the POA manager and its associated POA are initially in the holding state.

When a POA is in the holding state, it simply queues requests received from the ORB without dispatching them to their implementation objects. In the active state, the POA receives and processes requests.

Invoking the POA Manager’s activate operation causes it, and its associated POA, to enter the active state.

A POA object provides two useful operations that map between object references and servants: servant_to_reference and reference_to_servant.

The servant_to_reference operation has two behaviors. If the given servant is not already active in the POA, then the POA generates a new object reference for that servant, records the association in the POA, and returns the reference. If the servant is already active in the POA, then the operation merely returns its associated object reference.

The reference_to_servant operation returns the servant associated with a given object reference in the POA. If the object reference was not created by this POA, the operation raises the WrongAdapter exception.

The server’s perspective

From the perspective of the server, the Bank-Protocol library specifies the protocol that its servants must implement in order to satisfy the interfaces in the IDL bank.idl. A partial implementation of this protocol resides in the Bank-Skeletons library generated by the IDL compiler. This library should be used by any application that wants to act as a server by providing an implementation for a CORBA object matching an interface in the bank.idl file.

The Bank-Skeletons library defines an abstract servant class for each of the protocol classes corresponding to an IDL interface. Each of these classes inherits from the abstract class PortableServer/<Servant>, allowing instances of these classes to be registered with a POA.

A server provides an implementation of an abstract servant class by defining a concrete subclass of that class, called an implementation class, and defining methods, specialized on the implementation class, for each of the protocol functions corresponding to an IDL attribute or operation.

The Bank-Skeletons library defines a concrete skeleton method, specialized on the appropriate abstract servant class, for each protocol function stemming from an IDL attribute or operation. When the POA receives a request from a client through the ORB it looks up the servant targeted by that request, and invokes the corresponding skeleton method on that servant. The skeleton method performs an upcall to the method that implements the protocol function for the implementation class of the servant. If the upcall succeeds, the skeleton method sends the result to the client. If the method raises a Dylan condition corresponding to a CORBA user or system exception, the skeleton method sends the CORBA exception back to the client.

Requirements for implementing the bank server

As there were for the bank client, there are three parts to implementing the bank server:

  • Write the code to initialize the CORBA ORB, set up the POA and POA manager, and get an initial object reference.

  • Write the code for the CORBA objects that the server provides.

  • Write the code for the server GUI.

We start by writing the CORBA object code. As noted in Section 6.4, this entails writing concrete servant implementations.

The bank server GUI

Since this demonstration principally concerns CORBA, and because we would like to revamp the look-and-feel of the demonstration occasionally, we do not describe the GUI implementation in great detail. Instead, only a brief outline of the current design is given.

The bank server consists of one window that shows a table of raw account data. Each row in the table shows the name, the current balance, and the overdraft limit data.

There is also a log window for viewing incoming requests. The full implementation of the server GUI can be found in the file server-frame.dylan.

The bank server library and module

The bank server is implemented as a library:

define library bank-server
  use common-dylan;
  use dylan-orb;
  use bank-skeletons;
  use sql-odbc;
  use duim;
  // ...
end library bank-server;

that defines a single module:

define module bank-server
  use common-dylan;
  use dylan-orb;
  use bank-skeletons;
  use sql-odbc;
  use duim;
  use threads;
  // ...
end module bank-server;

Like the client, our server needs to use the Dylan-ORB system library and module, in addition to its application specific libraries. Because the server provides implementations (or servants) for CORBA objects satisfying interfaces defined in the bank.idl file, it also needs to use the Bank-Skeletons library and module.

Interoperating with ODBC requires the SQL-ODBC library and module.

Finally, our implementation of the server makes non-essential use of the DUIM and Threads libraries and modules to present the user with a dialog to shutdown the server. The full source code for the server is in the bank-server.dylan file.

Implementing the servant classes

The Bank-Skeletons library defines three abstract servant classes:

BankingDemo<account-servant>
BankingDemo/<checkingAccount-servant>
BankingDemo/<bank-servant>

These classes correspond to the IDL interfaces account, checkingAccount, and bank.

The class BankingDemo/<checkingAccount-servant> is defined to inherit from BankingDemo<account-servant>, matching the inheritance relationship declared in the IDL.

Each class inherits from the abstract class PortableServer/<Servant>. This allows instances of the class to be registered with a POA.

In our implementation of the bank server, these servant classes are implemented by the following concrete subclasses:

<bank-implementation>
<account-implementation>
<checkingAccount-implementation>

The <bank-implementation> class implements BankingDemo/<bank-servant> by representing a bank as a connection to a database:

define class <bank-implementation> (BankingDemo/<bank-servant>)
  slot connection :: <connection>,
    required-init-keyword: connection:;
  constant slot poa :: PortableServer/<POA>,
    required-init-keyword: poa:;
  constant slot name :: CORBA/<string>,
    required-init-keyword: name:;
end class <bank-implementation>;

The bank implementation class includes the slot poa to record the POA in which the bank servant is active, so that servants representing accounts at the bank can be registered in the same POA.

The <account-implementation> class implements BankingDemo/<account-servant>:

define class <account-implementation>
    (BankingDemo/<account-servant>)
  constant slot bank :: <bank-implementation>,
    required-init-keyword: bank:;
  constant slot name :: CORBA/<string>,
    required-init-keyword: name:;
end class <account-implementation>;

An instance of this class represents an account. The bank slot provides a connection to the database that holds the account’s record. The name slot identifies the record in the database.

Finally, the <checkingAccount-implementation> class implements BankingDemo/<checkingAccount-servant> simply by inheriting from <account-implementation>:

define class <checkingAccount-implementation>
  (<account-implementation>,
   BankingDemo/<checkingAccount-servant>)
end class <checkingAccount-implementation>;

Implementing the servant methods

The next step in implementing the server is to define methods, specialized on the implementation classes, for each of the protocol functions corresponding to an IDL attribute or operation.

To support this, the abstract servant classes:

BankingDemo/<account-servant>
BankingDemo/<checkingAccount-servant>
BankingDemo/<bank-servant>

are defined to inherit, respectively, from the abstract protocol classes:

BankingDemo/<account>
BankingDemo/<checkingAccount>
BankingDemo/<bank-servant>

As a result, implementing a protocol function boils down to defining a concrete method for that function, where the method specializes on the implementation class of its target object. Recall that the target object of a protocol function is the first parameter to that function.

We can now present the implementations of the protocol functions. The BankingDemo/account/name method returns the value of the account’s name slot:

define method BankingDemo/account/name
    (account :: <account-implementation>)
 => (name :: CORBA/<string>)
  account.name
end method BankingDemo/account/name;

The BankingDemo/account/balance method retrieves the balance field from the corresponding record on the database by executing an SQL SELECT statement:

define method BankingDemo/account/balance
    (account :: <account-implementation>)
    => (balance :: CORBA/<long>)
  with-connection(account.bank.connection)
    let query = make(<sql-statement>,
                     text: "SELECT Balance FROM Accounts "
                           "WHERE Name = ?");
    let result-set = execute(query,
                             parameters: vector(account.name));
    as(CORBA/<long>, result-set[0][0]);
  end with-connection;
end method BankingDemo/account/balance;

The BankingDemo/account/balance method increments the record’s balance field by executing an SQL UPDATE statement:

define method BankingDemo/account/credit
    (account :: <account-implementation>,
     amount :: CORBA/<unsigned-long>)
    => ()
  with-connection(account.bank.connection)
    let amount = abs(amount);
    let query = make(<sql-statement>,
                     text: "UPDATE Accounts "
                           "SET Balance = Balance + ? "
                           "WHERE Name = ?");
    execute(query, parameters: vector(as(<integer>, amount),
            account.name));
  end with-connection;
end method BankingDemo/account/credit;

The BankingDemo/account/debit method executes an SQL UPDATE statement that decrements the record’s balance field, provided the balance exceeds the desired amount:

define method BankingDemo/account/debit
    (account :: <account-implementation>, amount :: CORBA/<long>)
 => ()
  with-connection(account.bank.connection)
    let amount = abs(amount);
    let query = make(<sql-statement>,
                     text: "UPDATE Accounts "
                           "SET Balance = Balance - ? "
                           "WHERE Name = ? AND Balance >= ?");
    execute(query,
            parameters: vector(as(<integer>, amount),
                               account.name,
                               as(<integer>, amount)));
  end with-connection;
end method BankingDemo/account/debit;

The BankingDemo/checkingAccount/limit method is similar to the BankingDemo/account/balance method defined above:

define method BankingDemo/checkingAccount/limit
    (account :: <checkingAccount-implementation>)
    => (limit :: CORBA/<long>)
  with-connection(account.bank.connection)
    let query = make(<sql-statement>,
                     text: "select Limit from Accounts "
                           "where Name = ?");
    let result-set = execute(query,
                             parameters: vector(account.name));
    as(CORBA/<long>, result-set[0][0])
  end with-connection
end method BankingDemo/checkingAccount/limit;

Because we defined <checkingAccount-implementation> to inherit from <account-implementation>, there is no need to re-implement the BankingDemo/account/balance and BankingDemo/account/credit methods for this implementation class. However, we do want to define a specialized BankingDemo/account/debit method, to reflect that a checking account can be overdrawn up to its limit:

define method BankingDemo/account/debit
    (account :: <checkingAccount-implementation>,
     amount :: CORBA/<long>)
 => ()
  with-connection(account.bank.connection)
    let amount = abs(amount);
    let query = make(<sql-statement>,
                     text: "UPDATE Accounts "
                           "SET Balance = Balance - ? "
                           "WHERE Name = ? AND (Balance + Limit) >= ?");
    execute(query,
            parameters: vector(as(<integer>, amount),
                               account.name, as(<integer>,
                               amount)));
  end with-connection;
end method BankingDemo/account/debit;

The BankingDemo/bank/name method returns the value of the bank’s name slot:

define method BankingDemo/bank/name
    (bank :: <bank-implementation>)
 => (name :: CORBA/<string>)
  bank.name
end method BankingDemo/bank/name;

The BankingDemo/bank/openAccount method illustrates how CORBA user exceptions are raised:

define method BankingDemo/bank/openAccount
    (bank :: <bank-implementation>, name :: CORBA/<string>)
 => (account :: BankingDemo/<account>)
  if (existsAccount?(bank, name))
    error(make(BankingDemo/bank/<duplicateAccount>));
  else
    begin
      with-connection(bank.connection)
        let query = make(<sql-statement>,
              text: "INSERT INTO Accounts(Name, Balance, Limit) "
                    "VALUES(?, ?, ?)",
              input-indicator: #f);
        execute(query, parameters: vector(name, as(<integer>, 0),
                                   #f));
      end with-connection;
      let new-account = make(<account-implementation>,
                             bank: bank, name: name);
      as(BankingDemo/<account>,
         PortableServer/POA/servant-to-reference(bank.poa,
                                                 new-account));
    end;
  end if;
end method BankingDemo/bank/openAccount;

If the test existsAccount?(bank, name) succeeds, the call to

error (make(BankingDemo/bank/<duplicateAccount>));

raises a Dylan condition. (We omit the definition of existsAccount?, which can be found in the source.) Recall that the condition class BankingDemo/bank/<duplicateAccount> corresponds to the IDL duplicateAccount exception. The POA that invoked this method in response to a client’s request will catch the condition and send the IDL duplicateAccount exception back to the client.

If there is no existing account for the supplied name, the BankingDemo/bank/openAccount method creates a new record in the database by executing an SQL INSERT statement, initializing the “Limit” field of this record with the SQL NULL value. (Recall that the presence of the NULL value serves to distinguish ordinary accounts from checking accounts on the database.)

Finally, the method makes a new servant of class <account-implementation>, registers it with the bank’s POA with a call to PortableServer/POA/servant-to-reference, and narrows the resulting object reference to the more specific class BankingDemo/<account>, the class of object references to account objects, as required by the signature of the protocol function.

The BankingDemo/bank/openCheckingAccount method is similar, except that it initializes the Limit field of the new account record with the desired overdraft limit, and registers a new servant of class <checkingAccount-implementation>, returning an object reference of class BankingDemo/<checkingAccount>:

define method BankingDemo/bank/openCheckingAccount
    (bank :: <bank-implementation>, name :: CORBA/<string>,
     limit :: CORBA/<long>)
 => (checkingAccount :: BankingDemo/<checkingAccount>)
  if (existsAccount?(bank, name))
    error (make(BankingDemo/bank/<duplicateAccount>));
  else
    begin
      with-connection(bank.connection)
        let limit = abs(limit);
        let query =
          make(<sql-statement>,
              text: "INSERT INTO Accounts(Name, Balance, Limit) "
                    "VALUES(?, ?, ?)",
              input-indicator: #f);
        execute(query, parameters: vector(name, as(<integer>, 0),
                                          as(<integer>, limit)));
      end with-connection;
      let new-account = make(<checkingAccount-implementation>,
                             bank: bank, name: name);
      as(BankingDemo/<checkingAccount>,
      PortableServer/POA/servant-to-reference(bank.poa,
                                              new-account));
     end;
  end if;
end method BankingDemo/bank/openCheckingAccount;

The BankingDemo/bank/retrieveAccount method uses the name parameter to select the Limit field of an account record. If there is no record with that name, indicated by the query returning an empty result set, the method raises the CORBA user exception nonExistentAccount by signalling the corresponding Dylan error.

Otherwise, the method uses the value of the Limit field to distinguish whether the account is an account or a current account, creating a new servant of the appropriate class:

define method BankingDemo/bank/retrieveAccount
    (bank :: <bank-implementation>, name :: CORBA/<string>)
 => (account :: BankingDemo/<account>)
  with-connection(bank.connection)
    let query = make(<sql-statement>,
                  text: "SELECT Limit FROM Accounts "
                        "WHERE Name = ?",
                  output-indicator: #f);
    let result-set = execute(query, parameters: vector(name),
                          result-set-policy:
                            $scrollable-result-set-policy);
    if (empty? (result-set))
      error (make(BankingDemo/bank/<nonExistentAccount>));
    elseif (result-set[0][0])
      as(BankingDemo/<checkingAccount>,
      PortableServer/POA/servant-to-reference
        (bank.poa, make(<checkingAccount-implementation>,
                        bank: bank, name: name)));
    else
      as(BankingDemo/<account>,
      PortableServer/POA/servant-to-reference
        (bank.poa, make(<account-implementation>,
                        bank: bank, name: name)));
    end if;
  end with-connection;
end method BankingDemo/bank/retrieveAccount;

(Unlike the other queries in this example, this query is executed with result-set-policy: $scrollable-result-set-policy to ensure that testing the emptiness of the result set does not invalidate its records.)

Finally, the closeAccount removes the record of an account from the database by executing an SQL delete statement:

define method BankingDemo/bank/closeAccount
    (bank :: <bank-implementation>,
     account-reference :: BankingDemo/<account>)
 => ()
  let account
    = Portableserver/POA/reference-to-servant(bank.poa,
                                           account-reference);
  with-connection(bank.connection)
    let query = make(<sql-statement>,
                  text: "DELETE FROM Accounts "
                        "WHERE Name = ?");
    execute(query, parameters: vector(account.name));
  end with-connection;
end method BankingDemo/bank/closeAccount;

Note that we need to dereference the object reference account that is passed in as the parameter of the BankingDemo/bank/closeAccount operation. We call the Portableserver/POA/reference-to-servant operation of the POA to do so. Here, we make implicit use of our knowledge that, in our application, the server only encounters object references registered with its local POA. This assumption is not true in general.

Implementing CORBA initialization for the bank server

To complete the implementation of the bank server we need to write the code that enters it into the CORBA environment. In detail, we need to:

  • Initialize the server’s ORB

  • Get a reference to the ORB pseudo-object for use in future ORB operations

  • Get a reference to the POA pseudo-object for use in future POA operations

  • Make a bank servant and register it with the POA

  • Make the object reference of the bank servant available to the client

  • Activate the POA to start processing incoming requests

  • Prevent the process from exiting, providing availability

To do this, we need to make use of some additional operations specified in the CORBA module:

module CORBA {
  /// ...
  interface ORB {
    // ...
    typedef string ObjectId;
     exception InvalidName {};
     Object resolve_initial_references (in ObjectId identifier)
       raises (InvalidName);
    void run();
    void shutdown( in boolean wait_for_completion );
  }
}

The CORBA standard specifies the ORB operation resolve_initial_references. This operation provides a portable method for applications to obtain initial references to a small set of standard objects (objects other than the initial ORB). These objects are identified by a mnemonic name, using a string knows as an ObjectId. For instance, the ObjectID for an initial POA object is "RootPOA". (References to a select few other objects, such as the "Interface Repository" and "NamingService", can also be obtained in this manner.)

The ORB operation resolve_initial_references returns the object associated with an ObjectId, raising the exception InvalidName for an unrecognized ObjectID.

The run and shutdown operations are useful in multi-threaded programs, such as servers, which, apart from the main thread, need to run a separate request receiver thread for each POA.

(A single-threaded application, such as a pure ORB client, does not generally need to use these operations.)

A thread that calls an ORB’s run operation simply waits until it receives notification that the ORB has shut down.

Calling run in a server’s main thread can then be used to ensure that the server process does not exit until the ORB has been explicitly shut down.

Meanwhile, the shutdown operation instructs the ORB, and its object adapters, to shut down.

If the wait_for_completion parameter is TRUE, the operation blocks until all pending ORB processing has completed, otherwise it simply shuts down the ORB immediately.

define method initialize-server ()
  let location-service = get-location-service();

  // get reference to ORB
  let orb = CORBA/ORB-init(make(CORBA/<arg-list>), "Open Dylan ORB");

  // get reference to root POA, initially in the holding state
  let RootPOA = CORBA/ORB/resolve-initial-references(orb, "RootPOA");

  with-dbms ($dbms)
     // connect to the database
     let database = make(<database>, datasource-name: $datasource-name);
     let user =  make(<user>, user-name: $user-name, password: $user-password);
     let connection = connect(database, user);

     // make the server frame, initialize and refresh it.
     let server-frame = make(<server-frame>, connection: connection);
     server-frame.refresh-check-button.gadget-value := #t;
     refresh(server-frame);

     //  make the bank servant
     let bank = make(<bank-implementation>, connection: connection,
                     poa: RootPOA, name: "Dylan Bank",
                     server-frame: server-frame);

     // get the servant's object reference from the poa
     let bank-reference = PortableServer/POA/servant-to-reference(bank.poa, bank);

     // activate the bank's POA using its POA manager.
     let POAManager = PortableServer/POA/the-POAManager(bank.poa);
     PortableServer/POAManager/activate(POAManager);

     // register the bank with the location service
     register-bank(orb, location-service, bank-reference);

     // create a separate thread to shut down the orb, unblocking the main thread.
     make(<thread>,
          function: method ()
                      start-frame(server-frame);
                      CORBA/ORB/shutdown(orb, #t);
                    end method);

     // block the main thread
     CORBA/ORB/run(orb);

     // remove from location service
     unregister-bank(orb, location-service, bank-reference);

     // close the bank's connection.
     disconnect(connection);
  end with-dbms;
end method;

The initialize-server function first initializes the Open Dylan ORB by calling the Dylan generic function CORBA/ORB-init, just as we initialized the ORB in the client. The call returns a CORBA/<ORB> pseudo object.

Invoking CORBA/ORB/resolve-initial-references on this ORB, passing the ObjectID "RootPOA", returns a POA object of class PortableServer/<POA>. This is the CORBA standard method for obtaining the initial POA object. Note that RootPOA is initially in the holding state.

Next, we connect to the database and use the connection to make a bank servant. We register the servant with the POA, RootPOA, and publish the resulting object reference, encoded as a string, according to the location-service requested in the command line arguments. By default this is via a shared file. However, if the following is specified on the command line:

-location-service:naming-service

then a Name Service is used instead. Use the ORB command line option -ORBname-service to specify the IOR of the Name Service. Be sure to use the same command line options for the client and the server or they will not find each other!

We then obtain the POA Manager for the POA using the POA operation PortableServer/POA/the-POAManager. The call to PortableServer/POAManager/activate moves the POA out of the holding state, into the active state, ready to receive and process incoming requests.

To prevent the server from exiting before having the chance to process any requests, we introduce a new thread. This thread waits until the user responds to a DUIM dialog and then proceeds to shut down the ORB with a CORBA standard call to CORBA/ORB/shutdown. Meanwhile, back in the main thread, the subsequent call to CORBA/ORB/run causes the main thread to block, waiting for notification that the ORB has shut down.

Once the ORB has shut down, the main thread resumes, closes the connection to the bank, and exits, terminating the server application.

The full implementation of the server initialization is in the file init-server.dylan.

This completes the description of our implementation of the server.