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    *     https://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     /**
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 }