/*
* 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);
}
}
}