package hirondelle.web4j.database;

import hirondelle.web4j.BuildImpl;
import hirondelle.web4j.readconfig.Config;
import hirondelle.web4j.util.Consts;
import hirondelle.web4j.util.Util;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.logging.Logger;

/** 
 Template for executing a local, non-distributed transaction versus a single database,  
 using a single connection.

 <P>This abstract base class implements the template method design pattern.  
 
 <P>The {@link TxSimple} class should be the first choice for implementing a transaction.
 If it is not suitable (for example, if iteration is involved), then this class can always be used.
 The benefits of using this class to implement transactions is that the caller avoids  
 repeated code involving connections, commit/rollback, handling exceptions and errors, and so on.
 
 <P>See {@link TxIsolationLevel} for remarks on selection of correct isolation level. The {@link DbTx} class 
 is often useful for implementors.
 
 <P>Do not use this class in the context of a <tt>UserTransaction</tt>.
 
 <h3>Example Use Case</h3>
 A DAO method which uses a <tt>TxTemplate</tt> called <tt>AddAllUnknowns</tt> to perform multiple <tt>INSERT</tt> operations :
<PRE>
{@code 
public int addAll(Set<String> aUnknowns) throws DAOException {
   Tx addTx = new AddAllUnknowns(aUnknowns);
   return addTx.executeTx();
}
  }
</PRE>

 The <tt>TxTemplate</tt> class itself, defined inside the same DAO, as an inner class :
<PRE>
{@code 
  private static final class AddAllUnknowns extends TxTemplate {
    AddAllUnknowns(Set<String> aUnknowns){
      super(ConnectionSrc.TRANSLATION);
      fUnknowns = aUnknowns; 
    }
    &amp;Override public int executeMultipleSqls(Connection aConnection) throws SQLException, DAOException {
      int result = 0;
      for(String unknown: fUnknowns){
        addUnknown(unknown, aConnection);
        result = result + 1;
      }
      return result;
    }
    private Set<String> fUnknowns;
    private void addUnknown(String aUnknown, Connection aConnection) throws DAOException {
      DbTx.edit(aConnection, UnknownBaseTextEdit.ADD, aUnknown);
    }
  }
 }
</PRE>
*/
public abstract class TxTemplate implements Tx {
  
  /**
   Constructor for a transaction versus the default database, at the 
   default isolation level.
    
   <P>The default transaction isolation level is configured in <tt>web.xml</tt>.
  */
  public TxTemplate(){
    fDatabaseName = DEFAULT_DB;
    fTxIsolationLevel = fConfig.getSqlEditorDefaultTxIsolationLevel(DEFAULT_DB);
  }
  
  /**
   Constructor for transaction versus the default database, at a custom 
   isolation level.
    
   <P>The default transaction isolation level is configured in <tt>web.xml</tt>.
  */
  public TxTemplate(TxIsolationLevel aTxIsolationLevel){
    fDatabaseName = DEFAULT_DB;
    fTxIsolationLevel = aTxIsolationLevel;
  }
  
  /**
   Constructor for a transaction versus a non-default database, at its
   isolation level, as configured in <tt>web.xml</tt>. 
    
   @param aDatabaseName one of the  return values of {@link ConnectionSource#getDatabaseNames()}
  */
  public TxTemplate(String aDatabaseName){
    fDatabaseName = aDatabaseName;
    fTxIsolationLevel = fConfig.getSqlEditorDefaultTxIsolationLevel(aDatabaseName);
  }
  
  /**
   Constructor for a transaction versus a non-default database, at a custom 
   isolation level. 
    
   <P>The default transaction isolation level is configured in <tt>web.xml</tt>.
   
   @param aDatabaseName one of the  return values of {@link ConnectionSource#getDatabaseNames()}
  */
  public TxTemplate(String aDatabaseName, TxIsolationLevel aTxIsolationLevel){
    fDatabaseName = aDatabaseName;
    fTxIsolationLevel = aTxIsolationLevel;
  }
  
  /**
   <b>Template</b> method calls the abstract method {@link #executeMultipleSqls}.
   <P>Returns the same value as <tt>executeMultipleSqls</tt>.
  
   <P>A <tt>rollback</tt> is performed if <tt>executeMultipleSqls</tt> throws a {@link SQLException} or
   {@link DAOException}, or if {@link #executeMultipleSqls(Connection)} returns {@link #BUSINESS_RULE_FAILURE}.  
  */
  public final int executeTx() throws DAOException {
    int result = 0;
    fLogger.fine(
      "Editing within a local transaction, with isolation level : " + fTxIsolationLevel
    );
    ConnectionSource connSource = BuildImpl.forConnectionSource();
    if(Util.textHasContent(fDatabaseName)){
      fConnection = connSource.getConnection(fDatabaseName);
    }
    else {
      fConnection = connSource.getConnection();
    }
    
    try {
      TxIsolationLevel.set(fTxIsolationLevel, fConnection);
      startTx();
      result = executeMultipleSqls(fConnection);
      endTx(result);
    }
    catch(SQLException rootCause){
      //if SqlEditor is used, this branch will not be exercised, since it throws only
      //DAOExceptions
      fLogger.fine("Transaction throws SQLException.");
      rollbackTx();
      String message = 
        "Cannot execute edit. Error code : " +  rootCause.getErrorCode() + 
        Consts.SPACE + rootCause
      ;
      Integer errorCode = new Integer(rootCause.getErrorCode());
      if (fConfig.getErrorCodesForDuplicateKey(fDatabaseName).contains(errorCode)){
        throw new DuplicateException(message, rootCause);
      }
      else if (fConfig.getErrorCodesForForeignKey(fDatabaseName).contains(errorCode)){
        throw new ForeignKeyException(message, rootCause);
      }
      throw new DAOException(message, rootCause);
    }
    catch (DAOException ex){
      //if SqlEditor is used, it will always throw a DAOException, not SQLException
      fLogger.fine("Transaction throws DAOException.");
      rollbackTx();
      throw ex;
    }
    finally {
      DbUtil.logWarnings(fConnection);
      DbUtil.close(fConnection);
    }
    fLogger.fine("Total number of edited records: " + result);
    return result;
  }

  /**
   Execute multiple SQL operations in a single local transaction.
  
   <P>This method returns the number of records edited. If a business rule determines that a 
   rollback should be performed, then it is recommended that the special value
   {@link #BUSINESS_RULE_FAILURE} be returned by the implementation. This will signal to 
   {@link #executeTx()} that a rollback must be performed. (Another option for 
   signalling that a rollback is desired is to throw a checked exception.)  
  
   <P><em>Design Note</em>: allowing <tt>SQLException</tt> in the <tt>throws</tt> 
   clause simplifies the implementor significantly, since no <tt>try-catch</tt> blocks are 
   needed. Thus, the caller has simple, "straight-line" code.
  
   @param aConnection must be used by all SQL statements participating in this transaction
   @return number of records edited by this operation. Implementations may return 
   {@link #BUSINESS_RULE_FAILURE} if there is a business rule failure. 
  */
  public abstract int executeMultipleSqls(Connection aConnection) throws SQLException, DAOException;
  
  /**
   Value {@value}. Special value returned by {@link #executeMultipleSqls(Connection)} to indicate that  
   a business rule has been violated. Such a return value indicates to this class that a rollback must be 
   performed.
  */
  public static final int BUSINESS_RULE_FAILURE = -1;
  
  // PRIVATE 
  
  /**
   The connection through which all SQL statements attached to this 
   transaction are executed. This connection may be for the default 
   database, or any other defined database. See {@link #fDatabaseName}.
  */
  private Connection fConnection;
  
  /** 
   Identifier for the database. The connection taken from the default
   database only if this item has no content. An empty string implies the default
   database. 
  */
  private String fDatabaseName;
  private static final String DEFAULT_DB = "";
  
  /** The transaction isolation level, set only during the constructor. */
  private final TxIsolationLevel fTxIsolationLevel;
  
  private static final boolean fOFF = false;
  private static final boolean fON = true;
  private Config fConfig = new Config();
  private static final Logger fLogger = Util.getLogger(TxTemplate.class);  

  private void startTx() throws SQLException {
    fConnection.setAutoCommit(fOFF);
  }
  
  private void endTx(int aNumEdits) throws SQLException, DAOException {
    if ( BUSINESS_RULE_FAILURE == aNumEdits ) {
      fLogger.severe("Business rule failure occured. Cannot commit transaction.");
      rollbackTx();
    }
    else {
      fLogger.fine("Commiting transaction.");
      fConnection.commit();
      fConnection.setAutoCommit(fON);
    }
  }
  
  private void rollbackTx() throws DAOException {
    fLogger.severe("ROLLING BACK TRANSACTION.");
    try {
      fConnection.rollback();
    }
    catch(SQLException ex){
      throw new DAOException("Cannot rollback transaction", ex);
    }
  }
}
