001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.apache.commons.configuration2; 019 020import javax.sql.DataSource; 021import java.sql.Clob; 022import java.sql.Connection; 023import java.sql.PreparedStatement; 024import java.sql.ResultSet; 025import java.sql.SQLException; 026import java.sql.Statement; 027import java.util.ArrayList; 028import java.util.Collection; 029import java.util.Iterator; 030import java.util.List; 031 032import org.apache.commons.configuration2.convert.DisabledListDelimiterHandler; 033import org.apache.commons.configuration2.convert.ListDelimiterHandler; 034import org.apache.commons.configuration2.event.ConfigurationErrorEvent; 035import org.apache.commons.configuration2.event.ConfigurationEvent; 036import org.apache.commons.configuration2.event.EventType; 037import org.apache.commons.configuration2.io.ConfigurationLogger; 038import org.apache.commons.lang3.StringUtils; 039 040/** 041 * Configuration stored in a database. The properties are retrieved from a 042 * table containing at least one column for the keys, and one column for the 043 * values. It's possible to store several configurations in the same table by 044 * adding a column containing the name of the configuration. The name of the 045 * table and the columns have to be specified using the corresponding 046 * properties. 047 * <p> 048 * The recommended way to create an instance of {@code DatabaseConfiguration} 049 * is to use a <em>configuration builder</em>. The builder is configured with 050 * a special parameters object defining the database structures used by the 051 * configuration. Such an object can be created using the {@code database()} 052 * method of the {@code Parameters} class. See the examples below for more 053 * details. 054 * </p> 055 * 056 * <p> 057 * <strong>Example 1 - One configuration per table</strong> 058 * </p> 059 * 060 * <pre> 061 * CREATE TABLE myconfig ( 062 * `key` VARCHAR NOT NULL PRIMARY KEY, 063 * `value` VARCHAR 064 * ); 065 * 066 * INSERT INTO myconfig (key, value) VALUES ('foo', 'bar'); 067 * 068 * BasicConfigurationBuilder<DatabaseConfiguration> builder = 069 * new BasicConfigurationBuilder<DatabaseConfiguration>(DatabaseConfiguration.class); 070 * builder.configure( 071 * Parameters.database() 072 * .setDataSource(dataSource) 073 * .setTable("myconfig") 074 * .setKeyColumn("key") 075 * .setValueColumn("value") 076 * ); 077 * Configuration config = builder.getConfiguration(); 078 * String value = config.getString("foo"); 079 * </pre> 080 * 081 * <p> 082 * <strong>Example 2 - Multiple configurations per table</strong> 083 * </p> 084 * 085 * <pre> 086 * CREATE TABLE myconfigs ( 087 * `name` VARCHAR NOT NULL, 088 * `key` VARCHAR NOT NULL, 089 * `value` VARCHAR, 090 * CONSTRAINT sys_pk_myconfigs PRIMARY KEY (`name`, `key`) 091 * ); 092 * 093 * INSERT INTO myconfigs (name, key, value) VALUES ('config1', 'key1', 'value1'); 094 * INSERT INTO myconfigs (name, key, value) VALUES ('config2', 'key2', 'value2'); 095 * 096 * BasicConfigurationBuilder<DatabaseConfiguration> builder = 097 * new BasicConfigurationBuilder<DatabaseConfiguration>(DatabaseConfiguration.class); 098 * builder.configure( 099 * Parameters.database() 100 * .setDataSource(dataSource) 101 * .setTable("myconfigs") 102 * .setKeyColumn("key") 103 * .setValueColumn("value") 104 * .setConfigurationNameColumn("name") 105 * .setConfigurationName("config1") 106 * ); 107 * Configuration config1 = new DatabaseConfiguration(dataSource, "myconfigs", "name", "key", "value", "config1"); 108 * String value1 = conf.getString("key1"); 109 * </pre> 110 * The configuration can be instructed to perform commits after database updates. 111 * This is achieved by setting the {@code commits} parameter of the 112 * constructors to <b>true</b>. If commits should not be performed (which is the 113 * default behavior), it should be ensured that the connections returned by the 114 * {@code DataSource} are in auto-commit mode. 115 * 116 * <h1>Note: Like JDBC itself, protection against SQL injection is left to the user.</h1> 117 * @since 1.0 118 * 119 * @author <a href="mailto:ebourg@apache.org">Emmanuel Bourg</a> 120 * @version $Id: DatabaseConfiguration.java 1790899 2017-04-10 21:56:46Z ggregory $ 121 */ 122public class DatabaseConfiguration extends AbstractConfiguration 123{ 124 /** Constant for the statement used by getProperty.*/ 125 private static final String SQL_GET_PROPERTY = "SELECT * FROM %s WHERE %s =?"; 126 127 /** Constant for the statement used by isEmpty.*/ 128 private static final String SQL_IS_EMPTY = "SELECT count(*) FROM %s WHERE 1 = 1"; 129 130 /** Constant for the statement used by clearProperty.*/ 131 private static final String SQL_CLEAR_PROPERTY = "DELETE FROM %s WHERE %s =?"; 132 133 /** Constant for the statement used by clear.*/ 134 private static final String SQL_CLEAR = "DELETE FROM %s WHERE 1 = 1"; 135 136 /** Constant for the statement used by getKeys.*/ 137 private static final String SQL_GET_KEYS = "SELECT DISTINCT %s FROM %s WHERE 1 = 1"; 138 139 /** The data source to connect to the database. */ 140 private DataSource dataSource; 141 142 /** The configurationName of the table containing the configurations. */ 143 private String table; 144 145 /** The column containing the configurationName of the configuration. */ 146 private String configurationNameColumn; 147 148 /** The column containing the keys. */ 149 private String keyColumn; 150 151 /** The column containing the values. */ 152 private String valueColumn; 153 154 /** The configurationName of the configuration. */ 155 private String configurationName; 156 157 /** A flag whether commits should be performed by this configuration. */ 158 private boolean autoCommit; 159 160 /** 161 * Creates a new instance of {@code DatabaseConfiguration}. 162 */ 163 public DatabaseConfiguration() 164 { 165 initLogger(new ConfigurationLogger(DatabaseConfiguration.class)); 166 addErrorLogListener(); 167 } 168 169 /** 170 * Returns the {@code DataSource} for obtaining database connections. 171 * 172 * @return the {@code DataSource} 173 */ 174 public DataSource getDataSource() 175 { 176 return dataSource; 177 } 178 179 /** 180 * Sets the {@code DataSource} for obtaining database connections. 181 * 182 * @param dataSource the {@code DataSource} 183 */ 184 public void setDataSource(DataSource dataSource) 185 { 186 this.dataSource = dataSource; 187 } 188 189 /** 190 * Returns the name of the table containing configuration data. 191 * 192 * @return the name of the table to be queried 193 */ 194 public String getTable() 195 { 196 return table; 197 } 198 199 /** 200 * Sets the name of the table containing configuration data. 201 * 202 * @param table the table name 203 */ 204 public void setTable(String table) 205 { 206 this.table = table; 207 } 208 209 /** 210 * Returns the name of the table column with the configuration name. 211 * 212 * @return the name of the configuration name column 213 */ 214 public String getConfigurationNameColumn() 215 { 216 return configurationNameColumn; 217 } 218 219 /** 220 * Sets the name of the table column with the configuration name. 221 * 222 * @param configurationNameColumn the name of the column with the 223 * configuration name 224 */ 225 public void setConfigurationNameColumn(String configurationNameColumn) 226 { 227 this.configurationNameColumn = configurationNameColumn; 228 } 229 230 /** 231 * Returns the name of the column containing the configuration keys. 232 * 233 * @return the name of the key column 234 */ 235 public String getKeyColumn() 236 { 237 return keyColumn; 238 } 239 240 /** 241 * Sets the name of the column containing the configuration keys. 242 * 243 * @param keyColumn the name of the key column 244 */ 245 public void setKeyColumn(String keyColumn) 246 { 247 this.keyColumn = keyColumn; 248 } 249 250 /** 251 * Returns the name of the column containing the configuration values. 252 * 253 * @return the name of the value column 254 */ 255 public String getValueColumn() 256 { 257 return valueColumn; 258 } 259 260 /** 261 * Sets the name of the column containing the configuration values. 262 * 263 * @param valueColumn the name of the value column 264 */ 265 public void setValueColumn(String valueColumn) 266 { 267 this.valueColumn = valueColumn; 268 } 269 270 /** 271 * Returns the name of this configuration instance. 272 * 273 * @return the name of this configuration 274 */ 275 public String getConfigurationName() 276 { 277 return configurationName; 278 } 279 280 /** 281 * Sets the name of this configuration instance. 282 * 283 * @param configurationName the name of this configuration 284 */ 285 public void setConfigurationName(String configurationName) 286 { 287 this.configurationName = configurationName; 288 } 289 290 /** 291 * Returns a flag whether this configuration performs commits after database 292 * updates. 293 * 294 * @return a flag whether commits are performed 295 */ 296 public boolean isAutoCommit() 297 { 298 return autoCommit; 299 } 300 301 /** 302 * Sets the auto commit flag. If set to <b>true</b>, this configuration 303 * performs a commit after each database update. 304 * 305 * @param autoCommit the auto commit flag 306 */ 307 public void setAutoCommit(boolean autoCommit) 308 { 309 this.autoCommit = autoCommit; 310 } 311 312 /** 313 * Returns the value of the specified property. If this causes a database 314 * error, an error event will be generated of type 315 * {@code READ} with the causing exception. The 316 * event's {@code propertyName} is set to the passed in property key, 317 * the {@code propertyValue} is undefined. 318 * 319 * @param key the key of the desired property 320 * @return the value of this property 321 */ 322 @Override 323 protected Object getPropertyInternal(final String key) 324 { 325 JdbcOperation<Object> op = 326 new JdbcOperation<Object>(ConfigurationErrorEvent.READ, 327 ConfigurationErrorEvent.READ, key, null) 328 { 329 @Override 330 protected Object performOperation() throws SQLException 331 { 332 ResultSet rs = 333 openResultSet(String.format(SQL_GET_PROPERTY, 334 table, keyColumn), true, key); 335 336 List<Object> results = new ArrayList<>(); 337 while (rs.next()) 338 { 339 Object value = extractPropertyValue(rs); 340 // Split value if it contains the list delimiter 341 for (Object o : getListDelimiterHandler().parse(value)) 342 { 343 results.add(o); 344 } 345 } 346 347 if (!results.isEmpty()) 348 { 349 return (results.size() > 1) ? results : results 350 .get(0); 351 } 352 else 353 { 354 return null; 355 } 356 } 357 }; 358 359 return op.execute(); 360 } 361 362 /** 363 * Adds a property to this configuration. If this causes a database error, 364 * an error event will be generated of type {@code ADD_PROPERTY} 365 * with the causing exception. The event's {@code propertyName} is 366 * set to the passed in property key, the {@code propertyValue} 367 * points to the passed in value. 368 * 369 * @param key the property key 370 * @param obj the value of the property to add 371 */ 372 @Override 373 protected void addPropertyDirect(final String key, final Object obj) 374 { 375 new JdbcOperation<Void>(ConfigurationErrorEvent.WRITE, 376 ConfigurationEvent.ADD_PROPERTY, key, obj) 377 { 378 @Override 379 protected Void performOperation() throws SQLException 380 { 381 StringBuilder query = new StringBuilder("INSERT INTO "); 382 query.append(table).append(" ("); 383 query.append(keyColumn).append(", "); 384 query.append(valueColumn); 385 if (configurationNameColumn != null) 386 { 387 query.append(", ").append(configurationNameColumn); 388 } 389 query.append(") VALUES (?, ?"); 390 if (configurationNameColumn != null) 391 { 392 query.append(", ?"); 393 } 394 query.append(")"); 395 396 PreparedStatement pstmt = initStatement(query.toString(), 397 false, key, String.valueOf(obj)); 398 if (configurationNameColumn != null) 399 { 400 pstmt.setString(3, configurationName); 401 } 402 403 pstmt.executeUpdate(); 404 return null; 405 } 406 } 407 .execute(); 408 } 409 410 /** 411 * Adds a property to this configuration. This implementation 412 * temporarily disables list delimiter parsing, so that even if the value 413 * contains the list delimiter, only a single record is written into 414 * the managed table. The implementation of {@code getProperty()} 415 * takes care about delimiters. So list delimiters are fully supported 416 * by {@code DatabaseConfiguration}, but internally treated a bit 417 * differently. 418 * 419 * @param key the key of the new property 420 * @param value the value to be added 421 */ 422 @Override 423 protected void addPropertyInternal(String key, Object value) 424 { 425 ListDelimiterHandler oldHandler = getListDelimiterHandler(); 426 try 427 { 428 // temporarily disable delimiter parsing 429 setListDelimiterHandler(DisabledListDelimiterHandler.INSTANCE); 430 super.addPropertyInternal(key, value); 431 } 432 finally 433 { 434 setListDelimiterHandler(oldHandler); 435 } 436 } 437 438 /** 439 * Checks if this configuration is empty. If this causes a database error, 440 * an error event will be generated of type {@code READ} 441 * with the causing exception. Both the event's {@code propertyName} 442 * and {@code propertyValue} will be undefined. 443 * 444 * @return a flag whether this configuration is empty. 445 */ 446 @Override 447 protected boolean isEmptyInternal() 448 { 449 JdbcOperation<Integer> op = 450 new JdbcOperation<Integer>(ConfigurationErrorEvent.READ, 451 ConfigurationErrorEvent.READ, null, null) 452 { 453 @Override 454 protected Integer performOperation() throws SQLException 455 { 456 ResultSet rs = openResultSet(String.format( 457 SQL_IS_EMPTY, table), true); 458 459 return rs.next() ? Integer.valueOf(rs.getInt(1)) : null; 460 } 461 }; 462 463 Integer count = op.execute(); 464 return count == null || count.intValue() == 0; 465 } 466 467 /** 468 * Checks whether this configuration contains the specified key. If this 469 * causes a database error, an error event will be generated of type 470 * {@code READ} with the causing exception. The 471 * event's {@code propertyName} will be set to the passed in key, the 472 * {@code propertyValue} will be undefined. 473 * 474 * @param key the key to be checked 475 * @return a flag whether this key is defined 476 */ 477 @Override 478 protected boolean containsKeyInternal(final String key) 479 { 480 JdbcOperation<Boolean> op = 481 new JdbcOperation<Boolean>(ConfigurationErrorEvent.READ, 482 ConfigurationErrorEvent.READ, key, null) 483 { 484 @Override 485 protected Boolean performOperation() throws SQLException 486 { 487 ResultSet rs = openResultSet( 488 String.format(SQL_GET_PROPERTY, table, keyColumn), true, key); 489 490 return rs.next(); 491 } 492 }; 493 494 Boolean result = op.execute(); 495 return result != null && result.booleanValue(); 496 } 497 498 /** 499 * Removes the specified value from this configuration. If this causes a 500 * database error, an error event will be generated of type 501 * {@code CLEAR_PROPERTY} with the causing exception. The 502 * event's {@code propertyName} will be set to the passed in key, the 503 * {@code propertyValue} will be undefined. 504 * 505 * @param key the key of the property to be removed 506 */ 507 @Override 508 protected void clearPropertyDirect(final String key) 509 { 510 new JdbcOperation<Void>(ConfigurationErrorEvent.WRITE, 511 ConfigurationEvent.CLEAR_PROPERTY, key, null) 512 { 513 @Override 514 protected Void performOperation() throws SQLException 515 { 516 PreparedStatement ps = initStatement(String.format( 517 SQL_CLEAR_PROPERTY, table, keyColumn), true, key); 518 ps.executeUpdate(); 519 return null; 520 } 521 } 522 .execute(); 523 } 524 525 /** 526 * Removes all entries from this configuration. If this causes a database 527 * error, an error event will be generated of type 528 * {@code CLEAR} with the causing exception. Both the 529 * event's {@code propertyName} and the {@code propertyValue} 530 * will be undefined. 531 */ 532 @Override 533 protected void clearInternal() 534 { 535 new JdbcOperation<Void>(ConfigurationErrorEvent.WRITE, 536 ConfigurationEvent.CLEAR, null, null) 537 { 538 @Override 539 protected Void performOperation() throws SQLException 540 { 541 initStatement(String.format(SQL_CLEAR, 542 table), true).executeUpdate(); 543 return null; 544 } 545 } 546 .execute(); 547 } 548 549 /** 550 * Returns an iterator with the names of all properties contained in this 551 * configuration. If this causes a database 552 * error, an error event will be generated of type 553 * {@code READ} with the causing exception. Both the 554 * event's {@code propertyName} and the {@code propertyValue} 555 * will be undefined. 556 * @return an iterator with the contained keys (an empty iterator in case 557 * of an error) 558 */ 559 @Override 560 protected Iterator<String> getKeysInternal() 561 { 562 final Collection<String> keys = new ArrayList<>(); 563 new JdbcOperation<Collection<String>>(ConfigurationErrorEvent.READ, 564 ConfigurationErrorEvent.READ, null, null) 565 { 566 @Override 567 protected Collection<String> performOperation() throws SQLException 568 { 569 ResultSet rs = openResultSet(String.format( 570 SQL_GET_KEYS, keyColumn, table), true); 571 572 while (rs.next()) 573 { 574 keys.add(rs.getString(1)); 575 } 576 return keys; 577 } 578 } 579 .execute(); 580 581 return keys.iterator(); 582 } 583 584 /** 585 * Returns the used {@code DataSource} object. 586 * 587 * @return the data source 588 * @since 1.4 589 */ 590 public DataSource getDatasource() 591 { 592 return dataSource; 593 } 594 595 /** 596 * Close the specified database objects. 597 * Avoid closing if null and hide any SQLExceptions that occur. 598 * 599 * @param conn The database connection to close 600 * @param stmt The statement to close 601 * @param rs the result set to close 602 */ 603 protected void close(Connection conn, Statement stmt, ResultSet rs) 604 { 605 try 606 { 607 if (rs != null) 608 { 609 rs.close(); 610 } 611 } 612 catch (SQLException e) 613 { 614 getLogger().error("An error occurred on closing the result set", e); 615 } 616 617 try 618 { 619 if (stmt != null) 620 { 621 stmt.close(); 622 } 623 } 624 catch (SQLException e) 625 { 626 getLogger().error("An error occured on closing the statement", e); 627 } 628 629 try 630 { 631 if (conn != null) 632 { 633 conn.close(); 634 } 635 } 636 catch (SQLException e) 637 { 638 getLogger().error("An error occured on closing the connection", e); 639 } 640 } 641 642 /** 643 * Extracts the value of a property from the given result set. The passed in 644 * {@code ResultSet} was created by a SELECT statement on the underlying 645 * database table. This implementation reads the value of the column 646 * determined by the {@code valueColumn} property. Normally the contained 647 * value is directly returned. However, if it is of type {@code CLOB}, text 648 * is extracted as string. 649 * 650 * @param rs the current {@code ResultSet} 651 * @return the value of the property column 652 * @throws SQLException if an error occurs 653 */ 654 protected Object extractPropertyValue(ResultSet rs) throws SQLException 655 { 656 Object value = rs.getObject(valueColumn); 657 if (value instanceof Clob) 658 { 659 value = convertClob((Clob) value); 660 } 661 return value; 662 } 663 664 /** 665 * Converts a CLOB to a string. 666 * 667 * @param clob the CLOB to be converted 668 * @return the extracted string value 669 * @throws SQLException if an error occurs 670 */ 671 private static Object convertClob(Clob clob) throws SQLException 672 { 673 int len = (int) clob.length(); 674 return (len > 0) ? clob.getSubString(1, len) : StringUtils.EMPTY; 675 } 676 677 /** 678 * An internally used helper class for simplifying database access through 679 * plain JDBC. This class provides a simple framework for creating and 680 * executing a JDBC statement. It especially takes care of proper handling 681 * of JDBC resources even in case of an error. 682 * @param <T> the type of the results produced by a JDBC operation 683 */ 684 private abstract class JdbcOperation<T> 685 { 686 /** Stores the connection. */ 687 private Connection conn; 688 689 /** Stores the statement. */ 690 private PreparedStatement pstmt; 691 692 /** Stores the result set. */ 693 private ResultSet resultSet; 694 695 /** The type of the event to send in case of an error. */ 696 private final EventType<? extends ConfigurationErrorEvent> errorEventType; 697 698 /** The type of the operation which caused an error. */ 699 private final EventType<?> operationEventType; 700 701 /** The property configurationName for an error event. */ 702 private final String errorPropertyName; 703 704 /** The property value for an error event. */ 705 private final Object errorPropertyValue; 706 707 /** 708 * Creates a new instance of {@code JdbcOperation} and initializes the 709 * properties related to the error event. 710 * 711 * @param errEvType the type of the error event 712 * @param opType the operation event type 713 * @param errPropName the property configurationName for the error event 714 * @param errPropVal the property value for the error event 715 */ 716 protected JdbcOperation( 717 EventType<? extends ConfigurationErrorEvent> errEvType, 718 EventType<?> opType, String errPropName, Object errPropVal) 719 { 720 errorEventType = errEvType; 721 operationEventType = opType; 722 errorPropertyName = errPropName; 723 errorPropertyValue = errPropVal; 724 } 725 726 /** 727 * Executes this operation. This method obtains a database connection 728 * and then delegates to {@code performOperation()}. Afterwards it 729 * performs the necessary clean up. Exceptions that are thrown during 730 * the JDBC operation are caught and transformed into configuration 731 * error events. 732 * 733 * @return the result of the operation 734 */ 735 public T execute() 736 { 737 T result = null; 738 739 try 740 { 741 conn = getDatasource().getConnection(); 742 result = performOperation(); 743 744 if (isAutoCommit()) 745 { 746 conn.commit(); 747 } 748 } 749 catch (SQLException e) 750 { 751 fireError(errorEventType, operationEventType, errorPropertyName, 752 errorPropertyValue, e); 753 } 754 finally 755 { 756 close(conn, pstmt, resultSet); 757 } 758 759 return result; 760 } 761 762 /** 763 * Returns the current connection. This method can be called while 764 * {@code execute()} is running. It returns <b>null</b> otherwise. 765 * 766 * @return the current connection 767 */ 768 protected Connection getConnection() 769 { 770 return conn; 771 } 772 773 /** 774 * Creates a {@code PreparedStatement} object for executing the 775 * specified SQL statement. 776 * 777 * @param sql the statement to be executed 778 * @param nameCol a flag whether the configurationName column should be taken into 779 * account 780 * @return the prepared statement object 781 * @throws SQLException if an SQL error occurs 782 */ 783 protected PreparedStatement createStatement(String sql, boolean nameCol) 784 throws SQLException 785 { 786 String statement; 787 if (nameCol && configurationNameColumn != null) 788 { 789 StringBuilder buf = new StringBuilder(sql); 790 buf.append(" AND ").append(configurationNameColumn).append("=?"); 791 statement = buf.toString(); 792 } 793 else 794 { 795 statement = sql; 796 } 797 798 pstmt = getConnection().prepareStatement(statement); 799 return pstmt; 800 } 801 802 /** 803 * Creates an initializes a {@code PreparedStatement} object for 804 * executing an SQL statement. This method first calls 805 * {@code createStatement()} for creating the statement and then 806 * initializes the statement's parameters. 807 * 808 * @param sql the statement to be executed 809 * @param nameCol a flag whether the configurationName column should be taken into 810 * account 811 * @param params the parameters for the statement 812 * @return the initialized statement object 813 * @throws SQLException if an SQL error occurs 814 */ 815 protected PreparedStatement initStatement(String sql, boolean nameCol, 816 Object... params) throws SQLException 817 { 818 PreparedStatement ps = createStatement(sql, nameCol); 819 820 int idx = 1; 821 for (Object param : params) 822 { 823 ps.setObject(idx++, param); 824 } 825 if (nameCol && configurationNameColumn != null) 826 { 827 ps.setString(idx, configurationName); 828 } 829 830 return ps; 831 } 832 833 /** 834 * Creates a {@code PreparedStatement} for a query, initializes it and 835 * executes it. The resulting {@code ResultSet} is returned. 836 * 837 * @param sql the statement to be executed 838 * @param nameCol a flag whether the configurationName column should be taken into 839 * account 840 * @param params the parameters for the statement 841 * @return the {@code ResultSet} produced by the query 842 * @throws SQLException if an SQL error occurs 843 */ 844 protected ResultSet openResultSet(String sql, boolean nameCol, 845 Object... params) throws SQLException 846 { 847 resultSet = initStatement(sql, nameCol, params).executeQuery(); 848 return resultSet; 849 } 850 851 /** 852 * Performs the JDBC operation. This method is called by 853 * {@code execute()} after this object has been fully initialized. 854 * Here the actual JDBC logic has to be placed. 855 * 856 * @return the result of the operation 857 * @throws SQLException if an SQL error occurs 858 */ 859 protected abstract T performOperation() throws SQLException; 860 } 861}