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.security.AccessController;
import java.security.PrivilegedAction;
import java.sql.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
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.DatabaseParameterBufferExtension;
import org.firebirdsql.gds.impl.DbAttachInfo;
import org.firebirdsql.gds.impl.GDSHelper;
import org.firebirdsql.gds.impl.jni.EmbeddedGDSFactoryPlugin;
import org.firebirdsql.gds.impl.jni.LocalGDSFactoryPlugin;
import org.firebirdsql.gds.ng.*;
import org.firebirdsql.gds.ng.fields.RowValue;
import org.firebirdsql.gds.ng.listeners.DefaultDatabaseListener;
import org.firebirdsql.gds.ng.listeners.DefaultStatementListener;
import org.firebirdsql.gds.ng.listeners.ExceptionListener;
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
* @author Mark Rotteveel
* @version 1.0
*/
public class FBManagedConnection implements ManagedConnection, XAResource, ExceptionListener, Synchronizable {
public static final String WARNING_NO_CHARSET = "WARNING: No connection character set specified (property lc_ctype, encoding, charSet or localEncoding), defaulting to character set ";
public static final String ERROR_NO_CHARSET = "Connection rejected: No connection character set specified (property lc_ctype, encoding, charSet or localEncoding). "
+ "Please specify a connection character set (eg property charSet=utf-8) or consult the Jaybird documentation for more information.";
private static final Logger log = LoggerFactory.getLogger(FBManagedConnection.class);
private static final String DEFAULT_CONNECTION_ENCODING_PROPERTY = "org.firebirdsql.jdbc.defaultConnectionEncoding";
private static final String REQUIRE_CONNECTION_ENCODING_PROPERTY = "org.firebirdsql.jdbc.requireConnectionEncoding";
private final FBManagedConnectionFactory mcf;
private final List connectionEventListeners = new CopyOnWriteArrayList<>();
// TODO Review synchronization of connectionHandles (especially in blocks like in disassociateConnections, setConnectionSharing etc)
private final List connectionHandles = Collections.synchronizedList(new ArrayList());
// This is a bit of hack to be able to get attach warnings into the FBConnection that is created later.
private SQLWarning unnotifiedWarnings;
private int timeout = 0;
private final Map xidMap = new ConcurrentHashMap<>();
private GDSHelper gdsHelper;
private final FbDatabase database;
private final Object syncObject;
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.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 {
DatabaseParameterBuffer dpb = this.cri.getDpb();
if (dpb.getArgumentAsString(DatabaseParameterBuffer.LC_CTYPE) == null
&& dpb.getArgumentAsString(DatabaseParameterBufferExtension.LOCAL_ENCODING) == null) {
String defaultEncoding = getDefaultConnectionEncoding();
if (defaultEncoding == null) {
throw new SQLNonTransientConnectionException(ERROR_NO_CHARSET,
SQLStateConstants.SQL_STATE_CONNECTION_ERROR) ;
}
dpb.addArgument(DatabaseParameterBuffer.LC_CTYPE, defaultEncoding);
String warningMessage = WARNING_NO_CHARSET + defaultEncoding;
log.warn(warningMessage);
notifyWarning(new SQLWarning(warningMessage));
}
if (!dpb.hasArgument(DatabaseParameterBuffer.CONNECT_TIMEOUT) && DriverManager.getLoginTimeout() > 0) {
dpb.addArgument(DatabaseParameterBuffer.CONNECT_TIMEOUT, DriverManager.getLoginTimeout());
}
final FbConnectionProperties connectionProperties = new FbConnectionProperties();
connectionProperties.fromDpb(dpb);
// TODO Move this logic to the GDSType or database factory?
final String gdsTypeName = mcf.getGDSType().toString();
if (!(EmbeddedGDSFactoryPlugin.EMBEDDED_TYPE_NAME.equals(gdsTypeName)
|| LocalGDSFactoryPlugin.LOCAL_TYPE_NAME.equals(gdsTypeName))) {
final DbAttachInfo dbAttachInfo = DbAttachInfo.parseConnectString(mcf.getDatabase());
connectionProperties.setServerName(dbAttachInfo.getServer());
connectionProperties.setPortNumber(dbAttachInfo.getPort());
connectionProperties.setDatabaseName(dbAttachInfo.getFileName());
} else {
connectionProperties.setDatabaseName(mcf.getDatabase());
}
database = mcf.getDatabaseFactory().connect(connectionProperties);
database.addDatabaseListener(new MCDatabaseListener());
database.addExceptionListener(this);
database.attach();
syncObject = database.getSynchronizationObject();
gdsHelper = new GDSHelper(database);
} catch(SQLException ex) {
throw new FBResourceException(ex);
}
}
@Override
public void errorOccurred(Object source, SQLException ex) {
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
// ManagedConnectionFactory, option A.
for (Object cred : subject.getPrivateCredentials()) {
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 SQLException If this connection has no GDSHelper
*/
public GDSHelper getGDSHelper() throws SQLException {
if (gdsHelper == null)
// TODO Right error code?
throw new FbExceptionBuilder().exception(ISCConstants.isc_req_no_trans).toSQLException();
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 (FBConnection connection : connectionHandles) {
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 FBConnection} 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 alternative logging
}
/**
* 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 alternative logging
}
/**
* 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 {
final FBConnection abstractConnection = (FBConnection) connection;
abstractConnection.setManagedConnection(this);
connectionHandles.add(abstractConnection);
} 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();
try {
getGDSHelper().setCurrentTransaction(null);
} catch (SQLException e) {
throw new FBResourceException(e);
}
// reset the TPB from the previous transaction.
this.tpb = mcf.getDefaultTpb();
this.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 (FBConnection connection : connectionHandleCopy) {
try {
connection.close();
} catch(SQLException sqlex) {
chain.append(sqlex);
}
}
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();
FBConnection c = mcf.newConnection(this);
try {
if (unnotifiedWarnings != null) {
c.addWarning(unnotifiedWarnings);
unnotifiedWarnings = null;
}
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 (inTransaction())
throw new javax.resource.spi.IllegalStateException(
"Can't destroy managed connection with active transaction");
try {
gdsHelper.detachDatabase();
} catch (SQLException 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() {
log.debug("XAResource requested from FBManagedConnection");
return this;
}
// --------------------------------------------------------------
// XAResource implementation
// --------------------------------------------------------------
// TODO validate correctness of state set
private static final Set XID_ACTIVE_STATE = Collections.unmodifiableSet(EnumSet.of(TransactionState.ACTIVE, TransactionState.PREPARED, TransactionState.PREPARING));
boolean isXidActive(Xid xid) {
FbTransaction transaction = xidMap.get(xid);
return transaction != null && XID_ACTIVE_STATE.contains(transaction.getState());
}
/**
* 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 XAException
* if an error occurs
*/
void internalCommit(Xid xid, boolean onePhase) throws XAException {
if (log.isTraceEnabled()) log.trace("Commit called: " + xid);
FbTransaction committingTr = 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);
try {
if (committingTr == getGDSHelper().getCurrentTransaction())
throw new FBXAException("Commit called with non-ended xid", XAException.XAER_PROTO);
committingTr.commit();
} catch (SQLException ge) {
if (gdsHelper != null) {
try {
committingTr.rollback();
} catch (SQLException ge2) {
log.debug("Exception rolling back failed tx: ", ge2);
}
} else {
log.warn("Unable to rollback failed tx, connection closed or lost");
}
throw new FBXAException(ge.getMessage(), XAException.XAER_RMERR, ge);
} finally {
xidMap.remove(xid);
preparedXid.remove(xid);
}
}
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);
try {
internalEnd(id, flags);
} catch (SQLException e) {
throw new FBXAException(XAException.XAER_RMERR, e);
}
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, ex);
}
}
/**
* 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, SQLException {
if (log.isDebugEnabled()) log.debug("End called: " + xid);
FbTransaction endingTr = xidMap.get(xid);
if (endingTr == null)
throw new FBXAException("Unrecognized transaction", XAException.XAER_NOTA);
if (flags == XAResource.TMFAIL) {
try {
endingTr.rollback();
getGDSHelper().setCurrentTransaction(null);
} catch (SQLException ex) {
throw new FBXAException("can't rollback transaction", XAException.XAER_RMFAIL, ex);
}
}
else if (flags == XAResource.TMSUCCESS) {
if (gdsHelper != null && endingTr == gdsHelper.getCurrentTransaction())
gdsHelper.setCurrentTransaction(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 (gdsHelper != null && endingTr == gdsHelper.getCurrentTransaction())
gdsHelper.setCurrentTransaction(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
// TODO: Is there a reason why this piece of code can't use the JDBC Statement class?
FbTransaction trHandle2 = database.startTransaction(tpb.getTransactionParameterBuffer());
FbStatement stmtHandle2 = database.createStatement(trHandle2);
GDSHelper gdsHelper2 = new GDSHelper(database);
gdsHelper2.setCurrentTransaction(trHandle2);
stmtHandle2.prepare(FORGET_FIND_QUERY);
DataProvider dataProvider0 = new DataProvider(0);
stmtHandle2.addStatementListener(dataProvider0);
DataProvider dataProvider1 = new DataProvider(1);
stmtHandle2.addStatementListener(dataProvider1);
stmtHandle2.execute(RowValue.EMPTY_ROW_VALUE);
stmtHandle2.fetchRows(10);
FBField field0 = FBField.createField(stmtHandle2.getFieldDescriptor().getFieldDescriptor(0), dataProvider0, gdsHelper2, false);
FBField field1 = FBField.createField(stmtHandle2.getFieldDescriptor().getFieldDescriptor(1), dataProvider1, gdsHelper2, false);
int row = 0;
while(row < dataProvider0.getRowCount()) {
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) {
log.warn("incorrect XID format in RDB$TRANSACTIONS where RDB$TRANSACTION_ID=" + inLimboTxId, ex);
}
row++;
}
stmtHandle2.close();
trHandle2.commit();
} catch (SQLException | ResourceException ex) {
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
FbTransaction trHandle2 = database.startTransaction(tpb.getTransactionParameterBuffer());
FbStatement stmtHandle2 = database.createStatement(trHandle2);
GDSHelper gdsHelper2 = new GDSHelper(database);
gdsHelper2.setCurrentTransaction(trHandle2);
stmtHandle2.prepare(FORGET_DELETE_QUERY + inLimboId);
stmtHandle2.execute(RowValue.EMPTY_ROW_VALUE);
stmtHandle2.close();
trHandle2.commit();
} 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
&& database == ((FBManagedConnection) res).database;
}
/**
* 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 {
if (log.isTraceEnabled()) log.trace("prepare called: " + xid);
FbTransaction committingTr = xidMap.get(xid);
if (committingTr == null)
throw new FBXAException("Prepare called with unknown transaction", XAException.XAER_NOTA);
try {
if (committingTr == getGDSHelper().getCurrentTransaction())
throw new FBXAException("Prepare called with non-ended xid", XAException.XAER_PROTO);
FBXid fbxid;
if (xid instanceof FBXid) {
fbxid = (FBXid) xid;
} else {
fbxid = new FBXid(xid);
}
byte[] message = fbxid.toBytes();
committingTr.prepare(message);
} catch (SQLException ge) {
try {
if (gdsHelper != null) {
committingTr.rollback();
} else {
log.warn("Unable to rollback failed tx, connection closed or lost");
}
} catch (SQLException ge2) {
log.debug("Exception rolling back failed tx: ", ge2);
} finally {
xidMap.remove(xid);
}
log.warn("error in prepare", ge);
throw new FBXAException(XAException.XAER_RMERR, 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];
List xids = new ArrayList<>();
FbTransaction trHandle2 = database.startTransaction(tpb.getTransactionParameterBuffer());
FbStatement stmtHandle2 = database.createStatement(trHandle2);
GDSHelper gdsHelper2 = new GDSHelper(database);
gdsHelper2.setCurrentTransaction(trHandle2);
stmtHandle2.prepare(RECOVERY_QUERY);
DataProvider dataProvider0 = new DataProvider(0);
stmtHandle2.addStatementListener(dataProvider0);
DataProvider dataProvider1 = new DataProvider(1);
stmtHandle2.addStatementListener(dataProvider1);
stmtHandle2.execute(RowValue.EMPTY_ROW_VALUE);
stmtHandle2.fetchRows(10);
FBField field0 = FBField.createField(stmtHandle2.getFieldDescriptor().getFieldDescriptor(0), dataProvider0, gdsHelper2, false);
FBField field1 = FBField.createField(stmtHandle2.getFieldDescriptor().getFieldDescriptor(1), dataProvider1, gdsHelper2, false);
int row = 0;
while(row < dataProvider0.getRowCount()) {
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) {
log.warn("ignoring XID stored with invalid format in RDB$TRANSACTIONS for RDB$TRANSACTION_ID=" + inLimboTxId);
}
row++;
}
stmtHandle2.close();
trHandle2.commit();
return xids.toArray(new FBXid[0]);
} catch (SQLException | ResourceException e) {
throw new FBXAException("can't perform query to fetch xids", XAException.XAER_RMFAIL, e);
}
}
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 {
FbTransaction trHandle2 = database.startTransaction(tpb.getTransactionParameterBuffer());
FbStatement stmtHandle2 = database.createStatement(trHandle2);
GDSHelper gdsHelper2 = new GDSHelper(database);
gdsHelper2.setCurrentTransaction(trHandle2);
stmtHandle2.prepare(RECOVERY_QUERY_PARAMETRIZED);
DataProvider dataProvider0 = new DataProvider(0);
stmtHandle2.addStatementListener(dataProvider0);
DataProvider dataProvider1 = new DataProvider(1);
stmtHandle2.addStatementListener(dataProvider1);
final RowValue parameters = stmtHandle2.getParameterDescriptor().createDefaultFieldValues();
FBXid tempXid = new FBXid(externalXid);
parameters.getFieldValue(0).setFieldData(tempXid.toBytes());
stmtHandle2.execute(parameters);
stmtHandle2.fetchRows(1);
FBField field0 = FBField.createField(stmtHandle2.getFieldDescriptor().getFieldDescriptor(0), dataProvider0, gdsHelper2, false);
FBField field1 = FBField.createField(stmtHandle2.getFieldDescriptor().getFieldDescriptor(1), dataProvider1, gdsHelper2, false);
FBXid xid = null;
if (dataProvider0.getRowCount() > 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) {
log.warn("ignoring XID stored with invalid format in RDB$TRANSACTIONS for RDB$TRANSACTION_ID=" + inLimboTxId);
}
}
stmtHandle2.close();
trHandle2.commit();
return xid;
} catch (SQLException | ResourceException e) {
throw new FBXAException("can't perform query to fetch xids", XAException.XAER_RMFAIL, e);
}
}
@Override
public final Object getSynchronizationObject() {
return syncObject;
}
private static class DataProvider extends DefaultStatementListener implements FieldDataProvider {
private final List rows = new ArrayList<>();
private final int fieldPos;
private int row;
private DataProvider(int fieldPos) {
this.fieldPos = fieldPos;
}
public void setRow(int row) {
this.row = row;
}
public byte[] getFieldData() {
return rows.get(row).getFieldValue(fieldPos).getFieldData();
}
public void setFieldData(byte[] data) {
throw new UnsupportedOperationException();
}
public int getRowCount() {
return rows.size();
}
@Override
public void receivedRow(FbStatement sender, RowValue rowValue) {
rows.add(rowValue);
}
}
/**
* 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 {
if (log.isTraceEnabled()) log.trace("rollback called: " + xid);
FbTransaction committingTr = xidMap.get(xid);
if (committingTr == null) {
throw new FBXAException ("Rollback called with unknown transaction: " + xid);
}
try {
if (committingTr == getGDSHelper().getCurrentTransaction())
throw new FBXAException("Rollback called with non-ended xid", XAException.XAER_PROTO);
try {
committingTr.rollback();
} finally {
xidMap.remove(xid);
preparedXid.remove(xid);
}
} catch (SQLException ge) {
log.debug("Exception in rollback", ge);
throw new FBXAException(ge.getMessage(), XAException.XAER_RMERR, 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(), ge);
} catch (SQLException | ResourceException e) {
throw new FBXAException(XAException.XAER_RMERR, e);
}
}
/**
* 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 SQLException
*/
public void internalStart(Xid id, int flags) throws XAException, SQLException {
if (log.isTraceEnabled()) log.trace("start called: " + id);
if (getGDSHelper().getCurrentTransaction() != 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(FBConnection 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 SQLException, XAException {
// FIXME return old tr handle if it is still valid before proceeding
getGDSHelper().setCurrentTransaction(null);
if (flags == XAResource.TMRESUME) {
FbTransaction trHandle = 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().setCurrentTransaction(trHandle);
return;
}
for (Xid knownXid : xidMap.keySet()) {
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
try {
FbTransaction transaction = getGDSHelper().startTransaction(tpb.getTransactionParameterBuffer());
xidMap.put(xid, transaction);
} catch (SQLException e) {
throw new FBXAException(e.getMessage(), XAException.XAER_RMERR, e);
}
}
void notify(CELNotifier notifier, ConnectionEvent ce) {
for (ConnectionEventListener cel : connectionEventListeners) {
notifier.notify(cel, ce);
}
}
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, 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();
}
private void notifyWarning(SQLWarning warning) {
// Note: minor chance of a race condition here, but we take the chance.
if (connectionHandles.isEmpty()) {
if (unnotifiedWarnings == null) {
unnotifiedWarnings = warning;
} else {
unnotifiedWarnings.setNextWarning(warning);
}
}
for (FBConnection connection : new ArrayList<>(connectionHandles)) {
connection.addWarning(warning);
}
}
private static String getDefaultConnectionEncoding() {
try {
String defaultConnectionEncoding = getSystemPropertyPrivileged(DEFAULT_CONNECTION_ENCODING_PROPERTY);
if (defaultConnectionEncoding == null) {
if (Boolean.valueOf(getSystemPropertyPrivileged(REQUIRE_CONNECTION_ENCODING_PROPERTY))) {
return null;
}
return "NONE";
}
return defaultConnectionEncoding;
} catch (Exception e) {
log.error("Exception obtaining default connection encoding", e);
}
return "NONE";
}
private static String getSystemPropertyPrivileged(final String propertyName) {
return AccessController.doPrivileged(new PrivilegedAction() {
public String run() {
return System.getProperty(propertyName);
}
});
}
/**
* DatabaseListener implementation for use by this ManagedConnection.
*/
private class MCDatabaseListener extends DefaultDatabaseListener {
@Override
public void warningReceived(FbDatabase database, SQLWarning warning) {
if (database != FBManagedConnection.this.database) {
database.removeDatabaseListener(this);
return;
}
notifyWarning(warning);
}
}
}