package examples.cluster.ejb.teller;

import java.io.Serializable;
import java.rmi.RemoteException;
import java.util.*;
import javax.ejb.*;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.transaction.UserTransaction;
import java.sql.*;

import weblogic.common.*;

import examples.cluster.ejb.account.*;

/**
 * TellerBean is a stateless SessionBean. This bean illustrates:
 * <ul>
 * <li> No persistence of state between calls to the SessionBean
 * <li> Accessing properties in a deployment descriptor
 * <li> Finding entity beans
 * <li> Using bean-managed transactions with entity beans
 * <li> Connecting to a database from within a session bean
 * <li> Application-defined exceptions
 * </ul>
 *
 * @author Copyright (c) 1999-2000 by BEA Systems, Inc. All Rights Reserved.
 */
public class TellerBean implements SessionBean {
  static final boolean VERBOSE  = true;
  static final int     SLEEP    = 3000; //time to sleep to give an 
                                        //opportunity to test failover

  // -----------------------------------------------------------------
  // private variables
  private SessionContext ctx;
  private Context rootCtx;
  private transient String         serverName;
  private transient AccountHome    bank;
  private transient int            maxLogTableSize;
  private transient String         logPoolName;

  // -----------------------------------------------------------------
  // SessionBean implementation

  /**
   * This method is required by the EJB Specification,
   * but is not used by this example.
   *
   */
  public void ejbActivate() {
    if (VERBOSE)
      System.out.println("teller.ejbActivate called");
  }

  /**
   * This method is required by the EJB Specification,
   * but is not used by this example.
   *
   */
  public void ejbRemove() {
    if (VERBOSE)
      System.out.println("teller.ejbRemove called");
  }

  /**
   * This method is required by the EJB Specification,
   * but is not used by this example.
   *
   */
  public void ejbPassivate() {
    if (VERBOSE)
      System.out.println("teller.ejbPassivate called");
  }

  /**
   * Sets the session context, server name, properties,
   * maximum log table size and the log pool name.
   *
   * @param ctx               SessionContext
   */
  public void setSessionContext(SessionContext ctx) {
    if (VERBOSE)
      System.out.println("teller.setSessionContext called");
    this.ctx        = ctx;
    serverName      = getServerName();
  }

  // Public interface 

  /**
   * This method corresponds to the create method in the home interface
   * "TellerHome.java".
   * The parameter sets of the two methods are identical. When the client calls
   * <code>TellerHome.create()</code>, the container allocates an instance of 
   * the EJBean and calls <code>ejbCreate()</code>.
   *
   * @exception               javax.ejb.CreateException
   *                          if there is a problem creating the bean
   * @see                     examples.cluster.ejb.Teller
   */
  public void ejbCreate() throws CreateException {
    if (VERBOSE)
      System.out.println("teller.ejbCreate called");
    try {
	Properties p = new Properties();
	p.put(Context.INITIAL_CONTEXT_FACTORY,
	      "weblogic.jndi.WLInitialContextFactory");
	InitialContext ic = new InitialContext(p);
	rootCtx = (Context) ic.lookup("java:comp/env");
	maxLogTableSize = ((Integer) rootCtx.lookup("maxLogTableSize")).intValue();
	logPoolName = (String) rootCtx.lookup("logPoolName");
    } catch (NamingException ne) {
	throw new CreateException("Could not look up context");
    }
  }

  /**
   * Returns the balance of the account in a TellerResult object.
   *
   * @param accountId         String Account ID
   * @return                  TellerResult Result of inquiry
   * @exception               examples.cluster.ejb.teller.TellerException
   *                          if there is an error while checking the balance
   * @exception               java.rmi.RemoteException
   *                          if there is a communications or systems failure
   */
  public TellerResult balance(String accountId)
      throws TellerException,
             RemoteException
  {
    if (VERBOSE)
      System.out.println("teller.balance called");
    return (this.new Balance(accountId)).transaction();
  }

  /**
   * Deposits amount in specified account using a specific transactionID.
   *
   * @param accountId         String Account ID
   * @param amount            double Amount to deposit
   * @param transactionId     String Transaction ID
   * @return                  TellerResult Result of inquiry
   * @exception               examples.cluster.ejb.teller.TellerException
   *                          if there is an error while making the deposit
   * @exception               java.rmi.RemoteException
   *                          if there is a communications or systems failure
   */
  public TellerResult deposit(String accountId, double amount, String transactionId)
      throws TellerException,
             RemoteException
  {
    if (VERBOSE)
      System.out.println("teller.deposit called");
    return (this.new Deposit(accountId, amount, transactionId)).transaction();
  }

  /**
   * Withdraws amount from specified account using a specific transactionID.
   *
   * @param accountId         String Account ID
   * @param amount            double Amount to withdraw
   * @param transactionId     String Transaction ID
   * @return                  TellerResult Result of inquiry
   * @exception               examples.cluster.ejb.teller.TellerException
   *                          if there is an error while making the withdrawal
   * @exception               java.rmi.RemoteException
   *                          if there is a communications or systems failure
   */
  public TellerResult withdraw(String accountId, double amount, String transactionId)
      throws TellerException,
             RemoteException
  {
    if (VERBOSE)
      System.out.println("teller.withdraw called");
    return (this.new Withdrawal(accountId, amount, transactionId)).transaction();
  }

  /**
   * Transfers amount from accountFrom to accountTo using a specific transactionID.
   *
   * @param accountFrom       String Account ID of account taking amount from
   * @param accountTo         String Account ID of account sending amount to
   * @param amount            double Amount to transfer
   * @param transactionId     String Transaction ID
   * @return                  TellerResult Result of inquiry
   * @exception               examples.cluster.ejb.teller.TellerException
   *                          if there is an error while making the transfer
   * @exception               java.rmi.RemoteException
   *                          if there is a communications or systems failure
   */
  public TellerResult transfer(String accountFrom, String accountTo, 
    double amount, String transactionId)
      throws TellerException,
             RemoteException
  {
    if (VERBOSE)
      System.out.println("teller.transfer called");
    return (this.new Transfer(accountFrom, accountTo, amount, transactionId)).transaction();
  }

  /**
   * Returns <tt>true</tt> if the transaction with a given
   * id was completed (ie, the transaction ID can be found
   * in the transaction log table).
   * <p>
   * Note that the code in this method is specific for Oracle
   * databases and would require adjustment for other databases.
   *
   * @param transactionID     String Transaction ID
   * @return                  boolean
   * @exception               RemoteException if there is
   *                          a communications or systems failure
   */
  public boolean checkTransactionId(String transactionId)
      throws RemoteException {
    if (VERBOSE) {
      System.out.println("teller.checkTransaction called: failover test point");
      try {
        Thread.sleep(SLEEP);
      } catch (InterruptedException ie) {
      // do nothing
      }
    }
    Connection conn  = null;
    Statement  stmt  = null;
    ResultSet  rs    = null;
    boolean    check = false;
    try {
      conn = getDatabaseConnection();
      stmt = conn.createStatement();
      rs = stmt.executeQuery("select transId from ejbTransLog where transId = " + transactionId);
      while (rs.next())
        check = true;
    }
    catch (SQLException sqe) {
      throw new RemoteException (sqe.getMessage());
    }
    finally {
      try {
        if (rs  != null) rs.close();
        if (stmt!= null) stmt.close(); 
        if (conn!= null) conn.close(); 
      }
      catch (Exception ignore) {}
    }
    return check;
  }

// Private methods

  /**
   * Inserts into the transaction log table a record with 
   * the transaction ID and a timestamp in a LRU fashion. 
   * Once the table is filled to capacity (as set by
   * environmental property maxLogTableSize)
   * the oldest member of the table is replaced.
   * <p>
   * Note that the code in this method is specific for Oracle
   * databases and would require adjustment for other databases.
   *
   * @param transactionId String Transaction ID
   * @exception               RemoteException if there is
   *                          a communications or systems failure
   */
  private void setTransactionId(String transactionId) throws RemoteException {
    if (VERBOSE)
      System.out.println("account.setTransactionId called");
    Connection         conn = null;
    Statement          stmt = null;
    ResultSet          rs   = null;
    PreparedStatement  ps   = null;
    int                numberOfTrans = 0; 
    try {
      conn = getDatabaseConnection();
      System.out.println("account.conn.createStatement() called");
      stmt = conn.createStatement();
      rs   = stmt.executeQuery("select count(*) from ejbTransLog");
      while (rs.next())
        numberOfTrans = rs.getInt(1);

      java.sql.Timestamp now = new java.sql.Timestamp(new java.util.Date().getTime());

      if (numberOfTrans < maxLogTableSize) {
        if (VERBOSE)
          System.out.println("account.setTransactionId: inserting record into log");
        ps = conn.prepareStatement("insert into ejbTransLog values (?,?)");
      } else {
        if (VERBOSE)
          System.out.println("account.setTransactionId: updating record in log");
        // This prepared statement selects the oldest row based on the timestamp.
        // In the event there is more than one row with the same timestamp, it selects
        // the min of those rows based on the transaction id.
        ps = conn.prepareStatement(
             "update ejbTransLog set transId = ?, transCommitDate = ? where " +
             "transCommitDate = (select min(transCommitDate) from ejbTransLog) and " +
             "transId = (select min(transId) from ejbTransLog " +
             " where transCommitDate = " +
             "  (select min(transCommitDate) from ejbTransLog) )");
      }
      ps.setString(1, transactionId);
      ps.setTimestamp(2, now);
      ps.execute();
    }
    catch (SQLException sqe) {
      throw new RemoteException (sqe.getMessage());
    }
    finally {
      try {
        if (rs  != null) rs.close();
        if (ps  != null) ps.close(); 
        if (stmt!= null) stmt.close(); 
        if (conn!= null) conn.close(); 
      }
      catch (Exception ignore) {}
    }
  }

  /**
   * Returns a connection from the database pool defined in the
   * deployment descriptor for the bean.
   * <p>
   * Note that this connection pool must be the same pool used for the 
   * entity beans as they are in the same transaction.
   *
   * @return                  Connection Database connection
   * @exception               SQLException if there is
   *                          a problem getting the connection
   */
  private Connection getDatabaseConnection() throws SQLException {
    return DriverManager.getConnection("jdbc:weblogic:jts:" + logPoolName);
  }

  /**
   * Returns the name of the server in the cluster where the transaction is happening.
   *
   * @return                  String Server name (server that invocation ocurred on)
   */
  private String getServerName() {
    if (VERBOSE)
      System.out.println("teller.getServerName called");
    String toReturn = null;
    try {
      toReturn = T3Services.getT3Services().config().getProperty("weblogic.system.name");
      if (toReturn == null) {
        return "";
      } else {
        return toReturn;
      }
    }
    catch (T3Exception t3E) {
      return "";
    }
  }

  /**
   * Inner class that is used for transactions
   * with the entity beans.
   * <p>
   * Subclassed to perform specific transaction types.
   *
   */
  class Transaction{
    String transactionId;
    String account1Id;
    String account2Id;
    double amount;

    AccountPK       account1PK;//     = new AccountPK();
    Account         account1;
    AccountResult   account1Result;// = new AccountResult();

    AccountPK       account2PK;
    Account         account2;
    AccountResult   account2Result;// = new AccountResult();

    UserTransaction tx;

  /**
   * Conducts a transaction, based on transaction type.
   * <p>
   * Just prior to and just after committing the transaction,
   * the method prints a "failover test point" message in the 
   * server output log and then sleeps for approximatly two seconds
   * to allow you to test failover before and after a transaction has
   * been committed.
   *
   * @return                  TellerResult Result of inquiry
   * @exception               examples.cluster.ejb.teller.TellerException
   *                          if there is an error while checking the balance
   * @exception               java.rmi.RemoteException
   *                          if there is a communications or systems failure
   */
  TellerResult transaction() throws TellerException, RemoteException {
    if (VERBOSE)
      System.out.println("teller.transaction: transactionId = " + transactionId);
    try {
      Properties p = new Properties();
      InitialContext ic = new InitialContext(p);
      if (bank == null) {
        if (VERBOSE)
          System.out.println("teller.transaction: looking up bank home");
        bank = (AccountHome)ic.lookup("cluster.ejb.AccountHome");
      }
      if (transactionId != null) {
        tx = (UserTransaction)ic.lookup("javax.transaction.UserTransaction");
        tx.begin();
      }
      invokeTransaction();
      if (transactionId != null) {
        setTransactionId(transactionId);
        if (VERBOSE)
          System.out.println("teller.transaction: tx.commit() about to be called: " +
                             "failover test point");
        Thread.sleep(SLEEP);
        if (VERBOSE)
          System.out.println("teller.transaction: calling tx.commit()");
        tx.commit();
        if (VERBOSE)
          System.out.println("teller.transaction: tx.commit() completed: " +
                             "failover test point");
        Thread.sleep(SLEEP);
      }
    }
    catch (Exception e ) {
      if (transactionId != null) {
        try {
          System.out.println("Rolling back transaction " + transactionId + 
            " because of exception:\n" + e.getMessage());
          tx.rollback();
        } 
        catch (Exception ee) {
          // Something unrecoverable has happened
          throw new TellerException(ee.getMessage());
        }
      }
      throw new RemoteException(e.getMessage());
    }
    return new TellerResult(TellerBean.this.serverName, account1Result, account2Result);
  }

    /**
     * Basic transaction: finds an Account bean
     * based on the primary key.
     * Overshadowed in subclassess
     * that perform specific types of transactions.
     *
     * @exception               AccountException if there is
     *                          an error in performing the transaction
     * @exception               FinderException if there is
     *                          an error in finding the EJBean
     * @exception               RemoteException if there is
     *                          a communications or systems failure
     */
    void invokeTransaction() throws AccountException, FinderException, RemoteException {
      if (account1Id != null) {
        account1PK = new AccountPK(account1Id);
        account1 = bank.findByPrimaryKey(account1PK);
      }
    }
  }

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

    /**
     * Constructs a <tt>Balance</tt>.
     * 
     */
    Balance(String accountId) {
     this.account1Id = accountId;
    }

    /**
     * Finds and calls an Account bean to perform a balance lookup.
     * Overshadows <tt>Transaction.invokeTransaction()</tt>.
     *
     * @exception               AccountException if there is
     *                          an error in performing the transaction
     * @exception               FinderException if there is
     *                          an error in finding the EJBean
     * @exception               RemoteException if there is
     *                          a communications or systems failure
     */
    void invokeTransaction() throws AccountException, FinderException, RemoteException {
      super.invokeTransaction();
      account1Result = account1.balance();
    }
  }

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

    /**
     * Constructs a <tt>Deposit</tt>.
     * 
     */
    Deposit(String accountId, double amount, String transactionId) {
      this.account1Id    = accountId;
      this.amount        = amount;
      this.transactionId = transactionId;
    }

    /**
     * Finds and calls an Account bean to perform a deposit.
     * Overshadows <tt>Transaction.invokeTransaction()</tt>.
     *
     * @exception               AccountException if there is
     *                          an error in performing the transaction
     * @exception               FinderException if there is
     *                          an error in finding the EJBean
     * @exception               RemoteException if there is
     *                          a communications or systems failure
     */
    void invokeTransaction() throws AccountException, FinderException, RemoteException {
      super.invokeTransaction();
      account1Result = account1.deposit(amount);
    }
  }

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

    /**
     * Constructs a <tt>Withdrawl</tt>.
     * 
     */
    Withdrawal(String accountId, double amount, String transactionId) {
      this.account1Id    = accountId;
      this.amount        = amount;
      this.transactionId = transactionId;
    }

    /**
     * Finds and calls an Account bean to perform a withdrawal.
     * Overshadows <tt>Transaction.invokeTransaction()</tt>.
     *
     * @exception               AccountException if there is
     *                          an error in performing the transaction
     * @exception               FinderException if there is
     *                          an error in finding the EJBean
     * @exception               RemoteException if there is
     *                          a communications or systems failure
     */
    void invokeTransaction() throws AccountException, FinderException, RemoteException {
      super.invokeTransaction();
      account1Result = account1.withdraw(amount);
    }
  }

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

    /**
     * Constructs a <tt>Transfer</tt>.
     * 
     */
    Transfer(String accountFrom, String accountTo, double amount, String transactionId) {
      this.account1Id    = accountFrom;
      this.account2Id    = accountTo;
      this.amount        = amount;
      this.transactionId = transactionId;
    }

    /**
     * Finds and calls two Account beans to perform a transfer.
     * Overshadows <tt>Transaction.invokeTransaction()</tt>.
     *
     * @exception               AccountException if there is
     *                          an error in performing the transaction
     * @exception               FinderException if there is
     *                          an error in finding the EJBean
     * @exception               RemoteException if there is
     *                          a communications or systems failure
     */
    void invokeTransaction() throws AccountException, FinderException, RemoteException {
      super.invokeTransaction();
      if (account2Id != null) {
        account2PK     = new AccountPK(account2Id);
        account2       = bank.findByPrimaryKey(account2PK);
        account1Result = account1.withdraw(amount);
        account2Result = account2.deposit(amount);
      }
    }
  }
}
