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