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