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