/* * Copyright 2008-2013, ETH Zürich, Samuel Welten, Michael Kuhn, Tobias Langner, * Sandro Affentranger, Lukas Bossard, Michael Grob, Rahul Jain, * Dominic Langenegger, Sonia Mayor Alonso, Roger Odermatt, Tobias Schlueter, * Yannick Stucki, Sebastian Wendland, Samuel Zehnder, Samuel Zihlmann, * Samuel Zweifel * * This file is part of Jukefox. * * Jukefox is free software: you can redistribute it and/or modify it under the * terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or any later version. Jukefox is * distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * Jukefox. If not, see <http://www.gnu.org/licenses/>. */ package ch.ethz.dcg.jukefox.data.db; import java.util.Stack; import ch.ethz.dcg.jukefox.commons.utils.Log; import ch.ethz.dcg.jukefox.data.db.LockHelper.Lock; import ch.ethz.dcg.jukefox.data.db.SqlDbDataPortal.ISqlDbConnection; /** * This helper class enables the usage of EXCLUSIVE and IMMEDIATE transactions for SQLite databases where no two * transactions are allowed to run at the same time and nested transactions are not supported.<br/> * <br/> * All transactions are run on the connection retreived by {@link SqlDbDataPortal#getTransactionConnection()}. This * enables read-only queries to continue their work when the running transaction is of type * {@link TransactionType#IMMEDIATE}. */ public final class TransactionHelper { private static final String TAG = TransactionHelper.class.getSimpleName(); private final SqlDbDataPortal<? extends IContentValues> dbDataPortal; public enum TransactionType { EXCLUSIVE, IMMEDIATE } private TransactionType currentTransactionType = null; private final Stack<Lock> transactionLockStack = new Stack<Lock>(); private boolean transactionIsSuccessful; private boolean innerTransactionIsSuccessful; public TransactionHelper(SqlDbDataPortal<? extends IContentValues> dbDataPortal) { this.dbDataPortal = dbDataPortal; } /** * Starts an immediate transaction. * * @see IDbDataPortal#beginTransaction() */ public void beginTransaction() { beginTransaction(TransactionType.IMMEDIATE); } /** * Starts an exclusive transaction. * * @see IDbDataPortal#beginExclusiveTransaction() */ public void beginExclusiveTransaction() { beginTransaction(TransactionType.EXCLUSIVE); } /** * Adopted from the android source. * * @see The 2.2_r1.1 version of SQLiteDatabase.java */ private void beginTransaction(TransactionType transactionType) { // Get the transaction connection ISqlDbConnection transConnection = dbDataPortal.getTransactionConnection(); // Acquire the db-lock switch (transactionType) { case IMMEDIATE: Lock lR = dbDataPortal.lockR(transConnection); transactionLockStack.add(lR); if (currentTransactionType == null) { currentTransactionType = TransactionType.IMMEDIATE; // Only set it to IMMEDIATE if not already set --> strict two-phase-locking } break; case EXCLUSIVE: Lock lX = dbDataPortal.lockX(transConnection); transactionLockStack.add(lX); currentTransactionType = TransactionType.EXCLUSIVE; break; } boolean ok = false; try { if (transactionLockStack.size() > 1) { // A transaction is already open -> reuse it if (innerTransactionIsSuccessful) { String msg = "Cannot call beginTransaction between " + "calling setTransactionSuccessful and endTransaction"; IllegalStateException e = new IllegalStateException(msg); Log.w(TAG, e); throw e; } Log.d(TAG, "Reusing transaction, nesting level = " + transactionLockStack.size()); } else { // No transaction is open yet -> begin one now switch (transactionType) { case EXCLUSIVE: dbDataPortal.execSQLNoLock("BEGIN EXCLUSIVE;", transConnection); break; case IMMEDIATE: dbDataPortal.execSQLNoLock("BEGIN IMMEDIATE;", transConnection); break; } transactionIsSuccessful = true; innerTransactionIsSuccessful = false; } ok = true; } finally { if (!ok) { // exception occured -> unlock transactionLockStack.pop().release(); } } } /** * Adopted from the android source. * * @see The 2.2_r1.1 version of SQLiteDatabase.java */ public void setTransactionSuccessful() { if (!inTransaction()) { throw new IllegalStateException("no transaction pending"); } if (innerTransactionIsSuccessful) { throw new IllegalStateException( "setTransactionSuccessful may only be called once per call to beginTransaction"); } innerTransactionIsSuccessful = true; } /** * Adopted from the android source. * * @see The 2.2_r1.1 version of SQLiteDatabase.java */ public void endTransaction() { if (!inTransaction()) { throw new IllegalStateException("no transaction pending"); } if (innerTransactionIsSuccessful) { innerTransactionIsSuccessful = false; } else { transactionIsSuccessful = false; } if (transactionLockStack.size() > 1) { transactionLockStack.pop().release(); return; } // Get the transaction connection ISqlDbConnection transConnection = dbDataPortal.getTransactionConnection(); // Commit or abort the transcation if (transactionIsSuccessful) { dbDataPortal.execSQLNoLock("COMMIT;", transConnection); } else { try { dbDataPortal.execSQLNoLock("ROLLBACK;", transConnection); } catch (UncheckedSqlException e) { Log.d(TAG, "exception during rollback, maybe the DB previously performed an auto-rollback"); } } // Release our lock transactionLockStack.pop().release(); currentTransactionType = null; } /** * Returns true, if the current thread is in a transaction. * * @return If we are in a transaction */ public boolean inTransaction() { return !transactionLockStack.isEmpty() && Thread.currentThread().equals(transactionLockStack.peek().lockHolder.thread); } /** * Returns the transaction type of the current thread (null if no transaction is running). * * @return The transaction type */ public TransactionType getTransactionType() { if (!inTransaction()) { return null; } return currentTransactionType; } }