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