Skip to content

Commit

Permalink
Merge from sbrannen/SPR-5079
Browse files Browse the repository at this point in the history
* SPR-5079:
  Introduce programmatic tx mgmt in the TCF
  • Loading branch information
sbrannen committed Jul 2, 2014
2 parents 526b463 + f667e43 commit 90f0d14
Show file tree
Hide file tree
Showing 8 changed files with 663 additions and 108 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* Copyright 2002-2014 the original author or authors.
*
* Licensed 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.springframework.test.context.transaction;

import org.springframework.test.context.TestExecutionListeners;
import org.springframework.transaction.TransactionStatus;

/**
* {@code TestTransaction} provides a collection of static utility methods for
* programmatic interaction with <em>test-managed transactions</em>.
*
* <p>Test-managed transactions are transactions that are managed by the <em>Spring TestContext Framework</em>.
*
* <p>Support for {@code TestTransaction} is automatically available whenever
* the {@link TransactionalTestExecutionListener} is enabled. Note that the
* {@code TransactionalTestExecutionListener} is typically enabled by default,
* but it can also be manually enabled via the
* {@link TestExecutionListeners @TestExecutionListeners} annotation.
*
* @author Sam Brannen
* @since 4.1
* @see TransactionalTestExecutionListener
*/
public class TestTransaction {

/**
* Determine whether a test-managed transaction is currently <em>active</em>.
* @return {@code true} if a test-managed transaction is currently active
* @see #start()
* @see #end()
*/
public static boolean isActive() {
TransactionContext transactionContext = TransactionContextHolder.getCurrentTransactionContext();
if (transactionContext != null) {
TransactionStatus transactionStatus = transactionContext.getTransactionStatus();
return (transactionStatus != null) && (!transactionStatus.isCompleted());
}

// else
return false;
}

/**
* Determine whether the current test-managed transaction has been
* {@linkplain #flagForRollback() flagged for rollback} or
* {@linkplain #flagForCommit() flagged for commit}.
* @return {@code true} if the current test-managed transaction is flagged
* to be rolled back; {@code false} if the current test-managed transaction
* is flagged to be committed
* @throws IllegalStateException if a transaction is not active for the
* current test
* @see #isActive()
* @see #flagForRollback()
* @see #flagForCommit()
*/
public static boolean isFlaggedForRollback() {
return requireCurrentTransactionContext().isFlaggedForRollback();
}

/**
* Flag the current test-managed transaction for <em>rollback</em>.
* <p>Invoking this method will <em>not</em> end the current transaction.
* Rather, the value of this flag will be used to determine whether or not
* the current test-managed transaction should be rolled back or committed
* once it is {@linkplain #end ended}.
* @throws IllegalStateException if a transaction is not active for the
* current test
* @see #isActive()
* @see #isFlaggedForRollback()
* @see #start()
* @see #end()
*/
public static void flagForRollback() {
setFlaggedForRollback(true);
}

/**
* Flag the current test-managed transaction for <em>commit</em>.
* <p>Invoking this method will <em>not</em> end the current transaction.
* Rather, the value of this flag will be used to determine whether or not
* the current test-managed transaction should be rolled back or committed
* once it is {@linkplain #end ended}.
* @throws IllegalStateException if a transaction is not active for the
* current test
* @see #isActive()
* @see #isFlaggedForRollback()
* @see #start()
* @see #end()
*/
public static void flagForCommit() {
setFlaggedForRollback(false);
}

/**
* Start a new test-managed transaction.
* <p>Only call this method if {@link #end} has been called or if no
* transaction has been previously started.
* @throws IllegalStateException if the transaction context could not be
* retrieved or if a transaction is already active for the current test
* @see #isActive()
* @see #end()
*/
public static void start() {
requireCurrentTransactionContext().startTransaction();
}

/**
* Immediately force a <em>commit</em> or <em>rollback</em> of the current
* test-managed transaction, according to the {@linkplain #isFlaggedForRollback
* rollback flag}.
* @throws IllegalStateException if the transaction context could not be
* retrieved or if a transaction is not active for the current test
* @see #isActive()
* @see #start()
*/
public static void end() {
requireCurrentTransactionContext().endTransaction();
}

private static TransactionContext requireCurrentTransactionContext() {
TransactionContext txContext = TransactionContextHolder.getCurrentTransactionContext();
if (txContext == null) {
throw new IllegalStateException("TransactionContext is not active");
}
return txContext;
}

private static void setFlaggedForRollback(boolean flag) {
requireCurrentTransactionContext().setFlaggedForRollback(flag);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
* Copyright 2002-2014 the original author or authors.
*
* Licensed 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.springframework.test.context.transaction;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.test.context.TestContext;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionStatus;

/**
* Transaction context for a specific {@link TestContext}.
*
* @author Sam Brannen
* @author Juergen Hoeller
* @since 4.1
* @see org.springframework.transaction.annotation.Transactional
* @see org.springframework.test.context.transaction.TransactionalTestExecutionListener
*/
class TransactionContext {

private static final Log logger = LogFactory.getLog(TransactionContext.class);

private final TestContext testContext;

private final TransactionDefinition transactionDefinition;

private final PlatformTransactionManager transactionManager;

private final boolean defaultRollback;

private boolean flaggedForRollback;

private TransactionStatus transactionStatus;

private volatile int transactionsStarted = 0;


TransactionContext(TestContext testContext, PlatformTransactionManager transactionManager,
TransactionDefinition transactionDefinition, boolean defaultRollback) {
this.testContext = testContext;
this.transactionManager = transactionManager;
this.transactionDefinition = transactionDefinition;
this.defaultRollback = defaultRollback;
this.flaggedForRollback = defaultRollback;
}

TransactionStatus getTransactionStatus() {
return this.transactionStatus;
}

/**
* Has the current transaction been flagged for rollback?
* <p>In other words, should we roll back or commit the current transaction
* upon completion of the current test?
*/
boolean isFlaggedForRollback() {
return this.flaggedForRollback;
}

void setFlaggedForRollback(boolean flaggedForRollback) {
if (this.transactionStatus == null) {
throw new IllegalStateException(String.format(
"Failed to set rollback flag for test context %s: transaction does not exist.", this.testContext));
}
this.flaggedForRollback = flaggedForRollback;
}

/**
* Start a new transaction for the configured {@linkplain #getTestContext test context}.
* <p>Only call this method if {@link #endTransaction} has been called or if no
* transaction has been previously started.
* @throws TransactionException if starting the transaction fails
*/
void startTransaction() {
if (this.transactionStatus != null) {
throw new IllegalStateException(
"Cannot start a new transaction without ending the existing transaction first.");
}
this.flaggedForRollback = this.defaultRollback;
this.transactionStatus = this.transactionManager.getTransaction(this.transactionDefinition);
++this.transactionsStarted;
if (logger.isInfoEnabled()) {
logger.info(String.format(
"Began transaction (%s) for test context %s; transaction manager [%s]; rollback [%s]",
this.transactionsStarted, this.testContext, this.transactionManager, flaggedForRollback));
}
}

/**
* Immediately force a <em>commit</em> or <em>rollback</em> of the transaction
* for the configured {@linkplain #getTestContext test context}, according to
* the {@linkplain #isFlaggedForRollback rollback flag}.
*/
void endTransaction() {
if (logger.isTraceEnabled()) {
logger.trace(String.format(
"Ending transaction for test context %s; transaction status [%s]; rollback [%s]", this.testContext,
this.transactionStatus, flaggedForRollback));
}
if (this.transactionStatus == null) {
throw new IllegalStateException(String.format(
"Failed to end transaction for test context %s: transaction does not exist.", this.testContext));
}

try {
if (flaggedForRollback) {
this.transactionManager.rollback(this.transactionStatus);
}
else {
this.transactionManager.commit(this.transactionStatus);
}
}
finally {
this.transactionStatus = null;
}

if (logger.isInfoEnabled()) {
logger.info(String.format("%s transaction after test execution for test context %s.",
(flaggedForRollback ? "Rolled back" : "Committed"), this.testContext));
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2002-2014 the original author or authors.
*
* Licensed 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.springframework.test.context.transaction;

import org.springframework.core.NamedInheritableThreadLocal;

/**
* {@link InheritableThreadLocal}-based holder for the current {@link TransactionContext}.
*
* @author Sam Brannen
* @since 4.1
*/
class TransactionContextHolder {

private static final ThreadLocal<TransactionContext> currentTransactionContext = new NamedInheritableThreadLocal<TransactionContext>(
"Test Transaction Context");


static TransactionContext getCurrentTransactionContext() {
return currentTransactionContext.get();
}

static void setCurrentTransactionContext(TransactionContext transactionContext) {
currentTransactionContext.set(transactionContext);
}

static TransactionContext removeCurrentTransactionContext() {
synchronized (currentTransactionContext) {
TransactionContext transactionContext = currentTransactionContext.get();
currentTransactionContext.remove();
return transactionContext;
}
}

}
Loading

0 comments on commit 90f0d14

Please sign in to comment.