/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jena.jdbc.statements;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.SQLWarning;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.TimeUnit;

import org.apache.jena.atlas.iterator.Iter;
import org.apache.jena.jdbc.JdbcCompatibility;
import org.apache.jena.jdbc.connections.JenaConnection;
import org.apache.jena.jdbc.results.AskResults;
import org.apache.jena.jdbc.results.MaterializedSelectResults;
import org.apache.jena.jdbc.results.SelectResults;
import org.apache.jena.jdbc.results.TripleIteratorResults;
import org.apache.jena.jdbc.results.TripleListResults;
import org.apache.jena.query.* ;
import org.apache.jena.update.UpdateFactory ;
import org.apache.jena.update.UpdateProcessor ;
import org.apache.jena.update.UpdateRequest ;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Abstract Jena JDBC implementation of a statement that only permits read
 * operations
 * 
 */
public abstract class JenaStatement implements Statement {

    private static final Logger LOGGER = LoggerFactory.getLogger(JenaStatement.class);

    protected static final int DEFAULT_HOLDABILITY = ResultSet.CLOSE_CURSORS_AT_COMMIT;
    protected static final int DEFAULT_FETCH_DIRECTION = ResultSet.FETCH_FORWARD;
    protected static final int DEFAULT_FETCH_SIZE = 0;
    protected static final boolean DEFAULT_AUTO_COMMIT = JenaConnection.DEFAULT_AUTO_COMMIT;
    protected static final int DEFAULT_TRANSACTION_LEVEL = JenaConnection.DEFAULT_ISOLATION_LEVEL;
    protected static final int NO_LIMIT = 0;
    protected static final int DEFAULT_TYPE = ResultSet.TYPE_FORWARD_ONLY;
    protected static final int USE_CONNECTION_COMPATIBILITY = Integer.MIN_VALUE;

    private List<String> commands = new ArrayList<>();
    private SQLWarning warnings = null;
    private JenaConnection connection;
    private ResultSet currResults = null;
    private Queue<ResultSet> results = new LinkedList<>();
    private List<ResultSet> openResults = new ArrayList<>();
    private boolean closed = false;
    private int type = DEFAULT_TYPE;
    private int fetchDirection = DEFAULT_FETCH_DIRECTION;
    private int fetchSize = DEFAULT_FETCH_SIZE;
    private int holdability = DEFAULT_HOLDABILITY;
    private int updateCount = 0;
    private boolean autoCommit = DEFAULT_AUTO_COMMIT;
    private int transactionLevel = DEFAULT_TRANSACTION_LEVEL;
    private int maxRows = NO_LIMIT;
    @SuppressWarnings("unused")
    private boolean escapeProcessing = false;
    private int timeout = NO_LIMIT;
    private int compatibilityLevel = USE_CONNECTION_COMPATIBILITY;

    /**
     * Creates a new statement
     * 
     * @param connection
     *            Connection
     * @throws SQLException
     *             Thrown if the arguments are invalid
     */
    public JenaStatement(JenaConnection connection) throws SQLException {
        this(connection, DEFAULT_TYPE, DEFAULT_FETCH_DIRECTION, DEFAULT_FETCH_SIZE, DEFAULT_HOLDABILITY, DEFAULT_AUTO_COMMIT,
                DEFAULT_TRANSACTION_LEVEL);
    }

    /**
     * Creates a new statement
     * 
     * @param connection
     *            Connection
     * @param type
     *            Result Set type for result sets produced by this statement
     * @param fetchDir
     *            Fetch Direction
     * @param fetchSize
     *            Fetch Size
     * @param holdability
     *            Result Set holdability
     * @param autoCommit
     *            Auto-commit behaviour
     * @param transactionLevel
     *            Transaction level
     * @throws SQLException
     *             Thrown if there is an error with the statement parameters
     * 
     */
    public JenaStatement(JenaConnection connection, int type, int fetchDir, int fetchSize, int holdability, boolean autoCommit,
            int transactionLevel) throws SQLException {
        if (connection == null)
            throw new SQLException("Cannot create a Statement with a null connection");
        this.connection = connection;
        this.checkFetchDirection(fetchDir);
        this.type = type;
        this.fetchDirection = fetchDir;
        this.fetchSize = fetchSize;
        this.checkHoldability(holdability);
        this.holdability = holdability;
        this.autoCommit = autoCommit;
        this.transactionLevel = transactionLevel;
    }

    /**
     * Gets the underlying {@link JenaConnection} implementation, useful for
     * accessing Jena JDBC specific information such as desired JDBC
     * compatibility level
     * 
     * @return Underlying Jena Connection
     */
    public JenaConnection getJenaConnection() {
        return this.connection;
    }

    /**
     * Gets the JDBC compatibility level that is in use, see
     * {@link JdbcCompatibility} for explanations
     * <p>
     * By default this is set at the connection level and inherited, however you
     * may call {@link #setJdbcCompatibilityLevel(int)} to set the compatibility
     * level for this statement. This allows you to change the compatibility
     * level on a per-query basis if so desired.
     * </p>
     * 
     * @return Compatibility level
     */
    public int getJdbcCompatibilityLevel() {
        if (this.compatibilityLevel == USE_CONNECTION_COMPATIBILITY)
            return this.connection.getJdbcCompatibilityLevel();
        return this.compatibilityLevel;
    }

    /**
     * Sets the JDBC compatibility level that is in use, see
     * {@link JdbcCompatibility} for explanations.
     * <p>
     * By default this is set at the connection level and inherited, however you
     * may call {@link #setJdbcCompatibilityLevel(int)} to set the compatibility
     * level for this statement. This allows you to change the compatibility
     * level on a per-query basis if so desired.
     * </p>
     * <p>
     * Changing the level may not effect existing open objects, behaviour in
     * this case will be implementation specific.
     * </p>
     * 
     * @param level
     *            Compatibility level
     */
    public void setJdbcCompatibilityLevel(int level) {
        if (level == USE_CONNECTION_COMPATIBILITY) {
            this.compatibilityLevel = USE_CONNECTION_COMPATIBILITY;
        } else {
            this.compatibilityLevel = JdbcCompatibility.normalizeLevel(level);
        }
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void addBatch(String sql) {
        this.commands.add(sql);
    }

    @Override
    public void cancel() throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void clearBatch() {
        this.commands.clear();
    }

    @Override
    public void clearWarnings() {
        this.warnings = null;
    }

    @Override
    public void close() throws SQLException {
        if (this.closed)
            return;
        LOGGER.info("Closing statement");
        this.closed = true;
        // Close current result set (if any)
        if (this.currResults != null) {
            this.currResults.close();
            this.currResults = null;
        }
        // Close any remaining open results
        if (this.results.size() > 0 || this.openResults.size() > 0) {
            LOGGER.info("Closing " + (this.results.size() + this.openResults.size()) + " open result sets");

            // Queue results i.e. stuff resulting from a query that produced
            // multiple result sets or executeBatch() calls
            while (!this.results.isEmpty()) {
                ResultSet rset = this.results.poll();
                if (rset != null)
                    rset.close();
            }
            // Close open result sets i.e. stuff left around depending on
            // statement correction
            for (ResultSet rset : this.openResults) {
                rset.close();
            }
            this.openResults.clear();
            LOGGER.info("All open result sets were closed");
        }
        LOGGER.info("Statement was closed");
    }

    @Override
    public final boolean execute(String sql) throws SQLException {
        if (this.isClosed())
            throw new SQLException("The Statement is closed");

        // Pre-process the command text
        LOGGER.info("Received input command text:\n {}", sql);
        sql = this.connection.applyPreProcessors(sql);
        LOGGER.info("Command text after pre-processing:\n {}", sql);

        Query q = null;
        UpdateRequest u = null;
        try {
            // Start by assuming a query
            q = QueryFactory.create(sql);
        } catch (Exception e) {
            try {
                // If that fails try as an update instead
                u = UpdateFactory.create(sql);
            } catch (Exception e2) {
                LOGGER.error("Command text was not a valid SPARQL query/update", e2);
                throw new SQLException("Not a valid SPARQL query/update", e);
            }
        }

        if (q != null) {
            // Execute as a query
            LOGGER.info("Treating command text as a query");
            return this.executeQuery(q);
        } else if (u != null) {
            // Execute as an update
            LOGGER.info("Treating command text as an update");
            this.executeUpdate(u);
            return false;
        } else {
            throw new SQLException("Unable to create a SPARQL query/update");
        }
    }

    private boolean executeQuery(Query q) throws SQLException {
        if (this.isClosed())
            throw new SQLException("The Statement is closed");

        // Do we need transactions?
        boolean needsBegin = (!this.autoCommit && this.transactionLevel != Connection.TRANSACTION_NONE && !this
                .hasActiveTransaction());
        boolean needsCommit = (this.autoCommit && this.transactionLevel != Connection.TRANSACTION_NONE);

        // Do this first in a separate try catch so if we fail to start a
        // transaction we don't then try to roll it back which can mask the
        // actual cause of the error
        try {
            // Start a transaction if necessary
            if (needsCommit) {
                LOGGER.info("Running query in auto-commit mode");
                this.beginTransaction(ReadWrite.READ);
            } else if (needsBegin) {
                LOGGER.info("Starting a new transaction to run query, transaction will not be auto-committed");
                this.beginTransaction(ReadWrite.WRITE);
            }
        } catch (Exception e) {
            LOGGER.error("Starting the new transaction failed", e);
            throw new SQLException("Failed to start a new query transaction", e);
        }

        try {
            // Pre-process the query
            q = this.connection.applyPreProcessors(q);

            // Manipulate the query if appropriate
            if (this.maxRows > NO_LIMIT) {
                // If we have no LIMIT or the LIMIT is greater than the
                // permitted max rows
                // then we will set the LIMIT to the max rows
                if (!q.hasLimit() || q.getLimit() > this.maxRows) {
                    LOGGER.info("Enforced max rows on results by applying LIMIT {} to the query", this.maxRows);
                    q.setLimit(this.maxRows);
                }
            }

            // Create the query execution
            QueryExecution qe = this.createQueryExecution(q);

            // Manipulate the query execution if appropriate
            if (this.timeout > NO_LIMIT) {
                qe.setTimeout(this.timeout, TimeUnit.SECONDS, this.timeout, TimeUnit.SECONDS);
            }

            // Return the appropriate result set type
            if (q.isSelectType()) {
                switch (this.type) {
                case ResultSet.TYPE_SCROLL_INSENSITIVE:
                    this.currResults = new MaterializedSelectResults(this, qe, ResultSetFactory.makeRewindable(this.connection
                            .applyPostProcessors(qe.execSelect())), false);
                    break;
                case ResultSet.TYPE_FORWARD_ONLY:
                default:
                    this.currResults = new SelectResults(this, qe, this.connection.applyPostProcessors(qe.execSelect()),
                            needsCommit);
                    break;
                }
            } else if (q.isAskType()) {
                boolean askRes = qe.execAsk();
                qe.close();
                this.currResults = new AskResults(this, this.connection.applyPostProcessors(askRes), needsCommit);
            } else if (q.isDescribeType()) {
                switch (this.type) {
                case ResultSet.TYPE_SCROLL_INSENSITIVE:
                    this.currResults = new TripleListResults(this, qe, Iter.toList(this.connection.applyPostProcessors(qe
                            .execDescribeTriples())), false);
                    break;
                case ResultSet.TYPE_FORWARD_ONLY:
                default:
                    this.currResults = new TripleIteratorResults(this, qe, this.connection.applyPostProcessors(qe
                            .execDescribeTriples()), needsCommit);
                    break;
                }
            } else if (q.isConstructType()) {
                switch (this.type) {
                case ResultSet.TYPE_SCROLL_INSENSITIVE:
                    this.currResults = new TripleListResults(this, qe, Iter.toList(this.connection.applyPostProcessors(qe
                            .execConstructTriples())), false);
                    break;
                case ResultSet.TYPE_FORWARD_ONLY:
                default:
                    this.currResults = new TripleIteratorResults(this, qe, this.connection.applyPostProcessors(qe
                            .execConstructTriples()), needsCommit);
                    break;
                }
            } else {
                throw new SQLException("Unknown SPARQL Query type");
            }

            // Can immediately commit when type is
            // TYPE_SCROLL_INSENSITIVE and auto-committing since we have
            // already materialized results so don't need the read
            // transaction
            if (this.type == ResultSet.TYPE_SCROLL_INSENSITIVE && needsCommit) {
                LOGGER.info("Auto-committing query transaction since results have been materialized");
                this.commitTransaction();
            }

            return true;
        } catch (SQLException e) {
            if (needsCommit) {
                // When auto-committing and query fails roll back immediately
                LOGGER.warn("Rolling back failed query transaction", e);
                this.rollbackTransaction();
            }
            throw e;
        } catch (Throwable e) {
            LOGGER.error("SPARQL Query evaluation failed", e);
            if (needsCommit) {
                // When auto-committing and query fails roll back immediately
                LOGGER.warn("Rolling back failed query transaction");
                this.rollbackTransaction();
            }
            throw new SQLException("Error occurred during SPARQL query evaluation", e);
        }
    }

    /**
     * Helper method which derived classes must implement to provide a query
     * execution
     * 
     * @param q
     *            Query
     * @return Query Execution
     * @throws SQLException
     *             Thrown if there is a problem creating a query execution
     */
    protected abstract QueryExecution createQueryExecution(Query q) throws SQLException;

    private int executeUpdate(UpdateRequest u) throws SQLException {
        if (this.isClosed())
            throw new SQLException("The Statement is closed");
        if (this.connection.isReadOnly())
            throw new SQLException("The JDBC connection is currently in read-only mode, updates are not permitted");

        // Do we need transactions?
        boolean needsBegin = (!this.autoCommit && this.transactionLevel != Connection.TRANSACTION_NONE && !this
                .hasActiveTransaction());
        boolean needsCommit = (this.autoCommit && this.transactionLevel != Connection.TRANSACTION_NONE);

        try {
            // Start a Transaction if necessary
            if (needsCommit || needsBegin) {
                if (this.autoCommit) {
                    LOGGER.info("Running update in auto-commit mode");
                } else {
                    LOGGER.info("Starting a new transaction to run update, transaction will not be auto-committed");
                }
                this.beginTransaction(ReadWrite.WRITE);
            }
        } catch (Exception e) {
            LOGGER.error("Starting the new transaction failed", e);
            throw new SQLException("Failed to start a new query transaction", e);
        }

        try {
            // Pre-process the update
            u = this.connection.applyPreProcessors(u);

            // Execute updates
            UpdateProcessor processor = this.createUpdateProcessor(u);
            processor.execute();

            // If auto-committing can commit immediately
            if (needsCommit) {
                LOGGER.info("Auto-committing update transaction");
                this.commitTransaction();
            }

            return 0;
        } catch (SQLException e) {
            if (needsCommit) {
                LOGGER.warn("Rolling back failed update transaction", e);
                this.rollbackTransaction();
            }
            throw e;
        } catch (Exception e) {
            LOGGER.error("SPARQL Update evaluation failed", e);
            if (needsCommit) {
                LOGGER.warn("Rolling back failed update transaction");
                this.rollbackTransaction();
            }
            throw new SQLException("Error occurred during SPARQL update evaluation", e);
        }
    }

    /**
     * Helper method which derived classes must implement to provide an update
     * processor
     * 
     * @param u
     *            Update
     * @return Update Processor
     */
    protected abstract UpdateProcessor createUpdateProcessor(UpdateRequest u) throws SQLException;

    protected abstract boolean hasActiveTransaction() throws SQLException;

    protected abstract void beginTransaction(ReadWrite type) throws SQLException;

    protected abstract void commitTransaction() throws SQLException;

    protected abstract void rollbackTransaction() throws SQLException;

    @Override
    public boolean execute(String sql, int autoGeneratedKeys) throws SQLException {
        return this.execute(sql);
    }

    @Override
    public boolean execute(String sql, int[] columnIndexes) throws SQLException {
        return this.execute(sql);
    }

    @Override
    public boolean execute(String sql, String[] columnNames) throws SQLException {
        return this.execute(sql);
    }

    @Override
    public final int[] executeBatch() throws SQLException {
        if (this.isClosed())
            throw new SQLException("The Statement is closed");

        // Issue warning where appropriate
        if (this.commands.size() > 1 && this.autoCommit && this.holdability == ResultSet.CLOSE_CURSORS_AT_COMMIT) {
            this.setWarning("Executing this batch of commands may lead to unexpectedly closed result sets because auto-commit is enabled and commit behaviour is set to close cursors at commit");
        }

        // Go ahead and process the batch
        int[] rets = new int[this.commands.size()];
        ResultSet curr = this.currResults;
        for (int i = 0; i < this.commands.size(); i++) {
            if (this.execute(this.commands.get(i))) {
                // True means it returned a ResultSet
                this.results.add(this.getResultSet());
                this.currResults = null;
                rets[i] = SUCCESS_NO_INFO;
            } else {
                // Need to add a null to getMoreResults() to produce correct
                // behavior across subsequent calls to getMoreResults()
                this.results.add(null);
                rets[i] = this.getUpdateCount();
            }
        }
        this.currResults = curr;
        // Make the next available results the current results if there
        // are no current results
        if (this.currResults == null && !this.results.isEmpty()) {
            this.currResults = this.results.poll();
        }
        return rets;
    }

    @Override
    public final ResultSet executeQuery(String sql) throws SQLException {
        if (this.isClosed())
            throw new SQLException("The Statement is closed");

        // Pre-process the command text
        LOGGER.info("Received input command text:\n {}", sql);
        sql = this.connection.applyPreProcessors(sql);
        LOGGER.info("Command text after pre-processing:\n {}", sql);

        Query q = null;
        try {
            q = QueryFactory.create(sql);
        } catch (Exception e) {
            LOGGER.error("Invalid SPARQL query", e);
            throw new SQLException("Not a valid SPARQL query", e);
        }

        if (q == null)
            throw new SQLException("Unable to create a SPARQL Query");
        if (this.executeQuery(q)) {
            return this.currResults;
        } else {
            throw new SQLException("Query did not produce a result set");
        }
    }

    @Override
    public final int executeUpdate(String sql) throws SQLException {
        if (this.isClosed())
            throw new SQLException("The Statement is closed");
        if (this.connection.isReadOnly())
            throw new SQLException("The JDBC connection is currently in read-only mode, updates are not permitted");

        // Pre-process the command text
        LOGGER.info("Received input command text:\n {}", sql);
        sql = this.connection.applyPreProcessors(sql);
        LOGGER.info("Command text after pre-processing:\n {}", sql);

        UpdateRequest u = null;
        try {
            u = UpdateFactory.create(sql);
        } catch (Exception e) {
            LOGGER.error("Invalid SPARQL update", e);
            throw new SQLException("Not a valid SPARQL Update", e);
        }

        if (u == null)
            throw new SQLException("Unable to create a SPARQL Update Request");
        return this.executeUpdate(u);
    }

    @Override
    public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException {
        return this.executeUpdate(sql);
    }

    @Override
    public int executeUpdate(String sql, int[] columnIndexes) throws SQLException {
        return this.executeUpdate(sql);
    }

    @Override
    public int executeUpdate(String sql, String[] columnNames) throws SQLException {
        return this.executeUpdate(sql);
    }

    @Override
    public final Connection getConnection() {
        return this.connection;
    }

    @Override
    public int getFetchDirection() {
        return this.fetchDirection;
    }

    @Override
    public int getFetchSize() {
        return this.fetchSize;
    }

    @Override
    public ResultSet getGeneratedKeys() throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public int getMaxFieldSize() {
        return NO_LIMIT;
    }

    @Override
    public int getMaxRows() {
        return maxRows;
    }

    @Override
    public boolean getMoreResults() throws SQLException {
        if (this.isClosed())
            throw new SQLException("The Statement is closed");

        if (this.currResults != null) {
            this.currResults.close();
            this.currResults = null;
        }
        if (!this.results.isEmpty()) {
            this.currResults = this.results.poll();
            return true;
        } else {
            return false;
        }
    }

    @Override
    public boolean getMoreResults(int current) throws SQLException {
        if (this.isClosed())
            throw new SQLException("The Statement is closed");

        switch (current) {
        case Statement.CLOSE_CURRENT_RESULT:
            return this.getMoreResults();
        case Statement.CLOSE_ALL_RESULTS:
            for (ResultSet rset : this.openResults) {
                rset.close();
            }
            this.openResults.clear();
            return this.getMoreResults();
        case Statement.KEEP_CURRENT_RESULT:
            if (this.currResults != null) {
                this.openResults.add(this.currResults);
                this.currResults = null;
            }
            return this.getMoreResults();
        default:
            throw new SQLFeatureNotSupportedException(
                    "Unsupported mode for dealing with current results, only Statement.CLOSE_CURRENT_RESULT, Statement.CLOSE_ALL_RESULTS and Statement.KEEP_CURRENT_RESULT are supported");
        }
    }

    @Override
    public int getQueryTimeout() {
        return this.timeout;
    }

    @Override
    public final ResultSet getResultSet() throws SQLException {
        if (this.isClosed())
            throw new SQLException("The Statement is closed");
        return this.currResults;
    }

    /**
     * Helper method for use in execute() method implementations to set the
     * current results
     * 
     * @param results
     *            Results
     * @throws SQLException
     *             Thrown if there is an error closing the previous results
     */
    protected void setCurrentResults(ResultSet results) throws SQLException {
        if (this.currResults != null) {
            this.currResults.close();
        }
        this.currResults = results;
    }

    /**
     * Gets that result sets are read-only
     */
    @Override
    public final int getResultSetConcurrency() {
        return ResultSet.CONCUR_READ_ONLY;
    }

    @Override
    public int getResultSetHoldability() {
        return this.holdability;
    }

    protected void checkHoldability(int h) throws SQLException {
        switch (h) {
        case ResultSet.CLOSE_CURSORS_AT_COMMIT:
        case ResultSet.HOLD_CURSORS_OVER_COMMIT:
            return;
        default:
            throw new SQLException(String.format("Holdability %d is supported for Jena JDBC statements", h));
        }
    }

    @Override
    public final int getResultSetType() {
        return this.type;
    }

    @Override
    public int getUpdateCount() {
        return this.updateCount;
    }

    /**
     * Helper method which derived classes may use to set the update count
     * 
     * @param count
     *            Update Count
     */
    protected void setUpdateCount(int count) {
        this.updateCount = count;
    }

    @Override
    public SQLWarning getWarnings() {
        return this.warnings;
    }

    /**
     * Helper method that derived classes may use to set warnings
     * 
     * @param warning
     *            Warning
     */
    protected void setWarning(SQLWarning warning) {
        LOGGER.warn("SQL Warning was issued", warning);
        if (this.warnings == null) {
            this.warnings = warning;
        } else {
            // Chain with existing warnings
            warning.setNextWarning(this.warnings);
            this.warnings = warning;
        }
    }

    /**
     * Helper method that derived classes may use to set warnings
     * 
     * @param warning
     *            Warning
     */
    protected void setWarning(String warning) {
        this.setWarning(new SQLWarning(warning));
    }

    /**
     * Helper method that derived classes may use to set warnings
     * 
     * @param warning
     *            Warning
     * @param cause
     *            Cause
     */
    protected void setWarning(String warning, Throwable cause) {
        this.setWarning(new SQLWarning(warning, cause));
    }

    @Override
    public final boolean isClosed() {
        return this.closed;
    }

    @Override
    public final boolean isPoolable() {
        return true;
    }

    @Override
    public void setCursorName(String name) throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void setEscapeProcessing(boolean enable) {
        this.escapeProcessing = enable;
    }

    @Override
    public void setFetchDirection(int direction) throws SQLException {
        this.checkFetchDirection(direction);
        this.fetchDirection = direction;
    }

    /**
     * Helper method which checks whether a given fetch direction is valid
     * throwing an error if it is not
     * 
     * @param dir
     *            Fetch Direction
     * @throws SQLException
     *             Thrown if the direction is not valid
     */
    protected void checkFetchDirection(int dir) throws SQLException {
        switch (dir) {
        case ResultSet.FETCH_FORWARD:
            return;
        default:
            throw new SQLFeatureNotSupportedException("Only ResultSet.FETCH_FORWARD is supported as a fetch direction");
        }
    }

    @Override
    public void setFetchSize(int rows) {
        this.fetchSize = rows;
    }

    @Override
    public void setMaxFieldSize(int max) {
        // Ignored
        this.setWarning("setMaxFieldSize() was called but there is no field size limit for Jena JDBC connections");
    }

    @Override
    public void setMaxRows(int max) {
        if (max <= NO_LIMIT) {
            this.maxRows = NO_LIMIT;
        } else {
            this.maxRows = max;
        }
    }

    @Override
    public void setPoolable(boolean poolable) {
        // Ignored
        this.setWarning("setPoolable() was called but Jena JDBC statements are always considered poolable");
    }

    @Override
    public void setQueryTimeout(int seconds) {
        if (seconds <= NO_LIMIT) {
            this.timeout = NO_LIMIT;
        } else {
            this.timeout = seconds;
        }
    }

    // Java 6/7 compatibility
    @SuppressWarnings("javadoc")
    public boolean isCloseOnCompletion() {
        // Statements do not automatically close
        return false;
    }

    @SuppressWarnings("javadoc")
    public void closeOnCompletion() throws SQLException {
        // We don't support the JDBC 4.1 feature of closing statements
        // automatically
        throw new SQLFeatureNotSupportedException();
    }
}
