/*
* Copyright (c) 2005, 2018, Oracle and/or its affiliates. All rights reserved.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, version 2.0, as published by the
* Free Software Foundation.
*
* This program is also distributed with certain software (including but not
* limited to OpenSSL) that is licensed under separate terms, as designated in a
* particular file or component or in included license documentation. The
* authors of MySQL hereby grant you an additional permission to link the
* program and your derivative works with the separately licensed software that
* they have included with MySQL.
*
* Without limiting anything contained in the foregoing, this file, which is
* part of MySQL Connector/J, is also subject to the Universal FOSS Exception,
* version 1.0, a copy of which can be found at
* http://oss.oracle.com/licenses/universal-foss-exception.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License, version 2.0,
* for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package com.mysql.cj.jdbc;
import java.sql.
Connection;
import java.sql.
ResultSet;
import java.sql.
SQLException;
import java.sql.
Statement;
import java.util.
ArrayList;
import java.util.
Collections;
import java.util.
HashMap;
import java.util.
List;
import java.util.
Map;
import javax.sql.
XAConnection;
import javax.transaction.xa.
XAException;
import javax.transaction.xa.
XAResource;
import javax.transaction.xa.
Xid;
import com.mysql.cj.
Messages;
import com.mysql.cj.log.
Log;
import com.mysql.cj.util.
StringUtils;
public class
MysqlXAConnection extends
MysqlPooledConnection implements
XAConnection,
XAResource {
private static final int
MAX_COMMAND_LENGTH = 300;
private com.mysql.cj.jdbc.
JdbcConnection underlyingConnection;
private final static
Map<
Integer,
Integer>
MYSQL_ERROR_CODES_TO_XA_ERROR_CODES;
private
Log log;
protected boolean
logXaCommands;
static {
HashMap<
Integer,
Integer>
temp = new
HashMap<>();
temp.
put(1397,
XAException.
XAER_NOTA);
temp.
put(1398,
XAException.
XAER_INVAL);
temp.
put(1399,
XAException.
XAER_RMFAIL);
temp.
put(1400,
XAException.
XAER_OUTSIDE);
temp.
put(1401,
XAException.
XAER_RMERR);
temp.
put(1402,
XAException.
XA_RBROLLBACK);
temp.
put(1440,
XAException.
XAER_DUPID);
temp.
put(1613,
XAException.
XA_RBTIMEOUT);
temp.
put(1614,
XAException.
XA_RBDEADLOCK);
MYSQL_ERROR_CODES_TO_XA_ERROR_CODES =
Collections.
unmodifiableMap(
temp);
}
protected static
MysqlXAConnection getInstance(
JdbcConnection mysqlConnection, boolean
logXaCommands) throws
SQLException {
return new
MysqlXAConnection(
mysqlConnection,
logXaCommands);
}
public
MysqlXAConnection(
JdbcConnection connection, boolean
logXaCommands) {
super(
connection);
this.
underlyingConnection =
connection;
this.
log =
connection.
getSession().
getLog();
this.
logXaCommands =
logXaCommands;
}
@
Override
public
XAResource getXAResource() throws
SQLException {
return this;
}
@
Override
public int
getTransactionTimeout() throws
XAException {
return 0;
}
@
Override
public boolean
setTransactionTimeout(int
arg0) throws
XAException {
return false;
}
@
Override
public boolean
isSameRM(
XAResource xares) throws
XAException {
if (
xares instanceof
MysqlXAConnection) {
return this.
underlyingConnection.
isSameResource(((
MysqlXAConnection)
xares).
underlyingConnection);
}
return false;
}
@
Override
public
Xid[]
recover(int
flag) throws
XAException {
return
recover(this.
underlyingConnection,
flag);
}
protected static
Xid[]
recover(
Connection c, int
flag) throws
XAException {
/*
* 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.
*
* XA RECOVER output rows look like this (for an example xid value consisting of the parts 'abc', 'def', and 7):
*
* mysql> XA RECOVER;
* +----------+--------------+--------------+--------+
* | formatID | gtrid_length | bqual_length | data |
* +----------+--------------+--------------+--------+
* | 7 | 3 | 3 | abcdef |
* +----------+--------------+--------------+--------+
*
* The output columns have the following meanings:
*
* formatID is the formatID part of the transaction xid
* gtrid_length is the length in bytes of the gtrid part of the xid
* bqual_length is the length in bytes of the bqual part of the xid
* data is the concatenation of the gtrid and bqual parts of the xid
*/
boolean
startRscan = ((
flag &
TMSTARTRSCAN) > 0);
boolean
endRscan = ((
flag &
TMENDRSCAN) > 0);
if (!
startRscan && !
endRscan &&
flag !=
TMNOFLAGS) {
throw new
MysqlXAException(
XAException.
XAER_INVAL,
Messages.
getString("MysqlXAConnection.001"), null);
}
//
// We return all recovered XIDs at once, so if not TMSTARTRSCAN, return no new XIDs
//
// We don't attempt to maintain state to check for TMNOFLAGS "outside" of a scan
//
if (!
startRscan) {
return new
Xid[0];
}
ResultSet rs = null;
Statement stmt = null;
List<
MysqlXid>
recoveredXidList = new
ArrayList<>();
try {
// TODO: Cache this for lifetime of XAConnection
stmt =
c.
createStatement();
rs =
stmt.
executeQuery("XA RECOVER");
while (
rs.
next()) {
final int
formatId =
rs.
getInt(1);
int
gtridLength =
rs.
getInt(2);
int
bqualLength =
rs.
getInt(3);
byte[]
gtridAndBqual =
rs.
getBytes(4);
final byte[]
gtrid = new byte[
gtridLength];
final byte[]
bqual = new byte[
bqualLength];
if (
gtridAndBqual.length != (
gtridLength +
bqualLength)) {
throw new
MysqlXAException(
XAException.
XA_RBPROTO,
Messages.
getString("MysqlXAConnection.002"), null);
}
System.
arraycopy(
gtridAndBqual, 0,
gtrid, 0,
gtridLength);
System.
arraycopy(
gtridAndBqual,
gtridLength,
bqual, 0,
bqualLength);
recoveredXidList.
add(new
MysqlXid(
gtrid,
bqual,
formatId));
}
} catch (
SQLException sqlEx) {
throw
mapXAExceptionFromSQLException(
sqlEx);
} finally {
if (
rs != null) {
try {
rs.
close();
} catch (
SQLException sqlEx) {
throw
mapXAExceptionFromSQLException(
sqlEx);
}
}
if (
stmt != null) {
try {
stmt.
close();
} catch (
SQLException sqlEx) {
throw
mapXAExceptionFromSQLException(
sqlEx);
}
}
}
int
numXids =
recoveredXidList.
size();
Xid[]
asXids = new
Xid[
numXids];
Object[]
asObjects =
recoveredXidList.
toArray();
for (int
i = 0;
i <
numXids;
i++) {
asXids[
i] = (
Xid)
asObjects[
i];
}
return
asXids;
}
@
Override
public int
prepare(
Xid xid) throws
XAException {
StringBuilder commandBuf = new
StringBuilder(
MAX_COMMAND_LENGTH);
commandBuf.
append("XA PREPARE ");
appendXid(
commandBuf,
xid);
dispatchCommand(
commandBuf.
toString());
return
XA_OK; // TODO: Check for read-only
}
@
Override
public void
forget(
Xid xid) throws
XAException {
// mysql doesn't support this
}
@
Override
public void
rollback(
Xid xid) throws
XAException {
StringBuilder commandBuf = new
StringBuilder(
MAX_COMMAND_LENGTH);
commandBuf.
append("XA ROLLBACK ");
appendXid(
commandBuf,
xid);
try {
dispatchCommand(
commandBuf.
toString());
} finally {
this.
underlyingConnection.
setInGlobalTx(false);
}
}
@
Override
public void
end(
Xid xid, int
flags) throws
XAException {
StringBuilder commandBuf = new
StringBuilder(
MAX_COMMAND_LENGTH);
commandBuf.
append("XA END ");
appendXid(
commandBuf,
xid);
switch (
flags) {
case
TMSUCCESS:
break; // no-op
case
TMSUSPEND:
commandBuf.
append(" SUSPEND");
break;
case
TMFAIL:
break; // no-op
default:
throw new
XAException(
XAException.
XAER_INVAL);
}
dispatchCommand(
commandBuf.
toString());
}
@
Override
public void
start(
Xid xid, int
flags) throws
XAException {
StringBuilder commandBuf = new
StringBuilder(
MAX_COMMAND_LENGTH);
commandBuf.
append("XA START ");
appendXid(
commandBuf,
xid);
switch (
flags) {
case
TMJOIN:
commandBuf.
append(" JOIN");
break;
case
TMRESUME:
commandBuf.
append(" RESUME");
break;
case
TMNOFLAGS:
// no-op
break;
default:
throw new
XAException(
XAException.
XAER_INVAL);
}
dispatchCommand(
commandBuf.
toString());
this.
underlyingConnection.
setInGlobalTx(true);
}
@
Override
public void
commit(
Xid xid, boolean
onePhase) throws
XAException {
StringBuilder commandBuf = new
StringBuilder(
MAX_COMMAND_LENGTH);
commandBuf.
append("XA COMMIT ");
appendXid(
commandBuf,
xid);
if (
onePhase) {
commandBuf.
append(" ONE PHASE");
}
try {
dispatchCommand(
commandBuf.
toString());
} finally {
this.
underlyingConnection.
setInGlobalTx(false);
}
}
private
ResultSet dispatchCommand(
String command) throws
XAException {
Statement stmt = null;
try {
if (this.
logXaCommands) {
this.
log.
logDebug("Executing XA statement: " +
command);
}
// TODO: Cache this for lifetime of XAConnection
stmt = this.
underlyingConnection.
createStatement();
stmt.
execute(
command);
ResultSet rs =
stmt.
getResultSet();
return
rs;
} catch (
SQLException sqlEx) {
throw
mapXAExceptionFromSQLException(
sqlEx);
} finally {
if (
stmt != null) {
try {
stmt.
close();
} catch (
SQLException sqlEx) {
}
}
}
}
protected static
XAException mapXAExceptionFromSQLException(
SQLException sqlEx) {
Integer xaCode =
MYSQL_ERROR_CODES_TO_XA_ERROR_CODES.
get(
sqlEx.
getErrorCode());
if (
xaCode != null) {
return (
XAException) new
MysqlXAException(
xaCode.
intValue(),
sqlEx.
getMessage(), null).
initCause(
sqlEx);
}
return (
XAException) new
MysqlXAException(
XAException.
XAER_RMFAIL,
Messages.
getString("MysqlXAConnection.003"), null).
initCause(
sqlEx);
}
private static void
appendXid(
StringBuilder builder,
Xid xid) {
byte[]
gtrid =
xid.
getGlobalTransactionId();
byte[]
btrid =
xid.
getBranchQualifier();
if (
gtrid != null) {
StringUtils.
appendAsHex(
builder,
gtrid);
}
builder.
append(',');
if (
btrid != null) {
StringUtils.
appendAsHex(
builder,
btrid);
}
builder.
append(',');
StringUtils.
appendAsHex(
builder,
xid.
getFormatId());
}
@
Override
public synchronized
Connection getConnection() throws
SQLException {
Connection connToWrap =
getConnection(false, true);
return
connToWrap;
}
}