/*
* The contents of this file are subject to the Open Software License
* Version 3.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
* http://www.rosenlaw.com/OSL3.0.htm
*
* Software distributed under the License is distributed on an "AS IS"
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
* the License for the specific language governing rights and limitations
* under the License.
*
* This file is an original work developed by Netymon Pty Ltd
* (http://www.netymon.com, mailto:mail@netymon.com) under contract to
* Topaz Foundation. Portions created under this contract are
* Copyright (c) 2007 Topaz Foundation
* All Rights Reserved.
*/
package org.mulgara.resolver;
// Java2 packages
import java.io.Serializable;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
// Third party packages
import org.apache.log4j.Logger;
// Local packages
import org.mulgara.query.MulgaraTransactionException;
import org.mulgara.server.ResourceManagerInstanceAdaptor;
import org.mulgara.util.Assoc1toNMap;
/**
* Provides an external JTA-compliant TransactionManager with the ability to
* control Mulgara Transactions.
*
* @created 2007-11-07
*
* @author <a href="mailto:andrae@netymon.com">Andrae Muys</a>
*
* @company <A href="mailto:mail@netymon.com">Netymon Pty Ltd</A>
*
* @copyright ©2006 <a href="http://www.topazproject.org/">Topaz Project</a>
*
* @licence Open Software License v3.0</a>
*/
public class MulgaraXAResourceContext {
private static final Logger logger =
Logger.getLogger(MulgaraXAResourceContext.class.getName());
/**
* Map from keyed from the {@link Integer} value of the various flags
* defined in {@link XAResource} and mapping to the formatted name for that
* flag.
*/
private final static Map<Integer, String> flagMap = new HashMap<Integer, String>();
static {
flagMap.put(XAResource.TMENDRSCAN, "TMENDRSCAN");
flagMap.put(XAResource.TMFAIL, "TMFAIL");
flagMap.put(XAResource.TMJOIN, "TMJOIN");
flagMap.put(XAResource.TMONEPHASE, "TMONEPHASE");
flagMap.put(XAResource.TMRESUME, "TMRESUME");
flagMap.put(XAResource.TMSTARTRSCAN, "TMSTARTRSCAN");
flagMap.put(XAResource.TMSUCCESS, "TMSUCCESS");
flagMap.put(XAResource.TMSUSPEND, "TMSUSPEND");
}
private final MulgaraExternalTransactionFactory factory;
protected final DatabaseSession session;
private final Assoc1toNMap<MulgaraExternalTransaction, Xid> xa2xid;
private final UUID uniqueId;
MulgaraXAResourceContext(MulgaraExternalTransactionFactory factory, DatabaseSession session) {
if (logger.isDebugEnabled()) logger.debug("Creating MulgaraXAResource");
this.factory = factory;
this.session = session;
this.xa2xid = new Assoc1toNMap<MulgaraExternalTransaction, Xid>();
this.uniqueId = UUID.randomUUID();
}
public XAResource getResource(boolean writing) {
return new MulgaraXAResource(writing);
}
private class MulgaraXAResource implements XAResource,
ResourceManagerInstanceAdaptor {
private final boolean writing;
public MulgaraXAResource(boolean writing) {
this.writing = writing;
}
/**
* Commit transaction identified by xid.
*
* Transaction must be Idle, Prepared, or Heuristically-Completed.
* If transaction not Heuristically-Completed we are required to finish it,
* clean up, and forget it.
* If transaction is Heuristically-Completed we throw an exception and wait
* for a call to forget().
*/
public void commit(Xid xid, boolean onePhase) throws XAException {
factory.acquireMutex(0, XAException.class);
try {
xid = convertXid(xid);
if (logger.isDebugEnabled()) logger.debug("Performing commit: " + parseXid(xid));
MulgaraExternalTransaction xa = xa2xid.get1(xid);
if (xa == null) {
throw new XAException(XAException.XAER_NOTA);
} else if (xa.isHeuristicallyRollbacked()) {
// HEURRB causes difficulties with JOTM - so throw the less precise
// but still correct RBROLLBACK.
// Note: Found the problem here - The J2EE Connector Architecture
// 7.6.2.2 requires an XA_RB* exception in the case of 1PC and 7.6.2.5
// implies that HEURRB is not permitted during 2PC - this seems broken
// to me, but that's the spec.
// throw newXAException(XAException.XA_HEURRB, xa.getRollbackCause());
throw newXAException(XAException.XA_RBROLLBACK, xa.getRollbackCause());
} else if (xa.isHeuristicallyCommitted()) {
throw new XAException(XAException.XA_HEURCOM);
}
if (onePhase) {
try {
xa.prepare(xid);
} catch (XAException ex) {
if (ex.errorCode != XAException.XA_RDONLY) {
doRollback(xa, xid);
}
// Note: XA spec requires us to forget about transactions that fail
// during commit. doRollback throws exception under Heuristic
// Completion - when we do want to remember transaction.
xa2xid.remove1(xa);
throw ex;
}
}
try {
xa.commit(xid);
xa2xid.remove1(xa);
} catch (XAException ex) {
// We are not allowed to forget this transaction if we completed
// heuristically.
switch (ex.errorCode) {
case XAException.XA_HEURHAZ:
case XAException.XA_HEURCOM:
case XAException.XA_HEURRB:
case XAException.XA_HEURMIX:
throw ex;
default:
xa2xid.remove1(xa);
throw ex;
}
}
} finally {
factory.releaseMutex();
}
}
/**
* Deactivate a transaction.
*
* TMSUCCESS: Move to Idle and await call to rollback, prepare, or commit.
* TMFAIL: Move to RollbackOnly; await call to rollback.
* TMSUSPEND: Move to Idle and await start(TMRESUME) or end(TMSUCCESS|FAIL)
*
* In all cases disassociate from current session.
*/
public void end(Xid xid, int flags) throws XAException {
factory.acquireMutex(0, XAException.class);
try {
xid = convertXid(xid);
if (logger.isDebugEnabled()) logger.debug("Performing end(" + formatFlags(flags) + "): " + parseXid(xid));
MulgaraExternalTransaction xa = xa2xid.get1(xid);
if (xa == null) {
throw new XAException(XAException.XAER_NOTA);
}
switch (flags) {
case TMFAIL:
doRollback(xa, xid);
break;
case TMSUCCESS:
if (xa.isHeuristicallyRollbacked()) {
throw newXAException(XAException.XA_RBPROTO, xa.getRollbackCause());
}
break;
case TMSUSPEND: // Should I be tracking the xid's state to ensure
// conformance with the X/Open state diagrams?
break;
default:
logger.error("Invalid flag passed to end() : " + flags);
throw new XAException(XAException.XAER_INVAL);
}
try {
// If XA is currently associated with session, disassociate it.
factory.disassociateTransaction(xa);
} catch (MulgaraTransactionException em) {
logger.error("Error disassociating transaction from session", em);
throw new XAException(XAException.XAER_PROTO);
}
} finally {
factory.releaseMutex();
}
}
public void forget(Xid xid) throws XAException {
factory.acquireMutex(0, XAException.class);
try {
xid = convertXid(xid);
if (logger.isDebugEnabled()) logger.debug("Performing forget: " + parseXid(xid));
MulgaraExternalTransaction xa = xa2xid.get1(xid);
if (xa == null) {
throw new XAException(XAException.XAER_NOTA);
}
try {
if (!xa.isHeuristicallyRollbacked()) {
try {
xa.abortTransaction(new MulgaraTransactionException("External XA Manager specified 'forget'"));
} catch (MulgaraTransactionException em) {
logger.error("Failed to abort transaction in forget", em);
throw new XAException(XAException.XAER_RMERR);
}
}
} finally {
xa2xid.remove1(xa);
}
} finally {
factory.releaseMutex();
}
}
public int getTransactionTimeout() throws XAException {
factory.acquireMutex(0, XAException.class);
try {
if (logger.isDebugEnabled()) logger.debug("Performing getTransactionTimeout");
return (int) (session.getTransactionTimeout() / 1000);
} finally {
factory.releaseMutex();
}
}
public boolean isSameRM(XAResource xares) throws XAException {
factory.acquireMutex(0, XAException.class);
try {
if (logger.isDebugEnabled()) logger.debug("Performing isSameRM");
if (xares.getClass() != MulgaraXAResource.class) {
return false;
} else {
// Based on X/Open-XA-TP section 3.2 I believe a 'Resource Manager
// Instance' corresponds to a session, as each session 'supports
// independent transaction completion'.
return session == ((MulgaraXAResource)xares).getSession();
}
} finally {
factory.releaseMutex();
}
}
public int prepare(Xid xid) throws XAException {
factory.acquireMutex(0, XAException.class);
try {
xid = convertXid(xid);
if (logger.isDebugEnabled()) logger.debug("Performing prepare: " + parseXid(xid));
MulgaraExternalTransaction xa = xa2xid.get1(xid);
if (xa == null) {
throw new XAException(XAException.XAER_NOTA);
} else if (xa.isRollbacked()) {
throw new XAException(XAException.XA_RBROLLBACK);
}
xa.prepare(xid);
return XA_OK;
} finally {
factory.releaseMutex();
}
}
/**
* We don't currently support recover.
* FIXME: We should at least handle the case where we are asked to recover
* when we haven't crashed.
*/
public Xid[] recover(int flag) throws XAException {
factory.acquireMutex(0, XAException.class);
try {
if (logger.isDebugEnabled()) logger.debug("Performing recover");
return new Xid[] {};
} finally {
factory.releaseMutex();
}
}
public void rollback(Xid xid) throws XAException {
factory.acquireMutex(0, XAException.class);
try {
xid = convertXid(xid);
if (logger.isDebugEnabled()) logger.debug("Performing rollback: " + parseXid(xid));
MulgaraExternalTransaction xa = xa2xid.get1(xid);
if (xa == null) {
throw new XAException(XAException.XAER_NOTA);
}
doRollback(xa, xid);
// If we don't throw a Heuristic Exception we need to forget this
// transaction. doRollback only throws Heuristic Exceptions.
xa2xid.remove1(xa);
} finally {
factory.releaseMutex();
}
}
/**
* Performs rollback. Only throws exception if transaction is subject to
* Heuristic Completion.
*/
private void doRollback(MulgaraExternalTransaction xa, Xid xid) throws XAException {
if (xa.isHeuristicallyRollbacked()) {
logger.warn("Attempted to rollback heuristically rollbacked transaction: xa-code=" +
xa.getHeuristicCode() + ", reason-string='" + xa.getRollbackCause() + "'");
throw newXAException(xa.getHeuristicCode(), xa.getRollbackCause());
} else if (!xa.isRollbacked()) {
xa.rollback(xid);
}
}
public boolean setTransactionTimeout(int seconds) throws XAException {
if (seconds < 0)
throw new XAException(XAException.XAER_INVAL);
factory.acquireMutex(0, XAException.class);
try {
if (logger.isDebugEnabled()) logger.debug("Performing setTransactionTimeout");
session.setTransactionTimeout(seconds * 1000L);
return true;
} finally {
factory.releaseMutex();
}
}
public void start(Xid xid, int flags) throws XAException {
factory.acquireMutex(0, XAException.class);
try {
xid = convertXid(xid);
if (logger.isDebugEnabled()) logger.debug("Performing start(" + formatFlags(flags) + "): " + parseXid(xid));
switch (flags) {
case TMNOFLAGS:
if (xa2xid.containsN(xid)) {
throw new XAException(XAException.XAER_DUPID);
} else if (factory.hasAssociatedTransaction()) {
throw new XAException(XAException.XA_RBDEADLOCK);
} else {
// FIXME: Need to consider read-only transactions here.
try {
MulgaraExternalTransaction xa = factory.createTransaction(xid, writing);
xa2xid.put(xa, xid);
} catch (MulgaraTransactionException em) {
logger.error("Failed to create transaction", em);
throw new XAException(XAException.XAER_RMFAIL);
}
}
break;
case TMJOIN:
if (!factory.hasAssociatedTransaction()) {
throw new XAException(XAException.XAER_NOTA);
} else if (!factory.getAssociatedTransaction().getXid().equals(xid)) {
throw new XAException(XAException.XAER_OUTSIDE);
}
break;
case TMRESUME:
MulgaraExternalTransaction xa = xa2xid.get1(xid);
if (xa == null) {
throw new XAException(XAException.XAER_NOTA);
} else if (xa.isRollbacked()) {
throw new XAException(XAException.XA_RBROLLBACK);
} else {
if (!factory.associateTransaction(xa)) {
// session already associated with a transaction.
throw new XAException(XAException.XAER_PROTO);
}
}
break;
}
} finally {
factory.releaseMutex();
}
}
public Serializable getRMId() {
return uniqueId;
}
/**
* Required only because Java has trouble with accessing fields from
* inner-classes.
*/
private DatabaseSession getSession() { return session; }
}
private static XAException newXAException(int errorCode, String reason) {
XAException xae = new XAException(reason);
xae.errorCode = errorCode;
return xae;
}
public static String parseXid(Xid xid) {
return xid.toString();
}
private static InternalXid convertXid(Xid xid) {
return new InternalXid(xid);
}
/**
* Provides an Xid that compares equal by value.
*/
private static class InternalXid implements Xid {
private byte[] bq;
private int fi;
private byte[] gtid;
public InternalXid(Xid xid) {
byte[] tbq = xid.getBranchQualifier();
byte[] tgtid = xid.getGlobalTransactionId();
this.bq = new byte[tbq.length];
this.fi = xid.getFormatId();
this.gtid = new byte[tgtid.length];
System.arraycopy(tbq, 0, this.bq, 0, tbq.length);
System.arraycopy(tgtid, 0, this.gtid, 0, tgtid.length);
}
public byte[] getBranchQualifier() {
return bq;
}
public int getFormatId() {
return fi;
}
public byte[] getGlobalTransactionId() {
return gtid;
}
public int hashCode() {
return Arrays.hashCode(bq) ^ fi ^ Arrays.hashCode(gtid);
}
public boolean equals(Object rhs) {
if (!(rhs instanceof InternalXid)) {
return false;
} else {
InternalXid rhx = (InternalXid)rhs;
return this.fi == rhx.fi &&
Arrays.equals(this.bq, rhx.bq) &&
Arrays.equals(this.gtid, rhx.gtid);
}
}
public String toString() {
return ":" + fi + ":" + Arrays.hashCode(gtid) + ":" + Arrays.hashCode(bq) + ":";
}
}
/**
* Format bitmasks defined by {@link XAResource}.
*
* @param flags a bitmask composed from the constants defined in
* {@link XAResource}
* @return a formatted representation of the <var>flags</var>
*/
private static String formatFlags(int flags)
{
// Short-circuit evaluation if we've been explicitly passed no flags
if (flags == XAResource.TMNOFLAGS) {
return "TMNOFLAGS";
}
StringBuffer buffer = new StringBuffer();
// Add any flags that are present
for (Map.Entry<Integer, String> entry : flagMap.entrySet()) {
int flag = entry.getKey();
// If this flag is present, add it to the formatted output and remove
// from the bitmask
if ((flag & flags) == flag) {
if (buffer.length() > 0) {
buffer.append("|");
}
buffer.append(entry.getValue());
flags &= ~flag;
}
}
// We would expect to have removed all flags by this point
// If there's some unknown flag we've missed, format it as hexadecimal
if (flags != 0) {
if (buffer.length() > 0) {
buffer.append(",");
}
buffer.append("0x").append(Integer.toHexString(flags));
}
return buffer.toString();
}
}