001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.configuration2;
019
020import 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 <b>true</b>. 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     * 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 <b>null</b>
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 <b>true</b>, 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}