View Javadoc
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  
23  import java.sql.Connection;
24  import java.sql.DatabaseMetaData;
25  import java.sql.ResultSet;
26  import java.sql.ResultSetMetaData;
27  import java.sql.SQLException;
28  
29  import org.dbunit.dataset.AbstractTableMetaData;
30  import org.dbunit.dataset.Column;
31  import org.dbunit.dataset.DataSetException;
32  import org.dbunit.dataset.DefaultTableMetaData;
33  import org.dbunit.dataset.datatype.DataType;
34  import org.dbunit.dataset.datatype.DataTypeException;
35  import org.dbunit.dataset.datatype.IDataTypeFactory;
36  import org.dbunit.util.SQLHelper;
37  import org.slf4j.Logger;
38  import org.slf4j.LoggerFactory;
39  
40  /**
41   * {@link ResultSet} based {@link org.dbunit.dataset.ITableMetaData} implementation.
42   * <p>
43   * The lookup for the information needed to create the {@link Column} objects is retrieved
44   * in two phases:
45   * <ol>
46   * <li>Try to find the information from the given {@link ResultSet} via a {@link DatabaseMetaData}
47   * object. Therefore the {@link ResultSetMetaData} is used to get the catalog/schema/table/column
48   * names which in turn are used to get column information via
49   * {@link DatabaseMetaData#getColumns(String, String, String, String)}. The reason for this is
50   * that the {@link DatabaseMetaData} is more precise and contains more information about columns
51   * than the {@link ResultSetMetaData} does. Another reason is that some JDBC drivers (currently known
52   * from MYSQL driver) provide an inconsistent implementation of those two MetaData objects
53   * and the {@link DatabaseMetaData} is hence considered to be the master by dbunit.
54   * </li>
55   * <li>
56   * Since some JDBC drivers (one of them being Oracle) cannot (or just do not) provide the 
57   * catalog/schema/table/column values on a {@link ResultSetMetaData} instance the second 
58   * step will create the dbunit {@link Column} using the {@link ResultSetMetaData} methods 
59   * directly (for example {@link ResultSetMetaData#getColumnType(int)}. (This is also the way
60   * dbunit worked until the 2.4 release)
61   * </li>
62   * </ol> 
63   * </p>
64   * 
65   * @author gommma (gommma AT users.sourceforge.net)
66   * @author Last changed by: $Author$
67   * @version $Revision$ $Date$
68   * @since 2.3.0
69   */
70  public class ResultSetTableMetaData extends AbstractTableMetaData 
71  {
72      /**
73       * Logger for this class
74       */
75      private static final Logger logger = LoggerFactory.getLogger(DatabaseTableMetaData.class);
76  
77      /**
78       * The actual table metadata
79       */
80      private DefaultTableMetaData wrappedTableMetaData;
81  	private boolean _caseSensitiveMetaData;
82  
83  	/**
84  	 * @param tableName The name of the database table
85  	 * @param resultSet The JDBC result set that is used to retrieve the columns
86  	 * @param connection The connection which is needed to retrieve some configuration values
87  	 * @param caseSensitiveMetaData Whether or not the metadata is case sensitive
88  	 * @throws DataSetException
89  	 * @throws SQLException
90  	 */
91  	public ResultSetTableMetaData(String tableName,
92              ResultSet resultSet, IDatabaseConnection connection, boolean caseSensitiveMetaData) 
93  	throws DataSetException, SQLException 
94  	{
95  		super();
96          _caseSensitiveMetaData = caseSensitiveMetaData;
97  		this.wrappedTableMetaData = createMetaData(tableName, resultSet, connection);
98  		
99  	}
100 
101 	/**
102 	 * @param tableName The name of the database table
103 	 * @param resultSet The JDBC result set that is used to retrieve the columns
104 	 * @param dataTypeFactory
105      * @param caseSensitiveMetaData Whether or not the metadata is case sensitive
106 	 * @throws DataSetException
107 	 * @throws SQLException
108      * @deprecated since 2.4.4. use {@link ResultSetTableMetaData#ResultSetTableMetaData(String, ResultSet, IDatabaseConnection, boolean)}
109 	 */
110 	public ResultSetTableMetaData(String tableName,
111             ResultSet resultSet, IDataTypeFactory dataTypeFactory, boolean caseSensitiveMetaData) 
112 	throws DataSetException, SQLException 
113 	{
114 		super();
115 		_caseSensitiveMetaData = caseSensitiveMetaData;
116 		this.wrappedTableMetaData = createMetaData(tableName, resultSet, dataTypeFactory, new DefaultMetadataHandler());
117 	}
118 
119 	
120     private DefaultTableMetaData createMetaData(String tableName,
121             ResultSet resultSet, IDatabaseConnection connection)
122             throws SQLException, DataSetException
123     {
124     	if (logger.isTraceEnabled())
125     		logger.trace("createMetaData(tableName={}, resultSet={}, connection={}) - start",
126     				new Object[] { tableName, resultSet, connection });
127 
128     	DatabaseConfig dbConfig = connection.getConfig();
129     	IMetadataHandler columnFactory = (IMetadataHandler)dbConfig.getProperty(DatabaseConfig.PROPERTY_METADATA_HANDLER);
130         IDataTypeFactory typeFactory = super.getDataTypeFactory(connection);
131         return createMetaData(tableName, resultSet, typeFactory, columnFactory);
132     }
133 
134     private DefaultTableMetaData createMetaData(String tableName,
135             ResultSet resultSet, IDataTypeFactory dataTypeFactory, IMetadataHandler columnFactory)
136             throws DataSetException, SQLException
137     {
138     	if (logger.isTraceEnabled())
139     		logger.trace("createMetaData(tableName={}, resultSet={}, dataTypeFactory={}, columnFactory={}) - start",
140     				new Object[]{ tableName, resultSet, dataTypeFactory, columnFactory });
141 
142     	Connection connection = resultSet.getStatement().getConnection();
143     	DatabaseMetaData databaseMetaData = connection.getMetaData();
144     	
145         ResultSetMetaData metaData = resultSet.getMetaData();
146         Column[] columns = new Column[metaData.getColumnCount()];
147         for (int i = 0; i < columns.length; i++)
148         {
149             int rsIndex = i+1;
150             
151             // 1. try to create the column from the DatabaseMetaData object. The DatabaseMetaData
152             // provides more information and is more precise so that it should always be used in
153             // preference to the ResultSetMetaData object.
154             columns[i] = createColumnFromDbMetaData(metaData, rsIndex, databaseMetaData, dataTypeFactory, columnFactory);
155             
156             // 2. If we could not create the Column from a DatabaseMetaData object, try to create it
157             // from the ResultSetMetaData object directly
158             if(columns[i] == null)
159             {
160                 columns[i] = createColumnFromRsMetaData(metaData, rsIndex, tableName, dataTypeFactory);
161             }
162         }
163 
164         return new DefaultTableMetaData(tableName, columns);
165     }
166 
167     private Column createColumnFromRsMetaData(ResultSetMetaData rsMetaData,
168             int rsIndex, String tableName, IDataTypeFactory dataTypeFactory) 
169     throws SQLException, DataTypeException 
170     {
171         if(logger.isTraceEnabled()){
172             logger.trace("createColumnFromRsMetaData(rsMetaData={}, rsIndex={}," + 
173                     " tableName={}, dataTypeFactory={}) - start",
174                 new Object[]{rsMetaData, String.valueOf(rsIndex), 
175                     tableName, dataTypeFactory});
176         }
177 
178         int columnType = rsMetaData.getColumnType(rsIndex);
179         String columnTypeName = rsMetaData.getColumnTypeName(rsIndex);
180         String columnName = rsMetaData.getColumnLabel(rsIndex);
181         int isNullable = rsMetaData.isNullable(rsIndex);
182 
183         DataType dataType = dataTypeFactory.createDataType(
184                     columnType, columnTypeName, tableName, columnName);
185 
186         Column column = new Column(
187                 columnName,
188                 dataType,
189                 columnTypeName,
190                 Column.nullableValue(isNullable));
191         return column;
192     }
193 
194     /**
195      * Try to create the Column using information from the given {@link ResultSetMetaData}
196      * to search the column via the given {@link DatabaseMetaData}. If the
197      * {@link ResultSetMetaData} does not provide the required information 
198      * (one of catalog/schema/table is "")
199      * the search for the Column via {@link DatabaseMetaData} is not executed and <code>null</code>
200      * is returned immediately.
201      * @param rsMetaData The {@link ResultSetMetaData} from which to retrieve the {@link DatabaseMetaData}
202      * @param rsIndex The current index in the {@link ResultSetMetaData}
203      * @param databaseMetaData The {@link DatabaseMetaData} which is used to lookup detailed
204      * information about the column if possible
205      * @param dataTypeFactory dbunit {@link IDataTypeFactory} needed to create the Column
206      * @param metadataHandler the handler to be used for {@link DatabaseMetaData} handling
207      * @return The column or <code>null</code> if it can be not created using a 
208      * {@link DatabaseMetaData} object because of missing information in the 
209      * {@link ResultSetMetaData} object
210      * @throws SQLException
211      * @throws DataTypeException 
212      */
213     private Column createColumnFromDbMetaData(ResultSetMetaData rsMetaData, int rsIndex, 
214             DatabaseMetaData databaseMetaData, IDataTypeFactory dataTypeFactory,
215             IMetadataHandler metadataHandler) 
216     throws SQLException, DataTypeException 
217     {
218         if(logger.isTraceEnabled()){
219             logger.trace("createColumnFromMetaData(rsMetaData={}, rsIndex={}," + 
220                     " databaseMetaData={}, dataTypeFactory={}, columnFactory={}) - start",
221                 new Object[]{rsMetaData, String.valueOf(rsIndex), 
222                             databaseMetaData, dataTypeFactory, metadataHandler});
223         }
224         
225         // use DatabaseMetaData to retrieve the actual column definition
226         String catalogName = rsMetaData.getCatalogName(rsIndex);
227         String schemaName = rsMetaData.getSchemaName(rsIndex);
228         String tableName = rsMetaData.getTableName(rsIndex);
229         String columnName = rsMetaData.getColumnLabel(rsIndex);
230         
231         // Due to a bug in the DB2 JDBC driver we have to trim the names
232         catalogName = trim(catalogName);
233         schemaName = trim(schemaName);
234         tableName = trim(tableName);
235         columnName = trim(columnName);
236         
237         // Check if at least one of catalog/schema/table attributes is
238         // not applicable (i.e. "" is returned). If so do not try
239         // to get the column metadata from the DatabaseMetaData object.
240         // This is the case for all oracle JDBC drivers
241         if(catalogName != null && catalogName.equals("")) {
242             // Catalog name is not required
243             catalogName = null;
244         }
245         if(schemaName != null && schemaName.equals("")) {
246             logger.debug("The 'schemaName' from the ResultSetMetaData is empty-string and not applicable hence. " +
247             "Will not try to lookup column properties via DatabaseMetaData.getColumns.");
248             return null;
249         }
250         if(tableName != null && tableName.equals("")) {
251             logger.debug("The 'tableName' from the ResultSetMetaData is empty-string and not applicable hence. " +
252             "Will not try to lookup column properties via DatabaseMetaData.getColumns.");
253             return null;
254         }
255         
256         if(logger.isDebugEnabled())
257             logger.debug("All attributes from the ResultSetMetaData are valid, " +
258                     "trying to lookup values in DatabaseMetaData. catalog={}, schema={}, table={}, column={}",
259                     new Object[]{catalogName, schemaName, tableName, columnName} );
260         
261         // All of the retrieved attributes are valid, 
262         // so lookup the column via DatabaseMetaData
263         ResultSet columnsResultSet = metadataHandler.getColumns(databaseMetaData, schemaName, tableName);
264 
265         try
266         {
267             // Scroll resultset forward - must have one result which exactly matches the required parameters
268             scrollTo(columnsResultSet, metadataHandler, catalogName, schemaName, tableName, columnName);
269 
270             Column column = SQLHelper.createColumn(columnsResultSet, dataTypeFactory, true);
271             return column;
272         }
273         catch(IllegalStateException e)
274         {
275             logger.warn("Cannot find column from ResultSetMetaData info via DatabaseMetaData. Returning null." +
276                     " Even if this is expected to never happen it probably happened due to a JDBC driver bug." +
277                     " To get around this you may want to configure a user defined " + IMetadataHandler.class, e);
278             return null;
279         }
280         finally
281         {
282             SQLHelper.close(columnsResultSet);
283         }
284     }
285 
286 
287     /**
288      * Trims the given string in a null-safe way
289      * @param value
290      * @return
291      * @since 2.4.6
292      */
293     private String trim(String value) 
294     {
295         return (value==null ? null : value.trim());
296     }
297 
298     private void scrollTo(ResultSet columnsResultSet, IMetadataHandler metadataHandler,
299             String catalog, String schema, String table, String column) 
300     throws SQLException 
301     {
302         while(columnsResultSet.next())
303         {
304             boolean match = metadataHandler.matches(columnsResultSet, catalog, schema, table, column, _caseSensitiveMetaData);
305             if(match)
306             {
307                 // All right. Return immediately because the resultSet is positioned on the correct row
308                 return;
309             }
310         }
311 
312         // If we get here the column could not be found
313         String msg = 
314                 "Did not find column '" + column + 
315                 "' for <schema.table> '" + schema + "." + table + 
316                 "' in catalog '" + catalog + "' because names do not exactly match.";
317 
318         throw new IllegalStateException(msg);
319     }
320 
321 	public Column[] getColumns() throws DataSetException {
322 		return this.wrappedTableMetaData.getColumns();
323 	}
324 
325 	public Column[] getPrimaryKeys() throws DataSetException {
326 		return this.wrappedTableMetaData.getPrimaryKeys();
327 	}
328 
329 	public String getTableName() {
330 		return this.wrappedTableMetaData.getTableName();
331 	}
332 
333 	public String toString()
334 	{
335 		StringBuffer sb = new StringBuffer();
336 		sb.append(getClass().getName()).append("[");
337 		sb.append("wrappedTableMetaData=").append(this.wrappedTableMetaData);
338 		sb.append("]");
339 		return sb.toString();
340 	}
341 }