com.microsoft.sqlserver.jdbc.IOBuffer Maven / Gradle / Ivy
Show all versions of mssql-jdbc Show documentation
/*
* Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
* available under the terms of the MIT License. See the LICENSE file in the project root for more information.
*/
package com.microsoft.sqlserver.jdbc;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.security.KeyStore;
import java.security.Provider;
import java.security.Security;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.sql.Timestamp;
import java.text.MessageFormat;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.GregorianCalendar;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SimpleTimeZone;
import java.util.TimeZone;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import com.microsoft.sqlserver.jdbc.dataclassification.SensitivityClassification;
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;
static final int TDS_SQLRESCOLSRCS = 0xa2;
static final int TDS_SQLDATACLASSIFICATION = 0xa3;
// FedAuth
static final byte 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 ADALWORKFLOW_ACTIVEDIRECTORYMSI = 0x03;
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
// 0x03 is for x_eFeatureExtensionId_Rcs
static final byte TDS_FEATURE_EXT_AE = 0x04;
static final byte MAX_SUPPORTED_TCE_VERSION = 0x01; // max version
static final int CUSTOM_CIPHER_ALGORITHM_ID = 0; // max version
// 0x06 is for x_eFeatureExtensionId_LoginToken
// 0x07 is for x_eFeatureExtensionId_ClientSideTelemetry
// Data Classification constants
static final byte TDS_FEATURE_EXT_DATACLASSIFICATION = 0x09;
static final byte DATA_CLASSIFICATION_NOT_ENABLED = 0x00;
static final byte MAX_SUPPORTED_DATA_CLASSIFICATION_VERSION = 0x01;
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 byte TDS_FEATURE_EXT_UTF8SUPPORT = 0x0A;
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;
// Sql_variant length
static final int SQL_VARIANT_LENGTH = 8009;
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)";
case TDS_FEATURE_EXT_DATACLASSIFICATION:
return "TDS_FEATURE_EXT_DATACLASSIFICATION (0x09)";
case TDS_FEATURE_EXT_UTF8SUPPORT:
return "TDS_FEATURE_EXT_UTF8SUPPORT (0x0A)";
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, Calendar.FEBRUARY, 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() {}
}
final class UTC {
// UTC/GMT time zone singleton.
static final TimeZone timeZone = new SimpleTimeZone(0, "UTC");
private UTC() {}
}
final class TDSChannel implements Serializable {
/**
* Always update serialVersionUID when prompted.
*/
private static final long serialVersionUID = -866497813437384090L;
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.
*/
@SuppressWarnings("unused")
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);
// set SO_TIMEOUT
int socketTimeout = con.getSocketTimeoutMilliseconds();
tcpSocket.setSoTimeout(socketTimeout);
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 not responding 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 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 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 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(Locale.ENGLISH);
}
// 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) {
// 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;
}
// We do not allow wildcards in IDNs (xn--).
if (!nameInCert.startsWith("xn--") && nameInCert.contains("*")) {
int hostIndex = 0, certIndex = 0, match = 0, startIndex = -1, periodCount = 0;
while (hostIndex < hostName.length()) {
if ('.' == hostName.charAt(hostIndex)) {
periodCount++;
}
if (certIndex < nameInCert.length() && hostName.charAt(hostIndex) == nameInCert.charAt(certIndex)) {
hostIndex++;
certIndex++;
} else if (certIndex < nameInCert.length() && '*' == nameInCert.charAt(certIndex)) {
startIndex = certIndex;
match = hostIndex;
certIndex++;
} else if (startIndex != -1 && 0 == periodCount) {
certIndex = startIndex + 1;
match++;
hostIndex = match;
} else {
logFailMessage(nameInCert);
return false;
}
}
if (nameInCert.length() == certIndex && periodCount > 1) {
logSuccessMessage(nameInCert);
return true;
} else {
logFailMessage(nameInCert);
return false;
}
}
// Verify that the name in certificate matches exactly with the host name
if (!nameInCert.equals(hostName)) {
logFailMessage(nameInCert);
return false;
}
logSuccessMessage(nameInCert);
return true;
}
private void logFailMessage(String nameInCert) {
if (logger.isLoggable(Level.FINER)) {
logger.finer(logContext + " The name in certificate " + nameInCert
+ " does not match with the server name " + hostName + ".");
}
}
private void logSuccessMessage(String nameInCert) {
if (logger.isLoggable(Level.FINER)) {
logger.finer(logContext + " The name in certificate:" + nameInCert + " validated against server name "
+ hostName + ".");
}
}
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;
// 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;
// Use 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.toLowerCase(Locale.ENGLISH);
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
}
/**
* Enables SSL Handshake.
*
* @param host
* Server Host Name for SSL Handshake
* @param port
* Server Port for SSL Handshake
* @throws SQLServerException
*/
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;
boolean isFips = false;
String trustStoreType = null;
String sslProtocol = null;
// 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());
trustStoreType = con.activeConnectionProperties
.getProperty(SQLServerDriverStringProperty.TRUST_STORE_TYPE.toString());
if (StringUtils.isEmpty(trustStoreType)) {
trustStoreType = SQLServerDriverStringProperty.TRUST_STORE_TYPE.getDefaultValue();
}
isFips = Boolean.valueOf(
con.activeConnectionProperties.getProperty(SQLServerDriverBooleanProperty.FIPS.toString()));
sslProtocol = con.activeConnectionProperties
.getProperty(SQLServerDriverStringProperty.SSL_PROTOCOL.toString());
if (isFips) {
validateFips(trustStoreType, trustStoreFileName);
}
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 check if a specific TrustManager implemenation has been requested and
// if so instantiate it, optionally specifying a constructor argument to customize it.
else if (con.getTrustManagerClass() != null) {
Class tmClass = Class.forName(con.getTrustManagerClass());
if (!TrustManager.class.isAssignableFrom(tmClass)) {
throw new IllegalArgumentException(
"The class specified by the trustManagerClass property must implement javax.net.ssl.TrustManager");
}
String constructorArg = con.getTrustManagerConstructorArg();
if (constructorArg == null) {
tm = new TrustManager[] {(TrustManager) tmClass.getDeclaredConstructor().newInstance()};
} else {
tm = new TrustManager[] {
(TrustManager) tmClass.getDeclaredConstructor(String.class).newInstance(constructorArg)};
}
}
// 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(trustStoreType);
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 Only if it is not FIPS
if (!isFips) {
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(sslProtocol);
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");
// don't close proxy when SSL socket is closed
sslSocket = (SSLSocket) sslContext.getSocketFactory().createSocket(proxySocket, host, port, false);
// 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"));
// Retrieve the localized error message if possible.
String localizedMessage = e.getLocalizedMessage();
String errMsg = (localizedMessage != null) ? localizedMessage : e.getMessage();
/*
* Retrieve the error message of the cause too because actual error message can be wrapped into a different
* message when re-thrown from underlying InputStream.
*/
String causeErrMsg = null;
Throwable cause = e.getCause();
if (cause != null) {
String causeLocalizedMessage = cause.getLocalizedMessage();
causeErrMsg = (causeLocalizedMessage != null) ? causeLocalizedMessage : cause.getMessage();
}
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_sslFailed"));
Object[] msgArgs = {errMsg};
/*
* 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 (errMsg != null && errMsg.contains(SQLServerException.LOG_CLIENT_CONNECTION_ID_PREFIX)) {
errMsg = errMsg.substring(0, errMsg.indexOf(SQLServerException.LOG_CLIENT_CONNECTION_ID_PREFIX));
}
if (causeErrMsg != null && causeErrMsg.contains(SQLServerException.LOG_CLIENT_CONNECTION_ID_PREFIX)) {
causeErrMsg = causeErrMsg.substring(0,
causeErrMsg.indexOf(SQLServerException.LOG_CLIENT_CONNECTION_ID_PREFIX));
}
// Isolate the TLS1.2 intermittent connection error.
if (e instanceof IOException && (SSLHandhsakeState.SSL_HANDHSAKE_STARTED == handshakeState)
&& (SQLServerException.getErrString("R_truncatedServerResponse").equals(errMsg)
|| SQLServerException.getErrString("R_truncatedServerResponse").equals(causeErrMsg))) {
con.terminate(SQLServerException.DRIVER_ERROR_INTERMITTENT_TLS_FAILED, form.format(msgArgs), e);
} else {
con.terminate(SQLServerException.DRIVER_ERROR_SSL_FAILED, form.format(msgArgs), e);
}
}
}
/**
* Validate FIPS if fips set as true
*
* Valid FIPS settings:
* Encrypt should be true
* trustServerCertificate should be false
* if certificate is not installed TrustStoreType should be present.
*
* @param trustStoreType
* @param trustStoreFileName
* @throws SQLServerException
* @since 6.1.4
*/
private void validateFips(final String trustStoreType, final String trustStoreFileName) throws SQLServerException {
boolean isValid = false;
boolean isEncryptOn;
boolean isValidTrustStoreType;
boolean isValidTrustStore;
boolean isTrustServerCertificate;
String strError = SQLServerException.getErrString("R_invalidFipsConfig");
isEncryptOn = (TDS.ENCRYPT_ON == con.getRequestedEncryptionLevel());
isValidTrustStoreType = !StringUtils.isEmpty(trustStoreType);
isValidTrustStore = !StringUtils.isEmpty(trustStoreFileName);
isTrustServerCertificate = con.trustServerCertificate();
if (isEncryptOn && !isTrustServerCertificate) {
isValid = true;
if (isValidTrustStore && !isValidTrustStoreType) {
// In case of valid trust store we need to check TrustStoreType.
isValid = false;
if (logger.isLoggable(Level.FINER))
logger.finer(toString() + "TrustStoreType is required alongside with TrustStore.");
}
}
if (!isValid) {
throw new SQLServerException(strError, null, 0, null);
}
}
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());
if (e instanceof SocketTimeoutException) {
con.terminate(SQLServerException.ERROR_SOCKET_TIMEOUT, e.getMessage(), e);
} else {
con.terminate(SQLServerException.DRIVER_ERROR_IO_FAILED, e.getMessage(), e);
}
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(), e);
}
}
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(), e);
}
}
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()).append(":").append(tcpSocket.getLocalPort())
.append(" SPID:").append(spid).append(" ").append(messageDetail).append("\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");
}
if (packetLogger.isLoggable(Level.FINEST)) {
packetLogger.finest(logMsg.toString());
}
}
/**
* Get the current socket SO_TIMEOUT value.
*
* @return the current socket timeout value
* @throws IOException
* thrown if the socket timeout cannot be read
*/
final int getNetworkTimeout() throws IOException {
return tcpSocket.getSoTimeout();
}
/**
* Set the socket SO_TIMEOUT value.
*
* @param timeout
* the socket timeout in milliseconds
* @throws IOException
* thrown if the socket timeout cannot be set
*/
final void setNetworkTimeout(int timeout) throws IOException {
tcpSocket.setSoTimeout(timeout);
}
}
/**
* 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 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)) {
StringBuilder loggingString = new StringBuilder(this.toString());
loggingString.append(" Total no of InetAddresses: ");
loggingString.append(inetAddrs.length);
loggingString.append(". They are: ");
for (InetAddress inetAddr : inetAddrs) {
loggingString.append(inetAddr.toString()).append(";");
}
logger.finer(loggingString.toString());
}
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 (inetAddrs.length == 1) {
// Single address so do not start any threads
return getConnectedSocket(inetAddrs[0], portNumber, timeoutInMilliSeconds);
}
timeoutInMilliSeconds = Math.max(timeoutInMilliSeconds, minTimeoutForParallelConnections);
if (Util.isIBM()) {
if (logger.isLoggable(Level.FINER)) {
logger.finer(this.toString() + "Using Java NIO with timeout:" + timeoutInMilliSeconds);
}
findSocketUsingJavaNIO(inetAddrs, portNumber, timeoutInMilliSeconds);
} else {
if (logger.isLoggable(Level.FINER)) {
logger.finer(this.toString() + "Using Threading with timeout:" + timeoutInMilliSeconds);
}
findSocketUsingThreading(inetAddrs, portNumber, timeoutInMilliSeconds);
}
// 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) {
// re-interrupt the current thread, in order to restore the thread's interrupt status.
Thread.currentThread().interrupt();
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);
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 (InetAddress inetAddr : inetAddrs) {
SocketChannel sChannel = SocketChannel.open();
socketChannels.add(sChannel);
// make the channel non-blocking
sChannel.configureBlocking(false);
// register the channel for connect event
@SuppressWarnings("unused")
int ops = SelectionKey.OP_CONNECT;
SelectionKey key = sChannel.register(selector, ops);
sChannel.connect(new InetSocketAddress(inetAddr, portNumber));
if (logger.isLoggable(Level.FINER))
logger.finer(this.toString() + " initiated connection to address: " + inetAddr + ", 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 : "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() + " occurred while processing the channel: " + ch);
updateSelectedException(ex, this.toString());
// close the channel pro-actively so that we do not
// rely 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(InetAddress[] inetAddrs, int portNumber,
int timeoutInMilliSeconds) throws IOException, InterruptedException {
assert timeoutInMilliSeconds != 0 : "The timeout cannot be zero";
assert inetAddrs.length != 0 : "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.length;
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);
}
}
}
if (selectedSocket != null) {
result = Result.SUCCESS;
}
}
/**
* 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.notifyAll();
}
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.
*
* @param ex
* the IOException
* @param traceId
* the traceId of the thread
*/
public void updateSelectedException(IOException ex, String traceId) {
boolean updatedException = false;
if (selectedException == null
|| (!(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 fof tracing
*
* @return traceID string
*/
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()
+ " occurred while connecting to InetSocketAddress:" + inetSocketAddress);
}
exception = ex;
}
socketFinder.updateResult(socket, exception, this.toString());
}
}
/**
* Used for tracing
*
* @return traceID string
*/
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;
}
SharedTimer getSharedTimer() throws SQLServerException {
return con.getSharedTimer();
}
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 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);
((Buffer) 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() && Util.isActivityTraceOn()
&& !ActivityCorrelator.getCurrent().isSentToServer()) {
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;
}
((Buffer) socketBuffer).position(((Buffer) socketBuffer).limit());
((Buffer) 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 || TDS.PKT_BULK == this.tdsMessageType) {
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
((Buffer) logBuffer).position(((Buffer) logBuffer).position() + 1);
}
} else {
valueBytes[0] = value;
writeWrappedBytes(valueBytes, 1);
}
}
/**
* writing sqlCollation information for sqlVariant type when sending character types.
*
* @param variantType
* @throws SQLServerException
*/
void writeCollationForSqlVariant(SqlVariant variantType) throws SQLServerException {
writeInt(variantType.getCollation().getCollationInfo());
writeByte((byte) (variantType.getCollation().getCollationSortID() & 0xFF));
}
void writeChar(char value) throws SQLServerException {
if (stagingBuffer.remaining() >= 2) {
stagingBuffer.putChar(value);
if (tdsChannel.isLoggingPackets()) {
if (dataIsLoggable)
logBuffer.putChar(value);
else
((Buffer) logBuffer).position(((Buffer) 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
((Buffer) logBuffer).position(((Buffer) 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
((Buffer) logBuffer).position(((Buffer) 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 {
writeInt(Float.floatToRawIntBits(value));
}
/**
* 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
((Buffer) logBuffer).position(((Buffer) 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
* @param scale
* the scale of the column
* @throws SQLServerException
*/
void writeBigDecimal(BigDecimal bigDecimalVal, int srcJdbcType, int precision,
int scale) 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.
*/
/*
* setScale of all BigDecimal value based on metadata as scale is not sent separately for individual value. Use
* the rounding used in Server. Say, for BigDecimal("0.1"), if scale in metdadata is 0, then ArithmeticException
* would be thrown if RoundingMode is not set
*/
bigDecimalVal = bigDecimalVal.setScale(scale, RoundingMode.HALF_UP);
// data length + 1 byte for sign
int bLength = BYTES16 + 1;
writeByte((byte) (bLength));
// Byte array to hold all the data and padding bytes.
byte[] bytes = new byte[bLength];
byte[] valueBytes = DDC.convertBigDecimalToBytes(bigDecimalVal, scale);
// removing the precision and scale information from the valueBytes array
System.arraycopy(valueBytes, 2, bytes, 0, valueBytes.length - 2);
writeBytes(bytes);
}
/**
* Append a big decimal inside sql_variant in the TDS stream.
*
* @param bigDecimalVal
* the big decimal data value
* @param srcJdbcType
* the source JDBCType
*/
void writeSqlVariantInternalBigDecimal(BigDecimal bigDecimalVal, int srcJdbcType) throws SQLServerException {
/*
* Length including sign byte One 1-byte unsigned integer that represents the sign of the decimal value (0 =>
* Negative, 1 => positive) One 16-byte signed integer that represents the decimal value multiplied by 10^scale.
* In sql_variant, we send the bigdecimal with precision 38, therefore we use 16 bytes for the maximum size of
* this integer.
*/
boolean isNegative = (bigDecimalVal.signum() < 0);
BigInteger bi = bigDecimalVal.unscaledValue();
if (isNegative) {
bi = bi.negate();
}
int bLength;
bLength = BYTES16;
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; // 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 = 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; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT)
int subSecondNanos;
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;
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; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT)
int subSecondNanos;
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;
TimeZone timeZone; // Time zone to associate with the value in the Gregorian calendar
long utcMillis; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT)
int subSecondNanos;
int minutesOffset;
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;
TimeZone timeZone;
long utcMillis;
int subSecondNanos;
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
e);
}
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;
TimeZone timeZone;
long utcMillis;
int subSecondNanos;
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
e);
}
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
((Buffer) logBuffer).position(((Buffer) logBuffer).position() + 8);
}
} else {
Util.writeLong(value, valueBytes, 0);
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
((Buffer) logBuffer).position(((Buffer) 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;
int remaining = stagingBuffer.remaining();
assert remaining < valueLength;
assert valueLength <= stagingBuffer.capacity();
// Fill any remaining space in the staging buffer
remaining = stagingBuffer.remaining();
if (remaining > 0) {
stagingBuffer.put(value, 0, remaining);
if (tdsChannel.isLoggingPackets()) {
if (dataIsLoggable)
logBuffer.put(value, 0, remaining);
else
((Buffer) logBuffer).position(((Buffer) 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
((Buffer) logBuffer).position(((Buffer) 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 = {advertisedLength, 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,
Charset 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) {
if (null == charSet) {
streamByteBuffer[charsCopied] = (byte) (streamCharBuffer[charsCopied] & 0xFF);
} else {
// encoding as per collation
streamByteBuffer[charsCopied] = new String(streamCharBuffer[charsCopied] + "")
.getBytes(charSet)[0];
}
}
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 = {advertisedLength, 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 = {advertisedLength, actualLength};
error(form.format(msgArgs), SQLState.DATA_EXCEPTION_LENGTH_MISMATCH, DriverError.NOT_SET);
}
}
GregorianCalendar initializeCalender(TimeZone timeZone) {
GregorianCalendar calendar;
// 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 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 void writePacketHeader(int tdsMessageStatus) {
int tdsMessageLength = ((Buffer) 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(), ((Buffer) socketBuffer).position(), socketBuffer.remaining());
((Buffer) socketBuffer).position(((Buffer) 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 (((Buffer) 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.
((Buffer) socketBuffer).flip();
((Buffer) 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, ((Buffer) socketBuffer).limit(),
this.toString() + " sending packet (" + ((Buffer) socketBuffer).limit() + " bytes)");
}
// Prepare for the next packet
if (!atEOM)
preparePacket();
// Finally, start sending data from the new socket buffer.
tdsChannel.write(socketBuffer.array(), ((Buffer) socketBuffer).position(), socketBuffer.remaining());
((Buffer) socketBuffer).position(((Buffer) 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 output 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 output 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 ? 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 output 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);
}
}
/**
* 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 output 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);
}
}
/**
* 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 output 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);
}
}
/**
* 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 output 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);
}
}
/**
* 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 output 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));
}
}
void writeRPCSqlVariant(String sName, SqlVariant sqlVariantValue, boolean bOut) throws SQLServerException {
writeRPCNameValType(sName, bOut, TDSType.SQL_VARIANT);
// Data and length
if (null == sqlVariantValue) {
writeInt(0); // max length
writeInt(0); // actual length
}
}
/**
* 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 output 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);
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 output 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 output 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 tdsWritterCached = false;
ByteBuffer cachedTVPHeaders = null;
TDSCommand cachedCommand = null;
boolean cachedRequestComplete = false;
boolean cachedInterruptsEnabled = false;
boolean cachedProcessedResponse = false;
if (!value.isNull()) {
// If the preparedStatement and the ResultSet are created by the same connection, and TVP is set with
// ResultSet and Server Cursor
// is used, the tdsWriter of the calling preparedStatement is overwritten by the SQLServerResultSet#next()
// method when fetching new rows.
// Therefore, we need to send TVP data row by row before fetching new row.
if (TVPType.ResultSet == value.tvpType) {
if ((null != value.sourceResultSet) && (value.sourceResultSet instanceof SQLServerResultSet)) {
SQLServerResultSet sourceResultSet = (SQLServerResultSet) value.sourceResultSet;
SQLServerStatement src_stmt = (SQLServerStatement) sourceResultSet.getStatement();
int resultSetServerCursorId = sourceResultSet.getServerCursorId();
if (con.equals(src_stmt.getConnection()) && 0 != resultSetServerCursorId) {
cachedTVPHeaders = ByteBuffer.allocate(stagingBuffer.capacity()).order(stagingBuffer.order());
cachedTVPHeaders.put(stagingBuffer.array(), 0, ((Buffer) stagingBuffer).position());
cachedCommand = this.command;
cachedRequestComplete = command.getRequestComplete();
cachedInterruptsEnabled = command.getInterruptsEnabled();
cachedProcessedResponse = command.getProcessedResponse();
tdsWritterCached = true;
if (sourceResultSet.isForwardOnly()) {
sourceResultSet.setFetchSize(1);
}
}
}
}
Map columnMetadata = value.getColumnMetadata();
Iterator> columnsIterator;
while (value.next()) {
// restore command and TDS header, which have been overwritten by value.next()
if (tdsWritterCached) {
command = cachedCommand;
((Buffer) stagingBuffer).clear();
((Buffer) logBuffer).clear();
writeBytes(cachedTVPHeaders.array(), 0, ((Buffer) cachedTVPHeaders).position());
}
List