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);
}
}
}