ResultSetTableMetaData.java

  1. /*
  2.  *
  3.  * The DbUnit Database Testing Framework
  4.  * Copyright (C)2002-2008, DbUnit.org
  5.  *
  6.  * This library is free software; you can redistribute it and/or
  7.  * modify it under the terms of the GNU Lesser General Public
  8.  * License as published by the Free Software Foundation; either
  9.  * version 2.1 of the License, or (at your option) any later version.
  10.  *
  11.  * This library is distributed in the hope that it will be useful,
  12.  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13.  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  14.  * Lesser General Public License for more details.
  15.  *
  16.  * You should have received a copy of the GNU Lesser General Public
  17.  * License along with this library; if not, write to the Free Software
  18.  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  19.  *
  20.  */
  21. package org.dbunit.database;

  22. import java.sql.Connection;
  23. import java.sql.DatabaseMetaData;
  24. import java.sql.ResultSet;
  25. import java.sql.ResultSetMetaData;
  26. import java.sql.SQLException;

  27. import org.dbunit.dataset.AbstractTableMetaData;
  28. import org.dbunit.dataset.Column;
  29. import org.dbunit.dataset.DataSetException;
  30. import org.dbunit.dataset.DefaultTableMetaData;
  31. import org.dbunit.dataset.datatype.DataType;
  32. import org.dbunit.dataset.datatype.DataTypeException;
  33. import org.dbunit.dataset.datatype.IDataTypeFactory;
  34. import org.dbunit.util.SQLHelper;
  35. import org.slf4j.Logger;
  36. import org.slf4j.LoggerFactory;

  37. /**
  38.  * {@link ResultSet} based {@link org.dbunit.dataset.ITableMetaData} implementation.
  39.  * <p>
  40.  * The lookup for the information needed to create the {@link Column} objects is retrieved
  41.  * in two phases:
  42.  * <ol>
  43.  * <li>Try to find the information from the given {@link ResultSet} via a {@link DatabaseMetaData}
  44.  * object. Therefore the {@link ResultSetMetaData} is used to get the catalog/schema/table/column
  45.  * names which in turn are used to get column information via
  46.  * {@link DatabaseMetaData#getColumns(String, String, String, String)}. The reason for this is
  47.  * that the {@link DatabaseMetaData} is more precise and contains more information about columns
  48.  * than the {@link ResultSetMetaData} does. Another reason is that some JDBC drivers (currently known
  49.  * from MYSQL driver) provide an inconsistent implementation of those two MetaData objects
  50.  * and the {@link DatabaseMetaData} is hence considered to be the master by dbunit.
  51.  * </li>
  52.  * <li>
  53.  * Since some JDBC drivers (one of them being Oracle) cannot (or just do not) provide the
  54.  * catalog/schema/table/column values on a {@link ResultSetMetaData} instance the second
  55.  * step will create the dbunit {@link Column} using the {@link ResultSetMetaData} methods
  56.  * directly (for example {@link ResultSetMetaData#getColumnType(int)}. (This is also the way
  57.  * dbunit worked until the 2.4 release)
  58.  * </li>
  59.  * </ol>
  60.  * </p>
  61.  *
  62.  * @author gommma (gommma AT users.sourceforge.net)
  63.  * @author Last changed by: $Author$
  64.  * @version $Revision$ $Date$
  65.  * @since 2.3.0
  66.  */
  67. public class ResultSetTableMetaData extends AbstractTableMetaData
  68. {
  69.     /**
  70.      * Logger for this class
  71.      */
  72.     private static final Logger logger = LoggerFactory.getLogger(DatabaseTableMetaData.class);

  73.     /**
  74.      * The actual table metadata
  75.      */
  76.     private DefaultTableMetaData wrappedTableMetaData;
  77.     private boolean _caseSensitiveMetaData;

  78.     /**
  79.      * @param tableName The name of the database table
  80.      * @param resultSet The JDBC result set that is used to retrieve the columns
  81.      * @param connection The connection which is needed to retrieve some configuration values
  82.      * @param caseSensitiveMetaData Whether or not the metadata is case sensitive
  83.      * @throws DataSetException
  84.      * @throws SQLException
  85.      */
  86.     public ResultSetTableMetaData(String tableName,
  87.             ResultSet resultSet, IDatabaseConnection connection, boolean caseSensitiveMetaData)
  88.     throws DataSetException, SQLException
  89.     {
  90.         super();
  91.         _caseSensitiveMetaData = caseSensitiveMetaData;
  92.         this.wrappedTableMetaData = createMetaData(tableName, resultSet, connection);
  93.        
  94.     }

  95.     /**
  96.      * @param tableName The name of the database table
  97.      * @param resultSet The JDBC result set that is used to retrieve the columns
  98.      * @param dataTypeFactory
  99.      * @param caseSensitiveMetaData Whether or not the metadata is case sensitive
  100.      * @throws DataSetException
  101.      * @throws SQLException
  102.      * @deprecated since 2.4.4. use {@link ResultSetTableMetaData#ResultSetTableMetaData(String, ResultSet, IDatabaseConnection, boolean)}
  103.      */
  104.     public ResultSetTableMetaData(String tableName,
  105.             ResultSet resultSet, IDataTypeFactory dataTypeFactory, boolean caseSensitiveMetaData)
  106.     throws DataSetException, SQLException
  107.     {
  108.         super();
  109.         _caseSensitiveMetaData = caseSensitiveMetaData;
  110.         this.wrappedTableMetaData = createMetaData(tableName, resultSet, dataTypeFactory, new DefaultMetadataHandler());
  111.     }

  112.    
  113.     private DefaultTableMetaData createMetaData(String tableName,
  114.             ResultSet resultSet, IDatabaseConnection connection)
  115.             throws SQLException, DataSetException
  116.     {
  117.         if (logger.isTraceEnabled())
  118.             logger.trace("createMetaData(tableName={}, resultSet={}, connection={}) - start",
  119.                     new Object[] { tableName, resultSet, connection });

  120.         DatabaseConfig dbConfig = connection.getConfig();
  121.         IMetadataHandler columnFactory = (IMetadataHandler)dbConfig.getProperty(DatabaseConfig.PROPERTY_METADATA_HANDLER);
  122.         IDataTypeFactory typeFactory = super.getDataTypeFactory(connection);
  123.         return createMetaData(tableName, resultSet, typeFactory, columnFactory);
  124.     }

  125.     private DefaultTableMetaData createMetaData(String tableName,
  126.             ResultSet resultSet, IDataTypeFactory dataTypeFactory, IMetadataHandler columnFactory)
  127.             throws DataSetException, SQLException
  128.     {
  129.         if (logger.isTraceEnabled())
  130.             logger.trace("createMetaData(tableName={}, resultSet={}, dataTypeFactory={}, columnFactory={}) - start",
  131.                     new Object[]{ tableName, resultSet, dataTypeFactory, columnFactory });

  132.         Connection connection = resultSet.getStatement().getConnection();
  133.         DatabaseMetaData databaseMetaData = connection.getMetaData();
  134.        
  135.         ResultSetMetaData metaData = resultSet.getMetaData();
  136.         Column[] columns = new Column[metaData.getColumnCount()];
  137.         for (int i = 0; i < columns.length; i++)
  138.         {
  139.             int rsIndex = i+1;
  140.            
  141.             // 1. try to create the column from the DatabaseMetaData object. The DatabaseMetaData
  142.             // provides more information and is more precise so that it should always be used in
  143.             // preference to the ResultSetMetaData object.
  144.             columns[i] = createColumnFromDbMetaData(metaData, rsIndex, databaseMetaData, dataTypeFactory, columnFactory);
  145.            
  146.             // 2. If we could not create the Column from a DatabaseMetaData object, try to create it
  147.             // from the ResultSetMetaData object directly
  148.             if(columns[i] == null)
  149.             {
  150.                 columns[i] = createColumnFromRsMetaData(metaData, rsIndex, tableName, dataTypeFactory);
  151.             }
  152.         }

  153.         return new DefaultTableMetaData(tableName, columns);
  154.     }

  155.     private Column createColumnFromRsMetaData(ResultSetMetaData rsMetaData,
  156.             int rsIndex, String tableName, IDataTypeFactory dataTypeFactory)
  157.     throws SQLException, DataTypeException
  158.     {
  159.         if(logger.isTraceEnabled()){
  160.             logger.trace("createColumnFromRsMetaData(rsMetaData={}, rsIndex={}," +
  161.                     " tableName={}, dataTypeFactory={}) - start",
  162.                 new Object[]{rsMetaData, String.valueOf(rsIndex),
  163.                     tableName, dataTypeFactory});
  164.         }

  165.         int columnType = rsMetaData.getColumnType(rsIndex);
  166.         String columnTypeName = rsMetaData.getColumnTypeName(rsIndex);
  167.         String columnName = rsMetaData.getColumnLabel(rsIndex);
  168.         int isNullable = rsMetaData.isNullable(rsIndex);

  169.         DataType dataType = dataTypeFactory.createDataType(
  170.                     columnType, columnTypeName, tableName, columnName);

  171.         Column column = new Column(
  172.                 columnName,
  173.                 dataType,
  174.                 columnTypeName,
  175.                 Column.nullableValue(isNullable));
  176.         return column;
  177.     }

  178.     /**
  179.      * Try to create the Column using information from the given {@link ResultSetMetaData}
  180.      * to search the column via the given {@link DatabaseMetaData}. If the
  181.      * {@link ResultSetMetaData} does not provide the required information
  182.      * (one of catalog/schema/table is "")
  183.      * the search for the Column via {@link DatabaseMetaData} is not executed and <code>null</code>
  184.      * is returned immediately.
  185.      * @param rsMetaData The {@link ResultSetMetaData} from which to retrieve the {@link DatabaseMetaData}
  186.      * @param rsIndex The current index in the {@link ResultSetMetaData}
  187.      * @param databaseMetaData The {@link DatabaseMetaData} which is used to lookup detailed
  188.      * information about the column if possible
  189.      * @param dataTypeFactory dbunit {@link IDataTypeFactory} needed to create the Column
  190.      * @param metadataHandler the handler to be used for {@link DatabaseMetaData} handling
  191.      * @return The column or <code>null</code> if it can be not created using a
  192.      * {@link DatabaseMetaData} object because of missing information in the
  193.      * {@link ResultSetMetaData} object
  194.      * @throws SQLException
  195.      * @throws DataTypeException
  196.      */
  197.     private Column createColumnFromDbMetaData(ResultSetMetaData rsMetaData, int rsIndex,
  198.             DatabaseMetaData databaseMetaData, IDataTypeFactory dataTypeFactory,
  199.             IMetadataHandler metadataHandler)
  200.     throws SQLException, DataTypeException
  201.     {
  202.         if(logger.isTraceEnabled()){
  203.             logger.trace("createColumnFromMetaData(rsMetaData={}, rsIndex={}," +
  204.                     " databaseMetaData={}, dataTypeFactory={}, columnFactory={}) - start",
  205.                 new Object[]{rsMetaData, String.valueOf(rsIndex),
  206.                             databaseMetaData, dataTypeFactory, metadataHandler});
  207.         }
  208.        
  209.         // use DatabaseMetaData to retrieve the actual column definition
  210.         String catalogName = rsMetaData.getCatalogName(rsIndex);
  211.         String schemaName = rsMetaData.getSchemaName(rsIndex);
  212.         String tableName = rsMetaData.getTableName(rsIndex);
  213.         String columnName = rsMetaData.getColumnLabel(rsIndex);
  214.        
  215.         // Due to a bug in the DB2 JDBC driver we have to trim the names
  216.         catalogName = trim(catalogName);
  217.         schemaName = trim(schemaName);
  218.         tableName = trim(tableName);
  219.         columnName = trim(columnName);
  220.        
  221.         // Check if at least one of catalog/schema/table attributes is
  222.         // not applicable (i.e. "" is returned). If so do not try
  223.         // to get the column metadata from the DatabaseMetaData object.
  224.         // This is the case for all oracle JDBC drivers
  225.         if(catalogName != null && catalogName.equals("")) {
  226.             // Catalog name is not required
  227.             catalogName = null;
  228.         }
  229.         if(schemaName != null && schemaName.equals("")) {
  230.             logger.debug("The 'schemaName' from the ResultSetMetaData is empty-string and not applicable hence. " +
  231.             "Will not try to lookup column properties via DatabaseMetaData.getColumns.");
  232.             return null;
  233.         }
  234.         if(tableName != null && tableName.equals("")) {
  235.             logger.debug("The 'tableName' from the ResultSetMetaData is empty-string and not applicable hence. " +
  236.             "Will not try to lookup column properties via DatabaseMetaData.getColumns.");
  237.             return null;
  238.         }
  239.        
  240.         if(logger.isDebugEnabled())
  241.             logger.debug("All attributes from the ResultSetMetaData are valid, " +
  242.                     "trying to lookup values in DatabaseMetaData. catalog={}, schema={}, table={}, column={}",
  243.                     new Object[]{catalogName, schemaName, tableName, columnName} );
  244.        
  245.         // All of the retrieved attributes are valid,
  246.         // so lookup the column via DatabaseMetaData
  247.         ResultSet columnsResultSet = metadataHandler.getColumns(databaseMetaData, schemaName, tableName);

  248.         try
  249.         {
  250.             // Scroll resultset forward - must have one result which exactly matches the required parameters
  251.             scrollTo(columnsResultSet, metadataHandler, catalogName, schemaName, tableName, columnName);

  252.             Column column = SQLHelper.createColumn(columnsResultSet, dataTypeFactory, true);
  253.             return column;
  254.         }
  255.         catch(IllegalStateException e)
  256.         {
  257.             logger.warn("Cannot find column from ResultSetMetaData info via DatabaseMetaData. Returning null." +
  258.                     " Even if this is expected to never happen it probably happened due to a JDBC driver bug." +
  259.                     " To get around this you may want to configure a user defined " + IMetadataHandler.class, e);
  260.             return null;
  261.         }
  262.         finally
  263.         {
  264.             SQLHelper.close(columnsResultSet);
  265.         }
  266.     }


  267.     /**
  268.      * Trims the given string in a null-safe way
  269.      * @param value
  270.      * @return
  271.      * @since 2.4.6
  272.      */
  273.     private String trim(String value)
  274.     {
  275.         return (value==null ? null : value.trim());
  276.     }

  277.     private void scrollTo(ResultSet columnsResultSet, IMetadataHandler metadataHandler,
  278.             String catalog, String schema, String table, String column)
  279.     throws SQLException
  280.     {
  281.         while(columnsResultSet.next())
  282.         {
  283.             boolean match = metadataHandler.matches(columnsResultSet, catalog, schema, table, column, _caseSensitiveMetaData);
  284.             if(match)
  285.             {
  286.                 // All right. Return immediately because the resultSet is positioned on the correct row
  287.                 return;
  288.             }
  289.         }

  290.         // If we get here the column could not be found
  291.         String msg =
  292.                 "Did not find column '" + column +
  293.                 "' for <schema.table> '" + schema + "." + table +
  294.                 "' in catalog '" + catalog + "' because names do not exactly match.";

  295.         throw new IllegalStateException(msg);
  296.     }

  297.     public Column[] getColumns() throws DataSetException {
  298.         return this.wrappedTableMetaData.getColumns();
  299.     }

  300.     public Column[] getPrimaryKeys() throws DataSetException {
  301.         return this.wrappedTableMetaData.getPrimaryKeys();
  302.     }

  303.     public String getTableName() {
  304.         return this.wrappedTableMetaData.getTableName();
  305.     }

  306.     public String toString()
  307.     {
  308.         final StringBuilder sb = new StringBuilder();
  309.         sb.append(getClass().getName()).append("[");
  310.         sb.append("wrappedTableMetaData=").append(this.wrappedTableMetaData);
  311.         sb.append("]");
  312.         return sb.toString();
  313.     }
  314. }