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