/*
 * JBoss, the OpenSource J2EE webOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 *
 */
package org.jboss.resource.adapter.jdbc;

import java.io.PrintWriter;
import java.io.Serializable;
import java.io.ObjectStreamField;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Iterator;
import java.util.Properties;
import java.util.Set;
import java.security.PrivilegedAction;
import java.security.AccessController;
import javax.resource.ResourceException;
import javax.resource.spi.ConnectionManager;
import javax.resource.spi.ConnectionRequestInfo;
import javax.resource.spi.ManagedConnection;
import javax.resource.spi.ManagedConnectionFactory;
import javax.resource.spi.security.PasswordCredential;
import javax.security.auth.Subject;
import org.jboss.logging.Logger;
import org.jboss.resource.JBossResourceException;

/**
 * BaseWrapperManagedConnectionFactory.java
 *
 * Created: Fri Apr 19 13:33:08 2002
 *
 * @author <a href="mailto:d_jencks@users.sourceforge.net">David Jencks</a>
 * @author <a href="mailto:adrian@jboss.com">Adrian Brock</a>
 * @version $Revision: 1.16.2.4 $
 */

public abstract class BaseWrapperManagedConnectionFactory
   implements ManagedConnectionFactory, Serializable
{
   /** @since 4.0.1 */
   static final long serialVersionUID = -84923705377702088L;

   public static final int TRACK_STATEMENTS_FALSE_INT = 0;
   public static final int TRACK_STATEMENTS_TRUE_INT = 1;
   public static final int TRACK_STATEMENTS_NOWARN_INT = 2;

   public static final String TRACK_STATEMENTS_FALSE = "false";
   public static final String TRACK_STATEMENTS_TRUE = "true";
   public static final String TRACK_STATEMENTS_NOWARN = "nowarn";

   protected final Logger log = Logger.getLogger(getClass());

   protected String userName;
   protected String password;

   //This is used by Local wrapper for all properties, and is left
   //in this class for ease of writing getConnectionProperties,
   //which always holds the user/pw.
   protected final Properties connectionProps = new Properties();

   protected int transactionIsolation = -1;

   protected int preparedStatementCacheSize = 0;

   protected boolean doQueryTimeout = false;

   /**
    * The variable <code>newConnectionSQL</code> holds an SQL
    * statement which if not null is executed when a new Connection is
    * obtained for a new ManagedConnection.
    *
    */
   protected String newConnectionSQL;

   /**
    * The variable <code>checkValidConnectionSQL</code> holds an sql
    * statement that may be executed whenever a managed connection is
    * removed from the pool, to check that it is still valid.  This
    * requires setting up an mbean to execute it when notified by the
    * ConnectionManager.
    *
    */
   protected String checkValidConnectionSQL;

   /**
    * The classname used to check whether a connection is valid
    */
   protected String validConnectionCheckerClassName;

   /**
    * The instance of the valid connection checker
    */
   protected ValidConnectionChecker connectionChecker;

   private String exceptionSorterClassName;

   private ExceptionSorter exceptionSorter;

   protected int trackStatements = TRACK_STATEMENTS_NOWARN_INT;

   /** Whether to cached share prepared statements */
   protected boolean sharePS = false;
   
   public BaseWrapperManagedConnectionFactory ()
   {

   }
   // implementation of javax.resource.spi.ManagedConnectionFactory interface

   /**
    *
    * @return <description>
    * @exception javax.resource.ResourceException <description>
    */
   public PrintWriter getLogWriter() throws ResourceException
   {
      // TODO: implement this javax.resource.spi.ManagedConnectionFactory method
      return null;
   }

   /**
    *
    * @param param1 <description>
    * @exception javax.resource.ResourceException <description>
    */
   public void setLogWriter(PrintWriter param1) throws ResourceException
   {
      // TODO: implement this javax.resource.spi.ManagedConnectionFactory method
   }

   /**
    *
    * @param cm <description>
    * @return <description>
    * @exception javax.resource.ResourceException <description>
    */
   public Object createConnectionFactory(ConnectionManager cm) throws ResourceException
   {
      return new WrapperDataSource(this, cm);
   }

   /**
    *
    * @return <description>
    * @exception javax.resource.ResourceException <description>
    */
   public Object createConnectionFactory() throws ResourceException
   {
      throw new JBossResourceException("NYI");
      //return createConnectionFactory(new DefaultConnectionManager());
   }

   //-----------Property setting code
   /**
    * Get the value of userName.
    * @return value of userName.
    */
   public String getUserName()
   {
      return userName;
   }

   /**
    * Set the value of userName.
    * @param userName  Value to assign to userName.
    */
   public void setUserName(final String userName)
   {
      this.userName = userName;
   }

   /**
    * Get the value of password.
    * @return value of password.
    */
   public String getPassword()
   {
      return password;
   }

   /**
    * Set the value of password.
    * @param password  Value to assign to password.
    */
   public void setPassword(final String password)
   {
      this.password = password;
   }

   public int getPreparedStatementCacheSize()
   {
      return preparedStatementCacheSize;
   }

   public void setPreparedStatementCacheSize(int size)
   {
      preparedStatementCacheSize = size;
   }

   public boolean getSharePreparedStatements()
   {
      return sharePS;
   }

   public void setSharePreparedStatements(boolean sharePS)
   {
      this.sharePS = sharePS;
   }

   public boolean getTxQueryTimeout()
   {
      return doQueryTimeout;
   }

   public void setTxQueryTimeout(boolean qt)
   {
      doQueryTimeout = qt;
   }

   /**
    * Gets the TransactionIsolation attribute of the JDBCManagedConnectionFactory
    * object
    *
    * @return   The TransactionIsolation value
    */
   public String getTransactionIsolation()
   {
      switch (this.transactionIsolation)
      {
         case Connection.TRANSACTION_NONE:
            return "TRANSACTION_NONE";
         case Connection.TRANSACTION_READ_COMMITTED:
            return "TRANSACTION_READ_COMMITTED";
         case Connection.TRANSACTION_READ_UNCOMMITTED:
            return "TRANSACTION_READ_UNCOMMITTED";
         case Connection.TRANSACTION_REPEATABLE_READ:
            return "TRANSACTION_REPEATABLE_READ";
         case Connection.TRANSACTION_SERIALIZABLE:
            return "TRANSACTION_SERIALIZABLE";
         case -1:
            return "DEFAULT";
         default:
            return Integer.toString(transactionIsolation);
      }
   }


   /**
    * Sets the TransactionIsolation attribute of the JDBCManagedConnectionFactory
    * object
    *
    * @param transactionIsolation  The new TransactionIsolation value
    */
   public void setTransactionIsolation(String transactionIsolation)
   {
      if (transactionIsolation.equals("TRANSACTION_NONE"))
      {
         this.transactionIsolation = Connection.TRANSACTION_NONE;
      }
      else if (transactionIsolation.equals("TRANSACTION_READ_COMMITTED"))
      {
         this.transactionIsolation = Connection.TRANSACTION_READ_COMMITTED;
      }
      else if (transactionIsolation.equals("TRANSACTION_READ_UNCOMMITTED"))
      {
         this.transactionIsolation = Connection.TRANSACTION_READ_UNCOMMITTED;
      }
      else if (transactionIsolation.equals("TRANSACTION_REPEATABLE_READ"))
      {
         this.transactionIsolation = Connection.TRANSACTION_REPEATABLE_READ;
      }
      else if (transactionIsolation.equals("TRANSACTION_SERIALIZABLE"))
      {
         this.transactionIsolation = Connection.TRANSACTION_SERIALIZABLE;
      }
      else
      {
         try
         {
            this.transactionIsolation = Integer.parseInt(transactionIsolation);
         }
         catch (NumberFormatException nfe)
         {
            throw new IllegalArgumentException("Setting Isolation level to unknown state: " + transactionIsolation);
         }
      }
   }


   /**
    * Get the NewConnectionSQL value.
    * @return the NewConnectionSQL value.
    */
   public String getNewConnectionSQL()
   {
      return newConnectionSQL;
   }

   /**
    * Set the NewConnectionSQL value.
    * @param newConnectionSQL The new NewConnectionSQL value.
    */
   public void setNewConnectionSQL(String newConnectionSQL)
   {
      this.newConnectionSQL = newConnectionSQL;
   }


   /**
    * Get the CheckValidConnectionSQL value.
    * @return the CheckValidConnectionSQL value.
    */
   public String getCheckValidConnectionSQL()
   {
      return checkValidConnectionSQL;
   }

   /**
    * Set the CheckValidConnectionSQL value.
    * @param checkValidConnectionSQL The new CheckValidConnectionSQL value.
    */
   public void setCheckValidConnectionSQL(String checkValidConnectionSQL)
   {
      this.checkValidConnectionSQL = checkValidConnectionSQL;
   }


   /**
    * Whether to track statements
    * @return true when tracking statements
    */
   public String getTrackStatements()
   {
      if (trackStatements == TRACK_STATEMENTS_FALSE_INT)
         return TRACK_STATEMENTS_FALSE;
      else if (trackStatements == TRACK_STATEMENTS_TRUE_INT)
         return TRACK_STATEMENTS_TRUE;
      return TRACK_STATEMENTS_NOWARN;
   }

   /**
    * Set the track statements value.
    * @param value true to track statements.
    */
   public void setTrackStatements(String value)
   {
      if (value == null)
         throw new IllegalArgumentException("Null value for trackStatements"); 
      String trimmed = value.trim();
      if (trimmed.equalsIgnoreCase(TRACK_STATEMENTS_FALSE))
         trackStatements = TRACK_STATEMENTS_FALSE_INT;
      else if (trimmed.equalsIgnoreCase(TRACK_STATEMENTS_TRUE))
         trackStatements = TRACK_STATEMENTS_TRUE_INT;
      else
         trackStatements = TRACK_STATEMENTS_NOWARN_INT;
   }

   /**
    * Get the ExceptionSorterClassname value.
    * @return the ExceptionSorterClassname value.
    */
   public String getExceptionSorterClassName()
   {
      return exceptionSorterClassName;
   }

   /**
    * Set the ExceptionSorterClassname value.
    * @param exceptionSorterClassName The new ExceptionSorterClassName value.
    */
   public void setExceptionSorterClassName(String exceptionSorterClassName)
   {
      this.exceptionSorterClassName = exceptionSorterClassName;
   }

   /**
    * Get the valid connection checker class name
    *
    * @return the class name
    */
   public String getValidConnectionCheckerClassName()
   {
      return validConnectionCheckerClassName;
   }

   /**
    * Set the valid connection checker class name
    *
    * @param value the class name
    */
   public void setValidConnectionCheckerClassName(String value)
   {
      validConnectionCheckerClassName = value;
   }
 
   /**
    * Gets full set of connection properties, i.e. whatever is provided
    * in config plus "user" and "password" from subject/cri.
    *
    * <p>Note that the set is used to match connections to datasources as well
    * as to create new managed connections.
    *
    * <p>In fact, we have a problem here. Theoretically, there is a possible
    * name collision between config properties and "user"/"password".
    */
   protected Properties getConnectionProperties(Subject subject, ConnectionRequestInfo cri)
      throws ResourceException
   {
      if (cri != null && cri.getClass() != WrappedConnectionRequestInfo.class)
      {
         throw new JBossResourceException("Wrong kind of ConnectionRequestInfo: " + cri.getClass());
      } // end of if ()

      Properties props = new Properties();
      props.putAll(connectionProps);
      if (subject != null)
      {
         if (SubjectActions.addMatchingProperties(subject, props, this) == true)
         {
            return props;
         }
         throw new JBossResourceException("No matching credentials in Subject!");
      } // end of if ()
      WrappedConnectionRequestInfo lcri = (WrappedConnectionRequestInfo)cri;
      if (lcri != null)
      {
         props.setProperty("user", (lcri.getUserName() == null)? "": lcri.getUserName());
         props.setProperty("password", (lcri.getPassword() == null)? "": lcri.getPassword());
         return props;
      } // end of if ()
      if (userName != null)
      {
         props.setProperty("user", userName);
         props.setProperty("password", (password == null) ? "" : password);
      }
      return props;
   }

   boolean isExceptionFatal(SQLException e)
   {
      if (exceptionSorter != null)
         return exceptionSorter.isExceptionFatal(e);

      if (exceptionSorterClassName != null)
      {
         try
         {
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            Class clazz = cl.loadClass(exceptionSorterClassName);
            exceptionSorter = (ExceptionSorter)clazz.newInstance();
            return exceptionSorter.isExceptionFatal(e);
         }
         catch (Exception e2)
         {
            log.warn("exception trying to create exception sorter (disabling):", e2);
            exceptionSorter = new NullExceptionSorter();
         }
      }
      return false;
   }

   /**
    * Checks whether a connection is valid
    */
   SQLException isValidConnection(Connection c)
   {
      // Already got a checker
      if (connectionChecker != null)
         return connectionChecker.isValidConnection(c);

      // Class specified
      if (validConnectionCheckerClassName != null)
      {
         try
         {
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            Class clazz = cl.loadClass(validConnectionCheckerClassName);
            connectionChecker = (ValidConnectionChecker) clazz.newInstance();
            return connectionChecker.isValidConnection(c);
         }
         catch (Exception e)
         {
            log.warn("Exception trying to create connection checker (disabling):", e);
            connectionChecker = new NullValidConnectionChecker();
         }
      }

      // SQL statement specified
      if (checkValidConnectionSQL != null)
      {
         connectionChecker = new CheckValidConnectionSQL(checkValidConnectionSQL);
         return connectionChecker.isValidConnection(c);
      }

      // No Check
      return null;
   }

   static class SubjectActions implements PrivilegedAction
   {
      Subject subject;

      Properties props;

      ManagedConnectionFactory mcf;

      SubjectActions(Subject subject, Properties props, ManagedConnectionFactory mcf)
      {
         this.subject = subject;
         this.props = props;
         this.mcf = mcf;
      }

      public Object run()
      {
         Iterator i = subject.getPrivateCredentials().iterator();
         while (i.hasNext())
         {
            Object o = i.next();
            if (o instanceof PasswordCredential)
            {
               PasswordCredential cred = (PasswordCredential) o;
               if (cred.getManagedConnectionFactory().equals(mcf))
               {
                  props.setProperty("user", (cred.getUserName() == null) ? "" : cred.getUserName());
                  if( cred.getPassword() != null )
                     props.setProperty("password", new String(cred.getPassword()));
                  return Boolean.TRUE;
               }
            }
         }
         return Boolean.FALSE;
      }

      static boolean addMatchingProperties(Subject subject, Properties props, ManagedConnectionFactory mcf)
      {
         SubjectActions action = new SubjectActions(subject, props, mcf);
         Boolean matched = (Boolean) AccessController.doPrivileged(action);
         return matched.booleanValue();
      }
   }
}// BaseWrapperManagedConnectionFactory