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<DatabaseConfiguration> builder =
66 * new BasicConfigurationBuilder<DatabaseConfiguration>(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<DatabaseConfiguration> builder =
94 * new BasicConfigurationBuilder<DatabaseConfiguration>(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 }