001package org.itsallcode.jdbc;
002
003import java.sql.Connection;
004import java.util.function.Consumer;
005
006import org.itsallcode.jdbc.batch.*;
007import org.itsallcode.jdbc.resultset.RowMapper;
008import org.itsallcode.jdbc.resultset.SimpleResultSet;
009import org.itsallcode.jdbc.resultset.generic.Row;
010
011/**
012 * A running database transaction. The transaction will be rolled back
013 * automatically in {@link #close()} if not explicitly committed using
014 * {@link #commit()} or rolled back using {@link #rollback()} before.
015 * <p>
016 * Start a new transaction using {@link SimpleConnection#startTransaction()}.
017 * <p>
018 * Operations are not allowed on a closed, committed or rolled back transaction.
019 * <p>
020 * Closing an already closed transaction is a no-op.
021 */
022public final class Transaction implements DbOperations {
023
024    private final ConnectionWrapper connection;
025    private final Consumer<Transaction> transactionFinishedCallback;
026    private final boolean restoreAutoCommitRequired;
027    private boolean closed;
028    private boolean committed;
029    private boolean rolledBack;
030
031    private Transaction(final ConnectionWrapper connection, final Consumer<Transaction> transactionFinishedCallback,
032            final boolean restoreAutoCommitRequired) {
033        this.connection = connection;
034        this.transactionFinishedCallback = transactionFinishedCallback;
035        this.restoreAutoCommitRequired = restoreAutoCommitRequired;
036    }
037
038    static Transaction start(final ConnectionWrapper connection,
039            final Consumer<Transaction> transactionFinishedCallback) {
040        boolean restoreAutoCommitRequired = false;
041        if (connection.isAutoCommitEnabled()) {
042            connection.setAutoCommit(false);
043            restoreAutoCommitRequired = true;
044        }
045        return new Transaction(connection, transactionFinishedCallback, restoreAutoCommitRequired);
046    }
047
048    /**
049     * Commit the transaction.
050     * <p>
051     * No further operations are allowed on this transaction afterwards.
052     * 
053     * @see Connection#commit()
054     */
055    public void commit() {
056        checkOperationAllowed();
057        this.connection.commit();
058        this.committed = true;
059        this.transactionFinishedCallback.accept(this);
060    }
061
062    /**
063     * Rollback the transaction.
064     * <p>
065     * No further operations are allowed on this transaction afterwards.
066     * 
067     * @see Connection#rollback()
068     */
069    public void rollback() {
070        checkOperationAllowed();
071        this.connection.rollback();
072        this.rolledBack = true;
073        this.transactionFinishedCallback.accept(this);
074    }
075
076    @Override
077    public int executeUpdate(final String sql) {
078        checkOperationAllowed();
079        return connection.executeUpdate(sql);
080    }
081
082    @Override
083    public int executeUpdate(final String sql, final PreparedStatementSetter preparedStatementSetter) {
084        checkOperationAllowed();
085        return connection.executeUpdate(sql, preparedStatementSetter);
086    }
087
088    @Override
089    public SimpleResultSet<Row> query(final String sql) {
090        checkOperationAllowed();
091        return connection.query(sql);
092    }
093
094    @Override
095    public void executeScript(final String sqlScript) {
096        checkOperationAllowed();
097        connection.executeScript(sqlScript);
098    }
099
100    @Override
101    public <T> SimpleResultSet<T> query(final String sql, final PreparedStatementSetter preparedStatementSetter,
102            final RowMapper<T> rowMapper) {
103        checkOperationAllowed();
104        return connection.query(sql, preparedStatementSetter, rowMapper);
105    }
106
107    @Override
108    public StatementBatchBuilder statementBatch() {
109        checkOperationAllowed();
110        return connection.statementBatch();
111    }
112
113    @Override
114    public PreparedStatementBatchBuilder preparedStatementBatch() {
115        checkOperationAllowed();
116        return connection.preparedStatementBatch();
117    }
118
119    @Override
120    public <T> RowPreparedStatementBatchBuilder<T> preparedStatementBatch(final Class<T> rowType) {
121        checkOperationAllowed();
122        return connection.rowPreparedStatementBatch();
123    }
124
125    public Connection getOriginalConnection() {
126        checkOperationAllowed();
127        return connection.getOriginalConnection();
128    }
129
130    private void checkOperationAllowed() {
131        if (this.closed) {
132            throw new IllegalStateException("Operation not allowed on closed transaction");
133        }
134        if (this.rolledBack) {
135            throw new IllegalStateException("Operation not allowed on rolled back transaction");
136        }
137        if (this.committed) {
138            throw new IllegalStateException("Operation not allowed on committed transaction");
139        }
140    }
141
142    /**
143     * Rollback transaction if not already committed or rolled back and restore
144     * original auto commit setting if necessary.
145     * <p>
146     * Explicitly run {@link #commit()} before to commit your transaction.
147     * <p>
148     * No further operations are allowed on this transaction afterwards.
149     * <p>
150     * This <em>does not</em> close the connection, so you can continue using it.
151     */
152    @Override
153    public void close() {
154        if (closed) {
155            return;
156        }
157        if (!rolledBack && !committed) {
158            this.rollback();
159        }
160        if (restoreAutoCommitRequired) {
161            connection.setAutoCommit(true);
162        }
163        this.closed = true;
164    }
165}