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