All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.firebirdsql.jdbc.FBTpbMapper Maven / Gradle / Ivy

There is a newer version: 6.0.0-beta-1
Show 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 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 is provides mapping capabilities between standard JDBC
 * transaction isolation level and Firebird Transaction Parameters Block (TPB).
 *
 * @author Roman Rokytskyy
 */
public final class FBTpbMapper implements Serializable, Cloneable {

    private static final long serialVersionUID = 1690658870275668176L;

    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 = Collections.unmodifiableList(Arrays.asList(
            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 Map mapping = new ConcurrentHashMap<>();
    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
        // TODO Should use isc_tpb_mapping.properties

        TransactionParameterBuffer serializableTpb = new TransactionParameterBufferImpl();
        serializableTpb.addArgument(isc_tpb_write);
        serializableTpb.addArgument(isc_tpb_wait);
        serializableTpb.addArgument(isc_tpb_consistency);

        TransactionParameterBuffer repeatableReadTpb = new TransactionParameterBufferImpl();
        repeatableReadTpb.addArgument(isc_tpb_write);
        repeatableReadTpb.addArgument(isc_tpb_wait);
        repeatableReadTpb.addArgument(isc_tpb_concurrency);

        TransactionParameterBuffer 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);

        mapping.put(Connection.TRANSACTION_SERIALIZABLE, serializableTpb);
        mapping.put(Connection.TRANSACTION_REPEATABLE_READ, repeatableReadTpb);
        mapping.put(Connection.TRANSACTION_READ_COMMITTED, readCommittedTpb);
    }

    /**
     * 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 { this(); processMapping(stringMapping); } /** * 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("Transaction isolation " + jdbcTxIsolation + " is not supported."); } TransactionParameterBuffer tpb = processMapping(entry.getValue()); mapping.put(isolationLevel, tpb); } } /** * 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 { // TODO The documentation of DatabaseConnectionProperties.setTpbMapping suggests more functionality than // actually available // Make sure the documented 'res:' protocol works if (mappingResource.startsWith("res:")) { mappingResource = mappingResource.substring(4); } try { ResourceBundle res = ResourceBundle.getBundle(mappingResource, Locale.getDefault(), cl); Map mapping = new HashMap<>(); Enumeration en = res.getKeys(); while (en.hasMoreElements()) { String key = en.nextElement(); String value = res.getString(key); mapping.put(key, value); } 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 TransactionParameterBuffer result = new TransactionParameterBufferImpl(); StringTokenizer 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) { switch (transactionIsolation) { case Connection.TRANSACTION_SERIALIZABLE: case Connection.TRANSACTION_REPEATABLE_READ: case Connection.TRANSACTION_READ_COMMITTED: return mapping.get(transactionIsolation).deepCopy(); case Connection.TRANSACTION_READ_UNCOMMITTED: // promote transaction return mapping.get(Connection.TRANSACTION_READ_COMMITTED).deepCopy(); case Connection.TRANSACTION_NONE: default: // TODO Throw SQLException instead? throw new IllegalArgumentException( "Transaction isolation level " + transactionIsolation + " is not supported."); } } /** * Set mapping for the specified transaction isolation. * * @param transactionIsolation * transaction isolation level. * @param tpb * TPB parameters. * @throws IllegalArgumentException * if incorrect isolation level is specified. */ public void setMapping(int transactionIsolation, TransactionParameterBuffer tpb) { switch (transactionIsolation) { case Connection.TRANSACTION_SERIALIZABLE: case Connection.TRANSACTION_REPEATABLE_READ: case Connection.TRANSACTION_READ_COMMITTED: mapping.put(transactionIsolation, tpb.deepCopy()); break; case Connection.TRANSACTION_READ_UNCOMMITTED: case Connection.TRANSACTION_NONE: default: // TODO Throw SQLException instead? throw new IllegalArgumentException( "Transaction isolation level " + transactionIsolation + " is not supported."); } } /** * 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 mapping.get(defaultIsolationLevel); } int getDefaultTransactionIsolation() { return defaultIsolationLevel; } void setDefaultTransactionIsolation(int isolationLevel) { this.defaultIsolationLevel = isolationLevel; } public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof FBTpbMapper)) { return false; } FBTpbMapper that = (FBTpbMapper) obj; boolean result = this.mapping.equals(that.mapping); result &= (this.defaultIsolationLevel == that.defaultIsolationLevel); return result; } public int hashCode() { // TODO both these values are mutable, so potentially unstable hashcode return Objects.hash(mapping, defaultIsolationLevel); } public Object clone() { try { FBTpbMapper clone = (FBTpbMapper) super.clone(); ConcurrentHashMap newMapping = new ConcurrentHashMap<>(); for (Map.Entry entry : mapping.entrySet()) { newMapping.put(entry.getKey(), entry.getValue().deepCopy()); } clone.mapping = newMapping; return clone; } catch (CloneNotSupportedException ex) { throw new Error("Assertion failure: clone not supported"); // Can't happen } } 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 Map 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; } final Integer value; try { value = field.getInt(null); } catch (IllegalAccessException iaex) { continue; } // put the correct parameter name tempTpbTypes.put(name.substring(TPB_PREFIX.length()), value); // put the full name to tolerate people's mistakes tempTpbTypes.put(name, value); } tpbTypes = Collections.unmodifiableMap(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