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