Chapter 19

Creating CORBA Servers

by Mark Wutka


CONTENTS

ACORBA server has objects whose methods are invoked remotely by its clients. A single server can have any number of objects, and can activate and deactivate objects at any time. An active object is visible to the clients, whereas an inactive object cannot be accessed by clients.

Creating a Basic CORBA Server

The interface between the ORB and the implementation of a server is called a skeleton. A skeleton for an IDL interface gets information from the ORB, invokes the appropriate server method, and sends the results back to the ORB.

You normally don't have to write the skeleton itself; you just supply the implementation of the remote methods. Figure 19.1 illustrates the flow of a method invocation through a skeleton to your implementation.

Figure 19.1 : The Skeleton class translates COBRA requests into method invocation.

Listing 19.1 shows an IDL definition of a simple banking interface. You will see how to create a server for this interface in both JavaIDL and VisiBroker.


Listing 19.1  Source Code for Banking.idl
module banking {

     enum AccountType {
          CHECKING,
          SAVINGS
     };

     struct AccountInfo {
          string id;
          string password;
          AccountType which;
     };

     exception InvalidAccountException {
          AccountInfo account;
     };

     exception InsufficientFundsException {
     };

     interface Banking {

          long getBalance(in AccountInfo account)
               raises (InvalidAccountException);

          void withdraw(in AccountInfo account, in long amount)
               raises (InvalidAccountException,
                    InsufficientFundsException);

          void deposit(in AccountInfo account, in long amount)
               raises (InvalidAccountException);

          void transfer(in AccountInfo fromAccount,
               in AccountInfo toAccount, in long amount)
               raises (InvalidAccountException,
                    InsufficientFundsException);
     };
};

Using Classes Defined by IDL Structs

When an IDL struct is turned into a Java class, it does not have custom hashCode and equals methods. This means that two instances of this class having identical data are not equal.

If you want to add custom methods to these structs, you have to create a separate class and define methods to convert from one class to the other.

In Chapter 16, "Creating 3-Tier Distributed Applications with RMI," the RMI-based banking application defines an Account class that has its own hashCode and equals methods. This allows it to be stored in a hash table.

The following code shows the two methods that need to be added to the Account class to convert to and from AccountInfo objects.

// Allow this object to be created from an AccountInfo instance

        public Account(AccountInfo acct)
        {
                this.id = acct.id;
                this.password = acct.password;
                this.which = acct.which;
        }

// Convert this object to an AccountInfo instance

        public AccountInfo toAccountInfo()
        {
                return new AccountInfo(id, password, which);
        }

VisiBroker Skeletons

VisiBroker implements skeletons in a traditional way. When you generate your skeletons and stubs for an IDL module, it generates an abstract skeleton class.

This class has all the code to communicate with the ORB and to invoke the remote methods. It leaves the remote methods themselves as abstract methods.

When you use the remote methods, you create a subclass of the skeleton class and use those abstract methods. The name of the skeleton class generated for an IDL interface, at least for VisiBroker, is the name of the interface, prefaced by _sk_.

Every object that uses a CORBA interface in VisiBroker must identify itself by a name. The constructor for the skeleton class sends this name to the ORB itself.

When you create a subclass of the skeleton, you must create a constructor that passes a name up to the superclass, for example:

public class BankingImpl extends _sk_Banking
{
     public BankingImpl(String bankingObjectName)
     {
          super(bankingObjectName);
     // other initialization code here
     }

The implementation of the skeleton methods is straightforward. In fact, you can use the implementation of the BankingImpl class from Chapter 16, changing only the java.rmi.RemoteException exceptions to CORBA.SystemException.

The following code fragment is an example of one of the methods from the BankingImpl for VisiBroker:

// getBalance returns the amount of money in the account (in cents).
// If the account is invalid, it throws an InvalidAccountException

     public int getBalance(AccountInfo accountInfo)
     throws CORBA.SystemException, InvalidAccountException
     {

// Fetch the account from the table
          Integer balance = (Integer) accountTable.get(
               new Account(accountInfo));

// If the account wasn't there, throw an exception
          if (balance == null) {
               throw new InvalidAccountException(accountInfo);
          }

// Return the account's balance
          return balance.intValue();
     }

Once you create an object that uses a remote interface, you need a program that creates instances of these objects so the clients can use them. To do this, you must first initialize the VisiBroker ORB with the following line:

CORBA.ORB orb = CORBA.ORB.init();

Next, you must create an instance of the Basic Object Adapter, or BOA. The BOA is a standard CORBA object used to communicate with the ORB.

It was intended for systems where the ORB and the server are separate programs or even on separate machines. The BOA has methods for activating and deactivating objects, and for activating and deactivating the entire server.

You create an instance of a BOA with the following line:

CORBA.BOA boa = orb.BOA_init();

Next, create an instance of your implementation object. In this case, the implementation object is the BankingImpl object.

While you are instantiating a BankingImpl object, you want to refer to it as an instance of the Banking object, as far as the BOA is concerned, so you should store the new object in a variable of type Banking.

Also, when you create the implementation, you must give it the name that the other clients will use to access it. In this case, the name is Bank. You create the instance with the following line:

Banking banking = new BankingImpl("Bank");

Even though you have created an implementation object, the ORB still does not know about it. You must identify the object to the BOA by calling the obj_is_ready method:

boa.obj_is_ready(banking);

If you have more than one implementation object, you should identify them all to the BOA at this point. Once all the objects have been identified, call the BOA's impl_is_ready to tell the ORB that everything is ready to go:

boa.impl_is_ready();

When you have called impl_is_ready, your server is ready to go, and clients can begin connecting to it.

Listing 19.2 shows a startup object that initializes the ORB, creates an instance of the BankingImpl class, and activates it using the BOA.


Listing 19.2  Source Code for BankingServer.java
package banking;

// This class creates a BankingImpl object and activates
// it through the BOA (Basic Object Adaptor).

public class BankingServer
{

     public static void main(String[] args)
     {

          try {

// Initialize the ORB
               CORBA.ORB orb = CORBA.ORB.init();

// Create a BOA
               CORBA.BOA boa = orb.BOA_init();

// Create the banking service implementation. Clients will
// request access to the object by the name "Bank".
               Banking banking = new BankingImpl("Bank");

// Tell the BOA about the banking object
               boa.obj_is_ready(banking);

// Activate all the objects in the BOA
               boa.impl_is_ready();

          } catch (Exception e) {
               System.out.println("Got exception: "+e);
               e.printStackTrace();
          }
     }
}

Using the VisiBroker TIE Interface

The traditional approach of using an abstract skeleton class with an implementation subclass works well for many situations. In fact, in C++ you may never have any problems with it, since C++ allows multiple inheritance.

In Java, however, you can run into some hairy problems using this method. You may want to create a remote object that is a subclass of a non-CORBA object. Using the normal skeleton class provided by VisiBroker uses up your one permitted inheritance.

The VisiBroker TIE interface addresses this problem. The concept of the TIE is common in several ORBs and is sometimes called delegation.

TIE doesn't stand for anything. It refers to the idea that it ties a CORBA object to a non-CORBA object, providing remote access to the non-CORBA object.

In the regular skeleton implementation, the skeleton is the implementation. In the TIE implementation, the skeleton and the implementation are separate objects.

VisiBroker's implementation of the TIE in Java is quite simple. When you create Java files from IDL, VisiBroker creates a Java interface for each interface you define in IDL. The name of the Java interface is the name of the IDL interface with the word Operations appended to it.

For instance, the IDL interface Banking would generate a Java interface called BankingOperations. VisiBroker also generates a skeleton TIE implementation, whose name is the name of the IDL interface, prefaced by _tie_, like _tie_Banking.

Unlike the normal skeleton class, the TIE skeleton class is not an abstract class. Instead, it uses the methods in the IDL interface.

All these methods do, however, is invoke identical methods in an instance of BankingOperations. In other words, the TIE skeleton class works like a pass-thru.

Figure 19.2 illustrates the relationship between a TIE skeleton and an object using the BankingOperations interface.

Figure 19.2 : The TIE skeleton invokes implementation methods in another object.

When you create an implementation object for a TIE interface, it does not have to be a subclass of the skeleton. In fact, it shouldn't be.

Instead, it must use an Operations interface (BankingOperations, for example). The only link that the implementation has to CORBA is that each method in the Operations interface can throw the CORBA.SystemException exception.

Unlike the regular skeleton implementation, the constructor for the TIE implementation object does not have to accept the object's name. You can use the empty constructor if you like.

The only difference in start-up between the regular skeleton implementation and the TIE implementation is that for the TIE implementation, you first create the TIE implementation object, and then you create an instance of the TIE skeleton, passing it the implementation object.

The following code fragment creates a TIE implementation of the Banking interface, followed by the TIE skeleton for that interface:

BankingTieImpl impl = new BankingTieImpl();
Banking banking = new _tie_Banking(impl, "Bank");

As you can see, the object's name is passed to the constructor for the TIE skeleton, not to the TIE implementation.

JavaIDL Skeletons

JavaIDL doesn't even bother with an abstract skeleton class. Instead, it always generates a TIE interface for every class, although the JavaIDL specification never uses the term TIE.

Like VisiBroker, JavaIDL creates an Operations interface that has Java versions of the methods defined in the IDL interface. The only difference between the interface created by JavaIDL and the interface created by VisiBroker is the name of the CORBA system exception. JavaIDL calls it sunw.corba.SystemException.

The implementation object you create does not use the Operations interface directly. Instead, it uses the Servant interface, which extends the Operations interface. For example, your implementation for the Banking interface might be declared as

public class BankingImpl implements BankingServant

Although JavaIDL is intended to be Sun's recommendation for mapping IDL into Java, it was released with a lightweight ORB called the Door ORB. This ORB provides just enough functionality to get clients and servers talking to each other, but not much more.

Depending on the ORB, the initialization varies, as does the activation of the objects. For the Door ORB distributed with JavaIDL, you initialize the ORB with the following line:

sunw.door.Orb.initialize(servicePort);

The servicePort parameter you pass to the ORB is the port number it should use when listening for incoming clients. It must be an integer value. Your clients must use this port number when connecting to your server.

After you initialize the orb, you can instantiate your implementation object. For example:

BankingImpl impl = new BankingImpl();

Next, you create the skeleton, passing it the implementation object:

BankingRef server = BankingSkeleton.createRef(impl);

Finally, you activate the server by publishing the name of the object:

sunw.door.Orb.publish("Bank", server);

Listing 19.3 shows the complete JavaIDL startup program for the banking server.


Listing 19.3  Source Code for BankingServer.java
package banking;

public class BankingServer
{

// Define the port that clients will use to connect to this server
     public static final int servicePort = 5150;

     public static void main(String[] args)
     {

// Initialize the orb
          sunw.door.Orb.initialize(servicePort);

          try {

               BankingImpl impl = new BankingImpl();
// Create the server
               BankingRef server =
                    BankingSkeleton.createRef(impl);

// Register the object with the naming service as "Bank"
               sunw.door.Orb.publish("Bank", server);

          } catch (Exception e) {
               System.out.println("Got exception: "+e);
               e.printStackTrace();
          }
     }
}

Creating Callbacks in CORBA

Callbacks are a handy mechanism in distributed computing. You use them whenever your client wants to be notified of some event but doesn't want to sit and poll the server to see if the event has occurred yet.

In a regular Java program, you'd just create a callback interface and pass the server an object that uses the callback interface. When the event occurs, the server invokes a method in the callback object.

As it turns out, callbacks are just that easy in CORBA. You define a callback interface in your IDL file and then create a method in the server's interface that takes the callback interface as a parameter. The following IDL file defines a server interface and a callback interface:

module callbackDemo
{
     interface callbackInterface {
          void doNotify(in string whatHappened);
     };

     interface serverInterface {
          void setCallback(in callbackInterface callMe);
     };
};

Under JavaIDL, the setCallback method would be defined as:

void setCallback(callbackDemo.callbackInterfaceRef callMe)
    throws sunw.corba.SystemException;

Once you have the callbackDemo.callbackInterfaceRef object, you can invoke its whatHappened method at any time. At this point, the client and server are on a peer-to-peer level. They are each other's client and server.

Wrapping CORBA Around an Existing Object

When you create CORBA implementation objects, you are tying that object to a CORBA implementation. Even if you use the TIE interface in your ORB, your methods still can throw the CORBA SystemException exception.

This is not the ideal situation. Even though JavaIDL and VisiBroker are similar, you still have to make minor changes when going from one to the other. If the conversion is a one-time thing, that may not be a big deal.

If, on the other hand, you have to maintain multiple versions of a complex object, you don't want to keep multiple copies of the actual implementation code.

You can solve this problem, but it takes a little extra work up front. First, concentrate on using the object you want, without using CORBA, RMI, or any other remote interface mechanism.

This is the one copy you use across all your implementations. This object, or set of objects, can define its own types, exceptions, and interfaces.

Next, to make this object available remotely, define an IDL interface that is as close to the object's interface as you can get. There may be cases where they won't match exactly, but you can take care of that.

Once you generate the Java classes from the IDL definition, create an implementation that simply invokes methods on the real implementation object. This is essentially the same as a TIE interface, with one major exception: the implementation class has no knowledge of CORBA.

You can even use this technique to provide multiple ways to access a remote object. Figure 19.3 shows a diagram of the various ways you might provide access to your implementation object.

Figure 19.3 : A single object can be accessed by many types of remote object systems.

Although this may sound simple, it has some additional complexities that you must address. If your implementation object defines its own exceptions, you must map those exceptions to CORBA exceptions. You must also map between Java objects and CORBA-defined objects.

Once again, the banking interface is a good starting point for illustrating the problems and solutions in separating the application from CORBA.

The original banking interface is defined with a hierarchy of exceptions, a generic BankingException, with InsufficientFundsException and InvalidAccountException as subclasses. This poses a problem in CORBA, since exceptions aren't inherited.

You must define a BankingException exception in your IDL file, this way:

exception BankingException {};

In addition, since you probably want the banking application itself to be in the banking package, change the IDL module name to remotebanking.

The implementation for the Banking interface in the remotebanking module must do two kinds of mapping. First, it must convert instances of the Account object to instances of the AccountInfo object.

This may seem like a pain and, frankly, it is, but it's a necessary pain. If you start to intermingle the classes defined by CORBA with the real implementation of the application, you end up having to carry the CORBA portions along with the application, even if you don't use CORBA.

Mapping to and from CORBA-Defined Types

You can define static methods to handle the conversion from the application data types to the CORBA-defined data types. For example, the banking application defines an Account object.

The remotebanking module defines this object as AccountInfo. You can convert between the two with the following methods:

// Create a banking.Account from an AccountInfo object

public static banking.Account makeAccount(AccountInfo info)
{
        return new banking.Account(info.id, info.password,
                info.which);
}

// Create an AccountInfo object from a banking.Account object

public static AccountInfo makeAccountInfo(banking.Account account)
{
        return new AccountInfo(account.id, account.password,
                account.which);
}

Your remote implementation of the banking interface needs access to the real implementation, so the constructor for the RemoteBankingImpl object needs a reference to the banking.BankingImpl object:

protected banking.BankingImpl impl;

public RemoteBankingImpl(banking.BankingImpl impl)
{
        this.impl = impl;
}

Creating Remote Method Wrappers

Now all your remote methods have to do is convert any incoming AccountInfo objects to banking.Account objects, catch any exceptions, and throw the proper remote exceptions. Here is the implementation of the remote withdraw method:

// call the withdraw function in the real implementation, catching
// any exceptions and throwing the equivalent CORBA exception

public synchronized void withdraw(AccountInfo accountInfo, int amount)
throws sunw.corba.SystemException, InvalidAccountException,
        InsufficientFundsException, BankingException
{

        try {

// Call the real withdraw method, converting the accountInfo object
// to a banking.Account object first

                impl.withdraw( makeAccount(accountInfo), amount);

        } catch (banking.InvalidAccountException excep) {

// The banking.InvalidAccountException contains an Account object.
// Convert it to an AccountInfo object when throwing the CORBA exception

                throw new InvalidAccountException(
                        makeAccountInfo(excep.account));

        } catch (banking.InsufficientFundsException nsf) {
                throw new InsufficientFundsException();
        } catch (banking.BankingException e) {
                throw new BankingException();
        }
}

Although it would be nice if you could get the IDL-to-Java converter to generate this automatically, it has no way of knowing exactly how the real implementation looks.

Implementing Wrapped Callbacks

The notion of the callback is particularly nasty, since you can't just encapsulate the CORBA calls going to the server. The server has to make CORBA calls back to the client.

The stock quote system introduced in Chapter 16 has a good example of a callback system. Unfortunately, there's one feature in the stock quote system that needs to be changed.

Whenever you create a callback system, leave room for the callback to throw an exception. You may never use it, but it makes it much easier to create a distributed version of the application.

Figure 19.4 illustrates the relationship between the real implementation and the CORBA wrappers in a callback system.

Figure 19.4 : Callbacks need special handling in a CORBA system.

The revised version of the StockQuoteClient interface is shown in Listing 19.4.


Listing 19.4  Source Code for StockQuoteClient.java
package stocks;

// Defines a callback interface for the StockQuoteServer so
// it can notify its clients of new stock quotes.

public interface StockQuoteClient
{
     public void quote(StockQuote quote) throws
           StockQuoteClientException;
}

The StockQuoteClientException is a simple subclass of Exception with no extra parameters. The stock-quote server likes to know when it can no longer send quotes to a client. If an error occurs, it removes the client from its tables.

The trick to wrapping CORBA around a callback system is that you have to create a wrapper for the callback as well as for the server. The wrapper uses the methods that the non-CORBA server defines for its callback and then invokes the corresponding method in the CORBA client.

Listing 19.5 shows the implementation of the RemoteStockCallback object, which does the distributed callback.


Listing 19.5  Source Code for RemoteStockCallback.java
package remotestocks;

public class RemoteStockCallback implements stocks.StockQuoteClient
{

// Client is the CORBA client whose methods we want to invoke
     protected StockQuoteClientRef client;

// Server is the CORBA-wrapper for the stock quote server. If there
// is an error sending a quote to the client, we tell the wrapper
// about it so it can remove this client from its tables.
     protected RemoteStockQuoteImpl server;

// makeRemoteStockQuote converts a stocks.StockQuote object (the Non-CORBA
// object) into a remotestocks.StockQuote object (the CORBA version of
// the stock quote).

     protected static StockQuote makeRemoteStockQuote(
          stocks.StockQuote quote)
     {
          return new StockQuote(quote.stock, quote.amount,
               quote.change);
     }

     public RemoteStockCallback(StockQuoteClientRef client,
          RemoteStockQuoteImpl server)
     {
          this.client = client;
          this.server = server;
     }

// quote is invoked by the real stock quote server implementation, not
// the CORBA server.

     public void quote(stocks.StockQuote quote) throws
           stocks.StockQuoteClientException
     {
// Try to invoke the remote method, converting the stocks.StockQuote
// to the CORBA version, remotestocks.StockQuote
          try {
               client.quote(makeRemoteStockQuote(quote));
          } catch (Exception e) {

// If there was an error, remove this client from the server, then
// throw an exception to the real implementation server.

               try {
                    server.removeCallback(client);
               } catch (Exception ignore) {
               }
               throw new stocks.StockQuoteClientException();
          }
     }
}

Now in the normal implementation of the stock quote system, the client calls the addWatch method in the server, passing it the reference to the client and the name of the stock to watch. The server adds this client to its tables and invokes the quote method in the client every time the stock's value changes.

With a CORBA front-end on the server, things change slightly. When the request comes into the CORBA wrapper, it creates an instance of RemoteStockCallback, passing to its constructor the client stub that was passed in the addWatch call.

The wrapper then calls the addWatch method in the real server implementation, passing it the RemoteStockCallback. Here is the implementation of the addWatch method in the CORBA wrapper:

// addWatch adds a client to the list of clients watching a stock

public void addWatch(StockQuoteClientRef client, String stock)
throws sunw.corba.SystemException, UnknownStock
{

// See if we have already created a callback object for this client.
        RemoteStockCallback callback = (RemoteStockCallback)
                stockClients.get(client);

// If we didn't already have a callback, create one and add it
// to the table.
        if (callback == null) {
                callback = new RemoteStockCallback(client, this);

                stockClients.put(client, callback);
        }

// Now call the addWatch method in the real implementation
        try {
                impl.addWatch(callback, stock);
        } catch (stocks.UnknownStockException e) {
                throw new UnknownStock(stock);
        }
}

Now when the real stock quote server publishes a stock quote, it ends up calling the quote method in the RemoteStockCallback object. This callback object, in turn, calls the quote method in the distributed client.

Figure 19.5 illustrates the sequence of events for publishing a stock quote.

Figure 19.5 : RemoteStockCallback acts as a proxy for a distributed client.