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