org.firebirdsql.jdbc.FBTpbMapper 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
The newest version!
/*
* Firebird Open Source 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.jdbc;
import org.firebirdsql.gds.TransactionParameterBuffer;
import org.firebirdsql.gds.impl.TransactionParameterBufferImpl;
import org.firebirdsql.jaybird.fb.constants.TpbItems;
import org.firebirdsql.jaybird.props.internal.TransactionNameMapping;
import org.firebirdsql.util.InternalApi;
import java.io.Serial;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import static org.firebirdsql.jaybird.fb.constants.TpbItems.*;
/**
* This class provides mapping capabilities between standard JDBC transaction isolation level and Firebird Transaction
* Parameters Blocks (TPBs).
*
* This class is internal API of Jaybird. Future versions may radically change, move, or make inaccessible this type.
*
*
* @author Roman Rokytskyy
*/
@InternalApi
public final class FBTpbMapper implements Serializable {
@Serial
private static final long serialVersionUID = 1690658870275668176L;
/**
* Creates a new instance with the default configuration.
*
* @return instance with the default mapper configuration
*/
public static FBTpbMapper getDefaultMapper() {
return new FBTpbMapper();
}
/**
* Dirty reads, non-repeatable reads and phantom reads are prevented. This
* level includes the prohibitions in TRANSACTION_REPEATABLE_READ and
* further prohibits the situation where one transaction reads all rows that
* satisfy a WHERE condition, a second transaction inserts a row that
* satisfies that WHERE condition, and the first transaction rereads for the
* same condition, retrieving the additional "phantom" row in the second
* read.
*/
public static final String TRANSACTION_SERIALIZABLE = TransactionNameMapping.TRANSACTION_SERIALIZABLE;
/**
* Dirty reads and non-repeatable reads are prevented; phantom reads can
* occur. This level prohibits a transaction from reading a row with
* uncommitted changes in it, and it also prohibits the situation where one
* transaction reads a row, a second transaction alters the row, and the
* first transaction rereads the row, getting different values the second
* time (a "non-repeatable read").
*/
public static final String TRANSACTION_REPEATABLE_READ = TransactionNameMapping.TRANSACTION_REPEATABLE_READ;
/**
* Dirty reads are prevented; non-repeatable reads and phantom reads can
* occur. This level only prohibits a transaction from reading a row with
* uncommitted changes in it.
*/
public static final String TRANSACTION_READ_COMMITTED = TransactionNameMapping.TRANSACTION_READ_COMMITTED;
private static final List ISOLATION_LEVEL_NAMES =
List.of(TRANSACTION_SERIALIZABLE, TRANSACTION_REPEATABLE_READ, TRANSACTION_READ_COMMITTED);
// read uncommitted actually not supported
/**
* Dirty reads, non-repeatable reads and phantom reads can occur. This level
* allows a row changed by one transaction to be read by another transaction
* before any changes in that row have been committed (a "dirty read"). If
* any of the changes are rolled back, the second transaction will have
* retrieved an invalid row. This level is not actually supported
*/
public static final String TRANSACTION_READ_UNCOMMITTED = TransactionNameMapping.TRANSACTION_READ_UNCOMMITTED;
/**
* Indicates that transactions are not supported. This level is not supported
*/
public static final String TRANSACTION_NONE = TransactionNameMapping.TRANSACTION_NONE;
/**
* Convert transaction isolation level into string.
*
* @param isolationLevel
* transaction isolation level as integer constant.
* @return corresponding string representation.
*/
public static String getTransactionIsolationName(int isolationLevel) {
return TransactionNameMapping.toIsolationLevelName(isolationLevel);
}
/**
* Convert transaction isolation level name into a corresponding constant.
*
* @param isolationName
* name of the transaction isolation.
* @return corresponding constant.
*/
public static int getTransactionIsolationLevel(String isolationName) {
return TransactionNameMapping.toIsolationLevel(isolationName);
}
// ConcurrentHashMap because changes can - potentially - be made concurrently
private final Map mapping;
private int defaultIsolationLevel = Connection.TRANSACTION_READ_COMMITTED;
/**
* Create instance of this class with the default mapping of JDBC
* transaction isolation levels to Firebird TPB.
*/
public FBTpbMapper() {
// TODO instance creation should be delegated to FbDatabase or another factory
var serializableTpb = new TransactionParameterBufferImpl();
serializableTpb.addArgument(isc_tpb_write);
serializableTpb.addArgument(isc_tpb_wait);
serializableTpb.addArgument(isc_tpb_consistency);
var repeatableReadTpb = new TransactionParameterBufferImpl();
repeatableReadTpb.addArgument(isc_tpb_write);
repeatableReadTpb.addArgument(isc_tpb_wait);
repeatableReadTpb.addArgument(isc_tpb_concurrency);
var readCommittedTpb = new TransactionParameterBufferImpl();
readCommittedTpb.addArgument(isc_tpb_write);
readCommittedTpb.addArgument(isc_tpb_wait);
readCommittedTpb.addArgument(isc_tpb_read_committed);
readCommittedTpb.addArgument(isc_tpb_rec_version);
var mapping = new ConcurrentHashMap(3);
mapping.put(Connection.TRANSACTION_SERIALIZABLE, serializableTpb);
mapping.put(Connection.TRANSACTION_REPEATABLE_READ, repeatableReadTpb);
mapping.put(Connection.TRANSACTION_READ_COMMITTED, readCommittedTpb);
this.mapping = mapping;
}
/**
* Create instance of this class for the specified string mapping.
*
* @param stringMapping mapping of JDBC transaction isolation to Firebird
* mapping. Keys and values of this map must be strings. Keys can have
* the following values:
*
* - {@code "TRANSACTION_SERIALIZABLE"}
*
- {@code "TRANSACTION_REPEATABLE_READ"}
*
- {@code "TRANSACTION_READ_COMMITTED"}
*
- {@code "TRANSACTION_READ_UNCOMMITTED"}
*
* Values are specified as comma-separated list of following keywords:
*
* - {@code "isc_tpb_consistency"}
*
- {@code "isc_tpb_concurrency"}
*
- {@code "isc_tpb_read_committed"}
*
- {@code "isc_tpb_rec_version"}
*
- {@code "isc_tpb_no_rec_version"}
*
- {@code "isc_tpb_wait"}
*
- {@code "isc_tpb_nowait"}
*
- {@code "isc_tpb_read"}
*
- {@code "isc_tpb_write"}
*
- {@code "isc_tpb_lock_read"}
*
- {@code "isc_tpb_lock_write"}
*
- {@code "isc_tpb_shared"}
*
- {@code "isc_tpb_protected"}
*
* It is also allowed to strip "isc_tpb_" prefix from above shown constants.
* Meaning of these constants and possible combinations you can find in a
* documentation.
*
* @throws SQLException if mapping contains incorrect values.
*/
public FBTpbMapper(Map stringMapping) throws SQLException {
// Ensure defaults are populated
this();
processMapping(stringMapping);
}
private FBTpbMapper(FBTpbMapper source) {
var newMapping = new ConcurrentHashMap(source.mapping.size());
for (Map.Entry entry : source.mapping.entrySet()) {
newMapping.put(entry.getKey(), entry.getValue().deepCopy());
}
mapping = newMapping;
defaultIsolationLevel = source.defaultIsolationLevel;
}
/**
* Process specified string mapping. This method updates default mapping with values specified in
* a {@code stringMapping}.
*
* @param stringMapping
* mapping to process.
* @throws SQLException
* if mapping contains incorrect values.
*/
private void processMapping(Map stringMapping) throws SQLException {
for (Map.Entry entry : stringMapping.entrySet()) {
String jdbcTxIsolation = entry.getKey();
int isolationLevel;
try {
isolationLevel = getTransactionIsolationLevel(jdbcTxIsolation);
} catch (IllegalArgumentException ex) {
// TODO More specific exception, Jaybird error code
throw new SQLException(unsupportedIsolationLevel(jdbcTxIsolation));
}
TransactionParameterBuffer tpb = processMapping(entry.getValue());
mapping.put(isolationLevel, tpb);
}
}
private static String unsupportedIsolationLevel(int isolationLevel) {
return unsupportedIsolationLevel(String.valueOf(isolationLevel));
}
private static String unsupportedIsolationLevel(String jdbcTxIsolation) {
return "Transaction isolation " + jdbcTxIsolation + " is not supported.";
}
/**
* Create instance of this class and load mapping from the specified resource.
*
* @param mappingResource
* name of the resource to load.
* @param cl
* class loader that should be used to load specified resource.
* @throws SQLException
* if resource cannot be loaded or contains incorrect values.
*/
public FBTpbMapper(String mappingResource, ClassLoader cl) throws SQLException {
// Ensure defaults are populated
this();
// Make sure the old documented 'res:' protocol works
// TODO Remove in Jaybird 7 or later?
if (mappingResource.startsWith("res:")) {
mappingResource = mappingResource.substring(4);
}
try {
var res = ResourceBundle.getBundle(mappingResource, Locale.getDefault(), cl);
var mapping = new HashMap();
Enumeration en = res.getKeys();
while (en.hasMoreElements()) {
String key = en.nextElement();
mapping.put(key, res.getString(key));
}
processMapping(mapping);
} catch (MissingResourceException mrex) {
// TODO More specific exception, Jaybird error code
throw new SQLException("Cannot load TPB mapping. " + mrex.getMessage(), mrex);
}
}
/**
* This method extracts TPB mapping information from the connection parameters and adds it to the
* connectionProperties. The following format is supported:
*
* {@code info} contains separate mappings for each of following transaction isolation levels:
* {@code "TRANSACTION_SERIALIZABLE"}, {@code "TRANSACTION_REPEATABLE_READ"} and
* {@code "TRANSACTION_READ_COMMITTED"}.
*
*
* @param connectionProperties
* FirebirdConnectionProperties to set transaction state
* @param info
* connection parameters passed into a driver.
* @throws SQLException
* if specified mapping is incorrect.
* @see #processMapping(FirebirdConnectionProperties, Map)
*/
public static void processMapping(FirebirdConnectionProperties connectionProperties, Properties info)
throws SQLException {
for (String isolationName : ISOLATION_LEVEL_NAMES) {
String property = info.getProperty(isolationName);
if (property == null) continue;
connectionProperties.setTransactionParameters(
getTransactionIsolationLevel(isolationName),
processMapping(property));
}
}
/**
* This method extracts TPB mapping information from the connection parameters and adds it to the
* connectionProperties. The following format is supported:
*
* {@code info} contains separate mappings for each of following transaction isolation levels:
* {@code "TRANSACTION_SERIALIZABLE"}, {@code "TRANSACTION_REPEATABLE_READ"} and
* {@code "TRANSACTION_READ_COMMITTED"}.
*
*
* @param connectionProperties
* FirebirdConnectionProperties to set transaction state
* @param info
* connection parameters passed into a driver.
* @throws SQLException
* if specified mapping is incorrect.
* @see #processMapping(FirebirdConnectionProperties, Properties)
*/
public static void processMapping(FirebirdConnectionProperties connectionProperties, Map info)
throws SQLException {
for (String isolationName : ISOLATION_LEVEL_NAMES) {
String property = info.get(isolationName);
if (property == null) continue;
connectionProperties.setTransactionParameters(
getTransactionIsolationLevel(isolationName),
processMapping(property));
}
}
/**
* Process comma-separated list of keywords and convert them into TPB
* values.
*
* @param mapping
* comma-separated list of keywords.
* @return set containing values corresponding to the specified keywords.
* @throws SQLException
* if mapping contains keyword that is not a TPB parameter.
*/
public static TransactionParameterBuffer processMapping(String mapping) throws SQLException {
// TODO instance creation should be delegated to FbDatabase
var result = new TransactionParameterBufferImpl();
var st = new StringTokenizer(mapping, ",");
while (st.hasMoreTokens()) {
String token = st.nextToken();
Integer argValue = null;
if (token.contains("=")) {
String[] parts = token.split("=");
try {
argValue = Integer.valueOf(parts[1]);
} catch (NumberFormatException ex) {
// TODO More specific exception, Jaybird error code
throw new SQLException(parts[1] + " is not valid integer value");
}
token = parts[0];
}
Integer value = TpbMapping.getTpbParam(token);
if (value == null) {
// TODO More specific exception, Jaybird error code
throw new SQLException("Keyword " + token + " unknown. Please check your mapping.");
}
if (argValue == null) {
result.addArgument(value);
} else {
result.addArgument(value, argValue);
}
}
return result;
}
/**
* Get mapping for the specified transaction isolation level.
*
* @param transactionIsolation
* transaction isolation level.
* @return set with TPB parameters.
* @throws IllegalArgumentException
* if specified transaction isolation level is unknown.
*/
public TransactionParameterBuffer getMapping(int transactionIsolation) {
// promote READ UNCOMMITTED to READ COMMITTED
TransactionParameterBuffer tpb = mapping.get(transactionIsolation != Connection.TRANSACTION_READ_UNCOMMITTED
? transactionIsolation : Connection.TRANSACTION_READ_COMMITTED);
if (tpb == null) {
throw new IllegalArgumentException(unsupportedIsolationLevel(transactionIsolation));
}
return tpb.deepCopy();
}
/**
* Set mapping for the specified transaction isolation.
*
* @param transactionIsolation
* transaction isolation level.
* @param tpb
* TPB parameters.
* @throws IllegalArgumentException
* if incorrect isolation level is specified.
*/
@SuppressWarnings("java:S1301")
public void setMapping(int transactionIsolation, TransactionParameterBuffer tpb) {
switch (transactionIsolation) {
case Connection.TRANSACTION_SERIALIZABLE, Connection.TRANSACTION_REPEATABLE_READ,
Connection.TRANSACTION_READ_COMMITTED -> mapping.put(transactionIsolation, tpb.deepCopy());
// TODO Throw SQLException instead?
default -> throw new IllegalArgumentException(unsupportedIsolationLevel(transactionIsolation));
}
}
/**
* Get default mapping. Default mapping represents a TPB mapping for the
* default transaction isolation level (read committed).
*
* @return mapping for the default transaction isolation level.
*/
public TransactionParameterBuffer getDefaultMapping() {
return getMapping(defaultIsolationLevel);
}
int getDefaultTransactionIsolation() {
return defaultIsolationLevel;
}
void setDefaultTransactionIsolation(int isolationLevel) {
this.defaultIsolationLevel = isolationLevel;
}
public boolean equals(Object obj) {
if (this == obj) return true;
return obj instanceof FBTpbMapper that
&& this.mapping.equals(that.mapping)
&& this.defaultIsolationLevel == that.defaultIsolationLevel;
}
public int hashCode() {
// TODO both these values are mutable, so potentially unstable hashcode
return Objects.hash(mapping, defaultIsolationLevel);
}
public static FBTpbMapper copyOf(FBTpbMapper tpbMapper) {
return new FBTpbMapper(tpbMapper);
}
private static final class TpbMapping {
private static final String TPB_PREFIX = "isc_tpb_";
private static final Map tpbTypes;
// Initialize mappings between TPB constant names and their values; should be executed only once.
static {
final var tempTpbTypes = new HashMap(64);
final Field[] fields = TpbItems.class.getFields();
for (Field field : fields) {
final String name = field.getName();
if (!(name.startsWith(TPB_PREFIX) && field.getType().equals(int.class))) continue;
try {
final Integer value = field.getInt(null);
// put the correct parameter name
tempTpbTypes.put(name.substring(TPB_PREFIX.length()), value);
// put the full name for compatibility
tempTpbTypes.put(name, value);
} catch (IllegalAccessException ignored) {
// skip field
}
}
tpbTypes = Map.copyOf(tempTpbTypes);
}
/**
* Get value of TPB parameter for the specified name. This method tries to match string representation of
* the TPB parameter with its value.
*
* @param name
* string representation of TPB parameter, can have "isc_tpb_" prefix.
* @return value corresponding to the specified parameter name or {@code null} if nothing was found.
*/
private static Integer getTpbParam(String name) {
return tpbTypes.get(name);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy