Repository: microsoft/mssql-jdbc
Pull request #2078 (2023-02-17 22:23:01)

XA error correction
Fix for https://github.com/microsoft/mssql-jdbc/issues/1432


Patch:
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java b/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java
index 01c031a3..267034bf 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java
@@ -1370,8 +1370,9 @@ final class DDC {
             return false;
 
         // Maximum scale allowed is same as maximum precision allowed.
-        if (bigDecimalValue.scale() > SQLServerConnection.maxDecimalPrecision)
+        if (bigDecimalValue.scale() > SQLServerConnection.MAX_DECIMAL_PRECISION) {
             return true;
+        }
 
         // Convert to unscaled integer value, then compare with maxRPCDecimalValue.
         // NOTE: Handle negative scale as a special case for JDK 1.5 and later VMs.
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
index 19d69126..acb0cdfc 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
@@ -4756,7 +4756,7 @@ final class TDSWriter {
     void writeRPCBigDecimal(String sName, BigDecimal bdValue, int nScale, boolean bOut) throws SQLServerException {
         writeRPCNameValType(sName, bOut, TDSType.DECIMALN);
         writeByte((byte) 0x11); // maximum length
-        writeByte((byte) SQLServerConnection.maxDecimalPrecision); // precision
+        writeByte((byte) SQLServerConnection.MAX_DECIMAL_PRECISION); // precision
 
         byte[] val = DDC.convertBigDecimalToBytes(bdValue, nScale);
         writeBytes(val, 0, val.length);
@@ -6366,7 +6366,7 @@ final class TDSWriter {
     }
 
     private byte[] scaledNanosToEncodedBytes(long scaledNanos, int encodedLength) {
-        byte encodedBytes[] = new byte[encodedLength];
+        byte[] encodedBytes = new byte[encodedLength];
         for (int i = 0; i < encodedLength; i++)
             encodedBytes[i] = (byte) ((scaledNanos >> (8 * i)) & 0xFF);
         return encodedBytes;
@@ -6423,7 +6423,7 @@ final class TDSWriter {
                 long maxStreamLength = 65535L * con.getTDSPacketSize();
 
                 try {
-                    byte buff[] = new byte[8000];
+                    byte[] buff = new byte[8000];
                     int bytesRead;
 
                     while (streamLength < maxStreamLength && -1 != (bytesRead = stream.read(buff, 0, buff.length))) {
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java
index ee0a5a8e..88fa73a3 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java
@@ -240,7 +240,7 @@ public interface ISQLServerConnection extends java.sql.Connection {
      * Valid values are: datetime, datetime2 or datetimeoffset.
      * 
      * @param datetimeParameterTypeValue
-     *        The datatype to use when encoding Java dates into SQL Server. Valid values are: 
+     *        The datatype to use when encoding Java dates into SQL Server. Valid values are:
      *        datetime, datetime2 or datetimeoffset.
      * 
      * @throws SQLServerException
@@ -451,4 +451,19 @@ public interface ISQLServerConnection extends java.sql.Connection {
      */
     @Deprecated(since = "12.1.0", forRemoval = true)
     void setMsiTokenCacheTtl(int timeToLive);
+
+    /**
+     * Returns the fully qualified class name of the implementing class for {@link SQLServerAccessTokenCallback}.
+     *
+     * @return accessTokenCallbackClass
+     */
+    String getAccessTokenCallbackClass();
+
+    /**
+     * Sets 'accessTokenCallbackClass' to the fully qualified class name
+     * of the implementing class for {@link SQLServerAccessTokenCallback}.
+     *
+     * @param accessTokenCallbackClass
+     */
+    void setAccessTokenCallbackClass(String accessTokenCallbackClass);
 }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
index 4e6c8390..a435666e 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
@@ -347,7 +347,7 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource {
      * Sets the SQL server datatype to use for Java datetime and timestamp values.
      * 
      * @param datetimeParameterType
-     *        The SQL datatype to use when encoding Java dates for SQL Server. Valid values are: 
+     *        The SQL datatype to use when encoding Java dates for SQL Server. Valid values are:
      *        datetime, datetime2 or datetimeoffset.
      */
     void setDatetimeParameterType(String datetimeParameterType);
@@ -1268,4 +1268,19 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource {
      * @return Access token callback delegate.
      */
     SQLServerAccessTokenCallback getAccessTokenCallback();
+
+    /**
+     * Returns the fully qualified class name of the implementing class for {@link SQLServerAccessTokenCallback}.
+     *
+     * @return accessTokenCallbackClass
+     */
+    String getAccessTokenCallbackClass();
+
+    /**
+     * Sets 'accessTokenCallbackClass' to the fully qualified class name
+     * of the implementing class for {@link SQLServerAccessTokenCallback}.
+     *
+     * @param accessTokenCallbackClass
+     */
+    void setAccessTokenCallbackClass(String accessTokenCallbackClass);
 }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java b/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java
index 1266fdac..0573e19f 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java
@@ -472,8 +472,9 @@ final class Parameter {
                 case DECIMAL:
                 case NUMERIC:
                     // First, bound the scale by the maximum allowed by SQL Server
-                    if (scale > SQLServerConnection.maxDecimalPrecision)
-                        scale = SQLServerConnection.maxDecimalPrecision;
+                    if (scale > SQLServerConnection.MAX_DECIMAL_PRECISION) {
+                        scale = SQLServerConnection.MAX_DECIMAL_PRECISION;
+                    }
 
                     // Next, prepare with the largest of:
                     // - the value's scale (initial value, as limited above)
@@ -503,26 +504,27 @@ final class Parameter {
                             // then we set precision as the default precision instead of max precision
                             if (!isOutput()) {
                                 param.typeDefinition = SSType.DECIMAL.toString() + "("
-                                        + SQLServerConnection.defaultDecimalPrecision + "," + scale + ")";
+                                        + SQLServerConnection.DEFAULT_DECIMAL_PRECISION + "," + scale + ")";
                             }
                         } else {
-                            if (SQLServerConnection.defaultDecimalPrecision >= valueLength) {
+                            if (SQLServerConnection.DEFAULT_DECIMAL_PRECISION >= valueLength) {
                                 param.typeDefinition = SSType.DECIMAL.toString() + "("
-                                        + SQLServerConnection.defaultDecimalPrecision + "," + scale + ")";
+                                        + SQLServerConnection.DEFAULT_DECIMAL_PRECISION + "," + scale + ")";
 
-                                if (SQLServerConnection.defaultDecimalPrecision < (valueLength + scale)) {
+                                if (SQLServerConnection.DEFAULT_DECIMAL_PRECISION < (valueLength + scale)) {
                                     param.typeDefinition = SSType.DECIMAL.toString() + "("
-                                            + (SQLServerConnection.defaultDecimalPrecision + scale) + "," + scale + ")";
+                                            + (SQLServerConnection.DEFAULT_DECIMAL_PRECISION + scale) + "," + scale
+                                            + ")";
                                 }
                             } else {
                                 param.typeDefinition = SSType.DECIMAL.toString() + "("
-                                        + SQLServerConnection.maxDecimalPrecision + "," + scale + ")";
+                                        + SQLServerConnection.MAX_DECIMAL_PRECISION + "," + scale + ")";
                             }
                         }
 
                         if (isOutput()) {
                             param.typeDefinition = SSType.DECIMAL.toString() + "("
-                                    + SQLServerConnection.maxDecimalPrecision + ", " + scale + ")";
+                                    + SQLServerConnection.MAX_DECIMAL_PRECISION + ", " + scale + ")";
                         }
 
                         if (userProvidesPrecision) {
@@ -544,7 +546,7 @@ final class Parameter {
 
                         } else {
                             param.typeDefinition = SSType.DECIMAL.toString() + "("
-                                    + SQLServerConnection.maxDecimalPrecision + "," + scale + ")";
+                                    + SQLServerConnection.MAX_DECIMAL_PRECISION + "," + scale + ")";
                         }
                     }
 
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerAccessTokenCallback.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerAccessTokenCallback.java
index 2d3ba451..c56c6be4 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerAccessTokenCallback.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerAccessTokenCallback.java
@@ -17,5 +17,5 @@ public interface SQLServerAccessTokenCallback {
      *
      * @return Returns a {@link SqlAuthenticationToken}.
      */
-    SqlAuthenticationToken getAccessToken(String stsurl, String spn);
+    SqlAuthenticationToken getAccessToken(String spn, String stsurl);
 }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerAeadAes256CbcHmac256Algorithm.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerAeadAes256CbcHmac256Algorithm.java
index 55863ba8..4a78d845 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerAeadAes256CbcHmac256Algorithm.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerAeadAes256CbcHmac256Algorithm.java
@@ -44,7 +44,6 @@ class SQLServerAeadAes256CbcHmac256Algorithm extends SQLServerEncryptionAlgorith
     private boolean isDeterministic = false;
     // Each block in the AES is 128 bits
     private int blockSizeInBytes = 16;
-    private int keySizeInBytes = SQLServerAeadAes256CbcHmac256EncryptionKey.keySize / 8;
     private byte[] version = new byte[] {0x01};
     // Added so that java hashing algorithm is similar to c#
     private byte[] versionSize = new byte[] {1};
@@ -60,7 +59,7 @@ class SQLServerAeadAes256CbcHmac256Algorithm extends SQLServerEncryptionAlgorith
      * 1 block of cipher Text)
      */
     private int minimumCipherTextLengthInBytesWithAuthenticationTag = minimumCipherTextLengthInBytesNoAuthenticationTag
-            + keySizeInBytes;
+            + SQLServerAeadAes256CbcHmac256EncryptionKey.KEYSIZE_IN_BYTES;
 
     /**
      * Initializes a new instance of SQLServerAeadAes256CbcHmac256Algorithm with a given key, encryption type and
@@ -127,7 +126,8 @@ class SQLServerAeadAes256CbcHmac256Algorithm extends SQLServerEncryptionAlgorith
         int numBlocks = plainText.length / blockSizeInBytes + 1;
 
         int hmacStartIndex = 1;
-        int authenticationTagLen = hasAuthenticationTag ? keySizeInBytes : 0;
+        int authenticationTagLen = hasAuthenticationTag ? SQLServerAeadAes256CbcHmac256EncryptionKey.KEYSIZE_IN_BYTES
+                                                        : 0;
         int ivStartIndex = hmacStartIndex + authenticationTagLen;
         int cipherStartIndex = ivStartIndex + blockSizeInBytes;
 
@@ -236,8 +236,8 @@ class SQLServerAeadAes256CbcHmac256Algorithm extends SQLServerEncryptionAlgorith
         // Read authentication tag
         if (hasAuthenticationTag) {
             authenticationTagOffset = startIndex;
-            // authentication tag size is keySizeInBytes
-            startIndex += keySizeInBytes;
+            // authentication tag size is SQLServerAeadAes256CbcHmac256EncryptionKey.KEYSIZE_IN_BYTES
+            startIndex += SQLServerAeadAes256CbcHmac256EncryptionKey.KEYSIZE_IN_BYTES;
         }
 
         // Read IV from cipher text
@@ -330,7 +330,7 @@ class SQLServerAeadAes256CbcHmac256Algorithm extends SQLServerEncryptionAlgorith
             int length) throws NoSuchAlgorithmException, InvalidKeyException {
         assert (cipherText != null);
         byte[] computedHash;
-        byte[] authenticationTag = new byte[keySizeInBytes];
+        byte[] authenticationTag = new byte[SQLServerAeadAes256CbcHmac256EncryptionKey.KEYSIZE_IN_BYTES];
 
         Mac hmac = Mac.getInstance(HMAC_SHA_256);
         SecretKeySpec key = new SecretKeySpec(columnEncryptionkey.getMacKey(), HMAC_SHA_256);
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerAeadAes256CbcHmac256EncryptionKey.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerAeadAes256CbcHmac256EncryptionKey.java
index b1a8c585..fc334fec 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerAeadAes256CbcHmac256EncryptionKey.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerAeadAes256CbcHmac256EncryptionKey.java
@@ -21,7 +21,9 @@ import java.text.MessageFormat;
 class SQLServerAeadAes256CbcHmac256EncryptionKey extends SQLServerSymmetricKey {
 
     // This is the key size in the bits, since we are using AES256, it will 256
-    static final int keySize = 256;
+    static final int KEYSIZE = 256;
+    static final int KEYSIZE_IN_BYTES = KEYSIZE / 8;
+
     // Name of algorithm associated with this key
     private final String algorithmName;
     // Salt used to derive encryption key
@@ -47,22 +49,21 @@ class SQLServerAeadAes256CbcHmac256EncryptionKey extends SQLServerSymmetricKey {
         super(rootKey);
         this.algorithmName = algorithmName;
         encryptionKeySaltFormat = "Microsoft SQL Server cell encryption key with encryption algorithm:"
-                + this.algorithmName + " and key length:" + keySize;
+                + this.algorithmName + " and key length:" + KEYSIZE;
         macKeySaltFormat = "Microsoft SQL Server cell MAC key with encryption algorithm:" + this.algorithmName
-                + " and key length:" + keySize;
+                + " and key length:" + KEYSIZE;
         ivKeySaltFormat = "Microsoft SQL Server cell IV key with encryption algorithm:" + this.algorithmName
-                + " and key length:" + keySize;
-        int keySizeInBytes = (keySize / 8);
-        if (rootKey.length != keySizeInBytes) {
+                + " and key length:" + KEYSIZE;
+        if (rootKey.length != KEYSIZE_IN_BYTES) {
             MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidKeySize"));
-            Object[] msgArgs = {rootKey.length, keySizeInBytes, this.algorithmName};
+            Object[] msgArgs = {rootKey.length, KEYSIZE_IN_BYTES, this.algorithmName};
             throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
 
         }
 
         // Derive encryption key
 
-        byte[] encKeyBuff = new byte[keySizeInBytes];
+        byte[] encKeyBuff = new byte[KEYSIZE_IN_BYTES];
         try {
             // By default Java is big endian, we are getting bytes in little endian(LE in UTF-16LE)
             // to make it compatible with C# driver which is little endian
@@ -72,14 +73,14 @@ class SQLServerAeadAes256CbcHmac256EncryptionKey extends SQLServerSymmetricKey {
             encryptionKey = new SQLServerSymmetricKey(encKeyBuff);
 
             // Derive mac key from root key
-            byte[] macKeyBuff = new byte[keySizeInBytes];
+            byte[] macKeyBuff = new byte[KEYSIZE_IN_BYTES];
             macKeyBuff = SQLServerSecurityUtility.getHMACWithSHA256(macKeySaltFormat.getBytes(UTF_16LE), rootKey,
                     macKeyBuff.length);
 
             macKey = new SQLServerSymmetricKey(macKeyBuff);
 
             // Derive the initialization vector from root key
-            byte[] ivKeyBuff = new byte[keySizeInBytes];
+            byte[] ivKeyBuff = new byte[KEYSIZE_IN_BYTES];
             ivKeyBuff = SQLServerSecurityUtility.getHMACWithSHA256(ivKeySaltFormat.getBytes(UTF_16LE), rootKey,
                     ivKeyBuff.length);
             ivKey = new SQLServerSymmetricKey(ivKeyBuff);
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java
index 4dbfed76..8b4057b3 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java
@@ -230,7 +230,7 @@ class SQLServerBulkBatchInsertRecord extends SQLServerBulkRecord {
              * columnMetadata).
              */
             // case when the user has not provided the optional list of column names.
-            if (null == columnList || columnList.size() == 0) {
+            if (null == columnList || columnList.isEmpty()) {
                 valueData = valueList.get(index);
                 /*
                  * if the user has provided a wildcard for this column, fetch the set value from the batchParam.
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java
index b7188cd7..195b197e 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java
@@ -64,7 +64,7 @@ public class SQLServerBulkCSVFileRecord extends SQLServerBulkRecord implements j
      * Regex to ignore delimiter when the field is enclosed in quotes.
      * 
      */
-    private static final String escapeSplitPattern = "(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)";
+    private static final String ESCAPE_SPLIT_PATTERN = "(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)";
 
     /*
      * Class names for logging.
@@ -207,7 +207,7 @@ public class SQLServerBulkCSVFileRecord extends SQLServerBulkRecord implements j
             currentLine = fileReader.readLine();
             if (null != currentLine) {
                 columnNames = (escapeDelimiters && currentLine.contains("\"")) ? escapeQuotesRFC4180(
-                        currentLine.split(delimiter + escapeSplitPattern, -1)) : currentLine.split(delimiter, -1);
+                        currentLine.split(delimiter + ESCAPE_SPLIT_PATTERN, -1)) : currentLine.split(delimiter, -1);
             }
         }
     }
@@ -252,7 +252,7 @@ public class SQLServerBulkCSVFileRecord extends SQLServerBulkRecord implements j
              * empty strings are discarded. Empty string is returned if there is no value.
              */
             String[] data = (escapeDelimiters && currentLine.contains("\"")) ? escapeQuotesRFC4180(
-                    currentLine.split(delimiter + escapeSplitPattern, -1)) : currentLine.split(delimiter, -1);
+                    currentLine.split(delimiter + ESCAPE_SPLIT_PATTERN, -1)) : currentLine.split(delimiter, -1);
 
             // Cannot go directly from String[] to Object[] and expect it to act as an array.
 
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java
index e828705f..48160ee7 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java
@@ -170,7 +170,7 @@ public class SQLServerBulkCopy implements java.lang.AutoCloseable, java.io.Seria
     /**
      * Statement level encryption setting needed for querying against encrypted columns.
      */
-    private SQLServerStatementColumnEncryptionSetting stmtColumnEncriptionSetting = SQLServerStatementColumnEncryptionSetting.UseConnectionSetting;
+    private SQLServerStatementColumnEncryptionSetting stmtColumnEncriptionSetting = SQLServerStatementColumnEncryptionSetting.USE_CONNECTION_SETTING;
 
     /**
      * Destination table metadata
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionAzureKeyVaultProvider.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionAzureKeyVaultProvider.java
index 33fb520b..73f37f0b 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionAzureKeyVaultProvider.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionAzureKeyVaultProvider.java
@@ -906,7 +906,7 @@ public class SQLServerColumnEncryptionAzureKeyVaultProvider extends SQLServerCol
 
     private static List<String> getTrustedEndpoints() {
         Properties mssqlJdbcProperties = getMssqlJdbcProperties();
-        List<String> trustedEndpoints = new ArrayList<String>();
+        List<String> trustedEndpoints = new ArrayList<>();
         boolean append = true;
         if (null != mssqlJdbcProperties) {
             String endpoints = mssqlJdbcProperties.getProperty(AKV_TRUSTED_ENDPOINTS_KEYWORD);
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionJavaKeyStoreProvider.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionJavaKeyStoreProvider.java
index f96eb92e..5fbc111a 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionJavaKeyStoreProvider.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionJavaKeyStoreProvider.java
@@ -312,8 +312,7 @@ public class SQLServerColumnEncryptionJavaKeyStoreProvider extends SQLServerColu
     private byte[] getLittleEndianBytesFromShort(short value) {
         ByteBuffer byteBuffer = ByteBuffer.allocate(2);
         byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
-        byte[] byteValue = byteBuffer.putShort(value).array();
-        return byteValue;
+        return byteBuffer.putShort(value).array();
     }
 
     /*
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
index db1f9513..a15a9e68 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
@@ -144,6 +144,15 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
     /** fedAuth requested flag */
     private boolean federatedAuthenticationRequested = false;
 
+    /** Access token callback class name */
+    private transient String accessTokenCallbackClass = null;
+
+    /** Flag that determines whether the accessToken callback class is set **/
+    private boolean hasAccessTokenCallbackClass = false;
+
+    /** Flag that determines whether the accessToken callback was set **/
+    private transient SQLServerAccessTokenCallback accessTokenCallback = null;
+
     /**
      * Keep this distinct from _federatedAuthenticationRequested, since some fedauth library types may not need more
      * info
@@ -187,15 +196,15 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
      * https://docs.microsoft.com/sql/t-sql/functions/serverproperty-transact-sql
      */
     /** Engine Edition 5 = SQL Database */
-    private final int ENGINE_EDITION_SQL_AZURE_DB = 5;
+    private static final int ENGINE_EDITION_SQL_AZURE_DB = 5;
     /** Engine Edition 6 = Microsoft Azure Synapse Analytics */
-    private final int ENGINE_EDITION_SQL_AZURE_SYNAPSE_ANALYTICS = 6;
+    private static final int ENGINE_EDITION_SQL_AZURE_SYNAPSE_ANALYTICS = 6;
     /** Engine Edition 8 = Azure SQL Managed Instance */
-    private final int ENGINE_EDITION_SQL_AZURE_MI = 8;
+    private static final int ENGINE_EDITION_SQL_AZURE_MI = 8;
     /** Engine Edition 9 = Azure SQL Edge (This is returned for all editions of Azure SQL Edge) */
-    private final int ENGINE_EDITION_SQL_AZURE_SQL_EDGE = 9;
+    private static final int ENGINE_EDITION_SQL_AZURE_SQL_EDGE = 9;
     /** Engine Edition 11 = Azure Synapse serverless SQL pool */
-    private final int ENGINE_EDITION_SQL_AZURE_SYNAPSE_SERVERLESS_SQL_POOL = 11;
+    private static final int ENGINE_EDITION_SQL_AZURE_SYNAPSE_SERVERLESS_SQL_POOL = 11;
 
     /** flag indicating whether server is Azure */
     private Boolean isAzure = null;
@@ -539,12 +548,10 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                 default:
                     // If authenticationString not specified, check if access token callback was set.
                     // If access token callback is set, break.
-                    if (null != activeConnectionProperties
-                            .get(SQLServerDriverObjectProperty.ACCESS_TOKEN_CALLBACK.toString())) {
+                    if (null != accessTokenCallback || hasAccessTokenCallbackClass) {
                         this.authentication = SqlAuthentication.NOT_SPECIFIED;
                         break;
                     }
-                    assert (false);
                     MessageFormat form = new MessageFormat(
                             SQLServerException.getErrString("R_InvalidConnectionSetting"));
                     Object[] msgArgs = {"authentication", authenticationString};
@@ -1429,8 +1436,8 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         return !(state.equals(State.OPENED));
     }
 
-    final static int maxDecimalPrecision = 38; // @@max_precision for SQL 2000 and 2005 is 38.
-    final static int defaultDecimalPrecision = 18;
+    final static int MAX_DECIMAL_PRECISION = 38; // @@max_precision for SQL 2000 and 2005 is 38.
+    final static int DEFAULT_DECIMAL_PRECISION = 18;
 
     /** trace iD */
     final String traceID;
@@ -2452,10 +2459,15 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                     ntlmAuthentication = true;
                 }
 
-                SQLServerAccessTokenCallback callback = (SQLServerAccessTokenCallback) activeConnectionProperties
+                accessTokenCallback = (SQLServerAccessTokenCallback) activeConnectionProperties
                         .get(SQLServerDriverObjectProperty.ACCESS_TOKEN_CALLBACK.toString());
 
-                if (null != callback && (!activeConnectionProperties
+                hasAccessTokenCallbackClass = null != activeConnectionProperties
+                        .get(SQLServerDriverStringProperty.ACCESS_TOKEN_CALLBACK_CLASS.toString())
+                        && !activeConnectionProperties
+                                .getProperty(SQLServerDriverStringProperty.ACCESS_TOKEN_CALLBACK_CLASS.toString())
+                                .isEmpty();
+                if ((null != accessTokenCallback || hasAccessTokenCallbackClass) && (!activeConnectionProperties
                         .getProperty(SQLServerDriverStringProperty.USER.toString()).isEmpty()
                         || !activeConnectionProperties.getProperty(SQLServerDriverStringProperty.PASSWORD.toString())
                                 .isEmpty())) {
@@ -2463,6 +2475,14 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                             SQLServerException.getErrString("R_AccessTokenCallbackWithUserPassword"), null);
                 }
 
+                sPropKey = SQLServerDriverStringProperty.ACCESS_TOKEN_CALLBACK_CLASS.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null == sPropValue) {
+                    sPropValue = SQLServerDriverStringProperty.ACCESS_TOKEN_CALLBACK_CLASS.getDefaultValue();
+                    activeConnectionProperties.setProperty(sPropKey, sPropValue);
+                }
+                setAccessTokenCallbackClass(sPropValue);
+
                 sPropKey = SQLServerDriverStringProperty.AUTHENTICATION.toString();
                 sPropValue = activeConnectionProperties.getProperty(sPropKey);
                 if (null == sPropValue) {
@@ -3503,8 +3523,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
     void prelogin(String serverName, int portNumber) throws SQLServerException {
         // Build a TDS Pre-Login packet to send to the server.
         if ((!authenticationString.equalsIgnoreCase(SqlAuthentication.NOT_SPECIFIED.toString()))
-                || (null != accessTokenInByte) || null != activeConnectionProperties
-                        .get(SQLServerDriverObjectProperty.ACCESS_TOKEN_CALLBACK.toString())) {
+                || (null != accessTokenInByte) || null != accessTokenCallback || hasAccessTokenCallbackClass) {
             fedAuthRequiredByUser = true;
         }
 
@@ -3886,8 +3905,8 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                     // Or AccessToken is not null, mean token based authentication is used.
                     if (((null != authenticationString)
                             && (!authenticationString.equalsIgnoreCase(SqlAuthentication.NOT_SPECIFIED.toString())))
-                            || (null != accessTokenInByte) || null != activeConnectionProperties
-                                    .get(SQLServerDriverObjectProperty.ACCESS_TOKEN_CALLBACK.toString())) {
+                            || (null != accessTokenInByte) || null != accessTokenCallback
+                            || hasAccessTokenCallbackClass) {
                         fedAuthRequiredPreLoginResponse = (preloginResponse[optionOffset] == 1);
                     }
                     break;
@@ -4637,7 +4656,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                     new Object[] {resultSetType, resultSetConcurrency});
         checkClosed();
         SQLServerStatement st = new SQLServerStatement(this, resultSetType, resultSetConcurrency,
-                SQLServerStatementColumnEncryptionSetting.UseConnectionSetting);
+                SQLServerStatementColumnEncryptionSetting.USE_CONNECTION_SETTING);
         if (requestStarted) {
             addOpenStatement(st);
         }
@@ -4654,7 +4673,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         checkClosed();
 
         SQLServerPreparedStatement st = new SQLServerPreparedStatement(this, sql, resultSetType, resultSetConcurrency,
-                SQLServerStatementColumnEncryptionSetting.UseConnectionSetting);
+                SQLServerStatementColumnEncryptionSetting.USE_CONNECTION_SETTING);
 
         if (requestStarted) {
             addOpenStatement(st);
@@ -4690,7 +4709,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         checkClosed();
 
         SQLServerCallableStatement st = new SQLServerCallableStatement(this, sql, resultSetType, resultSetConcurrency,
-                SQLServerStatementColumnEncryptionSetting.UseConnectionSetting);
+                SQLServerStatementColumnEncryptionSetting.USE_CONNECTION_SETTING);
 
         if (requestStarted) {
             addOpenStatement(st);
@@ -4830,8 +4849,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                             break;
                         default:
                             // If not specified, check if access token callback was set. If it is set, break.
-                            if (null != activeConnectionProperties
-                                    .get(SQLServerDriverObjectProperty.ACCESS_TOKEN_CALLBACK.toString())) {
+                            if (null != accessTokenCallback || hasAccessTokenCallbackClass) {
                                 workflow = TDS.ADALWORKFLOW_ACCESSTOKENCALLBACK;
                                 break;
                             }
@@ -5051,8 +5069,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                         || authenticationString
                                 .equalsIgnoreCase(SqlAuthentication.ACTIVE_DIRECTORY_INTERACTIVE.toString()))
                         && fedAuthRequiredPreLoginResponse)
-                || null != activeConnectionProperties
-                        .get(SQLServerDriverObjectProperty.ACCESS_TOKEN_CALLBACK.toString())) {
+                || null != accessTokenCallback || hasAccessTokenCallbackClass) {
             federatedAuthenticationInfoRequested = true;
             fedAuthFeatureExtensionData = new FederatedAuthenticationFeatureExtensionData(TDS.TDS_FEDAUTH_LIBRARY_ADAL,
                     authenticationString, fedAuthRequiredPreLoginResponse);
@@ -5492,7 +5509,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
             }
 
             byte status = (byte) tdsReader.readUnsignedByte();
-            boolean fRecoverable = (status & 0x01) > 0 ? true : false;
+            boolean fRecoverable = (status & 0x01) > 0;
             dataBytesRead += 1;
 
             while (dataBytesRead < dataLength) {
@@ -5612,11 +5629,22 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
 
         attemptRefreshTokenLocked = true;
 
-        SQLServerAccessTokenCallback callback = (SQLServerAccessTokenCallback) activeConnectionProperties
-                .get(SQLServerDriverObjectProperty.ACCESS_TOKEN_CALLBACK.toString());
-
-        if (authenticationString.equals(SqlAuthentication.NOT_SPECIFIED.toString()) && null != callback) {
-            fedAuthToken = callback.getAccessToken(fedAuthInfo.spn, fedAuthInfo.stsurl);
+        if (authenticationString.equals(SqlAuthentication.NOT_SPECIFIED.toString()) && null != accessTokenCallbackClass
+                && !accessTokenCallbackClass.isEmpty()) {
+            try {
+                Object[] msgArgs = {"accessTokenCallbackClass",
+                        "com.microsoft.sqlserver.jdbc.SQLServerAccessTokenCallback"};
+                SQLServerAccessTokenCallback callbackInstance = Util.newInstance(SQLServerAccessTokenCallback.class,
+                        accessTokenCallbackClass, null, msgArgs);
+                fedAuthToken = callbackInstance.getAccessToken(fedAuthInfo.spn, fedAuthInfo.stsurl);
+            } catch (Exception e) {
+                MessageFormat form = new MessageFormat(
+                        SQLServerException.getErrString("R_InvalidAccessTokenCallbackClass"));
+                throw new SQLServerException(form.format(new Object[] {accessTokenCallbackClass}), e.getCause());
+            }
+        } else if (authenticationString.equals(SqlAuthentication.NOT_SPECIFIED.toString())
+                && null != accessTokenCallback) {
+            fedAuthToken = accessTokenCallback.getAccessToken(fedAuthInfo.spn, fedAuthInfo.stsurl);
         } else {
             fedAuthToken = getFedAuthToken(fedAuthInfo);
         }
@@ -5967,7 +5995,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                 if (1 == data[0]) {
                     serverSupportsDNSCaching = true;
                     if (null == dnsCache) {
-                        dnsCache = new ConcurrentHashMap<String, InetSocketAddress>();
+                        dnsCache = new ConcurrentHashMap<>();
                     }
                 } else {
                     serverSupportsDNSCaching = false;
@@ -6040,7 +6068,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
      * 
      * @throws SQLServerException
      */
-    final void JTAUnenlistConnection() throws SQLServerException {
+    final void jtaUnenlistConnection() throws SQLServerException {
         // delist the connection
         executeDTCCommand(TDS.TM_PROPAGATE_XACT, null, "MS_DTC delist connection");
         inXATransaction = false;
@@ -6053,13 +6081,13 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
      *        the cookie identifying the transaction
      * @throws SQLServerException
      */
-    final void JTAEnlistConnection(byte cookie[]) throws SQLServerException {
+    final void jtaEnlistConnection(byte[] cookie) throws SQLServerException {
         // Enlist the connection
         executeDTCCommand(TDS.TM_PROPAGATE_XACT, cookie, "MS_DTC enlist connection");
 
         // DTC sets the enlisted connection's isolation level to SERIALIZABLE by default.
         // Set the isolation level the way the app wants it.
-        connectionCommand(sqlStatementToSetTransactionIsolationLevel(), "JTAEnlistConnection");
+        connectionCommand(sqlStatementToSetTransactionIsolationLevel(), "jtaEnlistConnection");
         inXATransaction = true;
     }
 
@@ -6074,7 +6102,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         if (s == null)
             return new byte[0];
         int l = s.length();
-        byte data[] = new byte[l * 2];
+        byte[] data = new byte[l * 2];
         int offset = 0;
         for (int i = 0; i < l; i++) {
             int c = s.charAt(i);
@@ -6097,7 +6125,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         if (pwd == null)
             pwd = "";
         int len = pwd.length();
-        byte data[] = new byte[len * 2];
+        byte[] data = new byte[len * 2];
         for (int i1 = 0; i1 < len; i1++) {
             int j1 = pwd.charAt(i1) ^ 0x5a5a;
             j1 = (j1 & 0xf) << 4 | (j1 & 0xf0) >> 4 | (j1 & 0xf00) << 4 | (j1 & 0xf000) >> 4;
@@ -6226,17 +6254,17 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
             sPwd = null;
         }
 
-        byte hostnameBytes[] = toUCS16(hostName);
-        byte userBytes[] = toUCS16(sUser);
-        byte passwordBytes[] = encryptPassword(sPwd);
+        byte[] hostnameBytes = toUCS16(hostName);
+        byte[] userBytes = toUCS16(sUser);
+        byte[] passwordBytes = encryptPassword(sPwd);
         int passwordLen = (null != passwordBytes) ? passwordBytes.length : 0;
-        byte appNameBytes[] = toUCS16(appName);
-        byte serverNameBytes[] = toUCS16(serverName);
-        byte interfaceLibNameBytes[] = toUCS16(interfaceLibName);
-        byte interfaceLibVersionBytes[] = {(byte) SQLJdbcVersion.BUILD, (byte) SQLJdbcVersion.PATCH,
+        byte[] appNameBytes = toUCS16(appName);
+        byte[] serverNameBytes = toUCS16(serverName);
+        byte[] interfaceLibNameBytes = toUCS16(interfaceLibName);
+        byte[] interfaceLibVersionBytes = {(byte) SQLJdbcVersion.BUILD, (byte) SQLJdbcVersion.PATCH,
                 (byte) SQLJdbcVersion.MINOR, (byte) SQLJdbcVersion.MAJOR};
-        byte databaseNameBytes[] = toUCS16(databaseName);
-        byte netAddress[] = new byte[6];
+        byte[] databaseNameBytes = toUCS16(databaseName);
+        byte[] netAddress = new byte[6];
         int dataLen = 0;
 
         // TDS version 8 if strict mode
@@ -6541,7 +6569,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         loggerExternal.entering(loggingClassName, CREATE_STATEMENT,
                 new Object[] {nType, nConcur, resultSetHoldability});
         Statement st = createStatement(nType, nConcur, resultSetHoldability,
-                SQLServerStatementColumnEncryptionSetting.UseConnectionSetting);
+                SQLServerStatementColumnEncryptionSetting.USE_CONNECTION_SETTING);
         loggerExternal.exiting(loggingClassName, CREATE_STATEMENT, st);
         return st;
     }
@@ -6568,7 +6596,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         loggerExternal.entering(loggingClassName, "prepareStatement",
                 new Object[] {nType, nConcur, resultSetHoldability});
         PreparedStatement st = prepareStatement(sql, nType, nConcur, resultSetHoldability,
-                SQLServerStatementColumnEncryptionSetting.UseConnectionSetting);
+                SQLServerStatementColumnEncryptionSetting.USE_CONNECTION_SETTING);
         loggerExternal.exiting(loggingClassName, "prepareStatement", st);
         return st;
     }
@@ -6598,7 +6626,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         loggerExternal.entering(loggingClassName, "prepareStatement",
                 new Object[] {nType, nConcur, resultSetHoldability});
         CallableStatement st = prepareCall(sql, nType, nConcur, resultSetHoldability,
-                SQLServerStatementColumnEncryptionSetting.UseConnectionSetting);
+                SQLServerStatementColumnEncryptionSetting.USE_CONNECTION_SETTING);
         loggerExternal.exiting(loggingClassName, "prepareCall", st);
         return st;
     }
@@ -6630,7 +6658,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
             loggerExternal.entering(loggingClassName, "prepareStatement", new Object[] {sql, flag});
         }
         SQLServerPreparedStatement ps = (SQLServerPreparedStatement) prepareStatement(sql, flag,
-                SQLServerStatementColumnEncryptionSetting.UseConnectionSetting);
+                SQLServerStatementColumnEncryptionSetting.USE_CONNECTION_SETTING);
 
         loggerExternal.exiting(loggingClassName, "prepareStatement", ps);
         return ps;
@@ -6656,7 +6684,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
             loggerExternal.entering(loggingClassName, "prepareStatement", new Object[] {sql, columnIndexes});
         }
         SQLServerPreparedStatement ps = (SQLServerPreparedStatement) prepareStatement(sql, columnIndexes,
-                SQLServerStatementColumnEncryptionSetting.UseConnectionSetting);
+                SQLServerStatementColumnEncryptionSetting.USE_CONNECTION_SETTING);
 
         loggerExternal.exiting(loggingClassName, "prepareStatement", ps);
         return ps;
@@ -6687,7 +6715,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         }
 
         SQLServerPreparedStatement ps = (SQLServerPreparedStatement) prepareStatement(sql, columnNames,
-                SQLServerStatementColumnEncryptionSetting.UseConnectionSetting);
+                SQLServerStatementColumnEncryptionSetting.USE_CONNECTION_SETTING);
 
         loggerExternal.exiting(loggingClassName, "prepareStatement", ps);
         return ps;
@@ -6798,9 +6826,6 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         checkClosed();
 
         if (this.holdability != holdability) {
-            assert ResultSet.HOLD_CURSORS_OVER_COMMIT == holdability
-                    || ResultSet.CLOSE_CURSORS_AT_COMMIT == holdability : "invalid holdability " + holdability;
-
             connectionCommand(
                     (holdability == ResultSet.CLOSE_CURSORS_AT_COMMIT) ? "SET CURSOR_CLOSE_ON_COMMIT ON"
                                                                        : "SET CURSOR_CLOSE_ON_COMMIT OFF",
@@ -7066,7 +7091,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
 
         boolean isValid = true;
         try (SQLServerStatement stmt = new SQLServerStatement(this, ResultSet.TYPE_FORWARD_ONLY,
-                ResultSet.CONCUR_READ_ONLY, SQLServerStatementColumnEncryptionSetting.UseConnectionSetting)) {
+                ResultSet.CONCUR_READ_ONLY, SQLServerStatementColumnEncryptionSetting.USE_CONNECTION_SETTING)) {
 
             // If asked, limit the time to wait for the query to complete.
             if (0 != timeout)
@@ -7106,9 +7131,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         try {
             t = iface.cast(this);
         } catch (ClassCastException e) {
-
-            SQLServerException newe = new SQLServerException(e.getMessage(), e);
-            throw newe;
+            throw new SQLServerException(e.getMessage(), e);
         }
         loggerExternal.exiting(loggingClassName, "unwrap", t);
         return t;
@@ -7186,7 +7209,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                 originalSCatalog = sCatalog;
                 originalUseBulkCopyForBatchInsert = getUseBulkCopyForBatchInsert();
                 originalSqlWarnings = sqlWarnings;
-                openStatements = new LinkedList<ISQLServerStatement>();
+                openStatements = new LinkedList<>();
                 originalUseFmtOnly = useFmtOnly;
                 originalDelayLoadingLobs = delayLoadingLobs;
                 requestStarted = true;
@@ -7273,7 +7296,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
     static final char[] OUT = {' ', 'O', 'U', 'T'};
 
     String replaceParameterMarkers(String sqlSrc, int[] paramPositions, Parameter[] params,
-            boolean isReturnValueSyntax) throws SQLServerException {
+            boolean isReturnValueSyntax) {
         final int MAX_PARAM_NAME_LEN = 6;
         char[] sqlDst = new char[sqlSrc.length() + params.length * (MAX_PARAM_NAME_LEN + OUT.length)];
         int dstBegin = 0;
@@ -7354,7 +7377,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
     }
 
     // Detaches this connection from connection pool.
-    void DetachFromPool() {
+    void detachFromPool() {
         lock.lock();
         try {
             pooledConnectionParent = null;
@@ -7409,7 +7432,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                     for (InetAddress inetAddr : inetAddrs) {
                         // Send the UDP request
                         try {
-                            byte sendBuffer[] = (" " + instanceName).getBytes();
+                            byte[] sendBuffer = (" " + instanceName).getBytes();
                             sendBuffer[0] = 4;
                             DatagramPacket udpRequest = new DatagramPacket(sendBuffer, sendBuffer.length, inetAddr,
                                     BROWSER_PORT);
@@ -7428,7 +7451,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                     assert null != inetAddr;
                     // Send the UDP request
                     try {
-                        byte sendBuffer[] = (" " + instanceName).getBytes();
+                        byte[] sendBuffer = (" " + instanceName).getBytes();
                         sendBuffer[0] = 4;
                         DatagramPacket udpRequest = new DatagramPacket(sendBuffer, sendBuffer.length, inetAddr,
                                 BROWSER_PORT);
@@ -7446,7 +7469,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
 
             // Receive the UDP response
             try {
-                byte receiveBuffer[] = new byte[4096];
+                byte[] receiveBuffer = new byte[4096];
                 DatagramPacket udpResponse = new DatagramPacket(receiveBuffer, receiveBuffer.length);
                 datagramSocket.receive(udpResponse);
                 browserResult = new String(receiveBuffer, 3, receiveBuffer.length - 3);
@@ -7622,6 +7645,29 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         return 1 < getServerPreparedStatementDiscardThreshold();
     }
 
+    /**
+     * Returns the fully qualified class name of the implementing class for {@link SQLServerAccessTokenCallback}.
+     *
+     * @return accessTokenCallbackClass
+     */
+    public String getAccessTokenCallbackClass() {
+        if (null == this.accessTokenCallbackClass) {
+            return SQLServerDriverStringProperty.ACCESS_TOKEN_CALLBACK_CLASS.getDefaultValue();
+        }
+
+        return this.accessTokenCallbackClass;
+    }
+
+    /**
+     * Sets 'accessTokenCallbackClass' to the fully qualified class name
+     * of the implementing class for {@link SQLServerAccessTokenCallback}.
+     *
+     * @param accessTokenCallbackClass
+     */
+    public void setAccessTokenCallbackClass(String accessTokenCallbackClass) {
+        this.accessTokenCallbackClass = accessTokenCallbackClass;
+    }
+
     /**
      * Cleans up discarded prepared statement handles on the server using batched un-prepare actions if the batching
      * threshold has been reached.
@@ -7911,12 +7957,12 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
             this.connectionCommand("SELECT @@TRANCOUNT", "SQLServerConnection.supportsTransactions");
         } catch (SQLServerException e) {
             if (e.getMessage().equals(SQLServerException.getErrString("R_transactionsNotSupported"))) {
-                return supportsTransactions = false;
+                return Boolean.FALSE.equals(supportsTransactions);
             }
             throw e;
         }
 
-        return supportsTransactions = true;
+        return Boolean.TRUE.equals(supportsTransactions);
     }
 
     /**
@@ -7979,7 +8025,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
     }
 
     byte[] generateEnclavePackage(String userSQL, ArrayList<byte[]> enclaveCEKs) throws SQLServerException {
-        return (enclaveCEKs.size() > 0) ? enclaveProvider.getEnclavePackage(userSQL, enclaveCEKs) : null;
+        return (!enclaveCEKs.isEmpty()) ? enclaveProvider.getEnclavePackage(userSQL, enclaveCEKs) : null;
     }
 
     String getServerName() {
@@ -8005,7 +8051,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
  * 
  */
 final class SQLServerConnectionSecurityManager {
-    static final String dllName = SQLServerDriver.AUTH_DLL_NAME + ".dll";
+    static final String DLLNAME = SQLServerDriver.AUTH_DLL_NAME + ".dll";
     String serverName;
     int portNumber;
 
@@ -8021,7 +8067,7 @@ final class SQLServerConnectionSecurityManager {
      *         when an error occurs
      */
     @SuppressWarnings("deprecation")
-    public void checkConnect() throws SecurityException {
+    public void checkConnect() {
         SecurityManager security = System.getSecurityManager();
         if (null != security) {
             security.checkConnect(serverName, portNumber);
@@ -8035,10 +8081,10 @@ final class SQLServerConnectionSecurityManager {
      *         when an error occurs
      */
     @SuppressWarnings("deprecation")
-    public void checkLink() throws SecurityException {
+    public void checkLink() {
         SecurityManager security = System.getSecurityManager();
         if (null != security) {
-            security.checkLink(dllName);
+            security.checkLink(DLLNAME);
         }
     }
 }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java
index 5e4d933c..16864435 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java
@@ -41,7 +41,7 @@ class SQLServerConnectionPoolProxy implements ISQLServerConnection, java.io.Seri
     /**
      * Permission targets currently only callAbort is implemented
      */
-    private static final String callAbortPerm = "callAbort";
+    private static final String CALL_ABORT_PERM = "callAbort";
 
     /**
      * Generates the next unique connection id.
@@ -140,11 +140,11 @@ class SQLServerConnectionPoolProxy implements ISQLServerConnection, java.io.Seri
         SecurityManager secMgr = System.getSecurityManager();
         if (secMgr != null) {
             try {
-                java.sql.SQLPermission perm = new java.sql.SQLPermission(callAbortPerm);
+                java.sql.SQLPermission perm = new java.sql.SQLPermission(CALL_ABORT_PERM);
                 secMgr.checkPermission(perm);
             } catch (SecurityException ex) {
                 MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_permissionDenied"));
-                Object[] msgArgs = {callAbortPerm};
+                Object[] msgArgs = {CALL_ABORT_PERM};
                 throw new SQLServerException(form.format(msgArgs), null, 0, ex);
             }
         }
@@ -506,8 +506,7 @@ class SQLServerConnectionPoolProxy implements ISQLServerConnection, java.io.Seri
         try {
             t = iface.cast(this);
         } catch (ClassCastException e) {
-            SQLServerException newe = new SQLServerException(e.getMessage(), e);
-            throw newe;
+            throw new SQLServerException(e.getMessage(), e);
         }
         wrappedConnection.getConnectionLogger().exiting(toString(), "unwrap", t);
         return t;
@@ -661,4 +660,25 @@ class SQLServerConnectionPoolProxy implements ISQLServerConnection, java.io.Seri
     @Deprecated(since = "12.1.0", forRemoval = true)
     @Override
     public void setMsiTokenCacheTtl(int timeToLive) {}
+
+    /**
+     * Returns the fully qualified class name of the implementing class for {@link SQLServerAccessTokenCallback}.
+     *
+     * @return accessTokenCallbackClass
+     */
+    @Override
+    public String getAccessTokenCallbackClass() {
+        return wrappedConnection.getAccessTokenCallbackClass();
+    }
+
+    /**
+     * Sets 'accessTokenCallbackClass' to the fully qualified class name
+     * of the implementing class for {@link SQLServerAccessTokenCallback}.
+     *
+     * @param accessTokenCallbackClass
+     */
+    @Override
+    public void setAccessTokenCallbackClass(String accessTokenCallbackClass) {
+        wrappedConnection.setAccessTokenCallbackClass(accessTokenCallbackClass);
+    }
 }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java
index eeb9907a..b7e01811 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java
@@ -1165,9 +1165,9 @@ public class SQLServerDataSource
      */
     @Override
     @Deprecated(since = "9.4.1", forRemoval = true)
-    public void setAADSecurePrincipalId(String AADSecurePrincipalId) {
+    public void setAADSecurePrincipalId(String aadSecurePrincipalId) {
         setStringProperty(connectionProps, SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_ID.toString(),
-                AADSecurePrincipalId);
+                aadSecurePrincipalId);
     }
 
     /**
@@ -1175,9 +1175,9 @@ public class SQLServerDataSource
      */
     @Override
     @Deprecated(since = "9.4.1", forRemoval = true)
-    public void setAADSecurePrincipalSecret(String AADSecurePrincipalSecret) {
+    public void setAADSecurePrincipalSecret(String aadSecurePrincipalSecret) {
         setStringProperty(connectionProps, SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_SECRET.toString(),
-                AADSecurePrincipalSecret);
+                aadSecurePrincipalSecret);
     }
 
     @Override
@@ -1280,6 +1280,29 @@ public class SQLServerDataSource
                 SQLServerDriverObjectProperty.ACCESS_TOKEN_CALLBACK.getDefaultValue());
     }
 
+    /**
+     * Sets 'accessTokenCallbackClass' to the fully qualified class name
+     * of the implementing class for {@link SQLServerAccessTokenCallback}.
+     *
+     * @param accessTokenCallbackClass
+     */
+    @Override
+    public void setAccessTokenCallbackClass(String accessTokenCallbackClass) {
+        setStringProperty(connectionProps, SQLServerDriverStringProperty.ACCESS_TOKEN_CALLBACK_CLASS.toString(),
+                accessTokenCallbackClass);
+    }
+
+    /**
+     * Returns the fully qualified class name of the implementing class for {@link SQLServerAccessTokenCallback}.
+     *
+     * @return accessTokenCallbackClass
+     */
+    @Override
+    public String getAccessTokenCallbackClass() {
+        return getStringProperty(connectionProps, SQLServerDriverStringProperty.ACCESS_TOKEN_CALLBACK_CLASS.toString(),
+                null);
+    }
+
     /**
      * Sets a property string value.
      *
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java
index f722dbe1..2f539420 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java
@@ -43,7 +43,7 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
     /** connection */
     private SQLServerConnection connection;
 
-    static final String urlprefix = "jdbc:sqlserver://";
+    static final String URL_PREFIX = "jdbc:sqlserver://";
 
     static final private java.util.logging.Logger logger = java.util.logging.Logger
             .getLogger("com.microsoft.sqlserver.jdbc.internals.SQLServerDatabaseMetaData");
@@ -62,7 +62,7 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
     // varbinary(max) https://msdn.microsoft.com/en-us/library/ms143432.aspx
     static final int MAXLOBSIZE = 2147483647;
     // uniqueidentifier https://msdn.microsoft.com/en-us/library/ms187942.aspx
-    static final int uniqueidentifierSize = 36;
+    static final int UNIQUEIDENTIFIER_SIZE = 36;
 
     enum CallableHandles {
         SP_COLUMNS("{ call sp_columns(?, ?, ?, ?, ?) }", "{ call sp_columns_100(?, ?, ?, ?, ?, ?) }"),
@@ -157,8 +157,7 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
 
     @Override
     public boolean isWrapperFor(Class<?> iface) throws SQLException {
-        boolean f = iface.isInstance(this);
-        return f;
+        return iface.isInstance(this);
     }
 
     @Override
@@ -278,8 +277,7 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
      * @return Resultset from the execution
      * @throws SQLTimeoutException
      */
-    private SQLServerResultSet getResultSetFromInternalQueries(String catalog,
-            String query) throws SQLException, SQLTimeoutException {
+    private SQLServerResultSet getResultSetFromInternalQueries(String catalog, String query) throws SQLException {
         checkClosed();
         String orgCat = null;
         orgCat = switchCatalogs(catalog);
@@ -301,26 +299,26 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
      */
     private CallableStatement getCallableStatementHandle(CallableHandles request,
             String catalog) throws SQLServerException {
-        CallableStatement CS = null;
+        CallableStatement cs = null;
         HandleAssociation hassoc = handleMap.get(request);
         try {
             if (null == hassoc) {
-                CS = request.prepare(connection);
+                cs = request.prepare(connection);
                 hassoc = new HandleAssociation();
-                hassoc.addToMap(catalog, CS);
+                hassoc.addToMap(catalog, cs);
             } else { // hassoc != null
-                CS = hassoc.getMappedStatement(catalog);
+                cs = hassoc.getMappedStatement(catalog);
                 // No Cached Statement yet
-                if (null == CS || CS.isClosed()) {
-                    CS = request.prepare(connection);
-                    hassoc.addToMap(catalog, CS);
+                if (null == cs || cs.isClosed()) {
+                    cs = request.prepare(connection);
+                    hassoc.addToMap(catalog, cs);
                 }
             }
             handleMap.put(request, hassoc);
         } catch (SQLException e) {
-            SQLServerException.makeFromDriverError(connection, CS, e.toString(), null, false);
+            SQLServerException.makeFromDriverError(connection, cs, e.toString(), null, false);
         }
-        return CS;
+        return cs;
     }
 
     /**
@@ -464,7 +462,7 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
     }
 
     @Override
-    public java.sql.ResultSet getCatalogs() throws SQLException, SQLTimeoutException {
+    public java.sql.ResultSet getCatalogs() throws SQLException {
         if (loggerExternal.isLoggable(Level.FINER) && Util.isActivityTraceOn()) {
             loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getNext().toString());
         }
@@ -499,7 +497,7 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
         }
         checkClosed();
         // column_privileges supports columns being escaped.
-        col = EscapeIDName(col);
+        col = escapeIDName(col);
         /*
          * sp_column_privileges [ @table_name = ] 'table_name' [ , [ @table_owner = ] 'table_owner' ] [ ,
          * [ @table_qualifier = ] 'table_qualifier' ] [ , [ @column_name = ] 'column' ]
@@ -519,15 +517,15 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
 
     @Override
     public java.sql.ResultSet getTables(String catalog, String schema, String table,
-            String types[]) throws SQLServerException, SQLTimeoutException {
+            String[] types) throws SQLServerException, SQLTimeoutException {
         if (loggerExternal.isLoggable(Level.FINER) && Util.isActivityTraceOn()) {
             loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getNext().toString());
         }
         checkClosed();
 
         // sp_tables supports table name and owner ie schema escaped.
-        table = EscapeIDName(table);
-        schema = EscapeIDName(schema);
+        table = escapeIDName(table);
+        schema = escapeIDName(schema);
         /*
          * sp_tables [ [ @table_name = ] 'name' ] [ , [ @table_owner = ] 'owner' ] [ , [ @table_qualifier = ]
          * 'qualifier' ] [ , [ @table_type = ] "type" ]
@@ -556,7 +554,7 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
     static final char ESCAPE = '\\';
     static final char PERCENT = '%';
     static final char UNDERSCORE = '_';
-    static final char DOUBLE_RIGHT_BRACKET[] = {']', ']'};
+    static final char[] DOUBLE_RIGHT_BRACKET = {']', ']'};
 
     /**
      * Accepts a SQL identifier (such as a column name or table name) and escapes the identifier so sql 92 wild card
@@ -566,7 +564,7 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
      * @inID input identifier to escape.
      * @return the escaped value.
      */
-    private static String EscapeIDName(String inID) throws SQLServerException {
+    private static String escapeIDName(String inID) {
         if (null == inID)
             return inID;
         // SQL bracket escaping rules.
@@ -645,10 +643,10 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
             PreparedStatement pstmt = (SQLServerPreparedStatement) this.connection.prepareStatement(spColumnsSql);
             pstmt.closeOnCompletion();
             try {
-                pstmt.setString(1, (null != table && !table.isEmpty()) ? EscapeIDName(table) : "%");
-                pstmt.setString(2, (null != schema && !schema.isEmpty()) ? EscapeIDName(schema) : "%");
+                pstmt.setString(1, (null != table && !table.isEmpty()) ? escapeIDName(table) : "%");
+                pstmt.setString(2, (null != schema && !schema.isEmpty()) ? escapeIDName(schema) : "%");
                 pstmt.setString(3, (null != catalog && !catalog.isEmpty()) ? catalog : this.connection.getCatalog());
-                pstmt.setString(4, (null != col && !col.isEmpty()) ? EscapeIDName(col) : "%");
+                pstmt.setString(4, (null != col && !col.isEmpty()) ? escapeIDName(col) : "%");
                 pstmt.setInt(5, 2);// show sparse columns
                 pstmt.setInt(6, 3);// odbc version
 
@@ -729,11 +727,11 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
 
             try (PreparedStatement storedProcPstmt = this.connection
                     .prepareStatement("EXEC sp_columns_100 ?,?,?,?,?,?;")) {
-                storedProcPstmt.setString(1, (null != table && !table.isEmpty()) ? EscapeIDName(table) : "%");
-                storedProcPstmt.setString(2, (null != schema && !schema.isEmpty()) ? EscapeIDName(schema) : "%");
+                storedProcPstmt.setString(1, (null != table && !table.isEmpty()) ? escapeIDName(table) : "%");
+                storedProcPstmt.setString(2, (null != schema && !schema.isEmpty()) ? escapeIDName(schema) : "%");
                 storedProcPstmt.setString(3,
                         (null != catalog && !catalog.isEmpty()) ? catalog : this.connection.getCatalog());
-                storedProcPstmt.setString(4, (null != col && !col.isEmpty()) ? EscapeIDName(col) : "%");
+                storedProcPstmt.setString(4, (null != col && !col.isEmpty()) ? escapeIDName(col) : "%");
                 storedProcPstmt.setInt(5, 2);// show sparse columns
                 storedProcPstmt.setInt(6, 3);// odbc version
 
@@ -839,8 +837,8 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
         }
 
         String[] arguments = new String[3];
-        arguments[0] = EscapeIDName(functionNamePattern);
-        arguments[1] = EscapeIDName(schemaPattern);
+        arguments[0] = escapeIDName(functionNamePattern);
+        arguments[1] = escapeIDName(schemaPattern);
         arguments[2] = catalog;
         return getResultSetWithProvidedColumnNames(catalog, CallableHandles.SP_STORED_PROCEDURES, arguments,
                 getFunctionsColumnNames);
@@ -871,12 +869,12 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
         String[] arguments = new String[5];
 
         // proc name supports escaping
-        arguments[0] = EscapeIDName(functionNamePattern);
+        arguments[0] = escapeIDName(functionNamePattern);
         // schema name supports escaping.
-        arguments[1] = EscapeIDName(schemaPattern);
+        arguments[1] = escapeIDName(schemaPattern);
         arguments[2] = catalog;
         // col name supports escaping
-        arguments[3] = EscapeIDName(columnNamePattern);
+        arguments[3] = escapeIDName(columnNamePattern);
         arguments[4] = "3";
         SQLServerResultSet rs = getResultSetWithProvidedColumnNames(catalog, CallableHandles.SP_SPROC_COLUMNS,
                 arguments, getFunctionsColumnsColumnNames);
@@ -1353,12 +1351,12 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
         String[] arguments = new String[5];
 
         // proc name supports escaping
-        proc = EscapeIDName(proc);
+        proc = escapeIDName(proc);
         arguments[0] = proc;
         arguments[1] = schema;
         arguments[2] = catalog;
         // col name supports escaping
-        col = EscapeIDName(col);
+        col = escapeIDName(col);
         arguments[3] = col;
         arguments[4] = "3";
         SQLServerResultSet rs = getResultSetWithProvidedColumnNames(catalog, CallableHandles.SP_SPROC_COLUMNS,
@@ -1395,7 +1393,7 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
          * 'qualifier' ] [ , [@fUsePattern = ] 'fUsePattern' ]
          */
         String[] arguments = new String[3];
-        arguments[0] = EscapeIDName(proc);
+        arguments[0] = escapeIDName(proc);
         arguments[1] = schema;
         arguments[2] = catalog;
         return getResultSetWithProvidedColumnNames(catalog, CallableHandles.SP_STORED_PROCEDURES, arguments,
@@ -1587,8 +1585,8 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
             loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getNext().toString());
         }
         checkClosed();
-        table = EscapeIDName(table);
-        schema = EscapeIDName(schema);
+        table = escapeIDName(table);
+        schema = escapeIDName(schema);
         /*
          * sp_table_privileges [ @table_name = ] 'table_name' [ , [ @table_owner = ] 'table_owner' ] [ ,
          * [ @table_qualifier = ] 'table_qualifier' ] [ , [@fUsePattern =] 'fUsePattern']
@@ -1619,7 +1617,7 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
     }
 
     @Override
-    public java.sql.ResultSet getTypeInfo() throws SQLException, SQLTimeoutException {
+    public java.sql.ResultSet getTypeInfo() throws SQLException {
         if (loggerExternal.isLoggable(Level.FINER) && Util.isActivityTraceOn()) {
             loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getNext().toString());
         }
@@ -1667,7 +1665,7 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
                     && !name.equals(SQLServerDriverStringProperty.TRUST_STORE_PASSWORD.toString())) {
                 String val = info[index].value;
                 // skip empty strings
-                if (0 != val.length()) {
+                if (null != val && 0 != val.length()) {
                     // special case these server name, instance name and port
                     // number as these go in the front
                     if (name.equals(SQLServerDriverStringProperty.SERVER_NAME.toString())) {
@@ -1700,7 +1698,7 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
         }
         url.insert(0, serverName);
 
-        url.insert(0, urlprefix); // insert the prefix at the front.
+        url.insert(0, URL_PREFIX); // insert the prefix at the front.
         return (url.toString());
     }
 
@@ -2366,7 +2364,7 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
 
     @Override
     public java.sql.ResultSet getUDTs(String catalog, String schemaPattern, String typeNamePattern,
-            int[] types) throws SQLException, SQLTimeoutException {
+            int[] types) throws SQLException {
         if (loggerExternal.isLoggable(Level.FINER) && Util.isActivityTraceOn()) {
             loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getNext().toString());
         }
@@ -2468,7 +2466,7 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
 
     @Override
     public ResultSet getAttributes(String catalog, String schemaPattern, String typeNamePattern,
-            String attributeNamePattern) throws SQLException, SQLTimeoutException {
+            String attributeNamePattern) throws SQLException {
         if (loggerExternal.isLoggable(Level.FINER) && Util.isActivityTraceOn()) {
             loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getNext().toString());
         }
@@ -2498,8 +2496,7 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
     }
 
     @Override
-    public ResultSet getSuperTables(String catalog, String schemaPattern,
-            String tableNamePattern) throws SQLException, SQLTimeoutException {
+    public ResultSet getSuperTables(String catalog, String schemaPattern, String tableNamePattern) throws SQLException {
         if (loggerExternal.isLoggable(Level.FINER) && Util.isActivityTraceOn()) {
             loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getNext().toString());
         }
@@ -2512,8 +2509,7 @@ public final class SQLServerDatabaseMetaData implements java.sql.DatabaseMetaDat
     }
 
     @Override
-    public ResultSet getSuperTypes(String catalog, String schemaPattern,
-            String typeNamePattern) throws SQLException, SQLTimeoutException {
+    public ResultSet getSuperTypes(String catalog, String schemaPattern, String typeNamePattern) throws SQLException {
         if (loggerExternal.isLoggable(Level.FINER) && Util.isActivityTraceOn()) {
             loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getNext().toString());
         }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
index 95b0ebca..163fb388 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
@@ -605,7 +605,8 @@ enum SQLServerDriverStringProperty {
     MAX_RESULT_BUFFER("maxResultBuffer", "-1"),
     ENCRYPT("encrypt", EncryptOption.TRUE.toString()),
     SERVER_CERTIFICATE("serverCertificate", ""),
-    DATETIME_DATATYPE("datetimeParameterType", DatetimeType.DATETIME2.toString());
+    DATETIME_DATATYPE("datetimeParameterType", DatetimeType.DATETIME2.toString()),
+    ACCESS_TOKEN_CALLBACK_CLASS("accessTokenCallbackClass", "");
 
     private final String name;
     private final String defaultValue;
@@ -833,6 +834,12 @@ public final class SQLServerDriver implements java.sql.Driver {
                     SQLServerDriverStringProperty.TRUST_MANAGER_CLASS.getDefaultValue(), false, null),
             new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.TRUST_MANAGER_CONSTRUCTOR_ARG.toString(),
                     SQLServerDriverStringProperty.TRUST_MANAGER_CONSTRUCTOR_ARG.getDefaultValue(), false, null),
+            // Callback needs to be in list despite not being settable within connection string.
+            // The reason for this is for calls to mergeURLAndSuppliedProperties to work when setting the callback.
+            new SQLServerDriverPropertyInfo(SQLServerDriverObjectProperty.ACCESS_TOKEN_CALLBACK.toString(),
+                    SQLServerDriverObjectProperty.ACCESS_TOKEN_CALLBACK.getDefaultValue(), false, null),
+            new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.ACCESS_TOKEN_CALLBACK_CLASS.toString(),
+                    SQLServerDriverStringProperty.ACCESS_TOKEN_CALLBACK_CLASS.getDefaultValue(), false, null),
             new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.REPLICATION.toString(),
                     Boolean.toString(SQLServerDriverBooleanProperty.REPLICATION.getDefaultValue()), false, TRUE_FALSE),
             new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.SEND_TIME_AS_DATETIME.toString(),
@@ -931,19 +938,21 @@ public final class SQLServerDriver implements java.sql.Driver {
     private static final SQLServerDriverPropertyInfo[] DRIVER_PROPERTIES_PROPERTY_ONLY = {
             // default required available choices
             // property name value property (if appropriate)
+            new SQLServerDriverPropertyInfo(SQLServerDriverObjectProperty.ACCESS_TOKEN_CALLBACK.toString(),
+                    SQLServerDriverObjectProperty.ACCESS_TOKEN_CALLBACK.getDefaultValue(), false, null),
             new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.ACCESS_TOKEN.toString(),
                     SQLServerDriverStringProperty.ACCESS_TOKEN.getDefaultValue(), false, null),
             new SQLServerDriverPropertyInfo(SQLServerDriverObjectProperty.GSS_CREDENTIAL.toString(),
                     SQLServerDriverObjectProperty.GSS_CREDENTIAL.getDefaultValue(), false, null),};
 
-    private static final String driverPropertiesSynonyms[][] = {
+    private static final String[][] driverPropertiesSynonyms = {
             {"database", SQLServerDriverStringProperty.DATABASE_NAME.toString()},
             {"userName", SQLServerDriverStringProperty.USER.toString()},
             {"server", SQLServerDriverStringProperty.SERVER_NAME.toString()},
             {"domainName", SQLServerDriverStringProperty.DOMAIN.toString()},
             {"port", SQLServerDriverIntProperty.PORT_NUMBER.toString()}};
 
-    private static final String driverPropertyValuesSynonyms[][] = {
+    private static final String[][] driverPropertyValuesSynonyms = {
             {"ActiveDirectoryMSI", SqlAuthentication.ACTIVE_DIRECTORY_MANAGED_IDENTITY.toString()}};
 
     static private final AtomicInteger baseID = new AtomicInteger(0); // Unique id generator for each instance (used for
@@ -1074,6 +1083,9 @@ public final class SQLServerDriver implements java.sql.Driver {
                     fixedup.setProperty(newname, val);
                 } else if ("gsscredential".equalsIgnoreCase(newname) && (props.get(name) instanceof GSSCredential)) {
                     fixedup.put(newname, props.get(name));
+                } else if ("accessTokenCallback".equalsIgnoreCase(newname)
+                        && (props.get(name) instanceof SQLServerAccessTokenCallback)) {
+                    fixedup.put(newname, props.get(name));
                 } else {
                     MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidpropertyValue"));
                     Object[] msgArgs = {name};
@@ -1202,7 +1214,7 @@ public final class SQLServerDriver implements java.sql.Driver {
             "java.vm.specification.vendor", "java.vm.specification.version", "os.name", "os.version", "os.arch"};
 
     @Override
-    public java.sql.Connection connect(String Url, Properties suppliedProperties) throws SQLServerException {
+    public java.sql.Connection connect(String url, Properties suppliedProperties) throws SQLServerException {
         loggerExternal.entering(getClassNameLogging(), "connect", "Arguments not traced.");
         SQLServerConnection result = null;
 
@@ -1223,7 +1235,7 @@ public final class SQLServerDriver implements java.sql.Driver {
         }
 
         // Merge connectProperties (from URL) and supplied properties from user.
-        Properties connectProperties = parseAndMergeProperties(Url, suppliedProperties);
+        Properties connectProperties = parseAndMergeProperties(url, suppliedProperties);
         if (connectProperties != null) {
             result = DriverJDBCVersion.getSQLServerConnection(toString());
             result.connect(connectProperties, null);
@@ -1232,13 +1244,13 @@ public final class SQLServerDriver implements java.sql.Driver {
         return result;
     }
 
-    private Properties parseAndMergeProperties(String Url, Properties suppliedProperties) throws SQLServerException {
-        if (Url == null) {
+    private Properties parseAndMergeProperties(String url, Properties suppliedProperties) throws SQLServerException {
+        if (url == null) {
             throw new SQLServerException(null, SQLServerException.getErrString("R_nullConnection"), null, 0, false);
         }
 
         // Pull the URL properties into the connection properties
-        Properties connectProperties = Util.parseUrl(Url, drLogger);
+        Properties connectProperties = Util.parseUrl(url, drLogger);
         if (null == connectProperties)
             return null; // If we are the wrong driver dont throw an exception
 
@@ -1277,10 +1289,10 @@ public final class SQLServerDriver implements java.sql.Driver {
     }
 
     @Override
-    public DriverPropertyInfo[] getPropertyInfo(String Url, Properties Info) throws SQLServerException {
+    public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLServerException {
         loggerExternal.entering(getClassNameLogging(), "getPropertyInfo", "Arguments not traced.");
 
-        Properties connProperties = parseAndMergeProperties(Url, Info);
+        Properties connProperties = parseAndMergeProperties(url, info);
         // This means we are not the right driver throw an exception.
         if (null == connProperties)
             throw new SQLServerException(null, SQLServerException.getErrString("R_invalidConnection"), null, 0, false);
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java
index 92c51a1f..bfee69c2 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java
@@ -66,7 +66,7 @@ public final class SQLServerException extends java.sql.SQLException {
     static final int LOGON_FAILED = 18456;
     static final int PASSWORD_EXPIRED = 18488;
     static final int USER_ACCOUNT_LOCKED = 18486;
-    static java.util.logging.Logger exLogger = java.util.logging.Logger
+    static final java.util.logging.Logger exLogger = java.util.logging.Logger
             .getLogger("com.microsoft.sqlserver.jdbc.internals.SQLServerException");
 
     // Facility for driver-specific error codes
@@ -117,13 +117,13 @@ public final class SQLServerException extends java.sql.SQLException {
             exLogger.fine("*** SQLException:" + id + " " + this.toString() + " " + errText);
         if (bStack && exLogger.isLoggable(Level.FINE)) {
             StringBuilder sb = new StringBuilder(100);
-            StackTraceElement st[] = this.getStackTrace();
+            StackTraceElement[] st = this.getStackTrace();
             for (StackTraceElement aSt : st)
                 sb.append(aSt.toString());
             Throwable t = this.getCause();
             if (t != null) {
                 sb.append("\n caused by ").append(t).append("\n");
-                StackTraceElement tst[] = t.getStackTrace();
+                StackTraceElement[] tst = t.getStackTrace();
                 for (StackTraceElement aTst : tst)
                     sb.append(aTst.toString());
             }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerFMTQuery.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerFMTQuery.java
index 5a458430..8a4c2463 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerFMTQuery.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerFMTQuery.java
@@ -105,7 +105,7 @@ class SQLServerFMTQuery {
                 lexer.removeErrorListeners();
                 lexer.addErrorListener(new SQLServerErrorListener());
                 this.tokenList = (ArrayList<? extends Token>) lexer.getAllTokens();
-                if (tokenList.size() <= 0) {
+                if (tokenList.isEmpty()) {
                     SQLServerException.makeFromDriverError(null, this,
                             SQLServerResource.getResource("R_noTokensFoundInUserQuery"), null, false);
                 }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java
index e4fa77a1..c2b3226a 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java
@@ -34,7 +34,6 @@ import com.microsoft.aad.msal4j.SilentParameters;
 import com.microsoft.aad.msal4j.SystemBrowserOptions;
 import com.microsoft.aad.msal4j.UserNamePasswordParameters;
 import com.microsoft.sqlserver.jdbc.SQLServerConnection.ActiveDirectoryAuthentication;
-
 import com.microsoft.sqlserver.jdbc.SQLServerConnection.SqlFedAuthInfo;
 
 
@@ -58,7 +57,9 @@ class SQLServerMSAL4JUtils {
         try {
             final PublicClientApplication pca = PublicClientApplication
                     .builder(ActiveDirectoryAuthentication.JDBC_FEDAUTH_CLIENT_ID).executorService(executorService)
+                    .setTokenCacheAccessAspect(PersistentTokenCacheAccessAspect.getInstance())
                     .authority(fedAuthInfo.stsurl).build();
+
             final CompletableFuture<IAuthenticationResult> future = pca.acquireToken(UserNamePasswordParameters
                     .builder(Collections.singleton(fedAuthInfo.spn + SLASH_DEFAULT), user, password.toCharArray())
                     .build());
@@ -93,8 +94,10 @@ class SQLServerMSAL4JUtils {
             scopes.add(scope);
             IClientCredential credential = ClientCredentialFactory.createFromSecret(aadPrincipalSecret);
             ConfidentialClientApplication clientApplication = ConfidentialClientApplication
-                    .builder(aadPrincipalID, credential).executorService(executorService).authority(fedAuthInfo.stsurl)
-                    .build();
+                    .builder(aadPrincipalID, credential).executorService(executorService)
+                    .setTokenCacheAccessAspect(PersistentTokenCacheAccessAspect.getInstance())
+                    .authority(fedAuthInfo.stsurl).build();
+
             final CompletableFuture<IAuthenticationResult> future = clientApplication
                     .acquireToken(ClientCredentialParameters.builder(scopes).build());
             final IAuthenticationResult authenticationResult = future.get();
@@ -134,7 +137,9 @@ class SQLServerMSAL4JUtils {
 
             final PublicClientApplication pca = PublicClientApplication
                     .builder(ActiveDirectoryAuthentication.JDBC_FEDAUTH_CLIENT_ID).executorService(executorService)
+                    .setTokenCacheAccessAspect(PersistentTokenCacheAccessAspect.getInstance())
                     .authority(fedAuthInfo.stsurl).build();
+
             final CompletableFuture<IAuthenticationResult> future = pca
                     .acquireToken(IntegratedWindowsAuthenticationParameters
                             .builder(Collections.singleton(fedAuthInfo.spn + SLASH_DEFAULT), user).build());
@@ -166,7 +171,7 @@ class SQLServerMSAL4JUtils {
             PublicClientApplication pca = PublicClientApplication
                     .builder(ActiveDirectoryAuthentication.JDBC_FEDAUTH_CLIENT_ID).executorService(executorService)
                     .setTokenCacheAccessAspect(PersistentTokenCacheAccessAspect.getInstance())
-                    .authority(fedAuthInfo.stsurl).logPii((logger.isLoggable(Level.FINE))).build();
+                    .authority(fedAuthInfo.stsurl).build();
 
             CompletableFuture<IAuthenticationResult> future = null;
             IAuthenticationResult authenticationResult = null;
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerNoneEnclaveProvider.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerNoneEnclaveProvider.java
index c0db8ed3..642d3e89 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerNoneEnclaveProvider.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerNoneEnclaveProvider.java
@@ -134,10 +134,9 @@ public class SQLServerNoneEnclaveProvider implements ISQLServerEnclaveProvider {
                     }
                 }
             }
+        } catch (SQLServerException e) {
+            throw (SQLServerException) e;
         } catch (SQLException | IOException e) {
-            if (e instanceof SQLServerException) {
-                throw (SQLServerException) e;
-            }
             throw new SQLServerException(SQLServerException.getErrString("R_UnableRetrieveParameterMetadata"), null, 0,
                     e);
         }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParameterMetaData.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParameterMetaData.java
index 139c9a10..56fce55c 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParameterMetaData.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParameterMetaData.java
@@ -161,7 +161,7 @@ public final class SQLServerParameterMetaData implements ParameterMetaData {
                         } else if (SSType.IMAGE == ssType) {
                             qm.precision = SQLServerDatabaseMetaData.MAXLOBSIZE;
                         } else if (SSType.GUID == ssType) {
-                            qm.precision = SQLServerDatabaseMetaData.uniqueidentifierSize;
+                            qm.precision = SQLServerDatabaseMetaData.UNIQUEIDENTIFIER_SIZE;
                         } else if (SSType.TIMESTAMP == ssType) {
                             qm.precision = 8;
                         } else if (SSType.XML == ssType) {
@@ -449,7 +449,7 @@ public final class SQLServerParameterMetaData implements ParameterMetaData {
             return queryMetaMap.size();
         } else {
             // Row 1 is Return Type metadata
-            return (procMetadata.size() == 0 ? 0 : procMetadata.size() - 1);
+            return (procMetadata.isEmpty() ? 0 : procMetadata.size() - 1);
         }
     }
 
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParser.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParser.java
index bdd308d4..8b4c431e 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParser.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParser.java
@@ -95,7 +95,7 @@ final class SQLServerParser {
                                 }
                                 for (int i = 0; i < ls.size(); i++) {
                                     if ("?".equalsIgnoreCase(ls.get(i))) {
-                                        if (0 == tableValues.size()) {
+                                        if (tableValues.isEmpty()) {
                                             query.getColumns().add("?");
                                         } else {
                                             if (i < tableValues.size()) {
@@ -201,7 +201,7 @@ final class SQLServerParser {
             SQLServerLexer.OR_ASSIGN, SQLServerLexer.STAR, SQLServerLexer.DIVIDE, SQLServerLexer.MODULE,
             SQLServerLexer.PLUS, SQLServerLexer.MINUS, SQLServerLexer.LIKE, SQLServerLexer.IN, SQLServerLexer.BETWEEN);
 
-    static String findColumnAroundParameter(SQLServerTokenIterator iter) throws SQLServerException {
+    static String findColumnAroundParameter(SQLServerTokenIterator iter) {
         int index = iter.nextIndex();
         iter.previous();
         String value = findColumnBeforeParameter(iter);
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java
index aeef166b..e06979ec 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java
@@ -233,7 +233,7 @@ public class SQLServerPooledConnection implements PooledConnection, Serializable
                 // use internal close so there wont be an event due to us closing the connection, if not closed already.
                 lastProxyConnection.internalClose();
             if (null != physicalConnection) {
-                physicalConnection.DetachFromPool();
+                physicalConnection.detachFromPool();
                 physicalConnection.close();
             }
             physicalConnection = null;
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java
index d0ef10e4..4743407b 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java
@@ -61,7 +61,7 @@ public class SQLServerPreparedStatement extends SQLServerStatement implements IS
     private static final String ACTIVITY_ID = " ActivityId: ";
 
     /** batch statement delimiter */
-    final int nBatchStatementDelimiter = BATCH_STATEMENT_DELIMITER_TDS_72;
+    static final int NBATCH_STATEMENT_DELIMITER = BATCH_STATEMENT_DELIMITER_TDS_72;
 
     /** The prepared type definitions */
     private String preparedTypeDefinitions;
@@ -2087,7 +2087,7 @@ public class SQLServerPreparedStatement extends SQLServerStatement implements IS
                             SQLServerResultSet rs = stmt
                                     .executeQueryInternal("sp_executesql N'SET FMTONLY ON SELECT * FROM "
                                             + Util.escapeSingleQuotes(tableName) + " '")) {
-                        if (null != columnList && columnList.size() > 0) {
+                        if (null != columnList && !columnList.isEmpty()) {
                             if (columnList.size() != valueList.size()) {
 
                                 MessageFormat form = new MessageFormat(
@@ -2251,7 +2251,7 @@ public class SQLServerPreparedStatement extends SQLServerStatement implements IS
                             SQLServerResultSet rs = stmt
                                     .executeQueryInternal("sp_executesql N'SET FMTONLY ON SELECT * FROM "
                                             + Util.escapeSingleQuotes(tableName) + " '")) {
-                        if (null != columnList && columnList.size() > 0) {
+                        if (null != columnList && !columnList.isEmpty()) {
                             if (columnList.size() != valueList.size()) {
                                 MessageFormat form = new MessageFormat(
                                         SQLServerException.getErrString("R_colNotMatchTable"));
@@ -2899,7 +2899,7 @@ public class SQLServerPreparedStatement extends SQLServerStatement implements IS
 
                     if (numBatchesExecuted < numBatchesPrepared) {
                         // assert null != tdsWriter;
-                        tdsWriter.writeByte((byte) nBatchStatementDelimiter);
+                        tdsWriter.writeByte((byte) NBATCH_STATEMENT_DELIMITER);
                     } else {
                         resetForReexecute();
                         tdsWriter = batchCommand.startRequest(TDS.PKT_RPC);
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
index 48900ada..c936a572 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
@@ -240,6 +240,8 @@ public final class SQLServerResource extends ListResourceBundle {
         {"R_delayLoadingLobsPropertyDescription", "Boolean flag which indicates whether the driver will load LOB datatypes into memory."},
         {"R_AADSecurePrincipalIdPropertyDescription", "The Application Id of a registered application which has been granted permission to the database connected."},
         {"R_AADSecurePrincipalSecretPropertyDescription", "A Secret defined for a registered application which has been granted permission to the database connected."},
+        {"R_accessTokenCallbackClassPropertyDescription", "The class to instantiate as the SQLServerAccessTokenCallback for acquiring tokens."},
+        {"R_accessTokenCallbackPropertyDescription", "A SQLServerAccessTokenCallback object which is used to call a callback method to return an access token."},
         {"R_noParserSupport", "An error occurred while instantiating the required parser. Error: \"{0}\""},
         {"R_writeOnlyXML", "Cannot read from this SQLXML instance. This instance is for writing data only."},
         {"R_dataHasBeenReadXML", "Cannot read from this SQLXML instance. The data has already been read."},
@@ -414,6 +416,7 @@ public final class SQLServerResource extends ListResourceBundle {
         {"R_AECertHashEmpty", "Internal error. Empty certificate thumbprint specified in certificate path {0}."},
         {"R_AECertNotFound", "Certificate with thumbprint {2} not found in certificate store {1} in certificate location {0}. Verify the certificate path in the column master key definition in the database is correct, and the certificate has been imported correctly into the certificate location/store."},
         {"R_AEMaloc", "Memory allocation failure."},
+        {"R_InvalidAccessTokenCallbackClass", "Invalid accessTokenCallbackClass: {0}"},
         {"R_AEKeypathLong", "Internal error. Specified certificate path has {0} bytes, which exceeds maximum length of {1} bytes."},
         {"R_AEECEKLenBad", "The specified encrypted column encryption key''s ciphertext length: {0} does not match the ciphertext length: {1} when using column master key (certificate) in \"{2}\". The encrypted column encryption key may be corrupt, or the specified certificate path may be incorrect."},
         {"R_AEECEKSigLenBad", "The specified encrypted column encryption key''s signature length {0} does not match the length {1} when using the column master key (certificate) in \"{2}\". The encrypted column encryption key may be corrupt, or the specified certificate path may be incorrect."},
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java
index aceb8fe6..cc82f40d 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java
@@ -5503,7 +5503,7 @@ public class SQLServerResultSet implements ISQLServerResultSet, java.io.Serializ
         final void ensureStartMark() {
             if (null == startMark && !isForwardOnly()) {
                 if (logger.isLoggable(java.util.logging.Level.FINEST))
-                    logger.finest(toString() + " Setting fetch buffer start mark");
+                    logger.finest(super.toString() + " Setting fetch buffer start mark");
 
                 startMark = tdsReader.mark();
             }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSecurityUtility.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSecurityUtility.java
index 26df5bb5..9f8755f9 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSecurityUtility.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSecurityUtility.java
@@ -49,7 +49,7 @@ class SQLServerSecurityUtility {
     private SQLServerSecurityUtility() {
         throw new UnsupportedOperationException(SQLServerException.getErrString("R_notSupported"));
     }
-    
+
     /**
      * Give the hash of given plain text
      * 
@@ -132,13 +132,11 @@ class SQLServerSecurityUtility {
         Boolean[] hasEntry = new Boolean[1];
         List<String> trustedKeyPaths = SQLServerConnection.getColumnEncryptionTrustedMasterKeyPaths(serverName,
                 hasEntry);
-        if (hasEntry[0]) {
-            if ((null == trustedKeyPaths) || (0 == trustedKeyPaths.size())
-                    || (!trustedKeyPaths.contains(keyInfo.keyPath))) {
-                MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_UntrustedKeyPath"));
-                Object[] msgArgs = {keyInfo.keyPath, serverName};
-                throw new SQLServerException(null, form.format(msgArgs), null, 0, false);
-            }
+        if (hasEntry[0] && ((null == trustedKeyPaths) || (trustedKeyPaths.isEmpty())
+                || (!trustedKeyPaths.contains(keyInfo.keyPath)))) {
+            MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_UntrustedKeyPath"));
+            Object[] msgArgs = {keyInfo.keyPath, serverName};
+            throw new SQLServerException(null, form.format(msgArgs), null, 0, false);
         }
 
         SQLServerException lastException = null;
@@ -302,12 +300,11 @@ class SQLServerSecurityUtility {
         Boolean[] hasEntry = new Boolean[1];
         List<String> trustedKeyPaths = SQLServerConnection.getColumnEncryptionTrustedMasterKeyPaths(serverName,
                 hasEntry);
-        if (hasEntry[0]) {
-            if ((null == trustedKeyPaths) || (0 == trustedKeyPaths.size()) || (!trustedKeyPaths.contains(keyPath))) {
-                MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_UntrustedKeyPath"));
-                Object[] msgArgs = {keyPath, serverName};
-                throw new SQLServerException(form.format(msgArgs), null);
-            }
+        if (hasEntry[0]
+                && ((null == trustedKeyPaths) || (trustedKeyPaths.isEmpty()) || (!trustedKeyPaths.contains(keyPath)))) {
+            MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_UntrustedKeyPath"));
+            Object[] msgArgs = {keyPath, serverName};
+            throw new SQLServerException(form.format(msgArgs), null);
         }
 
         SQLServerColumnEncryptionKeyStoreProvider provider = null;
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSpatialDatatype.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSpatialDatatype.java
index 7b891b5c..31f6eb95 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSpatialDatatype.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSpatialDatatype.java
@@ -1775,7 +1775,7 @@ abstract class SQLServerSpatialDatatype {
      * Populates the various data structures contained within the Geometry/Geography instance.
      */
     void populateStructures() {
-        if (pointList.size() > 0) {
+        if (!pointList.isEmpty()) {
             xValues = new double[pointList.size()];
             yValues = new double[pointList.size()];
 
@@ -1808,7 +1808,7 @@ abstract class SQLServerSpatialDatatype {
             }
         }
 
-        if (figureList.size() > 0) {
+        if (!figureList.isEmpty()) {
             figures = new Figure[figureList.size()];
 
             for (int i = 0; i < figureList.size(); i++) {
@@ -1821,11 +1821,11 @@ abstract class SQLServerSpatialDatatype {
         // know until
         // We've parsed through the entire WKT and confirmed that there are 0 points.
         // Therefore, if so, we make the figure offset of the first shape to be -1.
-        if (pointList.size() == 0 && shapeList.size() > 0 && shapeList.get(0).getOpenGISType() == 7) {
+        if (pointList.isEmpty() && !shapeList.isEmpty() && shapeList.get(0).getOpenGISType() == 7) {
             shapeList.get(0).setFigureOffset(-1);
         }
 
-        if (shapeList.size() > 0) {
+        if (!shapeList.isEmpty()) {
             shapes = new Shape[shapeList.size()];
 
             for (int i = 0; i < shapeList.size(); i++) {
@@ -1833,7 +1833,7 @@ abstract class SQLServerSpatialDatatype {
             }
         }
 
-        if (segmentList.size() > 0) {
+        if (!segmentList.isEmpty()) {
             segments = new Segment[segmentList.size()];
 
             for (int i = 0; i < segmentList.size(); i++) {
@@ -2600,7 +2600,6 @@ abstract class SQLServerSpatialDatatype {
             if (wkt.charAt(currentWktPos) == ',') {
                 currentWktPos++;
                 skipWhiteSpaces();
-                numOfCoordinates++;
                 break;
             }
             skipWhiteSpaces();
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java
index b7f7cb1b..253c32a5 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java
@@ -177,7 +177,7 @@ public class SQLServerStatement implements ISQLServerStatement {
      * columnEncryptionSetting = true in the connection setting, Disabled if false. This may also be used to set other
      * behavior which overrides connection level setting.
      */
-    protected SQLServerStatementColumnEncryptionSetting stmtColumnEncriptionSetting = SQLServerStatementColumnEncryptionSetting.UseConnectionSetting;
+    protected SQLServerStatementColumnEncryptionSetting stmtColumnEncriptionSetting = SQLServerStatementColumnEncryptionSetting.USE_CONNECTION_SETTING;
 
     /**
      * Returns the statement column encryption encryption setting
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatementColumnEncryptionSetting.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatementColumnEncryptionSetting.java
index b3bcb4a5..70d3d540 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatementColumnEncryptionSetting.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatementColumnEncryptionSetting.java
@@ -15,21 +15,21 @@ public enum SQLServerStatementColumnEncryptionSetting {
     /**
      * if "Column Encryption Setting=Enabled" in the connection string, use Enabled. Otherwise, maps to Disabled.
      */
-    UseConnectionSetting,
+    USE_CONNECTION_SETTING,
 
     /**
      * Enables TCE for the command. Overrides the connection level setting for this command.
      */
-    Enabled,
+    ENABLED,
 
     /**
      * Parameters will not be encrypted, only the ResultSet will be decrypted. This is an optimization for queries that
      * do not pass any encrypted input parameters. Overrides the connection level setting for this command.
      */
-    ResultSetOnly,
+    RESULTSET_ONLY,
 
     /**
      * Disables TCE for the command.Overrides the connection level setting for this command.
      */
-    Disabled,
+    DISABLED,
 }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSymmetricKeyCache.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSymmetricKeyCache.java
index 0b8e02f2..76967861 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSymmetricKeyCache.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSymmetricKeyCache.java
@@ -14,6 +14,7 @@ import java.util.concurrent.locks.ReentrantLock;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+
 /**
  * 
  * Cache for the Symmetric keys
@@ -28,7 +29,7 @@ final class SQLServerSymmetricKeyCache {
             .getLogger("com.microsoft.sqlserver.jdbc.SQLServerSymmetricKeyCache");
 
     private SQLServerSymmetricKeyCache() {
-        cache = new SimpleTtlCache<String, SQLServerSymmetricKey>();
+        cache = new SimpleTtlCache<>();
     }
 
     static SQLServerSymmetricKeyCache getInstance() {
@@ -72,33 +73,34 @@ final class SQLServerSymmetricKeyCache {
             Boolean[] hasEntry = new Boolean[1];
             List<String> trustedKeyPaths = SQLServerConnection.getColumnEncryptionTrustedMasterKeyPaths(serverName,
                     hasEntry);
-            if (hasEntry[0]) {
-                if ((null == trustedKeyPaths) || (0 == trustedKeyPaths.size())
-                        || (!trustedKeyPaths.contains(keyInfo.keyPath))) {
-                    MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_UntrustedKeyPath"));
+            if (hasEntry[0] &&
+                 ((null == trustedKeyPaths) || (trustedKeyPaths.isEmpty())
+                        || (!trustedKeyPaths.contains(keyInfo.keyPath)))) {
+                MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_UntrustedKeyPath"));
                     Object[] msgArgs = {keyInfo.keyPath, serverName};
                     throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
                 }
-            }
 
             if (aeLogger.isLoggable(java.util.logging.Level.FINE)) {
                 aeLogger.fine("Checking Symmetric key cache...");
             }
 
             if (!cache.contains(keyLookupValue)) {
- 
+
                 // search system/global key store providers
-                SQLServerColumnEncryptionKeyStoreProvider provider = connection.getSystemOrGlobalColumnEncryptionKeyStoreProvider(keyInfo.keyStoreName);
+                SQLServerColumnEncryptionKeyStoreProvider provider = connection
+                        .getSystemOrGlobalColumnEncryptionKeyStoreProvider(keyInfo.keyStoreName);
                 assert null != provider : "Provider should not be null.";
 
                 byte[] plaintextKey;
-                
-                /* 
+
+                /*
                  * When provider decrypt Column Encryption Key, it can cache the decrypted key if cacheTTL > 0.
                  * To prevent conflicts between CEK caches, system providers and global providers should not use their own CEK caches.
                  */
                 provider.setColumnEncryptionCacheTtl(Duration.ZERO);
-                plaintextKey = provider.decryptColumnEncryptionKey(keyInfo.keyPath, keyInfo.algorithmName, keyInfo.encryptedKey);
+                plaintextKey = provider.decryptColumnEncryptionKey(keyInfo.keyPath, keyInfo.algorithmName,
+                        keyInfo.encryptedKey);
 
                 encryptionKey = new SQLServerSymmetricKey(plaintextKey);
 
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerVSMEnclaveProvider.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerVSMEnclaveProvider.java
index e50d1e94..80e84e9f 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerVSMEnclaveProvider.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerVSMEnclaveProvider.java
@@ -203,7 +203,7 @@ public class SQLServerVSMEnclaveProvider implements ISQLServerEnclaveProvider {
 
 class VSMAttestationParameters extends BaseAttestationRequest {
     // Type 3 is VSM, sent as Little Endian 0x30000000
-    private static final byte ENCLAVE_TYPE[] = new byte[] {0x3, 0x0, 0x0, 0x0};
+    private static final byte[] ENCLAVE_TYPE = new byte[] {0x3, 0x0, 0x0, 0x0};
 
     VSMAttestationParameters() throws SQLServerException {
         enclaveChallenge = new byte[] {0x0, 0x0, 0x0, 0x0};
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java
index 02bcfcb3..8829072d 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java
@@ -24,6 +24,7 @@ import javax.transaction.xa.XAException;
 import javax.transaction.xa.XAResource;
 import javax.transaction.xa.Xid;
 
+import com.microsoft.sqlserver.jdbc.SQLServerError.TransientError;
 
 /**
  * Implements Transaction id used to recover transactions.
@@ -381,7 +382,7 @@ public final class SQLServerXAResource implements javax.transaction.xa.XAResourc
 
     }
 
-    private XAReturnValue DTC_XA_Interface(int nType, Xid xid, int xaFlags) throws XAException {
+    private XAReturnValue dtc_XA_interface(int nType, Xid xid, int xaFlags) throws XAException {
 
         if (xaLogger.isLoggable(Level.FINER))
             xaLogger.finer(toString() + " Calling XA function for type:" + typeDisplay(nType) + " flags:"
@@ -668,8 +669,8 @@ public final class SQLServerXAResource implements javax.transaction.xa.XAResourc
                 // will throw exception
                 // "The function RECOVER: failed. The status is: -3"
                 recoveryAttempt++;
-                DTC_XA_Interface(XA_START, xid, TMNOFLAGS);
-                return DTC_XA_Interface(XA_RECOVER, xid, xaFlags);
+                dtc_XA_interface(XA_START, xid, TMNOFLAGS);
+                return dtc_XA_interface(XA_RECOVER, xid, xaFlags);
             }
             // prepare and end can return XA_RDONLY
             // Think should we just check for nStatus to be greater than or equal to zero instead of this check
@@ -685,7 +686,7 @@ public final class SQLServerXAResource implements javax.transaction.xa.XAResourc
                     try {
                         if (xaLogger.isLoggable(Level.FINER))
                             xaLogger.finer(toString() + " Begin un-enlist, enlisted count:" + enlistedTransactionCount);
-                        con.JTAUnenlistConnection();
+                        con.jtaUnenlistConnection();
                         enlistedTransactionCount--;
                         if (xaLogger.isLoggable(Level.FINER))
                             xaLogger.finer(toString() + " End un-enlist, enlisted count:" + enlistedTransactionCount);
@@ -712,7 +713,7 @@ public final class SQLServerXAResource implements javax.transaction.xa.XAResourc
                                 xaLogger.finer(
                                         toString() + " Begin enlisting, cookie:" + cookieDisplay(transactionCookie)
                                                 + " enlisted count:" + enlistedTransactionCount);
-                            con.JTAEnlistConnection(transactionCookie);
+                            con.jtaEnlistConnection(transactionCookie);
                             enlistedTransactionCount++;
                             if (xaLogger.isLoggable(Level.FINER))
                                 xaLogger.finer(toString() + " End enlisting, cookie:" + cookieDisplay(transactionCookie)
@@ -728,7 +729,7 @@ public final class SQLServerXAResource implements javax.transaction.xa.XAResourc
                     try {
                         if (xaLogger.isLoggable(Level.FINER))
                             xaLogger.finer(toString() + " Begin un-enlist, enlisted count:" + enlistedTransactionCount);
-                        con.JTAUnenlistConnection();
+                        con.jtaUnenlistConnection();
                         enlistedTransactionCount--;
                         if (xaLogger.isLoggable(Level.FINER))
                             xaLogger.finer(toString() + " End un-enlist, enlisted count:" + enlistedTransactionCount);
@@ -751,9 +752,24 @@ public final class SQLServerXAResource implements javax.transaction.xa.XAResourc
                     }
                 }
             }
-        } catch (SQLServerException | SQLTimeoutException ex) {
+        } catch (SQLTimeoutException ex) {
             if (xaLogger.isLoggable(Level.FINER))
                 xaLogger.finer(toString() + " exception:" + ex);
+            XAException e = new XAException(ex.toString());
+            e.errorCode = XAException.XAER_RMFAIL;
+            throw e;
+
+        } catch (SQLServerException ex) {
+            if (xaLogger.isLoggable(Level.FINER))
+                xaLogger.finer(toString() + " exception:" + ex);
+
+            if (ex.getMessage().equals(SQLServerException.getErrString("R_noServerResponse"))
+                    || TransientError.isTransientError(ex.getSQLServerError())) {
+                XAException e = new XAException(ex.toString());
+                e.errorCode = XAException.XAER_RMFAIL;
+                throw e;
+            }
+
             XAException e = new XAException(ex.toString());
             e.errorCode = XAException.XAER_RMERR;
             throw e;
@@ -788,7 +804,7 @@ public final class SQLServerXAResource implements javax.transaction.xa.XAResourc
         // of those will be prefixed by the transaction manager with a call to start with TMNOFLAGS
 
         tightlyCoupled = flags & SSTRANSTIGHTLYCPLD;
-        DTC_XA_Interface(XA_START, xid, flags);
+        dtc_XA_interface(XA_START, xid, flags);
     }
 
     @Override
@@ -803,7 +819,7 @@ public final class SQLServerXAResource implements javax.transaction.xa.XAResourc
          * work has failed. The resource manager may mark the transaction as rollback-only. If TMSUCCESS is specified,
          * the portion of work has completed successfully.
          */
-        DTC_XA_Interface(XA_END, xid, flags | tightlyCoupled);
+        dtc_XA_interface(XA_END, xid, flags | tightlyCoupled);
     }
 
     @Override
@@ -814,31 +830,28 @@ public final class SQLServerXAResource implements javax.transaction.xa.XAResourc
          * of the transaction. The possible values are: XA_RDONLY or XA_OK. If the resource manager wants to roll back
          * the transaction, it should do so by raising an appropriate XAException in the prepare method.
          */
-        int nStatus = XA_OK;
-        XAReturnValue r = DTC_XA_Interface(XA_PREPARE, xid, tightlyCoupled);
-        nStatus = r.nStatus;
-
-        return nStatus;
+        XAReturnValue r = dtc_XA_interface(XA_PREPARE, xid, tightlyCoupled);
+        return r.nStatus;
     }
 
     @Override
     public void commit(Xid xid, boolean onePhase) throws XAException {
-        DTC_XA_Interface(XA_COMMIT, xid, ((onePhase) ? TMONEPHASE : TMNOFLAGS) | tightlyCoupled);
+        dtc_XA_interface(XA_COMMIT, xid, ((onePhase) ? TMONEPHASE : TMNOFLAGS) | tightlyCoupled);
     }
 
     @Override
     public void rollback(Xid xid) throws XAException {
-        DTC_XA_Interface(XA_ROLLBACK, xid, tightlyCoupled);
+        dtc_XA_interface(XA_ROLLBACK, xid, tightlyCoupled);
     }
 
     @Override
     public void forget(Xid xid) throws XAException {
-        DTC_XA_Interface(XA_FORGET, xid, tightlyCoupled);
+        dtc_XA_interface(XA_FORGET, xid, tightlyCoupled);
     }
 
     @Override
     public Xid[] recover(int flags) throws XAException {
-        XAReturnValue r = DTC_XA_Interface(XA_RECOVER, null, flags | tightlyCoupled);
+        XAReturnValue r = dtc_XA_interface(XA_RECOVER, null, flags | tightlyCoupled);
         int offset = 0;
         ArrayList<XidImpl> al = new ArrayList<>();
 
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Util.java b/src/main/java/com/microsoft/sqlserver/jdbc/Util.java
index 89dd37bb..1397737f 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/Util.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/Util.java
@@ -801,14 +801,14 @@ final class Util {
             SQLServerConnection connection) {
         // Command leve setting trumps all
         switch (stmtColumnEncryptionSetting) {
-            case Disabled:
+            case DISABLED:
                 return false;
-            case Enabled:
-            case ResultSetOnly:
+            case ENABLED:
+            case RESULTSET_ONLY:
                 return true;
             default:
                 // Check connection level setting!
-                assert SQLServerStatementColumnEncryptionSetting.UseConnectionSetting == stmtColumnEncryptionSetting : "Unexpected value for command level override";
+                assert SQLServerStatementColumnEncryptionSetting.USE_CONNECTION_SETTING == stmtColumnEncryptionSetting : "Unexpected value for command level override";
                 return (connection != null && connection.isColumnEncryptionSettingEnabled());
         }
     }
@@ -823,14 +823,14 @@ final class Util {
             SQLServerConnection connection) {
         // Command leve setting trumps all
         switch (stmtColumnEncryptionSetting) {
-            case Disabled:
-            case ResultSetOnly:
+            case DISABLED:
+            case RESULTSET_ONLY:
                 return false;
-            case Enabled:
+            case ENABLED:
                 return true;
             default:
                 // Check connection level setting!
-                assert SQLServerStatementColumnEncryptionSetting.UseConnectionSetting == stmtColumnEncryptionSetting : "Unexpected value for command level override";
+                assert SQLServerStatementColumnEncryptionSetting.USE_CONNECTION_SETTING == stmtColumnEncryptionSetting : "Unexpected value for command level override";
                 return (connection != null && connection.isColumnEncryptionSettingEnabled());
         }
     }
@@ -985,7 +985,7 @@ final class Util {
 
     @SuppressWarnings("unchecked")
     static <T> T newInstance(Class<?> returnType, String className, String constructorArg,
-            Object[] msgArgs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException, ClassNotFoundException {
+            Object[] msgArgs) throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, ClassNotFoundException {
         Class<?> clazz = Class.forName(className);
         if (!returnType.isAssignableFrom(clazz)) {
             MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_unassignableError"));
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/dns/DNSRecordSRV.java b/src/main/java/com/microsoft/sqlserver/jdbc/dns/DNSRecordSRV.java
index 8187b0b0..7c8a25c8 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/dns/DNSRecordSRV.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/dns/DNSRecordSRV.java
@@ -26,7 +26,7 @@ public class DNSRecordSRV implements Comparable<DNSRecordSRV> {
      * @throws IllegalArgumentException
      *         if record is not correct and cannot be parsed
      */
-    public static DNSRecordSRV parseFromDNSRecord(String record) throws IllegalArgumentException {
+    public static DNSRecordSRV parseFromDNSRecord(String record) {
         Matcher m = PATTERN.matcher(record);
         if (!m.matches()) {
             throw new IllegalArgumentException("record '" + record + "' cannot be matched as a valid DNS SRV Record");
@@ -67,7 +67,7 @@ public class DNSRecordSRV implements Comparable<DNSRecordSRV> {
      * @throws IllegalArgumentException
      *         if priority {@literal <} 0 or weight {@literal <=} 1
      */
-    public DNSRecordSRV(int priority, int weight, int port, String serverName) throws IllegalArgumentException {
+    public DNSRecordSRV(int priority, int weight, int port, String serverName) {
         if (priority < 0) {
             throw new IllegalArgumentException("priority must be >= 0, but was: " + priority);
         }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java
index c5716f9b..c2e2e690 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java
@@ -2140,11 +2140,11 @@ final class AppDTVImpl extends DTVImpl {
                 Integer dtvScale, biScale = bigDecimalValue.scale();
                 if (null == dtv.getScale() && JDBCType.DECIMAL == dtv.getJdbcType()) {
                     dtvScale = bigDecimalValue
-                            .precision() > SQLServerConnection.maxDecimalPrecision ? SQLServerConnection.maxDecimalPrecision
+                            .precision() > SQLServerConnection.MAX_DECIMAL_PRECISION ? SQLServerConnection.MAX_DECIMAL_PRECISION
                                     - (bigDecimalValue.precision() - biScale) : biScale;
-                    if (dtvScale > SQLServerConnection.maxDecimalPrecision) {
-                        dtv.setScale(SQLServerConnection.maxDecimalPrecision);
-                        dtvScale = SQLServerConnection.maxDecimalPrecision;
+                    if (dtvScale > SQLServerConnection.MAX_DECIMAL_PRECISION) {
+                        dtv.setScale(SQLServerConnection.MAX_DECIMAL_PRECISION);
+                        dtvScale = SQLServerConnection.MAX_DECIMAL_PRECISION;
                     } else {
                         dtv.setScale(dtvScale);
                     }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java b/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java
index 3da844f6..a26133f6 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java
@@ -69,7 +69,6 @@ final class TDSParser {
                         + ((-1 == tdsTokenType) ? "EOF" : TDS.getTokenName(tdsTokenType)));
             }
             if (readOnlyWarningsFlag && TDS.TDS_MSG != tdsTokenType) {
-                parsing = false;
                 return;
             }
             switch (tdsTokenType) {


Test patch:
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/AESetup.java b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/AESetup.java
index 820fe5b0..96e8ead8 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/AESetup.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/AESetup.java
@@ -196,7 +196,7 @@ public class AESetup extends AbstractTest {
 
         readFromFile(Constants.JAVA_KEY_STORE_FILENAME, "Alias name");
 
-        stmtColEncSetting = SQLServerStatementColumnEncryptionSetting.Enabled;
+        stmtColEncSetting = SQLServerStatementColumnEncryptionSetting.ENABLED;
 
         AEInfo = new Properties();
         AEInfo.setProperty("ColumnEncryptionSetting", Constants.ENABLED);
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/MultiUserAKVTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/MultiUserAKVTest.java
index 61931c77..257d8f92 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/MultiUserAKVTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/MultiUserAKVTest.java
@@ -282,7 +282,7 @@ public class MultiUserAKVTest extends AESetup {
 
         try (SQLServerConnection con = PrepUtil.getConnection(AETestConnectionString, AEInfo);
                 SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) TestUtils.getPreparedStmt(con, sql,
-                        SQLServerStatementColumnEncryptionSetting.Enabled)) {
+                        SQLServerStatementColumnEncryptionSetting.ENABLED)) {
 
             pstmt.setInt(1, customerId);
 
@@ -365,7 +365,7 @@ public class MultiUserAKVTest extends AESetup {
 
         try (SQLServerConnection con = PrepUtil.getConnection(AETestConnectionString, AEInfo)) {
             try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) TestUtils.getPreparedStmt(con, sql,
-                    SQLServerStatementColumnEncryptionSetting.Enabled)) {
+                    SQLServerStatementColumnEncryptionSetting.ENABLED)) {
                 pstmt.setInt(1, customerId);
                 pstmt.executeQuery();
                 fail(TestResource.getResource("R_expectedExceptionNotThrown"));
@@ -379,7 +379,7 @@ public class MultiUserAKVTest extends AESetup {
              */
             con.registerColumnEncryptionKeyStoreProvidersOnConnection(notRequiredKeyStoreProvider);
             try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) TestUtils.getPreparedStmt(con, sql,
-                    SQLServerStatementColumnEncryptionSetting.Enabled)) {
+                    SQLServerStatementColumnEncryptionSetting.ENABLED)) {
                 pstmt.setInt(1, customerId);
                 pstmt.executeQuery();
                 fail(TestResource.getResource("R_expectedExceptionNotThrown"));
@@ -395,7 +395,7 @@ public class MultiUserAKVTest extends AESetup {
              */
             con.registerColumnEncryptionKeyStoreProvidersOnConnection(requiredKeyStoreProvider);
             try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) TestUtils.getPreparedStmt(con, sql,
-                    SQLServerStatementColumnEncryptionSetting.Enabled)) {
+                    SQLServerStatementColumnEncryptionSetting.ENABLED)) {
                 pstmt.setInt(1, customerId);
                 pstmt.executeQuery();
                 fail(TestResource.getResource("R_expectedExceptionNotThrown"));
@@ -406,7 +406,7 @@ public class MultiUserAKVTest extends AESetup {
             // Not required provider will replace the previous entry so required provider will not be found.
             con.registerColumnEncryptionKeyStoreProvidersOnConnection(notRequiredKeyStoreProvider);
             try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) TestUtils.getPreparedStmt(con, sql,
-                    SQLServerStatementColumnEncryptionSetting.Enabled)) {
+                    SQLServerStatementColumnEncryptionSetting.ENABLED)) {
                 pstmt.setInt(1, customerId);
                 pstmt.executeQuery();
                 fail(TestResource.getResource("R_expectedExceptionNotThrown"));
@@ -449,7 +449,7 @@ public class MultiUserAKVTest extends AESetup {
 
         try (SQLServerConnection con = PrepUtil.getConnection(AETestConnectionString, AEInfo);
                 SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) TestUtils.getPreparedStmt(con, sql,
-                        SQLServerStatementColumnEncryptionSetting.Enabled)) {
+                        SQLServerStatementColumnEncryptionSetting.ENABLED)) {
             pstmt.setInt(1, customerId);
 
             /*
@@ -503,7 +503,7 @@ public class MultiUserAKVTest extends AESetup {
              * contains the required provider
              */
             try (SQLServerPreparedStatement pstmt2 = (SQLServerPreparedStatement) TestUtils.getPreparedStmt(con, sql,
-                    SQLServerStatementColumnEncryptionSetting.Enabled)) {
+                    SQLServerStatementColumnEncryptionSetting.ENABLED)) {
                 pstmt2.setInt(1, customerId);
 
                 try {
@@ -527,7 +527,7 @@ public class MultiUserAKVTest extends AESetup {
 
         try (SQLServerConnection con = PrepUtil.getConnection(AETestConnectionString, AEInfo);
                 SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) TestUtils.getPreparedStmt(con, sqlQuery,
-                        SQLServerStatementColumnEncryptionSetting.Enabled)) {
+                        SQLServerStatementColumnEncryptionSetting.ENABLED)) {
             pstmt.setInt(1, customId);
             pstmt.setString(2, customName);
             pstmt.executeUpdate();
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java
index 5b4b2a51..f1e2298c 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java
@@ -4,7 +4,6 @@
  */
 package com.microsoft.sqlserver.jdbc;
 
-import static org.junit.Assert.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
@@ -1108,68 +1107,4 @@ public class SQLServerConnectionTest extends AbstractTest {
             assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_errorServerName")));
         }
     }
-
-    @Tag(Constants.xSQLv11)
-    @Tag(Constants.xSQLv12)
-    @Tag(Constants.xSQLv14)
-    @Tag(Constants.xSQLv15)
-    @Tag(Constants.xAzureSQLDW)
-    @Tag(Constants.reqExternalSetup)
-    @Test
-    public void testDSPooledConnectionAccessTokenCallback() throws Exception {
-        SQLServerConnectionPoolDataSource ds = new SQLServerConnectionPoolDataSource();
-
-        // User/password is not required for access token callback
-        String cs = TestUtils.addOrOverrideProperty(connectionString, "user", "");
-        cs = TestUtils.addOrOverrideProperty(cs, "password", "");
-        AbstractTest.updateDataSource(cs, ds);
-        ds.setAccessTokenCallback(TestUtils.accessTokenCallback);
-
-        TestUtils.expireTokenToggle = false;
-        SQLServerPooledConnection pc = (SQLServerPooledConnection) ds.getPooledConnection();
-        String conn1ID;
-        String conn2ID;
-
-        // Callback should provide valid token on connection open for all new connections
-        // When the access token hasn't expired, the connection ID should be the same
-        try (Connection conn1 = pc.getConnection()) {}
-        conn1ID = TestUtils.getConnectionID(pc);
-        try (Connection conn2 = pc.getConnection()) {}
-        conn2ID = TestUtils.getConnectionID(pc);
-        assertEquals(conn1ID, conn2ID);
-    }
-
-    @Tag(Constants.xSQLv11)
-    @Tag(Constants.xSQLv12)
-    @Tag(Constants.xSQLv14)
-    @Tag(Constants.xSQLv15)
-    @Tag(Constants.xAzureSQLDW)
-    @Tag(Constants.reqExternalSetup)
-    @Test
-    public void testDSPooledConnectionAccessTokenCallbackExpiredToken() throws Exception {
-        SQLServerConnectionPoolDataSource ds = new SQLServerConnectionPoolDataSource();
-
-        // User/password is not required for access token callback
-        String cs = TestUtils.addOrOverrideProperty(connectionString, "user", "");
-        cs = TestUtils.addOrOverrideProperty(cs, "password", "");
-        AbstractTest.updateDataSource(cs, ds);
-        ds.setAccessTokenCallback(TestUtils.accessTokenCallback);
-
-        SQLServerPooledConnection pc = (SQLServerPooledConnection) ds.getPooledConnection();
-        String conn1ID;
-        String conn2ID;
-
-        // When token expires after first connection, it should create a new connection to get a new token.
-        // Connection ID should not be the same.
-        TestUtils.expireTokenToggle = true;
-        pc = (SQLServerPooledConnection) ds.getPooledConnection();
-        try (Connection conn1 = pc.getConnection()) {}
-        conn1ID = TestUtils.getConnectionID(pc);
-        // Sleep until token expires
-        Thread.sleep(720000);
-        try (Connection conn2 = pc.getConnection()) {}
-        conn2ID = TestUtils.getConnectionID(pc);
-
-        assertNotEquals(conn1ID, conn2ID);
-    }
 }
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java b/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java
index 88ad138f..2f20821a 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java
@@ -96,6 +96,8 @@ public final class TestUtils {
 
     public static final int TEST_TOKEN_EXPIRY_SECONDS = 120; // token expiry time in secs
 
+    public static String ACCESS_TOKEN_CALLBACK = null;
+
     static String applicationKey;
     static String applicationClientID;
 
@@ -132,9 +134,11 @@ public final class TestUtils {
                 String accessToken = authenticationResult.accessToken();
                 long expiresOn = authenticationResult.expiresOnDate().getTime();
 
+                ACCESS_TOKEN_CALLBACK = accessToken;
+
                 if (expireTokenToggle) {
                     Date now = new Date();
-                    long minutesToExpireWithin = 10 * 60 * 1000; // Expire within 10 minutes
+                    long minutesToExpireWithin = TEST_TOKEN_EXPIRY_SECONDS * 1000; // Expire within 2 minutes
                     return new SqlAuthenticationToken(accessToken, now.getTime() + minutesToExpireWithin);
                 } else {
                     return new SqlAuthenticationToken(accessToken, expiresOn);
@@ -161,6 +165,26 @@ public final class TestUtils {
         }
     }
 
+    public static void setAccessTokenExpiry(Object con) {
+        Field fedAuthTokenField;
+        Field wrappedConnection;
+        try {
+            fedAuthTokenField = SQLServerConnection.class.getDeclaredField("fedAuthToken");
+            fedAuthTokenField.setAccessible(true);
+
+            wrappedConnection = SQLServerConnectionPoolProxy.class.getDeclaredField("wrappedConnection");
+            wrappedConnection.setAccessible(true);
+            Object wrappedConnectionObj = wrappedConnection.get(con);
+
+            Date newExpiry = new Date(
+                    System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(TEST_TOKEN_EXPIRY_SECONDS));
+            SqlAuthenticationToken newFedAuthToken = new SqlAuthenticationToken(ACCESS_TOKEN_CALLBACK, newExpiry);
+            fedAuthTokenField.set(wrappedConnectionObj, newFedAuthToken);
+        } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
+            Assert.fail("Failed to set token expiry: " + e.getMessage());
+        }
+    }
+
     private TestUtils() {}
 
     /**
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java
index daba4459..d0ce2556 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java
@@ -502,6 +502,8 @@ public class RequestBoundaryMethodsTest extends AbstractTest {
         verifiedMethodNames.add("setIPAddressPreference");
         verifiedMethodNames.add("getMsiTokenCacheTtl");
         verifiedMethodNames.add("setMsiTokenCacheTtl");
+        verifiedMethodNames.add("getAccessTokenCallbackClass");
+        verifiedMethodNames.add("setAccessTokenCallbackClass");
         return verifiedMethodNames;
     }
 }
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthCommon.java b/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthCommon.java
index 41beff62..a76bcaf5 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthCommon.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthCommon.java
@@ -159,13 +159,16 @@ public class FedauthCommon extends AbstractTest {
      * 
      */
     static void getFedauthInfo() {
-        int retry = THROTTLE_RETRY_COUNT;
+        int retry = 0;
         long interval = THROTTLE_RETRY_INTERVAL;
-        while (retry > 0) {
+        while (retry <= THROTTLE_RETRY_COUNT) {
             try {
-                final PublicClientApplication clientApplication = PublicClientApplication.builder(fedauthClientId)
-                        .executorService(Executors.newFixedThreadPool(1)).authority(stsurl).build();
-                final CompletableFuture<IAuthenticationResult> future = clientApplication
+                if (null == fedauthPcaApp) {
+                    fedauthPcaApp = PublicClientApplication.builder(fedauthClientId)
+                            .executorService(Executors.newFixedThreadPool(1)).authority(stsurl).build();
+                }
+
+                final CompletableFuture<IAuthenticationResult> future = fedauthPcaApp
                         .acquireToken(UserNamePasswordParameters.builder(Collections.singleton(spn + "/.default"),
                                 azureUserName, azurePassword.toCharArray()).build());
 
@@ -174,14 +177,14 @@ public class FedauthCommon extends AbstractTest {
                 secondsBeforeExpiration = TimeUnit.MILLISECONDS
                         .toSeconds(authenticationResult.expiresOnDate().getTime() - new Date().getTime());
                 accessToken = authenticationResult.accessToken();
-                retry = 0;
+                retry = THROTTLE_RETRY_COUNT + 1;
             } catch (MsalThrottlingException te) {
-                interval = ((MsalThrottlingException) te).retryInMs();
-                if (!checkForRetry(te, retry--, interval)) {
+                interval = te.retryInMs();
+                if (!checkForRetry(te, retry++, interval)) {
                     fail(ERR_FAILED_FEDAUTH + "no more retries: " + te.getMessage());
                 }
             } catch (Exception e) {
-                if (!checkForRetry(e, retry--, interval)) {
+                if (!checkForRetry(e, retry++, interval)) {
                     fail(ERR_FAILED_FEDAUTH + "no more retries: " + e.getMessage());
                 }
             }
@@ -189,15 +192,16 @@ public class FedauthCommon extends AbstractTest {
     }
 
     static boolean checkForRetry(Exception e, int retry, long interval) {
-        if (retry <= 0) {
+        if (retry > THROTTLE_RETRY_COUNT) {
             return false;
         }
         try {
-            System.out.println(e.getMessage() + "Get FedAuth token failed retry #" + retry + " in " + interval + " ms");
-            e.printStackTrace();
-
+            System.out
+                    .println(e.getMessage() + "Get FedAuth token failed, retry #" + retry + " in " + interval + " ms");
             Thread.sleep(interval);
         } catch (InterruptedException ex) {
+            e.printStackTrace();
+
             fail(ERR_FAILED_FEDAUTH + ex.getMessage());
         }
         return true;
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthTest.java
index 53c3d546..3d12a099 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthTest.java
@@ -4,6 +4,7 @@
  */
 package com.microsoft.sqlserver.jdbc.fedauth;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -17,7 +18,12 @@ import java.sql.Connection;
 import java.sql.DriverManager;
 import java.sql.SQLException;
 import java.sql.Statement;
+import java.util.Collections;
+import java.util.Date;
 import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
 
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
@@ -26,6 +32,10 @@ import org.junit.jupiter.api.Test;
 import org.junit.platform.runner.JUnitPlatform;
 import org.junit.runner.RunWith;
 
+import com.microsoft.aad.msal4j.IAccount;
+import com.microsoft.aad.msal4j.IAuthenticationResult;
+import com.microsoft.aad.msal4j.SilentParameters;
+import com.microsoft.aad.msal4j.UserNamePasswordParameters;
 import com.microsoft.sqlserver.jdbc.RandomUtil;
 import com.microsoft.sqlserver.jdbc.SQLServerDataSource;
 import com.microsoft.sqlserver.jdbc.TestResource;
@@ -40,9 +50,12 @@ public class FedauthTest extends FedauthCommon {
     static String charTable = TestUtils
             .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("JDBC_FedAuthTest")));
 
+    private static String accessTokenCallbackConnectionString;
+
     @BeforeAll
     public static void setupTests() throws Exception {
         connectionString = TestUtils.addOrOverrideProperty(connectionString, "trustServerCertificate", "true");
+        accessTokenCallbackConnectionString = "jdbc:sqlserver://" + azureServer + ";database=" + azureDatabase + ";";
         setConnection();
     }
 
@@ -352,6 +365,51 @@ public class FedauthTest extends FedauthCommon {
         validateException(url, "R_BothUserPasswordandDeprecated");
     }
 
+    @Tag(Constants.xSQLv11)
+    @Tag(Constants.xSQLv12)
+    @Tag(Constants.xSQLv14)
+    @Tag(Constants.xAzureSQLDW)
+    @Tag(Constants.reqExternalSetup)
+    @Test
+    public void testAccessTokenCallbackClassConnection() throws Exception {
+        String cs = TestUtils.addOrOverrideProperty(accessTokenCallbackConnectionString, "accessTokenCallbackClass",
+                PooledConnectionTest.AccessTokenCallbackClass.class.getName());
+        try (Connection conn1 = DriverManager.getConnection(cs)) {}
+    }
+
+    @Test
+    public void testAccessTokenCache() {
+        try {
+            Set<IAccount> accountsInCache = fedauthPcaApp.getAccounts().join();
+            assertTrue(null != accountsInCache && !accountsInCache.isEmpty());
+
+            IAccount account = getAccountByUsername(accountsInCache, azureUserName);
+            assertNotNull(account);
+
+            SilentParameters silentParameters = SilentParameters
+                    .builder(Collections.singleton(spn + "/.default"), account).build();
+
+            // this will fail if not cached
+            CompletableFuture<IAuthenticationResult> future = fedauthPcaApp.acquireTokenSilently(silentParameters);
+            IAuthenticationResult authenticationResult = future.get();
+            assertNotNull(authenticationResult.accessToken());
+            assertEquals(authenticationResult.account().username(), azureUserName);
+        } catch (Exception e) {
+            fail(e.getMessage());
+        }
+    }
+
+    private IAccount getAccountByUsername(Set<IAccount> accounts, String username) {
+        if (!accounts.isEmpty()) {
+            for (IAccount account : accounts) {
+                if (account.username().equalsIgnoreCase(username)) {
+                    return account;
+                }
+            }
+        }
+        return null;
+    }
+
     private static void validateException(String url, String resourceKey) {
         try (Connection conn = DriverManager.getConnection(url)) {
             fail(TestResource.getResource("R_expectedFailPassed"));
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/PooledConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/PooledConnectionTest.java
index 55415bbe..e109f0d6 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/PooledConnectionTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/PooledConnectionTest.java
@@ -4,6 +4,8 @@
  */
 package com.microsoft.sqlserver.jdbc.fedauth;
 
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 
@@ -13,9 +15,13 @@ import java.sql.DriverManager;
 import java.sql.SQLException;
 import java.sql.Statement;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Random;
+import java.util.Set;
+import java.util.UUID;
 import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
@@ -23,6 +29,16 @@ import java.util.concurrent.TimeUnit;
 
 import javax.sql.PooledConnection;
 
+import com.microsoft.aad.msal4j.ClientCredentialFactory;
+import com.microsoft.aad.msal4j.ClientCredentialParameters;
+import com.microsoft.aad.msal4j.ConfidentialClientApplication;
+import com.microsoft.aad.msal4j.IAuthenticationResult;
+import com.microsoft.aad.msal4j.IClientCredential;
+import com.microsoft.sqlserver.jdbc.SQLServerAccessTokenCallback;
+import com.microsoft.sqlserver.jdbc.SQLServerException;
+import com.microsoft.sqlserver.jdbc.SqlAuthenticationToken;
+import com.microsoft.sqlserver.jdbc.TestResource;
+import com.microsoft.sqlserver.testframework.AbstractTest;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Tag;
@@ -46,9 +62,40 @@ public class PooledConnectionTest extends FedauthCommon {
     static String charTable = TestUtils.escapeSingleQuotes(
             AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("JDBC_PooledConnection")));
 
+    private static String accessTokenCallbackConnectionString;
+
+    public static class AccessTokenCallbackClass implements SQLServerAccessTokenCallback {
+        @Override
+        public SqlAuthenticationToken getAccessToken(String spn, String stsurl) {
+            String scope = spn + "/.default";
+            Set<String> scopes = new HashSet<>();
+            scopes.add(scope);
+
+            try {
+                ExecutorService executorService = Executors.newSingleThreadExecutor();
+                IClientCredential credential = ClientCredentialFactory.createFromSecret(applicationKey);
+                ConfidentialClientApplication clientApplication = ConfidentialClientApplication
+                        .builder(applicationClientID, credential).executorService(executorService).authority(stsurl)
+                        .build();
+                CompletableFuture<IAuthenticationResult> future = clientApplication
+                        .acquireToken(ClientCredentialParameters.builder(scopes).build());
+
+                IAuthenticationResult authenticationResult = future.get();
+                String accessToken = authenticationResult.accessToken();
+                long expiresOn = authenticationResult.expiresOnDate().getTime();
+
+                return new SqlAuthenticationToken(accessToken, expiresOn);
+            } catch (Exception e) {
+                fail(TestResource.getResource("R_unexpectedException") + e.getMessage());
+            }
+            return null;
+        }
+    }
+
     @BeforeAll
     public static void setupTests() throws Exception {
         setConnection();
+        accessTokenCallbackConnectionString = "jdbc:sqlserver://" + azureServer + ";database=" + azureDatabase + ";";
     }
 
     @Test
@@ -263,6 +310,167 @@ public class PooledConnectionTest extends FedauthCommon {
         }
     }
 
+    @Tag(Constants.xSQLv11)
+    @Tag(Constants.xSQLv12)
+    @Tag(Constants.xSQLv14)
+    @Tag(Constants.xAzureSQLDW)
+    @Tag(Constants.reqExternalSetup)
+    @Test
+    public void testDSPooledConnectionAccessTokenCallback() throws Exception {
+        SQLServerConnectionPoolDataSource ds = new SQLServerConnectionPoolDataSource();
+
+        // User/password is not required for access token callback
+        AbstractTest.updateDataSource(accessTokenCallbackConnectionString, ds);
+        ds.setAccessTokenCallback(TestUtils.accessTokenCallback);
+
+        TestUtils.expireTokenToggle = false;
+        SQLServerPooledConnection pc = (SQLServerPooledConnection) ds.getPooledConnection();
+        String conn1ID;
+        String conn2ID;
+
+        // Callback should provide valid token on connection open for all new connections
+        // When the access token hasn't expired, the connection ID should be the same
+        try (Connection conn1 = pc.getConnection()) {}
+        conn1ID = TestUtils.getConnectionID(pc);
+        try (Connection conn2 = pc.getConnection()) {}
+        conn2ID = TestUtils.getConnectionID(pc);
+        assertEquals(conn1ID, conn2ID);
+    }
+
+    @Tag(Constants.xSQLv11)
+    @Tag(Constants.xSQLv12)
+    @Tag(Constants.xSQLv14)
+    @Tag(Constants.xAzureSQLDW)
+    @Tag(Constants.reqExternalSetup)
+    @Test
+    public void testDSPooledConnectionMergeAccessTokenCallbackProperty() throws Exception {
+        SQLServerConnectionPoolDataSource ds = new SQLServerConnectionPoolDataSource();
+
+        // Access token callback property from ds.setAccessTokenCallback should merge supplied properties from URL
+        ds.setURL(accessTokenCallbackConnectionString);
+        ds.setAccessTokenCallback(TestUtils.accessTokenCallback);
+
+        SQLServerPooledConnection pc = (SQLServerPooledConnection) ds.getPooledConnection();
+        String conn1ID;
+        String conn2ID;
+
+        try (Connection conn1 = pc.getConnection()) {}
+        conn1ID = TestUtils.getConnectionID(pc);
+        try (Connection conn2 = pc.getConnection()) {}
+        conn2ID = TestUtils.getConnectionID(pc);
+        assertEquals(conn1ID, conn2ID);
+    }
+
+    @Tag(Constants.xSQLv11)
+    @Tag(Constants.xSQLv12)
+    @Tag(Constants.xSQLv14)
+    @Tag(Constants.xAzureSQLDW)
+    @Tag(Constants.reqExternalSetup)
+    @Test
+    public void testDSPooledConnectionAccessTokenCallbackExpiredToken() throws Exception {
+        SQLServerConnectionPoolDataSource ds = new SQLServerConnectionPoolDataSource();
+
+        // User/password is not required for access token callback
+        AbstractTest.updateDataSource(accessTokenCallbackConnectionString, ds);
+        ds.setAccessTokenCallback(TestUtils.accessTokenCallback);
+
+        SQLServerPooledConnection pc = (SQLServerPooledConnection) ds.getPooledConnection();
+        String conn1ID;
+        String conn2ID;
+
+        // When token expires after first connection, it should create a new connection to get a new token.
+        // Connection ID should not be the same.
+        TestUtils.expireTokenToggle = true;
+        pc = (SQLServerPooledConnection) ds.getPooledConnection();
+        try (Connection conn1 = pc.getConnection()) {
+            TestUtils.setAccessTokenExpiry(conn1);
+        }
+        conn1ID = TestUtils.getConnectionID(pc);
+        // Sleep until token expires
+        Thread.sleep(130 * 1000);
+        try (Connection conn2 = pc.getConnection()) {}
+        conn2ID = TestUtils.getConnectionID(pc);
+
+        assertNotEquals(conn1ID, conn2ID);
+    }
+
+    @Tag(Constants.xSQLv11)
+    @Tag(Constants.xSQLv12)
+    @Tag(Constants.xSQLv14)
+    @Tag(Constants.xAzureSQLDW)
+    @Tag(Constants.reqExternalSetup)
+    @Test
+    public void testDSPooledConnectionAccessTokenCallbackClass() throws Exception {
+        SQLServerConnectionPoolDataSource ds = new SQLServerConnectionPoolDataSource();
+
+        // User/password is not required for access token callback
+        AbstractTest.updateDataSource(accessTokenCallbackConnectionString, ds);
+        ds.setAccessTokenCallbackClass(AccessTokenCallbackClass.class.getName());
+
+        SQLServerPooledConnection pc = (SQLServerPooledConnection) ds.getPooledConnection();
+        String conn1ID;
+        String conn2ID;
+
+        // Callback should provide valid token on connection open for all new connections
+        // When the access token hasn't expired, the connection ID should be the same
+        try (Connection conn1 = pc.getConnection()) {}
+        conn1ID = TestUtils.getConnectionID(pc);
+        try (Connection conn2 = pc.getConnection()) {}
+        conn2ID = TestUtils.getConnectionID(pc);
+        assertEquals(conn1ID, conn2ID);
+    }
+
+    @Tag(Constants.xSQLv11)
+    @Tag(Constants.xSQLv12)
+    @Tag(Constants.xSQLv14)
+    @Tag(Constants.xAzureSQLDW)
+    @Tag(Constants.reqExternalSetup)
+    @Test
+    public void testDSPooledConnectionAccessTokenCallbackClassExceptions() throws Exception {
+        SQLServerConnectionPoolDataSource ds = new SQLServerConnectionPoolDataSource();
+
+        // User/password is not required for access token callback
+        AbstractTest.updateDataSource(accessTokenCallbackConnectionString, ds);
+        ds.setAccessTokenCallbackClass(AccessTokenCallbackClass.class.getName());
+        ds.setUser("user");
+        SQLServerPooledConnection pc;
+
+        // Should fail with user set
+        try {
+            pc = (SQLServerPooledConnection) ds.getPooledConnection();
+            fail(TestResource.getResource("R_expectedFailPassed"));
+        } catch (SQLServerException e) {
+            assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_AccessTokenCallbackWithUserPassword")));
+        }
+
+        ds.setUser("");
+        ds.setPassword(UUID.randomUUID().toString());
+
+        // Should fail with password set
+        try {
+            pc = (SQLServerPooledConnection) ds.getPooledConnection();
+            fail(TestResource.getResource("R_expectedFailPassed"));
+        } catch (SQLServerException e) {
+            assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_AccessTokenCallbackWithUserPassword")));
+        }
+
+        // Should fail with invalid accessTokenCallbackClass value
+        ds.setAccessTokenCallbackClass("Invalid");
+        ds.setUser("");
+        ds.setPassword("");
+        try {
+            pc = (SQLServerPooledConnection) ds.getPooledConnection();
+            fail(TestResource.getResource("R_expectedFailPassed"));
+        } catch (SQLServerException e) {
+            assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidAccessTokenCallbackClass")));
+        }
+
+        // Should pass with no user or password set
+        ds.setAccessTokenCallbackClass(AccessTokenCallbackClass.class.getName());
+        pc = (SQLServerPooledConnection) ds.getPooledConnection();
+        try (Connection conn1 = pc.getConnection()) {}
+    }
+
     @AfterAll
     public static void terminate() throws SQLException {
         try (Connection conn = DriverManager.getConnection(adPasswordConnectionStr);
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/BasicConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/BasicConnectionTest.java
index 52f08dfd..6b7d5c5d 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/BasicConnectionTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/BasicConnectionTest.java
@@ -52,8 +52,8 @@ public class BasicConnectionTest extends AbstractTest {
     @Tag(Constants.fedAuth)
     public void testBasicConnectionAAD() throws Exception {
         // retry since this could fail due to server throttling
-        int retry = THROTTLE_RETRY_COUNT;
-        while (retry > 0) {
+        int retry = 1;
+        while (retry <= THROTTLE_RETRY_COUNT) {
             try {
                 String azureServer = getConfiguredProperty("azureServer");
                 String azureDatabase = getConfiguredProperty("azureDatabase");
@@ -64,15 +64,17 @@ public class BasicConnectionTest extends AbstractTest {
                 basicReconnect("jdbc:sqlserver://" + azureServer + ";database=" + azureDatabase + ";user="
                         + azureUserName + ";password=" + azurePassword
                         + ";loginTimeout=90;Authentication=ActiveDirectoryPassword");
+		retry = THROTTLE_RETRY_COUNT + 1;
             } catch (Exception e) {
                 if (e.getMessage().matches(TestUtils.formatErrorMsg("R_crClientAllRecoveryAttemptsFailed"))) {
-                    System.out.println(e.getMessage() + "Recovery failed retry #" + retry + " in "
+                    System.out.println(e.getMessage() + ". Recovery failed, retry #" + retry + " in "
                             + THROTTLE_RETRY_INTERVAL + " ms");
-                    e.printStackTrace();
 
                     Thread.sleep(THROTTLE_RETRY_INTERVAL);
-                    retry--;
+                    retry++;
                 } else {
+                    e.printStackTrace();
+
                     fail(e.getMessage());
                 }
             }
diff --git a/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java b/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java
index a504f9ca..c3656186 100644
--- a/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java
+++ b/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java
@@ -32,6 +32,7 @@ import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
 
+import com.microsoft.aad.msal4j.PublicClientApplication;
 import com.microsoft.sqlserver.jdbc.ISQLServerDataSource;
 import com.microsoft.sqlserver.jdbc.SQLServerColumnEncryptionAzureKeyVaultProvider;
 import com.microsoft.sqlserver.jdbc.SQLServerColumnEncryptionJavaKeyStoreProvider;
@@ -101,6 +102,8 @@ public abstract class AbstractTest {
     protected static String connectionString = null;
     protected static String connectionStringNTLM;
 
+    protected static PublicClientApplication fedauthPcaApp = null;
+
     private static boolean determinedSqlAzureOrSqlServer = false;
     private static boolean determinedSqlOS = false;
     private static boolean isSqlAzure = false;