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