/*
* Copyright 2008 The Topaz Foundation
*
* Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
* Contributions:
*/
package org.mulgara.resolver.spi;
// Java 2 standard packages
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
// Third party packages
import org.apache.log4j.Logger;
/**
* A skeleton XAResource implementation. This handles the basic
* resource-manager and transaction management and ensures correct {@link
* #isSameRM} implementation. Subclasses must implement the actual
* functionality in the {@link #doStart}, {@link #doPrepare}, {@link
* #doCommit}, and {@link #doRollback} methods.
*
* @created 2008-02-16
* @author Ronald Tschalär
* @licence Apache License v2.0
*/
public abstract class AbstractXAResource<R extends AbstractXAResource.RMInfo<T>,T extends AbstractXAResource.TxInfo>
extends DummyXAResource {
/** Logger. */
private static final Logger logger =
Logger.getLogger(AbstractXAResource.class.getName());
protected static final Map<ResolverFactory,RMInfo<? extends TxInfo>> resourceManagers =
new WeakHashMap<ResolverFactory,RMInfo<? extends TxInfo>>();
protected final R resourceManager;
//
// Constructor
//
/**
* Construct an XAResource.
*
* @param transactionTimeout transaction timeout period, in seconds
* @param resolverFactory the resolver-factory we belong to
*/
public AbstractXAResource(int transactionTimeout, ResolverFactory resolverFactory) {
super(transactionTimeout);
synchronized (resourceManagers) {
@SuppressWarnings("unchecked")
R rmgr = (R)resourceManagers.get(resolverFactory);
if (rmgr == null)
resourceManagers.put(resolverFactory, rmgr = newResourceManager());
this.resourceManager = rmgr;
}
}
/**
* Create a new resource-manager instance - invoked only from the
* constructor and only when no resource-manager instance exists for the
* given resolver-factory.
*/
protected abstract R newResourceManager();
/**
* Create a new transaction-info instance. This is invoked whenever a new
* transaction is started.
*/
protected abstract T newTransactionInfo();
//
// Methods implementing XAResource
//
public void start(Xid xid, int flags) throws XAException {
if (logger.isDebugEnabled()) {
logger.debug("Start xid=" + formatXid(xid) + " flags=" + formatFlags(flags));
}
xid = new XidWrapper(xid);
T tx = resourceManager.transactions.get(xid);
boolean isNew = false;
switch (flags) {
case XAResource.TMRESUME:
if (tx == null) {
logger.error("Attempting to resume unknown transaction.");
throw new XAException(XAException.XAER_NOTA);
}
if (logger.isDebugEnabled()) {
logger.debug("Resuming transaction on xid=" + formatXid(xid));
}
break;
case XAResource.TMNOFLAGS:
if (tx != null) {
logger.warn("Received plain start for existing tx: xid=" + formatXid(xid));
throw new XAException(XAException.XAER_DUPID);
}
// fallthrough
case XAResource.TMJOIN:
if (tx == null) {
resourceManager.transactions.put(xid, tx = newTransactionInfo());
tx.xid = xid;
isNew = true;
}
break;
default:
logger.error("Unrecognised flags in start: xid=" + formatXid(xid) + " flags=" + formatFlags(flags));
throw new XAException(XAException.XAER_INVAL);
}
try {
doStart(tx, flags, isNew);
} catch (Throwable t) {
logger.warn("Failed to do start", t);
reThrow(t, tx, false);
}
}
public void end(Xid xid, int flags) throws XAException {
if (logger.isDebugEnabled()) {
logger.debug("End xid=" + formatXid(xid) + " flags=" + formatFlags(flags));
}
T tx = getTxn(xid, "end");
try {
doEnd(tx, flags);
} catch (Throwable t) {
logger.warn("Failed to do end", t);
reThrow(t, tx, false);
}
}
public int prepare(Xid xid) throws XAException {
if (logger.isDebugEnabled()) {
logger.debug("Prepare xid=" + formatXid(xid));
}
T tx = getTxn(xid, "prepare");
try {
int sts = doPrepare(tx);
if (sts == XA_RDONLY) {
transactionCompleted(tx);
}
return sts;
} catch (Throwable t) {
logger.warn("Attempt to prepare failed", t);
reThrow(t, tx, true);
throw new Error("dummy for the compiler - never reached");
}
}
public void commit(Xid xid, boolean onePhase) throws XAException {
if (logger.isDebugEnabled()) {
logger.debug("Commit xid=" + formatXid(xid) + " onePhase=" + onePhase);
}
T tx = getTxn(xid, "commit");
if (onePhase) {
try {
int sts = doPrepare(tx);
if (sts == XA_RDONLY) {
transactionCompleted(tx);
return;
}
} catch (Throwable th) {
logger.error("Attempt to prepare in onePhaseCommit failed.", th);
rollback(xid);
throw (XAException)new XAException(XAException.XA_RBROLLBACK).initCause(th);
}
}
boolean clean = true;
try {
doCommit(tx);
} catch (XAException xae) {
if (isHeuristic(xae)) {
clean = false;
}
throw xae;
} catch (Throwable th) {
// This is a serious problem since the database is now in an
// inconsistent state.
// Make sure the exception is logged.
logger.fatal("Failed to commit resource in transaction " + formatXid(xid), th);
throw (XAException)new XAException(XAException.XAER_RMERR).initCause(th);
} finally {
if (clean) {
transactionCompleted(tx);
}
}
}
public void rollback(Xid xid) throws XAException {
if (logger.isDebugEnabled()) {
logger.debug("Rollback xid=" + formatXid(xid));
}
T tx = getTxn(xid, "roll back");
boolean clean = true;
try {
doRollback(tx);
} catch (XAException xae) {
if (isHeuristic(xae)) {
clean = false;
}
throw xae;
} catch (Throwable th) {
// This is a serious problem since the database is now in an
// inconsistent state.
// Make sure the exception is logged.
logger.fatal("Failed to rollback resource in transaction " + formatXid(xid), th);
throw (XAException)new XAException(XAException.XAER_RMERR).initCause(th);
} finally {
if (clean) {
transactionCompleted(tx);
}
}
}
public void forget(Xid xid) throws XAException {
if (logger.isDebugEnabled()) {
logger.debug("Forget xid=" + formatXid(xid));
}
T tx = getTxn(xid, "forget");
boolean clean = true;
try {
doForget(tx);
} catch (XAException xae) {
if (xae.errorCode == XAException.XAER_RMERR) {
clean = false;
}
throw xae;
} catch (Throwable th) {
logger.error("Failed to forget transaction " + formatXid(xid), th);
clean = false;
throw (XAException)new XAException(XAException.XAER_RMERR).initCause(th);
} finally {
if (clean) {
transactionCompleted(tx);
}
}
}
public Xid[] recover(int flag) throws XAException {
if (logger.isDebugEnabled()) {
logger.debug("Recover flag=" + formatFlags(flag));
}
throw new XAException(XAException.XAER_RMERR);
}
public boolean isSameRM(XAResource xaResource) throws XAException {
boolean same = (xaResource instanceof AbstractXAResource) &&
((AbstractXAResource<?,?>)xaResource).resourceManager == resourceManager;
if (logger.isDebugEnabled()) {
logger.debug("Is same resource manager? " + same + " :: " + xaResource + " on " + this);
}
return same;
}
/**
* Look up the transaction-info object.
*
* @param xid the xid
* @param op the current operation - used for error messages
* @return the transaction-info object
* @throws XAException if no transaction-info could be found for the given xid
*/
protected T getTxn(Xid xid, String op) throws XAException {
T tx = resourceManager.transactions.get(new XidWrapper(xid));
if (tx != null) return tx;
logger.error("Attempting to " + op + " unknown transaction: xid=" + formatXid(xid));
throw new XAException(XAException.XAER_NOTA);
}
/**
* Rethrow the caught exception. If it is not an <var>XAException</var> then it is wrapped in an
* XAException. This method never returns normally.
*
* @param t the exception to rethrow.
* @param tx the current transaction
* @param doneOnRb whether this transaction should be considered completed if <var>t</var>
* indicates a rollback.
* @throws XAException always
*/
protected void reThrow(Throwable t, T tx, boolean doneOnRb) throws XAException {
if (t instanceof XAException) {
XAException xae = (XAException)t;
if (xae.errorCode == XAException.XAER_RMFAIL || doneOnRb && isRollback(xae)) {
transactionCompleted(tx);
}
throw xae;
}
throw (XAException)new XAException(XAException.XAER_RMERR).initCause(t);
}
/**
* Invoked on start with valid flags and tx state.
*
* @param tx the transaction being started; always non-null
* @param flags one of TMNOFLAGS, TMRESUME, or TMJOIN
* @param isNew true if <var>tx</var> was created as part of this start()
* @throws Exception
*/
protected abstract void doStart(T tx, int flags, boolean isNew) throws Exception;
/**
* Invoked on end().
*
* @param tx the transaction being ended; always non-null
* @param flags one of TMSUCCESS, TMFAIL, or TMSUSPEND
* @throws Exception
*/
protected abstract void doEnd(T tx, int flags) throws Exception;
/**
* Invoked on prepare() or commit(onePhase=true).
*
* @param tx the transaction being prepared; always non-null
* @return XA_OK or XA_RDONLY
* @throws Exception
*/
protected abstract int doPrepare(T tx) throws Exception;
/**
* Invoked on commit().
*
* @param tx the transaction being committed; always non-null
* @throws Exception
*/
protected abstract void doCommit(T tx) throws Exception;
/**
* Invoked on (explicit or implicit) rollback().
*
* @param tx the transaction being rolled back; always non-null
* @throws Exception
*/
protected abstract void doRollback(T tx) throws Exception;
/**
* Invoked on forget().
*
* @param tx the transaction to forget; always non-null
* @throws Exception
*/
protected abstract void doForget(T tx) throws Exception;
/**
* This is invoked whenever a transaction has fully completed. Subclasses may override this but
* must make sure to always invoke <code>super.transactionCompleted()</code>.
*
* @param tx the transaction that completed.
*/
protected void transactionCompleted(T tx) {
resourceManager.transactions.remove(tx.xid);
}
/** The resource-manager info */
public static class RMInfo<T extends TxInfo> {
/** the list of active transactions */
public final Map<Xid,T> transactions = Collections.synchronizedMap(new HashMap<Xid,T>());
}
/** The info pertaining to a single transaction */
public static class TxInfo {
/** the underlying Xid of this transaction; not valid till the first start() */
public Xid xid;
}
/**
* Xid-wrapper that implements hashCode() and equals(). JTA does not require
* Xid's to implement hashCode() and equals(), so in order to be able to use
* them as keys in a map we need to wrap them with something that implements
* them based on the individual fields of the Xid.
*/
public static class XidWrapper implements Xid {
private final Xid xid;
private final int hash;
public XidWrapper(Xid xid) {
this.xid = xid;
this.hash = Arrays.hashCode(xid.getBranchQualifier());
}
public int getFormatId() {
return xid.getFormatId();
}
public byte[] getGlobalTransactionId() {
return xid.getGlobalTransactionId();
}
public byte[] getBranchQualifier() {
return xid.getBranchQualifier();
}
public int hashCode() {
return hash;
}
public boolean equals(Object other) {
if (other == this) return true;
if (!(other instanceof Xid)) return false;
Xid o = (Xid)other;
return o.getFormatId() == xid.getFormatId() &&
Arrays.equals(o.getGlobalTransactionId(), xid.getGlobalTransactionId()) &&
Arrays.equals(o. getBranchQualifier(), xid. getBranchQualifier());
}
}
}