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