    001/**
    002 * Licensed under the Apache License, Version 2.0 (the "License");
    003 * you may not use this file except in compliance with the License.
    004 * You may obtain a copy of the License at
    005 *
    006 *     http://www.apache.org/licenses/LICENSE-2.0
    007 *
    008 * Unless required by applicable law or agreed to in writing, software
    009 * distributed under the License is distributed on an "AS IS" BASIS,
    010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    011 * See the License for the specific language governing permissions and
    012 * limitations under the License.
    013 */
    014package org.apache.tapestry5.internal.jpa;
    015
    016import java.util.ArrayDeque;
    017import java.util.ArrayList;
    018import java.util.Deque;
    019import java.util.Iterator;
    020import java.util.List;
    021
    022import javax.persistence.EntityManager;
    023import javax.persistence.EntityTransaction;
    024
    025import org.apache.tapestry5.ioc.Invokable;
    026import org.apache.tapestry5.jpa.EntityTransactionManager.VoidInvokable;
    027import org.slf4j.Logger;
    028
    029public class PersistenceContextSpecificEntityTransactionManager
    030{
    031
    032    private final Logger logger;
    033    private final EntityManager entityManager;
    034
    035    private boolean transactionBeingCommitted;
    036
    037    private Deque<Invokable<?>> invokableUnitsForSequentialTransactions = new ArrayDeque<Invokable<?>>();
    038    private Deque<Invokable<?>> invokableUnits = new ArrayDeque<Invokable<?>>();
    039
    040    private List<Invokable<Boolean>> beforeCommitInvokables = new ArrayList<Invokable<Boolean>>();
    041    private List<Invokable<Boolean>> afterCommitInvokables = new ArrayList<Invokable<Boolean>>();
    042
    043    public PersistenceContextSpecificEntityTransactionManager(Logger logger,
    044            EntityManager entityManager)
    045    {
    046        this.logger = logger;
    047        this.entityManager = entityManager;
    048    }
    049
    050    private EntityTransaction getTransaction()
    051    {
    052        EntityTransaction transaction = entityManager.getTransaction();
    053        if (!transaction.isActive())
    054            transaction.begin();
    055        return transaction;
    056    }
    057
    058    public void addBeforeCommitInvokable(Invokable<Boolean> invokable)
    059    {
    060        beforeCommitInvokables.add(invokable);
    061    }
    062
    063    public void addAfterCommitInvokable(Invokable<Boolean> invokable)
    064    {
    065        afterCommitInvokables.add(invokable);
    066    }
    067
    068    public <T> T invokeInTransaction(Invokable<T> invokable)
    069    {
    070        if (transactionBeingCommitted)
    071        {
    072            // happens for example if you try to run a transaction in @PostCommit hook. We can only
    073            // allow VoidInvokables
    074            // to be executed later
    075            if (invokable instanceof VoidInvokable)
    076            {
    077                invokableUnitsForSequentialTransactions.push(invokable);
    078                return null;
    079            }
    080            else
    081            {
    082                rollbackTransaction(getTransaction());
    083                throw new RuntimeException(
    084                        "Current transaction is already being committed. Transactions started @PostCommit are not allowed to return a value");
    085            }
    086        }
    087
    088        final boolean topLevel = invokableUnits.isEmpty();
    089        invokableUnits.push(invokable);
    090        if (!topLevel)
    091        {
    092            if (logger.isWarnEnabled())
    093            {
    094                logger.warn("Nested transaction detected, current depth = " + invokableUnits.size());
    095            }
    096        }
    097
    098        final EntityTransaction transaction = getTransaction();
    099        try
    100        {
    101            T result = invokable.invoke();
    102
    103            if (topLevel && invokableUnits.peek().equals(invokable))
    104            {
    105                // Success or checked exception:
    106
    107                if (transaction.isActive())
    108                {
    109                    invokeBeforeCommit(transaction);
    110                }
    111
    112                // FIXME check if we are still on top
    113
    114                if (transaction.isActive())
    115                {
    116                    transactionBeingCommitted = true;
    117                    transaction.commit();
    118                    transactionBeingCommitted = false;
    119                    invokableUnits.clear();
    120                    invokeAfterCommit();
    121                    if (invokableUnitsForSequentialTransactions.size() > 0)
    122                        invokeInTransaction(invokableUnitsForSequentialTransactions.pop());
    123                }
    124            }
    125
    126            return result;
    127        }
    128        catch (final RuntimeException e)
    129        {
    130            if (transaction != null && transaction.isActive())
    131            {
    132                rollbackTransaction(transaction);
    133            }
    134
    135            throw e;
    136        }
    137        finally
    138        {
    139            invokableUnits.remove(invokable);
    140        }
    141    }
    142
    143    private void invokeBeforeCommit(final EntityTransaction transaction)
    144    {
    145        for (Iterator<Invokable<Boolean>> i = beforeCommitInvokables.iterator(); i.hasNext();)
    146        {
    147            Invokable<Boolean> invokable = i.next();
    148            i.remove();
    149            Boolean beforeCommitSucceeded = tryInvoke(transaction, invokable);
    150
    151            // Success or checked exception:
    152            if (beforeCommitSucceeded != null && !beforeCommitSucceeded.booleanValue())
    153            {
    154                rollbackTransaction(transaction);
    155
    156                // Don't invoke further callbacks
    157                break;
    158            }
    159        }
    160    }
    161
    162    private void invokeAfterCommit()
    163    {
    164
    165        for (Iterator<Invokable<Boolean>> i = afterCommitInvokables.iterator(); i.hasNext();)
    166        {
    167            Invokable<Boolean> invokable = i.next();
    168            i.remove();
    169            Boolean afterCommitSucceeded = invokable.invoke();
    170
    171            // Success or checked exception:
    172            if (afterCommitSucceeded != null && !afterCommitSucceeded.booleanValue())
    173            {
    174                if (invokableUnitsForSequentialTransactions.size() > 0) { throw new RuntimeException(
    175                        "After commit hook returned false but there are still uncommitted Invokables scheduled for the next transaction"); }
    176                return;
    177            }
    178        }
    179    }
    180
    181    private static <T> T tryInvoke(final EntityTransaction transaction, Invokable<T> invokable)
    182            throws RuntimeException
    183    {
    184        T result;
    185
    186        try
    187        {
    188            result = invokable.invoke();
    189        }
    190        catch (final RuntimeException e)
    191        {
    192            if (transaction != null && transaction.isActive())
    193            {
    194                rollbackTransaction(transaction);
    195            }
    196
    197            throw e;
    198        }
    199
    200        return result;
    201    }
    202
    203    private static void rollbackTransaction(EntityTransaction transaction)
    204    {
    205        try
    206        {
    207            transaction.rollback();
    208        }
    209        catch (Exception e)
    210        { // Ignore
    211        }
    212    }
    213}