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 Driver for SQL Server.
//---------------------------------------------------------------------------------------------------------------------------------
// 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);
}
}