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