/*
 * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.jca;

import com.caucho.log.Log;
import com.caucho.transaction.TransactionImpl;
import com.caucho.transaction.TransactionManagerImpl;
import com.caucho.util.L10N;
import com.caucho.webbeans.component.*;

import javax.resource.spi.ConnectionRequestInfo;
import javax.resource.spi.ManagedConnectionFactory;
import javax.security.auth.Subject;
import javax.transaction.*;
import javax.transaction.xa.Xid;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Implementation of the UserTransactionImpl for a thread instance.
 */
public class UserTransactionImpl
  implements UserTransaction
{
  private static final Logger log
    = Logger.getLogger(UserTransactionImpl.class.getName());
  private static final L10N L = new L10N(UserTransactionImpl.class);

  private TransactionManagerImpl _transactionManager;

  private ArrayList<UserPoolItem> _resources = new ArrayList<UserPoolItem>();
  private ArrayList<PoolItem> _poolItems = new ArrayList<PoolItem>();
  private ArrayList<BeginResource> _beginResources
    = new ArrayList<BeginResource>();
  private ArrayList<CloseResource> _closeResources
    = new ArrayList<CloseResource>();

  private boolean _isInContext;
  private int _xaDepth;
  
  /**
   * Creates the proxy.
  */
  public UserTransactionImpl(TransactionManagerImpl tm)
  {
    _transactionManager = tm;
  }
  
  /**
   * Sets the transaction's timeout.
   */
  public void setTransactionTimeout(int seconds)
    throws SystemException
  {
    _transactionManager.setTransactionTimeout(seconds);
  }
  
  /**
   * Gets the transaction's status
   */
  public int getStatus()
    throws SystemException
  {
    return _transactionManager.getStatus();
  }

  /**
   * inContext is valid within a managed UserTransactionImpl context, e.g
   * in a webApp, but not in a cron job.
   */
  public boolean isInContext()
  {
    return _isInContext;
  }

  /**
   * inContext is valid within a managed UserTransactionImpl context, e.g
   * in a webApp, but not in a cron job.
   */
  public void setInContext(boolean isInContext)
  {
    _isInContext = isInContext;
  }

  /**
   * Enlist a resource.
   */
  void enlistResource(UserPoolItem resource)
    throws SystemException, RollbackException
  {
    if (_resources.contains(resource))
      return;
    
    Transaction xa = _transactionManager.getTransaction();
    if (xa != null) {
      PoolItem poolItem = resource.getXAPoolItem();

      enlistPoolItem(xa, poolItem);
    }
    
    _resources.add(resource);
  }

  private void enlistPoolItem(Transaction xa, PoolItem poolItem)
    throws SystemException, RollbackException
  {
    if (poolItem == null)
      return;
    else if (! poolItem.supportsTransaction()) {
      // server/164j
      return;
    }
    
    // XXX: new
    if (_poolItems.contains(poolItem))
      return;
	
    poolItem.setTransaction(this);

    if (xa instanceof TransactionImpl) {
      TransactionImpl xaImpl = (TransactionImpl) xa;
      
      // server/164l
      if (xaImpl.allowLocalTransactionOptimization())
	poolItem.enableLocalTransactionOptimization(true);
    }

    if (poolItem.getXid() == null)
      xa.enlistResource(poolItem);
    
    _poolItems.add(poolItem);
  }

  /**
   * Delist a pool item
   */
  void delistPoolItem(PoolItem poolItem, int flags)
    throws SystemException, RollbackException
  {
    Transaction xa = _transactionManager.getTransaction();

    try {
      if (xa != null)
	xa.delistResource(poolItem, flags);
    } finally {
      _poolItems.remove(poolItem);
    }
  }

  /**
   * Delist a resource.
   */
  void delistResource(UserPoolItem resource)
  {
    _resources.remove(resource);
  }

  /**
   * Enlist a resource automatically called when a transaction begins
   */
  public void enlistBeginResource(BeginResource resource)
  {
    _beginResources.add(resource);

    try {
      Transaction xa = _transactionManager.getTransaction();
      if (xa != null)
	resource.begin(xa);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Enlist a resource automatically closed when the context ends.
   */
  public void enlistCloseResource(CloseResource resource)
  {
    _closeResources.add(resource);
  }

  /**
   * Allocates a resource matching the parameters.  If none matches,
   * return null.
   */
  UserPoolItem allocate(ManagedConnectionFactory mcf,
			Subject subject,
			ConnectionRequestInfo info)
  {
    if (_xaDepth == 0)
      return null;
    
    ArrayList<PoolItem> poolItems = _poolItems;
    int length = poolItems.size();
    
    for (int i = 0; i < length; i++) {
      PoolItem poolItem = poolItems.get(i);

      UserPoolItem item = poolItem.allocateXA(mcf, subject, info);

      if (item != null)
	return item;
    }

    return null;
  }

  /**
   * Finds the pool item joined to this one.
   * return null.
   */
  PoolItem findJoin(PoolItem item)
  {
    if (_xaDepth == 0)
      return null;
    
    ArrayList<PoolItem> poolItems = _poolItems;
    int length = poolItems.size();
    
    for (int i = 0; i < length; i++) {
      PoolItem poolItem = poolItems.get(i);

      if (poolItem.isJoin(item))
	return poolItem;
    }

    return null;
  }

  /**
   * Returns the XID.
   */
  public Xid getXid()
    throws SystemException, RollbackException
  {
    TransactionImpl xa = (TransactionImpl) _transactionManager.getTransaction();

    if (xa != null)
      return xa.getXid();
    else
      return null;
  }

  /**
   * Returns the XID.
   */
  public int getEnlistedResourceCount()
    throws SystemException, RollbackException
  {
    TransactionImpl xa = (TransactionImpl) _transactionManager.getTransaction();

    if (xa != null)
      return xa.getEnlistedResourceCount();
    else
      return 0;
  }
  
  /**
   * Start the transaction.
   */
  public void begin()
    throws NotSupportedException, SystemException
  {
    _transactionManager.begin();
    _xaDepth++;
    boolean isOkay = false;

    try {
      TransactionImpl xa = (TransactionImpl) _transactionManager.getTransaction();
      xa.setUserTransaction(this);
    
      _poolItems.clear();
    
      // enlist "cached" connections
      int length = _resources.size();

      for (int i = 0; i < length; i++) {
	UserPoolItem userPoolItem = _resources.get(i);

	for (int j = _poolItems.size() - 1; j >= 0; j--) {
	  PoolItem poolItem = _poolItems.get(j);

	  if (poolItem.share(userPoolItem)) {
	    break;
	  }
	}

	PoolItem xaPoolItem = userPoolItem.getXAPoolItem();
	if (! _poolItems.contains(xaPoolItem))
	  _poolItems.add(xaPoolItem);
      }

      for (int i = 0; i < _poolItems.size(); i++) {
	PoolItem poolItem = _poolItems.get(i);

	poolItem.enableLocalTransactionOptimization(_poolItems.size() == 1);

	try {
	  xa.enlistResource(poolItem);
	} catch (Exception e) {
	  throw new SystemException(e);
	}
      }

      // enlist begin resources
      for (int i = 0; i < _beginResources.size(); i++) {
	try {
	  BeginResource resource = _beginResources.get(i);

	  resource.begin(xa);
	} catch (Throwable e) {
	  log.log(Level.WARNING, e.toString(), e);
	}
      }

      isOkay = true;
    } finally {
      if (! isOkay) {
	log.warning("Rolling back transaction from failed begin()");
	
	// something has gone very wrong
	_xaDepth--;

	ArrayList<PoolItem> recoveryList = new ArrayList<PoolItem>(_poolItems);
	_poolItems.clear();
	_resources.clear();

	for (int i = 0; i < recoveryList.size(); i++) {
	  try {
	    PoolItem item = recoveryList.get(i);

	    item.abortConnection();
	    
	    item.destroy();
	  } catch (Throwable e) {
	    log.log(Level.FINE, e.toString(), e);
	  }
	}
	
	_transactionManager.rollback();
      }
    }
  }

  /**
   * Suspends the transaction.
   */
  public UserTransactionSuspendState userSuspend()
  {
    if (_xaDepth == 0)
      throw new IllegalStateException(L.l("suspend may only be called in a transaction."));

    _xaDepth--;
    
    UserTransactionSuspendState state;
    state = new UserTransactionSuspendState(_poolItems);
    _poolItems.clear();

    return state;
  }

  /**
   * Resumes the transaction.
   */
  public void userResume(UserTransactionSuspendState state)
  {
    /*
    if (_inTransaction)
      throw new IllegalStateException(L.l("resume may only be called outside of a transaction."));
    */

    _xaDepth++;

    _poolItems.addAll(state.getPoolItems());
  }
  
  /**
   * Marks the transaction as rollback only.
   */
  public void setRollbackOnly()
    throws IllegalStateException, SystemException
  {
    _transactionManager.setRollbackOnly();
  }
  
  /**
   * Marks the transaction as rollback only.
   */
  public void setRollbackOnly(Exception e)
    throws IllegalStateException
  {
    _transactionManager.setRollbackOnly(e);
  }
  
  /**
   * Commits the transaction
   */
  public void commit()
    throws IllegalStateException, RollbackException, HeuristicMixedException,
	   HeuristicRollbackException, SecurityException, SystemException
  {
    try {
      /* XXX: interaction with hessian XA
      if (_xaDepth == 0)
	throw new IllegalStateException("Can't commit outside of a transaction.  Either the UserTransaction.begin() is missing or the transaction has already been committed or rolled back.");
      */
      
      _transactionManager.commit();
    } finally {
      _poolItems.clear();
      if (_xaDepth > 0)
	_xaDepth--;
    }
  }
  
  /**
   * Rolls the transaction back
   */
  public void rollback()
    throws IllegalStateException, SecurityException, SystemException
  {
    try {
      _transactionManager.rollback();
    } finally {
      _poolItems.clear();
      if (_xaDepth > 0)
	_xaDepth--;
    }
  }

  /**
   * Aborts the transaction.
   */
  public void abortTransaction()
    throws IllegalStateException
  {
    IllegalStateException exn = null;

    _isInContext = false;
    
    boolean inTransaction = _xaDepth > 0;
    _xaDepth = 0;

    if (! inTransaction && _poolItems.size() > 0) {
      Exception e = new IllegalStateException("user transaction pool broken");
      log.log(Level.WARNING, e.toString(), e);
    }
    
    _poolItems.clear();

    if (inTransaction) {
      try {
	TransactionImpl xa = (TransactionImpl) _transactionManager.getTransaction();

	exn = new IllegalStateException(L.l("Transactions must have a commit() or rollback() in a finally block."));
      
	log.warning("Rolling back dangling transaction.  All transactions must have a commit() or rollback() in a finally block.");
      
	_transactionManager.rollback();
      } catch (Throwable e) {
	log.log(Level.WARNING, e.toString());
      }

    }

    _beginResources.clear();
    
    while (_closeResources.size() > 0) {
      try {
	CloseResource resource;

	resource = _closeResources.remove(_closeResources.size() - 1);
	resource.close();
      } catch (Throwable e) {
	log.log(Level.WARNING, e.toString(), e);
      }
    }

    boolean hasWarning = false;
    
    while (_resources.size() > 0) {
      UserPoolItem userPoolItem = _resources.remove(_resources.size() - 1);

      if (! userPoolItem.isCloseDanglingConnections())
	continue;
      
      if (! hasWarning) {
	hasWarning = true;

	log.warning("Closing dangling connections.  All connections must have a close() in a finally block.");
      }
      
      try {
	IllegalStateException stackTrace = userPoolItem.getAllocationStackTrace();

	if (stackTrace != null)
	  log.log(Level.WARNING, stackTrace.getMessage(), stackTrace);
	else {
	  // start saving the allocation stack trace.
	  userPoolItem.setSaveAllocationStackTrace(true);
	}
      
	if (exn == null)
	  exn = new IllegalStateException(L.l("Connection {0} was not closed. Connections must have a close() in a finally block.",
					      userPoolItem.getUserConnection()));

	userPoolItem.abortConnection();
      } catch (Throwable e) {
	log.log(Level.WARNING, e.toString(), e);
      }
    }

    _poolItems.clear();

    try {
      _transactionManager.setTransactionTimeout(0);
    } catch (Throwable e) {
      log.log(Level.WARNING, e.toString(), e);
    }

    if (exn != null)
      throw exn;
  }
}

