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&lt;DatabaseConfiguration&gt; builder =
066 *     new BasicConfigurationBuilder&lt;DatabaseConfiguration&gt;(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&lt;DatabaseConfiguration&gt; builder =
094 *     new BasicConfigurationBuilder&lt;DatabaseConfiguration&gt;(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}