001    /**
002     *  Licensed to the Apache Software Foundation (ASF) under one or more
003     *  contributor license agreements.  See the NOTICE file distributed with
004     *  this work for additional information regarding copyright ownership.
005     *  The ASF licenses this file to You under the Apache License, Version 2.0
006     *  (the "License"); you may not use this file except in compliance with
007     *  the License.  You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     *  Unless required by applicable law or agreed to in writing, software
012     *  distributed under the License is distributed on an "AS IS" BASIS,
013     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     *  See the License for the specific language governing permissions and
015     *  limitations under the License.
016     */
017    
018    package org.apache.geronimo.transaction.manager;
019    
020    import java.util.ArrayList;
021    import java.util.HashMap;
022    import java.util.Iterator;
023    import java.util.List;
024    import java.util.Map;
025    
026    import javax.transaction.*;
027    import javax.transaction.xa.XAException;
028    import javax.transaction.xa.Xid;
029    
030    import java.util.concurrent.ConcurrentHashMap;
031    import java.util.concurrent.CopyOnWriteArrayList;
032    import org.apache.commons.logging.Log;
033    import org.apache.commons.logging.LogFactory;
034    import org.apache.geronimo.transaction.log.UnrecoverableLog;
035    
036    /**
037     * Simple implementation of a transaction manager.
038     *
039     * @version $Rev: 552073 $ $Date: 2007-06-29 21:10:51 -0400 (Fri, 29 Jun 2007) $
040     */
041    public class TransactionManagerImpl implements TransactionManager, UserTransaction, TransactionSynchronizationRegistry, XidImporter, MonitorableTransactionManager, RecoverableTransactionManager {
042        private static final Log log = LogFactory.getLog(TransactionManagerImpl.class);
043        protected static final int DEFAULT_TIMEOUT = 600;
044        protected static final byte[] DEFAULT_TM_ID = new byte[] {71,84,77,73,68};
045    
046        final TransactionLog transactionLog;
047        final XidFactory xidFactory;
048        private final int defaultTransactionTimeoutMilliseconds;
049        private final ThreadLocal transactionTimeoutMilliseconds = new ThreadLocal();
050        private final ThreadLocal threadTx = new ThreadLocal();
051        private final ConcurrentHashMap associatedTransactions = new ConcurrentHashMap();
052        private static final Log recoveryLog = LogFactory.getLog("RecoveryController");
053        final Recovery recovery;
054        private final CopyOnWriteArrayList transactionAssociationListeners = new CopyOnWriteArrayList();
055        private List recoveryErrors = new ArrayList();
056    
057        public TransactionManagerImpl() throws XAException {
058            this(DEFAULT_TIMEOUT,
059                    null,
060                    null
061            );
062        }
063    
064        public TransactionManagerImpl(int defaultTransactionTimeoutSeconds) throws XAException {
065            this(defaultTransactionTimeoutSeconds,
066                    null,
067                    null
068            );
069        }
070    
071        public TransactionManagerImpl(int defaultTransactionTimeoutSeconds, TransactionLog transactionLog) throws XAException {
072            this(defaultTransactionTimeoutSeconds,
073                    null,
074                    transactionLog
075            );
076        }
077    
078        public TransactionManagerImpl(int defaultTransactionTimeoutSeconds, XidFactory xidFactory, TransactionLog transactionLog) throws XAException {
079            if (defaultTransactionTimeoutSeconds <= 0) {
080                throw new IllegalArgumentException("defaultTransactionTimeoutSeconds must be positive: attempted value: " + defaultTransactionTimeoutSeconds);
081            }
082    
083            this.defaultTransactionTimeoutMilliseconds = defaultTransactionTimeoutSeconds * 1000;
084    
085            if (transactionLog == null) {
086                this.transactionLog = new UnrecoverableLog();
087            } else {
088                this.transactionLog = transactionLog;
089            }
090    
091            if (xidFactory != null) {
092                this.xidFactory = xidFactory;
093            } else {
094                this.xidFactory = new XidFactoryImpl(DEFAULT_TM_ID);
095            }
096    
097            recovery = new RecoveryImpl(this.transactionLog, this.xidFactory);
098        }
099    
100        public Transaction getTransaction() {
101            return (Transaction) threadTx.get();
102        }
103    
104        private void associate(TransactionImpl tx) throws InvalidTransactionException {
105            if (tx == null) throw new NullPointerException("tx is null");
106    
107            Object existingAssociation = associatedTransactions.putIfAbsent(tx, Thread.currentThread());
108            if (existingAssociation != null) {
109                throw new InvalidTransactionException("Specified transaction is already associated with another thread");
110            }
111            threadTx.set(tx);
112            fireThreadAssociated(tx);
113        }
114    
115        private void unassociate() {
116            Transaction tx = getTransaction();
117            if (tx != null) {
118                associatedTransactions.remove(tx);
119                threadTx.set(null);
120                fireThreadUnassociated(tx);
121            }
122        }
123    
124        public void setTransactionTimeout(int seconds) throws SystemException {
125            if (seconds < 0) {
126                throw new SystemException("transaction timeout must be positive or 0 to reset to default");
127            }
128            if (seconds == 0) {
129                transactionTimeoutMilliseconds.set(null);
130            } else {
131                transactionTimeoutMilliseconds.set(new Long(seconds * 1000));
132            }
133        }
134    
135        public int getStatus() throws SystemException {
136            Transaction tx = getTransaction();
137            return (tx != null) ? tx.getStatus() : Status.STATUS_NO_TRANSACTION;
138        }
139    
140        public void begin() throws NotSupportedException, SystemException {
141            begin(getTransactionTimeoutMilliseconds(0L));
142        }
143    
144        public Transaction begin(long transactionTimeoutMilliseconds) throws NotSupportedException, SystemException {
145            if (getStatus() != Status.STATUS_NO_TRANSACTION) {
146                throw new NotSupportedException("Nested Transactions are not supported");
147            }
148            TransactionImpl tx = new TransactionImpl(xidFactory, transactionLog, getTransactionTimeoutMilliseconds(transactionTimeoutMilliseconds));
149    //        timeoutTimer.schedule(tx, getTransactionTimeoutMilliseconds(transactionTimeoutMilliseconds));
150            try {
151                associate(tx);
152            } catch (InvalidTransactionException e) {
153                // should not be possible since we just created that transaction and no one has a reference yet
154                throw (SystemException)new SystemException("Internal error: associate threw an InvalidTransactionException for a newly created transaction").initCause(e);
155            }
156            // Todo: Verify if this is correct thing to do. Use default timeout for next transaction.
157            this.transactionTimeoutMilliseconds.set(null);
158            return tx;
159        }
160    
161        public Transaction suspend() throws SystemException {
162            Transaction tx = getTransaction();
163            if (tx != null) {
164                unassociate();
165            }
166            return tx;
167        }
168    
169        public void resume(Transaction tx) throws IllegalStateException, InvalidTransactionException, SystemException {
170            if (getTransaction() != null) {
171                throw new IllegalStateException("Thread already associated with another transaction");
172            }
173            if (!(tx instanceof TransactionImpl)) {
174                throw new InvalidTransactionException("Cannot resume foreign transaction: " + tx);
175            }
176            associate((TransactionImpl) tx);
177        }
178    
179        public Object getResource(Object key) {
180            TransactionImpl tx = getActiveTransactionImpl();
181            return tx.getResource(key);
182        }
183    
184        private TransactionImpl getActiveTransactionImpl() {
185            TransactionImpl tx = (TransactionImpl)threadTx.get();
186            if (tx == null) {
187                throw new IllegalStateException("No tx on thread");
188            }
189            if (tx.getStatus() != Status.STATUS_ACTIVE && tx.getStatus() != Status.STATUS_MARKED_ROLLBACK) {
190                throw new IllegalStateException("Transaction " + tx + " is not active");
191            }
192            return tx;
193        }
194    
195        public boolean getRollbackOnly() {
196            TransactionImpl tx = getActiveTransactionImpl();
197            return tx.getRollbackOnly();
198        }
199    
200        public Object getTransactionKey() {
201            TransactionImpl tx = getActiveTransactionImpl();
202            return tx.getTransactionKey();
203        }
204    
205        public int getTransactionStatus() {
206            TransactionImpl tx = (TransactionImpl) getTransaction();
207            return tx == null? Status.STATUS_NO_TRANSACTION: tx.getTransactionStatus();
208        }
209    
210        public void putResource(Object key, Object value) {
211            TransactionImpl tx = getActiveTransactionImpl();
212            tx.putResource(key, value);
213        }
214    
215        /**
216         * jta 1.1 method so the jpa implementations can be told to flush their caches.
217         * @param synchronization
218         */
219        public void registerInterposedSynchronization(Synchronization synchronization) {
220            TransactionImpl tx = getActiveTransactionImpl();
221            tx.registerInterposedSynchronization(synchronization);
222        }
223    
224        public void setRollbackOnly() throws IllegalStateException {
225            TransactionImpl tx = (TransactionImpl) threadTx.get();
226            if (tx == null) {
227                throw new IllegalStateException("No transaction associated with current thread");
228            }
229            tx.setRollbackOnly();
230        }
231    
232        public void commit() throws HeuristicMixedException, HeuristicRollbackException, IllegalStateException, RollbackException, SecurityException, SystemException {
233            Transaction tx = getTransaction();
234            if (tx == null) {
235                throw new IllegalStateException("No transaction associated with current thread");
236            }
237            try {
238                tx.commit();
239            } finally {
240                unassociate();
241            }
242        }
243    
244        public void rollback() throws IllegalStateException, SecurityException, SystemException {
245            Transaction tx = getTransaction();
246            if (tx == null) {
247                throw new IllegalStateException("No transaction associated with current thread");
248            }
249            try {
250                tx.rollback();
251            } finally {
252                unassociate();
253            }
254        }
255    
256        //XidImporter implementation
257        public Transaction importXid(Xid xid, long transactionTimeoutMilliseconds) throws XAException, SystemException {
258            if (transactionTimeoutMilliseconds < 0) {
259                throw new SystemException("transaction timeout must be positive or 0 to reset to default");
260            }
261            TransactionImpl tx = new TransactionImpl(xid, xidFactory, transactionLog, getTransactionTimeoutMilliseconds(transactionTimeoutMilliseconds));
262            return tx;
263        }
264    
265        public void commit(Transaction tx, boolean onePhase) throws XAException {
266            if (onePhase) {
267                try {
268                    tx.commit();
269                } catch (HeuristicMixedException e) {
270                    throw (XAException) new XAException().initCause(e);
271                } catch (HeuristicRollbackException e) {
272                    throw (XAException) new XAException().initCause(e);
273                } catch (RollbackException e) {
274                    throw (XAException) new XAException().initCause(e);
275                } catch (SecurityException e) {
276                    throw (XAException) new XAException().initCause(e);
277                } catch (SystemException e) {
278                    throw (XAException) new XAException().initCause(e);
279                }
280            } else {
281                try {
282                    ((TransactionImpl) tx).preparedCommit();
283                } catch (SystemException e) {
284                    throw (XAException) new XAException().initCause(e);
285                }
286            }
287        }
288    
289        public void forget(Transaction tx) throws XAException {
290            //TODO implement this!
291        }
292    
293        public int prepare(Transaction tx) throws XAException {
294            try {
295                return ((TransactionImpl) tx).prepare();
296            } catch (SystemException e) {
297                throw (XAException) new XAException().initCause(e);
298            } catch (RollbackException e) {
299                throw (XAException) new XAException().initCause(e);
300            }
301        }
302    
303        public void rollback(Transaction tx) throws XAException {
304            try {
305                tx.rollback();
306            } catch (IllegalStateException e) {
307                throw (XAException) new XAException().initCause(e);
308            } catch (SystemException e) {
309                throw (XAException) new XAException().initCause(e);
310            }
311        }
312    
313        long getTransactionTimeoutMilliseconds(long transactionTimeoutMilliseconds) {
314            if (transactionTimeoutMilliseconds != 0) {
315                return transactionTimeoutMilliseconds;
316            }
317            Long timeout = (Long) this.transactionTimeoutMilliseconds.get();
318            if (timeout != null) {
319                return timeout.longValue();
320            }
321            return defaultTransactionTimeoutMilliseconds;
322        }
323    
324        //Recovery
325        public void recoveryError(Exception e) {
326            recoveryLog.error(e);
327            recoveryErrors.add(e);
328        }
329    
330        public void recoverResourceManager(NamedXAResource xaResource) {
331            try {
332                recovery.recoverResourceManager(xaResource);
333            } catch (XAException e) {
334                recoveryError(e);
335            }
336        }
337    
338        public Map getExternalXids() {
339            return new HashMap(recovery.getExternalXids());
340        }
341    
342        public void addTransactionAssociationListener(TransactionManagerMonitor listener) {
343            transactionAssociationListeners.addIfAbsent(listener);
344        }
345    
346        public void removeTransactionAssociationListener(TransactionManagerMonitor listener) {
347            transactionAssociationListeners.remove(listener);
348        }
349    
350        protected void fireThreadAssociated(Transaction tx) {
351            for (Iterator iterator = transactionAssociationListeners.iterator(); iterator.hasNext();) {
352                TransactionManagerMonitor listener = (TransactionManagerMonitor) iterator.next();
353                try {
354                    listener.threadAssociated(tx);
355                } catch (Exception e) {
356                    log.warn("Error calling transaction association listener", e);
357                }
358            }
359        }
360    
361        protected void fireThreadUnassociated(Transaction tx) {
362            for (Iterator iterator = transactionAssociationListeners.iterator(); iterator.hasNext();) {
363                TransactionManagerMonitor listener = (TransactionManagerMonitor) iterator.next();
364                try {
365                    listener.threadUnassociated(tx);
366                } catch (Exception e) {
367                    log.warn("Error calling transaction association listener", e);
368                }
369            }
370        }
371    }