/* * #%L * Wisdom-Framework * %% * Copyright (C) 2013 - 2014 Wisdom Framework * %% * 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. * #L% */ package org.wisdom.framework.transaction.impl; import com.google.common.collect.ArrayListMultimap; import org.wisdom.framework.transaction.Propagation; import javax.transaction.*; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; /** * Class managing transaction boundaries and action to perform when we enter / leave a transactional bloc. */ public class PropagationManager implements Synchronization { private final TransactionManager manager; Set<Transaction> transactions = new LinkedHashSet<>(); Set<Transaction> owned = new LinkedHashSet<>(); ArrayListMultimap<Thread, Transaction> suspended = ArrayListMultimap.create(); /** * Creates a new {@link org.wisdom.framework.transaction.impl.PropagationManager}. * * @param manager the transaction manager. */ public PropagationManager(TransactionManager manager) { this.manager = manager; } /** * Checks whether or not we have an active transaction. If so, returns it. * * @return the activate transaction, {@code null} if none. * @throws SystemException thrown by the transaction manager to indicate that it has encountered an * unexpected error condition that prevents future transaction services from * proceeding. */ private Transaction getActiveTransaction() throws SystemException { Transaction tx = manager.getTransaction(); if (tx != null && tx.getStatus() != Status.STATUS_NO_TRANSACTION) { return tx; } else { return null; } } /** * Enters a transactional bloc. * * @param propagation the propagation strategy * @param timeout the transaction timeout * @param interceptionId an identifier for the interception, used for logging. * @throws SystemException thrown by the transaction manager to indicate that it has encountered an * unexpected error condition that prevents future transaction services from * proceeding. * @throws NotSupportedException indicates that the request cannot be executed because the operation is not a * supported feature. * @throws RollbackException thrown when the transaction has been marked for rollback only or the transaction * has been rolled back instead of committed. */ public void onEntry(Propagation propagation, int timeout, String interceptionId) throws SystemException, NotSupportedException, RollbackException { Transaction transaction = getActiveTransaction(); switch (propagation) { case REQUIRES: // Are we already in a transaction? if (transaction == null) { // No, create one if (timeout > 0) { manager.setTransactionTimeout(timeout); } manager.begin(); Transaction tx = getActiveTransaction(); tx.registerSynchronization(this); owned.add(tx); } else { // Add the transaction to the transaction list transactions.add(transaction); } break; case MANDATORY: if (transaction == null) { // Error throw new IllegalStateException("The " + interceptionId + " must be called inside a " + "JTA transaction"); } else { if (transactions.add(transaction)) { transaction.registerSynchronization(this); } } break; case SUPPORTED: // if transaction != null, register the callback, else do nothing if (transaction != null) { if (transactions.add(transaction)) { transaction.registerSynchronization(this); } } // Else do nothing. break; case NOT_SUPPORTED: if (transaction != null) { // Suspend the current transaction. suspended.put(Thread.currentThread(), transaction); manager.suspend(); } break; case NEVER: if (transaction != null) { throw new IllegalStateException("The " + interceptionId + " must never be called inside a transaction"); } break; case REQUIRES_NEW: if (transaction == null) { // No current transaction, Just creates a new one if (timeout > 0) { manager.setTransactionTimeout(timeout); } manager.begin(); Transaction tx = getActiveTransaction(); owned.add(tx); } else { // suspend the current transaction suspended.put(Thread.currentThread(), manager.suspend()); if (timeout > 0) { manager.setTransactionTimeout(timeout); } manager.begin(); owned.add(manager.getTransaction()); } break; default: throw new UnsupportedOperationException("Unknown or unsupported propagation policy for " + interceptionId + " :" + propagation); } } /** * Leaves a transactional bloc. This method decides what do to with the current transaction. This includes * committing or resuming a transaction. * * @param propagation the propagation strategy * @param interceptionId an identifier for the interception, used for logging. * @param callback the transaction callback * @throws HeuristicRollbackException thrown by the commit operation to report that a heuristic decision was * made and that all relevant updates have been rolled back. * @throws HeuristicMixedException report that a heuristic decision was made and that some relevant updates have * been committed and others have been rolled back * @throws SystemException thrown by the transaction manager to indicate that it has encountered an * unexpected error condition that prevents future transaction services from * proceeding. * @throws InvalidTransactionException the current transaction is invalid */ public void onExit(Propagation propagation, String interceptionId, TransactionCallback callback) throws HeuristicRollbackException, HeuristicMixedException, SystemException, InvalidTransactionException { Transaction current = getActiveTransaction(); if (callback == null) { callback = new TransactionCallback() { @Override public void transactionCommitted(Transaction transaction) { // Do nothing } @Override public void transactionRolledBack(Transaction transaction) { // Do nothing } }; } switch (propagation) { case REQUIRES: // Are we the owner of the transaction? if (owned.contains(current)) { // Owner. try { current.commit(); // Commit the transaction owned.remove(current); callback.transactionCommitted(current); } catch (RollbackException e) { owned.remove(current); e.printStackTrace(); callback.transactionRolledBack(current); } } // Else wait for commit. break; case MANDATORY: // We are never the owner, so just exits the transaction. break; case SUPPORTED: // Do nothing. break; case NOT_SUPPORTED: // We may have suspended a transaction if one, resume it // If we have another transaction and we have suspended a transaction, // throw an IllegalStateException because it's impossible to resume // the suspended transaction. If we didn't suspend a transaction, accept the new transaction (user // responsibility) List<Transaction> susp = suspended.get(Thread.currentThread()); if (current != null && !susp.isEmpty()) { throw new IllegalStateException("Error while handling " + interceptionId + " : you cannot start a" + " transaction after having suspended one. We would not be able to resume the suspended " + "transaction"); } else if (current == null && !susp.isEmpty()) { manager.resume(susp.remove(susp.size() - 1)); } break; case NEVER: // Do nothing. break; case REQUIRES_NEW: // We're necessary the owner. try { current.commit(); // Commit the transaction owned.remove(current); callback.transactionCommitted(current); List<Transaction> suspendedTransactions = suspended.get(Thread.currentThread()); if (suspendedTransactions != null && !suspendedTransactions.isEmpty()) { // suspend the completed transaction. Transaction trans = suspendedTransactions.get(suspendedTransactions.size() - 1); manager.suspend(); suspendedTransactions.remove(trans); manager.resume(trans); } } catch (RollbackException e) { // The transaction was rolledback rather than committed owned.remove(current); callback.transactionRolledBack(current); List<Transaction> suspendedTransactions = suspended.get(Thread.currentThread()); if (suspendedTransactions != null && !suspendedTransactions.isEmpty()) { // suspend the transaction. Transaction trans = suspendedTransactions.get(suspendedTransactions.size() - 1); manager.suspend(); suspendedTransactions.remove(trans); manager.resume(trans); } } break; default: throw new UnsupportedOperationException("Unknown or unsupported propagation policy for " + interceptionId + " :" + propagation); } } /** * Default callback. */ @Override public void beforeCompletion() { } /** * Default callback * * @param status transaction status */ @Override public void afterCompletion(int status) { } /** * A transactional bloc has thrown an exception. This method decides what needs to be done in that case. * * @param e the exception * @param propagation the propagation strategy * @param noRollbackFor the set of exceptions that does not make the current transaction to rollback * @param rollbackFor the set of exceptions that makes the current transaction to rollback * @param interceptionId an identifier for the interception, used for logging. * @param callback the transaction callback * @throws SystemException thrown by the transaction manager to indicate that it has encountered an * unexpected error condition that prevents future transaction services from * proceeding. * @throws HeuristicRollbackException thrown by the commit operation to report that a heuristic decision was made * and that all relevant updates have been rolled back. * @throws HeuristicMixedException thrown to report that a heuristic decision was made and that some relevant * updates have been committed and others have been rolled back. * @throws InvalidTransactionException the request carried an invalid transaction context. */ public void onError(Exception e, Propagation propagation, Class<? extends Exception>[] noRollbackFor, Class<? extends Exception>[] rollbackFor, String interceptionId, TransactionCallback callback) throws SystemException, HeuristicRollbackException, HeuristicMixedException, InvalidTransactionException { Transaction current = getActiveTransaction(); if (current != null) { // We have a transaction. // Check whether or not the transaction needs to be marked as rollback only. if (!Arrays.asList(noRollbackFor).contains(e.getClass())) { if (Arrays.asList(rollbackFor).contains(e.getClass()) || rollbackFor.length == 0) { current.setRollbackOnly(); } } onExit(propagation, interceptionId, callback); } } }