1 /*
2  * Copyright (C) 2010 Google, Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.inject.persist.jpa;
18 
19 import com.google.inject.Inject;
20 import com.google.inject.persist.Transactional;
21 import com.google.inject.persist.UnitOfWork;
22 import java.lang.reflect.Method;
23 import javax.persistence.EntityManager;
24 import javax.persistence.EntityTransaction;
25 import org.aopalliance.intercept.MethodInterceptor;
26 import org.aopalliance.intercept.MethodInvocation;
27 
28 /** @author Dhanji R. Prasanna (dhanji@gmail.com) */
29 class JpaLocalTxnInterceptor implements MethodInterceptor {
30 
31   // TODO(gak): Move these args to the cxtor & make these final.
32   @Inject private JpaPersistService emProvider = null;
33 
34   @Inject private UnitOfWork unitOfWork = null;
35 
36   @Transactional
37   private static class Internal {}
38 
39   // Tracks if the unit of work was begun implicitly by this transaction.
40   private final ThreadLocal<Boolean> didWeStartWork = new ThreadLocal<>();
41 
42   @Override
invoke(MethodInvocation methodInvocation)43   public Object invoke(MethodInvocation methodInvocation) throws Throwable {
44 
45     // Should we start a unit of work?
46     if (!emProvider.isWorking()) {
47       emProvider.begin();
48       didWeStartWork.set(true);
49     }
50 
51     Transactional transactional = readTransactionMetadata(methodInvocation);
52     EntityManager em = this.emProvider.get();
53 
54     // Allow 'joining' of transactions if there is an enclosing @Transactional method.
55     if (em.getTransaction().isActive()) {
56       return methodInvocation.proceed();
57     }
58 
59     final EntityTransaction txn = em.getTransaction();
60     txn.begin();
61 
62     Object result;
63     try {
64       result = methodInvocation.proceed();
65 
66     } catch (Exception e) {
67       //commit transaction only if rollback didnt occur
68       if (rollbackIfNecessary(transactional, e, txn)) {
69         txn.commit();
70       }
71 
72       //propagate whatever exception is thrown anyway
73       throw e;
74     } finally {
75       // Close the em if necessary (guarded so this code doesn't run unless catch fired).
76       if (null != didWeStartWork.get() && !txn.isActive()) {
77         didWeStartWork.remove();
78         unitOfWork.end();
79       }
80     }
81 
82     //everything was normal so commit the txn (do not move into try block above as it
83     //  interferes with the advised method's throwing semantics)
84     try {
85       txn.commit();
86     } finally {
87       //close the em if necessary
88       if (null != didWeStartWork.get()) {
89         didWeStartWork.remove();
90         unitOfWork.end();
91       }
92     }
93 
94     //or return result
95     return result;
96   }
97 
98   // TODO(dhanji): Cache this method's results.
readTransactionMetadata(MethodInvocation methodInvocation)99   private Transactional readTransactionMetadata(MethodInvocation methodInvocation) {
100     Transactional transactional;
101     Method method = methodInvocation.getMethod();
102     Class<?> targetClass = methodInvocation.getThis().getClass();
103 
104     transactional = method.getAnnotation(Transactional.class);
105     if (null == transactional) {
106       // If none on method, try the class.
107       transactional = targetClass.getAnnotation(Transactional.class);
108     }
109     if (null == transactional) {
110       // If there is no transactional annotation present, use the default
111       transactional = Internal.class.getAnnotation(Transactional.class);
112     }
113 
114     return transactional;
115   }
116 
117   /**
118    * Returns True if rollback DID NOT HAPPEN (i.e. if commit should continue).
119    *
120    * @param transactional The metadata annotaiton of the method
121    * @param e The exception to test for rollback
122    * @param txn A JPA Transaction to issue rollbacks on
123    */
rollbackIfNecessary( Transactional transactional, Exception e, EntityTransaction txn)124   private boolean rollbackIfNecessary(
125       Transactional transactional, Exception e, EntityTransaction txn) {
126     boolean commit = true;
127 
128     //check rollback clauses
129     for (Class<? extends Exception> rollBackOn : transactional.rollbackOn()) {
130 
131       //if one matched, try to perform a rollback
132       if (rollBackOn.isInstance(e)) {
133         commit = false;
134 
135         //check ignore clauses (supercedes rollback clause)
136         for (Class<? extends Exception> exceptOn : transactional.ignore()) {
137           //An exception to the rollback clause was found, DON'T rollback
138           // (i.e. commit and throw anyway)
139           if (exceptOn.isInstance(e)) {
140             commit = true;
141             break;
142           }
143         }
144 
145         //rollback only if nothing matched the ignore check
146         if (!commit) {
147           txn.rollback();
148         }
149         //otherwise continue to commit
150 
151         break;
152       }
153     }
154 
155     return commit;
156   }
157 }
158