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: 1134 $
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 StringBuffer tableInfo = new StringBuffer();
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 StringBuffer sb = new StringBuffer();
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 // This is only available since Java 5 - so we ca try it and if it does not work default it
402 String isAutoIncrement = Column.AutoIncrement.NO.getKey();
403 try {
404 isAutoIncrement = resultSet.getString(23);
405 }
406 catch(SQLException e){
407 if(logger.isDebugEnabled())
408 logger.debug("Could not retrieve the 'isAutoIncrement' property because not yet running on Java 1.5 - defaulting to NO. " +
409 "Table=" + tableName + ", Column=" +columnName, e);
410 // Ignore this one here
411 }
412
413 // Convert SQL type to DataType
414 DataType dataType =
415 dataTypeFactory.createDataType(sqlType, sqlTypeName, tableName, columnName);
416 if (dataType != DataType.UNKNOWN)
417 {
418 Column column = new Column(columnName, dataType,
419 sqlTypeName, Column.nullableValue(nullable), columnDefaultValue, remarks,
420 Column.AutoIncrement.autoIncrementValue(isAutoIncrement));
421 return column;
422 }
423 else
424 {
425 if (datatypeWarning)
426 logger.warn(
427 tableName + "." + columnName +
428 " data type (" + sqlType + ", '" + sqlTypeName +
429 "') not recognized and will be ignored. See FAQ for more information.");
430
431 // datatype unknown - column not created
432 return null;
433 }
434 }
435
436 /**
437 * Checks if the given <code>resultSet</code> matches the given schema and table name.
438 * The comparison is <b>case sensitive</b>.
439 * @param resultSet A result set produced via {@link DatabaseMetaData#getColumns(String, String, String, String)}
440 * @param schema The name of the schema to check. If <code>null</code> it is ignored in the comparison
441 * @param table The name of the table to check. If <code>null</code> it is ignored in the comparison
442 * @param caseSensitive Whether or not the comparison should be case sensitive or not
443 * @return <code>true</code> if the column metadata of the given <code>resultSet</code> matches
444 * the given schema and table parameters.
445 * @throws SQLException
446 * @since 2.4.0
447 * @deprecated since 2.4.4 - use {@link IMetadataHandler#matches(ResultSet, String, String, String, String, boolean)}
448 */
449 public static boolean matches(ResultSet resultSet,
450 String schema, String table, boolean caseSensitive)
451 throws SQLException
452 {
453 return matches(resultSet, null, schema, table, null, caseSensitive);
454 }
455
456
457 /**
458 * Checks if the given <code>resultSet</code> matches the given schema and table name.
459 * The comparison is <b>case sensitive</b>.
460 * @param resultSet A result set produced via {@link DatabaseMetaData#getColumns(String, String, String, String)}
461 * @param catalog The name of the catalog to check. If <code>null</code> it is ignored in the comparison
462 * @param schema The name of the schema to check. If <code>null</code> it is ignored in the comparison
463 * @param table The name of the table to check. If <code>null</code> it is ignored in the comparison
464 * @param column The name of the column to check. If <code>null</code> it is ignored in the comparison
465 * @param caseSensitive Whether or not the comparison should be case sensitive or not
466 * @return <code>true</code> if the column metadata of the given <code>resultSet</code> matches
467 * the given schema and table parameters.
468 * @throws SQLException
469 * @since 2.4.0
470 * @deprecated since 2.4.4 - use {@link IMetadataHandler#matches(ResultSet, String, String, String, String, boolean)}
471 */
472 public static boolean matches(ResultSet resultSet,
473 String catalog, String schema,
474 String table, String column, boolean caseSensitive)
475 throws SQLException
476 {
477 String catalogName = resultSet.getString(1);
478 String schemaName = resultSet.getString(2);
479 String tableName = resultSet.getString(3);
480 String columnName = resultSet.getString(4);
481
482 // MYSQL provides only a catalog but no schema
483 if(schema != null && schemaName == null && catalog==null && catalogName != null){
484 logger.debug("Switching catalog/schema because the are mutually null");
485 schemaName = catalogName;
486 catalogName = null;
487 }
488
489 boolean areEqual =
490 areEqualIgnoreNull(catalog, catalogName, caseSensitive) &&
491 areEqualIgnoreNull(schema, schemaName, caseSensitive) &&
492 areEqualIgnoreNull(table, tableName, caseSensitive) &&
493 areEqualIgnoreNull(column, columnName, caseSensitive);
494 return areEqual;
495 }
496
497 /**
498 * Compares the given values and returns true if they are equal.
499 * If the first value is <code>null</code> or empty String it always
500 * returns <code>true</code> which is the way of ignoring <code>null</code>s
501 * for this specific case.
502 * @param value1 The first value to compare. Is ignored if null or empty String
503 * @param value2 The second value to be compared
504 * @return <code>true</code> if both values are equal or if the first value
505 * is <code>null</code> or empty string.
506 * @since 2.4.4
507 */
508 public static final boolean areEqualIgnoreNull(String value1, String value2, boolean caseSensitive)
509 {
510 if(value1==null || value1.equals(""))
511 {
512 return true;
513 }
514 else
515 {
516 if(caseSensitive && value1.equals(value2))
517 {
518 return true;
519 }
520 else if(!caseSensitive && value1.equalsIgnoreCase(value2))
521 {
522 return true;
523 }
524 else
525 {
526 return false;
527 }
528 }
529 }
530
531 /**
532 * Corrects the case of the given String according to the way in which the database stores metadata.
533 * @param databaseIdentifier A database identifier such as a table name or a schema name for
534 * which the case should be corrected.
535 * @param connection The connection used to lookup the database metadata. This is needed to determine
536 * the way in which the database stores its metadata.
537 * @return The database identifier in the correct case for the RDBMS
538 * @since 2.4.4
539 */
540 public static final String correctCase(final String databaseIdentifier, Connection connection)
541 {
542 logger.trace("correctCase(tableName={}, connection={}) - start", databaseIdentifier, connection);
543
544 try
545 {
546 return correctCase(databaseIdentifier, connection.getMetaData());
547 }
548 catch (SQLException e)
549 {
550 throw new DatabaseUnitRuntimeException("Exception while trying to access database metadata", e);
551 }
552 }
553
554 /**
555 * Corrects the case of the given String according to the way in which the database stores metadata.
556 * @param databaseIdentifier A database identifier such as a table name or a schema name for
557 * which the case should be corrected.
558 * @param databaseMetaData The database metadata needed to determine the way in which the database stores
559 * its metadata.
560 * @return The database identifier in the correct case for the RDBMS
561 * @since 2.4.4
562 */
563 public static final String correctCase(final String databaseIdentifier, DatabaseMetaData databaseMetaData)
564 {
565 logger.trace("correctCase(tableName={}, databaseMetaData={}) - start", databaseIdentifier, databaseMetaData);
566
567 if (databaseIdentifier == null) {
568 throw new NullPointerException(
569 "The parameter 'databaseIdentifier' must not be null");
570 }
571 if (databaseMetaData == null) {
572 throw new NullPointerException(
573 "The parameter 'databaseMetaData' must not be null");
574 }
575
576 try {
577 String resultTableName = databaseIdentifier;
578 String dbIdentifierQuoteString = databaseMetaData.getIdentifierQuoteString();
579 if(!isEscaped(databaseIdentifier, dbIdentifierQuoteString)){
580 if(databaseMetaData.storesLowerCaseIdentifiers())
581 {
582 resultTableName = databaseIdentifier.toLowerCase(Locale.ENGLISH);
583 }
584 else if(databaseMetaData.storesUpperCaseIdentifiers())
585 {
586 resultTableName = databaseIdentifier.toUpperCase(Locale.ENGLISH);
587 }
588 else
589 {
590 logger.debug("Database does not store upperCase or lowerCase identifiers. " +
591 "Will not correct case of the table names.");
592 }
593 }
594 else
595 {
596 if(logger.isDebugEnabled())
597 logger.debug("The tableName '{}' is escaped. Will not correct case.", databaseIdentifier);
598 }
599 return resultTableName;
600 }
601 catch (SQLException e)
602 {
603 throw new DatabaseUnitRuntimeException("Exception while trying to access database metadata", e);
604 }
605 }
606
607 /**
608 * Checks whether two given values are unequal and if so print a log message (level DEBUG)
609 * @param oldValue The old value of a property
610 * @param newValue The new value of a property
611 * @param message The message to be logged
612 * @param source The class which invokes this method - used for enriching the log message
613 * @since 2.4.4
614 */
615 public static final void logInfoIfValueChanged(String oldValue, String newValue, String message, Class source)
616 {
617 if(logger.isInfoEnabled())
618 {
619 if(oldValue != null && !oldValue.equals(newValue))
620 logger.debug("{}. {} oldValue={} newValue={}", new Object[] {source, message, oldValue, newValue});
621 }
622 }
623
624 /**
625 * Checks whether two given values are unequal and if so print a log message (level DEBUG)
626 * @param oldValue The old value of a property
627 * @param newValue The new value of a property
628 * @param message The message to be logged
629 * @param source The class which invokes this method - used for enriching the log message
630 * @since 2.4.8
631 */
632 public static final void logDebugIfValueChanged(String oldValue, String newValue, String message, Class source)
633 {
634 if (logger.isDebugEnabled())
635 {
636 if (oldValue != null && !oldValue.equals(newValue))
637 logger.debug("{}. {} oldValue={} newValue={}", new Object[] {source, message, oldValue, newValue});
638 }
639 }
640
641 /**
642 * @param tableName
643 * @param dbIdentifierQuoteString
644 * @return
645 * @since 2.4.4
646 */
647 private static final boolean isEscaped(String tableName, String dbIdentifierQuoteString)
648 {
649 logger.trace("isEscaped(tableName={}, dbIdentifierQuoteString={}) - start", tableName, dbIdentifierQuoteString);
650
651 if (dbIdentifierQuoteString == null) {
652 throw new NullPointerException(
653 "The parameter 'dbIdentifierQuoteString' must not be null");
654 }
655 boolean isEscaped = tableName!=null && (tableName.startsWith(dbIdentifierQuoteString));
656 if(logger.isDebugEnabled())
657 logger.debug("isEscaped returns '{}' for tableName={} (dbIdentifierQuoteString={})",
658 new Object[]{Boolean.valueOf(isEscaped), tableName, dbIdentifierQuoteString} );
659 return isEscaped;
660 }
661
662
663 /**
664 * Performs a method invocation and catches all exceptions that occur during the invocation.
665 * Utility which works similar to a closure, just a bit less elegant.
666 * @author gommma (gommma AT users.sourceforge.net)
667 * @author Last changed by: $Author: jbhurst $
668 * @version $Revision: 1134 $ $Date: 2010-01-07 21:32:54 +0100 (gio, 07 gen 2010) $
669 * @since 2.4.6
670 */
671 static abstract class ExceptionWrapper{
672
673 public static final String NOT_AVAILABLE_TEXT = "<not available>";
674
675 /**
676 * Default constructor
677 */
678 public ExceptionWrapper()
679 {
680 }
681
682 /**
683 * Executes the call and catches all exception that might occur.
684 * @param metaData
685 * @return The result of the call
686 */
687 public final String executeWrappedCall(DatabaseMetaData metaData) {
688 try{
689 String result = wrappedCall(metaData);
690 return result;
691 }
692 catch(Exception e){
693 logger.trace("Problem retrieving DB information via DatabaseMetaData", e);
694 return NOT_AVAILABLE_TEXT;
695 }
696 }
697 /**
698 * Calls the method that might throw an exception to be handled
699 * @param metaData
700 * @return The result of the call as human readable string
701 * @throws Exception Any exception that might occur during the method invocation
702 */
703 public abstract String wrappedCall(DatabaseMetaData metaData) throws Exception;
704 }
705
706 }