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 */
017package org.apache.activemq;
018
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.HashMap;
022import java.util.List;
023
024import javax.jms.JMSException;
025import javax.jms.TransactionInProgressException;
026import javax.jms.TransactionRolledBackException;
027import javax.transaction.xa.XAException;
028import javax.transaction.xa.XAResource;
029import javax.transaction.xa.Xid;
030
031import org.apache.activemq.command.ConnectionId;
032import org.apache.activemq.command.DataArrayResponse;
033import org.apache.activemq.command.DataStructure;
034import org.apache.activemq.command.IntegerResponse;
035import org.apache.activemq.command.LocalTransactionId;
036import org.apache.activemq.command.TransactionId;
037import org.apache.activemq.command.TransactionInfo;
038import org.apache.activemq.command.XATransactionId;
039import org.apache.activemq.transaction.Synchronization;
040import org.apache.activemq.util.JMSExceptionSupport;
041import org.apache.activemq.util.LongSequenceGenerator;
042import org.apache.activemq.util.XASupport;
043import org.slf4j.Logger;
044import org.slf4j.LoggerFactory;
045
046/**
047 * A TransactionContext provides the means to control a JMS transaction. It
048 * provides a local transaction interface and also an XAResource interface. <p/>
049 * An application server controls the transactional assignment of an XASession
050 * by obtaining its XAResource. It uses the XAResource to assign the session to
051 * a transaction, prepare and commit work on the transaction, and so on. <p/> An
052 * XAResource provides some fairly sophisticated facilities for interleaving
053 * work on multiple transactions, recovering a list of transactions in progress,
054 * and so on. A JTA aware JMS provider must fully implement this functionality.
055 * This could be done by using the services of a database that supports XA, or a
056 * JMS provider may choose to implement this functionality from scratch. <p/>
057 *
058 *
059 * @see javax.jms.Session
060 * @see javax.jms.QueueSession
061 * @see javax.jms.TopicSession
062 * @see javax.jms.XASession
063 */
064public class TransactionContext implements XAResource {
065
066    public static final String xaErrorCodeMarker = "xaErrorCode:";
067    private static final Logger LOG = LoggerFactory.getLogger(TransactionContext.class);
068
069    // XATransactionId -> ArrayList of TransactionContext objects
070    private final static HashMap<TransactionId, List<TransactionContext>> ENDED_XA_TRANSACTION_CONTEXTS =
071            new HashMap<TransactionId, List<TransactionContext>>();
072
073    private ActiveMQConnection connection;
074    private final LongSequenceGenerator localTransactionIdGenerator;
075    private List<Synchronization> synchronizations;
076
077    // To track XA transactions.
078    private Xid associatedXid;
079    private TransactionId transactionId;
080    private LocalTransactionEventListener localTransactionEventListener;
081    private int beforeEndIndex;
082    private volatile boolean rollbackOnly;
083
084    // for RAR recovery
085    public TransactionContext() {
086        localTransactionIdGenerator = null;
087    }
088
089    public TransactionContext(ActiveMQConnection connection) {
090        this.connection = connection;
091        this.localTransactionIdGenerator = connection.getLocalTransactionIdGenerator();
092    }
093
094    public boolean isInXATransaction() {
095        if (transactionId != null && transactionId.isXATransaction()) {
096            return true;
097        } else {
098            synchronized(ENDED_XA_TRANSACTION_CONTEXTS) {
099                for(List<TransactionContext> transactions : ENDED_XA_TRANSACTION_CONTEXTS.values()) {
100                      if (transactions.contains(this)) {
101                          return true;
102                      }
103                }
104            }
105        }
106
107        return false;
108    }
109
110    public void setRollbackOnly(boolean val) {
111        rollbackOnly = val;
112    }
113
114    public boolean isRollbackOnly() {
115        return rollbackOnly;
116    }
117
118    public boolean isInLocalTransaction() {
119        return transactionId != null && transactionId.isLocalTransaction();
120    }
121
122    public boolean isInTransaction() {
123        return transactionId != null;
124    }
125
126    /**
127     * @return Returns the localTransactionEventListener.
128     */
129    public LocalTransactionEventListener getLocalTransactionEventListener() {
130        return localTransactionEventListener;
131    }
132
133    /**
134     * Used by the resource adapter to listen to transaction events.
135     *
136     * @param localTransactionEventListener The localTransactionEventListener to
137     *                set.
138     */
139    public void setLocalTransactionEventListener(LocalTransactionEventListener localTransactionEventListener) {
140        this.localTransactionEventListener = localTransactionEventListener;
141    }
142
143    // ///////////////////////////////////////////////////////////
144    //
145    // Methods that work with the Synchronization objects registered with
146    // the transaction.
147    //
148    // ///////////////////////////////////////////////////////////
149
150    public void addSynchronization(Synchronization s) {
151        if (synchronizations == null) {
152            synchronizations = new ArrayList<Synchronization>(10);
153        }
154        synchronizations.add(s);
155    }
156
157    private void afterRollback() throws JMSException {
158        if (synchronizations == null) {
159            return;
160        }
161
162        Throwable firstException = null;
163        int size = synchronizations.size();
164        for (int i = 0; i < size; i++) {
165            try {
166                synchronizations.get(i).afterRollback();
167            } catch (Throwable t) {
168                LOG.debug("Exception from afterRollback on {}", synchronizations.get(i), t);
169                if (firstException == null) {
170                    firstException = t;
171                }
172            }
173        }
174        synchronizations = null;
175        if (firstException != null) {
176            throw JMSExceptionSupport.create(firstException);
177        }
178    }
179
180    private void afterCommit() throws JMSException {
181        if (synchronizations == null) {
182            return;
183        }
184
185        Throwable firstException = null;
186        int size = synchronizations.size();
187        for (int i = 0; i < size; i++) {
188            try {
189                synchronizations.get(i).afterCommit();
190            } catch (Throwable t) {
191                LOG.debug("Exception from afterCommit on {}", synchronizations.get(i), t);
192                if (firstException == null) {
193                    firstException = t;
194                }
195            }
196        }
197        synchronizations = null;
198        if (firstException != null) {
199            throw JMSExceptionSupport.create(firstException);
200        }
201    }
202
203    private void beforeEnd() throws JMSException {
204        if (synchronizations == null) {
205            return;
206        }
207
208        int size = synchronizations.size();
209        try {
210            for (;beforeEndIndex < size;) {
211                synchronizations.get(beforeEndIndex++).beforeEnd();
212            }
213        } catch (JMSException e) {
214            throw e;
215        } catch (Throwable e) {
216            throw JMSExceptionSupport.create(e);
217        }
218    }
219
220    public TransactionId getTransactionId() {
221        return transactionId;
222    }
223
224    // ///////////////////////////////////////////////////////////
225    //
226    // Local transaction interface.
227    //
228    // ///////////////////////////////////////////////////////////
229
230    /**
231     * Start a local transaction.
232     * @throws javax.jms.JMSException on internal error
233     */
234    public void begin() throws JMSException {
235
236        if (isInXATransaction()) {
237            throw new TransactionInProgressException("Cannot start local transaction.  XA transaction is already in progress.");
238        }
239
240        if (transactionId == null) {
241            synchronizations = null;
242            beforeEndIndex = 0;
243            setRollbackOnly(false);
244            this.transactionId = new LocalTransactionId(getConnectionId(), localTransactionIdGenerator.getNextSequenceId());
245            TransactionInfo info = new TransactionInfo(getConnectionId(), transactionId, TransactionInfo.BEGIN);
246            this.connection.ensureConnectionInfoSent();
247            this.connection.asyncSendPacket(info);
248
249            // Notify the listener that the tx was started.
250            if (localTransactionEventListener != null) {
251                localTransactionEventListener.beginEvent();
252            }
253
254            LOG.debug("Begin:{}", transactionId);
255        }
256    }
257
258    /**
259     * Rolls back any work done in this transaction and releases any locks
260     * currently held.
261     *
262     * @throws JMSException if the JMS provider fails to roll back the
263     *                 transaction due to some internal error.
264     * @throws javax.jms.IllegalStateException if the method is not called by a
265     *                 transacted session.
266     */
267    public void rollback() throws JMSException {
268        if (isInXATransaction()) {
269            throw new TransactionInProgressException("Cannot rollback() if an XA transaction is already in progress ");
270        }
271
272        try {
273            beforeEnd();
274        } catch (TransactionRolledBackException canOcurrOnFailover) {
275            LOG.warn("rollback processing error", canOcurrOnFailover);
276        }
277        if (transactionId != null) {
278            LOG.debug("Rollback: {} syncCount: {}",
279                transactionId, (synchronizations != null ? synchronizations.size() : 0));
280
281            TransactionInfo info = new TransactionInfo(getConnectionId(), transactionId, TransactionInfo.ROLLBACK);
282            this.transactionId = null;
283            //make this synchronous - see https://issues.apache.org/activemq/browse/AMQ-2364
284            this.connection.syncSendPacket(info, this.connection.isClosing() ? this.connection.getCloseTimeout() : 0);
285            // Notify the listener that the tx was rolled back
286            if (localTransactionEventListener != null) {
287                localTransactionEventListener.rollbackEvent();
288            }
289        }
290
291        afterRollback();
292    }
293
294    /**
295     * Commits all work done in this transaction and releases any locks
296     * currently held.
297     *
298     * @throws JMSException if the JMS provider fails to commit the transaction
299     *                 due to some internal error.
300     * @throws javax.jms.IllegalStateException if the method is not called by a
301     *                 transacted session.
302     */
303    public void commit() throws JMSException {
304        if (isInXATransaction()) {
305            throw new TransactionInProgressException("Cannot commit() if an XA transaction is already in progress ");
306        }
307
308        try {
309            beforeEnd();
310        } catch (JMSException e) {
311            rollback();
312            throw e;
313        }
314
315        if (transactionId != null && rollbackOnly) {
316            final String message = "Commit of " + transactionId + "  failed due to rollback only request; typically due to failover with pending acks";
317            try {
318                rollback();
319            } finally {
320                LOG.warn(message);
321                throw new TransactionRolledBackException(message);
322            }
323        }
324
325        // Only send commit if the transaction was started.
326        if (transactionId != null) {
327            LOG.debug("Commit: {} syncCount: {}",
328                transactionId, (synchronizations != null ? synchronizations.size() : 0));
329
330            TransactionInfo info = new TransactionInfo(getConnectionId(), transactionId, TransactionInfo.COMMIT_ONE_PHASE);
331            this.transactionId = null;
332            // Notify the listener that the tx was committed back
333            try {
334                this.connection.syncSendPacket(info);
335                if (localTransactionEventListener != null) {
336                    localTransactionEventListener.commitEvent();
337                }
338                afterCommit();
339            } catch (JMSException cause) {
340                LOG.info("commit failed for transaction {}", info.getTransactionId(), cause);
341                if (localTransactionEventListener != null) {
342                    localTransactionEventListener.rollbackEvent();
343                }
344                afterRollback();
345                throw cause;
346            }
347
348        }
349    }
350
351    // ///////////////////////////////////////////////////////////
352    //
353    // XAResource Implementation
354    //
355    // ///////////////////////////////////////////////////////////
356    /**
357     * Associates a transaction with the resource.
358     */
359    @Override
360    public void start(Xid xid, int flags) throws XAException {
361
362        LOG.debug("Start: {}, flags: {}", xid, XASupport.toString(flags));
363
364        if (isInLocalTransaction()) {
365            throw new XAException(XAException.XAER_PROTO);
366        }
367        // Are we already associated?
368        if (associatedXid != null) {
369            throw new XAException(XAException.XAER_PROTO);
370        }
371
372        // if ((flags & TMJOIN) == TMJOIN) {
373        // TODO: verify that the server has seen the xid
374        // // }
375        // if ((flags & TMRESUME) == TMRESUME) {
376        // // TODO: verify that the xid was suspended.
377        // }
378
379        // associate
380        synchronizations = null;
381        beforeEndIndex = 0;
382        setRollbackOnly(false);
383        setXid(xid);
384    }
385
386    /**
387     * @return connectionId for connection
388     */
389    private ConnectionId getConnectionId() {
390        return connection.getConnectionInfo().getConnectionId();
391    }
392
393    @Override
394    public void end(Xid xid, int flags) throws XAException {
395
396        LOG.debug("End: {}, flags: {}", xid, XASupport.toString(flags));
397
398        if (isInLocalTransaction()) {
399            throw new XAException(XAException.XAER_PROTO);
400        }
401
402        if ((flags & (TMSUSPEND | TMFAIL)) != 0) {
403            // You can only suspend the associated xid.
404            if (!equals(associatedXid, xid)) {
405                throw new XAException(XAException.XAER_PROTO);
406            }
407            invokeBeforeEnd();
408        } else if ((flags & TMSUCCESS) == TMSUCCESS) {
409            // set to null if this is the current xid.
410            // otherwise this could be an asynchronous success call
411            if (equals(associatedXid, xid)) {
412                invokeBeforeEnd();
413            }
414        } else {
415            throw new XAException(XAException.XAER_INVAL);
416        }
417    }
418
419    private void invokeBeforeEnd() throws XAException {
420        boolean throwingException = false;
421        try {
422            beforeEnd();
423        } catch (JMSException e) {
424            throwingException = true;
425            throw toXAException(e);
426        } finally {
427            try {
428                setXid(null);
429            } catch (XAException ignoreIfWillMask){
430                if (!throwingException) {
431                    throw ignoreIfWillMask;
432                }
433            }
434        }
435    }
436
437    private boolean equals(Xid xid1, Xid xid2) {
438        if (xid1 == xid2) {
439            return true;
440        }
441        if (xid1 == null ^ xid2 == null) {
442            return false;
443        }
444        return xid1.getFormatId() == xid2.getFormatId() && Arrays.equals(xid1.getBranchQualifier(), xid2.getBranchQualifier())
445               && Arrays.equals(xid1.getGlobalTransactionId(), xid2.getGlobalTransactionId());
446    }
447
448    @Override
449    public int prepare(Xid xid) throws XAException {
450        LOG.debug("Prepare: {}", xid);
451
452        // We allow interleaving multiple transactions, so
453        // we don't limit prepare to the associated xid.
454        XATransactionId x;
455        // THIS SHOULD NEVER HAPPEN because end(xid, TMSUCCESS) should have been
456        // called first
457        if (xid == null || (equals(associatedXid, xid))) {
458            throw new XAException(XAException.XAER_PROTO);
459        } else {
460            // TODO: cache the known xids so we don't keep recreating this one??
461            x = new XATransactionId(xid);
462        }
463
464        if (rollbackOnly) {
465            LOG.warn("prepare of: " + x + " failed because it was marked rollback only; typically due to failover with pending acks");
466            throw new XAException(XAException.XA_RBINTEGRITY);
467        }
468
469        try {
470            TransactionInfo info = new TransactionInfo(getConnectionId(), x, TransactionInfo.PREPARE);
471
472            // Find out if the server wants to commit or rollback.
473            IntegerResponse response = (IntegerResponse)this.connection.syncSendPacket(info);
474            if (XAResource.XA_RDONLY == response.getResult()) {
475                // transaction stops now, may be syncs that need a callback
476                List<TransactionContext> l;
477                synchronized(ENDED_XA_TRANSACTION_CONTEXTS) {
478                    l = ENDED_XA_TRANSACTION_CONTEXTS.remove(x);
479                }
480                // After commit may be expensive and can deadlock, do it outside global synch block
481                // No risk for concurrent updates as we own the list now
482                if (l != null) {
483                    if(! l.isEmpty()) {
484                        LOG.debug("firing afterCommit callbacks on XA_RDONLY from prepare: {}", xid);
485                        for (TransactionContext ctx : l) {
486                            ctx.afterCommit();
487                        }
488                    }
489                }
490            }
491            return response.getResult();
492
493        } catch (JMSException e) {
494            LOG.warn("prepare of: " + x + " failed with: " + e, e);
495            List<TransactionContext> l;
496            synchronized(ENDED_XA_TRANSACTION_CONTEXTS) {
497                l = ENDED_XA_TRANSACTION_CONTEXTS.remove(x);
498            }
499            // After rollback may be expensive and can deadlock, do it outside global synch block
500            // No risk for concurrent updates as we own the list now
501            if (l != null) {
502                for (TransactionContext ctx : l) {
503                    try {
504                        ctx.afterRollback();
505                    } catch (Throwable ignored) {
506                        LOG.debug("failed to firing afterRollback callbacks on prepare " +
507                                  "failure, txid: {}, context: {}", x, ctx, ignored);
508                    }
509                }
510            }
511            throw toXAException(e);
512        }
513    }
514
515    @Override
516    public void rollback(Xid xid) throws XAException {
517
518        if (LOG.isDebugEnabled()) {
519            LOG.debug("Rollback: " + xid);
520        }
521
522        // We allow interleaving multiple transactions, so
523        // we don't limit rollback to the associated xid.
524        XATransactionId x;
525        if (xid == null) {
526            throw new XAException(XAException.XAER_PROTO);
527        }
528        if (equals(associatedXid, xid)) {
529            // I think this can happen even without an end(xid) call. Need to
530            // check spec.
531            x = (XATransactionId)transactionId;
532        } else {
533            x = new XATransactionId(xid);
534        }
535
536        try {
537            this.connection.checkClosedOrFailed();
538            this.connection.ensureConnectionInfoSent();
539
540            // Let the server know that the tx is rollback.
541            TransactionInfo info = new TransactionInfo(getConnectionId(), x, TransactionInfo.ROLLBACK);
542            this.connection.syncSendPacket(info);
543
544            List<TransactionContext> l;
545            synchronized(ENDED_XA_TRANSACTION_CONTEXTS) {
546                l = ENDED_XA_TRANSACTION_CONTEXTS.remove(x);
547            }
548            // After rollback may be expensive and can deadlock, do it outside global synch block
549            // No risk for concurrent updates as we own the list now
550            if (l != null) {
551                for (TransactionContext ctx : l) {
552                    try {
553                        ctx.afterRollback();
554                    } catch (Exception ignored) {
555                        LOG.debug("ignoring exception from after rollback on ended transaction: {}", ignored, ignored);
556                    }
557                }                  
558            }
559        } catch (JMSException e) {
560            throw toXAException(e);
561        }
562    }
563
564    // XAResource interface
565    @Override
566    public void commit(Xid xid, boolean onePhase) throws XAException {
567
568        LOG.debug("Commit: {}, onePhase={}", xid, onePhase);
569
570        // We allow interleaving multiple transactions, so
571        // we don't limit commit to the associated xid.
572        XATransactionId x;
573        if (xid == null || (equals(associatedXid, xid))) {
574            // should never happen, end(xid,TMSUCCESS) must have been previously
575            // called
576            throw new XAException(XAException.XAER_PROTO);
577        } else {
578            x = new XATransactionId(xid);
579        }
580
581        if (rollbackOnly) {
582             LOG.warn("commit of: " + x + " failed because it was marked rollback only; typically due to failover with pending acks");
583             throw new XAException(XAException.XA_RBINTEGRITY);
584         }
585
586        try {
587            this.connection.checkClosedOrFailed();
588            this.connection.ensureConnectionInfoSent();
589
590            // Notify the server that the tx was committed back
591            TransactionInfo info = new TransactionInfo(getConnectionId(), x, onePhase ? TransactionInfo.COMMIT_ONE_PHASE : TransactionInfo.COMMIT_TWO_PHASE);
592
593            this.connection.syncSendPacket(info);
594
595            List<TransactionContext> l;
596            synchronized(ENDED_XA_TRANSACTION_CONTEXTS) {
597                l = ENDED_XA_TRANSACTION_CONTEXTS.remove(x);
598            }
599            // After commit may be expensive and can deadlock, do it outside global synch block
600            // No risk for concurrent updates as we own the list now
601            if (l != null) {
602                for (TransactionContext ctx : l) {
603                    try {
604                        ctx.afterCommit();
605                    } catch (Exception ignored) {
606                        LOG.debug("ignoring exception from after completion on ended transaction: {}", ignored, ignored);
607                    }
608                }
609            }
610
611        } catch (JMSException e) {
612            LOG.warn("commit of: " + x + " failed with: " + e, e);
613            if (onePhase) {
614                List<TransactionContext> l;
615                synchronized(ENDED_XA_TRANSACTION_CONTEXTS) {
616                    l = ENDED_XA_TRANSACTION_CONTEXTS.remove(x);
617                }
618                // After rollback may be expensive and can deadlock, do it outside global synch block
619                // No risk for concurrent updates as we own the list now
620                if (l != null) {
621                    for (TransactionContext ctx : l) {
622                        try {
623                            ctx.afterRollback();
624                        } catch (Throwable ignored) {
625                            LOG.debug("failed to firing afterRollback callbacks commit failure, txid: {}, context: {}", x, ctx, ignored);
626                        }
627                    }
628                }
629            }
630            throw toXAException(e);
631        }
632    }
633
634    @Override
635    public void forget(Xid xid) throws XAException {
636        LOG.debug("Forget: {}", xid);
637
638        // We allow interleaving multiple transactions, so
639        // we don't limit forget to the associated xid.
640        XATransactionId x;
641        if (xid == null) {
642            throw new XAException(XAException.XAER_PROTO);
643        }
644        if (equals(associatedXid, xid)) {
645            // TODO determine if this can happen... I think not.
646            x = (XATransactionId)transactionId;
647        } else {
648            x = new XATransactionId(xid);
649        }
650
651        TransactionInfo info = new TransactionInfo(getConnectionId(), x, TransactionInfo.FORGET);
652
653        try {
654            // Tell the server to forget the transaction.
655            this.connection.syncSendPacket(info);
656        } catch (JMSException e) {
657            throw toXAException(e);
658        }
659        synchronized(ENDED_XA_TRANSACTION_CONTEXTS) {
660            ENDED_XA_TRANSACTION_CONTEXTS.remove(x);
661        }
662    }
663
664    @Override
665    public boolean isSameRM(XAResource xaResource) throws XAException {
666        if (xaResource == null) {
667            return false;
668        }
669        if (!(xaResource instanceof TransactionContext)) {
670            return false;
671        }
672        TransactionContext xar = (TransactionContext)xaResource;
673        try {
674            return getResourceManagerId().equals(xar.getResourceManagerId());
675        } catch (Throwable e) {
676            throw (XAException)new XAException("Could not get resource manager id.").initCause(e);
677        }
678    }
679
680    @Override
681    public Xid[] recover(int flag) throws XAException {
682        LOG.debug("recover({})", flag);
683        XATransactionId[] answer;
684
685        if (XAResource.TMNOFLAGS == flag) {
686            // signal next in cursor scan, which for us is always the end b/c we don't maintain any cursor state
687            // allows looping scan to complete
688            answer = new XATransactionId[0];
689        } else {
690            TransactionInfo info = new TransactionInfo(getConnectionId(), null, TransactionInfo.RECOVER);
691            try {
692                this.connection.checkClosedOrFailed();
693                this.connection.ensureConnectionInfoSent();
694
695                DataArrayResponse receipt = (DataArrayResponse) this.connection.syncSendPacket(info);
696                DataStructure[] data = receipt.getData();
697                if (data instanceof XATransactionId[]) {
698                    answer = (XATransactionId[]) data;
699                } else {
700                    answer = new XATransactionId[data.length];
701                    System.arraycopy(data, 0, answer, 0, data.length);
702                }
703            } catch (JMSException e) {
704                throw toXAException(e);
705            }
706        }
707        LOG.debug("recover({})={}", flag, answer);
708        return answer;
709    }
710
711    @Override
712    public int getTransactionTimeout() throws XAException {
713        return 0;
714    }
715
716    @Override
717    public boolean setTransactionTimeout(int seconds) throws XAException {
718        return false;
719    }
720
721    // ///////////////////////////////////////////////////////////
722    //
723    // Helper methods.
724    //
725    // ///////////////////////////////////////////////////////////
726    protected String getResourceManagerId() throws JMSException {
727        return this.connection.getResourceManagerId();
728    }
729
730    private void setXid(Xid xid) throws XAException {
731
732        try {
733            this.connection.checkClosedOrFailed();
734            this.connection.ensureConnectionInfoSent();
735        } catch (JMSException e) {
736            disassociate();
737            throw toXAException(e);
738        }
739
740        if (xid != null) {
741            // associate
742            associatedXid = xid;
743            transactionId = new XATransactionId(xid);
744
745            TransactionInfo info = new TransactionInfo(getConnectionId(), transactionId, TransactionInfo.BEGIN);
746            try {
747                this.connection.asyncSendPacket(info);
748                LOG.debug("{} started XA transaction {}", this, transactionId);
749            } catch (JMSException e) {
750                disassociate();
751                throw toXAException(e);
752            }
753
754        } else {
755
756            if (transactionId != null) {
757                TransactionInfo info = new TransactionInfo(getConnectionId(), transactionId, TransactionInfo.END);
758                try {
759                    this.connection.syncSendPacket(info);
760                    LOG.debug("{} ended XA transaction {}", this, transactionId);
761                } catch (JMSException e) {
762                    disassociate();
763                    throw toXAException(e);
764                }
765
766                // Add our self to the list of contexts that are interested in
767                // post commit/rollback events.
768                List<TransactionContext> l;
769                synchronized(ENDED_XA_TRANSACTION_CONTEXTS) {
770                    l = ENDED_XA_TRANSACTION_CONTEXTS.get(transactionId);
771                    if (l == null) {
772                        l = new ArrayList<TransactionContext>(3);
773                        ENDED_XA_TRANSACTION_CONTEXTS.put(transactionId, l);
774                    }
775                    if (!l.contains(this)) {
776                        l.add(this);
777                    }
778                }
779            }
780
781            disassociate();
782        }
783    }
784
785    private void disassociate() {
786         // dis-associate
787         associatedXid = null;
788         transactionId = null;
789    }
790
791    /**
792     * Converts a JMSException from the server to an XAException. if the
793     * JMSException contained a linked XAException that is returned instead.
794     *
795     * @param e JMSException to convert
796     * @return XAException wrapping original exception or its message
797     */
798    public static XAException toXAException(JMSException e) {
799        if (e.getCause() != null && e.getCause() instanceof XAException) {
800            XAException original = (XAException)e.getCause();
801            XAException xae = new XAException(original.getMessage());
802            if (original != null) {
803                xae.errorCode = original.errorCode;
804            }
805            if (original != null && xae != null && xae.errorCode == XA_OK) {
806                // detail not unmarshalled see: org.apache.activemq.openwire.v1.BaseDataStreamMarshaller.createThrowable
807                xae.errorCode = parseFromMessageOr(original.getMessage(), XAException.XAER_RMERR);
808            }
809            if (original != null) {
810                xae.initCause(original);
811            }
812            return xae;
813        }
814
815        XAException xae = new XAException(e.getMessage());
816        xae.errorCode = XAException.XAER_RMFAIL;
817        xae.initCause(e);
818        return xae;
819    }
820
821    private static int parseFromMessageOr(String message, int fallbackCode) {
822        final String marker = "xaErrorCode:";
823        final int index = message.lastIndexOf(marker);
824        if (index > -1) {
825            try {
826                return Integer.parseInt(message.substring(index + marker.length()));
827            } catch (Exception ignored) {}
828        }
829        return fallbackCode;
830    }
831
832    public ActiveMQConnection getConnection() {
833        return connection;
834    }
835
836    // for RAR xa recovery where xaresource connection is per request
837    public ActiveMQConnection setConnection(ActiveMQConnection connection) {
838        ActiveMQConnection existing = this.connection;
839        this.connection = connection;
840        return existing;
841    }
842
843    public void cleanup() {
844        associatedXid = null;
845        transactionId = null;
846    }
847
848    @Override
849    public String toString() {
850        return "TransactionContext{" +
851                "transactionId=" + transactionId +
852                ",connection=" + connection +
853                '}';
854    }
855}