RMI-BASED SOFTWARE AGENTS
Chapter 16: RMI-Based Implementation of the Airline Reservation System
Overview
In the previous chapter, you learned about Java’s Remote Method Invocation (RMI) API.
In this chapter you’ll see an example of a larger RMI-based program: the Airline
Reservation System of Chapter 6, “The Airline Reservation System Model.”You’ll find the source code for the program on the CD-ROM. By studying the program, you’ll learn more about using sockets to exchange data across networks.
To refresh your recall of the Airline Reservation System, you should turn back to Chapter
6 and skim it quickly, focusing on the functional requirements of the system. You should also refresh your recollection of the socket-based implementation of the Airline
Reservation System presented in Chapter 14, “Socket-Based Implementation of the Airline Reservation System,” because the RMI-based program described in this chapter is functionally equivalent to the socket-based program described in Chapter 14.
One of the advantages of using RMI over sockets is that RMI lets you work at a higher level of abstraction. For example, you don’t have to code serialization routines or
command dispatchers. As a result, your code is more compact. The RMI-based program described in this chapter consists of a little over 800 lines, whereas the socket-based program has over 1,100 lines. Research has demonstrated that the time required to write a line of code is approximately the same, regardless which programming language or API is used. This implies that the smaller size of RMI programs translates into increased programmer productivity: A programmer using RMI should be almost 1.5 times as productive as one using sockets.
Here are the three transactions implemented by the sample program:
• Search Flights, which returns each flight stored in the database.
• Search Seats, which returns the reservation information for a specified passenger identified by passenger number.
• Book Seat, which enters a reservation for a specified passenger and flight. The transaction makes several simplifying assumptions. The passenger and flight must already exist as entities; the transaction does not create new passengers or flights.
The reservation number must be unique; attempting to add a reservation with the same number as an existing reservation updates the existing reservation.
Like the sample program of Chapter 14, the sample program of this chapter does not implement the visual interface for seat selection described in Chapter 6. Also, it doesn’t implement the reservation pool locking mechanism. However, near the end of this chapter, we’ll show you how to implement such a mechanism.
As before, let’s begin our tour of the program by looking at the application classes. Later, we’ll look at the client and server classes that provide the user interface and network connectivity.
APPLICATION CLASSES
The application classes are not much different from those presented in Chapter 14. The biggest difference is that we’ve omitted the methods to pack and unpack field values as Strings. Because RMI handles serialization automatically, we don’t need to provide these methods.
Note Because the classes are changed very little, they’re not shown here. You can examine them by copying them from the CD-ROM to your system’s hard drive.
Recall that every RMI server must define an interface that specifies the methods that the server makes available to its clients. Therefore, every RMI server has two source files:
one defining the class, and one defining its interface. Let’s examine the interface first.
The Server Interface
The server supports three transactions and provides a method for each transaction:
Note The following code is found in the file ServerInterface.java in the chapter16/
listings directory of the CD-ROM.
public interface ServerInterface extends Remote {
public Flight [] searchFlights(()
throws RemoteException, SQLException ; public Reservation [] searchSeats((
String passenger_no)
throws RemoteException, SQLException ; public void bookSeat(String flight_no, String passenger_no, String res_no) throws RemoteException, SQLException, FlightBookedException ;
}
These three methods are all that the application requires, because the server can invoke methods on the application objects to perform most of its processing. Notice that the interface extends Remote and that each method may throw the RemoteException , in addition to any exceptions thrown by application processing, such as SQLException and FlightBookedException . Notice also that two of the methods return arrays.
Because a Java array is an object, an array is serializable and can be passed as a remote method argument or return value, as long as the elements of the array are primitive or serializable.
Notice also that none of the application classes has been revised to extend the UnicastRemoteObject class. Therefore, RMI will pass instances of the application classes by value, rather than by reference. If several clients request access to the same Flight instance, for example, each receives its own instance rather than a reference to a single instance. This behavior is appropriate for the Airline Reservation System, which uses a SQL database to centrally store its persistent state.
However, some applications require that there be only one object instance associated with each real-world entity. To accommodate this requirement, simply define any such objects as extending UnicastRemoteObject . Alternatively, you can invoke the static exportObject method of the UnicastRemoteObject class, passing a reference to the object instance that you want passed by reference, not value. Doing so registers the instance reference as a remote reference. Of course, in either case, you must use the rmic utility to generate the skeleton class required by the RMI runtime system.
The Server Class
Note All the code from here until the next section, “The Client Class,”is found in the file Server.java in the chapter16/listings directory of the CD-ROM.
In addition to the three methods specified in its interface, the server class defines several fields, a main method, and a constructor. The class header specifies that the class extends the UnicastRemoteObject class and that it implements the
ServerInterface :
public class Server extends UnicastRemoteObject implements ServerInterface
The class defines four constant fields:
static final String SERVERNAME = "ARServer";
static final String DB = "jdbc:odbc:airline";
static final String USER = "";
static final String PASSWORD = "";
The SERVERNAME field specifies the name under which the server registers itself in the RMI registry. The DB field specifies the JDBC URL of the database, including the DSN
“airline.” Because the DSN was created without user ID and password, these are specified as null Strings. Defining these four values as constants makes it easier to revise them.
The only nonstatic field of the server class is the field used to hold the database connection once the database is opened:
Connection theConnection;
The main method performs these functions:
• It installs a security manager that prevents clients from misusing server resources.
• It registers the server instance in the RMI registry.
• It displays a console message announcing that the server is ready.
Here’s the source code for the main method:
public static void main(String [] args)) {
System.setSecurityManager(new RMISecurityManager());
try {
Server server = new Server();
Naming.rebind(SERVERNAME, server);
System.out.println("Server ready.");
}
catch (Exception ex) {
ex.printStackTrace();
} }
The constructor is responsible for opening a connection to the database:
public Server() throws Exception {
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
theConnection =
DriverManager.getConnection(DB, USER, PASSWORD);
}
The forName call can throw a NoClassDefFoundException , and the
getConnection call can throw a SQLException; therefore, the constructor specifies that it can throw an Exception, a common superclass of
NoClassDefFoundException and SQLException.
The searchFlights method is one of the three methods exported by the server, as specified in its interface. This method simply invokes the getFlights method of the
Flight class, which returns an array containing an instance of Flight for each row of the Flight database table:
public Flight [] searchFlights(() throws RemoteException, SQLException {
Flight [] flights ==
Flight.getFlights(theConnection);
return flights;
}
The searchSeats method works similarly. It simply invokes the getPassenger method of the Reservation class, which returns an array containing an instance of Reservation for each row of the Reservation database table:
public Reservation [] searchSeats((
String passenger_no)
throws RemoteException, SQLException {
Reservation [] res ==
Reservation.getPassenger(
theConnection, passenger_no);
return res;
}
The final exported method, bookSeat, invokes the Reservation constructor to create a new instance of Reservation that has the appropriate values for reservation
number, flight number, and passenger number. It then uses the dbWrite method of the Reservation class to create a table row that contains the specified values:
public void bookSeat(String flight_no, String passenger_no, String res_no) throws RemoteException, SQLException, FlightBookedException
{
Reservation res = new Reservation(
theConnection, res_no, flight_no, passenger_no);
res.dbWrite(theConnection);
}
The Client Class
Just as the RMI-based server class is much simpler than the socket-based server class, the RMI-based client class is much simpler than the socket-based client class. The client is implemented as an application, rather than an applet; therefore, its class header specifies that the class extends the Frame class:
public class RemoteClient extends Frame
The class defines two fields involved with accessing the RMI server and several user interface fields. A constant field specifies the URL of the server:
Note All the code from here until the section on “The Locking Mechanism” is found
in the file RemoteClient.java in the chapter16/listings directory of the CD- ROM.
public static final String URL = "rmi://127.0.0.1/ARServer";
The protocol, of course, is RMI. Notice that the server host is specified using an IP number rather than a host name. However, either can be used. The final component of the URL is the symbolic name of the server object, ARServer. Recall that the server registered itself using that name.
Another field holds the remote reference to the server object:
ServerInterface theServer;
This field is initialized in the default constructor.
Here are the user interface–related fields of the class:
Button theSearchFlights = new Button("Search Flights");
Button theSearchSeats = new Button("Search Seats");
Button theBookSeat = new Button("Book Seat");
TextField theFlightNo = new TextField();
TextField thePassengerNo = new TextField();
TextField theReservationNo = new TextField();
TextArea theResults = new TextArea();
The socket-based sample program of Chapter 14 is relatively complicated; therefore, we can’t offer significant explanation of its user interface. Because the RMI-based program is simpler, let’s look more thoroughly at its user interface, which is quite similar to that used by the socket-based sample program. Figure 16.1 shows the application window, which holds three TextField objects, a TextArea object, and three Button objects.
Figure 16.1: The RMI-based client and server are simpler than their socket- based counterparts.
The TextField objects allow the user to enter a flight number, passenger number, and reservation number. Not every transaction uses every value. Table 16.1 summarizes the values used by each transaction. If you fail to provide a required input value, the
program generally throws an exception. You’ll discern this by the stack trace the program displays on the console. If you provide unneeded input values, the program ignores them.
TABLE 16.1 INPUT VALUES USED BY AIRLINE RESERVATION SYSTEM TRANSACTIONS
Transaction Input Value(s)
Book Seat Flight number, passenger number, reservation number
Search Flights Passenger number
Search Seats (None)
Before examining the default constructor, which lays out the application window, take a quick look at the main method, which is quite short:
public static void main(String [] args)) {
new RemoteClient();
}
The main method simply instantiates a RemoteClient object. The object’s default constructor does the rest. Now, take a look at the constructor (you’ll see the rest shortly):
public RemoteClient() {
super("Airline Reservation System (Remote Client)");
setLayout(new BorderLayout());
Panel p;
p = new Panel(new GridLayout(0, 1));
add(p, BorderLayout.NORTH);
p.add(new Label("Flight No:"));
p.add(theFlightNo);
p.add(new Label("Passenger No:"));
p.add(thePassengerNo);
p.add(new Label("Reservation No:"));
p.add(theReservationNo);
p.add(new Label("Results:"));
add(theResults, BorderLayout.CENTER);
p = new Panel(new GridLayout(1, 0));
add(p, BorderLayout.SOUTH);
p.add(theSearchFlights);
p.add(theSearchSeats);
p.add(theBookSeat);
theResults.setEditable(false);
theSearchFlights.addActionListener(
new ButtonHandler());
theSearchSeats.addActionListener(
new ButtonHandler());
theBookSeat.addActionListener(
new ButtonHandler());
addWindowListener(new WindowHandler());
setSize(600, 400);
setVisible(true);
The default constructor uses the super constructor to pass an argument to the constructor of its superclass, Frame, which uses the argument to set the window title.
Then, the constructor establishes a BorderLayout layout manager that divides the screen into three areas: north (the top of the window), center, and south (the bottom of the window). The program does not use the east and west areas. The constructor then creates a Panel that establishes a multirow GridLayout layout manager for the north area and places the three TextField objects in the Panel, with three Label objects that describe the TextField objects and a fourth Label that describes the TextArea that appears in the center area of the window, immediately below the north area.
Next, the constructor adds the TextArea, referenced by the field theResults, to the center area of the window. In the south area of the window, the constructor places a Panel controlled by a second GridLayout—this one specifying a multicolumn layout.
The constructor places the three Button objects in this Panel, causing them to appear in the south area of the screen.
To prevent user editing of the contents of the TextArea, the constructor invokes the setEditable message. The constructor then establishes an ActionListener for each of the three Button objects, using instances of the ButtonHandler inner class.
Finally, the constructor invokes setSize to set the size of the application window to a value appropriate to a standard VGA monitor; it then makes the application window visible by invoking setVisible.
The constructor also obtains a reference to its remote server:
try {
theServer =
(ServerInterface) Naming.lookup(URL);
}
catch (Exception ex) { fatalError(ex); }
The static lookup method of the Naming class returns a reference to an object that implements the Remote interface. Because the object must be a reference to a Server instance that implements the ServerInterface , the constructor casts the returned value to a ServerInterface . This operation fails with a ClassCastException if returned objects fail to implement the ServerInterface . This might occur, for instance, if you use an incorrect URL, causing the RMI runtime to connect to the wrong server. If some other RMI-related error occurs, the call can throw a RemoteException . If you want to handle the two types of exceptions differently, you can associate distinct catch statements with the try .
The three transaction-handling methods are simple, just as they are in the server. Here’s the searchFlights method:
public void searchFlights() throws Exception
{
theResults.setText("");
Flight [] flights ==
theServer.searchFlights();
for (int i = 0; i < flights.length; i++) {
theResults.append(flights[i] ++ "\n");
} }
Notice that the searchFlights method simply invokes the corresponding searchFlights method on the server object and processes the result the server returns.
The searchSeats and bookSeat methods work similarly:
public void searchSeats() throws Exception
{
theResults.setText("");
Reservation [] seats ==
theServer.searchSeats(thePassengerNo.getText());
for (int i = 0; i < seats.length; i++) {
theResults.append(seats[i] ++ "\n");
} }
public void bookSeat() throws Exception {
theResults.setText("");
theServer.bookSeat(theFlightNo.getText(), thePassengerNo.getText(),
theReservationNo.getText());
}
The ButtonHandler inner class dispatches button-related events by invoking the appropriate transaction-handling method:
class ButtonHandler implements ActionListener {
public void actionPerformed(ActionEvent evt) {
try {
String cmd = evt.getActionCommand();
if (cmd.equals("Search Flights")) searchFlights();
else if (cmd.equals("Search Seats")) searchSeats();
else if (cmd.equals("Book Seat")) bookSeat();
}
catch (Exception ex) { fatalError(ex); } }
}
It uses the getActionCommand method to obtain the default action String associated with the Component—in this case a Button—that generated the
ActionEvent. By default, this method returns the label that appears on the face of the Button. The method uses the equals method to test for a match between a String literal and the action String.
Note Many programmers—and several Java authors—mistakenly use the equals operator (= ) to perform such comparisons. When the operands of the equals operator are object references, the operator tests whether the references refer to the same object. Two String objects can be different objects, even though they contain precisely the same characters. Therefore, as in the code example, you should use the equals method rather than the equals operator when comparing the contents of String objects.
A second inner class, WindowHandler , lets the user close the application by clicking the application window’s close box:
class WindowHandler extends WindowAdapter {
public void windowClosing(WindowEvent evt) {
setVisible(false);
System.exit(0);
} }
Finally, the class defines a simple error-handling routine, fatalError, called by several of the methods in the class. The WindowHandler method attempts no recovery, but neither does it shut down the application. It simply displays a message on the console and returns:
public void fatalError(Exception ex) {
ex.printStackTrace();
}