001/*
002 * Copyright 2015-2018 Transmogrify LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.pyranid;
018
019import static java.lang.String.format;
020import static java.util.Objects.requireNonNull;
021
022import java.sql.Connection;
023import java.sql.SQLException;
024import java.sql.Savepoint;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.List;
028import java.util.Optional;
029import java.util.concurrent.atomic.AtomicLong;
030import java.util.logging.Logger;
031import javax.sql.DataSource;
032
033/**
034 * @author <a href="http://revetkn.com">Mark Allen</a>
035 * @since 1.0.0
036 */
037public class Transaction {
038  private static final AtomicLong ID_GENERATOR = new AtomicLong(0);
039
040  private final long id = ID_GENERATOR.incrementAndGet();
041  private final Optional<DataSource> dataSource;
042  private final TransactionIsolation transactionIsolation;
043  private Optional<Connection> connection;
044  private boolean rollbackOnly;
045  private Optional<Boolean> initialAutoCommit;
046  private final List<Runnable> postCommitOperations;
047  private final List<Runnable> postRollbackOperations;
048  private final Logger logger = Logger.getLogger(Transaction.class.getName());
049
050  Transaction(DataSource dataSource, TransactionIsolation transactionIsolation) {
051    this.dataSource = Optional.of(requireNonNull(dataSource));
052    this.transactionIsolation = transactionIsolation;
053    this.connection = Optional.empty();
054    this.rollbackOnly = false;
055    this.initialAutoCommit = Optional.empty();
056    this.postCommitOperations = new ArrayList<>();
057    this.postRollbackOperations = new ArrayList<>();
058  }
059
060  @Override
061  public String toString() {
062    return format("%s{id=%s, transactionIsolation=%s, hasConnection=%s, isRollbackOnly=%s}",
063      getClass().getSimpleName(), id(), transactionIsolation(), hasConnection(), isRollbackOnly());
064  }
065
066  public Savepoint createSavepoint() {
067    try {
068      return connection().setSavepoint();
069    } catch (SQLException e) {
070      throw new DatabaseException("Unable to create savepoint", e);
071    }
072  }
073
074  public void rollback(Savepoint savepoint) {
075    requireNonNull(savepoint);
076
077    try {
078      connection().rollback(savepoint);
079    } catch (SQLException e) {
080      throw new DatabaseException("Unable to roll back to savepoint", e);
081    }
082  }
083
084  /**
085   * Should this transaction be rolled back upon completion?
086   * <p>
087   * Default value is {@code false}.
088   * 
089   * @return {@code true} if this transaction should be rolled back, {@code false} otherwise.
090   */
091  public boolean isRollbackOnly() {
092    return this.rollbackOnly;
093  }
094
095  public void setRollbackOnly(boolean rollbackOnly) {
096    this.rollbackOnly = rollbackOnly;
097  }
098
099  public void addPostCommitOperation(Runnable postCommitOperation) {
100    requireNonNull(postCommitOperation);
101    postCommitOperations.add(postCommitOperation);
102  }
103
104  public boolean removePostCommitOperation(Runnable postCommitOperation) {
105    requireNonNull(postCommitOperation);
106    return postCommitOperations.remove(postCommitOperation);
107  }
108
109  public void addPostRollbackOperation(Runnable postRollbackOperation) {
110    requireNonNull(postRollbackOperation);
111    postRollbackOperations.add(postRollbackOperation);
112  }
113
114  public boolean removePostRollbackOperation(Runnable postRollbackOperation) {
115    requireNonNull(postRollbackOperation);
116    return postRollbackOperations.remove(postRollbackOperation);
117  }
118
119  long id() {
120    return this.id;
121  }
122
123  boolean hasConnection() {
124    return this.connection.isPresent();
125  }
126
127  void commit() {
128    if (!hasConnection()) {
129      logger.finer("Transaction has no connection, so nothing to commit");
130      return;
131    }
132
133    logger.finer("Committing transaction...");
134
135    try {
136      connection().commit();
137      logger.finer("Transaction committed.");
138    } catch (SQLException e) {
139      throw new DatabaseException("Unable to commit transaction", e);
140    }
141  }
142
143  void rollback() {
144    if (!hasConnection()) {
145      logger.finer("Transaction has no connection, so nothing to roll back");
146      return;
147    }
148
149    logger.finer("Rolling back transaction...");
150
151    try {
152      connection().rollback();
153      logger.finer("Transaction rolled back.");
154    } catch (SQLException e) {
155      throw new DatabaseException("Unable to roll back transaction", e);
156    }
157  }
158
159  /**
160   * The connection associated with this transaction.
161   * <p>
162   * If no connection is associated yet, we ask the {@link DataSource} for one.
163   * 
164   * @return The connection associated with this transaction.
165   * @throws DatabaseException
166   *           if unable to acquire a connection.
167   */
168  Connection connection() {
169    if (hasConnection())
170      return this.connection.get();
171
172    try {
173      this.connection = Optional.of(dataSource.get().getConnection());
174    } catch (SQLException e) {
175      throw new DatabaseException("Unable to acquire database connection", e);
176    }
177
178    // Keep track of the initial setting for autocommit since it might need to get changed from "true" to "false" for
179    // the duration of the transaction and then back to "true" post-transaction.
180    try {
181      this.initialAutoCommit = Optional.of(this.connection.get().getAutoCommit());
182    } catch (SQLException e) {
183      throw new DatabaseException("Unable to determine database connection autocommit setting", e);
184    }
185
186    // Immediately flip autocommit to false if needed...if initially true, it will get set back to true by Database at
187    // the end of the transaction
188    if (this.initialAutoCommit.get())
189      setAutoCommit(false);
190
191    return this.connection.get();
192  }
193
194  void setAutoCommit(boolean autoCommit) {
195    try {
196      connection().setAutoCommit(autoCommit);
197    } catch (SQLException e) {
198      throw new DatabaseException(format("Unable to set database connection autocommit value to '%s'", autoCommit), e);
199    }
200  }
201
202  Optional<Boolean> initialAutoCommit() {
203    return initialAutoCommit;
204  }
205
206  public TransactionIsolation transactionIsolation() {
207    return transactionIsolation;
208  }
209
210  public List<Runnable> postCommitOperations() {
211    return Collections.unmodifiableList(postCommitOperations);
212  }
213
214  public List<Runnable> postRollbackOperations() {
215    return Collections.unmodifiableList(postRollbackOperations);
216  }
217}