001: /*
002: Copyright (C) 2005 MySQL AB
003:
004: This program is free software; you can redistribute it and/or modify
005: it under the terms of version 2 of the GNU General Public License as
006: published by the Free Software Foundation.
007:
008: There are special exceptions to the terms and conditions of the GPL
009: as it is applied to this software. View the full text of the
010: exception in file EXCEPTIONS-CONNECTOR-J in the directory of this
011: software distribution.
012:
013: This program is distributed in the hope that it will be useful,
014: but WITHOUT ANY WARRANTY; without even the implied warranty of
015: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
016: GNU General Public License for more details.
017:
018: You should have received a copy of the GNU General Public License
019: along with this program; if not, write to the Free Software
020: Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
021:
022: */
023: package com.mysql.jdbc.jdbc2.optional;
024:
025: import java.sql.Connection;
026: import java.sql.ResultSet;
027: import java.sql.SQLException;
028: import java.sql.Statement;
029: import java.util.ArrayList;
030: import java.util.Collections;
031: import java.util.HashMap;
032: import java.util.List;
033: import java.util.Map;
034:
035: import javax.sql.XAConnection;
036: import javax.transaction.xa.XAException;
037: import javax.transaction.xa.XAResource;
038: import javax.transaction.xa.Xid;
039:
040: import com.mysql.jdbc.ConnectionImpl;
041: import com.mysql.jdbc.Constants;
042: import com.mysql.jdbc.log.Log;
043:
044: /*
045: * XA BEGIN <xid> [JOIN | RESUME] XA START TRANSACTION <xid> [JOIN | RESUME] XA
046: * COMMIT <xid> [ONE PHASE] XA END <xid> [SUSPEND [FOR MIGRATE]] XA PREPARE
047: * <xid> XA RECOVER XA ROLLBACK <xid>
048: */
049:
050: /**
051: * An object that provides support for distributed transactions. An
052: * <code>XAConnection</code> object may be enlisted in a distributed
053: * transaction by means of an <code>XAResource</code> object. A transaction
054: * manager, usually part of a middle tier server, manages an
055: * <code>XAConnection</code> object through the <code>XAResource</code>
056: * object.
057: * <P>
058: * An application programmer does not use this interface directly; rather, it is
059: * used by a transaction manager working in the middle tier server.
060: *
061: * @since 1.4
062: */
063: public class MysqlXAConnection extends MysqlPooledConnection implements
064: XAConnection, XAResource {
065:
066: private com.mysql.jdbc.ConnectionImpl underlyingConnection;
067:
068: private final static Map MYSQL_ERROR_CODES_TO_XA_ERROR_CODES;
069:
070: private Log log;
071:
072: protected boolean logXaCommands;
073:
074: static {
075: HashMap temp = new HashMap();
076:
077: temp.put(Constants.integerValueOf(1397), Constants
078: .integerValueOf(XAException.XAER_NOTA));
079: temp.put(Constants.integerValueOf(1398), Constants
080: .integerValueOf(XAException.XAER_INVAL));
081: temp.put(Constants.integerValueOf(1399), Constants
082: .integerValueOf(XAException.XAER_RMFAIL));
083: temp.put(Constants.integerValueOf(1400), Constants
084: .integerValueOf(XAException.XAER_OUTSIDE));
085: temp.put(Constants.integerValueOf(1401), Constants
086: .integerValueOf(XAException.XAER_RMERR));
087: temp.put(Constants.integerValueOf(1402), Constants
088: .integerValueOf(XAException.XA_RBROLLBACK));
089:
090: MYSQL_ERROR_CODES_TO_XA_ERROR_CODES = Collections
091: .unmodifiableMap(temp);
092: }
093:
094: /**
095: * @param connection
096: */
097: public MysqlXAConnection(ConnectionImpl connection,
098: boolean logXaCommands) throws SQLException {
099: super (connection);
100: this .underlyingConnection = connection;
101: this .log = connection.getLog();
102: this .logXaCommands = logXaCommands;
103: }
104:
105: /**
106: * Retrieves an <code>XAResource</code> object that the transaction
107: * manager will use to manage this <code>XAConnection</code> object's
108: * participation in a distributed transaction.
109: *
110: * @return the <code>XAResource</code> object
111: * @exception SQLException
112: * if a database access error occurs
113: */
114: public XAResource getXAResource() throws SQLException {
115: return this ;
116: }
117:
118: /**
119: * Obtains the current transaction timeout value set for this XAResource
120: * instance. If XAResource.setTransactionTimeout was not used prior to
121: * invoking this method, the return value is the default timeout set for the
122: * resource manager; otherwise, the value used in the previous
123: * setTransactionTimeout call is returned.
124: *
125: * @return the transaction timeout value in seconds.
126: *
127: * @throws XAException
128: * An error has occurred. Possible exception values are
129: * XAER_RMERR and XAER_RMFAIL.
130: */
131: public int getTransactionTimeout() throws XAException {
132: // TODO Auto-generated method stub
133: return 0;
134: }
135:
136: /**
137: * Sets the current transaction timeout value for this XAResource instance.
138: * Once set, this timeout value is effective until setTransactionTimeout is
139: * invoked again with a different value.
140: *
141: * To reset the timeout value to the default value used by the resource
142: * manager, set the value to zero. If the timeout operation is performed
143: * successfully, the method returns true; otherwise false.
144: *
145: * If a resource manager does not support explicitly setting the transaction
146: * timeout value, this method returns false.
147: *
148: * @parameter seconds The transaction timeout value in seconds.
149: *
150: * @return true if the transaction timeout value is set successfully;
151: * otherwise false.
152: *
153: * @throws XAException
154: * An error has occurred. Possible exception values are
155: * XAER_RMERR, XAER_RMFAIL, or XAER_INVAL.
156: */
157: public boolean setTransactionTimeout(int arg0) throws XAException {
158: // TODO Auto-generated method stub
159: return false;
160: }
161:
162: /**
163: * This method is called to determine if the resource manager instance
164: * represented by the target object is the same as the resouce manager
165: * instance represented by the parameter xares.
166: *
167: * @parameter xares An XAResource object whose resource manager instance is
168: * to be compared with the resource manager instance of the
169: * target object.
170: *
171: * @return true if it's the same RM instance; otherwise false.
172: *
173: * @throws XAException
174: * An error has occurred. Possible exception values are
175: * XAER_RMERR and XAER_RMFAIL.
176: */
177: public boolean isSameRM(XAResource xares) throws XAException {
178:
179: if (xares instanceof MysqlXAConnection) {
180: return this .underlyingConnection
181: .isSameResource(((MysqlXAConnection) xares).underlyingConnection);
182: }
183:
184: return false;
185: }
186:
187: /**
188: * This method is called to obtain a list of prepared transaction branches
189: * from a resource manager. The transaction manager calls this method during
190: * recovery to obtain the list of transaction branches that are currently in
191: * prepared or heuristically completed states.
192: *
193: * The flag parameter indicates where the recover scan should start or end,
194: * or start and end. This method may be invoked one or more times during a
195: * recovery scan. The resource manager maintains a cursor which marks the
196: * current position of the prepared or heuristically completed transaction list.
197: * Each invocation of the recover method moves the cursor passed the set of Xids
198: * that are returned.
199: *
200: * Two consecutive invocation of this method that starts from the
201: * beginning of the list must return the same list of transaction branches
202: * unless one of the following takes place:
203: *
204: * - the transaction manager invokes the commit, forget, prepare, or rollback method for that resource
205: * manager, between the two consecutive invocation of the recovery scan.
206: *
207: * - the resource manager heuristically completes some transaction branches
208: * between the two invocation of the recovery scan.
209: *
210: * @param flag
211: * One of TMSTARTRSCAN, TMENDRSCAN, TMNOFLAGS. TMNOFLAGS must be
212: * used when no other flags are set in the parameter.
213: *
214: * @returns The resource manager returns zero or more XIDs of the
215: * transaction branches that are currently in a prepared or
216: * heuristically completed state. If an error occurs during the
217: * operation, the resource manager should throw the appropriate
218: * XAException.
219: *
220: * @throws XAException
221: * An error has occurred. Possible values are XAER_RMERR,
222: * XAER_RMFAIL, XAER_INVAL, and XAER_PROTO.
223: */
224: public Xid[] recover(int flag) throws XAException {
225: return recover(this .underlyingConnection, flag);
226: }
227:
228: protected static Xid[] recover(Connection c, int flag)
229: throws XAException {
230: /*
231: The XA RECOVER statement returns information for those XA transactions on the MySQL server that are in the PREPARED state. (See Section 13.4.7.2, “XA Transaction States”.) The output includes a row for each such XA transaction on the server, regardless of which client started it.
232:
233: XA RECOVER output rows look like this (for an example xid value consisting of the parts 'abc', 'def', and 7):
234:
235: mysql> XA RECOVER;
236: +----------+--------------+--------------+--------+
237: | formatID | gtrid_length | bqual_length | data |
238: +----------+--------------+--------------+--------+
239: | 7 | 3 | 3 | abcdef |
240: +----------+--------------+--------------+--------+
241:
242: The output columns have the following meanings:
243:
244: formatID is the formatID part of the transaction xid
245: gtrid_length is the length in bytes of the gtrid part of the xid
246: bqual_length is the length in bytes of the bqual part of the xid
247: data is the concatenation of the gtrid and bqual parts of the xid
248: */
249:
250: boolean startRscan = ((flag & TMSTARTRSCAN) > 0);
251: boolean endRscan = ((flag & TMENDRSCAN) > 0);
252:
253: if (!startRscan && !endRscan && flag != TMNOFLAGS) {
254: throw new MysqlXAException(
255: XAException.XAER_INVAL,
256: "Invalid flag, must use TMNOFLAGS, or any combination of TMSTARTRSCAN and TMENDRSCAN",
257: null);
258: }
259:
260: //
261: // We return all recovered XIDs at once, so if not
262: // TMSTARTRSCAN, return no new XIDs
263: //
264: // We don't attempt to maintain state to check for TMNOFLAGS
265: // "outside" of a scan
266: //
267:
268: if (!startRscan) {
269: return new Xid[0];
270: }
271:
272: ResultSet rs = null;
273: Statement stmt = null;
274:
275: List recoveredXidList = new ArrayList();
276:
277: try {
278: // TODO: Cache this for lifetime of XAConnection
279: stmt = c.createStatement();
280:
281: rs = stmt.executeQuery("XA RECOVER");
282:
283: while (rs.next()) {
284: final int formatId = rs.getInt(1);
285: int gtridLength = rs.getInt(2);
286: int bqualLength = rs.getInt(3);
287: byte[] gtridAndBqual = rs.getBytes(4);
288:
289: final byte[] gtrid = new byte[gtridLength];
290: final byte[] bqual = new byte[bqualLength];
291:
292: if (gtridAndBqual.length != (gtridLength + bqualLength)) {
293: throw new MysqlXAException(
294: XAException.XA_RBPROTO,
295: "Error while recovering XIDs from RM. GTRID and BQUAL are wrong sizes",
296: null);
297: }
298:
299: System.arraycopy(gtridAndBqual, 0, gtrid, 0,
300: gtridLength);
301: System.arraycopy(gtridAndBqual, gtridLength, bqual, 0,
302: bqualLength);
303:
304: recoveredXidList.add(new MysqlXid(gtrid, bqual,
305: formatId));
306: }
307: } catch (SQLException sqlEx) {
308: throw mapXAExceptionFromSQLException(sqlEx);
309: } finally {
310: if (rs != null) {
311: try {
312: rs.close();
313: } catch (SQLException sqlEx) {
314: throw mapXAExceptionFromSQLException(sqlEx);
315: }
316: }
317:
318: if (stmt != null) {
319: try {
320: stmt.close();
321: } catch (SQLException sqlEx) {
322: throw mapXAExceptionFromSQLException(sqlEx);
323: }
324: }
325: }
326:
327: int numXids = recoveredXidList.size();
328:
329: Xid[] asXids = new Xid[numXids];
330: Object[] asObjects = recoveredXidList.toArray();
331:
332: for (int i = 0; i < numXids; i++) {
333: asXids[i] = (Xid) asObjects[i];
334: }
335:
336: return asXids;
337: }
338:
339: /**
340: * Asks the resource manager to prepare for a transaction commit of the
341: * transaction specified in xid.
342: *
343: * @parameter xid A global transaction identifier.
344: *
345: * @returns A value indicating the resource manager's vote on the outcome of
346: * the transaction.
347: *
348: * The possible values are: XA_RDONLY or XA_OK. If the resource manager
349: * wants to roll back the transaction, it should do so by raising an
350: * appropriate XAException in the prepare method.
351: *
352: * @throws XAException
353: * An error has occurred. Possible exception values are: XA_RB*,
354: * XAER_RMERR, XAER_RMFAIL, XAER_NOTA, XAER_INVAL, or
355: * XAER_PROTO.
356: */
357: public int prepare(Xid xid) throws XAException {
358: StringBuffer commandBuf = new StringBuffer();
359: commandBuf.append("XA PREPARE ");
360: commandBuf.append(xidToString(xid));
361:
362: dispatchCommand(commandBuf.toString());
363:
364: return XA_OK; // TODO: Check for read-only
365: }
366:
367: /**
368: * Tells the resource manager to forget about a heuristically completed
369: * transaction branch.
370: *
371: * @parameter xid A global transaction identifier.
372: *
373: * @throws XAException
374: * An error has occurred. Possible exception values are
375: * XAER_RMERR, XAER_RMFAIL, XAER_NOTA, XAER_INVAL, or
376: * XAER_PROTO.
377: */
378: public void forget(Xid xid) throws XAException {
379: // TODO Auto-generated method stub
380: }
381:
382: /**
383: * Informs the resource manager to roll back work done on behalf of a
384: * transaction branch.
385: *
386: * @parameter xid A global transaction identifier.
387: *
388: * @throws XAException
389: * An error has occurred. Possible XAExceptions are XA_HEURHAZ,
390: * XA_HEURCOM, XA_HEURRB, XA_HEURMIX, XAER_RMERR, XAER_RMFAIL,
391: * XAER_NOTA, XAER_INVAL, or XAER_PROTO.
392: *
393: * If the transaction branch is already marked rollback-only the resource
394: * manager may throw one of the XA_RB* exceptions.
395: *
396: * Upon return, the resource manager has rolled back the branch's work and
397: * has released all held resources.
398: */
399: public void rollback(Xid xid) throws XAException {
400: StringBuffer commandBuf = new StringBuffer();
401: commandBuf.append("XA ROLLBACK ");
402: commandBuf.append(xidToString(xid));
403:
404: try {
405: dispatchCommand(commandBuf.toString());
406: } finally {
407: this .underlyingConnection.setInGlobalTx(false);
408: }
409: }
410:
411: /**
412: * Ends the work performed on behalf of a transaction branch.
413: *
414: * The resource manager disassociates the XA resource from the transaction
415: * branch specified and lets the transaction complete.
416: *
417: * If TMSUSPEND is specified in the flags, the transaction branch is
418: * temporarily suspended in an incomplete state. The transaction context is
419: * in a suspended state and must be resumed via the start method with
420: * TMRESUME specified.
421: *
422: * If TMFAIL is specified, the portion of work has failed. The resource
423: * manager may mark the transaction as rollback-only
424: *
425: * If TMSUCCESS is specified, the portion of work has completed
426: * successfully.
427: *
428: * @parameter xid A global transaction identifier that is the same as the
429: * identifier used previously in the start method.
430: *
431: * @parameter flags One of TMSUCCESS, TMFAIL, or TMSUSPEND.
432: *
433: * @throws XAException -
434: * An error has occurred. Possible XAException values are
435: * XAER_RMERR, XAER_RMFAIL, XAER_NOTA, XAER_INVAL, XAER_PROTO,
436: * or XA_RB*.
437: */
438: public void end(Xid xid, int flags) throws XAException {
439: StringBuffer commandBuf = new StringBuffer();
440: commandBuf.append("XA END ");
441: commandBuf.append(xidToString(xid));
442:
443: switch (flags) {
444: case TMSUCCESS:
445: break; // no-op
446: case TMSUSPEND:
447: commandBuf.append(" SUSPEND");
448: break;
449: case TMFAIL:
450: break; // no-op
451: default:
452: throw new XAException(XAException.XAER_INVAL);
453: }
454:
455: dispatchCommand(commandBuf.toString());
456: }
457:
458: /**
459: * Starts work on behalf of a transaction branch specified in xid.
460: *
461: * If TMJOIN is specified, the start applies to joining a transaction
462: * previously seen by the resource manager.
463: *
464: * If TMRESUME is specified, the start applies to resuming a suspended
465: * transaction specified in the parameter xid.
466: *
467: * If neither TMJOIN nor TMRESUME is specified and the transaction specified
468: * by xid has previously been seen by the resource manager, the resource
469: * manager throws the XAException exception with XAER_DUPID error code.
470: *
471: * @parameter xid A global transaction identifier to be associated with the
472: * resource.
473: *
474: * @parameter flags One of TMNOFLAGS, TMJOIN, or TMRESUME.
475: *
476: * @throws XAException
477: * An error has occurred. Possible exceptions are XA_RB*,
478: * XAER_RMERR, XAER_RMFAIL, XAER_DUPID, XAER_OUTSIDE, XAER_NOTA,
479: * XAER_INVAL, or XAER_PROTO.
480: */
481: public void start(Xid xid, int flags) throws XAException {
482: StringBuffer commandBuf = new StringBuffer();
483: commandBuf.append("XA START ");
484: commandBuf.append(xidToString(xid));
485:
486: switch (flags) {
487: case TMJOIN:
488: commandBuf.append(" JOIN");
489: break;
490: case TMRESUME:
491: commandBuf.append(" RESUME");
492: break;
493: case TMNOFLAGS:
494: // no-op
495: break;
496: default:
497: throw new XAException(XAException.XAER_INVAL);
498: }
499:
500: dispatchCommand(commandBuf.toString());
501:
502: this .underlyingConnection.setInGlobalTx(true);
503: }
504:
505: /**
506: * Commits the global transaction specified by xid.
507: *
508: * @parameter xid A global transaction identifier
509: * @parameter onePhase - If true, the resource manager should use a
510: * one-phase commit protocol to commit the work done on behalf of
511: * xid.
512: *
513: * @throws XAException
514: * An error has occurred. Possible XAExceptions are XA_HEURHAZ,
515: * XA_HEURCOM, XA_HEURRB, XA_HEURMIX, XAER_RMERR, XAER_RMFAIL,
516: * XAER_NOTA, XAER_INVAL, or XAER_PROTO.
517: *
518: * If the resource manager did not commit the transaction and the parameter
519: * onePhase is set to true, the resource manager may throw one of the XA_RB*
520: * exceptions.
521: *
522: * Upon return, the resource manager has rolled back the branch's work and
523: * has released all held resources.
524: */
525:
526: public void commit(Xid xid, boolean onePhase) throws XAException {
527: StringBuffer commandBuf = new StringBuffer();
528: commandBuf.append("XA COMMIT ");
529: commandBuf.append(xidToString(xid));
530:
531: if (onePhase) {
532: commandBuf.append(" ONE PHASE");
533: }
534:
535: try {
536: dispatchCommand(commandBuf.toString());
537: } finally {
538: this .underlyingConnection.setInGlobalTx(false);
539: }
540: }
541:
542: private ResultSet dispatchCommand(String command)
543: throws XAException {
544: Statement stmt = null;
545:
546: try {
547: if (this .logXaCommands) {
548: this .log.logDebug("Executing XA statement: " + command);
549: }
550:
551: // TODO: Cache this for lifetime of XAConnection
552: stmt = this .underlyingConnection.createStatement();
553:
554: stmt.execute(command);
555:
556: ResultSet rs = stmt.getResultSet();
557:
558: return rs;
559: } catch (SQLException sqlEx) {
560: throw mapXAExceptionFromSQLException(sqlEx);
561: } finally {
562: if (stmt != null) {
563: try {
564: stmt.close();
565: } catch (SQLException sqlEx) {
566: }
567: }
568: }
569: }
570:
571: protected static XAException mapXAExceptionFromSQLException(
572: SQLException sqlEx) {
573:
574: Integer xaCode = (Integer) MYSQL_ERROR_CODES_TO_XA_ERROR_CODES
575: .get(Constants.integerValueOf(sqlEx.getErrorCode()));
576:
577: if (xaCode != null) {
578: return new MysqlXAException(xaCode.intValue(), sqlEx
579: .getMessage(), null);
580: }
581:
582: // Punt? We don't know what the error code is here
583: return new MysqlXAException(sqlEx.getMessage(), null);
584: }
585:
586: private static String xidToString(Xid xid) {
587: byte[] gtrid = xid.getGlobalTransactionId();
588:
589: byte[] btrid = xid.getBranchQualifier();
590:
591: int lengthAsString = 6; // for (0x and ,) * 2
592:
593: if (gtrid != null) {
594: lengthAsString += (2 * gtrid.length);
595: }
596:
597: if (btrid != null) {
598: lengthAsString += (2 * btrid.length);
599: }
600:
601: String formatIdInHex = Integer.toHexString(xid.getFormatId());
602:
603: lengthAsString += formatIdInHex.length();
604: lengthAsString += 3; // for the '.' after formatId
605:
606: StringBuffer asString = new StringBuffer(lengthAsString);
607:
608: asString.append("0x");
609:
610: if (gtrid != null) {
611: for (int i = 0; i < gtrid.length; i++) {
612: String asHex = Integer.toHexString(gtrid[i] & 0xff);
613:
614: if (asHex.length() == 1) {
615: asString.append("0");
616: }
617:
618: asString.append(asHex);
619: }
620: }
621:
622: asString.append(",");
623:
624: if (btrid != null) {
625: asString.append("0x");
626:
627: for (int i = 0; i < btrid.length; i++) {
628: String asHex = Integer.toHexString(btrid[i] & 0xff);
629:
630: if (asHex.length() == 1) {
631: asString.append("0");
632: }
633:
634: asString.append(asHex);
635: }
636: }
637:
638: asString.append(",0x");
639: asString.append(formatIdInHex);
640:
641: return asString.toString();
642: }
643:
644: /*
645: * (non-Javadoc)
646: *
647: * @see javax.sql.PooledConnection#getConnection()
648: */
649: public synchronized Connection getConnection() throws SQLException {
650: Connection connToWrap = getConnection(false, true);
651:
652: return connToWrap;
653: }
654: }
|