/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2008, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.ejb.plugins.cmp.jdbc;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Reader;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;

import java.rmi.MarshalledObject;
import java.rmi.RemoteException;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import java.sql.SQLException;
import java.sql.Types;
import java.sql.CallableStatement;

import java.util.Map;
import java.util.HashMap;
import java.math.BigDecimal;

import javax.ejb.EJBObject;
import javax.ejb.Handle;

import org.jboss.invocation.MarshalledValue;
import org.jboss.logging.Logger;

/**
 * JDBCUtil takes care of some of the more anoying JDBC tasks.
 * It hanles safe closing of jdbc resources, setting statement
 * parameters and loading query results.
 *
 * @author <a href="mailto:dain@daingroup.com">Dain Sundstrom</a>
 * @author <a href="mailto:alex@jboss.org">Alex Loubyansky</a>
 * @author Steve Coy
 * @version $Revision: 81030 $
 */
public final class JDBCUtil
{
   private static final Logger log = Logger.getLogger(JDBCUtil.class.getName());

   public static void safeClose(Connection con)
   {
      if(con != null)
      {
         try
         {
            con.close();
         }
         catch(Exception e)
         {
            log.error(SQL_ERROR, e);
         }
      }
   }

   public static void safeClose(ResultSet rs)
   {
      if(rs != null)
      {
         try
         {
            rs.close();
         }
         catch(Exception e)
         {
            log.error(SQL_ERROR, e);
         }
      }
   }

   public static void safeClose(Statement statement)
   {
      if(statement != null)
      {
         try
         {
            statement.close();
         }
         catch(Exception e)
         {
            log.error(SQL_ERROR, e);
         }
      }
   }

   public static void safeClose(InputStream in)
   {
      if(in != null)
      {
         try
         {
            in.close();
         }
         catch(Exception e)
         {
            log.error(SQL_ERROR, e);
         }
      }
   }

   public static void safeClose(OutputStream out)
   {
      if(out != null)
      {
         try
         {
            out.close();
         }
         catch(Exception e)
         {
            log.error(SQL_ERROR, e);
         }
      }
   }

   public static void safeClose(Reader reader)
   {
      if(reader != null)
      {
         try
         {
            reader.close();
         }
         catch(Exception e)
         {
            log.error(SQL_ERROR, e);
         }
      }
   }

   /**
    * Coerces the input value into the correct type for the specified
    * jdbcType.
    *
    * @param jdbcType the jdbc type to which the value will be assigned
    * @param value the value to coerce
    * @return the corrected object
    */
   public static Object coerceToSQLType(int jdbcType, Object value)
   {
      if(value.getClass() == java.util.Date.class)
      {
         if(jdbcType == Types.DATE)
         {
            return new java.sql.Date(((java.util.Date)value).getTime());
         }
         else if(jdbcType == Types.TIME)
         {
            return new java.sql.Time(((java.util.Date)value).getTime());
         }
         else if(jdbcType == Types.TIMESTAMP)
         {
            return new java.sql.Timestamp(((java.util.Date)value).getTime());
         }
      }
      else if(value.getClass() == Character.class && jdbcType == Types.VARCHAR)
      {
         value = value.toString();
      }
      return value;
   }

   /**
    * Coverts the value into a byte array.
    * @param value the value to convert into a byte array
    * @return the byte representation of the value
    * @throws SQLException if a problem occures in the conversion
    */
   public static byte[] convertObjectToByteArray(Object value)
      throws SQLException
   {
      // Do we already have a byte array?
      if(value instanceof byte[])
      {
         return (byte[])value;
      }

      ByteArrayOutputStream baos = null;
      ObjectOutputStream oos = null;
      try
      {
         // ejb-reference: store the handle
         if(value instanceof EJBObject)
         {
            value = ((EJBObject)value).getHandle();
         }

         // Marshall the object using MashalledValue to handle classloaders
         value = new MarshalledValue(value);

         // return the serialize the value
         baos = new ByteArrayOutputStream();
         oos = new ObjectOutputStream(baos);
         oos.writeObject(value);
         return baos.toByteArray();
      }
      catch(RemoteException e)
      {
         throw new SQLException("Cannot get Handle of EJBObject: " + e);
      }
      catch(IOException e)
      {
         throw new SQLException("Can't serialize binary object: " + e);
      }
      finally
      {
         safeClose(oos);
         safeClose(baos);
      }
   }

   /**
    * Coverts the input into an object.
    * @param input the bytes to convert
    * @return the object repsentation of the input stream
    * @throws SQLException if a problem occures in the conversion
    */
   public static Object convertToObject(byte[] input)
      throws SQLException
   {
      ByteArrayInputStream bais = new ByteArrayInputStream(input);
      try
      {
         return convertToObject(bais);
      }
      finally
      {
         safeClose(bais);
      }
   }


   /**
    * Coverts the input into an object.
    * @param input the bytes to convert
    * @return the object repsentation of the input stream
    * @throws SQLException if a problem occures in the conversion
    */
   public static Object convertToObject(InputStream input)
      throws SQLException
   {
      Object value = null;
      if(input != null)
      {
         ObjectInputStream ois = null;
         try
         {
            // deserialize result
            ois = new ObjectInputStream(input);
            value = ois.readObject();

            // de-marshall value if possible
            if(value instanceof MarshalledValue)
            {
               value = ((MarshalledValue)value).get();
            }
            else if(value instanceof MarshalledObject)
            {
               value = ((MarshalledObject)value).get();
            }

            // ejb-reference: get the object back from the handle
            if(value instanceof Handle)
            {
               value = ((Handle)value).getEJBObject();
            }

         }
         catch(RemoteException e)
         {
            throw new SQLException("Unable to load EJBObject back from Handle: " + e);
         }
         catch(IOException e)
         {
            throw new SQLException("Unable to load to deserialize result: " + e);
         }
         catch(ClassNotFoundException e)
         {
            throw new SQLException("Unable to load to deserialize result: " + e);
         }
         finally
         {
            // ois will close the input stream it wraps
            safeClose(ois);
         }
      }
      return value;
   }

   /**
    * Get the indicated result set parameter as a character stream and return
    * it's entire content as a String.
    *
    * @param rs the <code>ResultSet</code> from which a result is
    *    being retrieved.
    * @param index index of the result column.
    * @return a String containing the content of the result column
    */
   public static String getLongString(ResultSet rs, int index)
      throws SQLException
   {
      String value;
      Reader textData = rs.getCharacterStream(index);
      if(textData != null)
      {
         try
         {
            // Use a modest buffer here to reduce function call overhead
            // when reading extremely large data.
            StringBuffer textBuffer = new StringBuffer();
            char[] tmpBuffer = new char[1000];
            int charsRead;
            while((charsRead = textData.read(tmpBuffer)) != -1)
               textBuffer.append(tmpBuffer, 0, charsRead);
            value = textBuffer.toString();
         }
         catch(java.io.IOException ioException)
         {
            throw new SQLException(ioException.getMessage());
         }
         finally
         {
            safeClose(textData);
         }
      }
      else
         value = null;
      return value;
   }


   /**
    * Read the entire input stream provided and return its content as a byte
    * array.
    * The method closes the passed in input stream!
    *
    * @param input the <code>InputStream</code> from which a result is
    *    being retrieved.
    * @return a byte array containing the content of the input stream
    */
   public static byte[] getByteArray(InputStream input)
      throws SQLException
   {
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      try
      {
         // Use a modest buffer here to reduce function call overhead
         // when reading extremely large data.
         byte[] tmpBuffer = new byte[1000];
         int bytesRead;
         while((bytesRead = input.read(tmpBuffer)) != -1)
            baos.write(tmpBuffer, 0, bytesRead);
         return baos.toByteArray();
      }
      catch(java.io.IOException ioException)
      {
         throw new SQLException(ioException.getMessage());
      }
      finally
      {
         safeClose(baos);
         safeClose(input);
      }
   }

   // Inner

   public static JDBCResultSetReader getResultSetReader(int jdbcType, Class destination)
   {
      JDBCResultSetReader reader;
      switch(jdbcType)
      {
         case Types.CLOB:
            reader = JDBCResultSetReader.CLOB_READER;
            break;
         case Types.LONGVARCHAR:
            reader = JDBCResultSetReader.LONGVARCHAR_READER;
            break;
         case Types.BINARY:
            reader = JDBCResultSetReader.BINARY_READER;
            break;
         case Types.VARBINARY:
            reader = JDBCResultSetReader.VARBINARY_READER;
            break;
         case Types.BLOB:
            reader = JDBCResultSetReader.BLOB_READER;
            break;
         case Types.LONGVARBINARY:
            reader = JDBCResultSetReader.LONGVARBINARY_READER;
            break;
         case Types.JAVA_OBJECT:
            reader = JDBCResultSetReader.JAVA_OBJECT_READER;
            break;
         case Types.STRUCT:
            reader = JDBCResultSetReader.STRUCT_READER;
            break;
         case Types.ARRAY:
            reader = JDBCResultSetReader.ARRAY_READER;
            break;
         case Types.OTHER:
            reader = JDBCResultSetReader.OTHER_READER;
            break;
         default:
            {
               reader = getResultReaderByType(destination);
            }
      }
      return reader;
   }

   public static JDBCResultSetReader getResultReaderByType(Class destination)
   {
      JDBCResultSetReader reader;
      if(destination == java.util.Date.class)
      {
         reader = JDBCResultSetReader.JAVA_UTIL_DATE_READER;
      }
      else if(destination == java.sql.Date.class)
      {
         reader = JDBCResultSetReader.JAVA_SQL_DATE_READER;
      }
      else if(destination == java.sql.Time.class)
      {
         reader = JDBCResultSetReader.JAVA_SQL_TIME_READER;
      }
      else if(destination == java.sql.Timestamp.class)
      {
         reader = JDBCResultSetReader.JAVA_SQL_TIMESTAMP_READER;
      }
      else if(destination == BigDecimal.class)
      {
         reader = JDBCResultSetReader.BIGDECIMAL_READER;
      }
      else if(destination == java.sql.Ref.class)
      {
         reader = JDBCResultSetReader.REF_READER;
      }
      else if(destination == String.class)
      {
         reader = JDBCResultSetReader.STRING_READER;
      }
      else if(destination == Boolean.class || destination == Boolean.TYPE)
      {
         reader = JDBCResultSetReader.BOOLEAN_READER;
      }
      else if(destination == Byte.class || destination == Byte.TYPE)
      {
         reader = JDBCResultSetReader.BYTE_READER;
      }
      else if(destination == Character.class || destination == Character.TYPE)
      {
         reader = JDBCResultSetReader.CHARACTER_READER;
      }
      else if(destination == Short.class || destination == Short.TYPE)
      {
         reader = JDBCResultSetReader.SHORT_READER;
      }
      else if(destination == Integer.class || destination == Integer.TYPE)
      {
         reader = JDBCResultSetReader.INT_READER;
      }
      else if(destination == Long.class || destination == Long.TYPE)
      {
         reader = JDBCResultSetReader.LONG_READER;
      }
      else if(destination == Float.class || destination == Float.TYPE)
      {
         reader = JDBCResultSetReader.FLOAT_READER;
      }
      else if(destination == Double.class || destination == Double.TYPE)
      {
         reader = JDBCResultSetReader.DOUBLE_READER;
      }
      else
      {
         reader = JDBCResultSetReader.OBJECT_READER;
      }
      return reader;
   }

   public static JDBCParameterSetter getParameterSetter(int jdbcType, Class javaType)
   {
      JDBCParameterSetter ps;

      switch(jdbcType)
      {
         case Types.CLOB:
         case Types.LONGVARCHAR:
            ps = JDBCParameterSetter.CLOB;
            break;

         case Types.BINARY:
         case Types.VARBINARY:
            ps = JDBCParameterSetter.BINARY;
            break;

         case Types.BLOB:
         case Types.LONGVARBINARY:
            ps = JDBCParameterSetter.BLOB;
            break;

         case Types.DECIMAL:
         case Types.NUMERIC:
            ps = JDBCParameterSetter.NUMERIC;
            break;

         case Types.JAVA_OBJECT:
         case Types.OTHER:
         case Types.STRUCT:

         default:
            ps = JDBCParameterSetter.OBJECT;
            break;
      }

      return ps;
   }

   //
   // All the above is used only for Oracle specific entity create command
   // and could be refactored/optimized.
   //

   private static final Map jdbcTypeNames;
   private final static Map csTypes;

   /**
    * Gets the JDBC type name corresponding to the given type code.
    * Only used in debug log messages.
    *
    * @param jdbcType the integer JDBC type code.
    * @return the JDBC type name.
    * @see Types
    */
   public static String getJDBCTypeName(int jdbcType)
   {
      return (String)jdbcTypeNames.get(new Integer(jdbcType));
   }

   private static final String SQL_ERROR = "SQL error";
   private static final String GET_TIMESTAMP = "getTimestamp";
   private static final String GET_DATE = "getDate";
   private static final String GET_TIME = "getTime";
   private static final String GET_BIGDECIMAL = "getBigDecimal";
   private static final String GET_REF = "getRef";
   private static final String GET_STRING = "getString";
   private static final String GET_BOOLEAN = "getBoolean";
   private static final String GET_BYTE = "getByte";
   private static final String GET_SHORT = "getShort";
   private static final String GET_INT = "getInt";
   private static final String GET_LONG = "getLong";
   private static final String GET_FLOAT = "getFloat";
   private static final String GET_DOUBLE = "getDouble";
   private static final String GET_BYTES = "getBytes";

   static
   {
      Class[] arg = new Class[]{Integer.TYPE};

      // Initialize the mapping between non-binary java result set
      // types and the method on CallableStatement that is used to retrieve
      // a value of the java type.
      csTypes = new HashMap();
      try
      {
         // java.util.Date
         csTypes.put(java.util.Date.class.getName(),
            CallableStatement.class.getMethod(GET_TIMESTAMP, arg));
         // java.sql.Date
         csTypes.put(java.sql.Date.class.getName(),
            CallableStatement.class.getMethod(GET_DATE, arg));
         // Time
         csTypes.put(java.sql.Time.class.getName(),
            CallableStatement.class.getMethod(GET_TIME, arg));
         // Timestamp
         csTypes.put(java.sql.Timestamp.class.getName(),
            CallableStatement.class.getMethod(GET_TIMESTAMP, arg));
         // BigDecimal
         csTypes.put(java.math.BigDecimal.class.getName(),
            CallableStatement.class.getMethod(GET_BIGDECIMAL, arg));
         // java.sql.Ref Does this really work?
         csTypes.put(java.sql.Ref.class.getName(),
            CallableStatement.class.getMethod(GET_REF, arg));
         // String
         csTypes.put(java.lang.String.class.getName(),
            CallableStatement.class.getMethod(GET_STRING, arg));
         // Boolean
         csTypes.put(java.lang.Boolean.class.getName(),
            CallableStatement.class.getMethod(GET_BOOLEAN, arg));
         // boolean
         csTypes.put(Boolean.TYPE.getName(),
            CallableStatement.class.getMethod(GET_BOOLEAN, arg));
         // Byte
         csTypes.put(java.lang.Byte.class.getName(),
            CallableStatement.class.getMethod(GET_BYTE, arg));
         // byte
         csTypes.put(Byte.TYPE.getName(),
            CallableStatement.class.getMethod(GET_BYTE, arg));
         // Character
         csTypes.put(java.lang.Character.class.getName(),
            CallableStatement.class.getMethod(GET_STRING, arg));
         // char
         csTypes.put(Character.TYPE.getName(),
            CallableStatement.class.getMethod(GET_STRING, arg));
         // Short
         csTypes.put(java.lang.Short.class.getName(),
            CallableStatement.class.getMethod(GET_SHORT, arg));
         // short
         csTypes.put(Short.TYPE.getName(),
            CallableStatement.class.getMethod(GET_SHORT, arg));
         // Integer
         csTypes.put(java.lang.Integer.class.getName(),
            CallableStatement.class.getMethod(GET_INT, arg));
         // int
         csTypes.put(Integer.TYPE.getName(),
            CallableStatement.class.getMethod(GET_INT, arg));
         // Long
         csTypes.put(java.lang.Long.class.getName(),
            CallableStatement.class.getMethod(GET_LONG, arg));
         // long
         csTypes.put(Long.TYPE.getName(),
            CallableStatement.class.getMethod(GET_LONG, arg));
         // Float
         csTypes.put(java.lang.Float.class.getName(),
            CallableStatement.class.getMethod(GET_FLOAT, arg));
         // float
         csTypes.put(Float.TYPE.getName(),
            CallableStatement.class.getMethod(GET_FLOAT, arg));
         // Double
         csTypes.put(java.lang.Double.class.getName(),
            CallableStatement.class.getMethod(GET_DOUBLE, arg));
         // double
         csTypes.put(Double.TYPE.getName(),
            CallableStatement.class.getMethod(GET_DOUBLE, arg));
         // byte[]   (scoy: I expect that this will no longer be invoked)
         csTypes.put("[B",
            CallableStatement.class.getMethod(GET_BYTES, arg));
      }
      catch(NoSuchMethodException e)
      {
         // Should never happen
         log.error(SQL_ERROR, e);
      }

      // Initializes the map between jdbcType (int) and the name of the type.
      // This map is used to print meaningful debug and error messages.
      jdbcTypeNames = new HashMap();
      Field[] fields = Types.class.getFields();
      for(int i = 0; i < fields.length; i++)
      {
         try
         {
            jdbcTypeNames.put(fields[i].get(null), fields[i].getName());
         }
         catch(IllegalAccessException e)
         {
            // Should never happen
            log.error(SQL_ERROR, e);
         }
      }
   }

   /**
    * Used for all retrieval of parameters from <code>CallableStatement</code>s.
    * Implements tracing, and allows some tweaking of returned types.
    *
    * @param log where to log to
    * @param cs the <code>CallableStatement</code> from which an out parameter is being retrieved
    * @param index index of the result column.
    * @param jdbcType a {@link java.sql.Types} constant used to determine the
    *                 most appropriate way to extract the data from rs.
    * @param destination The class of the variable this is going into
    * @return the value
    */
   public static Object getParameter(Logger log, CallableStatement cs, int index, int jdbcType, Class destination) throws SQLException
   {
      Object value = null;
      switch(jdbcType)
      {
         //
         // Large types
         //
         case Types.CLOB:
         case Types.LONGVARCHAR:
         case Types.BLOB:
         case Types.LONGVARBINARY:
            throw new UnsupportedOperationException();

            //
            // Small binary types
            //
         case Types.BINARY:
         case Types.VARBINARY:
            {
               byte[] bytes = cs.getBytes(index);
               if(!cs.wasNull())
               {
                  if(destination == byte[].class)
                     value = bytes;
                  else
                     value = convertToObject(bytes);
               }
               if(log.isTraceEnabled())
               {
                  log.trace("Get result: index=" + index +
                     ", javaType=" + destination.getName() +
                     ", Binary, value=" + value);
               }
            }
            break;

            //
            // Specialist types that the
            // driver should handle
            //
         case Types.JAVA_OBJECT:
         case Types.STRUCT:
         case Types.ARRAY:
         case Types.OTHER:
            {
               value = cs.getObject(index);
               if(log.isTraceEnabled())
               {
                  log.trace("Get result: index=" + index +
                     ", javaType=" + destination.getName() +
                     ", Object, value=" + value);
               }
            }
            break;

            //
            // Non-binary types
            //
         default:
            Method method = (Method)csTypes.get(destination.getName());
            if(method != null)
            {
               try
               {
                  value = method.invoke(cs, new Object[]{new Integer(index)});
                  if(cs.wasNull())
                  {
                     value = null;
                  }

                  if(log.isTraceEnabled())
                  {
                     log.trace("Get result: index=" + index +
                        ", javaType=" + destination.getName() +
                        ", Simple, value=" + value);
                  }
               }
               catch(IllegalAccessException e)
               {
                  // Whatever, I guess non-binary will not work for this field.
               }
               catch(InvocationTargetException e)
               {
                  // Whatever, I guess non-binary will not work for this field.
               }
            }
            else
            {
               value = cs.getObject(index);
               if(log.isTraceEnabled())
               {
                  log.trace("Get result: index=" + index +
                     ", javaType=" + destination.getName() +
                     ", Object, value=" + value);
               }
            }
      }
      return coerceToJavaType(value, destination);
   }

   private static Object coerceToJavaType(
      Object value,
      Class destination) throws SQLException
   {
      try
      {
         //
         // null
         //
         if(value == null)
         {
            return null;
         }

         //
         // java.rmi.MarshalledObject
         //
         // get unmarshalled value
         if(value instanceof MarshalledObject && !destination.equals(MarshalledObject.class))
         {
            value = ((MarshalledObject)value).get();
         }

         //
         // javax.ejb.Handle
         //
         // get the object back from the handle
         if(value instanceof Handle)
         {
            value = ((Handle)value).getEJBObject();
         }

         //
         // Primitive wrapper classes
         //
         // We have a primitive wrapper and we want a real primitive
         // just return the wrapper and the vm will convert it at the proxy
         if(destination.isPrimitive())
         {
            if(value == null)
               throw new IllegalStateException("Loaded NULL value for a field of a primitive type.");
            if((destination.equals(Byte.TYPE) && value instanceof Byte) ||
               (destination.equals(Short.TYPE) && value instanceof Short) ||
               (destination.equals(Character.TYPE) && value instanceof Character) ||
               (destination.equals(Boolean.TYPE) && value instanceof Boolean) ||
               (destination.equals(Integer.TYPE) && value instanceof Integer) ||
               (destination.equals(Long.TYPE) && value instanceof Long) ||
               (destination.equals(Float.TYPE) && value instanceof Float) ||
               (destination.equals(Double.TYPE) && value instanceof Double)
            )
            {
               return value;
            }
         }

         //
         // java.util.Date
         //
         // make new copy as sub types have problems in comparions
         if(destination == java.util.Date.class && value instanceof java.util.Date)
         {
            // handle timestamp special becauses it hoses the milisecond values
            if(value instanceof java.sql.Timestamp)
            {
               java.sql.Timestamp ts = (java.sql.Timestamp)value;

               // Timestamp returns whole seconds from getTime and partial
               // seconds are retrieved from getNanos()
               // Adrian Brock: Not in 1.4 it doesn't
               long temp = ts.getTime();
               if(temp % 1000 == 0)
                  temp += ts.getNanos() / 1000000;
               return new java.util.Date(temp);
            }
            else
            {
               return new java.util.Date(((java.util.Date)value).getTime());
            }
         }

         //
         // java.sql.Time
         //
         // make a new copy object; you never know what a driver will return
         if(destination == java.sql.Time.class && value instanceof java.sql.Time)
         {
            return new java.sql.Time(((java.sql.Time)value).getTime());
         }

         //
         // java.sql.Date
         //
         // make a new copy object; you never know what a driver will return
         if(destination == java.sql.Date.class && value instanceof java.sql.Date)
         {
            return new java.sql.Date(((java.sql.Date)value).getTime());
         }

         //
         // java.sql.Timestamp
         //
         // make a new copy object; you never know what a driver will return
         if(destination == java.sql.Timestamp.class && value instanceof java.sql.Timestamp)
         {
            // make a new Timestamp object; you never know
            // what a driver will return
            java.sql.Timestamp orignal = (java.sql.Timestamp)value;
            java.sql.Timestamp copy = new java.sql.Timestamp(orignal.getTime());
            copy.setNanos(orignal.getNanos());
            return copy;
         }

         //
         // java.lang.String --> java.lang.Character or char
         //
         // just grab first character
         if(value instanceof String && (destination == Character.class || destination == Character.TYPE))
         {
            return new Character(((String)value).charAt(0));
         }

         // Did we get the desired result?
         if(destination.isAssignableFrom(value.getClass()))
         {
            return value;
         }

         if(destination == java.math.BigInteger.class && value.getClass() == java.math.BigDecimal.class)
         {
            return ((java.math.BigDecimal)value).toBigInteger();
         }

         // oops got the wrong type - nothing we can do
         throw new SQLException("Got a " + value.getClass().getName() + "[cl=" +
            System.identityHashCode(value.getClass().getClassLoader()) +
            ", value=" + value + "] while looking for a " +
            destination.getName() + "[cl=" +
            System.identityHashCode(destination) + "]");
      }
      catch(RemoteException e)
      {
         throw new SQLException("Unable to load EJBObject back from Handle: "
            + e);
      }
      catch(IOException e)
      {
         throw new SQLException("Unable to load to deserialize result: " + e);
      }
      catch(ClassNotFoundException e)
      {
         throw new SQLException("Unable to load to deserialize result: " + e);
      }
   }
}
