org.firebirdsql.gds.ng.AbstractFbStatement Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jaybird Show documentation
Show all versions of jaybird Show documentation
JDBC Driver for the Firebird RDBMS
/*
* 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.gds.ng;
import org.firebirdsql.gds.ISCConstants;
import org.firebirdsql.gds.JaybirdErrorCodes;
import org.firebirdsql.gds.ng.fields.RowDescriptor;
import org.firebirdsql.gds.ng.fields.RowValue;
import org.firebirdsql.gds.ng.listeners.*;
import org.firebirdsql.jdbc.SQLStateConstants;
import org.firebirdsql.logging.Logger;
import org.firebirdsql.logging.LoggerFactory;
import java.sql.SQLException;
import java.sql.SQLNonTransientException;
import java.sql.SQLTransientException;
import java.sql.SQLWarning;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;
/**
* @author Mark Rotteveel
* @since 3.0
*/
public abstract class AbstractFbStatement implements FbStatement {
private static final long MAX_STATEMENT_TIMEOUT = 0xFF_FF_FF_FFL;
/**
* Set of states that will be reset to {@link StatementState#PREPARED} on transaction change
*/
private static final Set RESET_TO_PREPARED = Collections.unmodifiableSet(
EnumSet.of(StatementState.EXECUTING, StatementState.CURSOR_OPEN));
private static final Logger log = LoggerFactory.getLogger(AbstractFbStatement.class);
private final Object syncObject;
private final WarningMessageCallback warningCallback = new WarningMessageCallback() {
@Override
public void processWarning(SQLWarning warning) {
statementListenerDispatcher.warningReceived(AbstractFbStatement.this, warning);
}
};
protected final StatementListenerDispatcher statementListenerDispatcher = new StatementListenerDispatcher();
protected final ExceptionListenerDispatcher exceptionListenerDispatcher = new ExceptionListenerDispatcher(this);
private volatile boolean allRowsFetched = false;
private volatile StatementState state = StatementState.NEW;
private volatile StatementType type = StatementType.NONE;
private volatile RowDescriptor parameterDescriptor;
private volatile RowDescriptor fieldDescriptor;
private volatile FbTransaction transaction;
private long timeout;
private final TransactionListener transactionListener = new TransactionListener() {
@Override
public void transactionStateChanged(FbTransaction transaction, TransactionState newState,
TransactionState previousState) {
if (getTransaction() != transaction) {
transaction.removeTransactionListener(this);
return;
}
switch (newState) {
case COMMITTED:
case ROLLED_BACK:
synchronized (getSynchronizationObject()) {
try {
if (RESET_TO_PREPARED.contains(getState())) {
// Cursor has been closed due to commit, rollback, etc, back to prepared state
try {
switchState(StatementState.PREPARED);
} catch (SQLException e) {
throw new IllegalStateException("Received an SQLException when none was expected", e);
}
reset(false);
}
} finally {
transaction.removeTransactionListener(this);
try {
setTransaction(null);
} catch (SQLException e) {
//noinspection ThrowFromFinallyBlock
throw new IllegalStateException("Received an SQLException when none was expected", e);
}
}
}
}
}
};
protected AbstractFbStatement(Object syncObject) {
this.syncObject = syncObject;
exceptionListenerDispatcher.addListener(new StatementCancelledListener());
}
/**
* Gets the {@link TransactionListener} instance for this statement.
*
* This method should only be called by this object itself. Subclasses may provide their own transaction listener, but
* the instance returned by this method should be the same for the lifetime of this {@link FbStatement}.
*
*
* @return The transaction listener instance for this statement.
*/
protected final TransactionListener getTransactionListener() {
return transactionListener;
}
protected final WarningMessageCallback getStatementWarningCallback() {
return warningCallback;
}
/**
* Get synchronization object.
*
* @return object, cannot be null
.
*/
protected final Object getSynchronizationObject() {
return syncObject;
}
@Override
public void close() throws SQLException {
if (getState() == StatementState.CLOSED) return;
try {
synchronized (getSynchronizationObject()) {
// TODO do additional checks (see also old implementation and .NET)
try {
final StatementState currentState = getState();
forceState(StatementState.CLOSING);
if (currentState != StatementState.NEW) {
free(ISCConstants.DSQL_drop);
}
} finally {
forceState(StatementState.CLOSED);
setType(StatementType.NONE);
statementListenerDispatcher.shutdown();
setTransaction(null);
}
}
} catch (SQLException e) {
exceptionListenerDispatcher.errorOccurred(e);
throw e;
} finally {
exceptionListenerDispatcher.shutdown();
}
}
@Override
public final void closeCursor() throws SQLException {
closeCursor(false);
}
@Override
public final void closeCursor(boolean transactionEnd) throws SQLException {
try {
synchronized (getSynchronizationObject()) {
if (!getState().isCursorOpen()) return;
try {
if (!transactionEnd && getType().isTypeWithCursor()) {
free(ISCConstants.DSQL_close);
}
// TODO Any statement types that cannot be prepared and would need to go to ALLOCATED?
switchState(StatementState.PREPARED);
} catch (SQLException e) {
// TODO Close in case of exception?
switchState(StatementState.ERROR);
throw e;
}
}
} catch (SQLException e) {
exceptionListenerDispatcher.errorOccurred(e);
throw e;
}
}
@Override
public final void ensureClosedCursor(boolean transactionEnd) throws SQLException {
if (getState().isCursorOpen()) {
if (log.isDebugEnabled()) {
log.debug("ensureClosedCursor has to close a cursor at", new RuntimeException("debugging stacktrace"));
}
closeCursor(transactionEnd);
}
}
@Override
public StatementState getState() {
return state;
}
/**
* Sets the StatementState.
*
* @param newState
* New state
* @throws SQLException
* When the state is changed to an illegal next state
*/
protected final void switchState(final StatementState newState) throws SQLException {
synchronized (getSynchronizationObject()) {
final StatementState currentState = state;
if (currentState == newState || currentState == StatementState.CLOSED) return;
if (currentState.isValidTransition(newState)) {
state = newState;
statementListenerDispatcher.statementStateChanged(this, newState, currentState);
} else {
throw new SQLNonTransientException(String.format("Statement state %s only allows next states %s, received %s", currentState, currentState.validTransitionSet(), newState));
}
}
}
/**
* Forces the statement to the specified state without throwing an exception if this is not a valid transition.
*
* Does nothing if current state is CLOSED.
*
*
* @param newState
* New state
* @see #switchState(StatementState)
*/
private void forceState(final StatementState newState) {
synchronized (getSynchronizationObject()) {
final StatementState currentState = state;
if (currentState == newState || currentState == StatementState.CLOSED) return;
if (log.isDebugEnabled() && !currentState.isValidTransition(newState)) {
log.debug(
String.format("Forced statement transition is invalid; state %s only allows next states %s, forced to set %s",
currentState, currentState.validTransitionSet(), newState),
new IllegalStateException());
}
state = newState;
statementListenerDispatcher.statementStateChanged(this, newState, currentState);
}
}
@Override
public final StatementType getType() {
return type;
}
/**
* Sets the StatementType
*
* @param type
* New type
*/
protected void setType(StatementType type) {
this.type = type;
}
/**
* Queues row data for consumption
*
* @param rowData
* Row data
*/
protected final void queueRowData(RowValue rowData) {
statementListenerDispatcher.receivedRow(this, rowData);
}
/**
* Sets the allRowsFetched
property.
*
* When set to true all registered {@link org.firebirdsql.gds.ng.listeners.StatementListener} instances are notified
* for the {@link org.firebirdsql.gds.ng.listeners.StatementListener#allRowsFetched(FbStatement)} event.
*
*
* @param allRowsFetched
* true
: all rows fetched, false
not all rows fetched.
*/
protected final void setAllRowsFetched(boolean allRowsFetched) {
this.allRowsFetched = allRowsFetched;
if (allRowsFetched) {
statementListenerDispatcher.allRowsFetched(this);
}
}
protected final boolean isAllRowsFetched() {
return allRowsFetched;
}
/**
* Reset statement state, equivalent to calling {@link #reset(boolean)} with false
*/
protected final void reset() {
reset(false);
}
/**
* Reset statement state and clear parameter description, equivalent to calling {@link #reset(boolean)} with true
*/
protected final void resetAll() {
reset(true);
}
/**
* Resets the statement for next execution. Implementation in derived class must synchronize on {@link #getSynchronizationObject()} and
* call super.reset(resetAll)
*
* @param resetAll
* Also reset field and parameter info
*/
protected void reset(boolean resetAll) {
synchronized (getSynchronizationObject()) {
setAllRowsFetched(false);
if (resetAll) {
setParameterDescriptor(null);
setRowDescriptor(null);
setType(StatementType.NONE);
}
}
}
private static final Set PREPARE_ALLOWED_STATES = Collections.unmodifiableSet(
EnumSet.of(StatementState.NEW, StatementState.ALLOCATED, StatementState.PREPARED));
/**
* Is a call to {@link #prepare(String)} allowed for the supplied {@link StatementState}.
*
* @param state
* The statement state
* @return true
call to prepare
is allowed
*/
protected boolean isPrepareAllowed(final StatementState state) {
return PREPARE_ALLOWED_STATES.contains(state);
}
@Override
public final RowDescriptor getParameterDescriptor() {
return parameterDescriptor;
}
/**
* Sets the parameter descriptor.
*
* @param parameterDescriptor
* Parameter descriptor
*/
protected void setParameterDescriptor(RowDescriptor parameterDescriptor) {
this.parameterDescriptor = parameterDescriptor;
}
@Override
@Deprecated
public final RowDescriptor getFieldDescriptor() {
return getRowDescriptor();
}
/**
* Sets the (result set) field descriptor.
*
* @param fieldDescriptor
* Field descriptor
* @deprecated Use {@link #setRowDescriptor(RowDescriptor)} instead; will be removed in Jaybird 5
*/
@Deprecated
protected void setFieldDescriptor(RowDescriptor fieldDescriptor) {
setRowDescriptor(fieldDescriptor);
}
@Override
public final RowDescriptor getRowDescriptor() {
return fieldDescriptor;
}
/**
* Sets the (result set) row descriptor.
*
* @param rowDescriptor
* Row descriptor
*/
protected void setRowDescriptor(RowDescriptor rowDescriptor) {
this.fieldDescriptor = rowDescriptor;
}
/**
* @return The (full) statement info request items.
* @see #getParameterDescriptionInfoRequestItems()
*/
public byte[] getStatementInfoRequestItems() {
return ((AbstractFbDatabase) getDatabase()).getStatementInfoRequestItems();
}
/**
* @return The {@code isc_info_sql_describe_vars} info request items.
* @see #getStatementInfoRequestItems()
*/
public byte[] getParameterDescriptionInfoRequestItems() {
return ((AbstractFbDatabase) getDatabase()).getParameterDescriptionInfoRequestItems();
}
/**
* Request statement info.
*
* @param requestItems
* Array of info items to request
* @param bufferLength
* Response buffer length to use
* @param infoProcessor
* Implementation of {@link InfoProcessor} to transform
* the info response
* @return Transformed info response of type T
* @throws SQLException
* For errors retrieving or transforming the response.
*/
@Override
public final T getSqlInfo(final byte[] requestItems, final int bufferLength,
final InfoProcessor infoProcessor) throws SQLException {
final byte[] sqlInfo = getSqlInfo(requestItems, bufferLength);
try {
return infoProcessor.process(sqlInfo);
} catch (SQLException e) {
exceptionListenerDispatcher.errorOccurred(e);
throw e;
}
}
@Override
public final String getExecutionPlan() throws SQLException {
final ExecutionPlanProcessor processor = createExecutionPlanProcessor();
return getSqlInfo(processor.getDescribePlanInfoItems(), getDefaultSqlInfoSize(), processor);
}
@Override
public final String getExplainedExecutionPlan() throws SQLException {
synchronized (getSynchronizationObject()) {
checkExplainedExecutionPlanSupport();
final ExecutionPlanProcessor processor = createExecutionPlanProcessor();
return getSqlInfo(processor.getDescribeExplainedPlanInfoItems(), getDefaultSqlInfoSize(), processor);
}
}
private void checkExplainedExecutionPlanSupport() throws SQLException {
try {
checkStatementValid();
if (!getDatabase().getServerVersion().isEqualOrAbove(3, 0)) {
throw FbExceptionBuilder.forException(JaybirdErrorCodes.jb_explainedExecutionPlanNotSupported)
.toFlatSQLException();
}
} catch (SQLException e) {
exceptionListenerDispatcher.errorOccurred(e);
throw e;
}
}
/**
* @return New instance of {@link ExecutionPlanProcessor} (or subclass) for this statement.
*/
protected ExecutionPlanProcessor createExecutionPlanProcessor() {
return new ExecutionPlanProcessor(this);
}
@Override
public SqlCountHolder getSqlCounts() throws SQLException {
try {
checkStatementValid();
if (getState() == StatementState.CURSOR_OPEN && !isAllRowsFetched()) {
// We disallow fetching count when we haven't fetched all rows yet.
// TODO SQLState
throw new SQLNonTransientException("Cursor still open, fetch all rows or close cursor before fetching SQL counts");
}
} catch (SQLException e) {
exceptionListenerDispatcher.errorOccurred(e);
throw e;
}
final SqlCountProcessor countProcessor = createSqlCountProcessor();
// NOTE: implementation of SqlCountProcessor assumes the specified size is sufficient (actual requirement is 49 bytes max) and does not handle truncation
final SqlCountHolder sqlCounts = getSqlInfo(countProcessor.getRecordCountInfoItems(), 64, countProcessor);
statementListenerDispatcher.sqlCounts(this, sqlCounts);
return sqlCounts;
}
/**
* @return New instance of {@link SqlCountProcessor} (or subclass) for this statement.
*/
protected SqlCountProcessor createSqlCountProcessor() {
return new SqlCountProcessor();
}
/**
* Frees the currently allocated statement (either close the cursor with {@link ISCConstants#DSQL_close} or drop the statement
* handle using {@link ISCConstants#DSQL_drop}.
*
* @param option
* Free option
* @throws SQLException
*/
protected abstract void free(int option) throws SQLException;
@Override
public final void validateParameters(final RowValue parameters) throws SQLException {
final RowDescriptor parameterDescriptor = getParameterDescriptor();
final int expectedSize = parameterDescriptor != null ? parameterDescriptor.getCount() : 0;
final int actualSize = parameters.getCount();
// TODO Externalize sqlstates and error messages
if (actualSize != expectedSize) {
// TODO use HY021 (inconsistent descriptor information) instead?
throw new SQLNonTransientException(String.format("Invalid number of parameters, expected %d, got %d",
expectedSize, actualSize), "07008"); // invalid descriptor count
}
for (int fieldIndex = 0; fieldIndex < actualSize; fieldIndex++) {
if (!parameters.isInitialized(fieldIndex)) {
// Communicating 1-based index, so it doesn't cause confusion when JDBC user sees this.
// TODO use HY000 (dynamic parameter value needed) instead?
throw new SQLTransientException(String.format("Parameter with index %d was not set",
fieldIndex + 1), "0700C"); // undefined DATA value
}
}
}
@Override
public final void addStatementListener(StatementListener statementListener) {
if (getState() == StatementState.CLOSED) return;
statementListenerDispatcher.addListener(statementListener);
}
@Override
public final void removeStatementListener(StatementListener statementListener) {
statementListenerDispatcher.removeListener(statementListener);
}
@Override
public final void addExceptionListener(ExceptionListener listener) {
exceptionListenerDispatcher.addListener(listener);
}
@Override
public final void removeExceptionListener(ExceptionListener listener) {
exceptionListenerDispatcher.removeListener(listener);
}
/**
* Checks if this statement is not in {@link StatementState#CLOSED}, {@link StatementState#CLOSING},
* {@link StatementState#NEW} or {@link StatementState#ERROR}, and throws an SQLException
if it is.
*
* @throws SQLException
* When this statement is closed or in error state.
*/
protected final void checkStatementValid() throws SQLException {
switch (getState()) {
case NEW:
// TODO Externalize sqlstate
// TODO See if there is a firebird error code matching this (isc_cursor_not_open is not exactly the same)
throw new SQLNonTransientException("Statement not yet allocated", "24000");
case CLOSING:
case CLOSED:
// TODO Externalize sqlstate
// TODO See if there is a firebird error code matching this (isc_cursor_not_open is not exactly the same)
throw new SQLNonTransientException("Statement closed", "24000");
case ERROR:
// TODO SQLState?
// TODO See if there is a firebird error code matching this
throw new SQLNonTransientException("Statement is in error state and needs to be closed");
default:
// Valid state, continue
break;
}
}
/**
* Performs the same check as {@link #checkStatementValid()}, but considers {@code ignoreState} as valid.
*
* @param ignoreState
* The invalid state (see {@link #checkStatementValid()} to ignore
* @throws SQLException
* When this statement is closed or in error state.
*/
protected final void checkStatementValid(StatementState ignoreState) throws SQLException {
if (ignoreState == getState()) {
return;
}
checkStatementValid();
}
@Override
protected void finalize() throws Throwable {
try {
if (getState() != StatementState.CLOSED) close();
} finally {
super.finalize();
}
}
@Override
public FbTransaction getTransaction() {
return transaction;
}
/**
* Method to decide if a transaction implementation class is valid for the statement implementation.
*
* Eg a {@link org.firebirdsql.gds.ng.wire.version10.V10Statement} will only work with an
* {@link org.firebirdsql.gds.ng.wire.FbWireTransaction} implementation.
*
*
* @param transactionClass
* Class of the transaction
* @return true
when the transaction class is valid for the statement implementation.
*/
protected abstract boolean isValidTransactionClass(Class extends FbTransaction> transactionClass);
@Override
public final void setTransaction(final FbTransaction newTransaction) throws SQLException {
// TODO Should we really notify the exception listener for errors here?
try {
if (newTransaction == null || isValidTransactionClass(newTransaction.getClass())) {
// TODO Is there a statement or transaction state where we should not be switching transactions?
// Probably an error to switch when newTransaction is not null and current state is ERROR, CURSOR_OPEN, EXECUTING, CLOSING or CLOSED
synchronized (getSynchronizationObject()) {
if (newTransaction == transaction) return;
if (transaction != null) {
transaction.removeTransactionListener(getTransactionListener());
}
transaction = newTransaction;
if (newTransaction != null) {
newTransaction.addTransactionListener(getTransactionListener());
}
}
} else {
throw new SQLNonTransientException(String.format("Invalid transaction handle type, got \"%s\"",
newTransaction.getClass().getName()), SQLStateConstants.SQL_STATE_GENERAL_ERROR);
}
} catch (SQLNonTransientException e) {
exceptionListenerDispatcher.errorOccurred(e);
throw e;
}
}
@Override
public void setTimeout(long statementTimeout) throws SQLException {
try {
if (statementTimeout < 0) {
throw new FbExceptionBuilder()
.nonTransientException(JaybirdErrorCodes.jb_invalidTimeout)
.toFlatSQLException();
}
synchronized (getSynchronizationObject()) {
checkStatementValid(StatementState.NEW);
this.timeout = statementTimeout;
}
} catch (SQLException e) {
exceptionListenerDispatcher.errorOccurred(e);
throw e;
}
}
@Override
public long getTimeout() throws SQLException {
synchronized (getSynchronizationObject()) {
checkStatementValid(StatementState.NEW);
return timeout;
}
}
/**
* @return The timeout value, or {@code 0} if the timeout is larger than supported
* @throws SQLException
* If the statement is invalid
*/
protected long getAllowedTimeout() throws SQLException {
long timeout = getTimeout();
if (timeout > MAX_STATEMENT_TIMEOUT) {
return 0;
}
return timeout;
}
/**
* Parse the statement info response in statementInfoResponse
. If the response is truncated, a new
* request is done using {@link #getStatementInfoRequestItems()}
*
* @param statementInfoResponse
* Statement info response
*/
protected void parseStatementInfo(final byte[] statementInfoResponse) throws SQLException {
final StatementInfoProcessor infoProcessor = new StatementInfoProcessor(this, this.getDatabase());
InfoProcessor.StatementInfo statementInfo = infoProcessor.process(statementInfoResponse);
setType(statementInfo.getStatementType());
setRowDescriptor(statementInfo.getFields());
setParameterDescriptor(statementInfo.getParameters());
}
/**
* @return {@code true} if this is a stored procedure (or other singleton result producing statement) with at least 1 output field
*/
protected final boolean hasSingletonResult() {
return getType().isTypeWithSingletonResult() && hasFields();
}
/**
* @return {@code true} if this statement has at least one output field (either singleton or result set)
*/
protected final boolean hasFields() {
RowDescriptor fieldDescriptor = getRowDescriptor();
return fieldDescriptor != null && fieldDescriptor.getCount() > 0;
}
/**
* Signals the start of an execute for this statement.
*
* @return {@code OperationCloseHandle} handle for the operation
*/
protected final OperationCloseHandle signalExecute() {
return FbDatabaseOperation.signalExecute(getDatabase());
}
/**
* Signals the start of a fetch for this statement.
*
* @return {@code OperationCloseHandle} handle for the operation
*/
protected final OperationCloseHandle signalFetch() {
return FbDatabaseOperation.signalFetch(getDatabase());
}
/**
* Listener to reset the statement state when it has been cancelled due to statement timeout.
*/
private class StatementCancelledListener implements ExceptionListener {
@Override
public void errorOccurred(Object source, SQLException ex) {
if (source != AbstractFbStatement.this) {
return;
}
switch (ex.getErrorCode()) {
case ISCConstants.isc_cfg_stmt_timeout:
case ISCConstants.isc_att_stmt_timeout:
case ISCConstants.isc_req_stmt_timeout:
// Close cursor so statement can be reused
try {
closeCursor();
} catch (SQLException e) {
log.error("Unable to close cursor after statement timeout", e);
}
break;
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy