/*
 * $Id$
 *
 * Copyright 2006, The jCoderZ.org Project. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *    * Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *    * Redistributions in binary form must reproduce the above
 *      copyright notice, this list of conditions and the following
 *      disclaimer in the documentation and/or other materials
 *      provided with the distribution.
 *    * Neither the name of the jCoderZ.org Project nor the names of
 *      its contributors may be used to endorse or promote products
 *      derived from this software without specific prior written
 *      permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.jcoderz.commons.util;

import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.math.BigDecimal;
import java.net.URL;
import java.sql.Array;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.Date;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.ParameterMetaData;
import java.sql.PreparedStatement;
import java.sql.Ref;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Statement;
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Utility class for some DB routines.
 *
 * @author Albrecht Messner
 */
public final class DbUtil
{
   /** class name for use in logger */
   private static final String CLASSNAME = DbUtil.class.getName();

   /** logging facility */
   private static final Logger logger = Logger.getLogger(CLASSNAME);

   /** The oracle driver class for Oracle 8.x and 9.x */
   private static final String ORACLE_DRIVER_CLASSNAME
         = "oracle.jdbc.OracleDriver";
   /**
    * private constructor to avoid instantiation.
    */
   private DbUtil ()
   {
      // utility class -- no instances are allowed.
   }

   /**
    * Close a statement according to resource handling idiom.
    * @param stmt the statement to be closed
    */
   public static void close (Statement stmt)
   {
      if (stmt != null)
      {
         try
         {
            stmt.close();
         }
         catch (SQLException x)
         {
            logger.log(Level.FINE, "Error during resource cleanup: "
                  + "java.sql.Statement.close()", x);
         }
      }
   }

   /**
    * Close a DB connection according to resource handling idiom.
    * @param con the connection to be closed
    */
   public static void close (Connection con)
   {
      if (con != null)
      {
         try
         {
            con.close();
         }
         catch (SQLException x)
         {
            logger.log(Level.FINE, "Error during resource cleanup: "
                  + "java.sql.Connection.close()", x);
         }
      }
   }

   /**
    * Close a result set according to resource handling idiom.
    * @param resultSet the result set to close
    */
   public static void close (ResultSet resultSet)
   {
      if (resultSet != null)
      {
         try
         {
            resultSet.close();
         }
         catch (SQLException x)
         {
            logger.log(Level.FINE, "Error during resource cleanup: "
                  + "java.sql.ResultSet.close()", x);
         }
      }
   }

   /**
    * Sets a parameter in a PreparedStatement to either a value or null.
    *
    * @param pstmt the prepared statement
    * @param paramIndex the parameter index of the parameter to be set
    * @param value the value to which the parameter must be set
    * @throws SQLException if a database access error occurs.
    */
   public static void setVarcharOrNull (
         PreparedStatement pstmt, int paramIndex, String value)
         throws SQLException
   {
      setParameterOrNull(pstmt, paramIndex, value, Types.VARCHAR);
   }

   /**
    * Sets a parameter in a PreparedStatement to either a value or null.
    *
    * @param pstmt the prepared statement
    * @param paramIndex the parameter index of the parameter to be set
    * @param value the value to which the parameter must be set
    * @throws SQLException if a database access error occurs.
    */
   public static void setCharOrNull (
         PreparedStatement pstmt, int paramIndex, String value)
         throws SQLException
   {
      setParameterOrNull(pstmt, paramIndex, value, Types.CHAR);
   }

   /**
    * Sets a parameter in a PreparedStatement to either a value or null.
    *
    * @param pstmt the prepared statement
    * @param paramIndex the parameter index of the parameter to be set
    * @param value the value to which the parameter must be set
    * @throws SQLException if a database access error occurs.
    */
   public static void setClobOrNull (
         PreparedStatement pstmt, int paramIndex, String value)
         throws SQLException
   {
      setParameterOrNull(pstmt, paramIndex, value, Types.CLOB);
   }

   /**
    * Sets a parameter in a PreparedStatement to either a value or null.
    *
    * @param pstmt the prepared statement
    * @param paramIndex the parameter index of the parameter to be set
    * @param value the value to which the parameter must be set
    * @throws SQLException if a database access error occurs.
    */
   public static void setTimestampOrNull (
         PreparedStatement pstmt, int paramIndex, Timestamp value)
         throws SQLException
   {
      setParameterOrNull(pstmt, paramIndex, value, Types.TIMESTAMP);
   }

   /**
    * Sets a parameter in a PreparedStatement to either a value or null.
    *
    * @param pstmt the prepared statement
    * @param paramIndex the parameter index of the parameter to be set
    * @param value the value to which the parameter must be set
    * @throws SQLException if a database access error occurs.
    */
   public static void setIntegerOrNull (
         PreparedStatement pstmt, int paramIndex, Integer value)
         throws SQLException
   {
      setParameterOrNull(pstmt, paramIndex, value, Types.INTEGER);
   }

   /**
    * Sets a parameter in a PreparedStatement to either a value or null.
    *
    * @param pstmt the prepared statement
    * @param paramIndex the parameter index of the parameter to be set
    * @param value the value to which the parameter must be set
    * @throws SQLException if a database access error occurs.
    */
   public static void setLongOrNull (
         PreparedStatement pstmt, int paramIndex, Long value)
         throws SQLException
   {
      // Sorry, can't delegate this one to setParameterOrNull because
      // it's got a different type than setIntegerOrNull, but the same
      // SQL type code.
      if (value != null)
      {
         pstmt.setLong(paramIndex, value.longValue());
      }
      else
      {
         pstmt.setNull(paramIndex, Types.INTEGER);
      }
   }

   /**
    * Registers the oracle JDBC driver.
    *
    * NOTE: only use either in testcases that need a direct JDBC connection
    * or in stand-alone applications, never in the Application Server.
    */
   public static void registerOracleDriver ()
   {
      try
      {
         final Class driverClass
               = Class.forName(ORACLE_DRIVER_CLASSNAME);
         final Driver driverObject = (Driver) driverClass.newInstance();
         DriverManager.registerDriver(driverObject);
      }
      catch (Exception e)
      {
         final RuntimeException rte = new RuntimeException(
               "Failed to register oracle driver "
               + ORACLE_DRIVER_CLASSNAME, e);
         throw rte;
      }
   }

   /**
    * This method wraps the {@link PreparedStatement} given as argument
    * into a delegate object that calls the <code>executeBatch</code>
    * method every <code>maxBatchSize</code>th time a batch is added to
    * the statement with the <code>addBatch</code> method.
    *
    * @param pstmt the prepared statement to wrap
    * @param maxBatchSize the maximum batch size at which an executeBatch
    *       should be called
    * @return a PreparedStatement implementation that does automatic
    *       batch updates
    */
   public static PreparedStatement getLimitedBatchSizePreparedStatement (
         PreparedStatement pstmt, int maxBatchSize)
   {
      return null; // new LimitedBatchSizePreparedStatement(pstmt, maxBatchSize);
   }

   private static void setParameterOrNull (
         PreparedStatement pstmt, int paramIndex, Object value, int type)
         throws SQLException
   {
      if (value != null)
      {
         switch (type)
         {
            case Types.VARCHAR:
            case Types.CHAR:
               pstmt.setString(paramIndex, (String) value);
               break;
            case Types.CLOB:
               final String strVal = (String) value;
               pstmt.setCharacterStream(
                     paramIndex, new StringReader(strVal), strVal.length());
               break;
            case Types.TIMESTAMP:
               final Timestamp tstampVal = (Timestamp) value;
               pstmt.setTimestamp(paramIndex, tstampVal);
               break;
            case Types.INTEGER:
               final Integer intVal = (Integer) value;
               pstmt.setInt(paramIndex, intVal.intValue());
               break;
            default:
               throw new RuntimeException("Unexpected SQL type "
                     + type + " encountered");
         }
      }
      else
      {
         pstmt.setNull(paramIndex, type);
      }
   }

   private static final class LimitedBatchSizePreparedStatement
// FIXME: does not work with JDK1.6.0     implements PreparedStatement
   {
      private final PreparedStatement mPreparedStatement;
      private final int mMaxBatchSize;
      private final List mResultList = new ArrayList();

      private int mBatchCount = 0;

      private LimitedBatchSizePreparedStatement
            (PreparedStatement pstmt, int maxBatchSize)
      {
         mPreparedStatement = pstmt;
         mMaxBatchSize = maxBatchSize;
      }

      /** {@inheritDoc} */
      public void addBatch ()
            throws SQLException
      {
         if (mBatchCount >= mMaxBatchSize)
         {
            internalExecuteBatch();
         }
         mPreparedStatement.addBatch();
         mBatchCount++;
      }

      /** {@inheritDoc} */
      public void addBatch (String sql)
            throws SQLException
      {
         if (mBatchCount >= mMaxBatchSize)
         {
            internalExecuteBatch();
         }
         mPreparedStatement.addBatch(sql);
         mBatchCount++;
      }

      /** {@inheritDoc} */
      public int[] executeBatch ()
            throws SQLException
      {
         internalExecuteBatch();

         int totalBatchSize = 0;
         for (final Iterator it = mResultList.iterator(); it.hasNext(); )
         {
            final int[] updateResult = (int[]) it.next();
            totalBatchSize += updateResult.length;
         }

         final int[] result = new int[totalBatchSize];
         int offset = 0;
         for (final Iterator it = mResultList.iterator(); it.hasNext(); )
         {
            final int[] updateResult = (int[]) it.next();
            System.arraycopy(
                  updateResult, 0, result, offset, updateResult.length);
            offset += updateResult.length;
         }
         mResultList.clear();
         return result;
      }

      /** {@inheritDoc} */
      public void clearBatch ()
            throws SQLException
      {
         mPreparedStatement.clearBatch();
         mBatchCount = 0;
         mResultList.clear();
      }

      /** {@inheritDoc} */
      public String toString ()
      {
         return "[LimitedBatchSizePreparedStatement: mBatchSize=" 
               + mMaxBatchSize + ", mPreparedStatement=" 
               + mPreparedStatement + "]";
      }

      private void internalExecuteBatch ()
            throws SQLException
      {
         mBatchCount = 0;
         final int[] result = mPreparedStatement.executeBatch();
         mResultList.add(result.clone());
      }

      // ==============================================================
      // only delegates below this point
      // ==============================================================

      /** {@inheritDoc} */
      public void cancel ()
            throws SQLException
      {
         mPreparedStatement.cancel();
      }

      /** {@inheritDoc} */
      public void clearParameters ()
            throws SQLException
      {
         mPreparedStatement.clearParameters();
      }

      /** {@inheritDoc} */
      public void clearWarnings ()
            throws SQLException
      {
         mPreparedStatement.clearWarnings();
      }

      /** {@inheritDoc} */
      public void close ()
            throws SQLException
      {
         mPreparedStatement.close();
      }

      /** {@inheritDoc} */
      public boolean execute ()
            throws SQLException
      {
         return mPreparedStatement.execute();
      }

      /** {@inheritDoc} */
      public boolean execute (String sql)
            throws SQLException
      {
         return mPreparedStatement.execute(sql);
      }

      /** {@inheritDoc} */
      public boolean execute (String sql, int autoGeneratedKeys)
            throws SQLException
      {
         return mPreparedStatement.execute(sql, autoGeneratedKeys);
      }

      /** {@inheritDoc} */
      public boolean execute (String sql, int[] columnIndexes)
            throws SQLException
      {
         return mPreparedStatement.execute(sql, columnIndexes);
      }

      /** {@inheritDoc} */
      public boolean execute (String sql, String[] columnNames)
            throws SQLException
      {
         return mPreparedStatement.execute(sql, columnNames);
      }

      /** {@inheritDoc} */
      public ResultSet executeQuery ()
            throws SQLException
      {
         return mPreparedStatement.executeQuery();
      }

      /** {@inheritDoc} */
      public ResultSet executeQuery (String sql)
            throws SQLException
      {
         return mPreparedStatement.executeQuery(sql);
      }

      /** {@inheritDoc} */
      public int executeUpdate ()
            throws SQLException
      {
         return mPreparedStatement.executeUpdate();
      }

      /** {@inheritDoc} */
      public int executeUpdate (String sql)
            throws SQLException
      {
         return mPreparedStatement.executeUpdate(sql);
      }

      /** {@inheritDoc} */
      public int executeUpdate (String sql, int autoGeneratedKeys)
            throws SQLException
      {
         return mPreparedStatement.executeUpdate(sql, autoGeneratedKeys);
      }

      /** {@inheritDoc} */
      public int executeUpdate (String sql, int[] columnIndexes)
            throws SQLException
      {
         return mPreparedStatement.executeUpdate(sql, columnIndexes);
      }

      /** {@inheritDoc} */
      public int executeUpdate (String sql, String[] columnNames)
            throws SQLException
      {
         return mPreparedStatement.executeUpdate(sql, columnNames);
      }

      /** {@inheritDoc} */
      public Connection getConnection ()
            throws SQLException
      {
         return mPreparedStatement.getConnection();
      }

      /** {@inheritDoc} */
      public int getFetchDirection ()
            throws SQLException
      {
         return mPreparedStatement.getFetchDirection();
      }

      /** {@inheritDoc} */
      public int getFetchSize ()
            throws SQLException
      {
         return mPreparedStatement.getFetchSize();
      }

      /** {@inheritDoc} */
      public ResultSet getGeneratedKeys ()
            throws SQLException
      {
         return mPreparedStatement.getGeneratedKeys();
      }

      /** {@inheritDoc} */
      public int getMaxFieldSize ()
            throws SQLException
      {
         return mPreparedStatement.getMaxFieldSize();
      }

      /** {@inheritDoc} */
      public int getMaxRows ()
            throws SQLException
      {
         return mPreparedStatement.getMaxRows();
      }

      /** {@inheritDoc} */
      public ResultSetMetaData getMetaData ()
            throws SQLException
      {
         return mPreparedStatement.getMetaData();
      }

      /** {@inheritDoc} */
      public boolean getMoreResults ()
            throws SQLException
      {
         return mPreparedStatement.getMoreResults();
      }

      /** {@inheritDoc} */
      public boolean getMoreResults (int current)
            throws SQLException
      {
         return mPreparedStatement.getMoreResults(current);
      }

      /** {@inheritDoc} */
      public ParameterMetaData getParameterMetaData ()
            throws SQLException
      {
         return mPreparedStatement.getParameterMetaData();
      }

      /** {@inheritDoc} */
      public int getQueryTimeout ()
            throws SQLException
      {
         return mPreparedStatement.getQueryTimeout();
      }

      /** {@inheritDoc} */
      public ResultSet getResultSet ()
            throws SQLException
      {
         return mPreparedStatement.getResultSet();
      }

      /** {@inheritDoc} */
      public int getResultSetConcurrency ()
            throws SQLException
      {
         return mPreparedStatement.getResultSetConcurrency();
      }

      /** {@inheritDoc} */
      public int getResultSetHoldability ()
            throws SQLException
      {
         return mPreparedStatement.getResultSetHoldability();
      }

      /** {@inheritDoc} */
      public int getResultSetType ()
            throws SQLException
      {
         return mPreparedStatement.getResultSetType();
      }

      /** {@inheritDoc} */
      public int getUpdateCount ()
            throws SQLException
      {
         return mPreparedStatement.getUpdateCount();
      }

      /** {@inheritDoc} */
      public SQLWarning getWarnings ()
            throws SQLException
      {
         return mPreparedStatement.getWarnings();
      }

      /** {@inheritDoc} */
      public void setArray (int i, Array x)
            throws SQLException
      {
         mPreparedStatement.setArray(i, x);
      }

      /** {@inheritDoc} */
      public void setAsciiStream (int parameterIndex, InputStream x, int length)
            throws SQLException
      {
         mPreparedStatement.setAsciiStream(parameterIndex, x, length);
      }

      /** {@inheritDoc} */
      public void setBigDecimal (int parameterIndex, BigDecimal x)
            throws SQLException
      {
         mPreparedStatement.setBigDecimal(parameterIndex, x);
      }

      /** {@inheritDoc} */
      public void setBinaryStream (
            int parameterIndex, InputStream x, int length)
            throws SQLException
      {
         mPreparedStatement.setBinaryStream(parameterIndex, x, length);
      }

      /** {@inheritDoc} */
      public void setBlob (int i, Blob x)
            throws SQLException
      {
         mPreparedStatement.setBlob(i, x);
      }

      /** {@inheritDoc} */
      public void setBoolean (int parameterIndex, boolean x)
            throws SQLException
      {
         mPreparedStatement.setBoolean(parameterIndex, x);
      }

      /** {@inheritDoc} */
      public void setByte (int parameterIndex, byte x)
            throws SQLException
      {
         mPreparedStatement.setByte(parameterIndex, x);
      }

      /** {@inheritDoc} */
      public void setBytes (int parameterIndex, byte[] x)
            throws SQLException
      {
         mPreparedStatement.setBytes(parameterIndex, x);
      }

      /** {@inheritDoc} */
      public void setCharacterStream (int parameterIndex, Reader reader,
            int length)
            throws SQLException
      {
         mPreparedStatement.setCharacterStream(parameterIndex, reader, length);
      }

      /** {@inheritDoc} */
      public void setClob (int i, Clob x)
            throws SQLException
      {
         mPreparedStatement.setClob(i, x);
      }

      /** {@inheritDoc} */
      public void setCursorName (String name)
            throws SQLException
      {
         mPreparedStatement.setCursorName(name);
      }

      /** {@inheritDoc} */
      public void setDate (int parameterIndex, Date x)
            throws SQLException
      {
         mPreparedStatement.setDate(parameterIndex, x);
      }

      /** {@inheritDoc} */
      public void setDate (int parameterIndex, Date x, Calendar cal)
            throws SQLException
      {
         mPreparedStatement.setDate(parameterIndex, x, cal);
      }

      /** {@inheritDoc} */
      public void setDouble (int parameterIndex, double x)
            throws SQLException
      {
         mPreparedStatement.setDouble(parameterIndex, x);
      }

      /** {@inheritDoc} */
      public void setEscapeProcessing (boolean enable)
            throws SQLException
      {
         mPreparedStatement.setEscapeProcessing(enable);
      }

      /** {@inheritDoc} */
      public void setFetchDirection (int direction)
            throws SQLException
      {
         mPreparedStatement.setFetchDirection(direction);
      }

      /** {@inheritDoc} */
      public void setFetchSize (int rows)
            throws SQLException
      {
         mPreparedStatement.setFetchSize(rows);
      }

      /** {@inheritDoc} */
      public void setFloat (int parameterIndex, float x)
            throws SQLException
      {
         mPreparedStatement.setFloat(parameterIndex, x);
      }

      /** {@inheritDoc} */
      public void setInt (int parameterIndex, int x)
            throws SQLException
      {
         mPreparedStatement.setInt(parameterIndex, x);
      }

      /** {@inheritDoc} */
      public void setLong (int parameterIndex, long x)
            throws SQLException
      {
         mPreparedStatement.setLong(parameterIndex, x);
      }

      /** {@inheritDoc} */
      public void setMaxFieldSize (int max)
            throws SQLException
      {
         mPreparedStatement.setMaxFieldSize(max);
      }

      /** {@inheritDoc} */
      public void setMaxRows (int max)
            throws SQLException
      {
         mPreparedStatement.setMaxRows(max);
      }

      /** {@inheritDoc} */
      public void setNull (int parameterIndex, int sqlType)
            throws SQLException
      {
         mPreparedStatement.setNull(parameterIndex, sqlType);
      }

      /** {@inheritDoc} */
      public void setNull (int paramIndex, int sqlType, String typeName)
            throws SQLException
      {
         mPreparedStatement.setNull(paramIndex, sqlType, typeName);
      }

      /** {@inheritDoc} */
      public void setObject (int parameterIndex, Object x)
            throws SQLException
      {
         mPreparedStatement.setObject(parameterIndex, x);
      }

      /** {@inheritDoc} */
      public void setObject (int parameterIndex, Object x, int targetSqlType)
            throws SQLException
      {
         mPreparedStatement.setObject(parameterIndex, x, targetSqlType);
      }

      /** {@inheritDoc} */
      public void setObject (int parameterIndex, Object x, int targetSqlType,
            int scale)
            throws SQLException
      {
         mPreparedStatement.setObject(parameterIndex, x, targetSqlType, scale);
      }

      /** {@inheritDoc} */
      public void setQueryTimeout (int seconds)
            throws SQLException
      {
         mPreparedStatement.setQueryTimeout(seconds);
      }

      /** {@inheritDoc} */
      public void setRef (int i, Ref x)
            throws SQLException
      {
         mPreparedStatement.setRef(i, x);
      }

      /** {@inheritDoc} */
      public void setShort (int parameterIndex, short x)
            throws SQLException
      {
         mPreparedStatement.setShort(parameterIndex, x);
      }

      /** {@inheritDoc} */
      public void setString (int parameterIndex, String x)
            throws SQLException
      {
         mPreparedStatement.setString(parameterIndex, x);
      }

      /** {@inheritDoc} */
      public void setTime (int parameterIndex, Time x)
            throws SQLException
      {
         mPreparedStatement.setTime(parameterIndex, x);
      }

      /** {@inheritDoc} */
      public void setTime (int parameterIndex, Time x, Calendar cal)
            throws SQLException
      {
         mPreparedStatement.setTime(parameterIndex, x, cal);
      }

      /** {@inheritDoc} */
      public void setTimestamp (int parameterIndex, Timestamp x)
            throws SQLException
      {
         mPreparedStatement.setTimestamp(parameterIndex, x);
      }

      /** {@inheritDoc} */
      public void setTimestamp (int parameterIndex, Timestamp x, Calendar cal)
            throws SQLException
      {
         mPreparedStatement.setTimestamp(parameterIndex, x, cal);
      }

      /** {@inheritDoc} */
      public void setUnicodeStream (int parameterIndex, InputStream x,
            int length)
            throws SQLException
      {
         mPreparedStatement.setUnicodeStream(parameterIndex, x, length);
      }

      /** {@inheritDoc} */
      public void setURL (int parameterIndex, URL x)
            throws SQLException
      {
         mPreparedStatement.setURL(parameterIndex, x);
      }
   }
}

