package examples.cluster.ejb;

import javax.ejb.*;
import javax.naming.*;
import java.rmi.*;
import java.util.Hashtable;

import examples.cluster.utils.*;
import examples.cluster.ejb.teller.*;
import examples.cluster.ejb.account.AccountResult;

/**
 * This class illustrates load balancing and failover
 * using a container-managed JDBC EntityBean.
 * You'll need to have a properly configured database with 
 * the appropriate tables loaded before running this example.
 * <ul>
 * <li> It starts a loop, performs repeated balance lookups, deposits,
 *      withdrawls and transfers into two accounts
 * <li> If a transaction fails, it checks if the transaction was
 *      actually committed; if not, it retries the transaction
 * <li> It then displays statistics showing which servers performed each invocation
 * </ul>
 * <p>
 * The client demonstrates:
 * <ul>
 * <li> Writing a client to take advantage of load balancing and failover in
 *      a clustered setting
 * <li> Container-managed persistence
 * <li> How to search the JNDI tree for an appropriate container
 * <li> Application-defined exceptions
 * </ul>
 *
 * @author Copyright (c) 1999-2000 by BEA Systems, Inc. All Rights Reserved.
 */
public class Client {

  static String    url         = "t3://localhost:7001";
  static String    user        = null;
  static String    password    = null;
  static String    account1    = "10000";
  static String    account2    = "10005";
  static int       ITERATIONS  = 5;
  static final int MAXATTEMPTS = 5;
  static final int SLEEP       = 1000; //time to sleep between invocations
                                        //to give an opportunity to test failover
  static TellerHome   bank;
  static Teller       teller;
  static ClusterUtils stats    = new ClusterUtils();
  static int          i; // iteration
  static int          trans; // transaction within iteration

  Client() {
  }

  /**
   * Runs this example from the command line. Example:
   * <p>
   * <tt>java examples.cluster.ejb.Client "t3://localhost:7001" scott tiger</tt>
   * <p>
   * The parameters are optional, but if any are supplied,
   * they are interpreted in this order:
   * <p>
   * @param url               URL such as "t3://localhost:7001" of a Server or Cluster
   * @param user              User name, default null
   * @param password          User password, default null
   */
  public static void main(String[] args)
  {
    System.out.println("\nBeginning cluster.ejb.Client...\n");
    Client client = new Client();

    // Parse the argument list 
    if ((args == null) || (args.length == 0)) {} 
      else
    for (int i = 0; i < args.length; i++) {
      switch(i) {
        case 0:
          url = args[i];
          break;
        case 1:
          user = args[i];
          break;
        case 2:
          password = args[i];
          break;
        default:
      }
    }

    double        depositAmount    = 0;
    double        withdrawalAmount = 0;
    double        transferAmount   = 0;

    try {
      Context ctx  = getInitialContext();

      // Get a TellerHome object (bank), from whom we can create Tellers
      // who'll later help us execute transactions

      bank = (TellerHome)ctx.lookup("cluster.ejb.TellerHome");

      // Repeat each transaction set, doing four transactions in each set

      for (i = 0 ; i < ITERATIONS; i++) {
        System.out.println("Start of transaction set " + (i+1));

        // Invent amounts for the transactions
        depositAmount   = (100 * (i+1));
        transferAmount  = depositAmount/2;
        withdrawalAmount = transferAmount;
        trans = 0;

        client.new Balance(account1).transaction();
        Thread.sleep(SLEEP);

        client.new Deposit(account1, depositAmount).transaction();
        Thread.sleep(SLEEP);

        client.new Withdrawal(account1, withdrawalAmount).transaction();
        Thread.sleep(SLEEP);

        client.new Transfer(account1, account2, transferAmount).transaction();
        Thread.sleep(SLEEP);

        System.out.println("End of transaction set " + (i+1) + "\n");
      }
    }
    catch (TellerException pe) {
      System.out.println("Processing Error: " + pe);
    } 
    catch (Exception e) {
      System.out.println(":::::::::::::: Error :::::::::::::::::");
      e.printStackTrace();
    }
    System.out.println("\nStatistics for different servers:\n");
    System.out.println(stats.processStatistics());

    System.out.println("\nEnd cluster.ejb.Client...\n");
  }


  /**
   * Gets an initial context for the current user, password and url.
   *
   * @return                  Context
   * @exception               java.lang.Exception if there is
   *                          an error in getting the Context
   */
  static public Context getInitialContext() throws Exception {
    Hashtable h = new Hashtable();
    h.put(Context.INITIAL_CONTEXT_FACTORY,
        "weblogic.jndi.WLInitialContextFactory");
    h.put(Context.PROVIDER_URL, url);
    if (user != null) {
      System.out.println ("user: " + user);
      h.put(Context.SECURITY_PRINCIPAL, user);
      if (password == null) 
      password = "";
      h.put(Context.SECURITY_CREDENTIALS, password);
    } 
    return new InitialContext(h);
  }

  /**
   * Inner class that is used to call the transactions
   * with the session beans.
   * <p>
   * Subclassed to perform specific transaction types.
   *
   */
  class Transaction{

    TellerResult result;
    String tellerServer;

    String transId = getTransactionID();
    double amount;

    String account1Id;
    String account1Server;
    double account1Balance;

    String account2Id;
    String account2Server;
    double account2Balance;

    String tellerMessage;
    String actionMessage;
    String account1Message;
    String account2Message;

    /**
     * Loop that attempts to invoke a transaction
     * on a TellerBean. It will catch any RemoteExceptions
     * from the TellerBean, and after checking if the
     * transaction was comitted, will either return, or 
     * -- if the MAXATTEMPTS has not been exceeded -- 
     * attempt the transaction again.
     *
     * @exception               Exception if the transaction is not
     *                          completed in the MAXATTEMPTS
     * @exception               TellerException if there is
     *                          an error in performing the transaction
     * @exception               RemoteException if there is
     *                          a communications or systems failure
     * @exception               CreateException if there is
     *                          a problem creating a teller
     */
    void transaction() throws Exception, TellerException, RemoteException, CreateException {
      trans++;
      int attempts      = 0;
      int set           = i;
      boolean committed = false;
      boolean invoke    = true;
      System.out.println("Transaction " + trans + " of set " + (set + 1) +
                         " (" + transId + ")");
      while (attempts < MAXATTEMPTS) {
        try {
          attempts++;
          System.out.println("  Attempt " + attempts);
          if (teller == null)
            teller = bank.create();
          if (invoke) {
            invokeTransaction();
            buildReport();
            printReport();
            System.out.println("  End of transaction " + trans + 
                               " of set " + (set + 1));
            return;
          } else { // checking transaction
            committed = teller.checkTransactionId(transId);
            System.out.println("    Checked transaction " + transId + 
                               ": committed = " + committed);
            if (committed) {
              return;
            } else {
              System.out.println("    Attempting Transaction " + trans + " again");
              invoke = true;
            }
          }
        }
        catch (RemoteException re) {
          System.out.println("    Error: " + re);
          // Replace teller, in case that's the problem
          teller = null;
          invoke = false;
        }
      }
      throw new Exception("  Transaction " + trans + " of set " +
                          (set + 1) + " ended unsuccessfully");
    }

    /**
     * Builds a unique identifier for the transaction.
     *
     * @return                  String Transaction ID
     */
    String getTransactionID() {
      return new String("" + System.currentTimeMillis());
    }

    /**
     * Calls a teller bean to perform a transaction.
     * Subclassed in specific transaction types.
     *
     * @exception               TellerException if there is
     *                          an error in performing the transaction
     * @exception               RemoteException if there is
     *                          a communications or systems failure
     */
    void invokeTransaction() throws TellerException, RemoteException {
    }

    /**
     * Builds a report of the result of a transaction.
     * Subclassed in specific transaction types.
     *
     */
    void buildReport() {
      tellerServer    = result.getTellerServer();
      account1Server  = result.getAccount1Result().getServer();
      account1Balance = result.getAccount1Result().getBalance();
      tellerMessage   = "Teller (" + tellerServer + "): ";
      account1Message = "Account " + account1Id + 
                        " (on " + account1Server + "): " +
                        "balance: $" + account1Balance;
    }

    /**
     * Prints a report of the result of a transaction.
     *
     */
    void printReport() {
      System.out.println("    " + tellerMessage + actionMessage);
      stats.addClusterMessage("Teller on: " + tellerServer);
      if (account1Server != null) {
        stats.addClusterMessage("Account 1 on: " + account1Server);
        System.out.println("    " + account1Message);
      }
      if (account2Server != null) {
        stats.addClusterMessage("Account 2 on: " + account2Server);
        System.out.println("    " + account2Message);
      }
    }
  }

  /**
   * Performs a balance lookup.
   * 
   */
  class Balance extends Transaction {

    /**
     * Constructs a <tt>Balance</tt>.
     * 
     * @param accountId         String Account ID
     * @exception               TellerException if there is
     *                          an error in performing the transaction
     * @exception               RemoteException if there is
     *                          a communications or systems failure
     * @exception               CreateException if there is
     *                          a problem creating a teller
     */
    Balance(String accountId)
        throws TellerException, RemoteException, CreateException {
     this.account1Id = accountId;
    }

    /**
     * Calls a teller bean to perform a balance lookup.
     * Overshadows <tt>Transaction.invokeTransaction()</tt>.
     *
     * @exception               TellerException if there is
     *                          an error in performing the transaction
     * @exception               RemoteException if there is
     *                          a communications or systems failure
     */
    void invokeTransaction() throws TellerException, RemoteException {
      result = teller.balance(account1Id);
    }

    /**
     * Builds a report of the result of a transaction.
     * Overshadows <tt>Transaction.buildReport()</tt>.
     *
     */
    void buildReport() {
      super.buildReport();
      actionMessage = "balance of account";
    }
  }

  /**
   * Performs a deposit.
   * 
   */
  class Deposit extends Transaction {

    /**
     * Constructs a <tt>Deposit</tt>.
     * 
     * @param accountId         String Account ID
     * @param amount            double Amount
     * @exception               TellerException if there is
     *                          an error in performing the transaction
     * @exception               RemoteException if there is
     *                          a communications or systems failure
     * @exception               CreateException if there is
     *                          a problem creating a teller
     */
    Deposit(String accountId, double amount)
        throws TellerException, RemoteException, CreateException {
       this.account1Id = accountId;
       this.amount     = amount;
    }

    /**
     * Calls a teller bean to perform a deposit.
     * Overshadows <tt>Transaction.invokeTransaction()</tt>.
     *
     * @exception               TellerException if there is
     *                          an error in performing the transaction
     * @exception               RemoteException if there is
     *                          a communications or systems failure
     */
    void invokeTransaction() throws TellerException, RemoteException {
      result = teller.deposit(account1Id, amount, transId);
    }

    /**
     * Builds a report of the result of a transaction.
     * Overshadows <tt>Transaction.buildReport()</tt>.
     *
     */
    void buildReport() {
      super.buildReport();
      actionMessage = "deposited $" + amount;
    }
  }

  /**
   * Performs a withdrawal.
   * 
   */
  class Withdrawal extends Transaction {

    /**
     * Constructs a <tt>Withdrawal</tt>.
     * 
     * @param accountId         String Account ID
     * @param amount            double Amount
     * @exception               TellerException if there is
     *                          an error in performing the transaction
     * @exception               RemoteException if there is
     *                          a communications or systems failure
     * @exception               CreateException if there is
     *                          a problem creating a teller
     */
    Withdrawal(String accountId, double amount)
        throws TellerException, RemoteException, CreateException {
       this.account1Id = accountId;
       this.amount     = amount;
    }

    /**
     * Calls a teller bean to perform a withdrawal.
     * Overshadows <tt>Transaction.invokeTransaction()</tt>.
     *
     * @exception               TellerException if there is
     *                          an error in performing the transaction
     * @exception               RemoteException if there is
     *                          a communications or systems failure
     */
    void invokeTransaction() throws TellerException, RemoteException {
      result = teller.withdraw(account1Id, amount, transId);
    }

    /**
     * Builds a report of the result of a transaction.
     * Overshadows <tt>Transaction.buildReport()</tt>.
     *
     */
    void buildReport() {
      super.buildReport();
      actionMessage = "withdrew $" + amount;
    }
  }

  /**
   * Performs a transfer.
   * 
   */
  class Transfer extends Transaction {

    /**
     * Constructs a <tt>Transfer</tt>.
     * 
     * @param accountFrom       String Account from ID
     * @param amount            double Amount
     * @param accountTo         String Account to ID
     * @exception               TellerException if there is
     *                          an error in performing the transaction
     * @exception               RemoteException if there is
     *                          a communications or systems failure
     * @exception               CreateException if there is
     *                          a problem creating a teller
     */
    Transfer(String accountFrom, String accountTo, double amount)
        throws TellerException, RemoteException, CreateException {
      this.account1Id = accountFrom;
      this.account2Id = accountTo;
      this.amount     = amount;
    }

    /**
     * Calls a teller bean to perform a transfer.
     * Overshadows <tt>Transaction.invokeTransaction()</tt>.
     *
     * @exception               TellerException if there is
     *                          an error in performing the transaction
     * @exception               RemoteException if there is
     *                          a communications or systems failure
     */
    void invokeTransaction() throws TellerException, RemoteException {
      result = teller.transfer(account1Id, account2Id, amount, transId);
    }

    /**
     * Builds a report of the result of a transaction.
     * Overshadows <tt>Transaction.buildReport()</tt>.
     *
     */
    void buildReport() {
      super.buildReport();
      account2Server  = result.getAccount2Result().getServer();
      account2Balance = result.getAccount2Result().getBalance();
      actionMessage   = "Transfered $" + amount + " from " + account1Id + 
                        " to " + account2Id;
      account2Message = "Account " + account2Id + 
                        " (on " + account2Server + "): " +
                        "balance: $" + account2Balance;
    }
  }
}
