[ Team LiB ] Previous Section Next Section

21.3 A Persistent Bank Server

The RemoteBankServer class of Example 21-2 does not have a persistent store for customer account data. We solve that problem here with the PersistentBankServer class of Example 21-3. In addition to reiterating RMI programming techniques, this class also demonstrates the use of SQL atomic transactions to ensure database consistency.

After the PersistentBankServer creates its Connection object, it calls setAutoCommit( ) with the argument false to turn off autocommit mode. Then, for example, the openAccount( ) method groups three transactions into a single, atomic transaction: adding the account to the database, creating a table for the account history, and adding an initial entry into the history. If all three transactions are successful (i.e., they don't throw any exceptions), openAccount( ) calls commit( ) to commit the transactions to the database. However, if any one of the transactions throws an exception, the catch clause takes care of calling rollback( ) to roll back any transactions that succeeded. All remote methods in PersistentBankServer use this technique to keep the account database consistent.

In addition to demonstrating the techniques of atomic transaction processing, the PersistentBankServer class provides further examples of using SQL queries to interact with a database. In order to run this example, you need to create a properties file named BankDB.props with database connection information, like those used in Chapter 18. Before you run the server for the first time, you also need to create an accounts table in the database. You can do this using the ExecuteSQL program of Example 18-1 or by using any other database administration tool to execute this SQL statement:

CREATE TABLE accounts (name VARCHAR(20), password VARCHAR(20), balance INT);

Since this is an RMI server as well as a database client, you must run the rmic compiler to generate stub and skeleton classes and start the rmiregistry service, just as you did for RemoteBankServer.

Example 21-3. PersistentBankServer.java
package je3.rmi;
import java.rmi.*;
import java.rmi.server.*;
import java.rmi.registry.*;
import java.sql.*;
import java.io.*;
import java.util.*;
import java.util.Date; // import explicitly to disambiguate from java.sql.Date
import je3.rmi.Bank.*;  // Import inner classes of Bank

/**
 * This class is another implementation of the RemoteBank interface.
 * It uses a database connection as its back end, so that client data isn't
 * lost if the server goes down.  Note that it takes the database connection
 * out of "auto commit" mode and explicitly calls commit( ) and rollback( ) to
 * ensure that updates happen atomically.
 **/
public class PersistentBankServer extends UnicastRemoteObject
    implements RemoteBank
{
    Connection db;   // The connection to the database that stores account info
    
    /** The constructor.  Just save the database connection object away */
    public PersistentBankServer(Connection db) throws RemoteException { 
        this.db = db;
    }
    
    /** Open an account */
    public synchronized void openAccount(String name, String password)
        throws RemoteException, BankingException
    {
        // First, check if there is already an account with that name
        Statement s = null;
        try { 
            s = db.createStatement( );
            s.executeQuery("SELECT * FROM accounts WHERE name='" + name + "'");
            ResultSet r = s.getResultSet( );
            if (r.next( )) throw new BankingException("Account name in use.");
            
            // If it doesn't exist, go ahead and create it Also, create a
            // table for the transaction history of this account and insert an
            // initial transaction into it.
            s = db.createStatement( );
            s.executeUpdate("INSERT INTO accounts VALUES ('" + name + "', '" +
                            password + "', 0)");
            s.executeUpdate("CREATE TABLE " + name + 
                            "_history (msg VARCHAR(80))");
            s.executeUpdate("INSERT INTO " + name + "_history " +
                            "VALUES ('Account opened at " + new Date( ) + "')");
            
            // And if we've been successful so far, commit these updates,
            // ending the atomic transaction.  All the methods below also use
            // this atomic transaction commit/rollback scheme
            db.commit( );
        }
        catch(SQLException e) {
            // If an exception was thrown, "rollback" the prior updates,
            // removing them from the database.  This also ends the atomic
            // transaction.
            try { db.rollback( ); } catch (Exception e2) {  }
            // Pass the SQLException on in the body of a BankingException
            throw new BankingException("SQLException: " + e.getMessage( ) + 
                                       ": " + e.getSQLState( ));
        }
        // No matter what happens, don't forget to close the DB Statement
        finally { try { s.close( ); } catch (Exception e) {  } }
    }
    
    /** 
     * This convenience method checks whether the name and password match
     * an existing account.  If so, it returns the balance in that account.
     * If not, it throws an exception.  Note that this method does not call
     * commit( ) or rollback( ), so its query is part of a larger transaction.
     **/
    public int verify(String name, String password) 
        throws BankingException, SQLException
    {
        Statement s = null;
        try {
            s = db.createStatement( );
            s.executeQuery("SELECT balance FROM accounts " +
                           "WHERE name='" + name + "' " +
                           "  AND password = '" + password + "'");
            ResultSet r = s.getResultSet( );
            if (!r.next( ))
                throw new BankingException("Bad account name or password");
            return r.getInt(1);
        }
        finally { try { s.close( ); } catch (Exception e) {  } }
    }
    
    /** Close a named account */
    public synchronized FunnyMoney closeAccount(String name, String password)
        throws RemoteException, BankingException
    {
        int balance = 0;
        Statement s = null;
        try {
            balance = verify(name, password);
            s = db.createStatement( );
            // Delete the account from the accounts table
            s.executeUpdate("DELETE FROM accounts " + 
                            "WHERE name = '" + name + "' " +
                            "  AND password = '" + password + "'");
            // And drop the transaction history table for this account
            s.executeUpdate("DROP TABLE " + name + "_history");
            db.commit( );
        }
        catch (SQLException e) {
            try { db.rollback( ); } catch (Exception e2) {  }
            throw new BankingException("SQLException: " + e.getMessage( ) + 
                                       ": " + e.getSQLState( ));
        }
        finally { try { s.close( ); } catch (Exception e) {  } }
        
        // Finally, return whatever balance remained in the account
        return new FunnyMoney(balance);
    }
    
    /** Deposit the specified money into the named account */
    public synchronized void deposit(String name, String password, 
                                     FunnyMoney money) 
        throws RemoteException, BankingException
    {
        int balance = 0; 
        Statement s = null;
        try {
            balance = verify(name, password);
            s = db.createStatement( );
            // Update the balance
            s.executeUpdate("UPDATE accounts " +
                            "SET balance = " + balance + money.amount + " " +
                            "WHERE name='" + name + "' " +
                            "  AND password = '" + password + "'");
            // Add a row to the transaction history
            s.executeUpdate("INSERT INTO " + name + "_history " + 
                            "VALUES ('Deposited " + money.amount + 
                            " at " + new Date( ) + "')");
            db.commit( );
        }
        catch (SQLException e) {
            try { db.rollback( ); } catch (Exception e2) {  }
            throw new BankingException("SQLException: " + e.getMessage( ) + 
                                       ": " + e.getSQLState( ));
        }
        finally { try { s.close( ); } catch (Exception e) {  } }
    }
    
    /** Withdraw the specified amount from the named account */
    public synchronized FunnyMoney withdraw(String name, String password, 
                                            int amount)
        throws RemoteException, BankingException
    {
        int balance = 0;
        Statement s = null;
        try {
            balance = verify(name, password);
            if (balance < amount)
                throw new BankingException("Insufficient Funds");
            s = db.createStatement( );
            // Update the account balance
            s.executeUpdate("UPDATE accounts " +
                            "SET balance = " + (balance - amount) + " " +
                            "WHERE name='" + name + "' " +
                            "  AND password = '" + password + "'");
            // Add a row to the transaction history
            s.executeUpdate("INSERT INTO " + name + "_history " + 
                            "VALUES ('Withdrew " + amount + 
                            " at " + new Date( ) + "')");
            db.commit( );
        }
        catch (SQLException e) {
            try { db.rollback( ); } catch (Exception e2) {  }
            throw new BankingException("SQLException: " + e.getMessage( ) + 
                                       ": " + e.getSQLState( ));
        }
        finally { try { s.close( ); } catch (Exception e) {  } }
        
        return new FunnyMoney(amount);
    }
    
    /** Return the balance of the specified account */
    public synchronized int getBalance(String name, String password)
        throws RemoteException, BankingException
    {
        int balance;
        try {
            // Get the balance
            balance = verify(name, password);
            // Commit the transaction
            db.commit( );
        }
        catch (SQLException e) {
            try { db.rollback( ); } catch (Exception e2) {  }
            throw new BankingException("SQLException: " + e.getMessage( ) + 
                                       ": " + e.getSQLState( ));
        }
        // Return the balance
        return balance;
    }
    
    /** Get the transaction history of the named account */
    public synchronized List getTransactionHistory(String name, 
                                                   String password)
        throws RemoteException, BankingException
    {
        Statement s = null;
        List list = new ArrayList( );
        try {
            // Call verify to check the password, even though we don't 
            // care what the current balance is.
            verify(name, password);
            s = db.createStatement( );
            // Request everything out of the history table
            s.executeQuery("SELECT * from " + name + "_history");
            // Get the results of the query and put them in a Vector
            ResultSet r = s.getResultSet( );
            while(r.next( )) list.add(r.getString(1));
            // Commit the transaction
            db.commit( );
        }
        catch (SQLException e) {
            try { db.rollback( ); } catch (Exception e2) {  }
            throw new BankingException("SQLException: " + e.getMessage( ) + 
                                       ": " + e.getSQLState( ));
        }
        finally { try { s.close( ); } catch (Exception e) {  } }
        // Return the Vector of transaction history.
        return list;
    }
    
    /**
     * This main( ) method is the standalone program that figures out what
     * database to connect to with what driver, connects to the database,
     * creates a PersistentBankServer object, and registers it with the registry,
     * making it available for client use
     **/
    public static void main(String[  ] args) {
        try {
            // Create a new Properties object.  Attempt to initialize it from
            // the BankDB.props file or the file optionally specified on the 
            // command line, ignoring errors.
            Properties p = new Properties( );
            try { p.load(new FileInputStream(args[0])); }
            catch (Exception e) {
                try { p.load(new FileInputStream("BankDB.props")); }
                catch (Exception e2) {  }
            }
            
            // The BankDB.props file (or file specified on the command line)
            // must contain properties "driver" and "database", and may
            // optionally contain properties "user" and "password".
            String driver = p.getProperty("driver");
            String database = p.getProperty("database");
            String user = p.getProperty("user", "");
            String password = p.getProperty("password", "");
            
            // Load the database driver class
            Class.forName(driver);
            
            // Connect to the database that stores our accounts
            Connection db = DriverManager.getConnection(database,
                                                        user, password);
            
            // Configure the database to allow multiple queries and updates
            // to be grouped into atomic transactions
            db.setAutoCommit(false);
            db.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
            
            // Create a server object that uses our database connection
            PersistentBankServer bank = new PersistentBankServer(db);
            
            // Read a system property to figure out how to name this server.
            // Use "SecondRemote" as the default.
            String name = System.getProperty("bankname", "SecondRemote");
            
            // Register the server with the name
            Naming.rebind(name, bank);
            
            // And tell everyone that we're up and running.
            System.out.println(name + " is open and ready for customers.");
        }
        catch (Exception e) {
            System.err.println(e);
            if (e instanceof SQLException) 
                System.err.println("SQL State: " +
                                   ((SQLException)e).getSQLState( ));
            System.err.println("Usage: java [-Dbankname=<name>] " +
                        "je3.rmi.PersistentBankServer " +
                               "[<dbpropsfile>]");
            System.exit(1);
        }
    }
}
    [ Team LiB ] Previous Section Next Section