View Javadoc
1   /*
2    *
3    * The DbUnit Database Testing Framework
4    * Copyright (C)2005, 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  
22  package org.dbunit.util;
23  
24  import java.io.PrintStream;
25  import java.sql.Connection;
26  import java.sql.DatabaseMetaData;
27  import java.sql.ResultSet;
28  import java.sql.SQLException;
29  import java.sql.Statement;
30  import java.util.Locale;
31  
32  import org.dbunit.DatabaseUnitRuntimeException;
33  import org.dbunit.database.IMetadataHandler;
34  import org.dbunit.dataset.Column;
35  import org.dbunit.dataset.datatype.DataType;
36  import org.dbunit.dataset.datatype.DataTypeException;
37  import org.dbunit.dataset.datatype.IDataTypeFactory;
38  import org.slf4j.Logger;
39  import org.slf4j.LoggerFactory;
40  
41  /**
42   * Helper for SQL-related stuff.
43   * <br>
44   * TODO: testcases, also think about refactoring so that methods are not static anymore (for better extensibility)
45   * @author Felipe Leme (dbunit@felipeal.net)
46   * @version $Revision$
47   * @since Nov 5, 2005
48   * 
49   */
50  public class SQLHelper {
51  
52      /**
53       * The database product name reported by Sybase JDBC drivers.
54       */
55      public static final String DB_PRODUCT_SYBASE = "Sybase";
56  
57      /**
58       * Logger for this class
59       */
60      private static final Logger logger = LoggerFactory.getLogger(SQLHelper.class);
61  
62      // class is "static"
63      private SQLHelper() {}
64  
65      /**
66       * Gets the primary column for a table.
67       * @param conn connection with the database
68       * @param table table name
69       * @return name of primary column for a table (assuming it's just 1 column).
70       * @throws SQLException raised while getting the meta data
71       */
72      public static String getPrimaryKeyColumn( Connection conn, String table ) throws SQLException {
73          logger.debug("getPrimaryKeyColumn(conn={}, table={}) - start", conn, table);
74  
75          DatabaseMetaData metadata = conn.getMetaData();
76          ResultSet rs = metadata.getPrimaryKeys( null, null, table );
77          rs.next();
78          String pkColumn = rs.getString(4);
79          return pkColumn;
80      }
81  
82      /**
83       * Close a result set and a prepared statement, checking for null references.
84       * @param rs result set to be closed
85       * @param stmt prepared statement to be closed
86       * @throws SQLException exception raised in either close() method
87       */
88      public static void close(ResultSet rs, Statement stmt) throws SQLException {
89          logger.debug("close(rs={}, stmt={}) - start", rs, stmt);
90  
91          try {
92              SQLHelper.close(rs);
93          } finally {
94              SQLHelper.close( stmt );
95          }
96      }
97  
98      /**
99       * Close a SQL statement, checking for null references.
100      * @param stmt statement to be closed
101      * @throws SQLException exception raised while closing the statement
102      */
103     public static void close(Statement stmt) throws SQLException {
104         logger.debug("close(stmt={}) - start", stmt);
105 
106         if ( stmt != null ) {
107             stmt.close();
108         }
109     }
110 
111     /**
112      * Closes the given result set in a null-safe way
113      * @param resultSet
114      * @throws SQLException
115      */
116     public static void close(ResultSet resultSet) throws SQLException {
117         logger.debug("close(resultSet={}) - start", resultSet);
118 
119         if(resultSet != null) {
120             resultSet.close();
121         }
122     }
123 
124     /**
125      * Returns <code>true</code> if the given schema exists for the given connection.
126      * @param connection The connection to a database
127      * @param schema The schema to be searched
128      * @return Returns <code>true</code> if the given schema exists for the given connection.
129      * @throws SQLException
130      * @since 2.3.0
131      */
132     public static boolean schemaExists(Connection connection, String schema)
133             throws SQLException
134             {
135         logger.trace("schemaExists(connection={}, schema={}) - start", connection, schema);
136 
137         if(schema == null)
138         {
139             throw new NullPointerException("The parameter 'schema' must not be null");
140         }
141 
142         DatabaseMetaData metaData = connection.getMetaData();
143         ResultSet rs = metaData.getSchemas(); //null, schemaPattern);
144         try
145         {
146             while(rs.next())
147             {
148                 String foundSchema = rs.getString("TABLE_SCHEM");
149                 if(foundSchema.equals(schema))
150                 {
151                     return true;
152                 }
153             }
154 
155             // Especially for MySQL check the catalog
156             if(catalogExists(connection, schema))
157             {
158                 logger.debug("Found catalog with name {}. Returning true because DB is probably on MySQL", schema);
159                 return true;
160             }
161 
162             return false;
163         }
164         finally
165         {
166             rs.close();
167         }
168             }
169 
170     /**
171      * Checks via {@link DatabaseMetaData#getCatalogs()} whether or not the given catalog exists.
172      * @param connection
173      * @param catalog
174      * @return
175      * @throws SQLException
176      * @since 2.4.4
177      */
178     private static boolean catalogExists(Connection connection, String catalog) throws SQLException
179     {
180         logger.trace("catalogExists(connection={}, catalog={}) - start", connection, catalog);
181 
182         if(catalog == null)
183         {
184             throw new NullPointerException("The parameter 'catalog' must not be null");
185         }
186 
187         DatabaseMetaData metaData = connection.getMetaData();
188         ResultSet rs = metaData.getCatalogs();
189         try
190         {
191             while(rs.next())
192             {
193                 String foundCatalog = rs.getString("TABLE_CAT");
194                 if(foundCatalog.equals(catalog))
195                 {
196                     return true;
197                 }
198             }
199             return false;
200         }
201         finally
202         {
203             rs.close();
204         }
205 
206     }
207 
208     /**
209      * Checks if the given table exists.
210      * @param metaData The database meta data
211      * @param schema The schema in which the table should be searched. If <code>null</code>
212      * the schema is not used to narrow the table name.
213      * @param tableName The table name to be searched
214      * @return Returns <code>true</code> if the given table exists in the given schema.
215      * Else returns <code>false</code>.
216      * @throws SQLException
217      * @since 2.3.0
218      * @deprecated since 2.4.5 - use {@link IMetadataHandler#tableExists(DatabaseMetaData, String, String)}
219      */
220     public static boolean tableExists(DatabaseMetaData metaData, String schema,
221             String tableName)
222                     throws SQLException
223                     {
224         ResultSet tableRs = metaData.getTables(null, schema, tableName, null);
225         try
226         {
227             return tableRs.next();
228         }
229         finally
230         {
231             SQLHelper.close(tableRs);
232         }
233                     }
234 
235     /**
236      * Utility method for debugging to print all tables of the given metadata on the given stream
237      * @param metaData
238      * @param outputStream
239      * @throws SQLException
240      */
241     public static void printAllTables(DatabaseMetaData metaData, PrintStream outputStream) throws SQLException
242     {
243         ResultSet rs = metaData.getTables(null, null, null, null);
244         try
245         {
246             while (rs.next())
247             {
248                 String catalog = rs.getString("TABLE_CAT");
249                 String schema = rs.getString("TABLE_SCHEM");
250                 String table = rs.getString("TABLE_NAME");
251                 final StringBuilder tableInfo = new StringBuilder();
252      			if(catalog!=null) tableInfo.append(catalog).append(".");
253      			if(schema!=null) tableInfo.append(schema).append(".");
254                 tableInfo.append(table);
255                 // Print the info
256                 outputStream.println(tableInfo);
257             }
258             outputStream.flush();
259         }
260         finally
261         {
262             SQLHelper.close(rs);
263         }
264 
265     }
266 
267     /**
268      * Returns the database and JDBC driver information as pretty formatted string
269      * @param metaData The JDBC database metadata needed to retrieve database information
270      * @return The database information as formatted string
271      */
272     public static String getDatabaseInfo(DatabaseMetaData metaData)
273     {
274         final StringBuilder sb = new StringBuilder();
275         sb.append("\n");
276 
277         String dbInfo = null;
278 
279         dbInfo = new ExceptionWrapper(){
280             public String wrappedCall(DatabaseMetaData metaData) throws Exception {
281                 return metaData.getDatabaseProductName();
282             }
283         }.executeWrappedCall(metaData);
284         sb.append("\tdatabase product name=").append(dbInfo).append("\n");
285 
286         dbInfo = new ExceptionWrapper(){
287             public String wrappedCall(DatabaseMetaData metaData) throws Exception {
288                 return metaData.getDatabaseProductVersion();
289             }
290         }.executeWrappedCall(metaData);
291         sb.append("\tdatabase version=").append(dbInfo).append("\n");
292 
293         dbInfo = new ExceptionWrapper(){
294             public String wrappedCall(DatabaseMetaData metaData) throws Exception {
295                 return String.valueOf(metaData.getDatabaseMajorVersion());
296             }
297         }.executeWrappedCall(metaData);
298         sb.append("\tdatabase major version=").append(dbInfo).append("\n");
299 
300         dbInfo = new ExceptionWrapper(){
301             public String wrappedCall(DatabaseMetaData metaData) throws Exception {
302                 return String.valueOf(metaData.getDatabaseMinorVersion());
303             }
304         }.executeWrappedCall(metaData);
305         sb.append("\tdatabase minor version=").append(dbInfo).append("\n");
306 
307         dbInfo = new ExceptionWrapper(){
308             public String wrappedCall(DatabaseMetaData metaData) throws Exception {
309                 return metaData.getDriverName();
310             }
311         }.executeWrappedCall(metaData);
312         sb.append("\tjdbc driver name=").append(dbInfo).append("\n");
313 
314         dbInfo = new ExceptionWrapper(){
315             public String wrappedCall(DatabaseMetaData metaData) throws Exception {
316                 return metaData.getDriverVersion();
317             }
318         }.executeWrappedCall(metaData);
319         sb.append("\tjdbc driver version=").append(dbInfo).append("\n");
320 
321         dbInfo = new ExceptionWrapper(){
322             public String wrappedCall(DatabaseMetaData metaData) throws Exception {
323                 return String.valueOf(metaData.getDriverMajorVersion());
324             }
325         }.executeWrappedCall(metaData);
326         sb.append("\tjdbc driver major version=").append(dbInfo).append("\n");
327 
328         dbInfo = new ExceptionWrapper(){
329             public String wrappedCall(DatabaseMetaData metaData) throws Exception {
330                 return String.valueOf(metaData.getDriverMinorVersion());
331             }
332         }.executeWrappedCall(metaData);
333         sb.append("\tjdbc driver minor version=").append(dbInfo).append("\n");
334 
335         return sb.toString();
336     }
337 
338     /**
339      * Prints the database and JDBC driver information to the given output stream
340      * @param metaData The JDBC database metadata needed to retrieve database information
341      * @param outputStream The stream to which the information is printed
342      * @throws SQLException
343      */
344     public static void printDatabaseInfo(DatabaseMetaData metaData, PrintStream outputStream) throws SQLException
345     {
346         String dbInfo = getDatabaseInfo(metaData);
347         try {
348             outputStream.println(dbInfo);
349         }
350         finally {
351             outputStream.flush();
352         }
353     }
354 
355     /**
356      * Detects whether or not the given metadata describes the connection to a Sybase database
357      * or not.
358      * @param metaData The metadata to be checked whether it is a Sybase connection
359      * @return <code>true</code> if and only if the given metadata belongs to a Sybase database.
360      * @throws SQLException
361      */
362     public static boolean isSybaseDb(DatabaseMetaData metaData) throws SQLException
363     {
364         String dbProductName = metaData.getDatabaseProductName();
365         boolean isSybase = (dbProductName != null && dbProductName.equals(DB_PRODUCT_SYBASE));
366         return isSybase;
367     }
368 
369 
370     /**
371      * Utility method to create a {@link Column} object from a SQL {@link ResultSet} object.
372      * 
373      * @param resultSet A result set produced via {@link DatabaseMetaData#getColumns(String, String, String, String)}
374      * @param dataTypeFactory The factory used to lookup the {@link DataType} for this column
375      * @param datatypeWarning Whether or not a warning should be printed if the column could not
376      * be created because of an unknown datatype.
377      * @return The {@link Column} or <code>null</code> if the column could not be initialized because of an
378      * unknown datatype.
379      * @throws SQLException
380      * @throws DataTypeException
381      * @since 2.4.0
382      */
383     public static final Column createColumn(ResultSet resultSet,
384             IDataTypeFactory dataTypeFactory, boolean datatypeWarning)
385                     throws SQLException, DataTypeException
386                     {
387         String tableName = resultSet.getString(3);
388         String columnName = resultSet.getString(4);
389         int sqlType = resultSet.getInt(5);
390         //If Types.DISTINCT like SQL DOMAIN, then get Source Date Type of SQL-DOMAIN
391         if(sqlType == java.sql.Types.DISTINCT)
392         {
393             sqlType = resultSet.getInt("SOURCE_DATA_TYPE");
394         }
395 
396         String sqlTypeName = resultSet.getString(6);
397         //        int columnSize = resultSet.getInt(7);
398         int nullable = resultSet.getInt(11);
399         String remarks = resultSet.getString(12);
400         String columnDefaultValue = resultSet.getString(13);
401         String isAutoIncrement = resultSet.getString(23);
402         // some JDBC drivers do not have this column even though they claim to be compliant with JDBC 4.1 or later
403         String isGenerated = resultSet.getMetaData().getColumnCount() >= 24 ? resultSet.getString(24) : null;
404 
405         // Convert SQL type to DataType
406         DataType dataType =
407                 dataTypeFactory.createDataType(sqlType, sqlTypeName, tableName, columnName);
408         if (dataType != DataType.UNKNOWN)
409         {
410             Column column = new Column(columnName, dataType,
411                     sqlTypeName, Column.nullableValue(nullable), columnDefaultValue, remarks,
412                     Column.AutoIncrement.autoIncrementValue(isAutoIncrement),
413                     Column.convertMetaDataBoolean(isGenerated));
414             return column;
415         }
416         else
417         {
418             if (datatypeWarning)
419                 logger.warn(
420                         tableName + "." + columnName +
421                         " data type (" + sqlType + ", '" + sqlTypeName +
422                         "') not recognized and will be ignored. See FAQ for more information.");
423 
424             // datatype unknown - column not created
425             return null;
426         }
427                     }
428 
429     /**
430      * Checks if the given <code>resultSet</code> matches the given schema and table name.
431      * The comparison is <b>case sensitive</b>.
432      * @param resultSet A result set produced via {@link DatabaseMetaData#getColumns(String, String, String, String)}
433      * @param schema The name of the schema to check. If <code>null</code> it is ignored in the comparison
434      * @param table The name of the table to check. If <code>null</code> it is ignored in the comparison
435      * @param caseSensitive Whether or not the comparison should be case sensitive or not
436      * @return <code>true</code> if the column metadata of the given <code>resultSet</code> matches
437      * the given schema and table parameters.
438      * @throws SQLException
439      * @since 2.4.0
440      * @deprecated since 2.4.4 - use {@link IMetadataHandler#matches(ResultSet, String, String, String, String, boolean)}
441      */
442     public static boolean matches(ResultSet resultSet,
443             String schema, String table, boolean caseSensitive)
444                     throws SQLException
445                     {
446         return matches(resultSet, null, schema, table, null, caseSensitive);
447                     }
448 
449 
450     /**
451      * Checks if the given <code>resultSet</code> matches the given schema and table name.
452      * The comparison is <b>case sensitive</b>.
453      * @param resultSet A result set produced via {@link DatabaseMetaData#getColumns(String, String, String, String)}
454      * @param catalog The name of the catalog to check. If <code>null</code> it is ignored in the comparison
455      * @param schema The name of the schema to check. If <code>null</code> it is ignored in the comparison
456      * @param table The name of the table to check. If <code>null</code> it is ignored in the comparison
457      * @param column The name of the column to check. If <code>null</code> it is ignored in the comparison
458      * @param caseSensitive Whether or not the comparison should be case sensitive or not
459      * @return <code>true</code> if the column metadata of the given <code>resultSet</code> matches
460      * the given schema and table parameters.
461      * @throws SQLException
462      * @since 2.4.0
463      * @deprecated since 2.4.4 - use {@link IMetadataHandler#matches(ResultSet, String, String, String, String, boolean)}
464      */
465     public static boolean matches(ResultSet resultSet,
466             String catalog, String schema,
467             String table, String column, boolean caseSensitive)
468                     throws SQLException
469                     {
470         String catalogName = resultSet.getString(1);
471         String schemaName = resultSet.getString(2);
472         String tableName = resultSet.getString(3);
473         String columnName = resultSet.getString(4);
474 
475         // MYSQL provides only a catalog but no schema
476         if(schema != null && schemaName == null && catalog==null && catalogName != null){
477             logger.debug("Switching catalog/schema because the are mutually null");
478             schemaName = catalogName;
479             catalogName = null;
480         }
481 
482         boolean areEqual =
483                 areEqualIgnoreNull(catalog, catalogName, caseSensitive) &&
484                 areEqualIgnoreNull(schema, schemaName, caseSensitive) &&
485                 areEqualIgnoreNull(table, tableName, caseSensitive) &&
486                 areEqualIgnoreNull(column, columnName, caseSensitive);
487         return areEqual;
488                     }
489 
490     /**
491      * Compares the given values and returns true if they are equal.
492      * If the first value is <code>null</code> or empty String it always
493      * returns <code>true</code> which is the way of ignoring <code>null</code>s
494      * for this specific case.
495      * @param value1 The first value to compare. Is ignored if null or empty String
496      * @param value2 The second value to be compared
497      * @return <code>true</code> if both values are equal or if the first value
498      * is <code>null</code> or empty string.
499      * @since 2.4.4
500      */
501     public static final boolean areEqualIgnoreNull(String value1, String value2, boolean caseSensitive)
502     {
503         if(value1==null || value1.equals(""))
504         {
505             return true;
506         }
507         else
508         {
509             if(caseSensitive && value1.equals(value2))
510             {
511                 return true;
512             }
513             else if(!caseSensitive && value1.equalsIgnoreCase(value2))
514             {
515                 return true;
516             }
517             else
518             {
519                 return false;
520             }
521         }
522     }
523 
524     /**
525      * Corrects the case of the given String according to the way in which the database stores metadata.
526      * @param databaseIdentifier A database identifier such as a table name or a schema name for
527      * which the case should be corrected.
528      * @param connection The connection used to lookup the database metadata. This is needed to determine
529      * the way in which the database stores its metadata.
530      * @return The database identifier in the correct case for the RDBMS
531      * @since 2.4.4
532      */
533     public static final String correctCase(final String databaseIdentifier, Connection connection)
534     {
535         logger.trace("correctCase(tableName={}, connection={}) - start", databaseIdentifier, connection);
536 
537         try
538         {
539             return correctCase(databaseIdentifier, connection.getMetaData());
540         }
541         catch (SQLException e)
542         {
543             throw new DatabaseUnitRuntimeException("Exception while trying to access database metadata", e);
544         }
545     }
546 
547     /**
548      * Corrects the case of the given String according to the way in which the database stores metadata.
549      * @param databaseIdentifier A database identifier such as a table name or a schema name for
550      * which the case should be corrected.
551      * @param databaseMetaData The database metadata needed to determine the way in which the database stores
552      * its metadata.
553      * @return The database identifier in the correct case for the RDBMS
554      * @since 2.4.4
555      */
556     public static final String correctCase(final String databaseIdentifier, DatabaseMetaData databaseMetaData)
557     {
558         logger.trace("correctCase(tableName={}, databaseMetaData={}) - start", databaseIdentifier, databaseMetaData);
559 
560         if (databaseIdentifier == null) {
561             throw new NullPointerException(
562                     "The parameter 'databaseIdentifier' must not be null");
563         }
564         if (databaseMetaData == null) {
565             throw new NullPointerException(
566                     "The parameter 'databaseMetaData' must not be null");
567         }
568 
569         try {
570             String resultTableName = databaseIdentifier;
571             String dbIdentifierQuoteString = databaseMetaData.getIdentifierQuoteString();
572             if(!isEscaped(databaseIdentifier, dbIdentifierQuoteString)){
573                 if(databaseMetaData.storesLowerCaseIdentifiers())
574                 {
575                     resultTableName = databaseIdentifier.toLowerCase(Locale.ENGLISH);
576                 }
577                 else if(databaseMetaData.storesUpperCaseIdentifiers())
578                 {
579                     resultTableName = databaseIdentifier.toUpperCase(Locale.ENGLISH);
580                 }
581                 else
582                 {
583                     logger.debug("Database does not store upperCase or lowerCase identifiers. " +
584                             "Will not correct case of the table names.");
585                 }
586             }
587             else
588             {
589                 if(logger.isDebugEnabled())
590                     logger.debug("The tableName '{}' is escaped. Will not correct case.", databaseIdentifier);
591                 }
592             return resultTableName;
593         }
594         catch (SQLException e)
595         {
596             throw new DatabaseUnitRuntimeException("Exception while trying to access database metadata", e);
597         }
598     }
599 
600     /**
601      * Checks whether two given values are unequal and if so print a log message (level DEBUG)
602      * @param oldValue The old value of a property
603      * @param newValue The new value of a property
604      * @param message The message to be logged
605      * @param source The class which invokes this method - used for enriching the log message
606      * @since 2.4.4
607      */
608     public static final void logInfoIfValueChanged(String oldValue, String newValue, String message, Class source)
609     {
610         if(logger.isInfoEnabled())
611         {
612             if(oldValue != null && !oldValue.equals(newValue))
613                 logger.debug("{}. {} oldValue={} newValue={}", new Object[] {source, message, oldValue, newValue});
614             }
615         }
616 
617     /**
618      * Checks whether two given values are unequal and if so print a log message (level DEBUG)
619      * @param oldValue The old value of a property
620      * @param newValue The new value of a property
621      * @param message The message to be logged
622      * @param source The class which invokes this method - used for enriching the log message
623      * @since 2.4.8
624      */
625     public static final void logDebugIfValueChanged(String oldValue, String newValue, String message, Class source)
626     {
627         if (logger.isDebugEnabled())
628         {
629             if (oldValue != null && !oldValue.equals(newValue))
630                 logger.debug("{}. {} oldValue={} newValue={}", new Object[] {source, message, oldValue, newValue});
631             }
632         }
633 
634     /**
635      * @param tableName
636      * @param dbIdentifierQuoteString
637      * @return
638      * @since 2.4.4
639      */
640     private static final boolean isEscaped(String tableName, String dbIdentifierQuoteString)
641     {
642         logger.trace("isEscaped(tableName={}, dbIdentifierQuoteString={}) - start", tableName, dbIdentifierQuoteString);
643 
644         if (dbIdentifierQuoteString == null) {
645             throw new NullPointerException(
646                     "The parameter 'dbIdentifierQuoteString' must not be null");
647         }
648         boolean isEscaped = tableName!=null && (tableName.startsWith(dbIdentifierQuoteString));
649         if(logger.isDebugEnabled())
650             logger.debug("isEscaped returns '{}' for tableName={} (dbIdentifierQuoteString={})",
651                     new Object[]{Boolean.valueOf(isEscaped), tableName, dbIdentifierQuoteString} );
652         return isEscaped;
653     }
654 
655     /**
656      * Performs a method invocation and catches all exceptions that occur during the invocation.
657      * Utility which works similar to a closure, just a bit less elegant.
658      * @author gommma (gommma AT users.sourceforge.net)
659      * @author Last changed by: $Author$
660      * @version $Revision$ $Date$
661      * @since 2.4.6
662      */
663     static abstract class ExceptionWrapper{
664 
665         public static final String NOT_AVAILABLE_TEXT = "<not available>";
666 
667         /**
668          * Default constructor
669          */
670         public ExceptionWrapper()
671         {
672         }
673 
674         /**
675          * Executes the call and catches all exception that might occur.
676          * @param metaData
677          * @return The result of the call
678          */
679         public final String executeWrappedCall(DatabaseMetaData metaData) {
680             try{
681                 String result = wrappedCall(metaData);
682                 return result;
683             }
684             catch(Exception e){
685                 logger.trace("Problem retrieving DB information via DatabaseMetaData", e);
686                 return NOT_AVAILABLE_TEXT;
687             }
688         }
689         /**
690          * Calls the method that might throw an exception to be handled
691          * @param metaData
692          * @return The result of the call as human readable string
693          * @throws Exception Any exception that might occur during the method invocation
694          */
695         public abstract String wrappedCall(DatabaseMetaData metaData) throws Exception;
696     }
697 
698 }