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}