org.firebirdsql.jca.FBManagedConnection Maven / Gradle / Ivy
Show all versions of jaybird-jdk17 Show documentation
/*
* Firebird Open Source JavaEE Connector - JDBC Driver
*
* Distributable under LGPL license.
* You may obtain a copy of the License at http://www.gnu.org/copyleft/lgpl.html
*
* 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
* LGPL License for more details.
*
* This file was created by members of the firebird development team.
* All individual contributions remain the Copyright (C) of those
* individuals. Contributors to this file are either listed here or
* can be obtained from a source control history command.
*
* All rights reserved.
*/
package org.firebirdsql.jca;
import java.io.ByteArrayInputStream;
import java.io.PrintWriter;
import java.sql.*;
import java.util.*;
import javax.resource.ResourceException;
import javax.resource.spi.*;
import javax.resource.spi.security.PasswordCredential;
import javax.security.auth.Subject;
import javax.transaction.xa.*;
import org.firebirdsql.gds.*;
import org.firebirdsql.gds.impl.AbstractIscStmtHandle;
import org.firebirdsql.gds.impl.AbstractIscTrHandle;
import org.firebirdsql.gds.impl.GDSHelper;
import org.firebirdsql.gds.impl.GDSHelper.GDSHelperErrorListener;
import org.firebirdsql.jdbc.*;
import org.firebirdsql.jdbc.field.FBField;
import org.firebirdsql.jdbc.field.FieldDataProvider;
import org.firebirdsql.logging.Logger;
import org.firebirdsql.logging.LoggerFactory;
import org.firebirdsql.util.SQLExceptionChainBuilder;
/**
* The class FBManagedConnection
implements both the
* ManagedConnection and XAResource interfaces.
*
* @author David Jencks
* @version 1.0
*/
public class FBManagedConnection implements ManagedConnection, XAResource, GDSHelperErrorListener, Synchronizable {
public static final String WARNING_NO_CHARSET = "WARNING: No connection characterset specified (property lc_ctype, encoding, charSet or localEncoding), defaulting to characterset NONE";
private static final Logger log = LoggerFactory.getLogger(FBManagedConnection.class, false);
private final FBManagedConnectionFactory mcf;
private final ArrayList connectionEventListeners = new ArrayList();
private final ArrayList connectionHandles = new ArrayList();
private int timeout = 0;
private final Map xidMap = Collections.synchronizedMap(new HashMap());
private final GDS gds;
private final IscDbHandle dbHandle;
private GDSHelper gdsHelper;
private final FBConnectionRequestInfo cri;
private FBTpb tpb;
private int transactionIsolation;
private volatile boolean managedEnvironment = true;
private volatile boolean connectionSharing = true;
private final Set preparedXid = Collections.synchronizedSet(new HashSet());
private volatile boolean inDistributedTransaction = false;
FBManagedConnection(Subject subject, ConnectionRequestInfo cri,
FBManagedConnectionFactory mcf) throws ResourceException {
this.mcf = mcf;
this.gds = mcf.getGDS();
this.cri = getCombinedConnectionRequestInfo(subject, cri);
this.tpb = mcf.getDefaultTpb();
this.transactionIsolation = mcf.getDefaultTransactionIsolation();
//TODO: XIDs in limbo should be loaded so that XAER_DUPID can be thrown appropriately
try {
dbHandle = gds.createIscDbHandle();
DatabaseParameterBuffer dpb = this.cri.getDpb();
if (dpb.getArgumentAsString(DatabaseParameterBuffer.LC_CTYPE) == null) {
if (log != null) {
log.warn(WARNING_NO_CHARSET);
}
dbHandle.addWarning(new GDSWarning(WARNING_NO_CHARSET));
}
if (!dpb.hasArgument(DatabaseParameterBuffer.CONNECT_TIMEOUT) && DriverManager.getLoginTimeout() > 0) {
dpb.addArgument(DatabaseParameterBuffer.CONNECT_TIMEOUT, DriverManager.getLoginTimeout());
}
gds.iscAttachDatabase(mcf.getDatabase(), dbHandle, dpb);
gdsHelper = new GDSHelper(gds, dpb, dbHandle, this);
} catch(GDSException ex) {
throw new FBResourceException(ex);
}
}
/**
* Notify GDS container that error occured, if the ex
* represents a "fatal" one
*
* @see FatalGDSErrorHelper#isFatal(GDSException)
*/
public void errorOccured(GDSException ex) {
if (log != null) log.trace(ex.getMessage());
if (!FatalGDSErrorHelper.isFatal(ex))
return;
ConnectionEvent event = new ConnectionEvent(
FBManagedConnection.this,
ConnectionEvent.CONNECTION_ERROR_OCCURRED, ex);
FBManagedConnection.this.notify(
connectionErrorOccurredNotifier, event);
}
private FBConnectionRequestInfo getCombinedConnectionRequestInfo(
Subject subject, ConnectionRequestInfo cri)
throws ResourceException {
if (cri == null) {
cri = mcf.getDefaultConnectionRequestInfo();
}
try {
FBConnectionRequestInfo fbcri = (FBConnectionRequestInfo) cri;
if (subject != null) {
// see connector spec, section 8.2.6, contract for
// ManagedConnectinFactory, option A.
for (Iterator i = subject.getPrivateCredentials().iterator(); i
.hasNext();) {
Object cred = i.next();
if (cred instanceof PasswordCredential
&& mcf.equals(((PasswordCredential) cred)
.getManagedConnectionFactory())) {
PasswordCredential pcred = (PasswordCredential) cred;
String user = pcred.getUserName();
String password = new String(pcred.getPassword());
fbcri.setPassword(password);
fbcri.setUserName(user);
break;
}
}
}
return fbcri;
} catch (ClassCastException cce) {
throw new FBResourceException(
"Incorrect ConnectionRequestInfo class supplied");
}
}
/**
* Get instance of {@link GDSHelper} connected with this managed connection.
*
* @return instance of {@link GDSHelper}.
* @throws GDSException If this connection has no GDSHelper
*/
public GDSHelper getGDSHelper() throws GDSException {
if (gdsHelper == null)
throw new GDSException(ISCConstants.isc_arg_gds, ISCConstants.isc_req_no_trans);
return gdsHelper;
}
public String getDatabase() {
return mcf.getDatabase();
}
public boolean isManagedEnvironment() {
return managedEnvironment;
}
public boolean inTransaction() {
return gdsHelper != null && gdsHelper.inTransaction();
}
public void setManagedEnvironment(boolean managedEnvironment) throws ResourceException{
this.managedEnvironment = managedEnvironment;
// if connection sharing is not enabled, notify currently associated
// connection handle about the state change.
if (!connectionSharing) {
if (connectionHandles.size() > 1)
throw new javax.resource.spi.IllegalStateException(
"Multiple connections associated with this managed " +
"connection in non-sharing mode.");
// there will be at most one connection.
for (Iterator iter = connectionHandles.iterator(); iter.hasNext();) {
AbstractConnection connection = (AbstractConnection) iter.next();
try {
connection.setManagedEnvironment(managedEnvironment);
} catch(SQLException ex) {
throw new FBResourceException(ex);
}
}
}
}
/**
* Check if connection sharing is enabled. When connection sharing is
* enabled, multiple connection handles ({@link AbstractConnection} instances)
* can access this managed connection in thread-safe manner (they synchronize
* on this instance). This feature can be enabled only in JCA environment,
* any other environment must not use connection sharing.
*
* @return true
if connection sharing is enabled.
*/
public boolean isConnectionSharing() {
return connectionSharing;
}
/**
* Enable or disable connection sharing. See {@link #isConnectionSharing()}
* method for details.
*
* @param connectionSharing true
if connection sharing must be
* enabled.
* @throws ResourceException If connection sharing state cannot be changed
*/
public void setConnectionSharing(boolean connectionSharing) throws ResourceException {
if (!connectionHandles.isEmpty())
throw new javax.resource.spi.IllegalStateException(
"Cannot change connection sharing with active connection handles.");
this.connectionSharing = connectionSharing;
}
/**
* Returns a javax.resource.spi.LocalTransaction
instance.
* The LocalTransaction interface is used by the container to manage local
* transactions for a RM instance.
*
* @return LocalTransaction instance
* @throws ResourceException
* generic exception if operation fails
* @throws javax.resource.NotSupportedException
* if the operation is not supported
* @throws ResourceAdapterInternalException
* resource adapter internal error condition
*/
public LocalTransaction getLocalTransaction() {
return new FBLocalTransaction(this, null);
}
/**
* Gets the metadata information for this connection's underlying EIS
* resource manager instance. The ManagedConnectionMetaData interface
* provides information about the underlying EIS instance associated with
* the ManagedConenction instance.
*
* @return ManagedConnectionMetaData instance
* @throws ResourceException
* generic exception if operation fails
* @throws javax.resource.NotSupportedException
* if the operation is not supported
*/
public ManagedConnectionMetaData getMetaData() throws ResourceException {
return new FBManagedConnectionMetaData(this);
}
/**
* Sets the log writer for this ManagedConnection instance.
*
* The log writer is a character output stream to which all logging and
* tracing messages for this ManagedConnection instance will be printed.
* Application Server manages the association of output stream with the
* ManagedConnection instance based on the connection pooling requirements.
*
* When a ManagedConnection object is initially created, the default log
* writer associated with this instance is obtained from the
* ManagedConnectionFactory
. An application server can set a
* log writer specific to this ManagedConnection to log/trace this instance
* using setLogWriter method.
*
* @param out
* Character Output stream to be associated
* @throws ResourceException
* generic exception if operation fails
* @throws ResourceAdapterInternalException
* resource adapter related error condition
*/
public void setLogWriter(PrintWriter out) {
// ignore, we are using log4j.
}
/**
* Gets the log writer for this ManagedConnection instance.
*
* The log writer is a character output stream to which all logging and
* tracing messages for this ManagedConnection instance will be printed.
* ConnectionManager
manages the association of output stream
* with the ManagedConnection
instance based on the
* connection pooling requirements.
*
* The Log writer associated with a ManagedConnection
* instance can be one set as default from the ManagedConnectionFactory
* (that created this connection) or one set specifically for this instance
* by the application server.
*
* @return Character ourput stream associated with this
* ManagedConnection
* @throws ResourceException
* generic exception if operation fails
*/
public PrintWriter getLogWriter() {
return null;// we are using log4j.
}
/**
* Add an ConnectionEventListener
listener. The listener will
* be notified when a ConnectionEvent
occurs.
*
* @param listener
* The ConnectionEventListener
to be added
*/
public void addConnectionEventListener(ConnectionEventListener listener) {
connectionEventListeners.add(listener);
}
/**
* Remove a ConnectionEventListner
from the listing of
* listeners that will be notified for a ConnectionEvent
.
*
* @param listener
* The ConnectionEventListener
to be removed
*/
public void removeConnectionEventListener(ConnectionEventListener listener) {
connectionEventListeners.remove(listener);
}
/**
* Used by the container to change the association of an application-level
* connection handle with a ManagedConneciton instance. The container should
* find the right ManagedConnection instance and call the
* associateConnection method.
*
* The resource adapter is required to implement the associateConnection
* method. The method implementation for a ManagedConnection should
* dissociate the connection handle (passed as a parameter) from its
* currently associated ManagedConnection and associate the new connection
* handle with itself.
*
* @param connection
* Application-level connection handle
* @throws ResourceException
* Failed to associate the connection handle with this
* ManagedConnection instance
* @throws javax.resource.spi.IllegalStateException
* Illegal state for invoking this method
* @throws ResourceAdapterInternalException
* Resource adapter internal error condition
*/
public void associateConnection(Object connection) throws ResourceException {
if (!connectionSharing)
disassociateConnections();
try {
((AbstractConnection) connection).setManagedConnection(this);
connectionHandles.add(connection);
} catch (ClassCastException cce) {
throw new FBResourceException(
"invalid connection supplied to associateConnection.", cce);
}
}
/**
* Application server calls this method to force any cleanup on the
* ManagedConnection
instance.
*
* The method {@link ManagedConnection#cleanup}initiates a cleanup of the
* any client-specific state as maintained by a ManagedConnection instance.
* The cleanup should invalidate all connection handles that had been
* created using this ManagedConnection
instance. Any attempt
* by an application component to use the connection handle after cleanup of
* the underlying ManagedConnection
should result in an
* exception.
*
* The cleanup of ManagedConnection is always driven by an application
* server. An application server should not invoke
* {@link ManagedConnection#cleanup}when there is an uncompleted
* transaction (associated with a ManagedConnection instance) in progress.
*
* The invocation of {@link ManagedConnection#cleanup}method on an already
* cleaned-up connection should not throw an exception.
*
* The cleanup of ManagedConnection
instance resets its
* client specific state and prepares the connection to be put back in to a
* connection pool. The cleanup method should not cause resource adapter to
* close the physical pipe and reclaim system resources associated with the
* physical connection.
*
* @throws ResourceException
* generic exception if operation fails
* @throws ResourceAdapterInternalException
* resource adapter internal error condition
* @throws javax.resource.spi.IllegalStateException
* Illegal state for calling connection cleanup. Example - if a
* local transaction is in progress that doesn't allow
* connection cleanup
*/
public void cleanup() throws ResourceException {
disassociateConnections();
if (gdsHelper != null)
gdsHelper.setCurrentTrHandle(null);
// reset the TPB from the previous transaction.
tpb = mcf.getDefaultTpb();
transactionIsolation = mcf.getDefaultTransactionIsolation();
}
/**
* Disassociate connections from current managed connection.
*
*/
private void disassociateConnections() throws ResourceException {
SQLExceptionChainBuilder chain = new SQLExceptionChainBuilder();
// Iterate over copy of list as connection.close() will remove connection
List connectionHandleCopy = new ArrayList(connectionHandles);
for (Iterator i = connectionHandleCopy.iterator(); i.hasNext();) {
AbstractConnection connection = (AbstractConnection) i.next();
try {
connection.close();
} catch(SQLException sqlex) {
chain.append(sqlex);
}
}
connectionHandles.clear();
if (chain.hasException())
throw new FBResourceException(chain.getException());
}
/**
* Creates a new connection handle for the underlying physical connection
* represented by the ManagedConnection
instance. This
* connection handle is used by the application code to refer to the
* underlying physical connection. A connection handle is tied to its
* ManagedConnection
instance in a resource adapter
* implementation specific way.
*
*
* The ManagedConnection
uses the Subject and additional
* ConnectionRequestInfo
(which is specific to resource
* adapter and opaque to application server) to set the state of the
* physical connection.
*
* @param subject
* security context as JAAS subject
* @param cri
* ConnectionRequestInfo instance
* @return generic Object
instance representing the
* connection handle. For CCI, the connection handle created by a
* ManagedConnection
instance is of the type
* javax.resource.cci.Connection
.
* @throws ResourceException
* generic exception if operation fails
* @throws ResourceAdapterInternalException
* resource adapter internal error condition
* @throws javax.resource.spi.SecurityException
* security related error condition
* @throws CommException
* failed communication with EIS instance
* @throws EISSystemException
* internal error condition in EIS instance - used if EIS
* instance is involved in setting state of
* ManagedConnection
*/
public Object getConnection(Subject subject, ConnectionRequestInfo cri)
throws ResourceException {
if (!matches(subject, cri))
throw new FBResourceException("Incompatible subject or "
+ "ConnectionRequestInfo in getConnection!");
if (!connectionSharing)
disassociateConnections();
AbstractConnection c = mcf.newConnection(this);
try {
c.setManagedEnvironment(isManagedEnvironment());
connectionHandles.add(c);
return c;
} catch(SQLException ex) {
throw new FBResourceException(ex);
}
}
/**
* Destroys the physical connection to the underlying resource manager. To
* manage the size of the connection pool, an application server can
* explictly call {@link ManagedConnection#destroy}to destroy a physical
* connection. A resource adapter should destroy all allocated system
* resources for this ManagedConnection
instance when the
* method destroy is called.
*
* @throws ResourceException
* generic exception if operation failed
* @throws javax.resource.spi.IllegalStateException
* illegal state for destroying connection
*/
public void destroy() throws ResourceException {
if (gdsHelper == null)
return;
if (gdsHelper.inTransaction())
throw new javax.resource.spi.IllegalStateException(
"Can't destroy managed connection with active transaction");
try {
gdsHelper.detachDatabase();
} catch (GDSException ge) {
throw new FBResourceException("Can't detach from db.", ge);
} finally {
gdsHelper = null;
}
}
/**
* Return an XA resource to the caller.
*
* In both javax.sql.XAConnection
and
* javax.resource.spi.MangagedConnection
.
*
* @return the XAResource
*/
public XAResource getXAResource() {
if (log != null)
log.debug("XAResource requested from FBManagedConnection");
return this;
}
// --------------------------------------------------------------
// XAResource implementation
// --------------------------------------------------------------
boolean isXidActive(Xid xid) {
IscTrHandle trHandle = (IscTrHandle)xidMap.get(xid); //mcf.getTrHandleForXid(xid);
if (trHandle == null) return false;
IscDbHandle dbHandle = trHandle.getDbHandle();
if (dbHandle == null) return false;
return dbHandle.isValid();
}
/**
* Commits a transaction.
*
* @throws XAException
* Occurs when the state was not correct (end never called), the
* transaction ID is wrong, the connection was set to
* Auto-Commit, or the commit on the underlying connection
* fails. The error code differs depending on the exact
* situation.
*/
public void commit(Xid id, boolean onePhase) throws XAException {
try {
mcf.notifyCommit(this, id, onePhase);
} catch (GDSException ge) {
throw new XAException(ge.getXAErrorCode());
}
}
/**
* The internalCommit
method performs the requested commit
* and may throw a GDSException to be interpreted by the caller.
*
* @param xid
* a Xid
value
* @param onePhase
* a boolean
value
* @exception GDSException
* if an error occurs
*/
void internalCommit(Xid xid, boolean onePhase) throws XAException,
GDSException {
if (log != null) log.trace("Commit called: " + xid);
AbstractIscTrHandle committingTr = (AbstractIscTrHandle)xidMap.get(xid);
// check that prepare has NOT been called when onePhase = true
if (onePhase && isPrepared(xid))
throw new FBXAException("Cannot commit one-phase when transaction has been prepared", XAException.XAER_PROTO);
// check that prepare has been called when onePhase = false
if (!onePhase && !isPrepared(xid))
throw new FBXAException("Cannot commit two-phase when transaction has not been prepared", XAException.XAER_PROTO);
if (committingTr == null)
throw new FBXAException("Commit called with unknown transaction",
XAException.XAER_NOTA);
if (committingTr == getGDSHelper().getCurrentTrHandle())
throw new FBXAException("Commit called with non-ended xid",
XAException.XAER_PROTO);
try {
committingTr.forgetResultSets();
try {
getGDSHelper().commitTransaction(committingTr);
} catch (GDSException ge) {
if (gdsHelper != null) {
try {
gdsHelper.rollbackTransaction(committingTr);
} catch (GDSException ge2) {
if (log != null)
log.debug("Exception rolling back failed tx: ", ge2);
}
} else if (log != null) {
log.warn("Unable to rollback failed tx, connection closed or lost");
}
throw ge;
} finally {
xidMap.remove(xid);
preparedXid.remove(xid);
}
} catch (GDSException ge) {
ge.setXAErrorCode(XAException.XAER_RMERR);
throw ge;
}
}
private boolean isPrepared(Xid xid) {
return preparedXid.contains(xid);
}
/**
* Dissociates a resource from a global transaction.
*
* @throws XAException
* Occurs when the state was not correct (end called twice), or
* the transaction ID is wrong.
*/
public void end(Xid id, int flags) throws XAException {
if (flags != XAResource.TMSUCCESS && flags != XAResource.TMFAIL && flags != XAResource.TMSUSPEND)
throw new FBXAException("flag not allowed in this context: " + flags + ", valid flags are TMSUCCESS, TMFAIL, TMSUSPEND", XAException.XAER_PROTO);
internalEnd(id, flags);
mcf.notifyEnd(this, id);
inDistributedTransaction = false;
try {
// This will reset the managed environment of the associated connections and set the transaction coordinator to local
// TODO This is a bit of a hack; need to find a better way; this doesn't work with connectionSharing = true
setManagedEnvironment(isManagedEnvironment());
} catch (ResourceException ex) {
throw new FBXAException("Reset of managed state failed", XAException.XAER_RMERR);
}
}
/**
* The internalEnd
method ends the xid as requested if
* appropriate and throws a GDSException including the appropriate XA error
* code and a message if not. The caller can decode the exception as
* necessary.
*
* @param xid
* a Xid
value
* @param flags
* an int
value
* @exception XAException
* if an error occurs
*/
void internalEnd(Xid xid, int flags) throws XAException {
if (log != null) log.debug("End called: " + xid);
IscTrHandle endingTr = (IscTrHandle)xidMap.get(xid);
if (endingTr == null)
throw new FBXAException("Unrecognized transaction", XAException.XAER_NOTA);
if (flags == XAResource.TMFAIL) {
try {
gds.iscRollbackTransaction(endingTr);
getGDSHelper().setCurrentTrHandle(null);
} catch (GDSException ex) {
throw new FBXAException("can't rollback transaction", XAException.XAER_RMFAIL, ex);
}
}
else if (flags == XAResource.TMSUCCESS) {
if (endingTr == gdsHelper.getCurrentTrHandle())
gdsHelper.setCurrentTrHandle(null);
else
throw new FBXAException("You are trying to end a transaction "
+ "that is not the current transaction", XAException.XAER_INVAL);
}
else if (flags == XAResource.TMSUSPEND) {
if (endingTr == gdsHelper.getCurrentTrHandle())
gdsHelper.setCurrentTrHandle(null);
else
throw new FBXAException("You are trying to suspend a transaction "
+ "that is not the current transaction", XAException.XAER_INVAL);
}
}
private final static String FORGET_FIND_QUERY = "SELECT RDB$TRANSACTION_ID, RDB$TRANSACTION_DESCRIPTION "
+ "FROM RDB$TRANSACTIONS WHERE RDB$TRANSACTION_STATE IN (2, 3)";
private final static String FORGET_DELETE_QUERY = "DELETE FROM RDB$TRANSACTIONS WHERE RDB$TRANSACTION_ID = ";
/**
* Indicates that no further action will be taken on behalf of this
* transaction (after a heuristic failure). It is assumed this will be
* called after a failed commit or rollback.
*
* @throws XAException
* Occurs when the state was not correct (end never called), or
* the transaction ID is wrong.
*/
public void forget(Xid id) throws XAException {
long inLimboId = -1;
try {
// find XID
AbstractIscTrHandle trHandle2 = (AbstractIscTrHandle)gds.createIscTrHandle();
gds.iscStartTransaction(trHandle2, getGDSHelper().getCurrentDbHandle(), tpb.getTransactionParameterBuffer());
AbstractIscStmtHandle stmtHandle2 = (AbstractIscStmtHandle)gds.createIscStmtHandle();
gds.iscDsqlAllocateStatement(getGDSHelper().getCurrentDbHandle(), stmtHandle2);
GDSHelper gdsHelper2 = new GDSHelper(gds, getGDSHelper().getDatabaseParameterBuffer(), getGDSHelper().getCurrentDbHandle(), null);
gdsHelper2.setCurrentTrHandle(trHandle2);
gdsHelper2.prepareStatement(stmtHandle2, FORGET_FIND_QUERY, false);
gdsHelper2.executeStatement(stmtHandle2, false);
gdsHelper2.fetch(stmtHandle2, 10);
DataProvider dataProvider0 = new DataProvider(stmtHandle2, 0);
DataProvider dataProvider1 = new DataProvider(stmtHandle2, 1);
FBField field0 = FBField.createField(stmtHandle2.getOutSqlda().sqlvar[0], dataProvider0, gdsHelper2, false);
FBField field1 = FBField.createField(stmtHandle2.getOutSqlda().sqlvar[1], dataProvider1, gdsHelper2, false);
field0.setConnection(gdsHelper2);
field1.setConnection(gdsHelper2);
int row = 0;
while(row < stmtHandle2.getRows().length) {
if (stmtHandle2.getRows()[row] == null) {
row++;
continue;
}
dataProvider0.setRow(row);
dataProvider1.setRow(row);
long inLimboTxId = field0.getLong();
byte[] inLimboMessage = field1.getBytes();
try {
FBXid xid = new FBXid(new ByteArrayInputStream(inLimboMessage), inLimboTxId);
boolean gtridEquals = Arrays.equals(xid.getGlobalTransactionId(), id.getGlobalTransactionId());
boolean bqualEquals = Arrays.equals(xid.getBranchQualifier(), id.getBranchQualifier());
if (gtridEquals && bqualEquals) {
inLimboId = inLimboTxId;
break;
}
} catch(FBIncorrectXidException ex) {
if (log != null)
log.warn("incorrect XID format in RDB$TRANSACTIONS where RDB$TRANSACTION_ID=" + inLimboTxId, ex);
}
row++;
}
gdsHelper2.closeStatement(stmtHandle2, true);
gds.iscCommitTransaction(trHandle2);
} catch (GDSException ex) {
if (log != null)
log.debug("can't perform query to fetch xids", ex);
throw new FBXAException(XAException.XAER_RMFAIL, ex);
} catch (SQLException ex) {
if (log != null)
log.debug("can't perform query to fetch xids", ex);
throw new FBXAException(XAException.XAER_RMFAIL, ex);
} catch (ResourceException ex) {
if (log != null)
log.debug("can't perform query to fetch xids", ex);
throw new FBXAException(XAException.XAER_RMFAIL, ex);
}
if (inLimboId == -1)
throw new FBXAException("XID not found", XAException.XAER_NOTA); // TODO: is XAER_NOTA the proper error code ?
try {
// delete XID
AbstractIscTrHandle trHandle2 = (AbstractIscTrHandle)gds.createIscTrHandle();
gds.iscStartTransaction(trHandle2, getGDSHelper().getCurrentDbHandle(), tpb.getTransactionParameterBuffer());
AbstractIscStmtHandle stmtHandle2 = (AbstractIscStmtHandle)gds.createIscStmtHandle();
gds.iscDsqlAllocateStatement(getGDSHelper().getCurrentDbHandle(), stmtHandle2);
stmtHandle2 = (AbstractIscStmtHandle)gds.createIscStmtHandle();
gds.iscDsqlAllocateStatement(getGDSHelper().getCurrentDbHandle(), stmtHandle2);
GDSHelper gdsHelper2 = new GDSHelper(gds, getGDSHelper().getDatabaseParameterBuffer(), getGDSHelper().getCurrentDbHandle(), null);
gdsHelper2.setCurrentTrHandle(trHandle2);
gdsHelper2.prepareStatement(stmtHandle2, FORGET_DELETE_QUERY + inLimboId, false);
gdsHelper2.executeStatement(stmtHandle2, false);
gdsHelper2.closeStatement(stmtHandle2, true);
gds.iscCommitTransaction(trHandle2);
} catch (GDSException ex) {
throw new FBXAException("can't perform query to fetch xids", XAException.XAER_RMFAIL, ex);
} catch (SQLException ex) {
throw new FBXAException("can't perform query to fetch xids", XAException.XAER_RMFAIL, ex);
}
}
/**
* Gets the transaction timeout.
*/
public int getTransactionTimeout() throws javax.transaction.xa.XAException {
return timeout;
}
/**
* Retrieve whether this FBManagedConnection
uses the same
* ResourceManager as res
. This method relies on
* res
being a Firebird implementation of
* XAResource
.
*
* @param res
* The other XAResource
to compare to
* @return true
if res
uses the same
* ResourceManager, false
otherwise
*/
public boolean isSameRM(XAResource res) throws XAException {
return (res instanceof FBManagedConnection)
&& (dbHandle.equals(((FBManagedConnection) res).dbHandle));
}
/**
* Prepares a transaction to commit.
*
* @throws XAException
* Occurs when the state was not correct (end never called), the
* transaction ID is wrong, or the connection was set to
* Auto-Commit.
*/
public int prepare(Xid xid) throws javax.transaction.xa.XAException {
try {
return mcf.notifyPrepare(this, xid);
} catch (GDSException ge) {
throw new FBXAException(XAException.XAER_RMERR, ge);
}
}
int internalPrepare(Xid xid) throws FBXAException, GDSException {
if (log != null) log.trace("prepare called: " + xid);
AbstractIscTrHandle committingTr = (AbstractIscTrHandle)xidMap.get(xid);
if (committingTr == null)
throw new FBXAException("Prepare called with unknown transaction",
XAException.XAER_NOTA);
if (committingTr == getGDSHelper().getCurrentTrHandle())
throw new FBXAException("Prepare called with non-ended xid",
XAException.XAER_PROTO);
try {
FBXid fbxid;
if (xid instanceof FBXid) {
fbxid = (FBXid) xid;
} else {
fbxid = new FBXid(xid);
}
byte[] message = fbxid.toBytes();
getGDSHelper().prepareTransaction(committingTr, message);
} catch (GDSException ge) {
try {
if (gdsHelper != null) {
gdsHelper.rollbackTransaction(committingTr);
} else if (log != null) {
log.warn("Unable to rollback failed tx, connection closed or lost");
}
} catch (GDSException ge2) {
if (log != null)
log.debug("Exception rolling back failed tx: ", ge2);
} finally {
xidMap.remove(xid);
}
if (log != null) log.warn("error in prepare", ge);
throw ge;
}
preparedXid.add(xid);
return XA_OK;
}
private static final String RECOVERY_QUERY =
"SELECT RDB$TRANSACTION_ID, RDB$TRANSACTION_DESCRIPTION "
+ "FROM RDB$TRANSACTIONS";
/**
* Obtain a list of prepared transaction branches from a resource manager.
* The transaction manager calls this method during recovery to obtain the
* list of transaction branches that are currently in prepared or
* heuristically completed states.
*
* @param flags
* One of TMSTARTRSCAN, TMENDRSCAN, TMNOFLAGS. TMNOFLAGS must be
* used when no other flags are set in flags.
* @return The resource manager returns zero or more XIDs for the
* transaction branches that are currently in a prepared or
* heuristically completed state. If an error occurs during the
* operation, the resource manager should throw the appropriate
* XAException.
* @throws XAException
* An error has occurred. Possible values are XAER_RMERR,
* XAER_RMFAIL, XAER_INVAL, and XAER_PROTO.
*/
public Xid[] recover(int flags) throws javax.transaction.xa.XAException {
if (flags != XAResource.TMSTARTRSCAN && flags != XAResource.TMENDRSCAN && flags != XAResource.TMNOFLAGS && flags != (XAResource.TMSTARTRSCAN|XAResource.TMENDRSCAN))
throw new FBXAException("flag not allowed in this context: " + flags + ", valid flags are TMSTARTRSCAN, TMENDRSCAN, TMNOFLAGS, TMSTARTRSCAN|TMENDRSCAN", XAException.XAER_PROTO);
try {
// if (!((flags & XAResource.TMSTARTRSCAN) == 0))
// if ((flags & XAResource.TMENDRSCAN) == 0 && (flags & XAResource.TMNOFLAGS) == 0)
// return new Xid[0];
ArrayList xids = new ArrayList();
AbstractIscTrHandle trHandle2 = (AbstractIscTrHandle)gds.createIscTrHandle();
gds.iscStartTransaction(trHandle2, getGDSHelper().getCurrentDbHandle(), tpb.getTransactionParameterBuffer());
AbstractIscStmtHandle stmtHandle2 = (AbstractIscStmtHandle)gds.createIscStmtHandle();
gds.iscDsqlAllocateStatement(getGDSHelper().getCurrentDbHandle(), stmtHandle2);
GDSHelper gdsHelper2 = new GDSHelper(gds, getGDSHelper().getDatabaseParameterBuffer(), getGDSHelper().getCurrentDbHandle(), null);
gdsHelper2.setCurrentTrHandle(trHandle2);
gdsHelper2.prepareStatement(stmtHandle2, RECOVERY_QUERY, false);
gdsHelper2.executeStatement(stmtHandle2, false);
gdsHelper2.fetch(stmtHandle2, 10);
DataProvider dataProvider0 = new DataProvider(stmtHandle2, 0);
DataProvider dataProvider1 = new DataProvider(stmtHandle2, 1);
FBField field0 = FBField.createField(stmtHandle2.getOutSqlda().sqlvar[0], dataProvider0, gdsHelper2, false);
FBField field1 = FBField.createField(stmtHandle2.getOutSqlda().sqlvar[1], dataProvider1, gdsHelper2, false);
field0.setConnection(gdsHelper2);
field1.setConnection(gdsHelper2);
int row = 0;
while(row < stmtHandle2.getRows().length) {
if (stmtHandle2.getRows()[row] == null) {
row++;
continue;
}
dataProvider0.setRow(row);
dataProvider1.setRow(row);
long inLimboTxId = field0.getLong();
byte[] inLimboMessage = field1.getBytes();
try {
FBXid xid = new FBXid(new ByteArrayInputStream(inLimboMessage), inLimboTxId);
xids.add(xid);
} catch(FBIncorrectXidException ex) {
if (log != null)
log.warn("ignoring XID stored with invalid format in RDB$TRANSACTIONS for RDB$TRANSACTION_ID=" + inLimboTxId);
}
row++;
}
gdsHelper2.closeStatement(stmtHandle2, true);
gds.iscCommitTransaction(trHandle2);
return (FBXid[])xids.toArray(new FBXid[xids.size()]);
} catch(GDSException ex) {
throw new FBXAException("can't perform query to fetch xids", XAException.XAER_RMFAIL, ex);
} catch (SQLException sqle) {
throw new FBXAException("can't perform query to fetch xids", XAException.XAER_RMFAIL, sqle);
} catch (ResourceException re) {
throw new FBXAException("can't perform query to fetch xids", XAException.XAER_RMFAIL, re);
}
}
private static final String RECOVERY_QUERY_PARAMETRIZED =
"SELECT RDB$TRANSACTION_ID, RDB$TRANSACTION_DESCRIPTION "
+ "FROM RDB$TRANSACTIONS "
+ "WHERE RDB$TRANSACTION_DESCRIPTION = CAST(? AS VARCHAR(32764) CHARACTER SET OCTETS)";
/**
* Obtain a single prepared transaction branch from a resource manager, based on a Xid
*
* @param externalXid
* The Xid to find
* @return The Xid if found, otherwise null.
* @throws XAException
* An error has occurred. Possible values are XAER_RMERR,
* XAER_RMFAIL, XAER_INVAL, and XAER_PROTO.
*/
protected Xid findSingleXid(Xid externalXid) throws javax.transaction.xa.XAException {
try {
AbstractIscTrHandle trHandle2 = (AbstractIscTrHandle)gds.createIscTrHandle();
gds.iscStartTransaction(trHandle2, getGDSHelper().getCurrentDbHandle(), tpb.getTransactionParameterBuffer());
AbstractIscStmtHandle stmtHandle2 = (AbstractIscStmtHandle)gds.createIscStmtHandle();
gds.iscDsqlAllocateStatement(getGDSHelper().getCurrentDbHandle(), stmtHandle2);
GDSHelper gdsHelper2 = new GDSHelper(gds, getGDSHelper().getDatabaseParameterBuffer(), getGDSHelper().getCurrentDbHandle(), null);
gdsHelper2.setCurrentTrHandle(trHandle2);
gdsHelper2.prepareStatement(stmtHandle2, RECOVERY_QUERY_PARAMETRIZED, true);
FBXid tempXid = new FBXid(externalXid);
stmtHandle2.getInSqlda().sqlvar[0].sqldata = tempXid.toBytes();
gdsHelper2.executeStatement(stmtHandle2, false);
gdsHelper2.fetch(stmtHandle2, 1);
DataProvider dataProvider0 = new DataProvider(stmtHandle2, 0);
DataProvider dataProvider1 = new DataProvider(stmtHandle2, 1);
FBField field0 = FBField.createField(stmtHandle2.getOutSqlda().sqlvar[0], dataProvider0, gdsHelper2, false);
FBField field1 = FBField.createField(stmtHandle2.getOutSqlda().sqlvar[1], dataProvider1, gdsHelper2, false);
field0.setConnection(gdsHelper2);
field1.setConnection(gdsHelper2);
FBXid xid = null;
if (stmtHandle2.getRows().length > 0) {
dataProvider0.setRow(0);
dataProvider1.setRow(0);
long inLimboTxId = field0.getLong();
byte[] inLimboMessage = field1.getBytes();
try {
xid = new FBXid(new ByteArrayInputStream(inLimboMessage), inLimboTxId);
} catch(FBIncorrectXidException ex) {
if (log != null)
log.warn("ignoring XID stored with invalid format in RDB$TRANSACTIONS for RDB$TRANSACTION_ID=" + inLimboTxId);
}
}
gdsHelper2.closeStatement(stmtHandle2, true);
gds.iscCommitTransaction(trHandle2);
return xid;
} catch(GDSException ex) {
throw new FBXAException("can't perform query to fetch xids", XAException.XAER_RMFAIL, ex);
} catch (SQLException sqle) {
throw new FBXAException("can't perform query to fetch xids", XAException.XAER_RMFAIL, sqle);
} catch (ResourceException re) {
throw new FBXAException("can't perform query to fetch xids", XAException.XAER_RMFAIL, re);
}
}
@Override
public final Object getSynchronizationObject() {
return dbHandle;
}
private static class DataProvider implements FieldDataProvider {
private AbstractIscStmtHandle stmtHandle;
private int fieldPos;
private int row;
private DataProvider(AbstractIscStmtHandle stmtHandle, int fieldPos) {
this.stmtHandle = stmtHandle;
this.fieldPos = fieldPos;
}
public void setRow(int row) {
this.row = row;
}
public byte[] getFieldData() {
return ((byte[][])stmtHandle.getRows()[row])[fieldPos];
}
public void setFieldData(byte[] data) {
throw new UnsupportedOperationException();
}
}
/**
* Rolls back the work, assuming it was done on behalf of the specified
* transaction.
*
* @throws XAException
* Occurs when the state was not correct (end never called), the
* transaction ID is wrong, the connection was set to
* Auto-Commit, or the rollback on the underlying connection
* fails. The error code differs depending on the exact
* situation.
*/
public void rollback(Xid xid) throws XAException {
try {
mcf.notifyRollback(this, xid);
} catch (GDSException ge) {
throw new FBXAException(ge.getXAErrorCode(), ge);
}
}
void internalRollback(Xid xid) throws XAException, GDSException {
if (log != null) log.trace("rollback called: " + xid);
AbstractIscTrHandle committingTr = (AbstractIscTrHandle)xidMap.get(xid); //mcf.getTrHandleForXid(id);
if (committingTr == null) {
throw new FBXAException ("Rollback called with unknown transaction: " + xid);
}
if (committingTr == getGDSHelper().getCurrentTrHandle())
throw new FBXAException("Rollback called with non-ended xid",
XAException.XAER_PROTO);
try {
committingTr.forgetResultSets();
try {
getGDSHelper().rollbackTransaction(committingTr);
} finally {
xidMap.remove(xid);
preparedXid.remove(xid);
}
} catch (GDSException ge) {
if (log != null) log.debug("Exception in rollback", ge);
ge.setXAErrorCode(XAException.XAER_RMERR);
throw ge;
}
}
/**
* Sets the transaction timeout. This is saved, but the value is not used by
* the current implementation.
*
* @param timeout
* The timeout to be set in seconds
*/
public boolean setTransactionTimeout(int timeout)
throws javax.transaction.xa.XAException {
this.timeout = timeout;
return true;
}
public boolean inDistributedTransaction() {
return inDistributedTransaction;
}
/**
* Associates a JDBC connection with a global transaction. We assume that
* end will be called followed by prepare, commit, or rollback. If start is
* called after end but before commit or rollback, there is no way to
* distinguish work done by different transactions on the same connection).
* If start is called more than once before end, either it's a duplicate
* transaction ID or illegal transaction ID (since you can't have two
* transactions associated with one DB connection).
*
*
* @param id
* A global transaction identifier to be associated with the
* resource
* @param flags
* One of TMNOFLAGS, TMJOIN, or TMRESUME
* @throws XAException
* Occurs when the state was not correct (start called twice),
* the transaction ID is wrong, or the instance has already been
* closed.
*/
public void start(Xid id, int flags) throws XAException {
if (flags != XAResource.TMNOFLAGS && flags != XAResource.TMJOIN && flags != XAResource.TMRESUME)
throw new FBXAException("flag not allowed in this context: " + flags + ", valid flags are TMNOFLAGS, TMJOIN, TMRESUME", XAException.XAER_PROTO);
if (flags == XAResource.TMJOIN)
throw new FBXAException("Joining two transactions is not supported", XAException.XAER_RMFAIL);
try {
// reset the transaction parameters for the managed scenario
setTransactionIsolation(mcf.getDefaultTransactionIsolation());
internalStart(id, flags);
mcf.notifyStart(this, id);
inDistributedTransaction = true;
// This will reset the managed environment of the associated connections and set the transaction coordinator to managed
// TODO This is a bit of a hack; need to find a better way; this doesn't work with connectionSharing = true
setManagedEnvironment(isManagedEnvironment());
} catch (GDSException ge) {
throw new FBXAException(ge.getXAErrorCode());
} catch(ResourceException ex) {
throw new FBXAException(XAException.XAER_RMERR, ex);
}
}
/**
* Perform the internal processing to start associate a JDBC connection with
* a global transaction.
*
* @see #start(Xid, int)
* @param id
* A global transaction identifier to be associated with the
* resource
* @param flags
* One of TMNOFLAGS, TMJOIN, or TMRESUME
* @throws XAException If the transaction is already started, or this connection cannot participate in the distributed transaction
* @throws GDSException
*/
public void internalStart(Xid id, int flags) throws XAException, GDSException {
if (log != null) log.trace("start called: " + id);
if (getGDSHelper().getCurrentTrHandle() != null)
throw new FBXAException("Transaction already started", XAException.XAER_PROTO);
findIscTrHandle(id, flags);
}
// FB public methods. Could be package if packages reorganized.
/**
* Close this connection with regards to a wrapping
* AbstractConnection
.
*
* @param c
* The AbstractConnection
that is being closed
*/
public void close(AbstractConnection c) {
c.setManagedConnection(null);
connectionHandles.remove(c);
ConnectionEvent ce = new ConnectionEvent(this,
ConnectionEvent.CONNECTION_CLOSED, null);
ce.setConnectionHandle(c);
notify(connectionClosedNotifier, ce);
}
/**
* Get information about the current connection parameters.
*
* @return instance of {@link FBConnectionRequestInfo}.
*/
public FBConnectionRequestInfo getConnectionRequestInfo() {
return cri;
}
public TransactionParameterBuffer getTransactionParameters() {
return tpb.getTransactionParameterBuffer();
}
public void setTransactionParameters(TransactionParameterBuffer transactionParameters) {
tpb.setTransactionParameterBuffer(transactionParameters);
}
public TransactionParameterBuffer getTransactionParameters(int isolation) {
return mcf.getTransactionParameters(isolation);
}
public void setTransactionParameters(int isolation, TransactionParameterBuffer transactionParams) {
mcf.setTransactionParameters(isolation, transactionParams);
}
// --------------------------------------------------------------------
// package visibility
// --------------------------------------------------------------------
private void findIscTrHandle(Xid xid, int flags) throws GDSException, XAException {
// FIXME return old tr handle if it is still valid before proceeding
getGDSHelper().setCurrentTrHandle(null);
if (flags == XAResource.TMRESUME) {
AbstractIscTrHandle trHandle = (AbstractIscTrHandle) xidMap.get(xid);
if (trHandle == null) {
throw new FBXAException(
"You are trying to resume a transaction that is not attached to this XAResource",
XAException.XAER_INVAL);
}
getGDSHelper().setCurrentTrHandle(trHandle);
return;
}
Iterator it = xidMap.keySet().iterator();
while (it.hasNext()) {
Xid knownXid = (Xid) it.next();
boolean sameFormatId = knownXid.getFormatId() == xid.getFormatId();
boolean sameGtrid = Arrays.equals(knownXid.getGlobalTransactionId(), xid.getGlobalTransactionId());
boolean sameBqual = Arrays.equals(knownXid.getBranchQualifier(), xid.getBranchQualifier());
if (sameFormatId && sameGtrid && sameBqual)
throw new FBXAException(
"A transaction with the same XID has already been started",
XAException.XAER_DUPID);
}
// new xid for us
AbstractIscTrHandle trHandle = getGDSHelper().startTransaction(tpb.getTransactionParameterBuffer());
xidMap.put(xid, trHandle);
}
void notify(CELNotifier notifier, ConnectionEvent ce) {
if (connectionEventListeners.size() == 0) { return; }
if (connectionEventListeners.size() == 1) {
ConnectionEventListener cel = (ConnectionEventListener) connectionEventListeners
.get(0);
notifier.notify(cel, ce);
return;
} // end of if ()
ArrayList cels = (ArrayList) connectionEventListeners.clone();
for (Iterator i = cels.iterator(); i.hasNext();) {
notifier.notify((ConnectionEventListener) i.next(), ce);
} // end of for ()
}
interface CELNotifier {
void notify(ConnectionEventListener cel, ConnectionEvent ce);
}
static final CELNotifier connectionClosedNotifier = new CELNotifier() {
public void notify(ConnectionEventListener cel, ConnectionEvent ce) {
cel.connectionClosed(ce);
}
};
static final CELNotifier connectionErrorOccurredNotifier = new CELNotifier() {
public void notify(ConnectionEventListener cel, ConnectionEvent ce) {
cel.connectionErrorOccurred(ce);
}
};
static final CELNotifier localTransactionStartedNotifier = new CELNotifier() {
public void notify(ConnectionEventListener cel, ConnectionEvent ce) {
cel.localTransactionStarted(ce);
}
};
static final CELNotifier localTransactionCommittedNotifier = new CELNotifier() {
public void notify(ConnectionEventListener cel, ConnectionEvent ce) {
cel.localTransactionCommitted(ce);
}
};
static final CELNotifier localTransactionRolledbackNotifier = new CELNotifier() {
public void notify(ConnectionEventListener cel, ConnectionEvent ce) {
cel.localTransactionRolledback(ce);
}
};
boolean matches(Subject subj, ConnectionRequestInfo cri) {
if (cri == null) {
return true;
}
if (!(cri instanceof FBConnectionRequestInfo))
return false;
try {
return this.cri.equals(getCombinedConnectionRequestInfo(subj, (FBConnectionRequestInfo)cri));
} catch (ResourceException re) {
return false;
}
}
/**
* Get the transaction isolation level of this connection. The level is one
* of the static final fields of java.sql.Connection
(i.e.
* TRANSACTION_READ_COMMITTED
,
* TRANSACTION_READ_UNCOMMITTED
,
* TRANSACTION_REPEATABLE_READ
,
* TRANSACTION_SERIALIZABLE
.
*
* @see java.sql.Connection
* @see #setTransactionIsolation(int)
* @return Value representing a transaction isolation level defined in
* {@link java.sql.Connection}.
* @throws ResourceException
* If the transaction level cannot be retrieved
*/
public int getTransactionIsolation() throws ResourceException {
return transactionIsolation;
}
/**
* Set the transaction level for this connection. The level is one of the
* static final fields of java.sql.Connection
(i.e.
* TRANSACTION_READ_COMMITTED
,
* TRANSACTION_READ_UNCOMMITTED
,
* TRANSACTION_REPEATABLE_READ
,
* TRANSACTION_SERIALIZABLE
.
*
* @see java.sql.Connection
* @see #getTransactionIsolation()
* @param isolation
* Value representing a transaction isolation level defined in
* {@link java.sql.Connection}.
* @throws ResourceException
* If the transaction level cannot be retrieved
*/
public void setTransactionIsolation(int isolation) throws ResourceException {
transactionIsolation = isolation;
tpb = mcf.getTpb(isolation);
}
/**
* Get the managed connection factory that created this managed connection.
*
* @return instance of {@link ManagedConnectionFactory}.
*/
public ManagedConnectionFactory getManagedConnectionFactory() {
return mcf;
}
/**
* Set whether this connection is to be readonly
*
* @param readOnly
* If true
, the connection will be set read-only,
* otherwise it will be writable
*/
public void setReadOnly(boolean readOnly) {
tpb.setReadOnly(readOnly);
}
/**
* Retrieve whether this connection is readonly.
*
* @return true
if this connection is readonly,
* false
otherwise
*/
public boolean isReadOnly() {
return tpb.isReadOnly();
}
}