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}