    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}