in sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/implementation/mdesrc/cryptography/SqlSerializerUtil.java [1613:1935]
static final Object convertTemporalToObject(JDBCType jdbcType, SSType ssType, Calendar timeZoneCalendar,
int daysSinceBaseDate, long ticksSinceMidnight,
int fractionalSecondsScale) throws MicrosoftDataEncryptionException {
// In cases where a Calendar object (and therefore Timezone) is not passed to the method,
// use the path below instead to optimize performance.
if (null == timeZoneCalendar) {
return convertTemporalToObject(jdbcType, ssType, daysSinceBaseDate, ticksSinceMidnight,
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 = timeZoneCalendar.getTimeZone();
// 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;
// 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(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(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:
MessageFormat form = new MessageFormat(
MicrosoftDataEncryptionExceptionResource.getResource("R_UnexpectedSourceType"));
Object[] msgArgs = {ssType};
throw new MicrosoftDataEncryptionException(form.format(msgArgs));
}
int localMillisOffset = timeZoneCalendar.get(Calendar.ZONE_OFFSET);
// Convert the calendar value (in local time) to the desired Java object type.
switch (jdbcType.category) {
case BINARY:
case SQL_VARIANT: {
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 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(BASE_YEAR_1970, Calendar.JANUARY, 1);
return new java.sql.Time(cal.getTimeInMillis());
}
default:
MessageFormat form = new MessageFormat(
MicrosoftDataEncryptionExceptionResource.getResource("R_UnexpectedSourceType"));
Object[] msgArgs = {ssType};
throw new MicrosoftDataEncryptionException(form.format(msgArgs));
}
}
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(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);
if (jdbcType == JDBCType.LOCALDATETIME) {
return ts.toLocalDateTime();
}
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 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:
MessageFormat form = new MessageFormat(
MicrosoftDataEncryptionExceptionResource.getResource("R_UnexpectedSourceType"));
Object[] msgArgs = {ssType};
throw new MicrosoftDataEncryptionException(form.format(msgArgs));
}
}
default:
MessageFormat form = new MessageFormat(
MicrosoftDataEncryptionExceptionResource.getResource("R_UnexpectedTargetType"));
Object[] msgArgs = {jdbcType};
throw new MicrosoftDataEncryptionException(form.format(msgArgs));
}
}