BankExampleWithDatabase.java

/*
 * BankExampleWithDatabase.java
 *
 * Copyright (C) 2004-08 Side of Software (SOS)
 * All rights reserved.
 *
 *    http://www.sideofsoftware.com
 */

package examples;

import java.io.*;
import java.util.*;
import sos.db.*;

/**
 * A sample program that makes use of Side of Software's
 * Persistence Library. Compare this implementation
 * with BankExampleWithoutDatabase.java, which
 * is the same application, but without a database.<p>
 * This class is part of Side of Software's Persistence Library
 * tutorial.<p>
 *
 * @author  Side of Software
 */
public class BankExampleWithDatabase
{
  /** Number of accounts in this sample application. */
  private static final int NUM_ACCOUNTS = 10;
  
  /** Number of reports for each report thread to generate. */
  private static final int NUM_REPORTS = 10;
  
  /** Number of deposits for each deposit thread to make. */
  private static final int NUM_DEPOSITS = 10;
  
  /** Number of transfers for each transfer thread to make. */
  private static final int NUM_TRANSFERS = 10;
  
  /** Number of threads to make deposits. */
  private static final int NUM_DEPOSIT_THREADS = 5;
  
  /** Number of threads to make transfers. */
  private static final int NUM_TRANSFER_THREADS = 5;
  
  /** Number of threads to generate summary reports. */
  private static final int NUM_REPORT_THREADS = 10;

  /** Initial balance of new accounts. */
  private static final int INITIAL_BALANCE = 100;
  
  /** Amount of each deposit. */
  private static final int AMOUNT_TO_DEPOSIT = 10;
  
  /** Amount of each transfer. */
  private static final int AMOUNT_TO_TRANSFER = 20;
  
  /** The underlying database. */
  private Database db;
  
  /** A mapping from account number to account. */
  private Map accounts;

  /** 
   * Creates an instance of <code>ExampleWithDatabase</code>.
   */
  public BankExampleWithDatabase() throws IOException
  {
    // construct the database
    // db = new InMemoryDatabase( "Account Database" );
    db = new FileDatabase( "Account Database", "test\\accts.db" );
    
    // create the database with the account map as the root object
    db.create( new HashMap() );
    
    // open the database
    db.open( false );
    
    // load the root object
    accounts = (Map)db.getRoot();

    // create the accounts
    db.startTransaction();
    for( int i = 0; i < NUM_ACCOUNTS; i++ )
      openAccount( i, INITIAL_BALANCE );
    db.commitTransaction( null );
  }
  
  /**
   * Returns a new thread that repeatedly deposits to all of the accounts.<p>
   *
   * @param depositAmount amount of each deposit
   * @return the new thread that makes deposits
   */  
  private Thread createDepositThread( final double depositAmount )
  {
    Thread thread = new Thread( new Runnable() {
      public void run()
      {
        for( int acctNumber = 0; acctNumber < NUM_ACCOUNTS; acctNumber++ )
        {
          Account acct = (Account)lookupAccount( acctNumber );

          for( int i = 0; i < NUM_DEPOSITS; i++ )
          {
            deposit( acct, depositAmount );   // make the deposit
          }
        }
      }
    } );
    return thread;
  }
  
  /**
   * Returns a new thread that repeatedly sums the balances of all accounts.<p>
   *
   * @return the new thread to sum the balances
   */  
  private Thread createReportThread()
  {
    Thread thread = new Thread( new Runnable() {
      public void run()
      {
        for( int i = 0; i < NUM_REPORTS; i++ )
        {
          generateReport();   // generate the report
        }
      }
    } );
    return thread;
  }
  
  /**
   * Returns a new thread that repeatedly transfers from one account to the next.<p>
   *
   * @param transferAmount amount of each deposit
   * @return the new thread that makes deposits
   */  
  private Thread createTransferThread( final double transferAmount )
  {
    Thread thread = new Thread( new Runnable() {
      public void run()
      {
        for( int acctNumber = 0; acctNumber < NUM_ACCOUNTS; acctNumber++ )
        {
          Account fromAcct = lookupAccount( acctNumber );
          Account toAcct = lookupAccount( ( acctNumber + 1 ) % NUM_ACCOUNTS );

          for( int i = 0; i < NUM_TRANSFERS; i++ )
          {
            transfer( fromAcct, toAcct, transferAmount );   // make the transfer
          }
        }
      }
    } );
    return thread;
  }
  
  /**
   * Deposits the specified amount to the specified account.<p>
   *
   * @param acct account to add or subtract money
   * @param amount amount to deposit (may be negative)
   */  
  private void deposit( Account acct, double amount )
  {
    db.startTransaction();
    double balance = acct.getBalance();   // get the current balance
    balance += amount;                    // adjust the balance
    acct.setBalance( balance );           // set the new balance
    db.commitTransaction( null );
  }

  /**
   * Iterates through all accounts, summing the balances.<p>
   */
  private void generateReport()
  {
    db.startTransaction();

    int numAccounts = accounts.size();                // how many accounts?
    double total = 0;
    
    // for each account
    Collection values = accounts.values();
    Iterator acctIterator = values.iterator();
    while( acctIterator.hasNext() )
    {
      Account account = (Account)acctIterator.next();
      double balance = account.getBalance();          // get the balance
      total += balance;                               // keep a running total
    }
    
    db.commitTransaction( null );
    
    // print the report
    System.err.println( total + " for " + numAccounts + " accounts " );
  }
  
  /**
   * Finds the account with the specified account number. Returns <code>null</code>
   * if no account is found.<p>
   *
   * @param accountNumber the number of the account to find
   * @return the account with the specified account number or <code>null</code>
   *    if none found
   */  
  private Account lookupAccount( int accountNumber )
  {
    Object key = new Integer( accountNumber );
    return (Account)accounts.get( key );        // search the accounts
  }
  
  /**
   * Serves as the entry point for the sample program.<p>
   *
   * @param args ignored
   */
  public static void main(String[] args) throws InterruptedException, IOException, NoSuchMethodException
  {
    // indicate that getBalance does not modify the account
    ReadOnlyManager.setMethodStatus( Account.class, "getBalance", 
                                     null, ReadOnlyManager.READ_ONLY );
    
    BankExampleWithDatabase example = new BankExampleWithDatabase();
    example.run();
  }
  
  /**
   * Adds a new account with the specified number and initial balance.<p>
   *
   * @param accountNumber account number of account to open
   * @param initialBalance initial balance of the account to open
   * @return the new account
   */  
  private Account openAccount( int accountNumber, double initialBalance )
  {
    db.startTransaction();
    
    // create the account
    Account account = new DefaultAccount();
    account = (Account)db.addObject( account );
    
    // store this account in the map indexed by account number
    Object key = new Integer( accountNumber );
    accounts.put( key, account );
    
    // set the initial balance of this account
    account.setBalance( initialBalance );
    
    db.commitTransaction( null );
    return account;
  }

  /**
   * Runs the sample program.<p>
   */
  public void run() throws InterruptedException, IOException
  {
    // create threads that will test the banking system
    int numThreads = NUM_DEPOSIT_THREADS + NUM_TRANSFER_THREADS + NUM_REPORT_THREADS;
    Thread[] threads = new Thread[numThreads];
    
    int index = 0;
    
    // create threads that repeatedly deposit money to all accounts
    for( int i = 0; i < NUM_DEPOSIT_THREADS; i++ )
      threads[index++] = createDepositThread( AMOUNT_TO_DEPOSIT );
    
    // create threads that repeatedly transfer money among accounts
    for( int i = 0; i < NUM_TRANSFER_THREADS; i++ )
      threads[index++] = createTransferThread( AMOUNT_TO_TRANSFER );
    
    // create threads that repeatedly report account totals
    for( int i = 0; i < NUM_REPORT_THREADS; i++ )
      threads[index++] = createReportThread();

    // start the threads
    for( int i = 0; i < threads.length; i++ )
      threads[i].start();
    
    // wait until the threads stop
    for( int i = 0; i < threads.length; i++ )
      threads[i].join();
    
    // verify that the account balances are what we expected
    double expectedBalance = INITIAL_BALANCE + NUM_DEPOSIT_THREADS * NUM_DEPOSITS * AMOUNT_TO_DEPOSIT;
    for( int i = 0; i < NUM_ACCOUNTS; i++ )
    {
      Account acct = lookupAccount( i );
      double balance = acct.getBalance();
      assert balance == expectedBalance : "Incorrect balance for account " + i + ": " + balance;
    }
    
    // close the database
    db.close();
  }
  
  /**
   * Transfers the specified amount from one account to another.<p>
   *
   * @param fromAcct account to transfer from
   * @param toAcct account to transfer to
   * @param amount the amount to transfer
   */  
  private void transfer( Account fromAcct, Account toAcct, double amount )
  {
    db.startTransaction();
    deposit( fromAcct, -amount );   // take money from one account
    deposit( toAcct, amount );      // add the money to the other account
    db.commitTransaction( null );
  }
}