com.microsoft.sqlserver.jdbc.IOBuffer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of mssql-jdbc Show documentation
Show all versions of mssql-jdbc Show documentation
Microsoft JDBC 4.1 Driver for SQL Server.
The Azure Key Vault feature in Microsoft JDBC Driver for SQL Server depends on
Azure SDK for JAVA and Azure Active Directory Library For Java.
//---------------------------------------------------------------------------------------------------------------------------------
// File: IOBuffer.java
//
//
// Microsoft JDBC Driver for SQL Server
// Copyright(c) Microsoft Corporation
// All rights reserved.
// MIT License
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files(the ""Software""),
// to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and / or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions :
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//---------------------------------------------------------------------------------------------------------------------------------
package com.microsoft.sqlserver.jdbc;
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.net.*;
import java.math.*;
import java.util.concurrent.*;
import java.util.*;
import java.util.logging.*;
import java.text.MessageFormat;
import java.time.*;
import java.util.Map.Entry;
import javax.net.ssl.*;
import javax.xml.bind.DatatypeConverter;
import java.security.*;
import java.security.cert.*;
import java.sql.Timestamp;
final class TDS
{
// TDS protocol versions
static final int VER_DENALI = 0x74000004; // TDS 7.4
static final int VER_KATMAI = 0x730B0003; // TDS 7.3B(includes null bit compression)
static final int VER_YUKON = 0x72090002; // TDS 7.2
static final int VER_UNKNOWN = 0x00000000; // Unknown/uninitialized
static final int TDS_RET_STAT = 0x79;
static final int TDS_COLMETADATA = 0x81;
static final int TDS_TABNAME = 0xA4;
static final int TDS_COLINFO = 0xA5;
static final int TDS_ORDER = 0xA9;
static final int TDS_ERR = 0xAA;
static final int TDS_MSG = 0xAB;
static final int TDS_RETURN_VALUE = 0xAC;
static final int TDS_LOGIN_ACK = 0xAD;
static final int TDS_FEATURE_EXTENSION_ACK = 0xAE;
static final int TDS_ROW = 0xD1;
static final int TDS_NBCROW = 0xD2;
static final int TDS_ENV_CHG = 0xE3;
static final int TDS_SSPI = 0xED;
static final int TDS_DONE = 0xFD;
static final int TDS_DONEPROC = 0xFE;
static final int TDS_DONEINPROC = 0xFF;
static final int TDS_FEDAUTHINFO = 0xEE;
//FedAuth
static final int TDS_FEATURE_EXT_FEDAUTH = 0x02;
static final int TDS_FEDAUTH_LIBRARY_SECURITYTOKEN = 0x01;
static final int TDS_FEDAUTH_LIBRARY_ADAL = 0x02;
static final int TDS_FEDAUTH_LIBRARY_RESERVED = 0x7F;
static final byte ADALWORKFLOW_ACTIVEDIRECTORYPASSWORD = 0x01;
static final byte ADALWORKFLOW_ACTIVEDIRECTORYINTEGRATED = 0x02;
static final byte FEDAUTH_INFO_ID_STSURL = 0x01; // FedAuthInfoData is token endpoint URL from which to acquire fed auth token
static final byte FEDAUTH_INFO_ID_SPN = 0x02; // FedAuthInfoData is the SPN to use for acquiring fed auth token
// AE constants
static final int TDS_FEATURE_EXT_AE = 0x04;
static final int MAX_SUPPORTED_TCE_VERSION = 0x01; // max version
static final int CUSTOM_CIPHER_ALGORITHM_ID = 0; // max version
static final int AES_256_CBC = 1;
static final int AEAD_AES_256_CBC_HMAC_SHA256 = 2;
static final int AE_METADATA = 0x08;
static final int TDS_TVP = 0xF3;
static final int TVP_ROW = 0x01;
static final int TVP_NULL_TOKEN = 0xFFFF;
static final int TVP_STATUS_DEFAULT = 0x02;
static final int TVP_ORDER_UNIQUE_TOKEN = 0x10;
// TVP_ORDER_UNIQUE_TOKEN flags
static final byte TVP_ORDERASC_FLAG = 0x1;
static final byte TVP_ORDERDESC_FLAG = 0x2;
static final byte TVP_UNIQUE_FLAG = 0x4;
// TVP flags, may be used in other places
static final int FLAG_NULLABLE = 0x01;
static final int FLAG_TVP_DEFAULT_COLUMN = 0x200;
static final int FEATURE_EXT_TERMINATOR = -1;
static final String getTokenName(int tdsTokenType)
{
switch (tdsTokenType)
{
case TDS_RET_STAT: return "TDS_RET_STAT (0x79)";
case TDS_COLMETADATA: return "TDS_COLMETADATA (0x81)";
case TDS_TABNAME: return "TDS_TABNAME (0xA4)";
case TDS_COLINFO: return "TDS_COLINFO (0xA5)";
case TDS_ORDER: return "TDS_ORDER (0xA9)";
case TDS_ERR: return "TDS_ERR (0xAA)";
case TDS_MSG: return "TDS_MSG (0xAB)";
case TDS_RETURN_VALUE: return "TDS_RETURN_VALUE (0xAC)";
case TDS_LOGIN_ACK: return "TDS_LOGIN_ACK (0xAD)";
case TDS_FEATURE_EXTENSION_ACK: return "TDS_FEATURE_EXTENSION_ACK (0xAE)";
case TDS_ROW: return "TDS_ROW (0xD1)";
case TDS_NBCROW: return "TDS_NBCROW (0xD2)";
case TDS_ENV_CHG: return "TDS_ENV_CHG (0xE3)";
case TDS_SSPI: return "TDS_SSPI (0xED)";
case TDS_DONE: return "TDS_DONE (0xFD)";
case TDS_DONEPROC: return "TDS_DONEPROC (0xFE)";
case TDS_DONEINPROC: return "TDS_DONEINPROC (0xFF)";
case TDS_FEDAUTHINFO: return "TDS_FEDAUTHINFO (0xEE)";
default: return "unknown token (0x" + Integer.toHexString(tdsTokenType).toUpperCase() + ")";
}
}
// RPC ProcIDs for use with RPCRequest (PKT_RPC) calls
static final short PROCID_SP_CURSOR = 1;
static final short PROCID_SP_CURSOROPEN = 2;
static final short PROCID_SP_CURSORPREPARE = 3;
static final short PROCID_SP_CURSOREXECUTE = 4;
static final short PROCID_SP_CURSORPREPEXEC = 5;
static final short PROCID_SP_CURSORUNPREPARE = 6;
static final short PROCID_SP_CURSORFETCH = 7;
static final short PROCID_SP_CURSOROPTION = 8;
static final short PROCID_SP_CURSORCLOSE = 9;
static final short PROCID_SP_EXECUTESQL = 10;
static final short PROCID_SP_PREPARE = 11;
static final short PROCID_SP_EXECUTE = 12;
static final short PROCID_SP_PREPEXEC = 13;
static final short PROCID_SP_PREPEXECRPC = 14;
static final short PROCID_SP_UNPREPARE = 15;
// Constants for use with cursor RPCs
static final short SP_CURSOR_OP_UPDATE = 1;
static final short SP_CURSOR_OP_DELETE = 2;
static final short SP_CURSOR_OP_INSERT = 4;
static final short SP_CURSOR_OP_REFRESH = 8;
static final short SP_CURSOR_OP_LOCK = 16;
static final short SP_CURSOR_OP_SETPOSITION = 32;
static final short SP_CURSOR_OP_ABSOLUTE = 64;
// Constants for server-cursored result sets.
// See the Engine Cursors Functional Specification for details.
static final int FETCH_FIRST = 1;
static final int FETCH_NEXT = 2;
static final int FETCH_PREV = 4;
static final int FETCH_LAST = 8;
static final int FETCH_ABSOLUTE = 16;
static final int FETCH_RELATIVE = 32;
static final int FETCH_REFRESH = 128;
static final int FETCH_INFO = 256;
static final int FETCH_PREV_NOADJUST = 512;
static final byte RPC_OPTION_NO_METADATA = (byte) 0x02;
// Transaction manager request types
static final short TM_GET_DTC_ADDRESS = 0;
static final short TM_PROPAGATE_XACT = 1;
static final short TM_BEGIN_XACT = 5;
static final short TM_PROMOTE_PROMOTABLE_XACT = 6;
static final short TM_COMMIT_XACT = 7;
static final short TM_ROLLBACK_XACT = 8;
static final short TM_SAVE_XACT = 9;
static final byte PKT_QUERY = 1;
static final byte PKT_RPC = 3;
static final byte PKT_REPLY = 4;
static final byte PKT_CANCEL_REQ = 6;
static final byte PKT_BULK = 7;
static final byte PKT_DTC = 14;
static final byte PKT_LOGON70 = 16; //0x10
static final byte PKT_SSPI = 17;
static final byte PKT_PRELOGIN = 18; //0x12
static final byte PKT_FEDAUTH_TOKEN_MESSAGE = 8; // Authentication token for federated authentication
static final byte STATUS_NORMAL = 0x00;
static final byte STATUS_BIT_EOM = 0x01;
static final byte STATUS_BIT_ATTENTION = 0x02;//this is called ignore bit in TDS spec
static final byte STATUS_BIT_RESET_CONN = 0x08;
// Various TDS packet size constants
static final int INVALID_PACKET_SIZE = -1;
static final int INITIAL_PACKET_SIZE = 4096;
static final int MIN_PACKET_SIZE = 512;
static final int MAX_PACKET_SIZE = 32767;
static final int DEFAULT_PACKET_SIZE = 8000;
static final int SERVER_PACKET_SIZE = 0; // Accept server's configured packet size
// TDS packet header size and offsets
static final int PACKET_HEADER_SIZE = 8;
static final int PACKET_HEADER_MESSAGE_TYPE = 0;
static final int PACKET_HEADER_MESSAGE_STATUS = 1;
static final int PACKET_HEADER_MESSAGE_LENGTH = 2;
static final int PACKET_HEADER_SPID = 4;
static final int PACKET_HEADER_SEQUENCE_NUM = 6;
static final int PACKET_HEADER_WINDOW = 7; // Reserved/Not used
// MARS header length:
// 2 byte header type
// 8 byte transaction descriptor
// 4 byte outstanding request count
static final int MARS_HEADER_LENGTH = 18; // 2 byte header type, 8 byte transaction descriptor,
static final int TRACE_HEADER_LENGTH = 26; // header length (4) + header type (2) + guid (16) + Sequence number size (4)
static final short HEADERTYPE_TRACE = 3; // trace header type
// Message header length
static final int MESSAGE_HEADER_LENGTH = MARS_HEADER_LENGTH + 4; // length includes message header itself
static final byte B_PRELOGIN_OPTION_VERSION = 0x00;
static final byte B_PRELOGIN_OPTION_ENCRYPTION = 0x01;
static final byte B_PRELOGIN_OPTION_INSTOPT = 0x02;
static final byte B_PRELOGIN_OPTION_THREADID = 0x03;
static final byte B_PRELOGIN_OPTION_MARS = 0x04;
static final byte B_PRELOGIN_OPTION_TRACEID = 0x05;
static final byte B_PRELOGIN_OPTION_FEDAUTHREQUIRED = 0x06;
static final byte B_PRELOGIN_OPTION_TERMINATOR = (byte)0xFF;
// Login option byte 1
static final byte LOGIN_OPTION1_ORDER_X86 = 0x00;
static final byte LOGIN_OPTION1_ORDER_6800 = 0x01;
static final byte LOGIN_OPTION1_CHARSET_ASCII = 0x00;
static final byte LOGIN_OPTION1_CHARSET_EBCDIC = 0x02;
static final byte LOGIN_OPTION1_FLOAT_IEEE_754 = 0x00;
static final byte LOGIN_OPTION1_FLOAT_VAX = 0x04;
static final byte LOGIN_OPTION1_FLOAT_ND5000 = 0x08;
static final byte LOGIN_OPTION1_DUMPLOAD_ON = 0x00;
static final byte LOGIN_OPTION1_DUMPLOAD_OFF = 0x10;
static final byte LOGIN_OPTION1_USE_DB_ON = 0x00;
static final byte LOGIN_OPTION1_USE_DB_OFF = 0x20;
static final byte LOGIN_OPTION1_INIT_DB_WARN = 0x00;
static final byte LOGIN_OPTION1_INIT_DB_FATAL = 0x40;
static final byte LOGIN_OPTION1_SET_LANG_OFF = 0x00;
static final byte LOGIN_OPTION1_SET_LANG_ON = (byte)0x80;
// Login option byte 2
static final byte LOGIN_OPTION2_INIT_LANG_WARN = 0x00;
static final byte LOGIN_OPTION2_INIT_LANG_FATAL = 0x01;
static final byte LOGIN_OPTION2_ODBC_OFF = 0x00;
static final byte LOGIN_OPTION2_ODBC_ON = 0x02;
static final byte LOGIN_OPTION2_TRAN_BOUNDARY_OFF = 0x00;
static final byte LOGIN_OPTION2_TRAN_BOUNDARY_ON = 0x04;
static final byte LOGIN_OPTION2_CACHE_CONNECTION_OFF = 0x00;
static final byte LOGIN_OPTION2_CACHE_CONNECTION_ON = 0x08;
static final byte LOGIN_OPTION2_USER_NORMAL = 0x00;
static final byte LOGIN_OPTION2_USER_SERVER = 0x10;
static final byte LOGIN_OPTION2_USER_REMUSER = 0x20;
static final byte LOGIN_OPTION2_USER_SQLREPL = 0x30;
static final byte LOGIN_OPTION2_INTEGRATED_SECURITY_OFF = 0x00;
static final byte LOGIN_OPTION2_INTEGRATED_SECURITY_ON = (byte)0x80;
// Login option byte 3
static final byte LOGIN_OPTION3_DEFAULT = 0x00;
static final byte LOGIN_OPTION3_CHANGE_PASSWORD = 0x01;
static final byte LOGIN_OPTION3_SEND_YUKON_BINARY_XML = 0x02;
static final byte LOGIN_OPTION3_USER_INSTANCE = 0x04;
static final byte LOGIN_OPTION3_UNKNOWN_COLLATION_HANDLING = 0x08;
static final byte LOGIN_OPTION3_FEATURE_EXTENSION = 0x10;
// Login type flag (bits 5 - 7 reserved for future use)
static final byte LOGIN_SQLTYPE_DEFAULT = 0x00;
static final byte LOGIN_SQLTYPE_TSQL = 0x01;
static final byte LOGIN_SQLTYPE_ANSI_V1 = 0x02;
static final byte LOGIN_SQLTYPE_ANSI89_L1 = 0x03;
static final byte LOGIN_SQLTYPE_ANSI89_L2 = 0x04;
static final byte LOGIN_SQLTYPE_ANSI89_IEF = 0x05;
static final byte LOGIN_SQLTYPE_ANSI89_ENTRY = 0x06;
static final byte LOGIN_SQLTYPE_ANSI89_TRANS = 0x07;
static final byte LOGIN_SQLTYPE_ANSI89_INTER = 0x08;
static final byte LOGIN_SQLTYPE_ANSI89_FULL = 0x09;
static final byte LOGIN_OLEDB_OFF = 0x00;
static final byte LOGIN_OLEDB_ON = 0x10;
static final byte LOGIN_READ_ONLY_INTENT = 0x20;
static final byte LOGIN_READ_WRITE_INTENT = 0x00;
static final byte ENCRYPT_OFF = 0x00;
static final byte ENCRYPT_ON = 0x01;
static final byte ENCRYPT_NOT_SUP = 0x02;
static final byte ENCRYPT_REQ = 0x03;
static final byte ENCRYPT_INVALID = (byte)0xFF;
static final String getEncryptionLevel(int level)
{
switch (level)
{
case ENCRYPT_OFF: return "OFF";
case ENCRYPT_ON: return "ON";
case ENCRYPT_NOT_SUP: return "NOT SUPPORTED";
case ENCRYPT_REQ: return "REQUIRED";
default: return "unknown encryption level (0x" + Integer.toHexString(level).toUpperCase() + ")";
}
}
// Prelogin packet length, including the tds header,
// version, encrpytion, and traceid data sessions.
// For detailed info, please check the definition of
// preloginRequest in Prelogin function.
static final byte B_PRELOGIN_MESSAGE_LENGTH = 67;
static final byte B_PRELOGIN_MESSAGE_LENGTH_WITH_FEDAUTH = 73;
// Scroll options and concurrency options lifted out
// of the the Yukon cursors spec for sp_cursoropen.
final static int SCROLLOPT_KEYSET = 1;
final static int SCROLLOPT_DYNAMIC = 2;
final static int SCROLLOPT_FORWARD_ONLY = 4;
final static int SCROLLOPT_STATIC = 8;
final static int SCROLLOPT_FAST_FORWARD = 16;
final static int SCROLLOPT_PARAMETERIZED_STMT = 4096;
final static int SCROLLOPT_AUTO_FETCH = 8192;
final static int SCROLLOPT_AUTO_CLOSE = 16384;
final static int CCOPT_READ_ONLY = 1;
final static int CCOPT_SCROLL_LOCKS = 2;
final static int CCOPT_OPTIMISTIC_CC = 4;
final static int CCOPT_OPTIMISTIC_CCVAL = 8;
final static int CCOPT_ALLOW_DIRECT = 8192;
final static int CCOPT_UPDT_IN_PLACE = 16384;
// Result set rows include an extra, "hidden" ROWSTAT column which indicates
// the overall success or failure of the row fetch operation. With a keyset
// cursor, the value in the ROWSTAT column indicates whether the row has been
// deleted from the database.
static final int ROWSTAT_FETCH_SUCCEEDED = 1;
static final int ROWSTAT_FETCH_MISSING = 2;
// ColumnInfo status
final static int COLINFO_STATUS_EXPRESSION = 0x04;
final static int COLINFO_STATUS_KEY = 0x08;
final static int COLINFO_STATUS_HIDDEN = 0x10;
final static int COLINFO_STATUS_DIFFERENT_NAME = 0x20;
final static int MAX_FRACTIONAL_SECONDS_SCALE = 7;
final static Timestamp MAX_TIMESTAMP = Timestamp.valueOf("2079-06-06 23:59:59");
final static Timestamp MIN_TIMESTAMP = Timestamp.valueOf("1900-01-01 00:00:00");
static int nanosSinceMidnightLength(int scale)
{
final int[] scaledLengths = {3, 3, 3, 4, 4, 5, 5, 5};
assert scale >= 0;
assert scale <= MAX_FRACTIONAL_SECONDS_SCALE;
return scaledLengths[scale];
}
final static int DAYS_INTO_CE_LENGTH = 3;
final static int MINUTES_OFFSET_LENGTH = 2;
// Number of days in a "normal" (non-leap) year according to SQL Server.
final static int DAYS_PER_YEAR = 365;
final static int BASE_YEAR_1900 = 1900;
final static int BASE_YEAR_1970 = 1970;
final static String BASE_DATE_1970 = "1970-01-01";
static int timeValueLength(int scale)
{
return
nanosSinceMidnightLength(scale);
}
static int datetime2ValueLength(int scale)
{
return
DAYS_INTO_CE_LENGTH +
nanosSinceMidnightLength(scale);
}
static int datetimeoffsetValueLength(int scale)
{
return
DAYS_INTO_CE_LENGTH +
MINUTES_OFFSET_LENGTH +
nanosSinceMidnightLength(scale);
}
// TDS is just a namespace - it can't be instantiated.
private TDS() {}
}
class Nanos
{
static final int PER_SECOND = 1000000000;
static final int PER_MAX_SCALE_INTERVAL = PER_SECOND / (int) Math.pow(10, TDS.MAX_FRACTIONAL_SECONDS_SCALE);
static final int PER_MILLISECOND = PER_SECOND / 1000;
static final long PER_DAY = 24 * 60 * 60 * (long)PER_SECOND;
private Nanos() {}
}
// Constants relating to the historically accepted Julian-Gregorian calendar cutover date (October 15, 1582).
//
// Used in processing SQL Server temporal data types whose date component may precede that date.
//
// Scoping these constants to a class defers their initialization to first use.
class GregorianChange
{
// Cutover date for a pure Gregorian calendar - that is, a proleptic Gregorian calendar with
// Gregorian leap year behavior throughout its entire range. This is the cutover date is used
// with temporal server values, which are represented in terms of number of days relative to a
// base date.
static final java.util.Date PURE_CHANGE_DATE = new java.util.Date(Long.MIN_VALUE);
// The standard Julian to Gregorian cutover date (October 15, 1582) that the JDBC temporal
// classes (Time, Date, Timestamp) assume when converting to and from their UTC milliseconds
// representations.
static final java.util.Date STANDARD_CHANGE_DATE = (new GregorianCalendar(Locale.US)).getGregorianChange();
// A hint as to the number of days since 1/1/0001, past which we do not need to
// not rationalize the difference between SQL Server behavior (pure Gregorian)
// and Java behavior (standard Gregorian).
//
// Not having to rationalize the difference has a substantial (measured) performance benefit
// for temporal getters.
//
// The hint does not need to be exact, as long as it's later than the actual change date.
static final int DAYS_SINCE_BASE_DATE_HINT = DDC.daysSinceBaseDate(1583, 1, 1);
//Extra days that need to added to a pure gregorian date, post the gergorian
//cut over date, to match the default julian-gregorain calendar date of java.
static final int EXTRA_DAYS_TO_BE_ADDED;
static
{
//This issue refers to the following bugs in java(same issue).
//http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=7109480
//http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6459836
//The issue is fixed in JRE 1.7
//and exists in all the older versions.
//Due to the above bug, in older JVM versions(1.6 and before),
//the date calculation is incorrect at the Gregorian cut over date.
//i.e. the next date after Oct 4th 1582 is Oct 17th 1582, where as
// it should have been Oct 15th 1582.
//We intentionally do not make a check based on JRE version.
//If we do so, our code would break if the bug is fixed in a later update
//to an older JRE. So, we check for the existence of the bug instead.
GregorianCalendar cal= new GregorianCalendar(Locale.US);
cal.clear();
cal.set(1,1,577738,0,0,0);//577738 = 1+577737(no of days since epoch that brings us to oct 15th 1582)
if(cal.get(Calendar.DAY_OF_MONTH) == 15)
{
//If the date calculation is correct(the above bug is fixed),
//post the default gregorian cut over date, the pure gregorian date
//falls short by two days for all dates compared to julian-gregorian date.
//so, we add two extra days for functional correctness.
//Note: other ways, in which this issue can be fixed instead of
// trying to detect the JVM bug is
//a) use unoptimized code path in the function convertTemporalToObject
//b) use cal.add api instead of cal.set api in the current optimized code path
//In both the above approaches, the code is about 6-8 times slower,
//resulting in an overall perf regression of about (10-30)% for perf test cases
EXTRA_DAYS_TO_BE_ADDED = 2;
}
else
EXTRA_DAYS_TO_BE_ADDED = 0;
}
private GregorianChange() {}
}
// UTC/GMT time zone singleton. The enum type delays initialization until first use.
enum UTC
{
INSTANCE;
static final TimeZone timeZone = new SimpleTimeZone(0, "UTC");
}
final class TDSChannel
{
private static final Logger logger = Logger.getLogger("com.microsoft.sqlserver.jdbc.internals.TDS.Channel");
final Logger getLogger() { return logger; }
private final String traceID;
final public String toString() { return traceID; }
private final SQLServerConnection con;
private final TDSWriter tdsWriter;
final TDSWriter getWriter() { return tdsWriter; }
final TDSReader getReader(TDSCommand command) { return new TDSReader(this, con, command); }
// Socket for raw TCP/IP communications with SQL Server
private Socket tcpSocket;
// Socket for SSL-encrypted communications with SQL Server
private SSLSocket sslSocket;
// Socket providing the communications interface to the driver.
// For SSL-encrypted connections, this is the SSLSocket wrapped
// around the TCP socket. For unencrypted connections, it is
// just the TCP socket itself.
private Socket channelSocket;
// Implementation of a Socket proxy that can switch from TDS-wrapped I/O
// (using the TDSChannel itself) during SSL handshake to raw I/O over
// the TCP/IP socket.
ProxySocket proxySocket = null;
// I/O streams for raw TCP/IP communications with SQL Server
private InputStream tcpInputStream;
private OutputStream tcpOutputStream;
// I/O streams providing the communications interface to the driver.
// For SSL-encrypted connections, these are streams obtained from
// the SSL socket above. They wrap the underlying TCP streams.
// For unencrypted connections, they are just the TCP streams themselves.
private InputStream inputStream;
private OutputStream outputStream;
/** TDS packet payload logger */
private static Logger packetLogger = Logger.getLogger("com.microsoft.sqlserver.jdbc.internals.TDS.DATA");
private final boolean isLoggingPackets = packetLogger.isLoggable(Level.FINEST);
final boolean isLoggingPackets() { return isLoggingPackets; }
// Number of TDS messages sent to and received from the server
int numMsgsSent = 0;
int numMsgsRcvd = 0;
// Last SPID received from the server. Used for logging and to tag subsequent outgoing
// packets to facilitate diagnosing problems from the server side.
private int spid = 0;
void setSPID(int spid) { this.spid = spid; }
int getSPID() { return spid; }
void resetPooledConnection(){tdsWriter.resetPooledConnection();}
TDSChannel(SQLServerConnection con)
{
this.con = con;
traceID = "TDSChannel (" + con.toString() + ")";
this.tcpSocket = null;
this.sslSocket = null;
this.channelSocket = null;
this.tcpInputStream = null;
this.tcpOutputStream = null;
this.inputStream = null;
this.outputStream = null;
this.tdsWriter = new TDSWriter(this, con);
}
/**
* Opens the physical communications channel (TCP/IP socket and I/O streams) to the SQL Server.
*/
final void open(
String host,
int port,
int timeoutMillis,
boolean useParallel,
boolean useTnir,
boolean isTnirFirstAttempt,
int timeoutMillisForFullTimeout) throws SQLServerException
{
if (logger.isLoggable(Level.FINER))
logger.finer(this.toString() + ": Opening TCP socket...");
SocketFinder socketFinder = new SocketFinder(traceID, con);
channelSocket = tcpSocket = socketFinder.findSocket(host, port, timeoutMillis, useParallel, useTnir, isTnirFirstAttempt, timeoutMillisForFullTimeout);
try
{
// Set socket options
tcpSocket.setTcpNoDelay(true);
tcpSocket.setKeepAlive(true);
inputStream = tcpInputStream = tcpSocket.getInputStream();
outputStream = tcpOutputStream = tcpSocket.getOutputStream();
}
catch(IOException ex)
{
SQLServerException.ConvertConnectExceptionToSQLServerException(host, port, con, ex);
}
}
/**
* Disables SSL on this TDS channel.
*/
void disableSSL()
{
if (logger.isLoggable(Level.FINER))
logger.finer(toString() + " Disabling SSL...");
/*
* The mission:
* To close the SSLSocket and release everything that it is holding onto other
* than the TCP/IP socket and streams.
*
* The challenge:
* Simply closing the SSLSocket tries to do additional, unnecessary shutdown I/O
* over the TCP/IP streams that are bound to the socket proxy, resulting in a
* hang and confusing SQL Server.
*
* Solution:
* Rewire the ProxySocket's input and output streams (one more time) to closed
* streams. SSLSocket sees that the streams are already closed and does not
* attempt to do any further I/O on them before closing itself.
*/
// Create a couple of cheap closed streams
InputStream is = new ByteArrayInputStream(new byte[0]);
try
{
is.close();
}
catch (IOException e)
{
// No reason to expect a brand new ByteArrayInputStream not to close,
// but just in case...
logger.fine("Ignored error closing InputStream: " + e.getMessage());
}
OutputStream os = new ByteArrayOutputStream();
try
{
os.close();
}
catch (IOException e)
{
// No reason to expect a brand new ByteArrayOutputStream not to close,
// but just in case...
logger.fine("Ignored error closing OutputStream: " + e.getMessage());
}
// Rewire the proxy socket to the closed streams
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Rewiring proxy streams for SSL socket close");
proxySocket.setStreams(is, os);
// Now close the SSL socket. It will see that the proxy socket's streams
// are closed and not try to do any further I/O over them.
try
{
if (logger.isLoggable(Level.FINER))
logger.finer(toString() + " Closing SSL socket");
sslSocket.close();
}
catch (IOException e)
{
// Don't care if we can't close the SSL socket. We're done with it anyway.
logger.fine("Ignored error closing SSLSocket: " + e.getMessage());
}
// Do not close the proxy socket. Doing so would close our TCP socket
// to which the proxy socket is bound. Instead, just null out the reference
// to free up the few resources it holds onto.
proxySocket = null;
// Finally, with all of the SSL support out of the way, put the TDSChannel
// back to using the TCP/IP socket and streams directly.
inputStream = tcpInputStream;
outputStream = tcpOutputStream;
channelSocket = tcpSocket;
sslSocket = null;
if (logger.isLoggable(Level.FINER))
logger.finer(toString() + " SSL disabled");
}
/**
* Used during SSL handshake, this class implements an InputStream that reads SSL handshake response
* data (framed in TDS messages) from the TDS channel.
*/
private class SSLHandshakeInputStream extends InputStream
{
private final TDSReader tdsReader;
private final SSLHandshakeOutputStream sslHandshakeOutputStream;
private final Logger logger;
private final String logContext;
SSLHandshakeInputStream(TDSChannel tdsChannel, SSLHandshakeOutputStream sslHandshakeOutputStream)
{
this.tdsReader = tdsChannel.getReader(null);
this.sslHandshakeOutputStream = sslHandshakeOutputStream;
this.logger = tdsChannel.getLogger();
this.logContext = tdsChannel.toString() + " (SSLHandshakeInputStream):";
}
/**
* If there is no handshake response data available to be read from existing packets
* then this method ensures that the SSL handshake output stream has been flushed
* to the server, and reads another packet (starting the next TDS response message).
*
* Note that simply using TDSReader.ensurePayload isn't sufficient as it does not
* automatically start the new response message.
*/
private final void ensureSSLPayload() throws IOException
{
if (0 == tdsReader.available())
{
if (logger.isLoggable(Level.FINEST))
logger.finest(logContext + " No handshake response bytes available. Flushing SSL handshake output stream.");
try
{
sslHandshakeOutputStream.endMessage();
}
catch (SQLServerException e)
{
logger.finer(logContext + " Ending TDS message threw exception:" + e.getMessage());
throw new IOException(e.getMessage());
}
if (logger.isLoggable(Level.FINEST))
logger.finest(logContext + " Reading first packet of SSL handshake response");
try
{
tdsReader.readPacket();
}
catch (SQLServerException e)
{
logger.finer(logContext + " Reading response packet threw exception:" + e.getMessage());
throw new IOException(e.getMessage());
}
}
}
public long skip(long n) throws IOException
{
if (logger.isLoggable(Level.FINEST))
logger.finest(logContext + " Skipping " + n + " bytes...");
if (n <= 0) return 0;
if (n > Integer.MAX_VALUE)
n = Integer.MAX_VALUE;
ensureSSLPayload();
try
{
tdsReader.skip((int) n);
}
catch (SQLServerException e)
{
logger.finer(logContext + " Skipping bytes threw exception:" + e.getMessage());
throw new IOException(e.getMessage());
}
return n;
}
private final byte oneByte[] = new byte[1];
public int read() throws IOException
{
int bytesRead;
while (0 == (bytesRead = readInternal(oneByte, 0, oneByte.length)))
;
assert 1 == bytesRead || -1 == bytesRead;
return 1 == bytesRead ? oneByte[0] : -1;
}
public int read(byte[] b) throws IOException
{
return readInternal(b, 0, b.length);
}
public int read(byte b[], int offset, int maxBytes) throws IOException
{
return readInternal(b, offset, maxBytes);
}
private int readInternal(byte b[], int offset, int maxBytes) throws IOException
{
if (logger.isLoggable(Level.FINEST))
logger.finest(logContext + " Reading " + maxBytes + " bytes...");
ensureSSLPayload();
try
{
tdsReader.readBytes(b, offset, maxBytes);
}
catch (SQLServerException e)
{
logger.finer(logContext + " Reading bytes threw exception:" + e.getMessage());
throw new IOException(e.getMessage());
}
return maxBytes;
}
}
/**
* Used during SSL handshake, this class implements an OutputStream that writes SSL handshake request
* data (framed in TDS messages) to the TDS channel.
*/
private class SSLHandshakeOutputStream extends OutputStream
{
private final TDSWriter tdsWriter;
/** Flag indicating when it is necessary to start a new prelogin TDS message */
private boolean messageStarted;
private final Logger logger;
private final String logContext;
SSLHandshakeOutputStream(TDSChannel tdsChannel)
{
this.tdsWriter = tdsChannel.getWriter();
this.messageStarted = false;
this.logger = tdsChannel.getLogger();
this.logContext = tdsChannel.toString() + " (SSLHandshakeOutputStream):";
}
public void flush() throws IOException
{
// It seems that the security provider implementation in some JVMs
// (notably SunJSSE in the 6.0 JVM) likes to add spurious calls to
// flush the SSL handshake output stream during SSL handshaking.
// We need to ignore these calls because the SSL handshake payload
// needs to be completely encapsulated in TDS. The SSL handshake
// input stream always ensures that this output stream has been flushed
// before trying to read the response.
if (logger.isLoggable(Level.FINEST))
logger.finest(logContext + " Ignored a request to flush the stream");
}
void endMessage() throws SQLServerException
{
// We should only be asked to end the message if we have started one
assert messageStarted;
if (logger.isLoggable(Level.FINEST))
logger.finest(logContext + " Finishing TDS message");
// Flush any remaining bytes through the writer. Since there may be fewer bytes
// ready to send than a full TDS packet, we end the message here and start a new
// one later if additional handshake data needs to be sent.
tdsWriter.endMessage();
messageStarted = false;
}
private final byte singleByte[] = new byte[1];
public void write(int b) throws IOException
{
singleByte[0] = (byte)(b & 0xFF);
writeInternal(singleByte, 0, singleByte.length);
}
public void write(byte[] b) throws IOException
{
writeInternal(b, 0, b.length);
}
public void write(byte[] b, int off, int len) throws IOException
{
writeInternal(b, off, len);
}
private void writeInternal(byte[] b, int off, int len) throws IOException
{
try
{
// Start out the handshake request in a new prelogin message. Subsequent
// writes just add handshake data to the request until flushed.
if (!messageStarted)
{
if (logger.isLoggable(Level.FINEST))
logger.finest(logContext + " Starting new TDS packet...");
tdsWriter.startMessage(null, TDS.PKT_PRELOGIN);
messageStarted = true;
}
if (logger.isLoggable(Level.FINEST))
logger.finest(logContext + " Writing " + len + " bytes...");
tdsWriter.writeBytes(b, off, len);
}
catch (SQLServerException e)
{
logger.finer(logContext + " Writing bytes threw exception:" + e.getMessage());
throw new IOException(e.getMessage());
}
}
}
/**
* This class implements an InputStream that just forwards all of its methods to an underlying InputStream.
*
* It is more predictable than FilteredInputStream which forwards some of its read methods directly
* to the underlying stream, but not others.
*/
private final class ProxyInputStream extends InputStream
{
private InputStream filteredStream;
ProxyInputStream(InputStream is)
{
filteredStream = is;
}
final void setFilteredStream(InputStream is)
{
filteredStream = is;
}
public long skip(long n) throws IOException
{
long bytesSkipped;
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Skipping " + n + " bytes");
bytesSkipped = filteredStream.skip(n);
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Skipped " + n + " bytes");
return bytesSkipped;
}
public int available() throws IOException
{
int bytesAvailable = filteredStream.available();
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " " + bytesAvailable + " bytes available");
return bytesAvailable;
}
private final byte oneByte[] = new byte[1];
public int read() throws IOException
{
int bytesRead;
while (0 == (bytesRead = readInternal(oneByte, 0, oneByte.length)))
;
assert 1 == bytesRead || -1 == bytesRead;
return 1 == bytesRead ? oneByte[0] : -1;
}
public int read(byte[] b) throws IOException
{
return readInternal(b, 0, b.length);
}
public int read(byte b[], int offset, int maxBytes) throws IOException
{
return readInternal(b, offset, maxBytes);
}
private int readInternal(byte b[], int offset, int maxBytes) throws IOException
{
int bytesRead;
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Reading " + maxBytes + " bytes");
try
{
bytesRead = filteredStream.read(b, offset, maxBytes);
}
catch (IOException e)
{
if (logger.isLoggable(Level.FINER))
logger.finer(toString() + " " + e.getMessage());
logger.finer(toString() + " Reading bytes threw exception:" + e.getMessage());
throw e;
}
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Read " + bytesRead + " bytes");
return bytesRead;
}
public boolean markSupported()
{
boolean markSupported = filteredStream.markSupported();
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Returning markSupported: " + markSupported);
return markSupported;
}
public void mark(int readLimit)
{
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Marking next " + readLimit + " bytes");
filteredStream.mark(readLimit);
}
public void reset() throws IOException
{
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Resetting to previous mark");
filteredStream.reset();
}
public void close() throws IOException
{
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Closing");
filteredStream.close();
}
}
/**
* This class implements an OutputStream that just forwards all of its methods to an underlying OutputStream.
*
* This class essentially does what FilteredOutputStream does, but is more efficient for our usage.
* FilteredOutputStream transforms block writes to sequences of single-byte writes.
*/
final class ProxyOutputStream extends OutputStream
{
private OutputStream filteredStream;
ProxyOutputStream(OutputStream os)
{
filteredStream = os;
}
final void setFilteredStream(OutputStream os)
{
filteredStream = os;
}
public void close() throws IOException
{
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Closing");
filteredStream.close();
}
public void flush() throws IOException
{
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Flushing");
filteredStream.flush();
}
private final byte singleByte[] = new byte[1];
public void write(int b) throws IOException
{
singleByte[0] = (byte)(b & 0xFF);
writeInternal(singleByte, 0, singleByte.length);
}
public void write(byte[] b) throws IOException
{
writeInternal(b, 0, b.length);
}
public void write(byte[] b, int off, int len) throws IOException
{
writeInternal(b, off, len);
}
private void writeInternal(byte[] b, int off, int len) throws IOException
{
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Writing " + len + " bytes");
filteredStream.write(b, off, len);
}
}
/**
* This class implements a Socket whose I/O streams can be switched from using
* a TDSChannel for I/O to using its underlying TCP/IP socket.
*
* The SSL socket binds to a ProxySocket. The initial SSL handshake is done over
* TDSChannel I/O streams so that the handshake payload is framed in TDS packets.
* The I/O streams are then switched to TCP/IP I/O streams using setStreams,
* and SSL communications continue directly over the TCP/IP I/O streams.
*
* Most methods other than those for getting the I/O streams are simply forwarded
* to the TDSChannel's underlying TCP/IP socket. Methods that change the
* socket binding or provide direct channel access are disallowed.
*/
private class ProxySocket extends Socket
{
private final TDSChannel tdsChannel;
private final Logger logger;
private final String logContext;
private final ProxyInputStream proxyInputStream;
private final ProxyOutputStream proxyOutputStream;
ProxySocket(TDSChannel tdsChannel)
{
this.tdsChannel = tdsChannel;
this.logger = tdsChannel.getLogger();
this.logContext = tdsChannel.toString() + " (ProxySocket):";
// Create the I/O streams
SSLHandshakeOutputStream sslHandshakeOutputStream = new SSLHandshakeOutputStream(tdsChannel);
SSLHandshakeInputStream sslHandshakeInputStream = new SSLHandshakeInputStream(tdsChannel, sslHandshakeOutputStream);
this.proxyOutputStream = new ProxyOutputStream(sslHandshakeOutputStream);
this.proxyInputStream = new ProxyInputStream(sslHandshakeInputStream);
}
void setStreams(InputStream is, OutputStream os)
{
proxyInputStream.setFilteredStream(is);
proxyOutputStream.setFilteredStream(os);
}
public InputStream getInputStream() throws IOException
{
if (logger.isLoggable(Level.FINEST))
logger.finest(logContext + " Getting input stream");
return proxyInputStream;
}
public OutputStream getOutputStream() throws IOException
{
if (logger.isLoggable(Level.FINEST))
logger.finest(logContext + " Getting output stream");
return proxyOutputStream;
}
// Allow methods that should just forward to the underlying TCP socket or return fixed values
public InetAddress getInetAddress() { return tdsChannel.tcpSocket.getInetAddress(); }
public boolean getKeepAlive() throws SocketException { return tdsChannel.tcpSocket.getKeepAlive(); }
public InetAddress getLocalAddress() { return tdsChannel.tcpSocket.getLocalAddress(); }
public int getLocalPort() { return tdsChannel.tcpSocket.getLocalPort(); }
public SocketAddress getLocalSocketAddress() { return tdsChannel.tcpSocket.getLocalSocketAddress(); }
public boolean getOOBInline() throws SocketException { return tdsChannel.tcpSocket.getOOBInline(); }
public int getPort() { return tdsChannel.tcpSocket.getPort(); }
public int getReceiveBufferSize() throws SocketException { return tdsChannel.tcpSocket.getReceiveBufferSize(); }
public SocketAddress getRemoteSocketAddress() { return tdsChannel.tcpSocket.getRemoteSocketAddress(); }
public boolean getReuseAddress() throws SocketException { return tdsChannel.tcpSocket.getReuseAddress(); }
public int getSendBufferSize() throws SocketException { return tdsChannel.tcpSocket.getSendBufferSize(); }
public int getSoLinger() throws SocketException { return tdsChannel.tcpSocket.getSoLinger(); }
public int getSoTimeout() throws SocketException { return tdsChannel.tcpSocket.getSoTimeout(); }
public boolean getTcpNoDelay() throws SocketException { return tdsChannel.tcpSocket.getTcpNoDelay(); }
public int getTrafficClass() throws SocketException { return tdsChannel.tcpSocket.getTrafficClass(); }
public boolean isBound() { return true; }
public boolean isClosed() { return false; }
public boolean isConnected() { return true; }
public boolean isInputShutdown() { return false; }
public boolean isOutputShutdown() { return false; }
public String toString() { return tdsChannel.tcpSocket.toString(); }
public SocketChannel getChannel() { return null; }
// Disallow calls to methods that would change the underlying TCP socket
public void bind(SocketAddress bindPoint) throws IOException
{
logger.finer(logContext + " Disallowed call to bind. Throwing IOException.");
throw new IOException();
}
public void connect(SocketAddress endpoint) throws IOException
{
logger.finer(logContext + " Disallowed call to connect (without timeout). Throwing IOException.");
throw new IOException();
}
public void connect(SocketAddress endpoint, int timeout) throws IOException
{
logger.finer(logContext + " Disallowed call to connect (with timeout). Throwing IOException.");
throw new IOException();
}
// Ignore calls to methods that would otherwise allow the SSL socket
// to directly manipulate the underlying TCP socket
public void close() throws IOException
{
if (logger.isLoggable(Level.FINER))
logger.finer(logContext + " Ignoring close");
}
public void setReceiveBufferSize(int size) throws SocketException
{
if (logger.isLoggable(Level.FINER))
logger.finer(toString() + " Ignoring setReceiveBufferSize size:" + size);
}
public void setSendBufferSize(int size) throws SocketException
{
if (logger.isLoggable(Level.FINER))
logger.finer(toString() + " Ignoring setSendBufferSize size:" + size);
}
public void setReuseAddress(boolean on) throws SocketException
{
if (logger.isLoggable(Level.FINER))
logger.finer(toString() + " Ignoring setReuseAddress");
}
public void setSoLinger(boolean on, int linger) throws SocketException
{
if (logger.isLoggable(Level.FINER))
logger.finer(toString() + " Ignoring setSoLinger");
}
public void setSoTimeout(int timeout) throws SocketException
{
if (logger.isLoggable(Level.FINER))
logger.finer(toString() + " Ignoring setSoTimeout");
}
public void setTcpNoDelay(boolean on) throws SocketException
{
if (logger.isLoggable(Level.FINER))
logger.finer(toString() + " Ignoring setTcpNoDelay");
}
public void setTrafficClass(int tc) throws SocketException
{
if (logger.isLoggable(Level.FINER))
logger.finer(toString() + " Ignoring setTrafficClass");
}
public void shutdownInput() throws IOException
{
if (logger.isLoggable(Level.FINER))
logger.finer(toString() + " Ignoring shutdownInput");
}
public void shutdownOutput() throws IOException
{
if (logger.isLoggable(Level.FINER))
logger.finer(toString() + " Ignoring shutdownOutput");
}
public void sendUrgentData(int data) throws IOException
{
if (logger.isLoggable(Level.FINER))
logger.finer(toString() + " Ignoring sendUrgentData");
}
public void setKeepAlive(boolean on) throws SocketException
{
if (logger.isLoggable(Level.FINER))
logger.finer(toString() + " Ignoring setKeepAlive");
}
public void setOOBInline(boolean on) throws SocketException
{
if (logger.isLoggable(Level.FINER))
logger.finer(toString() + " Ignoring setOOBInline");
}
}
/**
* This class implements an X509TrustManager that always accepts the X509Certificate chain offered to it.
*
* A PermissiveX509TrustManager is used to "verify" the authenticity of the server when the
* trustServerCertificate connection property is set to true.
*/
private final class PermissiveX509TrustManager extends Object implements X509TrustManager
{
private final TDSChannel tdsChannel;
private final Logger logger;
private final String logContext;
PermissiveX509TrustManager(TDSChannel tdsChannel)
{
this.tdsChannel = tdsChannel;
this.logger = tdsChannel.getLogger();
this.logContext = tdsChannel.toString() + " (PermissiveX509TrustManager):";
}
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException
{
if (logger.isLoggable(Level.FINER))
logger.finer(logContext + " Trusting client certificate (!)");
}
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException
{
if (logger.isLoggable(Level.FINER))
logger.finer(logContext + " Trusting server certificate");
}
public X509Certificate[] getAcceptedIssuers()
{
return new X509Certificate[0];
}
}
/**
* This class implements an X509TrustManager that hostname for validation.
*
* This validates the subject name in the certificate with the host name
*/
private final class HostNameOverrideX509TrustManager extends Object implements X509TrustManager
{
private final Logger logger;
private final String logContext;
private final X509TrustManager defaultTrustManager;
private String hostName;
HostNameOverrideX509TrustManager(TDSChannel tdsChannel, X509TrustManager tm, String hostName)
{
this.logger = tdsChannel.getLogger();
this.logContext = tdsChannel.toString() + " (HostNameOverrideX509TrustManager):";
defaultTrustManager = tm;
// canonical name is in lower case so convert this to lowercase too.
this.hostName = hostName.toLowerCase();;
}
// Parse name in RFC 2253 format
// Returns the common name if successful, null if failed to find the common name.
// The parser tuned to be safe than sorry so if it sees something it cant parse correctly it returns null
private String parseCommonName(String distinguishedName)
{
int index;
// canonical name converts entire name to lowercase
index = distinguishedName.indexOf("cn=");
if (index == -1)
{
return null;
}
distinguishedName = distinguishedName.substring(index + 3);
// Parse until a comma or end is reached
// Note the parser will handle gracefully (essentially will return empty string) , inside the quotes (e.g cn="Foo, bar") however
//RFC 952 says that the hostName cant have commas however the parser should not (and will not) crash if it sees a , within quotes.
for (index = 0; index < distinguishedName.length(); index++)
{
if (distinguishedName.charAt(index) == ',' )
{
break;
}
}
String commonName = distinguishedName.substring(0, index);
// strip any quotes
if(commonName.length()>1 && ( '\"' == commonName.charAt(0)))
{
if('\"' == commonName.charAt(commonName.length() -1))
commonName = commonName.substring(1, commonName.length()-1);
else
{
// Be safe the name is not ended in " return null so the common Name wont match
commonName = null;
}
}
return commonName;
}
private boolean validateServerName(String nameInCert) throws CertificateException
{
// Failed to get the common name from DN or empty CN
if (null == nameInCert)
{
if (logger.isLoggable(Level.FINER))
logger.finer(logContext + " Failed to parse the name from the certificate or name is empty.");
return false;
}
// Verify that the name in certificate matches exactly with the host name
if ( !nameInCert.equals(hostName))
{
if (logger.isLoggable(Level.FINER))
logger.finer(logContext + " The name in certificate " + nameInCert + " does not match with the server name " + hostName + ".");
return false;
}
if (logger.isLoggable(Level.FINER))
logger.finer(logContext + " The name in certificate:" + nameInCert + " validated against server name " + hostName + ".");
return true;
}
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException
{
if (logger.isLoggable(Level.FINEST))
logger.finest(logContext + " Forwarding ClientTrusted.");
defaultTrustManager.checkClientTrusted(chain, authType);
}
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException
{
if (logger.isLoggable(Level.FINEST))
logger.finest(logContext + " Forwarding Trusting server certificate");
defaultTrustManager.checkServerTrusted(chain, authType);
if (logger.isLoggable(Level.FINEST))
logger.finest(logContext + " default serverTrusted succeeded proceeding with server name validation");
validateServerNameInCertificate(chain[0]);
}
private void validateServerNameInCertificate(X509Certificate cert) throws CertificateException
{
String nameInCertDN = cert.getSubjectX500Principal().getName("canonical");
if (logger.isLoggable(Level.FINER))
{
logger.finer(logContext + " Validating the server name:" + hostName);
logger.finer(logContext + " The DN name in certificate:" + nameInCertDN);
}
boolean isServerNameValidated = false;
// the name in cert is in RFC2253 format parse it to get the actual subject name
String subjectCN = parseCommonName(nameInCertDN);
isServerNameValidated = validateServerName(subjectCN);
if(!isServerNameValidated)
{
Collection> sanCollection = cert.getSubjectAlternativeNames();
if(sanCollection!= null)
{
//find a subjectAlternateName entry corresponding to DNS Name
for(List sanEntry : sanCollection)
{
if(sanEntry!=null && sanEntry.size() >= 2)
{
Object key = sanEntry.get(0);
Object value = sanEntry.get(1);
if (logger.isLoggable(Level.FINER))
{
logger.finer(logContext + "Key: "+ key
+ "; KeyClass:"+ (key!=null?key.getClass():null)
+";value: " + value
+ "; valueClass:"+ (value!=null? value.getClass():null)
);
}
//From Documentation(http://download.oracle.com/javase/6/docs/api/java/security/cert/X509Certificate.html):
//"Note that the Collection returned may contain
//more than one name of the same type."
//So, more than one entry of dnsNameType can be present.
//Java docs guarantee that the first entry in the list will be an integer.
//2 is the sequence no of a dnsName
if((key != null) && (key instanceof Integer) && ((Integer)key == 2))
{
//As per RFC2459, the DNSName will be in the
//"preferred name syntax" as specified by RFC
//1034 and the name can be in upper or lower case.
//And no significance is attached to case.
//Java docs guarantee that the second entry in the list
//will be a string for dnsName
if(value != null && value instanceof String)
{
String dnsNameInSANCert = (String)value;
//convert to upper case and then to lower case in english locale
//to avoid Turkish i issues.
//Note that, this conversion was not necessary for
//cert.getSubjectX500Principal().getName("canonical");
//as the above API already does this by default as per documentation.
dnsNameInSANCert = dnsNameInSANCert.toUpperCase(Locale.US);
dnsNameInSANCert = dnsNameInSANCert.toLowerCase(Locale.US);
isServerNameValidated = validateServerName(dnsNameInSANCert);
if(isServerNameValidated)
{
if (logger.isLoggable(Level.FINER))
{
logger.finer(logContext + " found a valid name in certificate: "
+ dnsNameInSANCert);
}
break;
}
}
if (logger.isLoggable(Level.FINER))
{
logger.finer(logContext + " the following name in certificate does not match the serverName: "
+ value);
}
}
}
else
{
if(logger.isLoggable(Level.FINER))
{
logger.finer(logContext + " found an invalid san entry: " + sanEntry);
}
}
}
}
}
if(!isServerNameValidated)
{
String msg = SQLServerException.getErrString("R_certNameFailed");
throw new CertificateException(msg);
}
}
public X509Certificate[] getAcceptedIssuers()
{
return defaultTrustManager.getAcceptedIssuers();
}
}
enum SSLHandhsakeState
{
SSL_HANDHSAKE_NOT_STARTED,
SSL_HANDHSAKE_STARTED,
SSL_HANDHSAKE_COMPLETE
};
void enableSSL(String host, int port) throws SQLServerException
{
// If enabling SSL fails, which it can for a number of reasons, the following items
// are used in logging information to the TDS channel logger to help diagnose the problem.
Provider tmfProvider = null; // TrustManagerFactory provider
Provider sslContextProvider = null; // SSLContext provider
Provider ksProvider = null; // KeyStore provider
String tmfDefaultAlgorithm = null; // Default algorithm (typically X.509) used by the TrustManagerFactory
SSLHandhsakeState handshakeState = SSLHandhsakeState.SSL_HANDHSAKE_NOT_STARTED;
// If anything in here fails, terminate the connection and throw an exception
try
{
if (logger.isLoggable(Level.FINER))
logger.finer(toString() + " Enabling SSL...");
String trustStoreFileName = con.activeConnectionProperties.getProperty(SQLServerDriverStringProperty.TRUST_STORE.toString());
String trustStorePassword = con.activeConnectionProperties.getProperty(SQLServerDriverStringProperty.TRUST_STORE_PASSWORD.toString());
String hostNameInCertificate = con.activeConnectionProperties.getProperty(SQLServerDriverStringProperty.HOSTNAME_IN_CERTIFICATE.toString());
assert
TDS.ENCRYPT_OFF == con.getRequestedEncryptionLevel() || // Login only SSL
TDS.ENCRYPT_ON == con.getRequestedEncryptionLevel(); // Full SSL
assert
TDS.ENCRYPT_OFF == con.getNegotiatedEncryptionLevel() || // Login only SSL
TDS.ENCRYPT_ON == con.getNegotiatedEncryptionLevel() || // Full SSL
TDS.ENCRYPT_REQ == con.getNegotiatedEncryptionLevel(); // Full SSL
// If we requested login only SSL or full SSL without server certificate validation,
// then we'll "validate" the server certificate using a naive TrustManager that trusts
// everything it sees.
TrustManager[] tm = null;
if (TDS.ENCRYPT_OFF == con.getRequestedEncryptionLevel() ||
(TDS.ENCRYPT_ON == con.getRequestedEncryptionLevel() && con.trustServerCertificate()))
{
if (logger.isLoggable(Level.FINER))
logger.finer(toString() + " SSL handshake will trust any certificate");
tm = new TrustManager[] { new PermissiveX509TrustManager(this) };
}
// Otherwise, we'll validate the certificate using a real TrustManager obtained
// from the a security provider that is capable of validating X.509 certificates.
else
{
if (logger.isLoggable(Level.FINER))
logger.finer(toString() + " SSL handshake will validate server certificate");
KeyStore ks = null;
// If we are using the system default trustStore and trustStorePassword
// then we can skip all of the KeyStore loading logic below.
// The security provider's implementation takes care of everything for us.
if (null == trustStoreFileName && null == trustStorePassword)
{
if (logger.isLoggable(Level.FINER))
logger.finer(toString() + " Using system default trust store and password");
}
// Otherwise either the trustStore, trustStorePassword, or both was specified.
// In that case, we need to load up a KeyStore ourselves.
else
{
// First, obtain an interface to a KeyStore that can load trust material
// stored in Java Key Store (JKS) format.
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Finding key store interface");
ks = KeyStore.getInstance("JKS");
ksProvider = ks.getProvider();
// Next, load up the trust store file from the specified location.
// Note: This function returns a null InputStream if the trust store cannot
// be loaded. This is by design. See the method comment and documentation
// for KeyStore.load for details.
InputStream is = loadTrustStore(trustStoreFileName);
// Finally, load the KeyStore with the trust material (if any) from the
// InputStream and close the stream.
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Loading key store");
try
{
ks.load(is, (null == trustStorePassword) ? null : trustStorePassword.toCharArray());
}
finally
{
// We are done with the trustStorePassword (if set). Clear it for better security.
con.activeConnectionProperties.remove(SQLServerDriverStringProperty.TRUST_STORE_PASSWORD.toString());
// We are also done with the trust store input stream.
if (null != is)
{
try
{
is.close();
}
catch (IOException e)
{
if (logger.isLoggable(Level.FINE))
logger.fine(toString() + " Ignoring error closing trust material InputStream...");
}
}
}
}
// Either we now have a KeyStore populated with trust material or we are using the
// default source of trust material (cacerts). Either way, we are now ready to
// use a TrustManagerFactory to create a TrustManager that uses the trust material
// to validate the server certificate.
// Next step is to get a TrustManagerFactory that can produce TrustManagers
// that understands X.509 certificates.
TrustManagerFactory tmf = null;
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Locating X.509 trust manager factory");
tmfDefaultAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
tmf = TrustManagerFactory.getInstance(tmfDefaultAlgorithm);
tmfProvider = tmf.getProvider();
// Tell the TrustManagerFactory to give us TrustManagers that we can use to
// validate the server certificate using the trust material in the KeyStore.
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Getting trust manager");
tmf.init(ks);
tm = tmf.getTrustManagers();
// if the host name in cert provided use it or use the host name
if(null != hostNameInCertificate)
{
tm = new TrustManager[] {new HostNameOverrideX509TrustManager(this, (X509TrustManager)tm[0], hostNameInCertificate)};
}
else
{
tm = new TrustManager[] {new HostNameOverrideX509TrustManager(this, (X509TrustManager)tm[0], host)};
}
} // end if (!con.trustServerCertificate())
// Now, with a real or fake TrustManager in hand, get a context for creating a
// SSL sockets through a SSL socket factory. We require at least TLS support.
SSLContext sslContext = null;
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Getting TLS or better SSL context");
sslContext = SSLContext.getInstance("TLS");
sslContextProvider = sslContext.getProvider();
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Initializing SSL context");
sslContext.init(null, tm, null);
// Got the SSL context. Now create an SSL socket over our own proxy socket
// which we can toggle between TDS-encapsulated and raw communications.
// Initially, the proxy is set to encapsulate the SSL handshake in TDS packets.
proxySocket = new ProxySocket(this);
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Creating SSL socket");
sslSocket =
(SSLSocket) sslContext.getSocketFactory().createSocket(
proxySocket,
host,
port,
false); // don't close proxy when SSL socket is closed
// At long last, start the SSL handshake ...
if (logger.isLoggable(Level.FINER))
logger.finer(toString() + " Starting SSL handshake");
// TLS 1.2 intermittent exception happens here.
handshakeState = SSLHandhsakeState.SSL_HANDHSAKE_STARTED;
sslSocket.startHandshake();
handshakeState = SSLHandhsakeState.SSL_HANDHSAKE_COMPLETE;
// After SSL handshake is complete, rewire proxy socket to use raw TCP/IP streams ...
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Rewiring proxy streams after handshake");
proxySocket.setStreams(inputStream, outputStream);
// ... and rewire TDSChannel to use SSL streams.
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Getting SSL InputStream");
inputStream = sslSocket.getInputStream();
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Getting SSL OutputStream");
outputStream = sslSocket.getOutputStream();
// SSL is now enabled; switch over the channel socket
channelSocket = sslSocket;
if (logger.isLoggable(Level.FINER))
logger.finer(toString() + " SSL enabled");
}
catch (Exception e)
{
// Log the original exception and its source at FINER level
if (logger.isLoggable(Level.FINER))
logger.log(Level.FINER, e.getMessage(), e);
// If enabling SSL fails, the following information may help diagnose the problem.
// Do not use Level INFO or above which is sent to standard output/error streams.
// This is because due to an intermittent TLS 1.2 connection issue, we will be retrying the connection and
// do not want to print this message in console.
if (logger.isLoggable(Level.FINER))
logger.log(Level.FINER,
"java.security path: " + JAVA_SECURITY + "\n" +
"Security providers: " + Arrays.asList(Security.getProviders()) + "\n" +
((null != sslContextProvider) ?
("SSLContext provider info: " + sslContextProvider.getInfo() + "\n" +
"SSLContext provider services:\n" + sslContextProvider.getServices() + "\n") : "") +
((null != tmfProvider) ?
("TrustManagerFactory provider info: " + tmfProvider.getInfo() + "\n") : "") +
((null != tmfDefaultAlgorithm) ?
("TrustManagerFactory default algorithm: " + tmfDefaultAlgorithm + "\n") : "") +
((null != ksProvider) ?
("KeyStore provider info: " + ksProvider.getInfo() + "\n") : "") +
"java.ext.dirs: " + System.getProperty("java.ext.dirs")
);
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_sslFailed"));
Object[] msgArgs = {e.getMessage()};
// It is important to get the localized message here, otherwise error messages won't match for different locales.
String errMsg = e.getLocalizedMessage();
// The error message may have a connection id appended to it. Extract the message only for comparison.
// This client connection id is appended in method checkAndAppendClientConnId().
if (-1 != errMsg.indexOf(SQLServerException.LOG_CLIENT_CONNECTION_ID_PREFIX))
{
errMsg = errMsg.substring(0, errMsg.indexOf(SQLServerException.LOG_CLIENT_CONNECTION_ID_PREFIX));
}
// Isolate the TLS1.2 intermittent connection error.
if (e instanceof IOException &&
(SSLHandhsakeState.SSL_HANDHSAKE_STARTED == handshakeState) &&
(errMsg.equals(SQLServerException.getErrString("R_truncatedServerResponse"))))
{
con.terminate(SQLServerException.DRIVER_ERROR_INTERMITTENT_TLS_FAILED, form.format(msgArgs), e);
}
else
{
con.terminate(SQLServerException.DRIVER_ERROR_SSL_FAILED, form.format(msgArgs), e);
}
}
}
private final static String SEPARATOR = System.getProperty("file.separator");
private final static String JAVA_HOME = System.getProperty("java.home");
private final static String JAVA_SECURITY = JAVA_HOME + SEPARATOR + "lib" + SEPARATOR + "security";
private final static String JSSECACERTS = JAVA_SECURITY + SEPARATOR + "jssecacerts";
private final static String CACERTS = JAVA_SECURITY + SEPARATOR + "cacerts";
/**
* Loads the contents of a trust store into an InputStream.
*
* When a location to a trust store is specified, this method attempts to load that store.
* Otherwise, it looks for and attempts to load the default trust store using essentially the
* same logic (outlined in the JSSE Reference Guide) as the default X.509 TrustManagerFactory.
*
* @return an InputStream containing the contents of the loaded trust store
* @return null if the trust store cannot be loaded.
*
* Note: It is by design that this function returns null when the trust store cannot be loaded
* rather than throwing an exception. The reason is that KeyStore.load, which uses the returned
* InputStream, interprets a null InputStream to mean that there are no trusted certificates,
* which mirrors the behavior of the default (no trust store, no password specified) path.
*/
final InputStream loadTrustStore(String trustStoreFileName)
{
FileInputStream is = null;
// First case: Trust store filename was specified
if (null != trustStoreFileName)
{
try
{
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Opening specified trust store: " + trustStoreFileName);
is = new FileInputStream(trustStoreFileName);
}
catch (FileNotFoundException e)
{
if (logger.isLoggable(Level.FINE))
logger.fine(toString() + " Trust store not found: " + e.getMessage());
// If the trustStoreFileName connection property is set, but the file is not found,
// then treat it as if the file was empty so that the TrustManager reports
// that no certificate is found.
}
}
// Second case: Trust store filename derived from javax.net.ssl.trustStore system property
else if (null != (trustStoreFileName = System.getProperty("javax.net.ssl.trustStore")))
{
try
{
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Opening default trust store (from javax.net.ssl.trustStore): " + trustStoreFileName);
is = new FileInputStream(trustStoreFileName);
}
catch (FileNotFoundException e)
{
if (logger.isLoggable(Level.FINE))
logger.fine(toString() + " Trust store not found: " + e.getMessage());
// If the javax.net.ssl.trustStore property is set, but the file is not found,
// then treat it as if the file was empty so that the TrustManager reports
// that no certificate is found.
}
}
// Third case: No trust store specified and no system property set. Use jssecerts/cacerts.
else
{
try
{
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Opening default trust store: " + JSSECACERTS);
is = new FileInputStream(JSSECACERTS);
}
catch (FileNotFoundException e)
{
if (logger.isLoggable(Level.FINE))
logger.fine(toString() + " Trust store not found: " + e.getMessage());
}
// No jssecerts. Try again with cacerts...
if (null == is)
{
try
{
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Opening default trust store: " + CACERTS);
is = new FileInputStream(CACERTS);
}
catch (FileNotFoundException e)
{
if (logger.isLoggable(Level.FINE))
logger.fine(toString() + " Trust store not found: " + e.getMessage());
// No jssecerts or cacerts. Treat it as if the trust store is empty so that
// the TrustManager reports that no certificate is found.
}
}
}
return is;
}
final int read(byte[] data, int offset, int length) throws SQLServerException
{
try
{
return inputStream.read(data, offset, length);
}
catch (IOException e)
{
if (logger.isLoggable(Level.FINE))
logger.fine(toString() + " read failed:" + e.getMessage());
con.terminate(SQLServerException.DRIVER_ERROR_IO_FAILED, e.getMessage());
return 0; // Keep the compiler happy.
}
}
final void write(byte[] data, int offset, int length) throws SQLServerException
{
try
{
outputStream.write(data, offset, length);
}
catch (IOException e)
{
if (logger.isLoggable(Level.FINER))
logger.finer(toString() + " write failed:" + e.getMessage());
con.terminate(SQLServerException.DRIVER_ERROR_IO_FAILED, e.getMessage());
}
}
final void flush() throws SQLServerException
{
try
{
outputStream.flush();
}
catch (IOException e)
{
if (logger.isLoggable(Level.FINER))
logger.finer(toString() + " flush failed:" + e.getMessage());
con.terminate(SQLServerException.DRIVER_ERROR_IO_FAILED, e.getMessage());
}
}
final void close()
{
if (null != sslSocket)
disableSSL();
if (null != inputStream)
{
if (logger.isLoggable(Level.FINEST))
logger.finest(this.toString() + ": Closing inputStream...");
try
{
inputStream.close();
}
catch (IOException e)
{
if (logger.isLoggable(Level.FINE))
logger.log(Level.FINE, this.toString() + ": Ignored error closing inputStream", e);
}
}
if (null != outputStream)
{
if (logger.isLoggable(Level.FINEST))
logger.finest(this.toString() + ": Closing outputStream...");
try
{
outputStream.close();
}
catch (IOException e)
{
if (logger.isLoggable(Level.FINE))
logger.log(Level.FINE, this.toString() + ": Ignored error closing outputStream", e);
}
}
if (null != tcpSocket)
{
if (logger.isLoggable(Level.FINER))
logger.finer(this.toString() + ": Closing TCP socket...");
try
{
tcpSocket.close();
}
catch(IOException e)
{
if (logger.isLoggable(Level.FINE))
logger.log(Level.FINE, this.toString() + ": Ignored error closing socket", e);
}
}
}
/**
* Logs TDS packet data to the com.microsoft.sqlserver.jdbc.TDS.DATA logger
*
* @param data the buffer containing the TDS packet payload data to log
* @param nStartOffset offset into the above buffer from where to start logging
* @param nLength length (in bytes) of payload
* @param messageDetail other loggable details about the payload
*/
/*L0*/ void logPacket(byte data[], int nStartOffset, int nLength, String messageDetail)
{
assert 0 <= nLength && nLength <= data.length;
assert 0 <= nStartOffset && nStartOffset <= data.length;
final char hexChars[] =
{
'0','1','2','3','4','5','6','7',
'8','9','A','B','C','D','E','F'
};
final char printableChars[] =
{
'.','.','.','.','.','.','.','.','.','.','.','.','.','.','.','.',
'.','.','.','.','.','.','.','.','.','.','.','.','.','.','.','.',
' ','!','\"','#','$','%','&','\'','(',')','*','+',',','-','.','/',
'0','1','2','3','4','5','6','7','8','9',':',';','<','=','>','?',
'@','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O',
'P','Q','R','S','T','U','V','W','X','Y','Z','[','\\',']','^','_',
'`','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o',
'p','q','r','s','t','u','v','w','x','y','z','{','|','}','~','.',
'.','.','.','.','.','.','.','.','.','.','.','.','.','.','.','.',
'.','.','.','.','.','.','.','.','.','.','.','.','.','.','.','.',
'.','.','.','.','.','.','.','.','.','.','.','.','.','.','.','.',
'.','.','.','.','.','.','.','.','.','.','.','.','.','.','.','.',
'.','.','.','.','.','.','.','.','.','.','.','.','.','.','.','.',
'.','.','.','.','.','.','.','.','.','.','.','.','.','.','.','.',
'.','.','.','.','.','.','.','.','.','.','.','.','.','.','.','.',
'.','.','.','.','.','.','.','.','.','.','.','.','.','.','.','.'
};
// Log message body lines have this form:
//
// "XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX ................"
// 012345678911111111112222222222333333333344444444445555555555666666
// 01234567890123456789012345678901234567890123456789012345
//
final char lineTemplate[] =
{
' ',' ',' ', ' ',' ',' ', ' ',' ',' ', ' ',' ',' ',
' ',' ',' ', ' ',' ',' ', ' ',' ',' ', ' ',' ',' ',
' ',' ',' ', ' ',' ',' ', ' ',' ',' ', ' ',' ',' ',
' ',' ',' ', ' ',' ',' ', ' ',' ',' ', ' ',' ',' ',
' ',' ',
'.','.','.','.','.','.','.','.',
'.','.','.','.','.','.','.','.'
};
char logLine[] = new char[lineTemplate.length];
System.arraycopy(lineTemplate, 0, logLine, 0, lineTemplate.length);
// Logging builds up a string buffer for the entire log trace
// before writing it out. So use an initial size large enough
// that the buffer doesn't have to resize itself.
StringBuilder logMsg = new StringBuilder(
messageDetail.length() + // Message detail
4*nLength + // 2-digit hex + space + ASCII, per byte
4*(1+nLength/16) + // 2 extra spaces + CR/LF, per line (16 bytes per line)
80); // Extra fluff: IP:Port, Connection #, SPID, ...
// Format the headline like so:
// /157.55.121.182:2983 Connection 1, SPID 53, Message info here ...
//
// Note: the log formatter itself timestamps what we write so we don't have
// to do it again here.
logMsg.append(tcpSocket.getLocalAddress().toString() + ":" + tcpSocket.getLocalPort() + " SPID:" + spid + " " + messageDetail + "\r\n");
// Fill in the body of the log message, line by line, 16 bytes per line.
int nBytesLogged = 0;
int nBytesThisLine;
while (true)
{
// Fill up the line with as many bytes as we can (up to 16 bytes)
for (nBytesThisLine = 0;
nBytesThisLine < 16 && nBytesLogged < nLength;
nBytesThisLine++, nBytesLogged++)
{
int nUnsignedByteVal = (data[nStartOffset + nBytesLogged] + 256) % 256;
logLine[3 * nBytesThisLine] = hexChars[nUnsignedByteVal / 16];
logLine[3 * nBytesThisLine + 1] = hexChars[nUnsignedByteVal % 16];
logLine[50 + nBytesThisLine] = printableChars[nUnsignedByteVal];
}
// Pad out the remainder with whitespace
for (int nBytesJustified = nBytesThisLine;
nBytesJustified < 16; nBytesJustified++)
{
logLine[3 * nBytesJustified] = ' ';
logLine[3 * nBytesJustified + 1] = ' ';
}
logMsg.append(logLine, 0, 50 + nBytesThisLine);
if (nBytesLogged == nLength)
break;
logMsg.append("\r\n");
}
packetLogger.finest(logMsg.toString());
}
}
/**
* SocketFinder is used to find a server socket to which
* a connection can be made. This class abstracts the logic
* of finding a socket from TDSChannel class.
*
* In the case when useParallel is set to true,
* this is achieved by trying to make parallel connections to multiple IP addresses.
* This class is responsible for spawning multiple threads
* and keeping track of the search result and the connected socket
* or exception to be thrown.
*
* In the case where multiSubnetFailover is false, we try our old logic of
* trying to connect to the first ip address
*
* Typical usage of this class is
* SocketFinder sf = new SocketFinder(traceId, conn);
* Socket = sf.getSocket(hostName, port, timeout);
*/
final class SocketFinder
{
/**
* Indicates the result of a search
*/
enum Result
{
UNKNOWN,//search is still in progress
SUCCESS,//found a socket
FAILURE//failed in finding a socket
}
//Thread pool - the values in the constructor are chosen based on the
//explanation given in design_connection_director_multisubnet.doc
private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 5, TimeUnit.SECONDS, new SynchronousQueue());
// When parallel connections are to be used, use minimum timeout slice of 1500 milliseconds.
private static final int minTimeoutForParallelConnections = 1500;
//lock used for synchronization while updating
//data within a socketFinder object
private final Object socketFinderlock = new Object();
//lock on which the parent thread would wait
//after spawning threads.
private final Object parentThreadLock = new Object();
//indicates whether the socketFinder has succeeded or failed
//in finding a socket or is still trying to find a socket
private volatile Result result = Result.UNKNOWN;
//total no of socket connector threads
//spawned by a socketFinder object
private int noOfSpawnedThreads = 0;
//no of threads that finished their socket connection
//attempts and notified socketFinder about their result
private volatile int noOfThreadsThatNotified = 0;
//If a valid connected socket is found, this value would be non-null,
//else this would be null
private volatile Socket selectedSocket = null;
//This would be one of the exceptions returned by the
//socketConnector threads
private volatile IOException selectedException = null;
//Logging variables
private static final Logger logger = Logger.getLogger("com.microsoft.sqlserver.jdbc.internals.SocketFinder");
private final String traceID;
//maximum number of IP Addresses supported
private static final int ipAddressLimit = 64;
//necessary for raising exceptions so that the connection pool can be notified
private final SQLServerConnection conn;
/**
* Constructs a new SocketFinder object with appropriate traceId
* @param callerTraceID traceID of the caller
* @param sqlServerConnection the SQLServer connection
*/
SocketFinder(String callerTraceID, SQLServerConnection sqlServerConnection)
{
traceID = "SocketFinder(" + callerTraceID + ")";
conn = sqlServerConnection;
}
/**
* Used to find a socket
* to which a connection can be made
* @param hostName
* @param portNumber
* @param timeoutInMilliSeconds
* @return connected socket
* @throws IOException
*/
Socket findSocket(
String hostName,
int portNumber,
int timeoutInMilliSeconds,
boolean useParallel,
boolean useTnir,
boolean isTnirFirstAttempt,
int timeoutInMilliSecondsForFullTimeout) throws SQLServerException
{
assert timeoutInMilliSeconds != 0: "The driver does not allow a time out of 0";
try
{
InetAddress[] inetAddrs = null;
// inetAddrs is only used if useParallel is true or TNIR is true. Skip resolving address if that's not the case.
if(useParallel || useTnir)
{
// Ignore TNIR if host resolves to more than 64 IPs. Make sure we are using original timeout for this.
inetAddrs = InetAddress.getAllByName(hostName);
if((useTnir) && (inetAddrs.length > ipAddressLimit))
{
useTnir = false;
timeoutInMilliSeconds = timeoutInMilliSecondsForFullTimeout;
}
}
if(!useParallel)
{
// MSF is false. TNIR could be true or false. DBMirroring could be true or false.
// For TNIR first attempt, we should do existing behavior including how host name is resolved.
if (useTnir && isTnirFirstAttempt)
{
return getDefaultSocket(hostName, portNumber, SQLServerConnection.TnirFirstAttemptTimeoutMs);
}
else if(!useTnir)
{
return getDefaultSocket(hostName, portNumber, timeoutInMilliSeconds);
}
}
// Code reaches here only if MSF = true or (TNIR = true and not TNIR first attempt)
if(logger.isLoggable(Level.FINER))
{
String loggingString = this.toString() + " Total no of InetAddresses: " + inetAddrs.length + ". They are: ";
for(InetAddress inetAddr : inetAddrs)
{
loggingString = loggingString + inetAddr.toString()+";";
}
logger.finer(loggingString);
}
if(inetAddrs.length > ipAddressLimit)
{
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_ipAddressLimitWithMultiSubnetFailover"));
Object[] msgArgs = {Integer.toString(ipAddressLimit)};
String errorStr = form.format(msgArgs);
//we do not want any retry to happen here. So, terminate the connection
//as the config is unsupported.
conn.terminate(SQLServerException.DRIVER_ERROR_UNSUPPORTED_CONFIG, errorStr);
}
if(Util.isIBM())
{
timeoutInMilliSeconds = Math.max(timeoutInMilliSeconds, minTimeoutForParallelConnections);
if(logger.isLoggable(Level.FINER))
{
logger.finer(this.toString() + "Using Java NIO with timeout:" + timeoutInMilliSeconds);
}
findSocketUsingJavaNIO(inetAddrs, portNumber, timeoutInMilliSeconds);
}
else
{
LinkedList inet4Addrs = new LinkedList();
LinkedList inet6Addrs = new LinkedList();
for(InetAddress inetAddr: inetAddrs)
{
if(inetAddr instanceof Inet4Address)
{
inet4Addrs.add((Inet4Address)inetAddr);
}
else
{
assert inetAddr instanceof Inet6Address : "Unexpected IP address " + inetAddr.toString();
inet6Addrs.add((Inet6Address)inetAddr);
}
}
//use half timeout only if both IPv4 and IPv6 addresses are present
int timeoutForEachIPAddressType;
if((!inet4Addrs.isEmpty()) && (!inet6Addrs.isEmpty()))
{
timeoutForEachIPAddressType = Math.max(timeoutInMilliSeconds/2, minTimeoutForParallelConnections);
}
else
timeoutForEachIPAddressType = Math.max(timeoutInMilliSeconds, minTimeoutForParallelConnections);
if(!inet4Addrs.isEmpty())
{
if(logger.isLoggable(Level.FINER))
{
logger.finer(this.toString() + "Using Java NIO with timeout:" + timeoutForEachIPAddressType);
}
//inet4Addrs.toArray(new InetAddress[0]) is java style of converting a linked list to an array of reqd size
findSocketUsingJavaNIO(inet4Addrs.toArray(new InetAddress[0]), portNumber, timeoutForEachIPAddressType);
}
if(!result.equals(Result.SUCCESS))
{
//try threading logic
if(!inet6Addrs.isEmpty())
{
//do not start any threads if there is only one ipv6 address
if(inet6Addrs.size() == 1)
{
return getConnectedSocket(inet6Addrs.get(0), portNumber, timeoutForEachIPAddressType);
}
if(logger.isLoggable(Level.FINER))
{
logger.finer(this.toString() + "Using Threading with timeout:" + timeoutForEachIPAddressType);
}
findSocketUsingThreading(inet6Addrs, portNumber, timeoutForEachIPAddressType);
}
}
}
//If the thread continued execution due to timeout, the result may not be known.
//In that case, update the result to failure. Note that this case is possible
//for both IPv4 and IPv6.
//Using double-checked locking for performance reasons.
if(result.equals(Result.UNKNOWN))
{
synchronized(socketFinderlock)
{
if(result.equals(Result.UNKNOWN))
{
result = Result.FAILURE;
if(logger.isLoggable(Level.FINER))
{
logger.finer(this.toString() + " The parent thread updated the result to failure");
}
}
}
}
//After we reach this point, there is no need for synchronization any more.
//Because, the result would be known(success/failure).
//And no threads would update SocketFinder
//as their function calls would now be no-ops.
if(result.equals(Result.FAILURE))
{
if(selectedException==null)
{
if(logger.isLoggable(Level.FINER))
{
logger.finer(this.toString() + " There is no selectedException. The wait calls timed out before any connect call returned or timed out.");
}
String message = SQLServerException.getErrString("R_connectionTimedOut");
selectedException = new IOException(message);
}
throw selectedException;
}
}
catch(InterruptedException ex)
{
close(selectedSocket);
SQLServerException.ConvertConnectExceptionToSQLServerException(hostName, portNumber, conn, ex);
}
catch(IOException ex)
{
close(selectedSocket);
//The code below has been moved from connectHelper.
//If we do not move it, the functions open(caller of findSocket)
//and findSocket will have to
//declare both IOException and SQLServerException in the throws clause
//as we throw custom SQLServerExceptions(eg:IPAddressLimit, wrapping other exceptions
//like interruptedException) in findSocket.
//That would be a bit awkward, because connecthelper(the caller of open)
//just wraps IOException into SQLServerException and throws SQLServerException.
//Instead, it would be good to wrap all exceptions at one place - Right here, their origin.
SQLServerException.ConvertConnectExceptionToSQLServerException(hostName, portNumber, conn, ex);
}
assert result.equals(Result.SUCCESS) == true;
assert selectedSocket != null :"Bug in code. Selected Socket cannot be null here.";
return selectedSocket;
}
/**
* This function uses java NIO to connect to all the addresses in inetAddrs
* with in a specified timeout. If it succeeds in connecting, it closes
* all the other open sockets and updates the result to success.
* @param inetAddrs the array of inetAddress to which connection should be made
* @param portNumber the port number at which connection should be made
* @param timeoutInMilliSeconds
* @throws IOException
*/
private void findSocketUsingJavaNIO(InetAddress[] inetAddrs, int portNumber, int timeoutInMilliSeconds) throws IOException
{
//The driver does not allow a time out of zero.
//Also, the unit of time the user can specify in the driver is seconds.
//So, even if the user specifies 1 second(least value), the least possible
//value that can come here as timeoutInMilliSeconds is 500 milliseconds.
assert timeoutInMilliSeconds != 0:"The timeout cannot be zero";
assert inetAddrs.length != 0:"Number of inetAddresses should not be zero in this function";
Selector selector = null;
LinkedList socketChannels = new LinkedList();
SocketChannel selectedChannel = null;
try
{
selector = Selector.open();
for(int i=0; i < inetAddrs.length; i++)
{
SocketChannel sChannel = SocketChannel.open();
socketChannels.add(sChannel);
//make the channel non-blocking
sChannel.configureBlocking(false);
//register the channel for connect event
int ops = SelectionKey.OP_CONNECT;
SelectionKey key = sChannel.register(selector, ops);
sChannel.connect(new InetSocketAddress(inetAddrs[i], portNumber));
if (logger.isLoggable(Level.FINER))
logger.finer(this.toString() + " initiated connection to address: "+ inetAddrs[i] + ", portNumber: " + portNumber);
}
long timerNow = System.currentTimeMillis();
long timerExpire = timerNow + timeoutInMilliSeconds;
//Denotes the no of channels that still need to processed
int noOfOutstandingChannels = inetAddrs.length;
while(true)
{
long timeRemaining = timerExpire - timerNow;
//if the timeout expired or a channel is selected or there are no more channels left to processes
if((timeRemaining <= 0) || (selectedChannel != null) || (noOfOutstandingChannels <= 0))
break;
//denotes the no of channels that are ready to be processed. i.e. they are either connected
//or encountered an exception while trying to connect
int readyChannels = selector.select(timeRemaining);
if (logger.isLoggable(Level.FINER))
logger.finer(this.toString() + " no of channels ready: " + readyChannels);
//There are no real time guarantees on the time out of the select API used above.
//This check is necessary
//a) to guard against cases where the select returns faster than expected.
//b) for cases where no channels could connect with in the time out
if(readyChannels != 0)
{
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext())
{
SelectionKey key = keyIterator.next();
SocketChannel ch = (SocketChannel)key.channel();
if (logger.isLoggable(Level.FINER))
logger.finer(this.toString() + " processing the channel :" + ch);//this traces the IP by default
boolean connected = false;
try
{
connected = ch.finishConnect();
//ch.finishConnect should either return true or throw an exception
//as we have subscribed for OP_CONNECT.
assert connected==true:"finishConnect on channel:"+ ch +" cannot be false";
selectedChannel = ch;
if (logger.isLoggable(Level.FINER))
logger.finer(this.toString() + " selected the channel :" + selectedChannel);
break;
}
catch(IOException ex)
{
if (logger.isLoggable(Level.FINER))
logger.finer(this.toString() + " the exception: " + ex.getClass() +
" with message: " + ex.getMessage() +
" occured while processing the channel: " + ch);
updateSelectedException(ex ,this.toString());
//close the channel pro-actively so that we do not
//hang on to network resources
ch.close();
}
//unregister the key and remove from the selector's selectedKeys
key.cancel();
keyIterator.remove();
noOfOutstandingChannels--;
}
}
timerNow = System.currentTimeMillis();
}
}
catch(IOException ex)
{
//in case of an exception, close the selected channel.
//All other channels will be closed in the finally block,
//as they need to be closed irrespective of a success/failure
close(selectedChannel);
throw ex;
}
finally
{
//close the selector
//As per java docs, on selector.close(), any uncancelled keys still
//associated with this
//selector are invalidated, their channels are deregistered, and any other
//resources associated with this selector are released.
//So, its not necessary to cancel each key again
close(selector);
//Close all channels except the selected one.
//As we close channels pro-actively in the try block,
//its possible that we close a channel twice.
//Closing a channel second time is a no-op.
//This code is should be in the finally block to guard against cases where
//we pre-maturely exit try block due to an exception in selector or other places.
for(SocketChannel s : socketChannels)
{
if(s!=selectedChannel)
{
close(s);
}
}
}
//if a channel was selected, make the necessary updates
if(selectedChannel != null)
{
//Note that this must be done after selector is closed. Otherwise,
//we would get an illegalBlockingMode exception at run time.
selectedChannel.configureBlocking(true);
selectedSocket = selectedChannel.socket();
result = Result.SUCCESS;
}
}
//This method contains the old logic of connecting to
//a socket of one of the IPs corresponding to a given host name.
//In the old code below, the logic around 0 timeout has been removed as
//0 timeout is not allowed. The code has been re-factored so that the logic
//is common for hostName or InetAddress.
private Socket getDefaultSocket(String hostName, int portNumber, int timeoutInMilliSeconds) throws IOException
{
// Open the socket, with or without a timeout, throwing an UnknownHostException
// if there is a failure to resolve the host name to an InetSocketAddress.
//
// Note that Socket(host, port) throws an UnknownHostException if the host name
// cannot be resolved, but that InetSocketAddress(host, port) does not - it sets
// the returned InetSocketAddress as unresolved.
InetSocketAddress addr = new InetSocketAddress(hostName, portNumber);
return getConnectedSocket(addr, timeoutInMilliSeconds);
}
private Socket getConnectedSocket(InetAddress inetAddr, int portNumber, int timeoutInMilliSeconds) throws IOException
{
InetSocketAddress addr = new InetSocketAddress(inetAddr, portNumber);
return getConnectedSocket(addr, timeoutInMilliSeconds);
}
private Socket getConnectedSocket(InetSocketAddress addr, int timeoutInMilliSeconds) throws IOException
{
assert timeoutInMilliSeconds != 0: "timeout cannot be zero";
if(addr.isUnresolved())
throw new java.net.UnknownHostException();
selectedSocket = new Socket();
selectedSocket.connect(addr,timeoutInMilliSeconds);
return selectedSocket;
}
private void findSocketUsingThreading(LinkedList inetAddrs, int portNumber, int timeoutInMilliSeconds) throws IOException, InterruptedException
{
assert timeoutInMilliSeconds != 0:"The timeout cannot be zero";
assert inetAddrs.isEmpty() == false:"Number of inetAddresses should not be zero in this function";
LinkedList sockets = new LinkedList();
LinkedList socketConnectors = new LinkedList();
try
{
//create a socket, inetSocketAddress and a corresponding socketConnector per inetAddress
noOfSpawnedThreads = inetAddrs.size();
for(InetAddress inetAddress: inetAddrs)
{
Socket s = new Socket();
sockets.add(s);
InetSocketAddress inetSocketAddress = new InetSocketAddress(inetAddress, portNumber);
SocketConnector socketConnector = new SocketConnector(s, inetSocketAddress, timeoutInMilliSeconds, this);
socketConnectors.add(socketConnector);
}
//acquire parent lock and spawn all threads
synchronized(parentThreadLock)
{
for(SocketConnector sc: socketConnectors)
{
threadPoolExecutor.execute(sc);
}
long timerNow = System.currentTimeMillis();
long timerExpire = timerNow + timeoutInMilliSeconds;
//The below loop is to guard against the spurious wake up problem
while(true)
{
long timeRemaining = timerExpire - timerNow;
if(logger.isLoggable(Level.FINER))
{
logger.finer(this.toString() + " TimeRemaining:" + timeRemaining + "; Result:" + result + "; Max. open thread count: " + threadPoolExecutor.getLargestPoolSize() + "; Current open thread count:" + threadPoolExecutor.getActiveCount());
}
//if there is no time left or if the result is determined, break.
//Note that a dirty read of result is totally fine here.
//Since this thread holds the parentThreadLock, even if we do a dirty
//read here, the child thread, after updating the result, would not be
//able to call notify on the parentThreadLock
//(and thus finish execution) as it would be waiting on parentThreadLock
//held by this thread(the parent thread).
//So, this thread will wait again and then be notified by the childThread.
//On the other hand, if we try to take socketFinderLock here to avoid
//dirty read, we would introduce a dead lock due to the
//reverse order of locking in updateResult method.
if(timeRemaining <=0 || (!result.equals(Result.UNKNOWN)))
break;
parentThreadLock.wait(timeRemaining);
if(logger.isLoggable(Level.FINER))
{
logger.finer(this.toString() + " The parent thread wokeup.");
}
timerNow = System.currentTimeMillis();
}
}
}
finally
{
//Close all sockets except the selected one.
//As we close sockets pro-actively in the child threads,
//its possible that we close a socket twice.
//Closing a socket second time is a no-op.
//If a child thread is waiting on the connect call on a socket s,
//closing the socket s here ensures that an exception is thrown
//in the child thread immediately. This mitigates the problem
//of thread explosion by ensuring that unnecessary threads die
//quickly without waiting for "min(timeOut, 21)" seconds
for(Socket s : sockets)
{
if(s!=selectedSocket)
{
close(s);
}
}
}
}
/**
* search result
*/
Result getResult()
{
return result;
}
void close(Selector selector)
{
if (null != selector)
{
if (logger.isLoggable(Level.FINER))
logger.finer(this.toString() + ": Closing Selector");
try
{
selector.close();
}
catch(IOException e)
{
if (logger.isLoggable(Level.FINE))
logger.log(Level.FINE, this.toString() + ": Ignored the following error while closing Selector", e);
}
}
}
void close(Socket socket)
{
if (null != socket)
{
if (logger.isLoggable(Level.FINER))
logger.finer(this.toString() + ": Closing TCP socket:" + socket);
try
{
socket.close();
}
catch(IOException e)
{
if (logger.isLoggable(Level.FINE))
logger.log(Level.FINE, this.toString() + ": Ignored the following error while closing socket", e);
}
}
}
void close(SocketChannel socketChannel)
{
if (null != socketChannel)
{
if (logger.isLoggable(Level.FINER))
logger.finer(this.toString() + ": Closing TCP socket channel:" + socketChannel);
try
{
socketChannel.close();
}
catch(IOException e)
{
if (logger.isLoggable(Level.FINE))
logger.log(Level.FINE, this.toString() +"Ignored the following error while closing socketChannel", e);
}
}
}
/**
* Used by socketConnector threads to notify the
* socketFinder of their connection attempt result(a connected socket or
* exception).
* It updates the result, socket and exception variables
* of socketFinder object.
* This method notifies the parent thread if a socket is found
* or if all the spawned threads have notified.
* It also closes a socket if it is not selected for use
* by socketFinder.
* @param socket the SocketConnector's socket
* @param exception Exception that occurred in socket connector thread
* @param threadId Id of the calling Thread for diagnosis
*/
void updateResult(Socket socket, IOException exception, String threadId)
{
if(result.equals(Result.UNKNOWN))
{
if(logger.isLoggable(Level.FINER))
{
logger.finer("The following child thread is waiting for socketFinderLock:" +threadId);
}
synchronized(socketFinderlock)
{
if(logger.isLoggable(Level.FINER))
{
logger.finer("The following child thread acquired socketFinderLock:" +threadId);
}
if(result.equals(Result.UNKNOWN))
{
//if the connection was successful and no socket has been
//selected yet
if(exception==null && selectedSocket==null)
{
selectedSocket = socket;
result = Result.SUCCESS;
if(logger.isLoggable(Level.FINER))
{
logger.finer("The socket of the following thread has been chosen:" +threadId);
}
}
//if an exception occurred
if(exception!=null)
{
updateSelectedException(exception, threadId);
}
}
noOfThreadsThatNotified++;
//if all threads notified, but the result is still unknown,
//update the result to failure
if((noOfThreadsThatNotified >= noOfSpawnedThreads)
&& result.equals(Result.UNKNOWN))
{
result = Result.FAILURE;
}
if(!result.equals(Result.UNKNOWN))
{
// 1) Note that at any point of time, there is only one
// thread(parent/child thread) competing for parentThreadLock.
// 2) The only time where a child thread could be waiting on
// parentThreadLock is before the wait call in the parentThread
// 3) After the above happens, the parent thread waits to be
// notified on parentThreadLock. After being notified,
// it would be the ONLY thread competing for the lock.
// for the following reasons
// a) The parentThreadLock is taken while holding the socketFinderLock.
// So, all child threads, except one, block on socketFinderLock
// (not parentThreadLock)
// b) After parentThreadLock is notified by a child thread, the result
// would be known(Refer the double-checked locking done at the
// start of this method). So, all child threads would exit
// as no-ops and would never compete with parent thread
// for acquiring parentThreadLock
// 4) As the parent thread is the only thread that competes for the
// parentThreadLock, it need not wait to acquire the lock once it wakes
// up and gets scheduled.
// This results in better performance as it would close unnecessary
// sockets and thus help child threads die quickly.
if(logger.isLoggable(Level.FINER))
{
logger.finer("The following child thread is waiting for parentThreadLock:" +threadId);
}
synchronized(parentThreadLock)
{
if(logger.isLoggable(Level.FINER))
{
logger.finer("The following child thread acquired parentThreadLock:" +threadId);
}
parentThreadLock.notify();
}
if(logger.isLoggable(Level.FINER))
{
logger.finer("The following child thread released parentThreadLock and notified the parent thread:" +threadId);
}
}
}
if(logger.isLoggable(Level.FINER))
{
logger.finer("The following child thread released socketFinderLock:" +threadId);
}
}
}
//Updates the selectedException if
//a) selectedException is null
//b) ex is a non-socketTimeoutException and selectedException is a socketTimeoutException
//If there are multiple exceptions, that are not related to socketTimeout
//the first non-socketTimeout exception is picked.
//If all exceptions are related to socketTimeout, the first exception is picked.
//Note: This method is not thread safe. The caller should ensure thread safety.
public void updateSelectedException(IOException ex, String traceId)
{
boolean updatedException = false;
if(selectedException==null)
{
selectedException = ex;
updatedException =true;
}
else if((!(ex instanceof SocketTimeoutException)) && (selectedException instanceof SocketTimeoutException))
{
selectedException = ex;
updatedException =true;
}
if(updatedException)
{
if(logger.isLoggable(Level.FINER))
{
logger.finer("The selected exception is updated to the following: ExceptionType:"
+ ex.getClass()+"; ExceptionMessage:" +ex.getMessage()+
"; by the following thread:" +traceId);
}
}
}
/**
* Used for tracing
*/
public String toString()
{
return traceID;
}
}
/**
* This is used to connect a socket in a separate thread
*/
final class SocketConnector implements Runnable
{
//socket on which connection attempt would be made
private final Socket socket;
//the socketFinder associated with this connector
private final SocketFinder socketFinder;
//inetSocketAddress to connect to
private final InetSocketAddress inetSocketAddress;
//timeout in milliseconds
private final int timeoutInMilliseconds;
//Logging variables
private static final Logger logger = Logger.getLogger("com.microsoft.sqlserver.jdbc.internals.SocketConnector");
private final String traceID;
//Id of the thread. used for diagnosis
private final String threadID;
//a counter used to give unique IDs to each connector thread.
//this will have the id of the thread that was last created.
private static long lastThreadID = 0;
/**
* Constructs a new SocketConnector object with the associated socket and
* socketFinder
*/
SocketConnector(Socket socket, InetSocketAddress inetSocketAddress, int timeOutInMilliSeconds, SocketFinder socketFinder)
{
this.socket = socket;
this.inetSocketAddress = inetSocketAddress;
this.timeoutInMilliseconds = timeOutInMilliSeconds;
this.socketFinder = socketFinder;
this.threadID = Long.toString(nextThreadID());
this.traceID = "SocketConnector:" + this.threadID + "(" + socketFinder.toString() + ")";
}
/**
* If search for socket has not finished, this function
* tries to connect a socket(with a timeout) synchronously.
* It further notifies the socketFinder the result of
* the connection attempt
*/
public void run()
{
IOException exception = null;
//Note that we do not need socketFinder lock here
//as we update nothing in socketFinder based on the condition.
//So, its perfectly fine to make a dirty read.
SocketFinder.Result result = socketFinder.getResult();
if(result.equals(SocketFinder.Result.UNKNOWN))
{
try
{
if(logger.isLoggable(Level.FINER))
{
logger.finer(this.toString() + " connecting to InetSocketAddress:"
+ inetSocketAddress + " with timeout:" + timeoutInMilliseconds);
}
socket.connect(inetSocketAddress, timeoutInMilliseconds);
}
catch(IOException ex)
{
if(logger.isLoggable(Level.FINER))
{
logger.finer(this.toString() + " exception:" + ex.getClass()
+" with message:" + ex.getMessage()
+ " occured while connecting to InetSocketAddress:"
+ inetSocketAddress);
}
exception = ex;
}
socketFinder.updateResult(socket, exception, this.toString());
}
}
/**
* Used for tracing
*/
public String toString()
{
return traceID;
}
/**
* Generates the next unique thread id.
*/
private static synchronized long nextThreadID()
{
if(lastThreadID == Long.MAX_VALUE)
{
if (logger.isLoggable(Level.FINER))
logger.finer("Resetting the Id count" );
lastThreadID = 1;
}
else
{
lastThreadID++;
}
return lastThreadID;
}
}
/**
* TDSWriter implements the client to server TDS data pipe.
*/
final class TDSWriter
{
private static Logger logger = Logger.getLogger("com.microsoft.sqlserver.jdbc.internals.TDS.Writer");
private final String traceID;
final public String toString() { return traceID; }
private final TDSChannel tdsChannel;
private final SQLServerConnection con;
// Flag to indicate whether data written via writeXXX() calls
// is loggable. Data is normally loggable. But sensitive
// data, such as user credentials, should never be logged for
// security reasons.
private boolean dataIsLoggable = true;
void setDataLoggable(boolean value) { dataIsLoggable = value; }
private TDSCommand command = null;
// TDS message type (Query, RPC, DTC, etc.) sent at the beginning
// of every TDS message header. Value is set when starting a new
// TDS message of the specified type.
private byte tdsMessageType;
private volatile int sendResetConnection = 0;
// Size (in bytes) of the TDS packets to/from the server.
// This size is normally fixed for the life of the connection,
// but it can change once after the logon packet because packet
// size negotiation happens at logon time.
private int currentPacketSize = 0;
// Size of the TDS packet header, which is:
// byte type
// byte status
// short length
// short SPID
// byte packet
// byte window
private final static int TDS_PACKET_HEADER_SIZE = 8;
private final static byte[] placeholderHeader = new byte[TDS_PACKET_HEADER_SIZE];
// Intermediate array used to convert typically "small" values such as fixed-length types
// (byte, int, long, etc.) and Strings from their native form to bytes for sending to
// the channel buffers.
private byte valueBytes[] = new byte[256];
// Monotonically increasing packet number associated with the current message
private volatile int packetNum = 0;
// Bytes for sending decimal/numeric data
private final static int BYTES4 = 4;
private final static int BYTES8 = 8;
private final static int BYTES12 = 12;
private final static int BYTES16 = 16;
public final static int BIGDECIMAL_MAX_LENGTH = 0x11;
//is set to true when EOM is sent for the current message.
//Note that this variable will never be accessed from multiple threads
//simultaneously and so it need not be volatile
private boolean isEOMSent = false;
boolean isEOMSent()
{
return isEOMSent;
}
// Packet data buffers
private ByteBuffer stagingBuffer;
private ByteBuffer socketBuffer;
private ByteBuffer logBuffer;
private CryptoMetadata cryptoMeta = null;
TDSWriter(TDSChannel tdsChannel, SQLServerConnection con)
{
this.tdsChannel = tdsChannel;
this.con = con;
traceID = "TDSWriter@" + Integer.toHexString(hashCode()) + " (" + con.toString() + ")";
}
// TDS message start/end operations
void preparePacket() throws SQLServerException
{
if (tdsChannel.isLoggingPackets())
{
Arrays.fill(logBuffer.array(), (byte)0xFE);
logBuffer.clear();
}
// Write a placeholder packet header. This will be replaced
// with the real packet header when the packet is flushed.
writeBytes(placeholderHeader);
}
/**
* Start a new TDS message.
*/
void writeMessageHeader() throws SQLServerException
{
// TDS 7.2 & later:
// Include ALL_Headers/MARS header in message's first packet
// Note: The PKT_BULK message does not nees this ALL_HEADERS
if ((TDS.PKT_QUERY == tdsMessageType ||
TDS.PKT_DTC == tdsMessageType ||
TDS.PKT_RPC == tdsMessageType))
{
boolean includeTraceHeader = false;
int totalHeaderLength = TDS.MESSAGE_HEADER_LENGTH;
if(TDS.PKT_QUERY == tdsMessageType || TDS.PKT_RPC == tdsMessageType)
{
if(con.isDenaliOrLater() && !ActivityCorrelator.getCurrent().IsSentToServer() && Util.IsActivityTraceOn())
{
includeTraceHeader = true;
totalHeaderLength += TDS.TRACE_HEADER_LENGTH;
}
}
writeInt(totalHeaderLength); // allHeaders.TotalLength (DWORD)
writeInt(TDS.MARS_HEADER_LENGTH); // MARS header length (DWORD)
writeShort((short) 2); // allHeaders.HeaderType(MARS header) (USHORT)
writeBytes(con.getTransactionDescriptor());
writeInt(1); // marsHeader.OutstandingRequestCount
if(includeTraceHeader)
{
writeInt(TDS.TRACE_HEADER_LENGTH); // trace header length (DWORD)
writeTraceHeaderData();
ActivityCorrelator.setCurrentActivityIdSentFlag(); // set the flag to indicate this ActivityId is sent
}
}
}
void writeTraceHeaderData() throws SQLServerException
{
ActivityId activityId = ActivityCorrelator.getCurrent();
final byte[] actIdByteArray = Util.asGuidByteArray(activityId.getId());
long seqNum = activityId.getSequence();
writeShort(TDS.HEADERTYPE_TRACE); // trace header type
writeBytes(actIdByteArray, 0, actIdByteArray.length); // guid part of ActivityId
writeInt((int)seqNum); // sequence number of ActivityId
if (logger.isLoggable(Level.FINER))
logger.finer("Send Trace Header - ActivityID: " + activityId.toString());
}
/**
* Convenience method to prepare the TDS channel for writing and start
* a new TDS message.
*
* @param command The TDS command
* @param tdsMessageType The TDS message type (PKT_QUERY, PKT_RPC, etc.)
*/
void startMessage(TDSCommand command, byte tdsMessageType) throws SQLServerException
{
this.command = command;
this.tdsMessageType = tdsMessageType;
this.packetNum = 0;
this.isEOMSent = false;
this.dataIsLoggable = true;
// If the TDS packet size has changed since the last request
// (which should really only happen after the login packet)
// then allocate new buffers that are the correct size.
int negotiatedPacketSize = con.getTDSPacketSize();
if (currentPacketSize != negotiatedPacketSize)
{
socketBuffer = ByteBuffer.allocate(negotiatedPacketSize).order(ByteOrder.LITTLE_ENDIAN);
stagingBuffer = ByteBuffer.allocate(negotiatedPacketSize).order(ByteOrder.LITTLE_ENDIAN);
logBuffer = ByteBuffer.allocate(negotiatedPacketSize).order(ByteOrder.LITTLE_ENDIAN);
currentPacketSize = negotiatedPacketSize;
}
socketBuffer.position(socketBuffer.limit());
stagingBuffer.clear();
preparePacket();
writeMessageHeader();
}
final void endMessage() throws SQLServerException
{
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Finishing TDS message");
writePacket(TDS.STATUS_BIT_EOM);
}
//If a complete request has not been sent to the server,
//the client MUST send the next packet with both ignore bit (0x02) and EOM bit (0x01)
//set in the status to cancel the request.
final boolean ignoreMessage() throws SQLServerException
{
if (packetNum > 0)
{
assert !isEOMSent;
if (logger.isLoggable(Level.FINER))
logger.finest(toString() + " Finishing TDS message by sending ignore bit and end of message");
writePacket(TDS.STATUS_BIT_EOM|TDS.STATUS_BIT_ATTENTION);
return true;
}
return false;
}
final void resetPooledConnection()
{
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " resetPooledConnection");
sendResetConnection = TDS.STATUS_BIT_RESET_CONN;
}
// Primitive write operations
void writeByte(byte value) throws SQLServerException
{
if (stagingBuffer.remaining() >= 1)
{
stagingBuffer.put(value);
if (tdsChannel.isLoggingPackets())
{
if (dataIsLoggable)
logBuffer.put(value);
else
logBuffer.position(logBuffer.position() + 1);
}
}
else
{
valueBytes[0] = value;
writeWrappedBytes(valueBytes, 1);
}
}
void writeChar(char value) throws SQLServerException
{
if (stagingBuffer.remaining() >= 2)
{
stagingBuffer.putChar(value);
if (tdsChannel.isLoggingPackets())
{
if (dataIsLoggable)
logBuffer.putChar(value);
else
logBuffer.position(logBuffer.position() + 2);
}
}
else
{
Util.writeShort((short) value, valueBytes, 0);
writeWrappedBytes(valueBytes, 2);
}
}
void writeShort(short value) throws SQLServerException
{
if (stagingBuffer.remaining() >= 2)
{
stagingBuffer.putShort(value);
if (tdsChannel.isLoggingPackets())
{
if (dataIsLoggable)
logBuffer.putShort(value);
else
logBuffer.position(logBuffer.position() + 2);
}
}
else
{
Util.writeShort(value, valueBytes, 0);
writeWrappedBytes(valueBytes, 2);
}
}
void writeInt(int value) throws SQLServerException
{
if (stagingBuffer.remaining() >= 4)
{
stagingBuffer.putInt(value);
if (tdsChannel.isLoggingPackets())
{
if (dataIsLoggable)
logBuffer.putInt(value);
else
logBuffer.position(logBuffer.position() + 4);
}
}
else
{
Util.writeInt(value, valueBytes, 0);
writeWrappedBytes(valueBytes, 4);
}
}
/**
* Append a real value in the TDS stream.
* @param value the data value
*/
void writeReal(Float value) throws SQLServerException
{
if (false) //stagingBuffer.remaining() >= 4)
{
stagingBuffer.putFloat(value);
if (tdsChannel.isLoggingPackets())
{
if (dataIsLoggable)
logBuffer.putFloat(value);
else
logBuffer.position(logBuffer.position() + 4);
}
}
else
{
writeInt(Float.floatToRawIntBits(value.floatValue()));
}
}
/**
* Append a double value in the TDS stream.
* @param value the data value
*/
void writeDouble(double value) throws SQLServerException
{
if (stagingBuffer.remaining() >= 8)
{
stagingBuffer.putDouble(value);
if (tdsChannel.isLoggingPackets())
{
if (dataIsLoggable)
logBuffer.putDouble(value);
else
logBuffer.position(logBuffer.position() + 8);
}
}
else
{
long bits = Double.doubleToLongBits(value);
long mask = 0xFF;
int nShift =0;
for (int i=0; i<8; i++)
{
writeByte((byte) ((bits & mask) >> nShift));
nShift += 8;
mask = mask << 8;
}
}
}
/**
* Append a big decimal in the TDS stream.
* @param bigDecimalVal the big decimal data value
* @param srcJdbcType the source JDBCType
* @param precision the precision of the data value
*/
void writeBigDecimal(BigDecimal bigDecimalVal, int srcJdbcType, int precision) throws SQLServerException
{
/*
* Length including sign byte
* One 1-byte unsigned integer that represents the sign of the decimal value (0 => Negative, 1 => positive)
* One 4-, 8-, 12-, or 16-byte signed integer that represents the decimal value multiplied by 10^scale.
* The maximum size of this integer is determined based on p as follows:
* 4 bytes if 1 <= p <= 9.
* 8 bytes if 10 <= p <= 19.
* 12 bytes if 20 <= p <= 28.
* 16 bytes if 29 <= p <= 38.
*/
boolean isNegative = (bigDecimalVal.signum() < 0);
BigInteger bi = bigDecimalVal.unscaledValue();
if (isNegative)
bi=bi.negate();
if (9 >= precision)
{
writeByte((byte) (BYTES4 + 1));
writeByte((byte) (isNegative ? 0 : 1));
writeInt(bi.intValue());
}
else if (19 >= precision)
{
writeByte((byte) (BYTES8 + 1));
writeByte((byte) (isNegative ? 0 : 1));
writeLong(bi.longValue());
}
else
{
int bLength;
if (28 >= precision)
bLength = BYTES12;
else
bLength = BYTES16;
writeByte((byte) (bLength + 1));
writeByte((byte) (isNegative ? 0 : 1));
// Get the bytes of the BigInteger value. It is in reverse order, with
// most significant byte in 0-th element. We need to reverse it first before sending over TDS.
byte[] unscaledBytes = bi.toByteArray();
if(unscaledBytes.length > bLength)
{
// If precession of input is greater than maximum allowed (p><= 38) throw Exception
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_valueOutOfRange"));
Object[] msgArgs = {JDBCType.of(srcJdbcType)};
throw new SQLServerException(
form.format(msgArgs),
SQLState.DATA_EXCEPTION_LENGTH_MISMATCH,
DriverError.NOT_SET,
null);
}
// Byte array to hold all the reversed and padding bytes.
byte[] bytes = new byte[bLength];
// We need to fill up the rest of the array with zeros, as unscaledBytes may have less bytes
// than the required size for TDS.
int remaining = bLength - unscaledBytes.length;
// Reverse the bytes.
int i, j;
for (i = 0, j = unscaledBytes.length - 1; i < unscaledBytes.length;)
bytes[i++] = unscaledBytes[j--];
// Fill the rest of the array with zeros.
for(; i < remaining; i++)
bytes[i] = (byte) 0x00;
writeBytes(bytes);
}
}
void writeSmalldatetime(String value) throws SQLServerException
{
GregorianCalendar calendar = initializeCalender(TimeZone.getDefault());
long utcMillis = 0; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT)
java.sql.Timestamp timestampValue = java.sql.Timestamp.valueOf(value);
utcMillis = timestampValue.getTime();
// Load the calendar with the desired value
calendar.setTimeInMillis(utcMillis);
// Number of days since the SQL Server Base Date (January 1, 1900)
int daysSinceSQLBaseDate =
DDC.daysSinceBaseDate(
calendar.get(Calendar.YEAR),
calendar.get(Calendar.DAY_OF_YEAR),
TDS.BASE_YEAR_1900);
// Next, figure out the number of milliseconds since midnight of the current day.
int millisSinceMidnight = (int)
1000 * calendar.get(Calendar.SECOND) + // Seconds into the current minute
60 * 1000 * calendar.get(Calendar.MINUTE) + // Minutes into the current hour
60 * 60 * 1000 * calendar.get(Calendar.HOUR_OF_DAY); // Hours into the current day
// The last millisecond of the current day is always rounded to the first millisecond
// of the next day because DATETIME is only accurate to 1/300th of a second.
if (1000 * 60 * 60 * 24 - 1 <= millisSinceMidnight)
{
++daysSinceSQLBaseDate;
millisSinceMidnight = 0;
}
// Number of days since the SQL Server Base Date (January 1, 1900)
writeShort((short)daysSinceSQLBaseDate);
int secondsSinceMidnight = (millisSinceMidnight/1000);
int minutesSinceMidnight = (secondsSinceMidnight/60);
// Values that are 29.998 seconds or less are rounded down to the nearest minute
minutesSinceMidnight = ((secondsSinceMidnight % 60) > 29.998) ? minutesSinceMidnight+1 : minutesSinceMidnight;
// Minutes since midnight
writeShort((short)minutesSinceMidnight);
}
void writeDatetime(String value) throws SQLServerException
{
GregorianCalendar calendar = initializeCalender(TimeZone.getDefault());
long utcMillis = 0; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT)
int subSecondNanos = 0;
java.sql.Timestamp timestampValue = java.sql.Timestamp.valueOf(value);
utcMillis = timestampValue.getTime();
subSecondNanos = timestampValue.getNanos();
// Load the calendar with the desired value
calendar.setTimeInMillis(utcMillis);
// Number of days there have been since the SQL Base Date.
//These are based on SQL Server algorithms
int daysSinceSQLBaseDate =
DDC.daysSinceBaseDate(
calendar.get(Calendar.YEAR),
calendar.get(Calendar.DAY_OF_YEAR),
TDS.BASE_YEAR_1900);
// Number of milliseconds since midnight of the current day.
int millisSinceMidnight =
(subSecondNanos + Nanos.PER_MILLISECOND / 2) / Nanos.PER_MILLISECOND + // Millis into the current second
1000 * calendar.get(Calendar.SECOND) + // Seconds into the current minute
60 * 1000 * calendar.get(Calendar.MINUTE) + // Minutes into the current hour
60 * 60 * 1000 * calendar.get(Calendar.HOUR_OF_DAY); // Hours into the current day
// The last millisecond of the current day is always rounded to the first millisecond
// of the next day because DATETIME is only accurate to 1/300th of a second.
if (1000 * 60 * 60 * 24 - 1 <= millisSinceMidnight)
{
++daysSinceSQLBaseDate;
millisSinceMidnight = 0;
}
// Last-ditch verification that the value is in the valid range for the
// DATETIMEN TDS data type (1/1/1753 to 12/31/9999). If it's not, then
// throw an exception now so that statement execution is safely canceled.
// Attempting to put an invalid value on the wire would result in a TDS
// exception, which would close the connection.
//These are based on SQL Server algorithms
if (daysSinceSQLBaseDate < DDC.daysSinceBaseDate(1753, 1, TDS.BASE_YEAR_1900) ||
daysSinceSQLBaseDate >= DDC.daysSinceBaseDate(10000, 1, TDS.BASE_YEAR_1900))
{
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_valueOutOfRange"));
Object[] msgArgs = {SSType.DATETIME};
throw new SQLServerException(
form.format(msgArgs),
SQLState.DATA_EXCEPTION_DATETIME_FIELD_OVERFLOW,
DriverError.NOT_SET,
null);
}
// Number of days since the SQL Server Base Date (January 1, 1900)
writeInt(daysSinceSQLBaseDate);
// Milliseconds since midnight (at a resolution of three hundredths of a second)
writeInt((3 * millisSinceMidnight + 5) / 10);
}
void writeDate(String value) throws SQLServerException
{
GregorianCalendar calendar = initializeCalender(TimeZone.getDefault());
long utcMillis = 0;
java.sql.Date dateValue = java.sql.Date.valueOf(value);
utcMillis = dateValue.getTime();
// Load the calendar with the desired value
calendar.setTimeInMillis(utcMillis);
writeScaledTemporal(
calendar,
0, // subsecond nanos (none for a date value)
0, // scale (dates are not scaled)
SSType.DATE);
}
void writeTime(java.sql.Timestamp value, int scale) throws SQLServerException
{
GregorianCalendar calendar = initializeCalender(TimeZone.getDefault());
long utcMillis = 0; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT)
int subSecondNanos = 0;
utcMillis = value.getTime();
subSecondNanos = value.getNanos();
// Load the calendar with the desired value
calendar.setTimeInMillis(utcMillis);
writeScaledTemporal(
calendar,
subSecondNanos,
scale,
SSType.TIME);
}
void writeDateTimeOffset(Object value,int scale,SSType destSSType) throws SQLServerException
{
GregorianCalendar calendar = null;
TimeZone timeZone = TimeZone.getDefault(); // Time zone to associate with the value in the Gregorian calendar
long utcMillis = 0; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT)
int subSecondNanos = 0;
int minutesOffset=0;
microsoft.sql.DateTimeOffset dtoValue = (microsoft.sql.DateTimeOffset) value;
utcMillis = dtoValue.getTimestamp().getTime();
subSecondNanos = dtoValue.getTimestamp().getNanos();
minutesOffset = dtoValue.getMinutesOffset();
// If the target data type is DATETIMEOFFSET, then use UTC for the calendar that
// will hold the value, since writeRPCDateTimeOffset expects a UTC calendar.
// Otherwise, when converting from DATETIMEOFFSET to other temporal data types,
// use a local time zone determined by the minutes offset of the value, since
// the writers for those types expect local calendars.
timeZone = (SSType.DATETIMEOFFSET == destSSType) ?
UTC.timeZone : new SimpleTimeZone(minutesOffset * 60 * 1000, "");
calendar = new GregorianCalendar(timeZone, Locale.US);
calendar.setLenient(true);
calendar.clear();
calendar.setTimeInMillis(utcMillis);
writeScaledTemporal(
calendar,
subSecondNanos,
scale,
SSType.DATETIMEOFFSET);
writeShort((short) minutesOffset);
}
void writeOffsetDateTimeWithTimezone(OffsetDateTime offsetDateTimeValue, int scale) throws SQLServerException
{
GregorianCalendar calendar = null;
TimeZone timeZone;
long utcMillis = 0;
int subSecondNanos = 0;
int minutesOffset=0;
try
{
// offsetTimeValue.getOffset() returns a ZoneOffset object which has only hours and minutes
// components. So the result of the division will be an integer always. SQL Server also supports
// offsets in minutes precision.
minutesOffset = offsetDateTimeValue.getOffset().getTotalSeconds()/60;
}
catch(Exception e)
{
throw new SQLServerException(
SQLServerException.getErrString("R_zoneOffsetError"),
null, // SQLState is null as this error is generated in the driver
0, // Use 0 instead of DriverError.NOT_SET to use the correct constructor
null);
}
subSecondNanos = offsetDateTimeValue.getNano();
// writeScaledTemporal() expects subSecondNanos in 9 digits precssion
// but getNano() used in OffsetDateTime returns precession based on nanoseconds read from csv
// padding zeros to match the expectation of writeScaledTemporal()
int padding = 9 - String.valueOf(subSecondNanos).length();
while (padding > 0)
{
subSecondNanos= subSecondNanos*10;
padding--;
}
// For TIME_WITH_TIMEZONE, use UTC for the calendar that will hold the value
timeZone=UTC.timeZone;
// The behavior is similar to microsoft.sql.DateTimeOffset
// In Timestamp format, only YEAR needs to have 4 digits. The leading zeros for the rest of the fields can be omitted.
String offDateTimeStr = String.format("%04d",offsetDateTimeValue.getYear())
+ '-' + offsetDateTimeValue.getMonthValue()
+ '-' + offsetDateTimeValue.getDayOfMonth()
+ ' ' + offsetDateTimeValue.getHour()
+ ':' + offsetDateTimeValue.getMinute()
+ ':' + offsetDateTimeValue.getSecond();
utcMillis = Timestamp.valueOf(offDateTimeStr).getTime();
calendar = initializeCalender(timeZone);
calendar.setTimeInMillis(utcMillis);
// Local timezone value in minutes
int minuteAdjustment = ((TimeZone.getDefault().getRawOffset())/(60 * 1000));
// check if date is in day light savings and add daylight saving minutes
if(TimeZone.getDefault().inDaylightTime(calendar.getTime()))
minuteAdjustment += (TimeZone.getDefault().getDSTSavings())/(60*1000);
// If the local time is negative then positive minutesOffset must be subtracted from calender
minuteAdjustment += (minuteAdjustment < 0) ? (minutesOffset*(-1)) : minutesOffset;
calendar.add(Calendar.MINUTE, minuteAdjustment);
writeScaledTemporal(
calendar,
subSecondNanos,
scale,
SSType.DATETIMEOFFSET);
writeShort((short) minutesOffset);
}
void writeOffsetTimeWithTimezone(OffsetTime offsetTimeValue, int scale) throws SQLServerException
{
GregorianCalendar calendar = null;
TimeZone timeZone;
long utcMillis = 0;
int subSecondNanos = 0;
int minutesOffset=0;
try
{
// offsetTimeValue.getOffset() returns a ZoneOffset object which has only hours and minutes
// components. So the result of the division will be an integer always. SQL Server also supports
// offsets in minutes precision.
minutesOffset = offsetTimeValue.getOffset().getTotalSeconds()/60;
}
catch(Exception e)
{
throw new SQLServerException(
SQLServerException.getErrString("R_zoneOffsetError"),
null, // SQLState is null as this error is generated in the driver
0, // Use 0 instead of DriverError.NOT_SET to use the correct constructor
null);
}
subSecondNanos = offsetTimeValue.getNano();
// writeScaledTemporal() expects subSecondNanos in 9 digits precssion
// but getNano() used in OffsetDateTime returns precession based on nanoseconds read from csv
// padding zeros to match the expectation of writeScaledTemporal()
int padding = 9 - String.valueOf(subSecondNanos).length();
while (padding > 0)
{
subSecondNanos= subSecondNanos*10;
padding--;
}
// For TIME_WITH_TIMEZONE, use UTC for the calendar that will hold the value
timeZone = UTC.timeZone;
// Using TDS.BASE_YEAR_1900, based on SQL server behavious
// If date only contains a time part, the return value is 1900, the base year.
// https://msdn.microsoft.com/en-us/library/ms186313.aspx
// In Timestamp format, leading zeros for the fields can be omitted.
String offsetTimeStr = TDS.BASE_YEAR_1900 + "-01-01"
+ ' ' + offsetTimeValue.getHour()
+ ':' + offsetTimeValue.getMinute()
+ ':' + offsetTimeValue.getSecond();
utcMillis = Timestamp.valueOf(offsetTimeStr).getTime();
calendar = initializeCalender(timeZone);
calendar.setTimeInMillis(utcMillis);
int minuteAdjustment=(TimeZone.getDefault().getRawOffset())/(60 * 1000);
// check if date is in day light savings and add daylight saving minutes to Local timezone(in minutes)
if(TimeZone.getDefault().inDaylightTime(calendar.getTime()))
minuteAdjustment += ((TimeZone.getDefault().getDSTSavings())/(60*1000));
// If the local time is negative then positive minutesOffset must be subtracted from calender
minuteAdjustment += (minuteAdjustment < 0) ? (minutesOffset*(-1)) : minutesOffset;
calendar.add(Calendar.MINUTE, minuteAdjustment);
writeScaledTemporal(
calendar,
subSecondNanos,
scale,
SSType.DATETIMEOFFSET);
writeShort((short) minutesOffset);
}
void writeLong(long value) throws SQLServerException
{
if (stagingBuffer.remaining() >= 8)
{
stagingBuffer.putLong(value);
if (tdsChannel.isLoggingPackets())
{
if (dataIsLoggable)
logBuffer.putLong(value);
else
logBuffer.position(logBuffer.position() + 8);
}
}
else
{
valueBytes[0] = (byte)((value >> 0) & 0xFF);
valueBytes[1] = (byte)((value >> 8) & 0xFF);
valueBytes[2] = (byte)((value >> 16) & 0xFF);
valueBytes[3] = (byte)((value >> 24) & 0xFF);
valueBytes[4] = (byte)((value >> 32) & 0xFF);
valueBytes[5] = (byte)((value >> 40) & 0xFF);
valueBytes[6] = (byte)((value >> 48) & 0xFF);
valueBytes[7] = (byte)((value >> 56) & 0xFF);
writeWrappedBytes(valueBytes, 8);
}
}
void writeBytes(byte[] value) throws SQLServerException
{
writeBytes(value, 0, value.length);
}
void writeBytes(byte[] value, int offset, int length) throws SQLServerException
{
assert length <= value.length;
int bytesWritten = 0;
int bytesToWrite;
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Writing " + length + " bytes");
while ((bytesToWrite = length - bytesWritten) > 0)
{
if (0 == stagingBuffer.remaining())
writePacket(TDS.STATUS_NORMAL);
if (bytesToWrite > stagingBuffer.remaining())
bytesToWrite = stagingBuffer.remaining();
stagingBuffer.put(value, offset + bytesWritten, bytesToWrite);
if (tdsChannel.isLoggingPackets())
{
if (dataIsLoggable)
logBuffer.put(value, offset + bytesWritten, bytesToWrite);
else
logBuffer.position(logBuffer.position() + bytesToWrite);
}
bytesWritten += bytesToWrite;
}
}
void writeWrappedBytes(byte value[], int valueLength) throws SQLServerException
{
// This function should only be used to write a value that is longer than
// what remains in the current staging buffer. However, the value must
// be short enough to fit in an empty buffer.
assert valueLength <= value.length;
assert stagingBuffer.remaining() < valueLength;
assert valueLength <= stagingBuffer.capacity();
// Fill any remaining space in the staging buffer
int remaining = stagingBuffer.remaining();
if (remaining > 0)
{
stagingBuffer.put(value, 0, remaining);
if (tdsChannel.isLoggingPackets())
{
if (dataIsLoggable)
logBuffer.put(value, 0, remaining);
else
logBuffer.position(logBuffer.position() + remaining);
}
}
writePacket(TDS.STATUS_NORMAL);
// After swapping, the staging buffer should once again be empty, so the
// remainder of the value can be written to it.
stagingBuffer.put(value, remaining, valueLength - remaining);
if (tdsChannel.isLoggingPackets())
{
if (dataIsLoggable)
logBuffer.put(value, remaining, valueLength - remaining);
else
logBuffer.position(logBuffer.position() + remaining);
}
}
void writeString(String value) throws SQLServerException
{
int charsCopied = 0;
int length = value.length();
while (charsCopied < length)
{
int bytesToCopy = 2 * (length - charsCopied);
if (bytesToCopy > valueBytes.length)
bytesToCopy = valueBytes.length;
int bytesCopied = 0;
while (bytesCopied < bytesToCopy)
{
char ch = value.charAt(charsCopied++);
valueBytes[bytesCopied++] = (byte)((ch >> 0) & 0xFF);
valueBytes[bytesCopied++] = (byte)((ch >> 8) & 0xFF);
}
writeBytes(valueBytes, 0, bytesCopied);
}
}
void writeStream(
InputStream inputStream,
long advertisedLength,
boolean writeChunkSizes) throws SQLServerException
{
assert DataTypes.UNKNOWN_STREAM_LENGTH == advertisedLength || advertisedLength >= 0;
long actualLength = 0;
final byte[] streamByteBuffer = new byte[4 * currentPacketSize];
int bytesRead = 0;
int bytesToWrite;
do
{
// Read in next chunk
for (bytesToWrite = 0; -1 != bytesRead && bytesToWrite < streamByteBuffer.length; bytesToWrite += bytesRead)
{
try
{
bytesRead = inputStream.read(streamByteBuffer, bytesToWrite, streamByteBuffer.length - bytesToWrite);
}
catch (IOException e)
{
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorReadingStream"));
Object[] msgArgs = {e.toString()};
error(form.format(msgArgs), SQLState.DATA_EXCEPTION_NOT_SPECIFIC, DriverError.NOT_SET);
}
if (-1 == bytesRead)
break;
// Check for invalid bytesRead returned from InputStream.read
if (bytesRead < 0 || bytesRead > streamByteBuffer.length - bytesToWrite)
{
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorReadingStream"));
Object[] msgArgs = {SQLServerException.getErrString("R_streamReadReturnedInvalidValue")};
error(form.format(msgArgs), SQLState.DATA_EXCEPTION_NOT_SPECIFIC, DriverError.NOT_SET);
}
}
// Write it out
if (writeChunkSizes)
writeInt(bytesToWrite);
writeBytes(streamByteBuffer, 0, bytesToWrite);
actualLength += bytesToWrite;
}
while (-1 != bytesRead || bytesToWrite > 0);
// If we were given an input stream length that we had to match and
// the actual stream length did not match then cancel the request.
if (DataTypes.UNKNOWN_STREAM_LENGTH != advertisedLength && actualLength != advertisedLength)
{
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_mismatchedStreamLength"));
Object[] msgArgs = {Long.valueOf(advertisedLength), Long.valueOf(actualLength)};
error(form.format(msgArgs), SQLState.DATA_EXCEPTION_LENGTH_MISMATCH, DriverError.NOT_SET);
}
}
/*
* Adding another function for writing non-unicode reader instead of re-factoring the writeReader() for performance efficiency.
* As this method will only be used in bulk copy, it needs to be efficient.
* Note: Any changes in algorithm/logic should propagate to both writeReader() and writeNonUnicodeReader().
*/
void writeNonUnicodeReader(
Reader reader,
long advertisedLength,
boolean isDestBinary,
String charSet) throws SQLServerException
{
assert DataTypes.UNKNOWN_STREAM_LENGTH == advertisedLength || advertisedLength >= 0;
long actualLength = 0;
char[] streamCharBuffer = new char[currentPacketSize];
// The unicode version, writeReader() allocates a byte buffer that is 4 times the currentPacketSize, not sure why.
byte[] streamByteBuffer = new byte[currentPacketSize];
int charsRead = 0;
int charsToWrite;
int bytesToWrite;
String streamString;
do
{
// Read in next chunk
for (charsToWrite = 0; -1 != charsRead && charsToWrite < streamCharBuffer.length; charsToWrite += charsRead)
{
try
{
charsRead = reader.read(streamCharBuffer, charsToWrite, streamCharBuffer.length - charsToWrite);
}
catch (IOException e)
{
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorReadingStream"));
Object[] msgArgs = {e.toString()};
error(form.format(msgArgs), SQLState.DATA_EXCEPTION_NOT_SPECIFIC, DriverError.NOT_SET);
}
if (-1 == charsRead)
break;
// Check for invalid bytesRead returned from Reader.read
if (charsRead < 0 || charsRead > streamCharBuffer.length - charsToWrite)
{
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorReadingStream"));
Object[] msgArgs = {SQLServerException.getErrString("R_streamReadReturnedInvalidValue")};
error(form.format(msgArgs), SQLState.DATA_EXCEPTION_NOT_SPECIFIC, DriverError.NOT_SET);
}
}
if(!isDestBinary)
{
// Write it out
// This also writes the PLP_TERMINATOR token after all the data in the the stream are sent.
// The Do-While loop goes on one more time as charsToWrite is greater than 0 for the last chunk, and
// in this last round the only thing that is written is an int value of 0, which is the PLP Terminator token(0x00000000).
writeInt(charsToWrite);
for (int charsCopied = 0; charsCopied < charsToWrite; ++charsCopied)
{
try
{
if(null == charSet)
{
streamByteBuffer[charsCopied] = (byte)(streamCharBuffer[charsCopied] & 0xFF);
}
else
{
// encoding as per collation
streamByteBuffer[charsCopied] = new String(streamCharBuffer[charsCopied] +
"")
.getBytes(charSet)[0];
}
}
catch (UnsupportedEncodingException e)
{
throw new SQLServerException(
SQLServerException.getErrString("R_encodingErrorWritingTDS"),
e);
}
}
writeBytes(streamByteBuffer, 0, charsToWrite);
}
else
{
bytesToWrite = charsToWrite;
if(0 != charsToWrite)
bytesToWrite = charsToWrite /2;
streamString = new String(streamCharBuffer);
byte[] bytes = ParameterUtils.HexToBin(streamString.trim());
writeInt(bytesToWrite);
writeBytes(bytes , 0 , bytesToWrite);
}
actualLength += charsToWrite;
}
while (-1 != charsRead || charsToWrite > 0);
// If we were given an input stream length that we had to match and
// the actual stream length did not match then cancel the request.
if (DataTypes.UNKNOWN_STREAM_LENGTH != advertisedLength && actualLength != advertisedLength)
{
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_mismatchedStreamLength"));
Object[] msgArgs = {Long.valueOf(advertisedLength), Long.valueOf(actualLength)};
error(form.format(msgArgs), SQLState.DATA_EXCEPTION_LENGTH_MISMATCH, DriverError.NOT_SET);
}
}
/*
* Note: There is another method with same code logic for non unicode reader, writeNonUnicodeReader(), implemented for performance efficiency.
* Any changes in algorithm/logic should propagate to both writeReader() and writeNonUnicodeReader().
*/
void writeReader(
Reader reader,
long advertisedLength,
boolean writeChunkSizes) throws SQLServerException
{
assert DataTypes.UNKNOWN_STREAM_LENGTH == advertisedLength || advertisedLength >= 0;
long actualLength = 0;
char[] streamCharBuffer = new char[2 * currentPacketSize];
byte[] streamByteBuffer = new byte[4 * currentPacketSize];
int charsRead = 0;
int charsToWrite;
do
{
// Read in next chunk
for (charsToWrite = 0; -1 != charsRead && charsToWrite < streamCharBuffer.length; charsToWrite += charsRead)
{
try
{
charsRead = reader.read(streamCharBuffer, charsToWrite, streamCharBuffer.length - charsToWrite);
}
catch (IOException e)
{
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorReadingStream"));
Object[] msgArgs = {e.toString()};
error(form.format(msgArgs), SQLState.DATA_EXCEPTION_NOT_SPECIFIC, DriverError.NOT_SET);
}
if (-1 == charsRead)
break;
// Check for invalid bytesRead returned from Reader.read
if (charsRead < 0 || charsRead > streamCharBuffer.length - charsToWrite)
{
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorReadingStream"));
Object[] msgArgs = {SQLServerException.getErrString("R_streamReadReturnedInvalidValue")};
error(form.format(msgArgs), SQLState.DATA_EXCEPTION_NOT_SPECIFIC, DriverError.NOT_SET);
}
}
// Write it out
if (writeChunkSizes)
writeInt(2 * charsToWrite);
// Convert from Unicode characters to bytes
//
// Note: The following inlined code is much faster than the equivalent
// call to (new String(streamCharBuffer)).getBytes("UTF-16LE") because it
// saves a conversion to String and use of Charset in that conversion.
for (int charsCopied = 0; charsCopied < charsToWrite; ++charsCopied)
{
streamByteBuffer[2 * charsCopied] = (byte)((streamCharBuffer[charsCopied] >> 0) & 0xFF);
streamByteBuffer[2 * charsCopied + 1] = (byte)((streamCharBuffer[charsCopied] >> 8) & 0xFF);
}
writeBytes(streamByteBuffer, 0, 2 * charsToWrite);
actualLength += charsToWrite;
}
while (-1 != charsRead || charsToWrite > 0);
// If we were given an input stream length that we had to match and
// the actual stream length did not match then cancel the request.
if (DataTypes.UNKNOWN_STREAM_LENGTH != advertisedLength && actualLength != advertisedLength)
{
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_mismatchedStreamLength"));
Object[] msgArgs = {Long.valueOf(advertisedLength), Long.valueOf(actualLength)};
error(form.format(msgArgs), SQLState.DATA_EXCEPTION_LENGTH_MISMATCH, DriverError.NOT_SET);
}
}
GregorianCalendar initializeCalender(TimeZone timeZone)
{
GregorianCalendar calendar = null;
// Create the calendar that will hold the value. For DateTimeOffset values, the calendar's
// time zone is UTC. For other values, the calendar's time zone is a local time zone.
calendar = new GregorianCalendar(timeZone, Locale.US);
// Set the calendar lenient to allow setting the DAY_OF_YEAR and MILLISECOND fields
// to roll other fields to their correct values.
calendar.setLenient(true);
// Clear the calendar of any existing state. The state of a new Calendar object always
// reflects the current date, time, DST offset, etc.
calendar.clear();
return calendar;
}
final void error(String reason, SQLState sqlState, DriverError driverError) throws SQLServerException
{
assert null != command;
command.interrupt(reason);
throw new SQLServerException(reason, sqlState, driverError, null);
}
/**
* Sends an attention signal to the server, if necessary, to tell it to stop processing
* the current command on this connection.
*
* If no packets of the command's request have yet been sent to the server, then no
* attention signal needs to be sent. The interrupt will be handled entirely by the
* driver.
*
* This method does not need synchronization as it does not manipulate interrupt
* state and writing is guaranteed to occur only from one thread at a time.
*/
final boolean sendAttention() throws SQLServerException
{
// If any request packets were already written to the server then send an
// attention signal to the server to tell it to ignore the request or
// cancel its execution.
if (packetNum > 0)
{
//Ideally, we would want to add the following assert here.
//But to add that the variable isEOMSent would have to be made
//volatile as this piece of code would be reached from multiple
//threads. So, not doing it to avoid perf hit. Note that
//isEOMSent would be updated in writePacket everytime an EOM is sent
//assert isEOMSent;
if (logger.isLoggable(Level.FINE))
logger.fine(this + ": sending attention...");
++tdsChannel.numMsgsSent;
startMessage(command, TDS.PKT_CANCEL_REQ);
endMessage();
return true;
}
return false;
}
private final void writePacket(int tdsMessageStatus) throws SQLServerException
{
final boolean atEOM = (TDS.STATUS_BIT_EOM == (TDS.STATUS_BIT_EOM & tdsMessageStatus));
final boolean isCancelled = ( (TDS.PKT_CANCEL_REQ == tdsMessageType)
||
((tdsMessageStatus & TDS.STATUS_BIT_ATTENTION)==TDS.STATUS_BIT_ATTENTION)
);
// Before writing each packet to the channel, check if an interrupt has occurred.
if (null != command && (!isCancelled))
command.checkForInterrupt();
writePacketHeader(tdsMessageStatus|sendResetConnection);
sendResetConnection = 0;
flush(atEOM);
// If this is the last packet then flush the remainder of the request
// through the socket. The first flush() call ensured that data currently
// waiting in the socket buffer was sent, flipped the buffers, and started
// sending data from the staging buffer (flipped to be the new socket buffer).
// This flush() call ensures that all remaining data in the socket buffer is sent.
if (atEOM)
{
flush(atEOM);
isEOMSent = true;
++tdsChannel.numMsgsSent;
}
// If we just sent the first login request packet and SSL encryption was enabled
// for login only, then disable SSL now.
if (TDS.PKT_LOGON70 == tdsMessageType &&
1 == packetNum &&
TDS.ENCRYPT_OFF == con.getNegotiatedEncryptionLevel())
{
tdsChannel.disableSSL();
}
// Notify the currently associated command (if any) that we have written the last
// of the response packets to the channel.
if (null != command && (!isCancelled) && atEOM)
command.onRequestComplete();
}
private final void writePacketHeader(int tdsMessageStatus)
{
int tdsMessageLength = stagingBuffer.position();
++packetNum;
// Write the TDS packet header back at the start of the staging buffer
stagingBuffer.put(TDS.PACKET_HEADER_MESSAGE_TYPE, tdsMessageType);
stagingBuffer.put(TDS.PACKET_HEADER_MESSAGE_STATUS, (byte) tdsMessageStatus);
stagingBuffer.put(TDS.PACKET_HEADER_MESSAGE_LENGTH, (byte)((tdsMessageLength >> 8) & 0xFF)); // Note: message length is 16 bits,
stagingBuffer.put(TDS.PACKET_HEADER_MESSAGE_LENGTH + 1, (byte)((tdsMessageLength >> 0) & 0xFF)); // written BIG ENDIAN
stagingBuffer.put(TDS.PACKET_HEADER_SPID, (byte)((tdsChannel.getSPID() >> 8) & 0xFF)); // Note: SPID is 16 bits,
stagingBuffer.put(TDS.PACKET_HEADER_SPID + 1, (byte)((tdsChannel.getSPID() >> 0) & 0xFF)); // written BIG ENDIAN
stagingBuffer.put(TDS.PACKET_HEADER_SEQUENCE_NUM, (byte)(packetNum % 256));
stagingBuffer.put(TDS.PACKET_HEADER_WINDOW, (byte)0); // Window (Reserved/Not used)
// Write the header to the log buffer too if logging.
if (tdsChannel.isLoggingPackets())
{
logBuffer.put(TDS.PACKET_HEADER_MESSAGE_TYPE, tdsMessageType);
logBuffer.put(TDS.PACKET_HEADER_MESSAGE_STATUS, (byte) tdsMessageStatus);
logBuffer.put(TDS.PACKET_HEADER_MESSAGE_LENGTH, (byte)((tdsMessageLength >> 8) & 0xFF)); // Note: message length is 16 bits,
logBuffer.put(TDS.PACKET_HEADER_MESSAGE_LENGTH + 1, (byte)((tdsMessageLength >> 0) & 0xFF)); // written BIG ENDIAN
logBuffer.put(TDS.PACKET_HEADER_SPID, (byte)((tdsChannel.getSPID() >> 8) & 0xFF)); // Note: SPID is 16 bits,
logBuffer.put(TDS.PACKET_HEADER_SPID + 1, (byte)((tdsChannel.getSPID() >> 0) & 0xFF)); // written BIG ENDIAN
logBuffer.put(TDS.PACKET_HEADER_SEQUENCE_NUM, (byte)(packetNum % 256));
logBuffer.put(TDS.PACKET_HEADER_WINDOW, (byte)0); // Window (Reserved/Not used);
}
}
void flush(boolean atEOM) throws SQLServerException
{
// First, flush any data left in the socket buffer.
tdsChannel.write(socketBuffer.array(), socketBuffer.position(), socketBuffer.remaining());
socketBuffer.position(socketBuffer.limit());
// If there is data in the staging buffer that needs to be written
// to the socket, the socket buffer is now empty, so swap buffers
// and start writing data from the staging buffer.
if (stagingBuffer.position() >= TDS_PACKET_HEADER_SIZE)
{
// Swap the packet buffers ...
ByteBuffer swapBuffer = stagingBuffer;
stagingBuffer = socketBuffer;
socketBuffer = swapBuffer;
// ... and prepare to send data from the from the new socket
// buffer (the old staging buffer).
//
// We need to use flip() rather than rewind() here so that
// the socket buffer's limit is properly set for the last
// packet, which may be shorter than the other packets.
socketBuffer.flip();
stagingBuffer.clear();
// If we are logging TDS packets then log the packet we're about
// to send over the wire now.
if (tdsChannel.isLoggingPackets())
{
tdsChannel.logPacket(
logBuffer.array(),
0,
socketBuffer.limit(),
this.toString() + " sending packet (" + socketBuffer.limit() + " bytes)");
}
// Prepare for the next packet
if (!atEOM)
preparePacket();
// Finally, start sending data from the new socket buffer.
tdsChannel.write(socketBuffer.array(), socketBuffer.position(), socketBuffer.remaining());
socketBuffer.position(socketBuffer.limit());
}
}
// Composite write operations
/**
* Write out elements common to all RPC values.
* @param sName the optional parameter name
* @param bOut boolean true if the value that follows is being registered as an ouput parameter
* @param tdsType TDS type of the value that follows
*/
void writeRPCNameValType(String sName, boolean bOut, TDSType tdsType) throws SQLServerException
{
int nNameLen=0;
if (null != sName)
nNameLen = sName.length()+1; //The @ prefix is required for the param
writeByte((byte) nNameLen); // param name len
if (nNameLen > 0)
{
writeChar('@');
writeString(sName);
}
if(null!=cryptoMeta)
writeByte((byte) (bOut ? 1|TDS.AE_METADATA : 0|TDS.AE_METADATA)); // status
else
writeByte((byte) (bOut ? 1 : 0)); // status
writeByte(tdsType.byteValue()); // type
}
/**
* Append a boolean value in RPC transmission format.
* @param sName the optional parameter name
* @param booleanValue the data value
* @param bOut boolean true if the data value is being registered as an ouput parameter
*/
void writeRPCBit(String sName, Boolean booleanValue, boolean bOut) throws SQLServerException
{
writeRPCNameValType(sName, bOut, TDSType.BITN);
writeByte((byte) 1); // max length of datatype
if (null == booleanValue)
{
writeByte((byte) 0); // len of data bytes
}
else
{
writeByte((byte) 1); //length of datatype
writeByte((byte)(booleanValue.booleanValue() ? 1 : 0));
}
}
/**
* Append a short value in RPC transmission format.
* @param sName the optional parameter name
* @param shortValue the data value
* @param bOut boolean true if the data value is being registered as an ouput parameter
*/
void writeRPCByte(String sName, Byte byteValue, boolean bOut) throws SQLServerException
{
writeRPCNameValType(sName, bOut, TDSType.INTN);
writeByte((byte) 1); // max length of datatype
if (null == byteValue)
{
writeByte((byte) 0); // len of data bytes
}
else
{
writeByte((byte) 1); //length of datatype
writeByte(byteValue.byteValue());
}
}
/**
* Append a short value in RPC transmission format.
* @param sName the optional parameter name
* @param shortValue the data value
* @param bOut boolean true if the data value is being registered as an ouput parameter
*/
void writeRPCShort(String sName, Short shortValue, boolean bOut) throws SQLServerException
{
writeRPCNameValType(sName, bOut, TDSType.INTN);
writeByte((byte) 2); // max length of datatype
if (null == shortValue)
{
writeByte((byte) 0); // len of data bytes
}
else
{
writeByte((byte) 2); //length of datatype
writeShort(shortValue.shortValue());
}
}
/**
* Append an int value in RPC transmission format.
* @param sName the optional parameter name
* @param intValue the data value
* @param bOut boolean true if the data value is being registered as an ouput parameter
*/
void writeRPCInt(String sName, Integer intValue, boolean bOut) throws SQLServerException
{
writeRPCNameValType(sName, bOut, TDSType.INTN);
writeByte((byte) 4); // max length of datatype
if (null == intValue)
{
writeByte((byte) 0); // len of data bytes
}
else
{
writeByte((byte) 4); // length of datatype
writeInt(intValue.intValue());
}
}
/**
* Append a long value in RPC transmission format.
* @param sName the optional parameter name
* @param longValue the data value
* @param bOut boolean true if the data value is being registered as an ouput parameter
*/
void writeRPCLong(String sName, Long longValue, boolean bOut) throws SQLServerException
{
writeRPCNameValType(sName, bOut, TDSType.INTN);
writeByte((byte) 8); // max length of datatype
if (null == longValue)
{
writeByte((byte) 0); // len of data bytes
}
else
{
writeByte((byte) 8); // length of datatype
writeLong(longValue.longValue());
}
}
/**
* Append a real value in RPC transmission format.
* @param sName the optional parameter name
* @param floatValue the data value
* @param bOut boolean true if the data value is being registered as an ouput parameter
*/
void writeRPCReal(String sName, Float floatValue, boolean bOut) throws SQLServerException
{
writeRPCNameValType(sName, bOut, TDSType.FLOATN);
// Data and length
if (null == floatValue)
{
writeByte((byte) 4); // max length
writeByte((byte) 0); // actual length (0 == null)
}
else
{
writeByte((byte) 4); // max length
writeByte((byte) 4); // actual length
writeInt(Float.floatToRawIntBits(floatValue.floatValue()));
}
}
/**
* Append a double value in RPC transmission format.
* @param sName the optional parameter name
* @param doubleValue the data value
* @param bOut boolean true if the data value is being registered as an ouput parameter
*/
void writeRPCDouble(String sName, Double doubleValue, boolean bOut) throws SQLServerException
{
writeRPCNameValType(sName, bOut, TDSType.FLOATN);
int l = 8;
writeByte((byte) l); // max length of datatype
// Data and length
if (null == doubleValue)
{
writeByte((byte) 0); // len of data bytes
}
else
{
writeByte((byte) l); // len of data bytes
long bits = Double.doubleToLongBits(doubleValue.doubleValue());
long mask = 0xFF;
int nShift =0;
for (int i=0; i<8; i++)
{
writeByte((byte) ((bits & mask) >> nShift));
nShift += 8;
mask = mask << 8;
}
}
}
/**
* Append a big decimal in RPC transmission format.
* @param sName the optional parameter name
* @param bdValue the data value
* @param nScale the desired scale
* @param bOut boolean true if the data value is being registered as an ouput parameter
*/
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
byte[] valueBytes = DDC.convertBigDecimalToBytes(bdValue, nScale);
writeBytes(valueBytes, 0, valueBytes.length);
}
/**
* Appends a standard v*max header for RPC parameter transmission.
* @param headerLength the total length of the PLP data block.
* @param isNull true if the value is NULL.
* @param collation The SQL collation associated with the value that follows the v*max header. Null for non-textual types.
*/
void writeVMaxHeader(long headerLength, boolean isNull, SQLCollation collation) throws SQLServerException
{
// Send v*max length indicator 0xFFFF.
writeShort((short) 0xFFFF);
// Send collation if requested.
if (null != collation)
collation.writeCollation(this);
// Handle null here and return, we're done here if it's null.
if (isNull)
{
// Null header for v*max types is 0xFFFFFFFFFFFFFFFF.
writeLong(0xFFFFFFFFFFFFFFFFL);
}
else if (DataTypes.UNKNOWN_STREAM_LENGTH == headerLength)
{
// Append v*max length.
// UNKNOWN_PLP_LEN is 0xFFFFFFFFFFFFFFFE
writeLong(0xFFFFFFFFFFFFFFFEL);
// NOTE: Don't send the first chunk length, this will be calculated by caller.
}
else
{
// For v*max types with known length, length is
// We're sending same total length as chunk length (as we're sending 1 chunk).
writeLong(headerLength);
}
}
/**
* Utility for internal writeRPCString calls
*/
void writeRPCStringUnicode(String sValue) throws SQLServerException
{
writeRPCStringUnicode(null, sValue, false, null);
}
/**
* Writes a string value as Unicode for RPC
* @param sName the optional parameter name
* @param sValue the data value
* @param bOut boolean true if the data value is being registered as an ouput parameter
* @param collation the collation of the data value
*/
void writeRPCStringUnicode(
String sName,
String sValue,
boolean bOut,
SQLCollation collation) throws SQLServerException
{
boolean bValueNull = (sValue==null);
int nValueLen = bValueNull ? 0 : (2 * sValue.length());
boolean isShortValue = nValueLen <= DataTypes.SHORT_VARTYPE_MAX_BYTES;
// Textual RPC requires a collation. If none is provided, as is the case when
// the SSType is non-textual, then use the database collation by default.
if (null == collation)
collation = con.getDatabaseCollation();
// Use PLP encoding on Yukon and later with long values and OUT parameters
boolean usePLP = (!isShortValue || bOut);
if (usePLP)
{
writeRPCNameValType(sName, bOut, TDSType.NVARCHAR);
// Handle Yukon v*max type header here.
writeVMaxHeader(
nValueLen, // Length
bValueNull, // Is null?
collation);
// Send the data.
if (!bValueNull)
{
if (nValueLen > 0)
{
writeInt(nValueLen);
writeString(sValue);
}
// Send the terminator PLP chunk.
writeInt(0);
}
}
else // non-PLP type
{
// Write maximum length of data
if (isShortValue)
{
writeRPCNameValType(sName, bOut, TDSType.NVARCHAR);
writeShort((short) DataTypes.SHORT_VARTYPE_MAX_BYTES);
}
else
{
writeRPCNameValType(sName, bOut, TDSType.NTEXT);
writeInt(DataTypes.IMAGE_TEXT_MAX_BYTES);
}
collation.writeCollation(this);
// Data and length
if (bValueNull)
{
writeShort((short) -1); // actual len
}
else
{
// Write actual length of data
if (isShortValue)
writeShort((short) nValueLen);
else
writeInt(nValueLen);
// If length is zero, we're done.
if (0 != nValueLen)
writeString(sValue); //data
}
}
}
void writeTVP(TVP value) throws SQLServerException
{
if (!value.isNull())
{
writeByte((byte) 0); // status
}
else
{
// Default TVP
writeByte((byte) TDS.TVP_STATUS_DEFAULT); // default TVP
}
writeByte((byte)TDS.TDS_TVP);
/*
TVP_TYPENAME = DbName
OwningSchema
TypeName
*/
// Database where TVP type resides
if(null != value.getDbNameTVP())
{
writeByte((byte) value.getDbNameTVP().length());
writeString(value.getDbNameTVP());
}
else
writeByte((byte) 0x00); // empty DB name
// Schema where TVP type resides
if(null != value.getOwningSchemaNameTVP())
{
writeByte((byte) value.getOwningSchemaNameTVP().length());
writeString(value.getOwningSchemaNameTVP());
}
else
writeByte((byte) 0x00); // empty Schema name
// TVP type name
if(null != value.getTVPName())
{
writeByte((byte) value.getTVPName().length());
writeString(value.getTVPName());
}
else
writeByte((byte) 0x00); // empty TVP name
if (!value.isNull())
{
writeTVPColumnMetaData(value);
// optional OrderUnique metadata
writeTvpOrderUnique(value);
}
else
{
writeShort((short) TDS.TVP_NULL_TOKEN);
}
// TVP_END_TOKEN
writeByte((byte) 0x00);
try
{
writeTVPRows(value);
}
catch(NumberFormatException e)
{
throw new SQLServerException(SQLServerException.getErrString("R_TVPInvalidColumnValue"), e);
}
catch(ClassCastException e)
{
throw new SQLServerException(SQLServerException.getErrString("R_TVPInvalidColumnValue"), e);
}
}
void writeTVPRows(TVP value) throws SQLServerException
{
boolean isShortValue, isNull;
int dataLength;
if (!value.isNull())
{
Map columnMetadata = value.getColumnMetadata();
Iterator> columnsIterator;
while(value.next())
{
Object[] rowData = value.getRowData();
// ROW
writeByte((byte) TDS.TVP_ROW);
columnsIterator = columnMetadata.entrySet().iterator();
int currentColumn = 0;
while(columnsIterator.hasNext())
{
Map.Entry columnPair = (Map.Entry)columnsIterator.next();
// If useServerDefault is set, client MUST NOT emit TvpColumnData for the associated column
if (columnPair.getValue().useServerDefault)
{
currentColumn++;
continue;
}
JDBCType jdbcType = JDBCType.of(columnPair.getValue().javaSqlType);
String currentColumnStringValue = null;
Object currentObject = null;
if (null != rowData)
{
// if rowData has value for the current column, retrieve it. If not, current column will stay null.
if (rowData.length > currentColumn)
{
currentObject = rowData[currentColumn];
if (null != currentObject)
{
currentColumnStringValue = String.valueOf(currentObject);
}
}
}
switch(jdbcType)
{
case BIGINT:
if(null == currentColumnStringValue)
writeByte((byte) 0);
else
{
writeByte((byte) 8);
writeLong(Long.valueOf(currentColumnStringValue).longValue());
}
break;
case BIT:
if(null == currentColumnStringValue)
writeByte((byte) 0);
else
{
writeByte((byte) 1);
writeByte((byte) (Boolean.valueOf(currentColumnStringValue).booleanValue() ? 1 : 0));
}
break;
case INTEGER:
if(null == currentColumnStringValue)
writeByte((byte) 0);
else
{
writeByte((byte)4);
writeInt(Integer.valueOf(currentColumnStringValue).intValue());
}
break;
case SMALLINT:
case TINYINT:
if(null == currentColumnStringValue)
writeByte((byte) 0);
else
{
writeByte((byte) 2); //length of datatype
writeShort(Short.valueOf(currentColumnStringValue).shortValue());
}
break;
case DECIMAL:
case NUMERIC:
if(null == currentColumnStringValue)
writeByte((byte) 0);
else
{
writeByte((byte) TDSWriter.BIGDECIMAL_MAX_LENGTH); // maximum length
BigDecimal bdValue = new BigDecimal(currentColumnStringValue);
// setScale of all BigDecimal value based on metadata sent
bdValue = bdValue.setScale(columnPair.getValue().scale, RoundingMode.HALF_UP);
byte[] valueBytes = DDC.convertBigDecimalToBytes(bdValue, bdValue.scale());
// 1-byte for sign and 16-byte for integer
byte[] byteValue = new byte[17];
// removing the precision and scale information from the valueBytes array
System.arraycopy(valueBytes, 2, byteValue, 0, valueBytes.length - 2);
writeBytes(byteValue);
}
break;
case DOUBLE:
if (null == currentColumnStringValue)
writeByte((byte) 0); // len of data bytes
else
{
writeByte((byte) 8); // len of data bytes
long bits = Double.doubleToLongBits(Double.valueOf(currentColumnStringValue).doubleValue());
long mask = 0xFF;
int nShift =0;
for (int i=0; i<8; i++)
{
writeByte((byte) ((bits & mask) >> nShift));
nShift += 8;
mask = mask << 8;
}
}
break;
case FLOAT:
case REAL:
if (null == currentColumnStringValue)
writeByte((byte) 0); // actual length (0 == null)
else
{
writeByte((byte) 4); // actual length
writeInt(Float.floatToRawIntBits(Float.valueOf(currentColumnStringValue).floatValue()));
}
break;
case DATE:
case TIME:
case TIMESTAMP:
case DATETIMEOFFSET:
case TIMESTAMP_WITH_TIMEZONE:
case TIME_WITH_TIMEZONE:
case CHAR:
case VARCHAR:
case NCHAR:
case NVARCHAR:
isShortValue = (2 * columnPair.getValue().precision) <= DataTypes.SHORT_VARTYPE_MAX_BYTES;
isNull = (null == currentColumnStringValue);
dataLength = isNull ? 0: currentColumnStringValue.length() * 2 ;
if(!isShortValue)
{
// check null
if (isNull)
// Null header for v*max types is 0xFFFFFFFFFFFFFFFF.
writeLong(0xFFFFFFFFFFFFFFFFL);
else if (DataTypes.UNKNOWN_STREAM_LENGTH == dataLength)
// Append v*max length.
// UNKNOWN_PLP_LEN is 0xFFFFFFFFFFFFFFFE
writeLong(0xFFFFFFFFFFFFFFFEL);
else
// For v*max types with known length, length is
writeLong(dataLength);
if(!isNull)
{
if(dataLength > 0)
{
writeInt(dataLength);
writeString(currentColumnStringValue);
}
// Send the terminator PLP chunk.
writeInt(0);
}
}
else
{
if(isNull)
writeShort((short) -1); // actual len
else
{
writeShort((short) dataLength);
writeString(currentColumnStringValue);
}
}
break;
case BINARY:
case VARBINARY:
// Handle conversions as done in other types.
isShortValue = columnPair.getValue().precision <= DataTypes.SHORT_VARTYPE_MAX_BYTES;
isNull = (null == currentObject);
if(currentObject instanceof String)
dataLength = isNull ? 0: (toByteArray(currentObject.toString())).length ;
else
dataLength = isNull ? 0: ((byte[])currentObject).length ;
if(!isShortValue)
{
// check null
if (isNull)
// Null header for v*max types is 0xFFFFFFFFFFFFFFFF.
writeLong(0xFFFFFFFFFFFFFFFFL);
else if (DataTypes.UNKNOWN_STREAM_LENGTH == dataLength)
// Append v*max length.
// UNKNOWN_PLP_LEN is 0xFFFFFFFFFFFFFFFE
writeLong(0xFFFFFFFFFFFFFFFEL);
else
// For v*max types with known length, length is
writeLong(dataLength);
if(!isNull)
{
if(dataLength > 0)
{
writeInt(dataLength);
if(currentObject instanceof String)
writeBytes(toByteArray(currentObject.toString()));
else
writeBytes((byte[])currentObject);
}
// Send the terminator PLP chunk.
writeInt(0);
}
}
else
{
if(isNull)
writeShort((short) -1); // actual len
else
{
writeShort((short) dataLength);
if(currentObject instanceof String)
writeBytes( toByteArray(currentObject.toString())) ;
else
writeBytes((byte[])currentObject);
}
}
break;
default:
assert false : "Unexpected JDBC type " + jdbcType.toString();
}
currentColumn ++;
}
}
}
// TVP_END_TOKEN
writeByte((byte) 0x00);
}
private static byte[] toByteArray(String s)
{
return DatatypeConverter.parseHexBinary(s);
}
void writeTVPColumnMetaData(TVP value) throws SQLServerException
{
boolean isShortValue;
//TVP_COLMETADATA
writeShort((short) value.getTVPColumnCount());
Map columnMetadata = value.getColumnMetadata();
Iterator> columnsIterator = columnMetadata.entrySet().iterator();
/*
TypeColumnMetaData = UserType
Flags
TYPE_INFO
ColName ;
*/
while(columnsIterator.hasNext())
{
Map.Entry pair = (Map.Entry)columnsIterator.next();
JDBCType jdbcType = JDBCType.of(pair.getValue().javaSqlType);
boolean useServerDefault = pair.getValue().useServerDefault;
// ULONG ; UserType of column
// The value will be 0x0000 with the exceptions of TIMESTAMP (0x0050) and alias types (greater than 0x00FF).
writeInt(0);
/*
Flags = fNullable ; Column is nullable - %x01
fCaseSen -- Ignored ;
usUpdateable -- Ignored ;
fIdentity ; Column is identity column - %x10
fComputed ; Column is computed - %x20
usReservedODBC -- Ignored ;
fFixedLenCLRType-- Ignored ;
fDefault ; Column is default value - %x200
usReserved -- Ignored ;
*/
short flags = TDS.FLAG_NULLABLE;
if (useServerDefault)
{
flags |= TDS.FLAG_TVP_DEFAULT_COLUMN;
}
writeShort(flags);
// Type info
switch(jdbcType)
{
case BIGINT:
writeByte(TDSType.INTN.byteValue());
writeByte((byte) 8); // max length of datatype
break;
case BIT:
writeByte(TDSType.BITN.byteValue());
writeByte((byte) 1); // max length of datatype
break;
case INTEGER:
writeByte(TDSType.INTN.byteValue());
writeByte((byte) 4); // max length of datatype
break;
case SMALLINT:
case TINYINT:
writeByte(TDSType.INTN.byteValue());
writeByte((byte) 2); // max length of datatype
break;
case DECIMAL:
case NUMERIC:
writeByte(TDSType.NUMERICN.byteValue());
writeByte((byte) 0x11); // maximum length
writeByte((byte) pair.getValue().precision);
writeByte((byte) pair.getValue().scale);
break;
case DOUBLE:
writeByte(TDSType.FLOATN.byteValue());
writeByte((byte) 8); // max length of datatype
break;
case FLOAT:
case REAL:
writeByte(TDSType.FLOATN.byteValue());
writeByte((byte) 4); // max length of datatype
break;
case DATE:
case TIME:
case TIMESTAMP:
case DATETIMEOFFSET:
case TIMESTAMP_WITH_TIMEZONE:
case TIME_WITH_TIMEZONE:
case CHAR:
case VARCHAR:
case NCHAR:
case NVARCHAR:
writeByte(TDSType.NVARCHAR.byteValue());
isShortValue = (2 * pair.getValue().precision) <= DataTypes.SHORT_VARTYPE_MAX_BYTES;
// Use PLP encoding on Yukon and later with long values
if (!isShortValue) // PLP
{
// Handle Yukon v*max type header here.
writeShort((short) 0xFFFF);
con.getDatabaseCollation().writeCollation(this);
}
else // non PLP
{
writeShort((short) DataTypes.SHORT_VARTYPE_MAX_BYTES);
con.getDatabaseCollation().writeCollation(this);
}
break;
case BINARY:
case VARBINARY:
writeByte(TDSType.BIGVARBINARY.byteValue());
isShortValue = pair.getValue().precision <= DataTypes.SHORT_VARTYPE_MAX_BYTES;
// Use PLP encoding on Yukon and later with long values
if (!isShortValue) // PLP
// Handle Yukon v*max type header here.
writeShort((short) 0xFFFF);
else // non PLP
writeShort((short) DataTypes.SHORT_VARTYPE_MAX_BYTES);
break;
default:
assert false : "Unexpected JDBC type " + jdbcType.toString();
}
// Column name - must be null (from TDS - TVP_COLMETADATA)
writeByte((byte) 0x00);
//[TVP_ORDER_UNIQUE]
//[TVP_COLUMN_ORDERING]
}
}
void writeTvpOrderUnique(TVP value) throws SQLServerException
{
/*
TVP_ORDER_UNIQUE = TVP_ORDER_UNIQUE_TOKEN (Count (ColNum OrderUniqueFlags))
*/
Map columnMetadata = value.getColumnMetadata();
Iterator> columnsIterator = columnMetadata.entrySet().iterator();
LinkedList columnList = new LinkedList();
while(columnsIterator.hasNext())
{
byte flags = 0;
Map.Entry pair = (Map.Entry)columnsIterator.next();
SQLServerMetaData metaData = pair.getValue();
if( SQLServerSortOrder.Ascending == metaData.sortOrder )
flags = TDS.TVP_ORDERASC_FLAG;
else if( SQLServerSortOrder.Descending == metaData.sortOrder )
flags = TDS.TVP_ORDERDESC_FLAG;
if(metaData.isUniqueKey)
flags |= TDS.TVP_UNIQUE_FLAG;
// Remember this column if any flags were set
if (0 != flags)
columnList.add(new TdsOrderUnique(pair.getKey(), flags));
}
// Write flagged columns
if(!columnList.isEmpty())
{
writeByte((byte) TDS.TVP_ORDER_UNIQUE_TOKEN);
writeShort((short)columnList.size());
for(TdsOrderUnique column: columnList)
{
writeShort((short) (column.columnOrdinal+1));
writeByte(column.flags);
}
}
}
private class TdsOrderUnique
{
int columnOrdinal;
byte flags;
TdsOrderUnique(int ordinal, byte flags)
{
this.columnOrdinal = ordinal;
this.flags = flags;
}
}
void setCryptoMetaData(CryptoMetadata cryptoMetaForBulk)
{
this.cryptoMeta = cryptoMetaForBulk;
}
CryptoMetadata getCryptoMetaData()
{
return cryptoMeta;
}
void writeEncryptedRPCByteArray(byte bValue[]) throws SQLServerException
{
boolean bValueNull = (bValue==null);
long nValueLen = bValueNull ? 0 : bValue.length;
boolean isShortValue = (nValueLen <= DataTypes.SHORT_VARTYPE_MAX_BYTES);
boolean isPLP = (!isShortValue) && (nValueLen <= DataTypes.MAX_VARTYPE_MAX_BYTES);
// Handle Shiloh types here.
if (isShortValue)
{
writeShort((short) DataTypes.SHORT_VARTYPE_MAX_BYTES);
}
else if (isPLP){
writeShort((short) DataTypes.SQL_USHORTVARMAXLEN);
}
else
{
writeInt(DataTypes.IMAGE_TEXT_MAX_BYTES);
}
// Data and length
if (bValueNull)
{
writeShort((short) -1); // actual len
}
else
{
if (isShortValue)
{
writeShort((short) nValueLen); // actual len
}
else if (isPLP)
{
writeLong((long) nValueLen); //actual length
}
else
{
writeInt((int)nValueLen); // actual len
}
// If length is zero, we're done.
if (0 != nValueLen){
if(isPLP){
writeInt((int)nValueLen);
}
writeBytes(bValue);
}
if(isPLP){
writeInt(0); //PLP_TERMINATOR, 0x00000000
}
}
}
void writeEncryptedRPCPLP() throws SQLServerException
{
writeShort((short) DataTypes.SQL_USHORTVARMAXLEN);
writeLong((long) 0); //actual length
writeInt(0); //PLP_TERMINATOR, 0x00000000
}
void writeCryptoMetaData() throws SQLServerException
{
writeByte((byte) cryptoMeta.cipherAlgorithmId);
writeByte((byte) cryptoMeta.encryptionType.getValue());
writeInt((int) cryptoMeta.cekTableEntry.getColumnEncryptionKeyValues().get(0).databaseId);
writeInt((int) cryptoMeta.cekTableEntry.getColumnEncryptionKeyValues().get(0).cekId);
writeInt((int) cryptoMeta.cekTableEntry.getColumnEncryptionKeyValues().get(0).cekVersion);
writeBytes(cryptoMeta.cekTableEntry.getColumnEncryptionKeyValues().get(0).cekMdVersion);
writeByte((byte) cryptoMeta.normalizationRuleVersion);
}
void writeRPCByteArray(
String sName,
byte bValue[],
boolean bOut,
JDBCType jdbcType,
SQLCollation collation) throws SQLServerException
{
boolean bValueNull = (bValue==null);
int nValueLen = bValueNull ? 0 : bValue.length;
boolean isShortValue = (nValueLen <= DataTypes.SHORT_VARTYPE_MAX_BYTES);
// Use PLP encoding on Yukon and later with long values and OUT parameters
boolean usePLP = (!isShortValue || bOut);
TDSType tdsType;
if(null != cryptoMeta)
{
// send encrypted data as BIGVARBINARY
tdsType = (isShortValue || usePLP) ? TDSType.BIGVARBINARY : TDSType.IMAGE;
collation = null;
}
else
switch (jdbcType)
{
case BINARY:
case VARBINARY:
case LONGVARBINARY:
case BLOB:
default:
tdsType = (isShortValue || usePLP) ? TDSType.BIGVARBINARY : TDSType.IMAGE;
collation = null;
break;
case CHAR:
case VARCHAR:
case LONGVARCHAR:
case CLOB:
tdsType = (isShortValue || usePLP) ? TDSType.BIGVARCHAR : TDSType.TEXT;
if (null == collation)
collation = con.getDatabaseCollation();
break;
case NCHAR:
case NVARCHAR:
case LONGNVARCHAR:
case NCLOB:
tdsType = (isShortValue || usePLP) ? TDSType.NVARCHAR : TDSType.NTEXT;
if (null == collation)
collation = con.getDatabaseCollation();
break;
}
writeRPCNameValType(sName, bOut, tdsType);
if (usePLP)
{
// Handle Yukon v*max type header here.
writeVMaxHeader( nValueLen, bValueNull, collation);
// Send the data.
if (!bValueNull)
{
if (nValueLen > 0)
{
writeInt(nValueLen);
writeBytes(bValue);
}
// Send the terminator PLP chunk.
writeInt(0);
}
}
else // non-PLP type
{
// Handle Shiloh types here.
if (isShortValue)
{
writeShort((short) DataTypes.SHORT_VARTYPE_MAX_BYTES);
}
else
{
writeInt(DataTypes.IMAGE_TEXT_MAX_BYTES);
}
if (null != collation)
collation.writeCollation(this);
// Data and length
if (bValueNull)
{
writeShort((short) -1); // actual len
}
else
{
if (isShortValue)
writeShort((short) nValueLen); // actual len
else
writeInt(nValueLen); // actual len
// If length is zero, we're done.
if (0 != nValueLen)
writeBytes(bValue);
}
}
}
/**
* Append a timestamp in RPC transmission format as a SQL Server DATETIME data type
* @param sName the optional parameter name
* @param cal Pure Gregorian calendar containing the timestamp, including its associated time zone
* @param subSecondNanos the sub-second nanoseconds (0 - 999,999,999)
* @param bOut boolean true if the data value is being registered as an ouput parameter
*
*/
void writeRPCDateTime(
String sName,
GregorianCalendar cal,
int subSecondNanos,
boolean bOut) throws SQLServerException
{
assert (subSecondNanos >= 0) && (subSecondNanos < Nanos.PER_SECOND):"Invalid subNanoSeconds value: " + subSecondNanos;
assert (cal != null) || (cal == null && subSecondNanos == 0):"Invalid subNanoSeconds value when calendar is null: " + subSecondNanos;
writeRPCNameValType(sName, bOut, TDSType.DATETIMEN);
writeByte((byte) 8); // max length of datatype
if (null == cal)
{
writeByte((byte) 0); // len of data bytes
return;
}
writeByte((byte) 8); // len of data bytes
// We need to extract the Calendar's current date & time in terms
// of the number of days since the SQL Base Date (1/1/1900) plus
// the number of milliseconds since midnight in the current day.
//
// We cannot rely on any pre-calculated value for the number of
// milliseconds in a day or the number of milliseconds since the
// base date to do this because days with DST changes are shorter
// or longer than "normal" days.
//
// ASSUMPTION: We assume we are dealing with a GregorianCalendar here.
// If not, we have no basis in which to compare dates. E.g. if we
// are dealing with a Chinese Calendar implementation which does not
// use the same value for Calendar.YEAR as the GregorianCalendar,
// we cannot meaningfully compute a value relative to 1/1/1900.
// First, figure out how many days there have been since the SQL Base Date.
//These are based on SQL Server algorithms
int daysSinceSQLBaseDate =
DDC.daysSinceBaseDate(
cal.get(Calendar.YEAR),
cal.get(Calendar.DAY_OF_YEAR),
TDS.BASE_YEAR_1900);
// Next, figure out the number of milliseconds since midnight of the current day.
int millisSinceMidnight =
(subSecondNanos + Nanos.PER_MILLISECOND / 2) / Nanos.PER_MILLISECOND + // Millis into the current second
1000 * cal.get(Calendar.SECOND) + // Seconds into the current minute
60 * 1000 * cal.get(Calendar.MINUTE) + // Minutes into the current hour
60 * 60 * 1000 * cal.get(Calendar.HOUR_OF_DAY); // Hours into the current day
// The last millisecond of the current day is always rounded to the first millisecond
// of the next day because DATETIME is only accurate to 1/300th of a second.
if (millisSinceMidnight >= 1000 * 60 * 60 * 24 - 1)
{
++daysSinceSQLBaseDate;
millisSinceMidnight = 0;
}
// Last-ditch verification that the value is in the valid range for the
// DATETIMEN TDS data type (1/1/1753 to 12/31/9999). If it's not, then
// throw an exception now so that statement execution is safely canceled.
// Attempting to put an invalid value on the wire would result in a TDS
// exception, which would close the connection.
//These are based on SQL Server algorithms
if (daysSinceSQLBaseDate < DDC.daysSinceBaseDate(1753, 1, TDS.BASE_YEAR_1900) ||
daysSinceSQLBaseDate >= DDC.daysSinceBaseDate(10000, 1, TDS.BASE_YEAR_1900))
{
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_valueOutOfRange"));
Object[] msgArgs = {SSType.DATETIME};
throw new SQLServerException(
form.format(msgArgs),
SQLState.DATA_EXCEPTION_DATETIME_FIELD_OVERFLOW,
DriverError.NOT_SET,
null);
}
// And put it all on the wire...
// Number of days since the SQL Server Base Date (January 1, 1900)
writeInt(daysSinceSQLBaseDate);
// Milliseconds since midnight (at a resolution of three hundredths of a second)
writeInt((3 * millisSinceMidnight + 5) / 10);
}
void writeRPCTime(
String sName,
GregorianCalendar localCalendar,
int subSecondNanos,
int scale,
boolean bOut) throws SQLServerException
{
writeRPCNameValType(sName, bOut, TDSType.TIMEN);
writeByte((byte) scale);
if (null == localCalendar)
{
writeByte((byte) 0);
return;
}
writeByte((byte) TDS.timeValueLength(scale));
writeScaledTemporal(
localCalendar,
subSecondNanos,
scale,
SSType.TIME);
}
void writeRPCDate(
String sName,
GregorianCalendar localCalendar,
boolean bOut) throws SQLServerException
{
writeRPCNameValType(sName, bOut, TDSType.DATEN);
if (null == localCalendar)
{
writeByte((byte) 0);
return;
}
writeByte((byte) TDS.DAYS_INTO_CE_LENGTH);
writeScaledTemporal(
localCalendar,
0, // subsecond nanos (none for a date value)
0, // scale (dates are not scaled)
SSType.DATE);
}
void writeEncryptedRPCTime(
String sName,
GregorianCalendar localCalendar,
int subSecondNanos,
int scale,
boolean bOut) throws SQLServerException
{
if (con.getSendTimeAsDatetime())
{
throw new SQLServerException(SQLServerException.getErrString("R_sendTimeAsDateTimeForAE"), null);
}
writeRPCNameValType(sName, bOut, TDSType.BIGVARBINARY);
if (null == localCalendar)
writeEncryptedRPCByteArray(null);
else
writeEncryptedRPCByteArray(
writeEncryptedScaledTemporal(
localCalendar,
subSecondNanos,
scale,
SSType.TIME,
(short) 0));
writeByte(TDSType.TIMEN.byteValue());
writeByte((byte) scale);
writeCryptoMetaData();
}
void writeEncryptedRPCDate(
String sName,
GregorianCalendar localCalendar,
boolean bOut) throws SQLServerException
{
writeRPCNameValType(sName, bOut, TDSType.BIGVARBINARY);
if (null == localCalendar)
writeEncryptedRPCByteArray(null);
else
writeEncryptedRPCByteArray
(writeEncryptedScaledTemporal(
localCalendar,
0, // subsecond nanos (none for a date value)
0, // scale (dates are not scaled)
SSType.DATE,
(short) 0));
writeByte(TDSType.DATEN.byteValue());
writeCryptoMetaData();
}
void writeEncryptedRPCDateTime(
String sName,
GregorianCalendar cal,
int subSecondNanos,
boolean bOut,
JDBCType jdbcType) throws SQLServerException
{
assert (subSecondNanos >= 0) && (subSecondNanos < Nanos.PER_SECOND):"Invalid subNanoSeconds value: " + subSecondNanos;
assert (cal != null) || (cal == null && subSecondNanos == 0):"Invalid subNanoSeconds value when calendar is null: " + subSecondNanos;
writeRPCNameValType(sName, bOut, TDSType.BIGVARBINARY);
if (null == cal)
writeEncryptedRPCByteArray(null);
else
writeEncryptedRPCByteArray(
getEncryptedDateTimeAsBytes(cal, subSecondNanos, jdbcType));
if(JDBCType.SMALLDATETIME == jdbcType)
{
writeByte(TDSType.DATETIMEN.byteValue());
writeByte((byte) 4);
}
else
{
writeByte(TDSType.DATETIMEN.byteValue());
writeByte((byte) 8);
}
writeCryptoMetaData();
}
// getEncryptedDateTimeAsBytes is called if jdbcType/ssType is SMALLDATETIME or DATETIME
byte[] getEncryptedDateTimeAsBytes(GregorianCalendar cal, int subSecondNanos, JDBCType jdbcType) throws SQLServerException
{
int daysSinceSQLBaseDate =
DDC.daysSinceBaseDate(
cal.get(Calendar.YEAR),
cal.get(Calendar.DAY_OF_YEAR),
TDS.BASE_YEAR_1900);
// Next, figure out the number of milliseconds since midnight of the current day.
int millisSinceMidnight =
(subSecondNanos + Nanos.PER_MILLISECOND / 2) / Nanos.PER_MILLISECOND + // Millis into the current second
1000 * cal.get(Calendar.SECOND) + // Seconds into the current minute
60 * 1000 * cal.get(Calendar.MINUTE) + // Minutes into the current hour
60 * 60 * 1000 * cal.get(Calendar.HOUR_OF_DAY); // Hours into the current day
// The last millisecond of the current day is always rounded to the first millisecond
// of the next day because DATETIME is only accurate to 1/300th of a second.
if (millisSinceMidnight >= 1000 * 60 * 60 * 24 - 1)
{
++daysSinceSQLBaseDate;
millisSinceMidnight = 0;
}
if(JDBCType.SMALLDATETIME == jdbcType)
{
int secondsSinceMidnight = (millisSinceMidnight/1000);
int minutesSinceMidnight = (secondsSinceMidnight/60);
// Values that are 29.998 seconds or less are rounded down to the nearest minute
minutesSinceMidnight = ((secondsSinceMidnight % 60) > 29.998) ? minutesSinceMidnight+1 : minutesSinceMidnight;
// minutesSinceMidnight for (23:59:30)
int maxMinutesSinceMidnight_SmallDateTime = 1440;
//Verification for smalldatetime to be within valid range of (1900.01.01) to (2079.06.06)
// smalldatetime for unencrypted does not allow insertion of 2079.06.06 23:59:59 and it is rounded up
// to 2079.06.07 00:00:00, therefore, we are checking minutesSinceMidnight for that condition. If it's not within valid range, then
// throw an exception now so that statement execution is safely canceled.
// 157 is the calculated day of year from 06-06 , 1440 is minutesince midnight for (23:59:30)
if ( (daysSinceSQLBaseDate < DDC.daysSinceBaseDate(1900, 1, TDS.BASE_YEAR_1900) || daysSinceSQLBaseDate > DDC.daysSinceBaseDate(2079, 157, TDS.BASE_YEAR_1900) )
|| (daysSinceSQLBaseDate == DDC.daysSinceBaseDate(2079, 157, TDS.BASE_YEAR_1900) && minutesSinceMidnight >= maxMinutesSinceMidnight_SmallDateTime ) )
{
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_valueOutOfRange"));
Object[] msgArgs = {SSType.SMALLDATETIME};
throw new SQLServerException(
form.format(msgArgs),
SQLState.DATA_EXCEPTION_DATETIME_FIELD_OVERFLOW,
DriverError.NOT_SET,
null);
}
ByteBuffer days = ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN);
days.putShort((short)daysSinceSQLBaseDate);
ByteBuffer seconds = ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN);
seconds.putShort((short)minutesSinceMidnight);
byte[] value = new byte[4];
System.arraycopy(days.array(), 0, value, 0, 2);
System.arraycopy(seconds.array(), 0, value, 2, 2);
return SQLServerSecurityUtility.encryptWithKey(value, cryptoMeta, con);
}
else if(JDBCType.DATETIME == jdbcType)
{
// Last-ditch verification that the value is in the valid range for the
// DATETIMEN TDS data type (1/1/1753 to 12/31/9999). If it's not, then
// throw an exception now so that statement execution is safely canceled.
// Attempting to put an invalid value on the wire would result in a TDS
// exception, which would close the connection.
//These are based on SQL Server algorithms
// And put it all on the wire...
if (daysSinceSQLBaseDate < DDC.daysSinceBaseDate(1753, 1, TDS.BASE_YEAR_1900) ||
daysSinceSQLBaseDate >= DDC.daysSinceBaseDate(10000, 1, TDS.BASE_YEAR_1900))
{
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_valueOutOfRange"));
Object[] msgArgs = {SSType.DATETIME};
throw new SQLServerException(
form.format(msgArgs),
SQLState.DATA_EXCEPTION_DATETIME_FIELD_OVERFLOW,
DriverError.NOT_SET,
null);
}
// Number of days since the SQL Server Base Date (January 1, 1900)
ByteBuffer days = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN);
days.putInt(daysSinceSQLBaseDate);
ByteBuffer seconds = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN);
seconds.putInt((3 * millisSinceMidnight + 5) / 10);
byte[] value = new byte[8];
System.arraycopy(days.array(), 0, value, 0, 4);
System.arraycopy(seconds.array(), 0, value, 4, 4);
return SQLServerSecurityUtility.encryptWithKey(value, cryptoMeta, con);
}
assert false : "Unexpected JDBCType type " + jdbcType;
return null;
}
void writeEncryptedRPCDateTime2(
String sName,
GregorianCalendar localCalendar,
int subSecondNanos,
int scale,
boolean bOut) throws SQLServerException
{
writeRPCNameValType(sName, bOut, TDSType.BIGVARBINARY);
if (null == localCalendar)
writeEncryptedRPCByteArray(null);
else
writeEncryptedRPCByteArray(
writeEncryptedScaledTemporal(
localCalendar,
subSecondNanos,
scale,
SSType.DATETIME2,
(short) 0));
writeByte(TDSType.DATETIME2N.byteValue());
writeByte((byte) (scale));
writeCryptoMetaData();
}
void writeEncryptedRPCDateTimeOffset(
String sName,
GregorianCalendar utcCalendar,
int minutesOffset,
int subSecondNanos,
int scale,
boolean bOut) throws SQLServerException
{
writeRPCNameValType(sName, bOut, TDSType.BIGVARBINARY);
if (null == utcCalendar)
writeEncryptedRPCByteArray(null);
else
{
assert 0 == utcCalendar.get(Calendar.ZONE_OFFSET);
writeEncryptedRPCByteArray(
writeEncryptedScaledTemporal(
utcCalendar,
subSecondNanos,
scale,
SSType.DATETIMEOFFSET,
(short) minutesOffset));
}
writeByte(TDSType.DATETIMEOFFSETN.byteValue());
writeByte((byte) (scale));
writeCryptoMetaData();
}
void writeRPCDateTime2(
String sName,
GregorianCalendar localCalendar,
int subSecondNanos,
int scale,
boolean bOut) throws SQLServerException
{
writeRPCNameValType(sName, bOut, TDSType.DATETIME2N);
writeByte((byte) scale);
if (null == localCalendar)
{
writeByte((byte) 0);
return;
}
writeByte((byte) TDS.datetime2ValueLength(scale));
writeScaledTemporal(
localCalendar,
subSecondNanos,
scale,
SSType.DATETIME2);
}
void writeRPCDateTimeOffset(
String sName,
GregorianCalendar utcCalendar,
int minutesOffset,
int subSecondNanos,
int scale,
boolean bOut) throws SQLServerException
{
writeRPCNameValType(sName, bOut, TDSType.DATETIMEOFFSETN);
writeByte((byte) scale);
if (null == utcCalendar)
{
writeByte((byte) 0);
return;
}
assert 0 == utcCalendar.get(Calendar.ZONE_OFFSET);
writeByte((byte) TDS.datetimeoffsetValueLength(scale));
writeScaledTemporal(
utcCalendar,
subSecondNanos,
scale,
SSType.DATETIMEOFFSET);
writeShort((short) minutesOffset);
}
/**
* Returns subSecondNanos rounded to the maximum precision supported.
* The maximum fractional scale is MAX_FRACTIONAL_SECONDS_SCALE(7).
* Eg1: if you pass 456,790,123 the function would return 456,790,100
* Eg2: if you pass 456,790,150 the function would return 456,790,200
* Eg3: if you pass 999,999,951 the function would return 1,000,000,000
* This is done to ensure that we have consistent rounding behaviour in setters and getters. Bug #507919
*/
private int getRoundedSubSecondNanos(int subSecondNanos)
{
int roundedNanos = ((subSecondNanos + (Nanos.PER_MAX_SCALE_INTERVAL/2))/Nanos.PER_MAX_SCALE_INTERVAL)*Nanos.PER_MAX_SCALE_INTERVAL;
return roundedNanos;
}
/**
* Writes to the TDS channel a temporal value as an instance instance of one of
* the scaled temporal SQL types: DATE, TIME, DATETIME2, or DATETIMEOFFSET.
*
* @param cal Calendar representing the value to write, except for any sub-second nanoseconds
* @param subSecondNanos the sub-second nanoseconds (0 - 999,999,999)
* @param scale the scale (in digits: 0 - 7) to use for the sub-second nanos component
* @param ssType the SQL Server data type (DATE, TIME, DATETIME2, or DATETIMEOFFSET)
*
* @throws SQLServerException if an I/O error occurs or if the value is not in the valid range
*/
private void writeScaledTemporal(
GregorianCalendar cal,
int subSecondNanos,
int scale,
SSType ssType) throws SQLServerException
{
assert con.isKatmaiOrLater();
assert
SSType.DATE == ssType ||
SSType.TIME == ssType ||
SSType.DATETIME2 == ssType ||
SSType.DATETIMEOFFSET == ssType :
"Unexpected SSType: " + ssType;
// First, for types with a time component, write the scaled nanos since midnight
if (SSType.TIME == ssType ||
SSType.DATETIME2 == ssType ||
SSType.DATETIMEOFFSET == ssType)
{
assert subSecondNanos >= 0;
assert subSecondNanos < Nanos.PER_SECOND;
assert scale >= 0;
assert scale <= TDS.MAX_FRACTIONAL_SECONDS_SCALE;
int secondsSinceMidnight =
cal.get(Calendar.SECOND) +
60 * cal.get(Calendar.MINUTE) +
60 * 60 * cal.get(Calendar.HOUR_OF_DAY);
// Scale nanos since midnight to the desired scale, rounding the value as necessary
long divisor = Nanos.PER_MAX_SCALE_INTERVAL * (long) Math.pow(10, TDS.MAX_FRACTIONAL_SECONDS_SCALE - scale);
//The scaledNanos variable represents the fractional seconds of the value at the scale
//indicated by the scale variable. So, for example, scaledNanos = 3 means 300 nanoseconds
//at scale TDS.MAX_FRACTIONAL_SECONDS_SCALE, but 3000 nanoseconds at
//TDS.MAX_FRACTIONAL_SECONDS_SCALE - 1
long scaledNanos =
((long) Nanos.PER_SECOND * secondsSinceMidnight + getRoundedSubSecondNanos(subSecondNanos) + divisor/2) / divisor;
//SQL Server rounding behavior indicates that it always rounds up unless
//we are at the max value of the type(NOT every day), in which case it truncates.
// Side effect on Calendar date:
// If rounding nanos to the specified scale rolls the value to the next day ...
if (Nanos.PER_DAY / divisor == scaledNanos)
{
//If the type is time, always truncate
if(SSType.TIME == ssType)
{
--scaledNanos;
}
//If the type is datetime2 or datetimeoffset, truncate only if its the max value supported
else
{
assert SSType.DATETIME2 == ssType || SSType.DATETIMEOFFSET == ssType:
"Unexpected SSType: " + ssType;
// ... then bump the date, provided that the resulting date is still within
// the valid date range.
//
// Extreme edge case (literally, the VERY edge...):
// If nanos overflow rolls the date value out of range (that is, we have a value
// a few nanoseconds later than 9999-12-31 23:59:59) then truncate the nanos
// instead of rolling.
//
// This case is very likely never hit by "real world" applications, but exists
// here as a security measure to ensure that such values don't result in a
// connection-closing TDS exception.
cal.add(Calendar.SECOND, 1);
if (cal.get(Calendar.YEAR) <= 9999)
{
scaledNanos = 0;
}
else
{
cal.add(Calendar.SECOND, -1);
--scaledNanos;
}
}
}
// Encode the scaled nanos to TDS
int encodedLength = TDS.nanosSinceMidnightLength(scale);
byte[] encodedBytes = scaledNanosToEncodedBytes(scaledNanos, encodedLength);
writeBytes(encodedBytes);
}
// Second, for types with a date component, write the days into the Common Era
if (SSType.DATE == ssType ||
SSType.DATETIME2 == ssType ||
SSType.DATETIMEOFFSET == ssType)
{
// Computation of the number of days into the Common Era assumes that
// the DAY_OF_YEAR field reflects a pure Gregorian calendar - one that
// uses Gregorian leap year rules across the entire range of dates.
//
// For the DAY_OF_YEAR field to accurately reflect pure Gregorian behavior,
// we need to use a pure Gregorian calendar for dates that are Julian dates
// under a standard Gregorian calendar and for (Gregorian) dates later than
// the cutover date in the cutover year.
if (cal.getTimeInMillis() < GregorianChange.STANDARD_CHANGE_DATE.getTime() ||
cal.getActualMaximum(Calendar.DAY_OF_YEAR) < TDS.DAYS_PER_YEAR)
{
int year = cal.get(Calendar.YEAR);
int month = cal.get(Calendar.MONTH);
int date = cal.get(Calendar.DATE);
// Set the cutover as early as possible (pure Gregorian behavior)
cal.setGregorianChange(GregorianChange.PURE_CHANGE_DATE);
// Initialize the date field by field (preserving the "wall calendar" value)
cal.set(year, month, date);
}
int daysIntoCE =
DDC.daysSinceBaseDate(
cal.get(Calendar.YEAR),
cal.get(Calendar.DAY_OF_YEAR),
1);
// Last-ditch verification that the value is in the valid range for the
// DATE/DATETIME2/DATETIMEOFFSET TDS data type (1/1/0001 to 12/31/9999).
// If it's not, then throw an exception now so that statement execution
// is safely canceled. Attempting to put an invalid value on the wire
// would result in a TDS exception, which would close the connection.
if (daysIntoCE < 0 || daysIntoCE >= DDC.daysSinceBaseDate(10000, 1, 1))
{
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_valueOutOfRange"));
Object[] msgArgs = {ssType};
throw new SQLServerException(
form.format(msgArgs),
SQLState.DATA_EXCEPTION_DATETIME_FIELD_OVERFLOW,
DriverError.NOT_SET,
null);
}
byte encodedBytes[] = new byte[3];
encodedBytes[0] = (byte)((daysIntoCE >> 0) & 0xFF);
encodedBytes[1] = (byte)((daysIntoCE >> 8) & 0xFF);
encodedBytes[2] = (byte)((daysIntoCE >> 16) & 0xFF);
writeBytes(encodedBytes);
}
}
/**
* Writes to the TDS channel a temporal value as an instance instance of one of
* the scaled temporal SQL types: DATE, TIME, DATETIME2, or DATETIMEOFFSET.
*
* @param cal Calendar representing the value to write, except for any sub-second nanoseconds
* @param subSecondNanos the sub-second nanoseconds (0 - 999,999,999)
* @param scale the scale (in digits: 0 - 7) to use for the sub-second nanos component
* @param ssType the SQL Server data type (DATE, TIME, DATETIME2, or DATETIMEOFFSET)
* @param minutesOffset the offset value for DATETIMEOFFSET
* @throws SQLServerException if an I/O error occurs or if the value is not in the valid range
*/
byte[] writeEncryptedScaledTemporal(
GregorianCalendar cal,
int subSecondNanos,
int scale,
SSType ssType,
short minutesOffset) throws SQLServerException
{
assert con.isKatmaiOrLater();
assert
SSType.DATE == ssType ||
SSType.TIME == ssType ||
SSType.DATETIME2 == ssType ||
SSType.DATETIMEOFFSET == ssType :
"Unexpected SSType: " + ssType;
// store the time and minutesOffset portion of DATETIME2 and DATETIMEOFFSET to be used with date portion
byte encodedBytesForEncryption[] = null;
int secondsSinceMidnight = 0;
long divisor = 0;
long scaledNanos = 0;
// First, for types with a time component, write the scaled nanos since midnight
if (SSType.TIME == ssType ||
SSType.DATETIME2 == ssType ||
SSType.DATETIMEOFFSET == ssType)
{
assert subSecondNanos >= 0;
assert subSecondNanos < Nanos.PER_SECOND;
assert scale >= 0;
assert scale <= TDS.MAX_FRACTIONAL_SECONDS_SCALE;
secondsSinceMidnight =
cal.get(Calendar.SECOND) +
60 * cal.get(Calendar.MINUTE) +
60 * 60 * cal.get(Calendar.HOUR_OF_DAY);
// Scale nanos since midnight to the desired scale, rounding the value as necessary
divisor = Nanos.PER_MAX_SCALE_INTERVAL * (long) Math.pow(10, TDS.MAX_FRACTIONAL_SECONDS_SCALE - scale);
//The scaledNanos variable represents the fractional seconds of the value at the scale
//indicated by the scale variable. So, for example, scaledNanos = 3 means 300 nanoseconds
//at scale TDS.MAX_FRACTIONAL_SECONDS_SCALE, but 3000 nanoseconds at
//TDS.MAX_FRACTIONAL_SECONDS_SCALE - 1
scaledNanos =
(((long) Nanos.PER_SECOND * secondsSinceMidnight + getRoundedSubSecondNanos(subSecondNanos) + divisor/2) / divisor) * divisor/100;
//for encrypted time value, SQL server cannot do rounding or casting,
//So, driver needs to cast it before encryption.
if(SSType.TIME == ssType && 864000000000L <= scaledNanos){
scaledNanos =
(((long) Nanos.PER_SECOND * secondsSinceMidnight + getRoundedSubSecondNanos(subSecondNanos)) / divisor) * divisor/100;
}
//SQL Server rounding behavior indicates that it always rounds up unless
//we are at the max value of the type(NOT every day), in which case it truncates.
// Side effect on Calendar date:
// If rounding nanos to the specified scale rolls the value to the next day ...
if (Nanos.PER_DAY / divisor == scaledNanos)
{
//If the type is time, always truncate
if(SSType.TIME == ssType)
{
--scaledNanos;
}
//If the type is datetime2 or datetimeoffset, truncate only if its the max value supported
else
{
assert SSType.DATETIME2 == ssType || SSType.DATETIMEOFFSET == ssType:
"Unexpected SSType: " + ssType;
// ... then bump the date, provided that the resulting date is still within
// the valid date range.
//
// Extreme edge case (literally, the VERY edge...):
// If nanos overflow rolls the date value out of range (that is, we have a value
// a few nanoseconds later than 9999-12-31 23:59:59) then truncate the nanos
// instead of rolling.
//
// This case is very likely never hit by "real world" applications, but exists
// here as a security measure to ensure that such values don't result in a
// connection-closing TDS exception.
cal.add(Calendar.SECOND, 1);
if (cal.get(Calendar.YEAR) <= 9999)
{
scaledNanos = 0;
}
else
{
cal.add(Calendar.SECOND, -1);
--scaledNanos;
}
}
}
// Encode the scaled nanos to TDS
int encodedLength = TDS.nanosSinceMidnightLength(TDS.MAX_FRACTIONAL_SECONDS_SCALE);
byte[] encodedBytes = scaledNanosToEncodedBytes(scaledNanos, encodedLength);
if(SSType.TIME == ssType)
{
byte [] cipherText = SQLServerSecurityUtility.encryptWithKey(encodedBytes, cryptoMeta, con);
return cipherText;
}
else if(SSType.DATETIME2 == ssType)
{
// for DATETIME2 sends both date and time part together for encryption
encodedBytesForEncryption = new byte[encodedLength + 3];
System.arraycopy(encodedBytes, 0, encodedBytesForEncryption, 0, encodedBytes.length);
}
else if(SSType.DATETIMEOFFSET == ssType)
{
// for DATETIMEOFFSET sends date, time and offset part together for encryption
encodedBytesForEncryption = new byte[encodedLength + 5];
System.arraycopy(encodedBytes, 0, encodedBytesForEncryption, 0, encodedBytes.length);
}
}
// Second, for types with a date component, write the days into the Common Era
if (SSType.DATE == ssType ||
SSType.DATETIME2 == ssType ||
SSType.DATETIMEOFFSET == ssType)
{
// Computation of the number of days into the Common Era assumes that
// the DAY_OF_YEAR field reflects a pure Gregorian calendar - one that
// uses Gregorian leap year rules across the entire range of dates.
//
// For the DAY_OF_YEAR field to accurately reflect pure Gregorian behavior,
// we need to use a pure Gregorian calendar for dates that are Julian dates
// under a standard Gregorian calendar and for (Gregorian) dates later than
// the cutover date in the cutover year.
if (cal.getTimeInMillis() < GregorianChange.STANDARD_CHANGE_DATE.getTime() ||
cal.getActualMaximum(Calendar.DAY_OF_YEAR) < TDS.DAYS_PER_YEAR)
{
int year = cal.get(Calendar.YEAR);
int month = cal.get(Calendar.MONTH);
int date = cal.get(Calendar.DATE);
// Set the cutover as early as possible (pure Gregorian behavior)
cal.setGregorianChange(GregorianChange.PURE_CHANGE_DATE);
// Initialize the date field by field (preserving the "wall calendar" value)
cal.set(year, month, date);
}
int daysIntoCE =
DDC.daysSinceBaseDate(
cal.get(Calendar.YEAR),
cal.get(Calendar.DAY_OF_YEAR),
1);
// Last-ditch verification that the value is in the valid range for the
// DATE/DATETIME2/DATETIMEOFFSET TDS data type (1/1/0001 to 12/31/9999).
// If it's not, then throw an exception now so that statement execution
// is safely canceled. Attempting to put an invalid value on the wire
// would result in a TDS exception, which would close the connection.
if (daysIntoCE < 0 || daysIntoCE >= DDC.daysSinceBaseDate(10000, 1, 1))
{
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_valueOutOfRange"));
Object[] msgArgs = {ssType};
throw new SQLServerException(
form.format(msgArgs),
SQLState.DATA_EXCEPTION_DATETIME_FIELD_OVERFLOW,
DriverError.NOT_SET,
null);
}
byte encodedBytes[] = new byte[3];
encodedBytes[0] = (byte)((daysIntoCE >> 0) & 0xFF);
encodedBytes[1] = (byte)((daysIntoCE >> 8) & 0xFF);
encodedBytes[2] = (byte)((daysIntoCE >> 16) & 0xFF);
byte [] cipherText;
if(SSType.DATE == ssType)
{
cipherText = SQLServerSecurityUtility.encryptWithKey(encodedBytes, cryptoMeta, con);
}
else if(SSType.DATETIME2 == ssType)
{
//for Max value, does not round up, do casting instead.
if(3652058 == daysIntoCE){ //9999-12-31
if(864000000000L == scaledNanos){ //24:00:00 in nanoseconds
//does not round up
scaledNanos =
(((long) Nanos.PER_SECOND * secondsSinceMidnight + getRoundedSubSecondNanos(subSecondNanos)) / divisor) * divisor/100;
int encodedLength = TDS.nanosSinceMidnightLength(TDS.MAX_FRACTIONAL_SECONDS_SCALE);
byte[] encodedNanoBytes = scaledNanosToEncodedBytes(scaledNanos, encodedLength);
// for DATETIME2 sends both date and time part together for encryption
encodedBytesForEncryption = new byte[encodedLength + 3];
System.arraycopy(encodedNanoBytes, 0, encodedBytesForEncryption, 0, encodedNanoBytes.length);
}
}
// Copy the 3 byte date value
System.arraycopy(encodedBytes, 0, encodedBytesForEncryption, (encodedBytesForEncryption.length - 3), 3);
cipherText = SQLServerSecurityUtility.encryptWithKey(encodedBytesForEncryption, cryptoMeta, con);
}
else
{
//for Max value, does not round up, do casting instead.
if(3652058 == daysIntoCE){ //9999-12-31
if(864000000000L == scaledNanos){ //24:00:00 in nanoseconds
//does not round up
scaledNanos =
(((long) Nanos.PER_SECOND * secondsSinceMidnight + getRoundedSubSecondNanos(subSecondNanos)) / divisor) * divisor/100;
int encodedLength = TDS.nanosSinceMidnightLength(TDS.MAX_FRACTIONAL_SECONDS_SCALE);
byte[] encodedNanoBytes = scaledNanosToEncodedBytes(scaledNanos, encodedLength);
// for DATETIMEOFFSET sends date, time and offset part together for encryption
encodedBytesForEncryption = new byte[encodedLength + 5];
System.arraycopy(encodedNanoBytes, 0, encodedBytesForEncryption, 0, encodedNanoBytes.length);
}
}
// Copy the 3 byte date value
System.arraycopy(encodedBytes, 0, encodedBytesForEncryption, (encodedBytesForEncryption.length - 5), 3);
// Copy the 2 byte minutesOffset value
System.arraycopy(
ByteBuffer.allocate(Short.SIZE / Byte.SIZE).order(ByteOrder.LITTLE_ENDIAN).putShort(minutesOffset).array(),
0,
encodedBytesForEncryption,
(encodedBytesForEncryption.length - 2),
2);
cipherText = SQLServerSecurityUtility.encryptWithKey(encodedBytesForEncryption, cryptoMeta, con);
}
return cipherText;
}
// Invalid type ssType. This condition should never happen.
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_unknownSSType"));
Object[] msgArgs = {ssType};
SQLServerException.makeFromDriverError(null, null, form.format(msgArgs), null, true);
return null;
}
private byte[] scaledNanosToEncodedBytes(long scaledNanos, int encodedLength) {
byte encodedBytes[] = new byte[encodedLength];
for (int i = 0; i < encodedLength; i++)
encodedBytes[i] = (byte)((scaledNanos >> (8 * i)) & 0xFF);
return encodedBytes;
}
/**
* Append the data in a stream in RPC transmission format.
* @param sName the optional parameter name
* @param stream is the stream
* @param streamLength length of the stream (may be unknown)
* @param bOut boolean true if the data value is being registered as an ouput parameter
* @param jdbcType The JDBC type used to determine whether the value is textual or non-textual.
* @param collation The SQL collation associated with the value. Null for non-textual SQL Server types.
* @throws SQLServerException
*/
void writeRPCInputStream(
String sName,
InputStream stream,
long streamLength,
boolean bOut,
JDBCType jdbcType,
SQLCollation collation) throws SQLServerException
{
assert null != stream;
assert DataTypes.UNKNOWN_STREAM_LENGTH == streamLength || streamLength >= 0;
// Send long values and values with unknown length
// using PLP chunking on Yukon and later.
boolean usePLP = (DataTypes.UNKNOWN_STREAM_LENGTH == streamLength || streamLength > DataTypes.SHORT_VARTYPE_MAX_BYTES);
if (usePLP)
{
assert DataTypes.UNKNOWN_STREAM_LENGTH == streamLength || streamLength <= DataTypes.MAX_VARTYPE_MAX_BYTES;
writeRPCNameValType(
sName,
bOut,
jdbcType.isTextual() ?
TDSType.BIGVARCHAR :
TDSType.BIGVARBINARY);
// Handle Yukon v*max type header here.
writeVMaxHeader(
streamLength,
false,
jdbcType.isTextual() ? collation : null);
}
// Send non-PLP in all other cases
else
{
// If the length of the InputStream is unknown then we need to buffer the entire stream
// in memory so that we can determine its length and send that length to the server
// before the stream data itself.
if (DataTypes.UNKNOWN_STREAM_LENGTH == streamLength)
{
// Create ByteArrayOutputStream with initial buffer size of 8K to handle typical
// binary field sizes more efficiently. Note we can grow beyond 8000 bytes.
ByteArrayOutputStream baos = new ByteArrayOutputStream(8000);
streamLength = 0L;
// Since Shiloh is limited to 64K TDS packets, that's a good upper bound on the maximum
// length of InputStream we should try to handle before throwing an exception.
long maxStreamLength = 65535L * con.getTDSPacketSize();
try
{
byte buff[] = new byte[8000];
int bytesRead;
while (streamLength < maxStreamLength && -1 != (bytesRead = stream.read(buff, 0, buff.length)))
{
baos.write(buff);
streamLength += bytesRead;
}
}
catch (IOException e)
{
throw new SQLServerException(
e.getMessage(),
SQLState.DATA_EXCEPTION_NOT_SPECIFIC,
DriverError.NOT_SET,
e);
}
if (streamLength >= maxStreamLength)
{
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidLength"));
Object[] msgArgs = {Long.valueOf(streamLength)};
SQLServerException.makeFromDriverError(null, null, form.format(msgArgs), "", true);
}
assert streamLength <= Integer.MAX_VALUE;
stream = new ByteArrayInputStream(baos.toByteArray(), 0, (int) streamLength);
}
assert 0 <= streamLength && streamLength <= DataTypes.IMAGE_TEXT_MAX_BYTES;
boolean useVarType = streamLength <= DataTypes.SHORT_VARTYPE_MAX_BYTES;
writeRPCNameValType(
sName,
bOut,
jdbcType.isTextual() ?
(useVarType ? TDSType.BIGVARCHAR : TDSType.TEXT) :
(useVarType ? TDSType.BIGVARBINARY : TDSType.IMAGE));
// Write maximum length, optional collation, and actual length
if (useVarType)
{
writeShort((short) DataTypes.SHORT_VARTYPE_MAX_BYTES);
if (jdbcType.isTextual())
collation.writeCollation(this);
writeShort((short) streamLength);
}
else
{
writeInt(DataTypes.IMAGE_TEXT_MAX_BYTES);
if (jdbcType.isTextual())
collation.writeCollation(this);
writeInt((int) streamLength);
}
}
// Write the data
writeStream(stream, streamLength, usePLP);
}
/**
* Append the XML data in a stream in RPC transmission format.
* @param sName the optional parameter name
* @param stream is the stream
* @param streamLength length of the stream (may be unknown)
* @param bOut boolean true if the data value is being registered as an ouput parameter
* @throws SQLServerException
*/
void writeRPCXML(
String sName,
InputStream stream,
long streamLength,
boolean bOut
) throws SQLServerException
{
assert DataTypes.UNKNOWN_STREAM_LENGTH == streamLength || streamLength >= 0;
assert DataTypes.UNKNOWN_STREAM_LENGTH == streamLength || streamLength <= DataTypes.MAX_VARTYPE_MAX_BYTES;
writeRPCNameValType(
sName,
bOut,
TDSType.XML);
writeByte((byte) 0); // No schema
// Handle null here and return, we're done here if it's null.
if (null ==stream)
{
// Null header for v*max types is 0xFFFFFFFFFFFFFFFF.
writeLong(0xFFFFFFFFFFFFFFFFL);
}
else if (DataTypes.UNKNOWN_STREAM_LENGTH == streamLength)
{
// Append v*max length.
// UNKNOWN_PLP_LEN is 0xFFFFFFFFFFFFFFFE
writeLong(0xFFFFFFFFFFFFFFFEL);
// NOTE: Don't send the first chunk length, this will be calculated by caller.
}
else
{
// For v*max types with known length, length is
// We're sending same total length as chunk length (as we're sending 1 chunk).
writeLong(streamLength);
}
if(null != stream)
// Write the data
writeStream(stream, streamLength, true);
}
/**
* Append the data in a character reader in RPC transmission format.
* @param sName the optional parameter name
* @param re the reader
* @param reLength the reader data length (in characters)
* @param bOut boolean true if the data value is being registered as an ouput parameter
* @param collation The SQL collation associated with the value. Null for non-textual SQL Server types.
* @throws SQLServerException
*/
void writeRPCReaderUnicode(
String sName,
Reader re,
long reLength,
boolean bOut,
SQLCollation collation) throws SQLServerException
{
assert null != re;
assert DataTypes.UNKNOWN_STREAM_LENGTH == reLength || reLength >= 0;
// Textual RPC requires a collation. If none is provided, as is the case when
// the SSType is non-textual, then use the database collation by default.
if (null == collation)
collation = con.getDatabaseCollation();
// Send long values and values with unknown length
// using PLP chunking on Yukon and later.
boolean usePLP = (DataTypes.UNKNOWN_STREAM_LENGTH == reLength || reLength > DataTypes.SHORT_VARTYPE_MAX_CHARS);
if (usePLP)
{
assert DataTypes.UNKNOWN_STREAM_LENGTH == reLength || reLength <= DataTypes.MAX_VARTYPE_MAX_CHARS;
writeRPCNameValType(
sName,
bOut,
TDSType.NVARCHAR);
// Handle Yukon v*max type header here.
writeVMaxHeader(
(DataTypes.UNKNOWN_STREAM_LENGTH == reLength) ? DataTypes.UNKNOWN_STREAM_LENGTH : 2 * reLength, // Length (in bytes)
false,
collation);
}
// Send non-PLP in all other cases
else
{
// Length must be known if we're not sending PLP-chunked data. Yukon is handled above.
// For Shiloh, this is enforced in DTV by converting the Reader to some other length-
// prefixed value in the setter.
assert 0 <= reLength && reLength <= DataTypes.NTEXT_MAX_CHARS;
// For non-PLP types, use the long TEXT type rather than the short VARCHAR
// type if the stream is too long to fit in the latter or if we don't know the length up
// front so we have to assume that it might be too long.
boolean useVarType = reLength <= DataTypes.SHORT_VARTYPE_MAX_CHARS;
writeRPCNameValType(
sName,
bOut,
useVarType ? TDSType.NVARCHAR : TDSType.NTEXT);
// Write maximum length, collation, and actual length of the data
if (useVarType)
{
writeShort((short) DataTypes.SHORT_VARTYPE_MAX_BYTES);
collation.writeCollation(this);
writeShort((short) (2 * reLength));
}
else
{
writeInt(DataTypes.NTEXT_MAX_CHARS);
collation.writeCollation(this);
writeInt((int) (2 * reLength));
}
}
// Write the data
writeReader(re, reLength, usePLP);
}
}
/**
* TDSPacket provides a mechanism for chaining TDS response packets
* together in a singly-linked list.
*
* Having both the link and the data in the same class allows TDSReader
* marks (see below) to automatically hold onto exactly as much response
* data as they need, and no more. Java reference semantics ensure that
* a mark holds onto its referenced packet and subsequent packets (through
* next references). When all marked references to a packet go away,
* the packet, and any linked unmarked packets, can be reclaimed by GC.
*/
final class TDSPacket
{
final byte[] header = new byte[TDS.PACKET_HEADER_SIZE];
final byte[] payload;
int payloadLength;
volatile TDSPacket next;
final public String toString()
{
return "TDSPacket(SPID:" + Util.readUnsignedShortBigEndian(header, TDS.PACKET_HEADER_SPID) +
" Seq:" + header[TDS.PACKET_HEADER_SEQUENCE_NUM] + ")";
}
TDSPacket(int size)
{
payload = new byte[size];
payloadLength = 0;
next = null;
}
final boolean isEOM() { return TDS.STATUS_BIT_EOM == (header[TDS.PACKET_HEADER_MESSAGE_STATUS] & TDS.STATUS_BIT_EOM); }
};
/**
* TDSReaderMark encapsulates a fixed position in the response data stream.
*
* Response data is quantized into a linked chain of packets. A mark refers
* to a specific location in a specific packet and relies on Java's reference
* semantics to automatically keep all subsequent packets accessible until
* the mark is destroyed.
*/
final class TDSReaderMark
{
final TDSPacket packet;
final int payloadOffset;
TDSReaderMark(TDSPacket packet, int payloadOffset)
{
this.packet = packet;
this.payloadOffset = payloadOffset;
}
}
/**
* TDSReader encapsulates the TDS response data stream.
*
* Bytes are read from SQL Server into a FIFO of packets.
* Reader methods traverse the packets to access the data.
*/
final class TDSReader
{
private final static Logger logger = Logger.getLogger("com.microsoft.sqlserver.jdbc.internals.TDS.Reader");
final private String traceID;
final public String toString() { return traceID; }
private final TDSChannel tdsChannel;
private final SQLServerConnection con;
private final TDSCommand command;
final TDSCommand getCommand() { assert null != command; return command; }
final SQLServerConnection getConnection() { return con; }
private TDSPacket currentPacket = new TDSPacket(0);
private TDSPacket lastPacket = currentPacket;
private int payloadOffset = 0;
private int packetNum = 0;
private boolean isStreaming = true;
private boolean useColumnEncryption = false;
private boolean serverSupportsColumnEncryption = false;
private final byte valueBytes[] = new byte[256];
private static int lastReaderID = 0;
private synchronized static int nextReaderID() { return ++lastReaderID; }
TDSReader(TDSChannel tdsChannel, SQLServerConnection con, TDSCommand command)
{
this.tdsChannel = tdsChannel;
this.con = con;
this.command = command; // may be null
// if the logging level is not detailed than fine or more we will not have proper readerids.
if (logger.isLoggable(Level.FINE))
traceID = "TDSReader@" + nextReaderID()+ " (" + con.toString() + ")";
else
traceID = con.toString();
if (con.isColumnEncryptionSettingEnabled())
{
useColumnEncryption = true;
}
serverSupportsColumnEncryption = con.getServerSupportsColumnEncryption();
}
final boolean isColumnEncryptionSettingEnabled()
{
return useColumnEncryption;
}
final boolean getServerSupportsColumnEncryption()
{
return serverSupportsColumnEncryption;
}
final void throwInvalidTDS() throws SQLServerException
{
if(logger.isLoggable(Level.SEVERE))
logger.severe(toString() + " got unexpected value in TDS response at offset:" + payloadOffset);
con.throwInvalidTDS();
}
final void throwInvalidTDSToken(String tokenName) throws SQLServerException
{
if(logger.isLoggable(Level.SEVERE))
logger.severe(toString() + " got unexpected value in TDS response at offset:" + payloadOffset);
con.throwInvalidTDSToken(tokenName);
}
/**
* Ensures that payload data is available to be read, automatically advancing to
* (and possibly reading) the next packet.
*
* @return true if additional data is available to be read
* false if no more data is available
*/
private final boolean ensurePayload() throws SQLServerException
{
if (payloadOffset == currentPacket.payloadLength)
if (!nextPacket()) return false;
assert payloadOffset < currentPacket.payloadLength;
return true;
}
/**
* Advance (and possibly read) the next packet.
*
* @return true if additional data is available to be read
* false if no more data is available
*/
private final boolean nextPacket() throws SQLServerException
{
assert null != currentPacket;
// Shouldn't call this function unless we're at the end of the current packet...
TDSPacket consumedPacket = currentPacket;
assert payloadOffset == consumedPacket.payloadLength;
// If no buffered packets are left then maybe we can read one...
// This action must be synchronized against against another thread calling
// readAllPackets() to read in ALL of the remaining packets of the current response.
if (null == consumedPacket.next)
{
readPacket();
if (null == consumedPacket.next)
return false;
}
// Advance to that packet. If we are streaming through the
// response, then unlink the current packet from the next
// before moving to allow the packet to be reclaimed.
TDSPacket nextPacket = consumedPacket.next;
if (isStreaming)
{
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Moving to next packet -- unlinking consumed packet");
consumedPacket.next = null;
}
currentPacket = nextPacket;
payloadOffset = 0;
return true;
}
/**
* Reads the next packet of the TDS channel.
*
* This method is synchronized to guard against simultaneously reading packets
* from one thread that is processing the response and another thread that is
* trying to buffer it with TDSCommand.detach().
*/
synchronized final boolean readPacket() throws SQLServerException
{
if (null != command && !command.readingResponse())
return false;
// Number of packets in should always be less than number of packets out.
// If the server has been notified for an interrupt, it may be less by
// more than one packet.
assert
tdsChannel.numMsgsRcvd < tdsChannel.numMsgsSent :
"numMsgsRcvd:" + tdsChannel.numMsgsRcvd +
" should be less than numMsgsSent:" + tdsChannel.numMsgsSent;
TDSPacket newPacket = new TDSPacket(con.getTDSPacketSize());
// First, read the packet header.
for (int headerBytesRead = 0; headerBytesRead < TDS.PACKET_HEADER_SIZE;)
{
int bytesRead = tdsChannel.read(newPacket.header, headerBytesRead, TDS.PACKET_HEADER_SIZE - headerBytesRead);
if (bytesRead < 0)
{
if (logger.isLoggable(Level.FINER))
logger.finer(toString() + " Premature EOS in response. packetNum:" + packetNum + " headerBytesRead:" + headerBytesRead);
con.terminate(
SQLServerException.DRIVER_ERROR_IO_FAILED,
((0 == packetNum && 0 == headerBytesRead) ?
SQLServerException.getErrString("R_noServerResponse") :
SQLServerException.getErrString("R_truncatedServerResponse")));
}
headerBytesRead += bytesRead;
}
// Header size is a 2 byte unsigned short integer in big-endian order.
int packetLength = Util.readUnsignedShortBigEndian(newPacket.header, TDS.PACKET_HEADER_MESSAGE_LENGTH);
// Make header size is properly bounded and compute length of the packet payload.
if (packetLength < TDS.PACKET_HEADER_SIZE || packetLength > con.getTDSPacketSize())
{
logger.warning(toString() + " TDS header contained invalid packet length:" + packetLength + "; packet size:" + con.getTDSPacketSize());
throwInvalidTDS();
}
newPacket.payloadLength = packetLength - TDS.PACKET_HEADER_SIZE;
// Just grab the SPID for logging (another big-endian unsigned short).
tdsChannel.setSPID(Util.readUnsignedShortBigEndian(newPacket.header, TDS.PACKET_HEADER_SPID));
// Packet header looks good enough.
// When logging, copy the packet header to the log buffer.
byte[] logBuffer = null;
if (tdsChannel.isLoggingPackets())
{
logBuffer = new byte[packetLength];
System.arraycopy(newPacket.header, 0, logBuffer, 0, TDS.PACKET_HEADER_SIZE);
}
// Now for the payload...
for (int payloadBytesRead = 0; payloadBytesRead < newPacket.payloadLength;)
{
int bytesRead = tdsChannel.read(newPacket.payload, payloadBytesRead, newPacket.payloadLength - payloadBytesRead);
if (bytesRead < 0)
con.terminate(SQLServerException.DRIVER_ERROR_IO_FAILED, SQLServerException.getErrString("R_truncatedServerResponse"));
payloadBytesRead += bytesRead;
}
++packetNum;
lastPacket.next = newPacket;
lastPacket = newPacket;
// When logging, append the payload to the log buffer and write out the whole thing.
if (tdsChannel.isLoggingPackets())
{
System.arraycopy(newPacket.payload, 0, logBuffer, TDS.PACKET_HEADER_SIZE, newPacket.payloadLength);
tdsChannel.logPacket(logBuffer, 0, packetLength,
this.toString() + " received Packet:" + packetNum + " (" + newPacket.payloadLength + " bytes)");
}
// If end of message, then bump the count of messages received and disable
// interrupts. If an interrupt happened prior to disabling, then expect
// to read the attention ack packet as well.
if (newPacket.isEOM())
{
++tdsChannel.numMsgsRcvd;
// Notify the command (if any) that we've reached the end of the response.
if (null != command)
command.onResponseEOM();
}
return true;
}
final TDSReaderMark mark()
{
TDSReaderMark mark = new TDSReaderMark(currentPacket, payloadOffset);
isStreaming = false;
if (logger.isLoggable(Level.FINEST))
logger.finest(this.toString() + ": Buffering from: " + mark.toString());
return mark;
}
final void reset(TDSReaderMark mark)
{
if (logger.isLoggable(Level.FINEST))
logger.finest(this.toString() + ": Resetting to: " + mark.toString());
currentPacket = mark.packet;
payloadOffset = mark.payloadOffset;
}
final void stream()
{
isStreaming = true;
}
/**
* Returns the number of bytes that can be read (or skipped over) from this
* TDSReader without blocking by the next caller of a method for this TDSReader.
*
* @return the actual number of bytes available.
*/
final int available()
{
// The number of bytes that can be read without blocking is just the number
// of bytes that are currently buffered. That is the number of bytes left
// in the current packet plus the number of bytes in the remaining packets.
int available = currentPacket.payloadLength - payloadOffset;
for (TDSPacket packet = currentPacket.next; null != packet; packet = packet.next)
available += packet.payloadLength;
return available;
}
final int peekTokenType() throws SQLServerException
{
// Check whether we're at EOF
if (!ensurePayload()) return -1;
// Peek at the current byte (don't increment payloadOffset!)
return currentPacket.payload[payloadOffset] & 0xFF;
}
final int readUnsignedByte() throws SQLServerException
{
// Ensure that we have a packet to read from.
if (!ensurePayload())
throwInvalidTDS();
return currentPacket.payload[payloadOffset++] & 0xFF;
}
final short readShort() throws SQLServerException
{
if (payloadOffset + 2 <= currentPacket.payloadLength)
{
short value = Util.readShort(currentPacket.payload, payloadOffset);
payloadOffset += 2;
return value;
}
return Util.readShort(readWrappedBytes(2), 0);
}
final int readUnsignedShort() throws SQLServerException
{
if (payloadOffset + 2 <= currentPacket.payloadLength)
{
int value = Util.readUnsignedShort(currentPacket.payload, payloadOffset);
payloadOffset += 2;
return value;
}
return Util.readUnsignedShort(readWrappedBytes(2), 0);
}
final String readUnicodeString(int length) throws SQLServerException
{
int byteLength = 2 * length;
byte bytes[] = new byte[byteLength];
readBytes(bytes, 0, byteLength);
return Util.readUnicodeString(bytes, 0, byteLength, con);
}
final char readChar() throws SQLServerException
{
return (char) readShort();
}
final int readInt() throws SQLServerException
{
if (payloadOffset + 4 <= currentPacket.payloadLength)
{
int value = Util.readInt(currentPacket.payload, payloadOffset);
payloadOffset += 4;
return value;
}
return Util.readInt(readWrappedBytes(4), 0);
}
final int readIntBigEndian() throws SQLServerException
{
if (payloadOffset + 4 <= currentPacket.payloadLength)
{
int value = Util.readIntBigEndian(currentPacket.payload, payloadOffset);
payloadOffset += 4;
return value;
}
return Util.readIntBigEndian(readWrappedBytes(4), 0);
}
final long readUnsignedInt() throws SQLServerException
{
return readInt() & 0xFFFFFFFFL;
}
final long readLong() throws SQLServerException
{
if (payloadOffset + 8 <= currentPacket.payloadLength)
{
long value = Util.readLong(currentPacket.payload, payloadOffset);
payloadOffset += 8;
return value;
}
return Util.readLong(readWrappedBytes(8), 0);
}
final void readBytes(byte[] value, int valueOffset, int valueLength) throws SQLServerException
{
for (int bytesRead = 0; bytesRead < valueLength;)
{
// Ensure that we have a packet to read from.
if (!ensurePayload())
throwInvalidTDS();
// Figure out how many bytes to copy from the current packet
// (the lesser of the remaining value bytes and the bytes left in the packet).
int bytesToCopy = valueLength - bytesRead;
if (bytesToCopy > currentPacket.payloadLength - payloadOffset)
bytesToCopy = currentPacket.payloadLength - payloadOffset;
// Copy some bytes from the current packet to the destination value.
if(logger.isLoggable(Level.FINEST))
logger.finest(toString() +" Reading " + bytesToCopy + " bytes from offset " + payloadOffset);
System.arraycopy(currentPacket.payload, payloadOffset, value, valueOffset + bytesRead, bytesToCopy);
bytesRead += bytesToCopy;
payloadOffset += bytesToCopy;
}
}
final byte[] readWrappedBytes(int valueLength) throws SQLServerException
{
assert valueLength <= valueBytes.length;
readBytes(valueBytes, 0, valueLength);
return valueBytes;
}
final Object readDecimal(
int valueLength,
TypeInfo typeInfo,
JDBCType jdbcType,
StreamType streamType) throws SQLServerException
{
if (valueLength > valueBytes.length)
{
logger.warning(toString() + " Invalid value length:" + valueLength);
throwInvalidTDS();
}
readBytes(valueBytes, 0, valueLength);
return DDC.convertBigDecimalToObject(
Util.readBigDecimal(valueBytes, valueLength, typeInfo.getScale()),
jdbcType,
streamType);
}
final Object readMoney(int valueLength, JDBCType jdbcType, StreamType streamType) throws SQLServerException
{
BigInteger bi;
switch (valueLength)
{
case 8: // money
{
int intBitsHi = readInt();
int intBitsLo = readInt();
if (JDBCType.BINARY == jdbcType)
{
byte value[] = new byte[8];
Util.writeIntBigEndian(intBitsHi, value, 0);
Util.writeIntBigEndian(intBitsLo, value, 4);
return value;
}
bi = BigInteger.valueOf(((long) intBitsHi << 32) | (intBitsLo & 0xFFFFFFFFL));
break;
}
case 4: // smallmoney
if (JDBCType.BINARY == jdbcType)
{
byte value[] = new byte[4];
Util.writeIntBigEndian(readInt(), value, 0);
return value;
}
bi = BigInteger.valueOf(readInt());
break;
default:
throwInvalidTDS();
return null;
}
return DDC.convertBigDecimalToObject(new BigDecimal(bi, 4), jdbcType, streamType);
}
final Object readReal(
int valueLength,
JDBCType jdbcType,
StreamType streamType) throws SQLServerException
{
if (4 != valueLength)
throwInvalidTDS();
return DDC.convertFloatToObject(Float.intBitsToFloat(readInt()), jdbcType, streamType);
}
final Object readFloat(
int valueLength,
JDBCType jdbcType,
StreamType streamType) throws SQLServerException
{
if (8 != valueLength)
throwInvalidTDS();
return DDC.convertDoubleToObject(Double.longBitsToDouble(readLong()), jdbcType, streamType);
}
final Object readDateTime(
int valueLength,
Calendar appTimeZoneCalendar,
JDBCType jdbcType,
StreamType streamType) throws SQLServerException
{
// Build and return the right kind of temporal object.
int daysSinceSQLBaseDate;
int ticksSinceMidnight;
int msecSinceMidnight;
switch (valueLength)
{
case 8:
// SQL datetime is 4 bytes for days since SQL Base Date
// (January 1, 1900 00:00:00 GMT) and 4 bytes for
// the number of three hundredths (1/300) of a second
// since midnight.
daysSinceSQLBaseDate = readInt();
ticksSinceMidnight = readInt();
if (JDBCType.BINARY == jdbcType)
{
byte value[] = new byte[8];
Util.writeIntBigEndian(daysSinceSQLBaseDate, value, 0);
Util.writeIntBigEndian(ticksSinceMidnight, value, 4);
return value;
}
msecSinceMidnight = (ticksSinceMidnight * 10 + 1) / 3; // Convert to msec (1 tick = 1 300th of a sec = 3 msec)
break;
case 4:
// SQL smalldatetime has less precision. It stores 2 bytes
// for the days since SQL Base Date and 2 bytes for minutes
// after midnight.
daysSinceSQLBaseDate = readUnsignedShort();
ticksSinceMidnight = readUnsignedShort();
if (JDBCType.BINARY == jdbcType)
{
byte value[] = new byte[4];
Util.writeShortBigEndian((short) daysSinceSQLBaseDate, value, 0);
Util.writeShortBigEndian((short) ticksSinceMidnight, value, 2);
return value;
}
msecSinceMidnight = ticksSinceMidnight * 60 * 1000; // Convert to msec (1 tick = 1 min = 60,000 msec)
break;
default:
throwInvalidTDS();
return null;
}
// Convert the DATETIME/SMALLDATETIME value to the desired Java type.
return DDC.convertTemporalToObject(
jdbcType,
SSType.DATETIME,
appTimeZoneCalendar,
daysSinceSQLBaseDate,
msecSinceMidnight,
0); // scale (ignored for fixed-scale DATETIME/SMALLDATETIME types)
}
final Object readDate(
int valueLength,
Calendar appTimeZoneCalendar,
JDBCType jdbcType) throws SQLServerException
{
if (TDS.DAYS_INTO_CE_LENGTH != valueLength)
throwInvalidTDS();
// Initialize the date fields to their appropriate values.
int localDaysIntoCE = readDaysIntoCE();
// Convert the DATE value to the desired Java type.
return DDC.convertTemporalToObject(
jdbcType,
SSType.DATE,
appTimeZoneCalendar,
localDaysIntoCE,
0, // midnight local to app time zone
0); // scale (ignored for DATE)
}
final Object readTime(
int valueLength,
TypeInfo typeInfo,
Calendar appTimeZoneCalendar,
JDBCType jdbcType) throws SQLServerException
{
if (TDS.timeValueLength(typeInfo.getScale()) != valueLength)
throwInvalidTDS();
// Read the value from the server
long localNanosSinceMidnight = readNanosSinceMidnight(typeInfo.getScale());
// Convert the TIME value to the desired Java type.
return DDC.convertTemporalToObject(
jdbcType,
SSType.TIME,
appTimeZoneCalendar,
0,
localNanosSinceMidnight,
typeInfo.getScale());
}
final Object readDateTime2(
int valueLength,
TypeInfo typeInfo,
Calendar appTimeZoneCalendar,
JDBCType jdbcType) throws SQLServerException
{
if (TDS.datetime2ValueLength(typeInfo.getScale()) != valueLength)
throwInvalidTDS();
// Read the value's constituent components
long localNanosSinceMidnight = readNanosSinceMidnight(typeInfo.getScale());
int localDaysIntoCE = readDaysIntoCE();
// Convert the DATETIME2 value to the desired Java type.
return DDC.convertTemporalToObject(
jdbcType,
SSType.DATETIME2,
appTimeZoneCalendar,
localDaysIntoCE,
localNanosSinceMidnight,
typeInfo.getScale());
}
final Object readDateTimeOffset(
int valueLength,
TypeInfo typeInfo,
JDBCType jdbcType) throws SQLServerException
{
if (TDS.datetimeoffsetValueLength(typeInfo.getScale()) != valueLength)
throwInvalidTDS();
// The nanos since midnight and days into Common Era parts of DATETIMEOFFSET values
// are in UTC. Use the minutes offset part to convert to local.
long utcNanosSinceMidnight = readNanosSinceMidnight(typeInfo.getScale());
int utcDaysIntoCE = readDaysIntoCE();
int localMinutesOffset = readShort();
// Convert the DATETIMEOFFSET value to the desired Java type.
return DDC.convertTemporalToObject(
jdbcType,
SSType.DATETIMEOFFSET,
new GregorianCalendar(new SimpleTimeZone(localMinutesOffset * 60 * 1000, ""), Locale.US),
utcDaysIntoCE,
utcNanosSinceMidnight,
typeInfo.getScale());
}
private int readDaysIntoCE() throws SQLServerException
{
byte value[] = new byte[TDS.DAYS_INTO_CE_LENGTH];
readBytes(value, 0, value.length);
int daysIntoCE = 0;
for (int i = 0; i < value.length; i++)
daysIntoCE |= ((value[i] & 0xFF) << (8 * i));
// Theoretically should never encounter a value that is outside of the valid date range
if (daysIntoCE < 0)
throwInvalidTDS();
return daysIntoCE;
}
// Scale multipliers used to convert variable-scaled temporal values to a fixed 100ns scale.
//
// Using this array is measurably faster than using Math.pow(10, ...)
private final static int[] SCALED_MULTIPLIERS =
{
10000000,
1000000,
100000,
10000,
1000,
100,
10,
1
};
private long readNanosSinceMidnight(int scale) throws SQLServerException
{
assert 0 <= scale && scale <= TDS.MAX_FRACTIONAL_SECONDS_SCALE;
byte value[] = new byte[TDS.nanosSinceMidnightLength(scale)];
readBytes(value, 0, value.length);
long hundredNanosSinceMidnight = 0;
for (int i = 0; i < value.length; i++)
hundredNanosSinceMidnight |= (value[i] & 0xFFL) << (8 * i);
hundredNanosSinceMidnight *= SCALED_MULTIPLIERS[scale];
if (!(0 <= hundredNanosSinceMidnight && hundredNanosSinceMidnight < Nanos.PER_DAY / 100))
throwInvalidTDS();
return 100 * hundredNanosSinceMidnight;
}
final static String guidTemplate = "NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN";
final Object readGUID(int valueLength, JDBCType jdbcType, StreamType streamType) throws SQLServerException
{
// GUIDs must be exactly 16 bytes
if (16 != valueLength)
throwInvalidTDS();
// Read in the GUID's binary value
byte guid[] = new byte[16];
readBytes(guid, 0, 16);
switch (jdbcType)
{
case CHAR:
case VARCHAR:
case LONGVARCHAR:
case GUID:
{
StringBuilder sb = new StringBuilder(guidTemplate.length());
for (int i=0; i<4; i++)
{
sb.append(Util.hexChars[(guid[3-i] & 0xF0) >> 4]);
sb.append(Util.hexChars[guid[3-i] & 0x0F]);
}
sb.append('-');
for (int i=0; i<2; i++)
{
sb.append(Util.hexChars[(guid[5-i] & 0xF0) >> 4]);
sb.append(Util.hexChars[guid[5-i] & 0x0F]);
}
sb.append('-');
for (int i=0; i<2; i++)
{
sb.append(Util.hexChars[(guid[7-i] & 0xF0) >> 4]);
sb.append(Util.hexChars[guid[7-i] & 0x0F]);
}
sb.append('-');
for (int i=0; i<2; i++)
{
sb.append(Util.hexChars[(guid[8+i] & 0xF0) >> 4]);
sb.append(Util.hexChars[guid[8+i] & 0x0F]);
}
sb.append('-');
for (int i=0; i<6; i++)
{
sb.append(Util.hexChars[(guid[10+i] & 0xF0) >> 4]);
sb.append(Util.hexChars[guid[10+i] & 0x0F]);
}
try
{
return DDC.convertStringToObject(sb.toString(), Encoding.UNICODE.charsetName(), jdbcType, streamType);
}
catch (UnsupportedEncodingException e)
{
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorConvertingValue"));
throw new SQLServerException(form.format(new Object[] {"UNIQUEIDENTIFIER", jdbcType}), null, 0, e);
}
}
default:
{
if (StreamType.BINARY == streamType || StreamType.ASCII == streamType)
return new ByteArrayInputStream(guid);
return guid;
}
}
}
/**
* Reads a multi-part table name from TDS and returns it as an array of Strings.
*/
final SQLIdentifier readSQLIdentifier() throws SQLServerException
{
// Multi-part names should have between 1 and 4 parts
int numParts = readUnsignedByte();
if (!(1 <= numParts && numParts <= 4))
throwInvalidTDS();
// Each part is a length-prefixed Unicode string
String[] nameParts = new String[numParts];
for (int i = 0; i < numParts; i++)
nameParts[i] = readUnicodeString(readUnsignedShort());
// Build the identifier from the name parts
SQLIdentifier identifier = new SQLIdentifier();
identifier.setObjectName(nameParts[numParts-1]);
if (numParts >= 2)
identifier.setSchemaName(nameParts[numParts-2]);
if (numParts >= 3)
identifier.setDatabaseName(nameParts[numParts-3]);
if (4 == numParts)
identifier.setServerName(nameParts[numParts-4]);
return identifier;
}
final SQLCollation readCollation() throws SQLServerException
{
SQLCollation collation = null;
try
{
collation = new SQLCollation(this);
}
catch (UnsupportedEncodingException e)
{
con.terminate(SQLServerException.DRIVER_ERROR_INVALID_TDS, e.getMessage(), e);
// not reached
}
return collation;
}
final void skip(int bytesToSkip) throws SQLServerException
{
assert bytesToSkip >= 0;
while (bytesToSkip > 0)
{
// Ensure that we have a packet to read from.
if (!ensurePayload())
throwInvalidTDS();
int bytesSkipped = bytesToSkip;
if (bytesSkipped > currentPacket.payloadLength - payloadOffset)
bytesSkipped = currentPacket.payloadLength - payloadOffset;
bytesToSkip -= bytesSkipped;
payloadOffset += bytesSkipped;
}
}
final void TryProcessFeatureExtAck(boolean featureExtAckReceived) throws SQLServerException
{
if( isColumnEncryptionSettingEnabled() && !featureExtAckReceived)
throw new SQLServerException(this , SQLServerException.getErrString("R_AE_NotSupportedByServer"), null, 0 , false);
}
}
/**
* Timer for use with Commands that support a timeout.
*
* Once started, the timer runs for the prescribed number
* of seconds unless stopped. If the timer runs out, it
* interrupts its associated Command with a reason like
* "timed out".
*/
final class TimeoutTimer implements Runnable
{
private final int timeoutSeconds;
private final TDSCommand command;
private Thread timerThread;
private volatile boolean canceled = false;
TimeoutTimer(int timeoutSeconds, TDSCommand command)
{
assert timeoutSeconds > 0;
assert null != command;
this.timeoutSeconds = timeoutSeconds;
this.command = command;
}
final void start()
{
timerThread = new Thread(this);
timerThread.setDaemon(true);
timerThread.start();
}
final void stop()
{
canceled = true;
timerThread.interrupt();
}
public void run()
{
int secondsRemaining = timeoutSeconds;
try
{
// Poll every second while time is left on the timer.
// Return if/when the timer is canceled.
do
{
if (canceled)
return;
Thread.sleep(1000);
}
while (--secondsRemaining > 0);
}
catch (InterruptedException e)
{
return;
}
// If the timer wasn't canceled before it ran out of
// time then interrupt the registered command.
try
{
command.interrupt(SQLServerException.getErrString("R_queryTimedOut"));
}
catch (SQLServerException e)
{
// Unfortunately, there's nothing we can do if we
// fail to time out the request. There is no way
// to report back what happened.
command.log(Level.FINE, "Command could not be timed out. Reason: " + e.getMessage());
}
}
}
/**
* TDSCommand encapsulates an interruptable TDS conversation.
*
* A conversation may consist of one or more TDS request and response messages.
* A command may be interrupted at any point, from any thread, and for any reason.
* Acknowledgement and handling of an interrupt is fully encapsulated by this class.
*
* Commands may be created with an optional timeout (in seconds). Timeouts are implemented as
* a form of interrupt, where the interrupt event occurs when the timeout period expires.
* Currently, only the time to receive the response from the channel counts against
* the timeout period.
*/
abstract class TDSCommand
{
abstract boolean doExecute() throws SQLServerException;
final static Logger logger = Logger.getLogger("com.microsoft.sqlserver.jdbc.internals.TDS.Command");
private final String logContext;
final String getLogContext() { return logContext; }
private String traceID;
final public String toString()
{
if(traceID== null)
traceID="TDSCommand@" + Integer.toHexString(hashCode()) + " (" + logContext + ")";
return traceID;
}
final void log(Level level, String message) { logger.log(level, toString() + ": " + message); }
// Optional timer that is set if the command was created with a non-zero timeout period.
// When the timer expires, the command is interrupted.
private final TimeoutTimer timeoutTimer;
// TDS channel accessors
// These are set/reset at command execution time.
// Volatile ensures visibility to execution thread and interrupt thread
private volatile TDSWriter tdsWriter;
private volatile TDSReader tdsReader;
// Lock to ensure atomicity when manipulating more than one of the following
// shared interrupt state variables below.
private final Object interruptLock = new Object();
// Flag set when this command starts execution, indicating that it is
// ready to respond to interrupts; and cleared when its last response packet is
// received, indicating that it is no longer able to respond to interrupts.
// If the command is interrupted after interrupts have been disabled, then the
// interrupt is ignored.
private volatile boolean interruptsEnabled = false;
// Flag set to indicate that an interrupt has happened.
private volatile boolean wasInterrupted = false;
private final boolean wasInterrupted() { return wasInterrupted; }
// The reason for the interrupt.
private volatile String interruptReason = null;
// Flag set when this command's request to the server is complete.
// If a command is interrupted before its request is complete, it is the executing
// thread's responsibility to send the attention signal to the server if necessary.
// After the request is complete, the interrupting thread must send the attention signal.
private volatile boolean requestComplete;
// Flag set when an attention signal has been sent to the server, indicating that a
// TDS packet containing the attention ack message is to be expected in the response.
// This flag is cleared after the attention ack message has been received and processed.
private volatile boolean attentionPending = false;
boolean attentionPending() { return attentionPending; }
// Flag set when this command's response has been processed. Until this flag is set,
// there may be unprocessed information left in the response, such as transaction
// ENVCHANGE notifications.
private volatile boolean processedResponse;
// Flag set when this command's response is ready to be read from the server and cleared
// after its response has been received, but not necessarily processed, up to and including
// any attention ack. The command's response is read either on demand as it is processed,
// or by detaching.
private volatile boolean readingResponse;
final boolean readingResponse() { return readingResponse; }
/**
* Creates this command with an optional timeout.
*
* @param logContext the string describing the context for this command.
* @param timeoutSeconds (optional) the time before which the command must complete before it
* is interrupted. A value of 0 means no timeout.
*/
TDSCommand(String logContext, int timeoutSeconds)
{
this.logContext = logContext;
this.timeoutTimer = (timeoutSeconds > 0) ? (new TimeoutTimer(timeoutSeconds, this)) : null;
}
/**
* Executes this command.
*
* @param tdsWriter
* @param tdsReader
* @throws SQLServerException on any error executing the command, including cancel or timeout.
*/
boolean execute(
TDSWriter tdsWriter,
TDSReader tdsReader) throws SQLServerException
{
this.tdsWriter = tdsWriter;
this.tdsReader = tdsReader;
assert null != tdsReader;
try
{
return doExecute(); // Derived classes implement the execution details
}
catch (SQLServerException e)
{
try
{
// If command execution threw an exception for any reason before the request
// was complete then interrupt the command (it may already be interrupted)
// and close it out to ensure that any response to the error/interrupt
// is processed.
// no point in trying to cancel on a closed connection.
if (!requestComplete && !tdsReader.getConnection().isClosed())
{
interrupt(e.getMessage());
onRequestComplete();
close();
}
}
catch (SQLServerException interruptException)
{
if (logger.isLoggable(Level.FINE))
logger.fine(this.toString() + ": Ignoring error in sending attention: "
+ interruptException.getMessage());
}
// throw the original exception even if trying to interrupt fails even in the case
// of trying to send a cancel to the server.
throw e;
}
}
/**
* Provides sane default response handling.
*
* This default implementation just consumes everything in the response message.
*/
void processResponse(TDSReader tdsReader) throws SQLServerException
{
if (logger.isLoggable(Level.FINEST))
logger.finest(this.toString() + ": Processing response");
try
{
TDSParser.parse(tdsReader, getLogContext());
}
catch (SQLServerException e)
{
if (SQLServerException.DRIVER_ERROR_FROM_DATABASE != e.getDriverErrorCode())
throw e;
if (logger.isLoggable(Level.FINEST))
logger.finest(this.toString() + ": Ignoring error from database: " + e.getMessage());
}
}
/**
* Clears this command from the TDS channel so that another command can execute.
*
* This method does not process the response. It just buffers it in memory,
* including any attention ack that may be present.
*/
final void detach() throws SQLServerException
{
if (logger.isLoggable(Level.FINEST))
logger.finest(this + ": detaching...");
// Read any remaining response packets from the server.
// This operation may be timed out or cancelled from another thread.
while (tdsReader.readPacket())
;
// Postcondition: the entire response has been read
assert !readingResponse;
}
final void close()
{
if (logger.isLoggable(Level.FINEST))
logger.finest(this + ": closing...");
if (logger.isLoggable(Level.FINEST))
logger.finest(this + ": processing response...");
while (!processedResponse)
{
try
{
processResponse(tdsReader);
}
catch (SQLServerException e)
{
if (logger.isLoggable(Level.FINEST))
logger.finest(this + ": close ignoring error processing response: " + e.getMessage());
if (tdsReader.getConnection().isSessionUnAvailable())
{
processedResponse = true;
attentionPending = false;
}
}
}
if (attentionPending)
{
if (logger.isLoggable(Level.FINEST))
logger.finest(this + ": processing attention ack...");
try
{
TDSParser.parse(tdsReader, "attention ack");
}
catch (SQLServerException e)
{
if (tdsReader.getConnection().isSessionUnAvailable())
{
if (logger.isLoggable(Level.FINEST))
logger.finest(this + ": giving up on attention ack after connection closed by exception: " + e);
attentionPending = false;
}
else
{
if (logger.isLoggable(Level.FINEST))
logger.finest(this + ": ignored exception: " + e);
}
}
// If the parser returns to us without processing the expected attention ack,
// then assume that no attention ack is forthcoming from the server and
// terminate the connection to prevent any other command from executing.
if (attentionPending)
{
logger.severe(this + ": expected attn ack missing or not processed; terminating connection...");
try
{
tdsReader.throwInvalidTDS();
}
catch (SQLServerException e)
{
if (logger.isLoggable(Level.FINEST))
logger.finest(this + ": ignored expected invalid TDS exception: " + e);
assert tdsReader.getConnection().isSessionUnAvailable();
attentionPending = false;
}
}
}
// Postcondition:
// Response has been processed and there is no attention pending -- the command is closed.
// Of course the connection may be closed too, but the command is done regardless...
assert processedResponse && !attentionPending;
}
/**
* Interrupts execution of this command, typically from another thread.
*
* Only the first interrupt has any effect. Subsequent interrupts are ignored.
* Interrupts are also ignored until enabled. If interrupting the command
* requires an attention signal to be sent to the server, then this method sends
* that signal if the command's request is already complete.
*
* Signalling mechanism is "fire and forget". It is up to either the execution
* thread or, possibly, a detaching thread, to ensure that any pending attention
* ack later will be received and processed.
*
* @param reason the reason for the interrupt, typically cancel or timeout.
* @throws SQLServerException if interrupting fails for some reason. This call does not
* throw the reason for the interrupt.
*/
void interrupt(String reason) throws SQLServerException
{
// Multiple, possibly simultaneous, interrupts may occur.
// Only the first one should be recognized and acted upon.
synchronized (interruptLock)
{
if (interruptsEnabled && !wasInterrupted())
{
if (logger.isLoggable(Level.FINEST))
logger.finest(this + ": Raising interrupt for reason:" + reason);
wasInterrupted = true;
interruptReason = reason;
if (requestComplete)
attentionPending = tdsWriter.sendAttention();
}
}
}
private boolean interruptChecked = false;
/**
* Checks once whether an interrupt has occurred, and, if it has, throws an
* exception indicating that fact.
*
* Any calls after the first to check for interrupts are no-ops. This method
* is called periodically from this command's execution thread to notify the
* app when an interrupt has happened.
*
* It should only be called from places where consistent behavior can be ensured
* after the exception is thrown. For example, it should not be called at arbitrary
* times while processing the response, as doing so could leave the response token
* stream in an inconsistent state. Currently, response processing only checks for
* interrupts after every result or OUT parameter.
*
* Request processing checks for interrupts before writing each packet.
*
* @throws SQLServerException if this command was interrupted, throws the
* reason for the interrupt.
*/
final void checkForInterrupt() throws SQLServerException
{
// Throw an exception with the interrupt reason if this command was interrupted.
// Note that the interrupt reason may be null. Checking whether the
// command was interrupted does not require the interrupt lock since only one
// of the shared state variables is being manipulated; interruptChecked is not
// shared with the interrupt thread.
if (wasInterrupted() && !interruptChecked)
{
interruptChecked = true;
if (logger.isLoggable(Level.FINEST))
logger.finest(this + ": throwing interrupt exception, reason: " + interruptReason);
throw new SQLServerException(
interruptReason,
SQLState.STATEMENT_CANCELED,
DriverError.NOT_SET,
null);
}
}
/**
* Notifies this command when no more request packets are to be sent to the server.
*
* After the last packet has been sent, the only way to interrupt the request
* is to send an attention signal from the interrupt() method.
*
* Note that this method is called when the request completes normally
* (last packet sent with EOM bit) or when it completes after being interrupted
* (0 or more packets sent with no EOM bit).
*/
final void onRequestComplete() throws SQLServerException
{
assert !requestComplete;
if (logger.isLoggable(Level.FINEST))
logger.finest(this + ": request complete");
synchronized (interruptLock)
{
requestComplete = true;
// If this command was interrupted before its request was complete then
// we need to send the attention signal if necessary. Note that if no
// attention signal is sent (i.e. no packets were sent to the server before
// the interrupt happened), then don't expect an attention ack or any
// other response.
if (!interruptsEnabled)
{
assert !attentionPending;
assert !processedResponse;
assert !readingResponse;
processedResponse = true;
}
else if (wasInterrupted())
{
if(tdsWriter.isEOMSent())
{
attentionPending = tdsWriter.sendAttention();
readingResponse = attentionPending;
}
else
{
assert !attentionPending;
readingResponse = tdsWriter.ignoreMessage();
}
processedResponse = !readingResponse;
}
else
{
assert !attentionPending;
assert !processedResponse;
readingResponse = true;
}
}
}
/**
* Notifies this command when the last packet of the response has been read.
*
* When the last packet is read, interrupts are disabled. If an interrupt
* occurred prior to disabling that caused an attention signal to be sent
* to the server, then an extra packet containing the attention ack is read.
*
* This ensures that on return from this method, the TDS channel is clear
* of all response packets for this command.
*
* Note that this method is called for the attention ack message itself as well,
* so we need to be sure not to expect more than one attention ack...
*/
final void onResponseEOM() throws SQLServerException
{
boolean readAttentionAck = false;
// Atomically disable interrupts and check for a previous interrupt requiring
// an attention ack to be read.
synchronized (interruptLock)
{
if (interruptsEnabled)
{
if (logger.isLoggable(Level.FINEST))
logger.finest(this + ": disabling interrupts");
// Determine whether we still need to read the attention ack packet.
//
// When a command is interrupted, Yukon (and later) always sends a response
// containing at least a DONE(ERROR) token before it sends the attention ack,
// even if the command's request was not complete.
readAttentionAck = attentionPending;
interruptsEnabled = false;
}
}
// If an attention packet needs to be read then read it. This should
// be done outside of the interrupt lock to avoid unnecessarily blocking
// interrupting threads. Note that it is remotely possible that the call
// to readPacket won't actually read anything if the attention ack was
// already read by TDSCommand.detach(), in which case this method could
// be called from multiple threads, leading to a benign race to clear the
// readingResponse flag.
if (readAttentionAck)
tdsReader.readPacket();
readingResponse = false;
}
/**
* Notifies this command when the end of its response token stream has been reached.
*
* After this call, we are guaranteed that tokens in the response have been processed.
*/
final void onTokenEOF()
{
processedResponse = true;
}
/**
* Notifies this command when the attention ack (a DONE token with a special flag)
* has been processed.
*
* After this call, the attention ack should no longer be expected.
*/
final void onAttentionAck()
{
assert attentionPending;
attentionPending = false;
}
/**
* Starts sending this command's TDS request to the server.
*
* @param tdsMessageType the type of the TDS message (RPC, QUERY, etc.)
* @return the TDS writer used to write the request.
* @throws SQLServerException on any error, including acknowledgement of an interrupt.
*/
final TDSWriter startRequest(byte tdsMessageType) throws SQLServerException
{
if (logger.isLoggable(Level.FINEST))
logger.finest(this + ": starting request...");
// Start this command's request message
try
{
tdsWriter.startMessage(this, tdsMessageType);
}
catch (SQLServerException e)
{
if (logger.isLoggable(Level.FINEST))
logger.finest(this + ": starting request: exception: " + e.getMessage());
throw e;
}
// (Re)initialize this command's interrupt state for its current execution.
// To ensure atomically consistent behavior, do not leave the interrupt lock
// until interrupts have been (re)enabled.
synchronized (interruptLock)
{
requestComplete = false;
readingResponse = false;
processedResponse = false;
attentionPending = false;
wasInterrupted = false;
interruptReason = null;
interruptsEnabled = true;
}
return tdsWriter;
}
/**
* Finishes the TDS request and then starts reading the TDS response from the server.
*
* @return the TDS reader used to read the response.
* @throws SQLServerException if there is any kind of error.
*/
final TDSReader startResponse() throws SQLServerException
{
return startResponse(false);
}
final TDSReader startResponse(boolean isAdaptive) throws SQLServerException
{
// Finish sending the request message. If this command was interrupted
// at any point before endMessage() returns, then endMessage() throws an
// exception with the reason for the interrupt. Request interrupts
// are disabled by the time endMessage() returns.
if (logger.isLoggable(Level.FINEST))
logger.finest(this + ": finishing request");
try
{
tdsWriter.endMessage();
}
catch (SQLServerException e)
{
if (logger.isLoggable(Level.FINEST))
logger.finest(this + ": finishing request: endMessage threw exception: " + e.getMessage());
throw e;
}
// If command execution is subject to timeout then start timing until
// the server returns the first response packet.
if (null != timeoutTimer)
{
if (logger.isLoggable(Level.FINEST))
logger.finest(this.toString() + ": Starting timer...");
timeoutTimer.start();
}
if (logger.isLoggable(Level.FINEST))
logger.finest(this.toString() + ": Reading response...");
try
{
// Wait for the server to execute the request and read the first packet
// (responseBuffering=adaptive) or all packets (responseBuffering=full)
// of the response.
if (isAdaptive)
{
tdsReader.readPacket();
}
else
{
while (tdsReader.readPacket())
;
}
}
catch (SQLServerException e)
{
if (logger.isLoggable(Level.FINEST))
logger.finest(this.toString() + ": Exception reading response: " + e.getMessage());
throw e;
}
finally
{
// If command execution was subject to timeout then stop timing as soon
// as the server returns the first response packet or errors out.
if (null != timeoutTimer)
{
if (logger.isLoggable(Level.FINEST))
logger.finest(this.toString() + ": Stopping timer...");
timeoutTimer.stop();
}
}
return tdsReader;
}
}
/**
* UninterruptableTDSCommand encapsulates an uninterruptable TDS conversation.
*
* TDSCommands have interruptability built in. However, some TDSCommands such as
* DTC commands, connection commands, cursor close and prepared statement handle close
* shouldn't be interruptable. This class provides a base implementation for such
* commands.
*/
abstract class UninterruptableTDSCommand extends TDSCommand
{
UninterruptableTDSCommand(String logContext)
{
super(logContext, 0);
}
final void interrupt(String reason) throws SQLServerException
{
// Interrupting an uninterruptable command is a no-op. That is,
// it can happen, but it should have no effect.
logger.finest(toString() + " Ignoring interrupt of uninterruptable TDS command; Reason:" + reason);
}
}