    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *     https://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017
    018package org.apache.commons.configuration2;
    019
    020import java.sql.Clob;
    021import java.sql.Connection;
    022import java.sql.PreparedStatement;
    023import java.sql.ResultSet;
    024import java.sql.SQLException;
    025import java.sql.Statement;
    026import java.util.ArrayList;
    027import java.util.Collection;
    028import java.util.Iterator;
    029import java.util.List;
    030
    031import javax.sql.DataSource;
    032
    033import org.apache.commons.configuration2.convert.DisabledListDelimiterHandler;
    034import org.apache.commons.configuration2.convert.ListDelimiterHandler;
    035import org.apache.commons.configuration2.event.ConfigurationErrorEvent;
    036import org.apache.commons.configuration2.event.ConfigurationEvent;
    037import org.apache.commons.configuration2.event.EventType;
    038import org.apache.commons.configuration2.io.ConfigurationLogger;
    039import org.apache.commons.lang3.StringUtils;
    040
    041/**
    042 * Configuration stored in a database. The properties are retrieved from a table containing at least one column for the
    043 * keys, and one column for the values. It's possible to store several configurations in the same table by adding a
    044 * column containing the name of the configuration. The name of the table and the columns have to be specified using the
    045 * corresponding properties.
    046 * <p>
    047 * The recommended way to create an instance of {@code DatabaseConfiguration} is to use a <em>configuration
    048 * builder</em>. The builder is configured with a special parameters object defining the database structures used by the
    049 * configuration. Such an object can be created using the {@code database()} method of the {@code Parameters} class. See
    050 * the examples below for more details.
    051 * </p>
    052 *
    053 * <p>
    054 * <strong>Example 1 - One configuration per table</strong>
    055 * </p>
    056 *
    057 * <pre>
    058 * CREATE TABLE myconfig (
    059 *     `key`   VARCHAR NOT NULL PRIMARY KEY,
    060 *     `value` VARCHAR
    061 * );
    062 *
    063 * INSERT INTO myconfig (key, value) VALUES ('foo', 'bar');
    064 *
    065 * BasicConfigurationBuilder&lt;DatabaseConfiguration&gt; builder =
    066 *     new BasicConfigurationBuilder&lt;DatabaseConfiguration&gt;(DatabaseConfiguration.class);
    067 * builder.configure(
    068 *     Parameters.database()
    069 *         .setDataSource(dataSource)
    070 *         .setTable("myconfig")
    071 *         .setKeyColumn("key")
    072 *         .setValueColumn("value")
    073 * );
    074 * Configuration config = builder.getConfiguration();
    075 * String value = config.getString("foo");
    076 * </pre>
    077 *
    078 * <p>
    079 * <strong>Example 2 - Multiple configurations per table</strong>
    080 * </p>
    081 *
    082 * <pre>
    083 * CREATE TABLE myconfigs (
    084 *     `name`  VARCHAR NOT NULL,
    085 *     `key`   VARCHAR NOT NULL,
    086 *     `value` VARCHAR,
    087 *     CONSTRAINT sys_pk_myconfigs PRIMARY KEY (`name`, `key`)
    088 * );
    089 *
    090 * INSERT INTO myconfigs (name, key, value) VALUES ('config1', 'key1', 'value1');
    091 * INSERT INTO myconfigs (name, key, value) VALUES ('config2', 'key2', 'value2');
    092 *
    093 * BasicConfigurationBuilder&lt;DatabaseConfiguration&gt; builder =
    094 *     new BasicConfigurationBuilder&lt;DatabaseConfiguration&gt;(DatabaseConfiguration.class);
    095 * builder.configure(
    096 *     Parameters.database()
    097 *         .setDataSource(dataSource)
    098 *         .setTable("myconfigs")
    099 *         .setKeyColumn("key")
    100 *         .setValueColumn("value")
    101 *         .setConfigurationNameColumn("name")
    102 *         .setConfigurationName("config1")
    103 * );
    104 * Configuration config1 = new DatabaseConfiguration(dataSource, "myconfigs", "name", "key", "value", "config1");
    105 * String value1 = conf.getString("key1");
    106 * </pre>
    107 *
    108 * The configuration can be instructed to perform commits after database updates. This is achieved by setting the
    109 * {@code commits} parameter of the constructors to <strong>true</strong>. If commits should not be performed (which is the
    110 * default behavior), it should be ensured that the connections returned by the {@code DataSource} are in auto-commit
    111 * mode.
    112 * <p>
    113 * <strong>Note: Like JDBC itself, protection against SQL injection is left to the user.</strong>
    114 * </p>
    115 *
    116 * @since 1.0
    117 */
    118public class DatabaseConfiguration extends AbstractConfiguration {
    119
    120    /**
    121     * An internally used helper class for simplifying database access through plain JDBC. This class provides a simple
    122     * framework for creating and executing a JDBC statement. It especially takes care of proper handling of JDBC resources
    123     * even in case of an error.
    124     *
    125     * @param <T> the type of the results produced by a JDBC operation
    126     */
    127    private abstract class AbstractJdbcOperation<T> {
    128
    129        /** Stores the connection. */
    130        private Connection connection;
    131
    132        /** Stores the statement. */
    133        private PreparedStatement preparedStatement;
    134
    135        /** Stores the result set. */
    136        private ResultSet resultSet;
    137
    138        /** The type of the event to send in case of an error. */
    139        private final EventType<? extends ConfigurationErrorEvent> errorEventType;
    140
    141        /** The type of the operation which caused an error. */
    142        private final EventType<?> operationEventType;
    143
    144        /** The property configurationName for an error event. */
    145        private final String errorPropertyName;
    146
    147        /** The property value for an error event. */
    148        private final Object errorPropertyValue;
    149
    150        /**
    151         * Creates a new instance of {@code JdbcOperation} and initializes the properties related to the error event.
    152         *
    153         * @param errEvType the type of the error event
    154         * @param opType the operation event type
    155         * @param errPropName the property configurationName for the error event
    156         * @param errPropVal the property value for the error event
    157         */
    158        protected AbstractJdbcOperation(final EventType<? extends ConfigurationErrorEvent> errEvType, final EventType<?> opType, final String errPropName,
    159            final Object errPropVal) {
    160            errorEventType = errEvType;
    161            operationEventType = opType;
    162            errorPropertyName = errPropName;
    163            errorPropertyValue = errPropVal;
    164        }
    165
    166        /**
    167         * Creates a {@code PreparedStatement} object for executing the specified SQL statement.
    168         *
    169         * @param sql the statement to be executed
    170         * @param nameCol a flag whether the configurationName column should be taken into account
    171         * @return the prepared statement object
    172         * @throws SQLException if an SQL error occurs
    173         */
    174        protected PreparedStatement createStatement(final String sql, final boolean nameCol) throws SQLException {
    175            final String statement;
    176            if (nameCol && configurationNameColumn != null) {
    177                final StringBuilder buf = new StringBuilder(sql);
    178                buf.append(" AND ").append(configurationNameColumn).append("=?");
    179                statement = buf.toString();
    180            } else {
    181                statement = sql;
    182            }
    183
    184            preparedStatement = getConnection().prepareStatement(statement);
    185            return preparedStatement;
    186        }
    187
    188        /**
    189         * Executes this operation. This method obtains a database connection and then delegates to {@code performOperation()}.
    190         * Afterwards it performs the necessary clean up. Exceptions that are thrown during the JDBC operation are caught and
    191         * transformed into configuration error events.
    192         *
    193         * @return the result of the operation
    194         */
    195        public T execute() {
    196            T result = null;
    197
    198            if (getDataSource() != null) {
    199                try {
    200                    connection = getDataSource().getConnection();
    201                    result = performOperation();
    202
    203                    if (isAutoCommit()) {
    204                        connection.commit();
    205                    }
    206                } catch (final SQLException e) {
    207                    fireError(errorEventType, operationEventType, errorPropertyName, errorPropertyValue, e);
    208                } finally {
    209                    close(connection, preparedStatement, resultSet);
    210                }
    211            }
    212
    213            return result;
    214        }
    215
    216        /**
    217         * Gets the current connection. This method can be called while {@code execute()} is running. It returns <strong>null</strong>
    218         * otherwise.
    219         *
    220         * @return the current connection
    221         */
    222        protected Connection getConnection() {
    223            return connection;
    224        }
    225
    226        /**
    227         * Creates an initializes a {@code PreparedStatement} object for executing an SQL statement. This method first calls
    228         * {@code createStatement()} for creating the statement and then initializes the statement's parameters.
    229         *
    230         * @param sql the statement to be executed
    231         * @param nameCol a flag whether the configurationName column should be taken into account
    232         * @param params the parameters for the statement
    233         * @return the initialized statement object
    234         * @throws SQLException if an SQL error occurs
    235         */
    236        protected PreparedStatement initStatement(final String sql, final boolean nameCol, final Object... params) throws SQLException {
    237            final PreparedStatement ps = createStatement(sql, nameCol);
    238
    239            int idx = 1;
    240            for (final Object param : params) {
    241                ps.setObject(idx++, param);
    242            }
    243            if (nameCol && configurationNameColumn != null) {
    244                ps.setString(idx, configurationName);
    245            }
    246
    247            return ps;
    248        }
    249
    250        /**
    251         * Creates a {@code PreparedStatement} for a query, initializes it and executes it. The resulting {@code ResultSet} is
    252         * returned.
    253         *
    254         * @param sql the statement to be executed
    255         * @param nameCol a flag whether the configurationName column should be taken into account
    256         * @param params the parameters for the statement
    257         * @return the {@code ResultSet} produced by the query
    258         * @throws SQLException if an SQL error occurs
    259         */
    260        protected ResultSet openResultSet(final String sql, final boolean nameCol, final Object... params) throws SQLException {
    261            return resultSet = initStatement(sql, nameCol, params).executeQuery();
    262        }
    263
    264        /**
    265         * Performs the JDBC operation. This method is called by {@code execute()} after this object has been fully initialized.
    266         * Here the actual JDBC logic has to be placed.
    267         *
    268         * @return the result of the operation
    269         * @throws SQLException if an SQL error occurs
    270         */
    271        protected abstract T performOperation() throws SQLException;
    272    }
    273
    274    /** Constant for the statement used by getProperty. */
    275    private static final String SQL_GET_PROPERTY = "SELECT * FROM %s WHERE %s =?";
    276
    277    /** Constant for the statement used by isEmpty. */
    278    private static final String SQL_IS_EMPTY = "SELECT count(*) FROM %s WHERE 1 = 1";
    279
    280    /** Constant for the statement used by clearProperty. */
    281    private static final String SQL_CLEAR_PROPERTY = "DELETE FROM %s WHERE %s =?";
    282
    283    /** Constant for the statement used by clear. */
    284    private static final String SQL_CLEAR = "DELETE FROM %s WHERE 1 = 1";
    285
    286    /** Constant for the statement used by getKeys. */
    287    private static final String SQL_GET_KEYS = "SELECT DISTINCT %s FROM %s WHERE 1 = 1";
    288
    289    /**
    290     * Converts a CLOB to a string.
    291     *
    292     * @param clob the CLOB to be converted
    293     * @return the extracted string value
    294     * @throws SQLException if an error occurs
    295     */
    296    private static Object convertClob(final Clob clob) throws SQLException {
    297        final int len = (int) clob.length();
    298        return len > 0 ? clob.getSubString(1, len) : StringUtils.EMPTY;
    299    }
    300
    301    /** The data source to connect to the database. */
    302    private DataSource dataSource;
    303
    304    /** The configurationName of the table containing the configurations. */
    305    private String table;
    306
    307    /** The column containing the configurationName of the configuration. */
    308    private String configurationNameColumn;
    309
    310    /** The column containing the keys. */
    311    private String keyColumn;
    312
    313    /** The column containing the values. */
    314    private String valueColumn;
    315
    316    /** The configurationName of the configuration. */
    317    private String configurationName;
    318
    319    /** A flag whether commits should be performed by this configuration. */
    320    private boolean autoCommit;
    321
    322    /**
    323     * Creates a new instance of {@code DatabaseConfiguration}.
    324     */
    325    public DatabaseConfiguration() {
    326        initLogger(new ConfigurationLogger(DatabaseConfiguration.class));
    327        addErrorLogListener();
    328    }
    329
    330    /**
    331     * Adds a property to this configuration. If this causes a database error, an error event will be generated of type
    332     * {@code ADD_PROPERTY} with the causing exception. The event's {@code propertyName} is set to the passed in property
    333     * key, the {@code propertyValue} points to the passed in value.
    334     *
    335     * @param key the property key
    336     * @param obj the value of the property to add
    337     */
    338    @Override
    339    protected void addPropertyDirect(final String key, final Object obj) {
    340        new AbstractJdbcOperation<Void>(ConfigurationErrorEvent.WRITE, ConfigurationEvent.ADD_PROPERTY, key, obj) {
    341            @Override
    342            protected Void performOperation() throws SQLException {
    343                final StringBuilder query = new StringBuilder("INSERT INTO ");
    344                query.append(table).append(" (");
    345                query.append(keyColumn).append(", ");
    346                query.append(valueColumn);
    347                if (configurationNameColumn != null) {
    348                    query.append(", ").append(configurationNameColumn);
    349                }
    350                query.append(") VALUES (?, ?");
    351                if (configurationNameColumn != null) {
    352                    query.append(", ?");
    353                }
    354                query.append(")");
    355
    356                try (PreparedStatement pstmt = initStatement(query.toString(), false, key, String.valueOf(obj))) {
    357                    if (configurationNameColumn != null) {
    358                        pstmt.setString(3, configurationName);
    359                    }
    360
    361                    pstmt.executeUpdate();
    362                    return null;
    363                }
    364            }
    365        }.execute();
    366    }
    367
    368    /**
    369     * Adds a property to this configuration. This implementation temporarily disables list delimiter parsing, so that even
    370     * if the value contains the list delimiter, only a single record is written into the managed table. The implementation
    371     * of {@code getProperty()} takes care about delimiters. So list delimiters are fully supported by
    372     * {@code DatabaseConfiguration}, but internally treated a bit differently.
    373     *
    374     * @param key the key of the new property
    375     * @param value the value to be added
    376     */
    377    @Override
    378    protected void addPropertyInternal(final String key, final Object value) {
    379        final ListDelimiterHandler oldHandler = getListDelimiterHandler();
    380        try {
    381            // temporarily disable delimiter parsing
    382            setListDelimiterHandler(DisabledListDelimiterHandler.INSTANCE);
    383            super.addPropertyInternal(key, value);
    384        } finally {
    385            setListDelimiterHandler(oldHandler);
    386        }
    387    }
    388
    389    /**
    390     * Removes all entries from this configuration. If this causes a database error, an error event will be generated of
    391     * type {@code CLEAR} with the causing exception. Both the event's {@code propertyName} and the {@code propertyValue}
    392     * will be undefined.
    393     */
    394    @Override
    395    protected void clearInternal() {
    396        new AbstractJdbcOperation<Void>(ConfigurationErrorEvent.WRITE, ConfigurationEvent.CLEAR, null, null) {
    397            @Override
    398            protected Void performOperation() throws SQLException {
    399                try (PreparedStatement statement = initStatement(String.format(SQL_CLEAR, table), true)) {
    400                    statement.executeUpdate();
    401                }
    402                return null;
    403            }
    404        }.execute();
    405    }
    406
    407    /**
    408     * Removes the specified value from this configuration. If this causes a database error, an error event will be
    409     * generated of type {@code CLEAR_PROPERTY} with the causing exception. The event's {@code propertyName} will be set to
    410     * the passed in key, the {@code propertyValue} will be undefined.
    411     *
    412     * @param key the key of the property to be removed
    413     */
    414    @Override
    415    protected void clearPropertyDirect(final String key) {
    416        new AbstractJdbcOperation<Void>(ConfigurationErrorEvent.WRITE, ConfigurationEvent.CLEAR_PROPERTY, key, null) {
    417            @Override
    418            protected Void performOperation() throws SQLException {
    419                try (PreparedStatement ps = initStatement(String.format(SQL_CLEAR_PROPERTY, table, keyColumn), true, key)) {
    420                    ps.executeUpdate();
    421                    return null;
    422                }
    423            }
    424        }.execute();
    425    }
    426
    427    /**
    428     * Close the specified database objects. Avoid closing if null and hide any SQLExceptions that occur.
    429     *
    430     * @param conn The database connection to close
    431     * @param stmt The statement to close
    432     * @param rs the result set to close
    433     */
    434    protected void close(final Connection conn, final Statement stmt, final ResultSet rs) {
    435        try {
    436            if (rs != null) {
    437                rs.close();
    438            }
    439        } catch (final SQLException e) {
    440            getLogger().error("An error occurred on closing the result set", e);
    441        }
    442
    443        try {
    444            if (stmt != null) {
    445                stmt.close();
    446            }
    447        } catch (final SQLException e) {
    448            getLogger().error("An error occurred on closing the statement", e);
    449        }
    450
    451        try {
    452            if (conn != null) {
    453                conn.close();
    454            }
    455        } catch (final SQLException e) {
    456            getLogger().error("An error occurred on closing the connection", e);
    457        }
    458    }
    459
    460    /**
    461     * Checks whether this configuration contains the specified key. If this causes a database error, an error event will be
    462     * generated of type {@code READ} with the causing exception. The event's {@code propertyName} will be set to the passed
    463     * in key, the {@code propertyValue} will be undefined.
    464     *
    465     * @param key the key to be checked
    466     * @return a flag whether this key is defined
    467     */
    468    @Override
    469    protected boolean containsKeyInternal(final String key) {
    470        final AbstractJdbcOperation<Boolean> op = new AbstractJdbcOperation<Boolean>(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, key, null) {
    471            @Override
    472            protected Boolean performOperation() throws SQLException {
    473                try (ResultSet rs = openResultSet(String.format(SQL_GET_PROPERTY, table, keyColumn), true, key)) {
    474                    return rs.next();
    475                }
    476            }
    477        };
    478
    479        final Boolean result = op.execute();
    480        return result != null && result.booleanValue();
    481    }
    482
    483    /**
    484     * Tests whether this configuration contains one or more matches to this value. This operation stops at first
    485     * match but may be more expensive than the containsKey method.
    486     *
    487     * @since 2.11.0
    488     */
    489    @Override
    490    protected boolean containsValueInternal(final Object value) {
    491        final AbstractJdbcOperation<Boolean> op = new AbstractJdbcOperation<Boolean>(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, null, value) {
    492            @Override
    493            protected Boolean performOperation() throws SQLException {
    494                try (ResultSet rs = openResultSet(String.format(SQL_GET_PROPERTY, table, valueColumn), false, value)) {
    495                    return rs.next();
    496                }
    497            }
    498        };
    499        final Boolean result = op.execute();
    500        return result != null && result.booleanValue();
    501    }
    502
    503    /**
    504     * Extracts the value of a property from the given result set. The passed in {@code ResultSet} was created by a SELECT
    505     * statement on the underlying database table. This implementation reads the value of the column determined by the
    506     * {@code valueColumn} property. Normally the contained value is directly returned. However, if it is of type
    507     * {@code CLOB}, text is extracted as string.
    508     *
    509     * @param rs the current {@code ResultSet}
    510     * @return the value of the property column
    511     * @throws SQLException if an error occurs
    512     */
    513    protected Object extractPropertyValue(final ResultSet rs) throws SQLException {
    514        Object value = rs.getObject(valueColumn);
    515        if (value instanceof Clob) {
    516            value = convertClob((Clob) value);
    517        }
    518        return value;
    519    }
    520
    521    /**
    522     * Gets the name of this configuration instance.
    523     *
    524     * @return the name of this configuration
    525     */
    526    public String getConfigurationName() {
    527        return configurationName;
    528    }
    529
    530    /**
    531     * Gets the name of the table column with the configuration name.
    532     *
    533     * @return the name of the configuration name column
    534     */
    535    public String getConfigurationNameColumn() {
    536        return configurationNameColumn;
    537    }
    538
    539    /**
    540     * Gets the used {@code DataSource} object.
    541     *
    542     * @return the data source
    543     * @since 1.4
    544     * @deprecated Use {@link #getDataSource()}
    545     */
    546    @Deprecated
    547    public DataSource getDatasource() {
    548        return dataSource;
    549    }
    550
    551    /**
    552     * Gets the {@code DataSource} for obtaining database connections.
    553     *
    554     * @return the {@code DataSource}
    555     */
    556    public DataSource getDataSource() {
    557        return dataSource;
    558    }
    559
    560    /**
    561     * Gets the name of the column containing the configuration keys.
    562     *
    563     * @return the name of the key column
    564     */
    565    public String getKeyColumn() {
    566        return keyColumn;
    567    }
    568
    569    /**
    570     * Returns an iterator with the names of all properties contained in this configuration. If this causes a database
    571     * error, an error event will be generated of type {@code READ} with the causing exception. Both the event's
    572     * {@code propertyName} and the {@code propertyValue} will be undefined.
    573     *
    574     * @return an iterator with the contained keys (an empty iterator in case of an error)
    575     */
    576    @Override
    577    protected Iterator<String> getKeysInternal() {
    578        final Collection<String> keys = new ArrayList<>();
    579        new AbstractJdbcOperation<Collection<String>>(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, null, null) {
    580            @Override
    581            protected Collection<String> performOperation() throws SQLException {
    582                try (ResultSet rs = openResultSet(String.format(SQL_GET_KEYS, keyColumn, table), true)) {
    583                    while (rs.next()) {
    584                        keys.add(rs.getString(1));
    585                    }
    586                    return keys;
    587                }
    588            }
    589        }.execute();
    590
    591        return keys.iterator();
    592    }
    593
    594    /**
    595     * Gets the value of the specified property. If this causes a database error, an error event will be generated of
    596     * type {@code READ} with the causing exception. The event's {@code propertyName} is set to the passed in property key,
    597     * the {@code propertyValue} is undefined.
    598     *
    599     * @param key the key of the desired property
    600     * @return the value of this property
    601     */
    602    @Override
    603    protected Object getPropertyInternal(final String key) {
    604        final AbstractJdbcOperation<Object> op = new AbstractJdbcOperation<Object>(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, key, null) {
    605            @Override
    606            protected Object performOperation() throws SQLException {
    607                final List<Object> results = new ArrayList<>();
    608                try (ResultSet rs = openResultSet(String.format(SQL_GET_PROPERTY, table, keyColumn), true, key)) {
    609                    while (rs.next()) {
    610                        // Split value if it contains the list delimiter
    611                        getListDelimiterHandler().parse(extractPropertyValue(rs)).forEach(results::add);
    612                    }
    613                }
    614                if (!results.isEmpty()) {
    615                    return results.size() > 1 ? results : results.get(0);
    616                }
    617                return null;
    618            }
    619        };
    620
    621        return op.execute();
    622    }
    623
    624    /**
    625     * Gets the name of the table containing configuration data.
    626     *
    627     * @return the name of the table to be queried
    628     */
    629    public String getTable() {
    630        return table;
    631    }
    632
    633    /**
    634     * Gets the name of the column containing the configuration values.
    635     *
    636     * @return the name of the value column
    637     */
    638    public String getValueColumn() {
    639        return valueColumn;
    640    }
    641
    642    /**
    643     * Returns a flag whether this configuration performs commits after database updates.
    644     *
    645     * @return a flag whether commits are performed
    646     */
    647    public boolean isAutoCommit() {
    648        return autoCommit;
    649    }
    650
    651    /**
    652     * Checks if this configuration is empty. If this causes a database error, an error event will be generated of type
    653     * {@code READ} with the causing exception. Both the event's {@code propertyName} and {@code propertyValue} will be
    654     * undefined.
    655     *
    656     * @return a flag whether this configuration is empty.
    657     */
    658    @Override
    659    protected boolean isEmptyInternal() {
    660        final AbstractJdbcOperation<Integer> op = new AbstractJdbcOperation<Integer>(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, null, null) {
    661            @Override
    662            protected Integer performOperation() throws SQLException {
    663                try (ResultSet rs = openResultSet(String.format(SQL_IS_EMPTY, table), true)) {
    664                    return rs.next() ? Integer.valueOf(rs.getInt(1)) : null;
    665                }
    666            }
    667        };
    668
    669        final Integer count = op.execute();
    670        return count == null || count.intValue() == 0;
    671    }
    672
    673    /**
    674     * Sets the auto commit flag. If set to <strong>true</strong>, this configuration performs a commit after each database update.
    675     *
    676     * @param autoCommit the auto commit flag
    677     */
    678    public void setAutoCommit(final boolean autoCommit) {
    679        this.autoCommit = autoCommit;
    680    }
    681
    682    /**
    683     * Sets the name of this configuration instance.
    684     *
    685     * @param configurationName the name of this configuration
    686     */
    687    public void setConfigurationName(final String configurationName) {
    688        this.configurationName = configurationName;
    689    }
    690
    691    /**
    692     * Sets the name of the table column with the configuration name.
    693     *
    694     * @param configurationNameColumn the name of the column with the configuration name
    695     */
    696    public void setConfigurationNameColumn(final String configurationNameColumn) {
    697        this.configurationNameColumn = configurationNameColumn;
    698    }
    699
    700    /**
    701     * Sets the {@code DataSource} for obtaining database connections.
    702     *
    703     * @param dataSource the {@code DataSource}
    704     */
    705    public void setDataSource(final DataSource dataSource) {
    706        this.dataSource = dataSource;
    707    }
    708
    709    /**
    710     * Sets the name of the column containing the configuration keys.
    711     *
    712     * @param keyColumn the name of the key column
    713     */
    714    public void setKeyColumn(final String keyColumn) {
    715        this.keyColumn = keyColumn;
    716    }
    717
    718    /**
    719     * Sets the name of the table containing configuration data.
    720     *
    721     * @param table the table name
    722     */
    723    public void setTable(final String table) {
    724        this.table = table;
    725    }
    726
    727    /**
    728     * Sets the name of the column containing the configuration values.
    729     *
    730     * @param valueColumn the name of the value column
    731     */
    732    public void setValueColumn(final String valueColumn) {
    733        this.valueColumn = valueColumn;
    734    }
    735}