static final Object convertTemporalToObject()

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));
        }
    }