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