/*
* 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 static java.nio.charset.StandardCharsets.US_ASCII;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.text.MessageFormat;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
/**
* Utility class for all Data Dependant Conversions (DDC).
*/
final class DDC {
/**
* Convert an Integer object to desired target user type.
*
* @param intvalue
* the value to convert.
* @param valueLength
* the value to convert.
* @param jdbcType
* the jdbc type required.
* @param streamType
* the type of stream required.
* @return the required object.
*/
static final Object convertIntegerToObject(int intValue,
int valueLength,
JDBCType jdbcType,
StreamType streamType) {
switch (jdbcType) {
case INTEGER:
return new Integer(intValue);
case SMALLINT: // 2.21 small and tinyint returned as short
case TINYINT:
return new Short((short) intValue);
case BIT:
case BOOLEAN:
return new Boolean(0 != intValue);
case BIGINT:
return new Long(intValue);
case DECIMAL:
case NUMERIC:
case MONEY:
case SMALLMONEY:
return new BigDecimal(Integer.toString(intValue));
case FLOAT:
case DOUBLE:
return new Double(intValue);
case REAL:
return new Float(intValue);
case BINARY:
return convertIntToBytes(intValue, valueLength);
default:
return Integer.toString(intValue);
}
}
/**
* Convert a Long object to desired target user type.
*
* @param longVal
* the value to convert.
* @param jdbcType
* the jdbc type required.
* @param baseSSType
* the base SQLServer type.
* @param streamType
* the stream type.
* @return the required object.
*/
static final Object convertLongToObject(long longVal,
JDBCType jdbcType,
SSType baseSSType,
StreamType streamType) {
switch (jdbcType) {
case BIGINT:
return new Long(longVal);
case INTEGER:
return new Integer((int) longVal);
case SMALLINT: // small and tinyint returned as short
case TINYINT:
return new Short((short) longVal);
case BIT:
case BOOLEAN:
return new Boolean(0 != longVal);
case DECIMAL:
case NUMERIC:
case MONEY:
case SMALLMONEY:
return new BigDecimal(Long.toString(longVal));
case FLOAT:
case DOUBLE:
return new Double(longVal);
case REAL:
return new Float(longVal);
case BINARY:
byte[] convertedBytes = convertLongToBytes(longVal);
int bytesToReturnLength = 0;
byte[] bytesToReturn;
switch (baseSSType) {
case BIT:
case TINYINT:
bytesToReturnLength = 1;
bytesToReturn = new byte[bytesToReturnLength];
System.arraycopy(convertedBytes, convertedBytes.length - bytesToReturnLength, bytesToReturn, 0, bytesToReturnLength);
return bytesToReturn;
case SMALLINT:
bytesToReturnLength = 2;
bytesToReturn = new byte[bytesToReturnLength];
System.arraycopy(convertedBytes, convertedBytes.length - bytesToReturnLength, bytesToReturn, 0, bytesToReturnLength);
return bytesToReturn;
case INTEGER:
bytesToReturnLength = 4;
bytesToReturn = new byte[bytesToReturnLength];
System.arraycopy(convertedBytes, convertedBytes.length - bytesToReturnLength, bytesToReturn, 0, bytesToReturnLength);
return bytesToReturn;
case BIGINT:
bytesToReturnLength = 8;
bytesToReturn = new byte[bytesToReturnLength];
System.arraycopy(convertedBytes, convertedBytes.length - bytesToReturnLength, bytesToReturn, 0, bytesToReturnLength);
return bytesToReturn;
default:
return convertedBytes;
}
case VARBINARY:
switch (baseSSType) {
case BIGINT:
return new Long(longVal);
case INTEGER:
return new Integer((int) longVal);
case SMALLINT: // small and tinyint returned as short
case TINYINT:
return new Short((short) longVal);
case BIT:
return new Boolean(0 != longVal);
case DECIMAL:
case NUMERIC:
case MONEY:
case SMALLMONEY:
return new BigDecimal(Long.toString(longVal));
case FLOAT:
return new Double(longVal);
case REAL:
return new Float(longVal);
case BINARY:
return convertLongToBytes(longVal);
default:
return Long.toString(longVal);
}
default:
return Long.toString(longVal);
}
}
/**
* Encodes an integer value to a byte array in big-endian order.
*
* @param intValue
* the integer value to encode.
* @param valueLength
* the number of bytes to encode.
* @return the byte array containing the big-endian encoded value.
*/
static final byte[] convertIntToBytes(int intValue,
int valueLength) {
byte bytes[] = new byte[valueLength];
for (int i = valueLength; i-- > 0;) {
bytes[i] = (byte) (intValue & 0xFF);
intValue >>= 8;
}
return bytes;
}
/**
* Convert a Float object to desired target user type.
*
* @param floatVal
* the value to convert.
* @param jdbcType
* the jdbc type required.
* @param streamType
* the stream type.
* @return the required object.
*/
static final Object convertFloatToObject(float floatVal,
JDBCType jdbcType,
StreamType streamType) {
switch (jdbcType) {
case REAL:
return new Float(floatVal);
case INTEGER:
return new Integer((int) floatVal);
case SMALLINT: // small and tinyint returned as short
case TINYINT:
return new Short((short) floatVal);
case BIT:
case BOOLEAN:
return new Boolean(0 != Float.compare(0.0f, floatVal));
case BIGINT:
return new Long((long) floatVal);
case DECIMAL:
case NUMERIC:
case MONEY:
case SMALLMONEY:
return new BigDecimal(Float.toString(floatVal));
case FLOAT:
case DOUBLE:
return new Double((new Float(floatVal)).doubleValue());
case BINARY:
return convertIntToBytes(Float.floatToRawIntBits(floatVal), 4);
default:
return Float.toString(floatVal);
}
}
/**
* Encodes a long value to a byte array in big-endian order.
*
* @param longValue
* the long value to encode.
* @return the byte array containing the big-endian encoded value.
*/
static final byte[] convertLongToBytes(long longValue) {
byte bytes[] = new byte[8];
for (int i = 8; i-- > 0;) {
bytes[i] = (byte) (longValue & 0xFF);
longValue >>= 8;
}
return bytes;
}
/**
* Convert a Double object to desired target user type.
*
* @param doubleVal
* the value to convert.
* @param jdbcType
* the jdbc type required.
* @param streamType
* the stream type.
* @return the required object.
*/
static final Object convertDoubleToObject(double doubleVal,
JDBCType jdbcType,
StreamType streamType) {
switch (jdbcType) {
case FLOAT:
case DOUBLE:
return new Double(doubleVal);
case REAL:
return new Float((new Double(doubleVal)).floatValue());
case INTEGER:
return new Integer((int) doubleVal);
case SMALLINT: // small and tinyint returned as short
case TINYINT:
return new Short((short) doubleVal);
case BIT:
case BOOLEAN:
return new Boolean(0 != Double.compare(0.0d, doubleVal));
case BIGINT:
return new Long((long) doubleVal);
case DECIMAL:
case NUMERIC:
case MONEY:
case SMALLMONEY:
return new BigDecimal(Double.toString(doubleVal));
case BINARY:
return convertLongToBytes(Double.doubleToRawLongBits(doubleVal));
default:
return Double.toString(doubleVal);
}
}
static final byte[] convertBigDecimalToBytes(BigDecimal bigDecimalVal,
int scale) {
byte[] valueBytes;
if (bigDecimalVal == null) {
valueBytes = new byte[2];
valueBytes[0] = (byte) scale;
valueBytes[1] = 0; // data length
}
else {
boolean isNegative = (bigDecimalVal.signum() < 0);
// NOTE: Handle negative scale as a special case for JDK 1.5 and later VMs.
if (bigDecimalVal.scale() < 0)
bigDecimalVal = bigDecimalVal.setScale(0);
BigInteger bi = bigDecimalVal.unscaledValue();
if (isNegative)
bi = bi.negate();
byte[] unscaledBytes = bi.toByteArray();
valueBytes = new byte[unscaledBytes.length + 3];
int j = 0;
valueBytes[j++] = (byte) bigDecimalVal.scale();
valueBytes[j++] = (byte) (unscaledBytes.length + 1); // data length + sign
valueBytes[j++] = (byte) (isNegative ? 0 : 1); // 1 = +ve, 0 = -ve
for (int i = unscaledBytes.length - 1; i >= 0; i--)
valueBytes[j++] = unscaledBytes[i];
}
return valueBytes;
}
/**
* Convert a BigDecimal object to desired target user type.
*
* @param bigDecimalVal
* the value to convert.
* @param jdbcType
* the jdbc type required.
* @param streamType
* the stream type.
* @return the required object.
*/
static final Object convertBigDecimalToObject(BigDecimal bigDecimalVal,
JDBCType jdbcType,
StreamType streamType) {
switch (jdbcType) {
case DECIMAL:
case NUMERIC:
case MONEY:
case SMALLMONEY:
return bigDecimalVal;
case FLOAT:
case DOUBLE:
return new Double(bigDecimalVal.doubleValue());
case REAL:
return new Float(bigDecimalVal.floatValue());
case INTEGER:
return new Integer(bigDecimalVal.intValue());
case SMALLINT: // small and tinyint returned as short
case TINYINT:
return new Short(bigDecimalVal.shortValue());
case BIT:
case BOOLEAN:
return new Boolean(0 != bigDecimalVal.compareTo(BigDecimal.valueOf(0)));
case BIGINT:
return new Long(bigDecimalVal.longValue());
case BINARY:
return convertBigDecimalToBytes(bigDecimalVal, bigDecimalVal.scale());
default:
return bigDecimalVal.toString();
}
}
/**
* Convert a Money object to desired target user type.
*
* @param bigDecimalVal
* the value to convert.
* @param jdbcType
* the jdbc type required.
* @param streamType
* the stream type.
* @param numberOfBytes
* the number of bytes to convert
* @return the required object.
*/
static final Object convertMoneyToObject(BigDecimal bigDecimalVal,
JDBCType jdbcType,
StreamType streamType,
int numberOfBytes) {
switch (jdbcType) {
case DECIMAL:
case NUMERIC:
case MONEY:
case SMALLMONEY:
return bigDecimalVal;
case FLOAT:
case DOUBLE:
return new Double(bigDecimalVal.doubleValue());
case REAL:
return new Float(bigDecimalVal.floatValue());
case INTEGER:
return new Integer(bigDecimalVal.intValue());
case SMALLINT: // small and tinyint returned as short
case TINYINT:
return new Short(bigDecimalVal.shortValue());
case BIT:
case BOOLEAN:
return new Boolean(0 != bigDecimalVal.compareTo(BigDecimal.valueOf(0)));
case BIGINT:
return new Long(bigDecimalVal.longValue());
case BINARY:
return convertToBytes(bigDecimalVal, bigDecimalVal.scale(), numberOfBytes);
default:
return bigDecimalVal.toString();
}
}
// converts big decimal to money and smallmoney
private static byte[] convertToBytes(BigDecimal value,
int scale,
int numBytes) {
boolean isNeg = value.signum() < 0;
value = value.setScale(scale);
BigInteger bigInt = value.unscaledValue();
byte[] unscaledBytes = bigInt.toByteArray();
byte[] ret = new byte[numBytes];
if (unscaledBytes.length < numBytes) {
for (int i = 0; i < numBytes - unscaledBytes.length; ++i) {
ret[i] = (byte) (isNeg ? -1 : 0);
}
}
int offset = numBytes - unscaledBytes.length;
for (int i = offset; i < numBytes; ++i) {
ret[i] = unscaledBytes[i - offset];
}
return ret;
}
/**
* Convert a byte array to desired target user type.
*
* @param bytesValue
* the value to convert.
* @param jdbcType
* the jdbc type required.
* @param baseTypeInfo
* the type information associated with bytesValue.
* @return the required object.
* @throws SQLServerException
* when an error occurs.
*/
static final Object convertBytesToObject(byte[] bytesValue,
JDBCType jdbcType,
TypeInfo baseTypeInfo) throws SQLServerException {
switch (jdbcType) {
case CHAR:
String str = Util.bytesToHexString(bytesValue, bytesValue.length);
if ((SSType.BINARY == baseTypeInfo.getSSType()) && (str.length() < (baseTypeInfo.getPrecision() * 2))) {
StringBuffer strbuf = new StringBuffer(str);
while (strbuf.length() < (baseTypeInfo.getPrecision() * 2)) {
strbuf.append('0');
}
return strbuf.toString();
}
return str;
case BINARY:
case VARBINARY:
case LONGVARBINARY:
if ((SSType.BINARY == baseTypeInfo.getSSType()) && (bytesValue.length < baseTypeInfo.getPrecision())) {
byte[] newBytes = new byte[baseTypeInfo.getPrecision()];
System.arraycopy(bytesValue, 0, newBytes, 0, bytesValue.length);
return newBytes;
}
return bytesValue;
default:
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_unsupportedConversionFromTo"));
throw new SQLServerException(form.format(new Object[] {baseTypeInfo.getSSType().name(), jdbcType}), null, 0, null);
}
}
/**
* Convert a String object to desired target user type.
*
* @param stringVal
* the value to convert.
* @param charset
* the character set.
* @param jdbcType
* the jdbc type required.
* @return the required object.
*/
static final Object convertStringToObject(String stringVal,
Charset charset,
JDBCType jdbcType,
StreamType streamType) throws UnsupportedEncodingException, IllegalArgumentException {
switch (jdbcType) {
// Convert String to Numeric types.
case DECIMAL:
case NUMERIC:
case MONEY:
case SMALLMONEY:
return new BigDecimal(stringVal.trim());
case FLOAT:
case DOUBLE:
return Double.valueOf(stringVal.trim());
case REAL:
return Float.valueOf(stringVal.trim());
case INTEGER:
return Integer.valueOf(stringVal.trim());
case SMALLINT: // small and tinyint returned as short
case TINYINT:
return Short.valueOf(stringVal.trim());
case BIT:
case BOOLEAN:
String trimmedString = stringVal.trim();
return (1 == trimmedString.length()) ? Boolean.valueOf('1' == trimmedString.charAt(0)) : Boolean.valueOf(trimmedString);
case BIGINT:
return Long.valueOf(stringVal.trim());
// Convert String to Temporal types.
case TIMESTAMP:
return java.sql.Timestamp.valueOf(stringVal.trim());
case DATE:
return java.sql.Date.valueOf(getDatePart(stringVal.trim()));
case TIME: {
// Accepted character formats for conversion to java.sql.Time are:
// hh:mm:ss[.nnnnnnnnn]
// YYYY-MM-DD hh:mm:ss[.nnnnnnnnn]
//
// To handle either of these formats:
// 1) Normalize and parse as a Timestamp
// 2) Round fractional seconds up to the nearest millisecond (max resolution of java.sql.Time)
// 3) Renormalize (as rounding may have changed the date) to a java.sql.Time
java.sql.Timestamp ts = java.sql.Timestamp.valueOf(TDS.BASE_DATE_1970 + " " + getTimePart(stringVal.trim()));
GregorianCalendar cal = new GregorianCalendar(Locale.US);
cal.clear();
cal.setTimeInMillis(ts.getTime());
if (ts.getNanos() % Nanos.PER_MILLISECOND >= Nanos.PER_MILLISECOND / 2)
cal.add(Calendar.MILLISECOND, 1);
cal.set(TDS.BASE_YEAR_1970, Calendar.JANUARY, 1);
return new java.sql.Time(cal.getTimeInMillis());
}
case BINARY:
return stringVal.getBytes(charset);
default:
// For everything else, just return either a string or appropriate stream.
switch (streamType) {
case CHARACTER:
return new StringReader(stringVal);
case ASCII:
return new ByteArrayInputStream(stringVal.getBytes(US_ASCII));
case BINARY:
return new ByteArrayInputStream(stringVal.getBytes());
default:
return stringVal;
}
}
}
static final Object convertStreamToObject(BaseInputStream stream,
TypeInfo typeInfo,
JDBCType jdbcType,
InputStreamGetterArgs getterArgs) throws SQLServerException {
// Need to handle the simple case of a null value here, as it is not done
// outside this function.
if (null == stream)
return null;
assert null != typeInfo;
assert null != getterArgs;
SSType ssType = typeInfo.getSSType();
try {
switch (jdbcType) {
case CHAR:
case VARCHAR:
case LONGVARCHAR:
case NCHAR:
case NVARCHAR:
case LONGNVARCHAR:
default:
// Binary streams to character types:
// - Direct conversion to ASCII stream
// - Convert as hexized value to other character types
if (SSType.BINARY == ssType || SSType.VARBINARY == ssType || SSType.VARBINARYMAX == ssType || SSType.TIMESTAMP == ssType
|| SSType.IMAGE == ssType || SSType.UDT == ssType) {
if (StreamType.ASCII == getterArgs.streamType) {
return stream;
}
else {
assert StreamType.CHARACTER == getterArgs.streamType || StreamType.NONE == getterArgs.streamType;
byte[] byteValue = stream.getBytes();
if (JDBCType.GUID == jdbcType) {
return Util.readGUID(byteValue);
}
else {
String hexString = Util.bytesToHexString(byteValue, byteValue.length);
if (StreamType.NONE == getterArgs.streamType)
return hexString;
return new StringReader(hexString);
}
}
}
// Handle streams converting to ASCII
if (StreamType.ASCII == getterArgs.streamType) {
// Fast path for SBCS data that converts directly/easily to ASCII
if (typeInfo.supportsFastAsciiConversion())
return new AsciiFilteredInputStream(stream);
// Slightly less fast path for MBCS data that converts directly/easily to ASCII
if (getterArgs.isAdaptive) {
return AsciiFilteredUnicodeInputStream.MakeAsciiFilteredUnicodeInputStream(stream,
new BufferedReader(new InputStreamReader(stream, typeInfo.getCharset())));
}
else {
return new ByteArrayInputStream((new String(stream.getBytes(), typeInfo.getCharset())).getBytes(US_ASCII));
}
}
else if (StreamType.CHARACTER == getterArgs.streamType || StreamType.NCHARACTER == getterArgs.streamType) {
if (getterArgs.isAdaptive)
return new BufferedReader(new InputStreamReader(stream, typeInfo.getCharset()));
else
return new StringReader(new String(stream.getBytes(), typeInfo.getCharset()));
}
// None of the special/fast textual conversion cases applied. Just go the normal route of converting via String.
return convertStringToObject(new String(stream.getBytes(), typeInfo.getCharset()), typeInfo.getCharset(), jdbcType,
getterArgs.streamType);
case CLOB:
return new SQLServerClob(stream, typeInfo);
case NCLOB:
return new SQLServerNClob(stream, typeInfo);
case SQLXML:
return new SQLServerSQLXML(stream, getterArgs, typeInfo);
case BINARY:
case VARBINARY:
case LONGVARBINARY:
case BLOB:
// Where allowed, streams convert directly to binary representation
if (StreamType.BINARY == getterArgs.streamType)
return stream;
if (JDBCType.BLOB == jdbcType)
return new SQLServerBlob(stream);
return stream.getBytes();
}
}
// Conversion can throw either of these exceptions:
//
// UnsupportedEncodingException (binary conversions)
// IllegalArgumentException (any conversion - note: numerics throw NumberFormatException subclass)
//
// Catch them and translate them to a SQLException so that we don't propagate an unexpected exception
// type all the way up to the app, which may not catch it either...
catch (IllegalArgumentException e) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorConvertingValue"));
throw new SQLServerException(form.format(new Object[] {typeInfo.getSSType(), jdbcType}), null, 0, e);
}
catch (UnsupportedEncodingException e) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorConvertingValue"));
throw new SQLServerException(form.format(new Object[] {typeInfo.getSSType(), jdbcType}), null, 0, e);
}
}
// Returns date portion of string.
// Expects one of "<date>" or "<date><space><time>".
private static String getDatePart(String s) {
int sp = s.indexOf(' ');
if (-1 == sp)
return s;
return s.substring(0, sp);
}
// Returns time portion of string.
// Expects one of "<time>" or "<date><space><time>".
private static String getTimePart(String s) {
int sp = s.indexOf(' ');
if (-1 == sp)
return s;
return s.substring(sp + 1);
}
// Formats nanoseconds as a String of the form ".nnnnnnn...." where the number
// of digits is equal to the scale. Returns the empty string for scale = 0;
private static String fractionalSecondsString(long subSecondNanos,
int scale) {
assert 0 <= subSecondNanos && subSecondNanos < Nanos.PER_SECOND;
assert 0 <= scale && scale <= TDS.MAX_FRACTIONAL_SECONDS_SCALE;
// Fast path for 0 scale (avoids creation of two BigDecimal objects and
// two Strings when the answer is going to be "" anyway...)
if (0 == scale)
return "";
return java.math.BigDecimal.valueOf(subSecondNanos % Nanos.PER_SECOND, 9).setScale(scale).toPlainString().substring(1);
}
/**
* Convert a SQL Server temporal value to the desired Java object type.
*
* Accepted SQL server data types:
*
* DATETIME SMALLDATETIME DATE TIME DATETIME2 DATETIMEOFFSET
*
* Converts to Java types (determined by JDBC type):
*
* java.sql.Date java.sql.Time java.sql.Timestamp java.lang.String
*
* @param jdbcType
* the JDBC type indicating the desired conversion
*
* @param ssType
* the SQL Server data type of the value being converted
*
* @param timeZoneCalendar
* (optional) a Calendar representing the time zone to associate with the resulting converted value. For DATETIMEOFFSET, this parameter
* represents the time zone associated with the value. Null means to use the default VM time zone.
*
* @param daysSinceBaseDate
* The date part of the value, expressed as a number of days since the base date for the specified SQL Server data type. For DATETIME
* and SMALLDATETIME, the base date is 1/1/1900. For other types, the base date is 1/1/0001. The number of days assumes Gregorian leap
* year behavior over the entire supported range of values. For TIME values, this parameter must be the number of days between 1/1/0001
* and 1/1/1900 when converting to java.sql.Timestamp.
*
* @param ticksSinceMidnight
* The time part of the value, expressed as a number of time units (ticks) since midnight. For DATETIME and SMALLDATETIME SQL Server
* data types, time units are in milliseconds. For other types, time units are in nanoseconds. For DATE values, this parameter must be
* 0.
*
* @param fractionalSecondsScale
* the desired fractional seconds scale to use when formatting the value as a String. Ignored for conversions to Java types other than
* String.
*
* @return a Java object of the desired type.
*/
static final Object convertTemporalToObject(JDBCType jdbcType,
SSType ssType,
Calendar timeZoneCalendar,
int daysSinceBaseDate,
long ticksSinceMidnight,
int fractionalSecondsScale) {
// Determine the local time zone to associate with the value. Use the default VM
// time zone if no time zone is otherwise specified.
TimeZone localTimeZone = (null != timeZoneCalendar) ? timeZoneCalendar.getTimeZone() : TimeZone.getDefault();
// Assumed time zone associated with the date and time parts of the value.
//
// For DATETIMEOFFSET, the date and time parts are assumed to be relative to UTC.
// For other data types, the date and time parts are assumed to be relative to the local time zone.
TimeZone componentTimeZone = (SSType.DATETIMEOFFSET == ssType) ? UTC.timeZone : localTimeZone;
int subSecondNanos = 0;
// The date and time parts assume a Gregorian calendar with Gregorian leap year behavior
// over the entire supported range of values. Create and initialize such a calendar to
// use to interpret the date and time parts in their associated time zone.
GregorianCalendar cal = new GregorianCalendar(componentTimeZone, Locale.US);
// Allow overflow in "smaller" fields (such as MILLISECOND and DAY_OF_YEAR) to update
// "larger" fields (such as HOUR, MINUTE, SECOND, and YEAR, MONTH, DATE).
cal.setLenient(true);
// Clear old state from the calendar. Newly created calendars are always initialized to the
// current date and time.
cal.clear();
// Set the calendar value according to the specified local time zone and constituent
// date (days since base date) and time (ticks since midnight) parts.
switch (ssType) {
case TIME: {
// Set the calendar to the specified value. Lenient calendar behavior will update
// individual fields according to standard Gregorian leap year rules, which are sufficient
// for all TIME values.
//
// When initializing the value, set the date component to 1/1/1900 to facilitate conversion
// to String and java.sql.Timestamp. Note that conversion to java.sql.Time, which is
// the expected majority conversion, resets the date to 1/1/1970. It is not measurably
// faster to conditionalize the date on the target data type to avoid resetting it.
//
// Ticks are in nanoseconds.
cal.set(TDS.BASE_YEAR_1900, Calendar.JANUARY, 1, 0, 0, 0);
cal.set(Calendar.MILLISECOND, (int) (ticksSinceMidnight / Nanos.PER_MILLISECOND));
subSecondNanos = (int) (ticksSinceMidnight % Nanos.PER_SECOND);
break;
}
case DATE:
case DATETIME2:
case DATETIMEOFFSET: {
// For dates after the standard Julian-Gregorian calendar change date,
// the calendar value can be accurately set using a straightforward
// (and measurably better performing) assignment.
//
// This optimized path is not functionally correct for dates earlier
// than the standard Gregorian change date.
if (daysSinceBaseDate >= GregorianChange.DAYS_SINCE_BASE_DATE_HINT) {
// Set the calendar to the specified value. Lenient calendar behavior will update
// individual fields according to pure Gregorian calendar rules.
//
// Ticks are in nanoseconds.
cal.set(1, Calendar.JANUARY, 1 + daysSinceBaseDate + GregorianChange.EXTRA_DAYS_TO_BE_ADDED, 0, 0, 0);
cal.set(Calendar.MILLISECOND, (int) (ticksSinceMidnight / Nanos.PER_MILLISECOND));
}
// For dates before the standard change date, it is necessary to rationalize
// the difference between SQL Server (pure Gregorian) calendar behavior and
// Java (standard Gregorian) calendar behavior. Rationalization ensures that
// the "wall calendar" representation of the value on both server and client
// are the same, taking into account the difference in the respective calendars'
// leap year rules.
//
// This code path is functionally correct, but less performant, than the
// optimized path above for dates after the standard Gregorian change date.
else {
cal.setGregorianChange(GregorianChange.PURE_CHANGE_DATE);
// Set the calendar to the specified value. Lenient calendar behavior will update
// individual fields according to pure Gregorian calendar rules.
//
// Ticks are in nanoseconds.
cal.set(1, Calendar.JANUARY, 1 + daysSinceBaseDate, 0, 0, 0);
cal.set(Calendar.MILLISECOND, (int) (ticksSinceMidnight / Nanos.PER_MILLISECOND));
// Recompute the calendar's internal UTC milliseconds value according to the historically
// standard Gregorian cutover date, which is needed for constructing java.sql.Time,
// java.sql.Date, and java.sql.Timestamp values from UTC milliseconds.
int year = cal.get(Calendar.YEAR);
int month = cal.get(Calendar.MONTH);
int date = cal.get(Calendar.DATE);
int hour = cal.get(Calendar.HOUR_OF_DAY);
int minute = cal.get(Calendar.MINUTE);
int second = cal.get(Calendar.SECOND);
int millis = cal.get(Calendar.MILLISECOND);
cal.setGregorianChange(GregorianChange.STANDARD_CHANGE_DATE);
cal.set(year, month, date, hour, minute, second);
cal.set(Calendar.MILLISECOND, millis);
}
// For DATETIMEOFFSET values, recompute the calendar's UTC milliseconds value according
// to the specified local time zone (the time zone associated with the offset part
// of the DATETIMEOFFSET value).
//
// Optimization: Skip this step if there is no time zone difference
// (i.e. the time zone of the DATETIMEOFFSET value is UTC).
if (SSType.DATETIMEOFFSET == ssType && !componentTimeZone.hasSameRules(localTimeZone)) {
GregorianCalendar localCalendar = new GregorianCalendar(localTimeZone, Locale.US);
localCalendar.clear();
localCalendar.setTimeInMillis(cal.getTimeInMillis());
cal = localCalendar;
}
subSecondNanos = (int) (ticksSinceMidnight % Nanos.PER_SECOND);
break;
}
case DATETIME: // and SMALLDATETIME
{
// For Yukon (and earlier) data types DATETIME and SMALLDATETIME, there is no need to
// change the Gregorian cutover because the earliest representable value (1/1/1753)
// is after the historically standard cutover date (10/15/1582).
// Set the calendar to the specified value. Lenient calendar behavior will update
// individual fields according to standard Gregorian leap year rules, which are sufficient
// for all values in the supported DATETIME range.
//
// Ticks are in milliseconds.
cal.set(TDS.BASE_YEAR_1900, Calendar.JANUARY, 1 + daysSinceBaseDate, 0, 0, 0);
cal.set(Calendar.MILLISECOND, (int) ticksSinceMidnight);
subSecondNanos = (int) ((ticksSinceMidnight * Nanos.PER_MILLISECOND) % Nanos.PER_SECOND);
break;
}
default:
throw new AssertionError("Unexpected SSType: " + ssType);
}
int localMillisOffset = 0;
if (null == timeZoneCalendar) {
TimeZone tz = TimeZone.getDefault();
GregorianCalendar _cal = new GregorianCalendar(componentTimeZone, Locale.US);
_cal.setLenient(true);
_cal.clear();
localMillisOffset = tz.getOffset(_cal.getTimeInMillis());
}
else {
localMillisOffset = timeZoneCalendar.get(Calendar.ZONE_OFFSET);
}
// Convert the calendar value (in local time) to the desired Java object type.
switch (jdbcType.category) {
case BINARY: {
switch (ssType) {
case DATE: {
// Per JDBC spec, the time part of java.sql.Date values is initialized to midnight
// in the specified local time zone.
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
return new java.sql.Date(cal.getTimeInMillis());
}
case DATETIME:
case DATETIME2: {
java.sql.Timestamp ts = new java.sql.Timestamp(cal.getTimeInMillis());
ts.setNanos(subSecondNanos);
return ts;
}
case DATETIMEOFFSET: {
// Per driver spec, conversion to DateTimeOffset is only supported from
// DATETIMEOFFSET SQL Server values.
assert SSType.DATETIMEOFFSET == ssType;
// For DATETIMEOFFSET SQL Server values, the time zone offset is in minutes.
// The offset from Java TimeZone objects is in milliseconds. Because we
// are only dealing with DATETIMEOFFSET SQL Server values here, we can assume
// that the offset is precise only to the minute and that rescaling from
// milliseconds precision results in no loss of precision.
assert 0 == localMillisOffset % (60 * 1000);
java.sql.Timestamp ts = new java.sql.Timestamp(cal.getTimeInMillis());
ts.setNanos(subSecondNanos);
return microsoft.sql.DateTimeOffset.valueOf(ts, localMillisOffset / (60 * 1000));
}
case TIME: {
// Per driver spec, values of sql server data types types (including TIME) which have greater
// than millisecond precision are rounded, not truncated, to the nearest millisecond when
// converting to java.sql.Time. Since the milliseconds value in the calendar is truncated,
// round it now.
if (subSecondNanos % Nanos.PER_MILLISECOND >= Nanos.PER_MILLISECOND / 2)
cal.add(Calendar.MILLISECOND, 1);
// Per JDBC spec, the date part of java.sql.Time values is initialized to 1/1/1970
// in the specified local time zone. This must be done after rounding (above) to
// prevent rounding values within nanoseconds of the next day from ending up normalized
// to 1/2/1970 instead...
cal.set(TDS.BASE_YEAR_1970, Calendar.JANUARY, 1);
return new java.sql.Time(cal.getTimeInMillis());
}
default:
throw new AssertionError("Unexpected SSType: " + ssType);
}
}
case DATE: {
// Per JDBC spec, the time part of java.sql.Date values is initialized to midnight
// in the specified local time zone.
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
return new java.sql.Date(cal.getTimeInMillis());
}
case TIME: {
// Per driver spec, values of sql server data types types (including TIME) which have greater
// than millisecond precision are rounded, not truncated, to the nearest millisecond when
// converting to java.sql.Time. Since the milliseconds value in the calendar is truncated,
// round it now.
if (subSecondNanos % Nanos.PER_MILLISECOND >= Nanos.PER_MILLISECOND / 2)
cal.add(Calendar.MILLISECOND, 1);
// Per JDBC spec, the date part of java.sql.Time values is initialized to 1/1/1970
// in the specified local time zone. This must be done after rounding (above) to
// prevent rounding values within nanoseconds of the next day from ending up normalized
// to 1/2/1970 instead...
cal.set(TDS.BASE_YEAR_1970, Calendar.JANUARY, 1);
return new java.sql.Time(cal.getTimeInMillis());
}
case TIMESTAMP: {
java.sql.Timestamp ts = new java.sql.Timestamp(cal.getTimeInMillis());
ts.setNanos(subSecondNanos);
return ts;
}
case DATETIMEOFFSET: {
// Per driver spec, conversion to DateTimeOffset is only supported from
// DATETIMEOFFSET SQL Server values.
assert SSType.DATETIMEOFFSET == ssType;
// For DATETIMEOFFSET SQL Server values, the time zone offset is in minutes.
// The offset from Java TimeZone objects is in milliseconds. Because we
// are only dealing with DATETIMEOFFSET SQL Server values here, we can assume
// that the offset is precise only to the minute and that rescaling from
// milliseconds precision results in no loss of precision.
assert 0 == localMillisOffset % (60 * 1000);
java.sql.Timestamp ts = new java.sql.Timestamp(cal.getTimeInMillis());
ts.setNanos(subSecondNanos);
return microsoft.sql.DateTimeOffset.valueOf(ts, localMillisOffset / (60 * 1000));
}
case CHARACTER: {
switch (ssType) {
case DATE: {
return String.format(Locale.US, "%1$tF", // yyyy-mm-dd
cal);
}
case TIME: {
return String.format(Locale.US, "%1$tT%2$s", // hh:mm:ss[.nnnnnnn]
cal, fractionalSecondsString(subSecondNanos, fractionalSecondsScale));
}
case DATETIME2: {
return String.format(Locale.US, "%1$tF %1$tT%2$s", // yyyy-mm-dd hh:mm:ss[.nnnnnnn]
cal, fractionalSecondsString(subSecondNanos, fractionalSecondsScale));
}
case DATETIMEOFFSET: {
// The offset part of a DATETIMEOFFSET value is precise only to the minute,
// but TimeZone returns the raw offset as precise to the millisecond.
assert 0 == localMillisOffset % (60 * 1000);
int unsignedMinutesOffset = Math.abs(localMillisOffset / (60 * 1000));
return String.format(Locale.US, "%1$tF %1$tT%2$s %3$c%4$02d:%5$02d", // yyyy-mm-dd hh:mm:ss[.nnnnnnn] [+|-]hh:mm
cal, fractionalSecondsString(subSecondNanos, fractionalSecondsScale), (localMillisOffset >= 0) ? '+' : '-',
unsignedMinutesOffset / 60, unsignedMinutesOffset % 60);
}
case DATETIME: // and SMALLDATETIME
{
return (new java.sql.Timestamp(cal.getTimeInMillis())).toString();
}
default:
throw new AssertionError("Unexpected SSType: " + ssType);
}
}
default:
throw new AssertionError("Unexpected JDBCType: " + jdbcType);
}
}
/**
* Returns the number of days elapsed from January 1 of the specified baseYear (Gregorian) to the specified dayOfYear in the specified year,
* assuming pure Gregorian calendar rules (no Julian to Gregorian cutover).
*/
static int daysSinceBaseDate(int year,
int dayOfYear,
int baseYear) {
assert year >= 1;
assert baseYear >= 1;
assert dayOfYear >= 1;
return (dayOfYear - 1) + // Days into the current year
(year - baseYear) * TDS.DAYS_PER_YEAR + // plus whole years (in days) ...
leapDaysBeforeYear(year) - // ... plus leap days
leapDaysBeforeYear(baseYear);
}
/**
* Returns the number of leap days that have occurred between January 1, 1AD and January 1 of the specified year, assuming a Proleptic Gregorian
* Calendar
*/
private static int leapDaysBeforeYear(int year) {
assert year >= 1;
// On leap years, the US Naval Observatory says:
// "According to the Gregorian calendar, which is the civil calendar
// in use today, years evenly divisible by 4 are leap years, with
// the exception of centurial years that are not evenly divisible
// by 400. Therefore, the years 1700, 1800, 1900 and 2100 are not
// leap years, but 1600, 2000, and 2400 are leap years."
//
// So, using year 1AD as a base, we can compute the number of leap
// days between 1AD and the specified year as follows:
return (year - 1) / 4 - (year - 1) / 100 + (year - 1) / 400;
}
// Maximum allowed RPC decimal value (raw integer value with scale removed).
// This limits the value to 38 digits of precision for SQL.
private final static BigInteger maxRPCDecimalValue = new BigInteger("99999999999999999999999999999999999999");
// Returns true if input bigDecimalValue exceeds allowable
// TDS wire format precision or scale for DECIMAL TDS token.
static final boolean exceedsMaxRPCDecimalPrecisionOrScale(BigDecimal bigDecimalValue) {
if (null == bigDecimalValue)
return false;
// Maximum scale allowed is same as maximum precision allowed.
if (bigDecimalValue.scale() > SQLServerConnection.maxDecimalPrecision)
return true;
// Convert to unscaled integer value, then compare with maxRPCDecimalValue.
// NOTE: Handle negative scale as a special case for JDK 1.5 and later VMs.
BigInteger bi = (bigDecimalValue.scale() < 0) ? bigDecimalValue.setScale(0).unscaledValue() : bigDecimalValue.unscaledValue();
if (bigDecimalValue.signum() < 0)
bi = bi.negate();
return (bi.compareTo(maxRPCDecimalValue) > 0);
}
// Converts a Reader to a String.
static String convertReaderToString(Reader reader,
int readerLength) throws SQLServerException {
assert DataTypes.UNKNOWN_STREAM_LENGTH == readerLength || readerLength >= 0;
// Optimize simple cases.
if (null == reader)
return null;
if (0 == readerLength)
return "";
try {
// Set up a StringBuilder big enough to hold the Reader value. If we weren't told the size of
// the value then start with a "reasonable" guess StringBuilder size. If necessary, the StringBuilder
// will grow automatically to accomodate arbitrary amounts of data.
StringBuilder sb = new StringBuilder((DataTypes.UNKNOWN_STREAM_LENGTH != readerLength) ? readerLength : 4000);
// Set up the buffer into which blocks of characters are read from the Reader. This buffer
// should be no larger than the Reader value's size (if known). For known very large values,
// limit the buffer's size to reduce this function's memory requirements.
char charArray[] = new char[(DataTypes.UNKNOWN_STREAM_LENGTH != readerLength && readerLength < 4000) ? readerLength : 4000];
// Loop and read characters, chunk into StringBuilder until EOS.
int readChars;
while ((readChars = reader.read(charArray, 0, charArray.length)) > 0) {
// Check for invalid bytesRead returned from InputStream.read
if (readChars > charArray.length) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorReadingStream"));
Object[] msgArgs = {SQLServerException.getErrString("R_streamReadReturnedInvalidValue")};
SQLServerException.makeFromDriverError(null, null, form.format(msgArgs), "", true);
}
sb.append(charArray, 0, readChars);
}
return sb.toString();
}
catch (IOException ioEx) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorReadingStream"));
Object[] msgArgs = {ioEx.toString()};
SQLServerException.makeFromDriverError(null, null, form.format(msgArgs), "", true);
}
// Unreachable code, but needed for compiler.
return null;
}
}
/**
* InputStream implementation that wraps a contained InputStream, filtering it for 7-bit ASCII characters.
*
* The wrapped input stream must supply byte values from a SBCS character set whose first 128 entries match the 7-bit US-ASCII character set. Values
* that lie outside of the 7-bit US-ASCII range are translated to the '?' character.
*/
final class AsciiFilteredInputStream extends InputStream {
private final InputStream containedStream;
private final static byte[] ASCII_FILTER;
static {
ASCII_FILTER = new byte[256];
// First 128 entries map ASCII values in to ASCII values out
for (int i = 0; i < 128; i++)
ASCII_FILTER[i] = (byte) i;
// Remaining 128 filter entries map other values to '?'
for (int i = 128; i < 256; i++)
ASCII_FILTER[i] = (byte) '?';
}
AsciiFilteredInputStream(BaseInputStream containedStream) throws SQLServerException {
if (BaseInputStream.logger.isLoggable(java.util.logging.Level.FINER))
BaseInputStream.logger.finer(containedStream.toString() + " wrapping in AsciiFilteredInputStream");
this.containedStream = containedStream;
}
public void close() throws IOException {
containedStream.close();
}
public long skip(long n) throws IOException {
return containedStream.skip(n);
}
public int available() throws IOException {
return containedStream.available();
}
public int read() throws IOException {
int value = containedStream.read();
if (value >= 0 && value <= 255)
return ASCII_FILTER[value];
return value;
}
public int read(byte[] b) throws IOException {
int bytesRead = containedStream.read(b);
if (bytesRead > 0) {
assert bytesRead <= b.length;
for (int i = 0; i < bytesRead; i++)
b[i] = ASCII_FILTER[b[i] & 0xFF];
}
return bytesRead;
}
public int read(byte b[],
int offset,
int maxBytes) throws IOException {
int bytesRead = containedStream.read(b, offset, maxBytes);
if (bytesRead > 0) {
assert offset + bytesRead <= b.length;
for (int i = 0; i < bytesRead; i++)
b[offset + i] = ASCII_FILTER[b[offset + i] & 0xFF];
}
return bytesRead;
}
public boolean markSupported() {
return containedStream.markSupported();
}
public void mark(int readLimit) {
containedStream.mark(readLimit);
}
public void reset() throws IOException {
containedStream.reset();
}
}
/**
* InputStream implementation that wraps a contained InputStream, filtering it for 7-bit ASCII characters from UNICODE.
*
* The wrapped input stream must supply byte values from a UNICODE character set.
*
*/
final class AsciiFilteredUnicodeInputStream extends InputStream {
private final Reader containedReader;
private final Charset asciiCharSet;
static AsciiFilteredUnicodeInputStream MakeAsciiFilteredUnicodeInputStream(BaseInputStream strm,
Reader rd) throws SQLServerException {
if (BaseInputStream.logger.isLoggable(java.util.logging.Level.FINER))
BaseInputStream.logger.finer(strm.toString() + " wrapping in AsciiFilteredInputStream");
return new AsciiFilteredUnicodeInputStream(rd);
}
// Note the Reader provided should support mark, reset
private AsciiFilteredUnicodeInputStream(Reader rd) throws SQLServerException {
containedReader = rd;
asciiCharSet = US_ASCII;
}
public void close() throws IOException {
containedReader.close();
}
public long skip(long n) throws IOException {
return containedReader.skip(n);
}
public int available() throws IOException {
// from the JDBC spec
// Note: A stream may return 0 when the method InputStream.available is called whether there is data available or not.
// Reader does not give us available data.
return 0;
}
private final byte[] bSingleByte = new byte[1];
public int read() throws IOException {
int bytesRead = read(bSingleByte);
return (-1 == bytesRead) ? -1 : (bSingleByte[0] & 0xFF);
}
public int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}
public int read(byte b[],
int offset,
int maxBytes) throws IOException {
char tempBufferToHoldCharDataForConversion[] = new char[maxBytes];
int charsRead = containedReader.read(tempBufferToHoldCharDataForConversion);
if (charsRead > 0) {
if (charsRead < maxBytes)
maxBytes = charsRead;
ByteBuffer encodedBuff = asciiCharSet.encode(CharBuffer.wrap(tempBufferToHoldCharDataForConversion));
encodedBuff.get(b, offset, maxBytes);
}
return charsRead;
}
public boolean markSupported() {
return containedReader.markSupported();
}
public void mark(int readLimit) {
try {
containedReader.mark(readLimit);
}
catch (IOException e) {
// unfortunately inputstream mark does not throw an exception so we have to eat any exception from the reader here
// likely to be a bug in the original InputStream spec.
return;
}
}
public void reset() throws IOException {
containedReader.reset();
}
}
// Helper class to hold + pass around stream/reader setter arguments.
final class StreamSetterArgs {
private long length;
final long getLength() {
return length;
}
final void setLength(long newLength) {
// We only expect length to be changed from an initial unknown value (-1)
// to an actual length (+ve or 0).
assert DataTypes.UNKNOWN_STREAM_LENGTH == length;
assert newLength >= 0;
length = newLength;
}
final StreamType streamType;
StreamSetterArgs(StreamType streamType,
long length) {
this.streamType = streamType;
this.length = length;
}
}
// Helper class to hold + pass around InputStream getter arguments.
final class InputStreamGetterArgs {
final StreamType streamType;
final boolean isAdaptive;
final boolean isStreaming;
final String logContext;
static final InputStreamGetterArgs defaultArgs = new InputStreamGetterArgs(StreamType.NONE, false, false, "");
static final InputStreamGetterArgs getDefaultArgs() {
return defaultArgs;
}
InputStreamGetterArgs(StreamType streamType,
boolean isAdaptive,
boolean isStreaming,
String logContext) {
this.streamType = streamType;
this.isAdaptive = isAdaptive;
this.isStreaming = isStreaming;
this.logContext = logContext;
}
}