Carlene12War
Carlene12War

Reputation: 37

Different date in local and the server java date time

When I run below code which converts a xml date to java date, it prints different value(CST) in local vs different value(UTC) in the server running the same code, what could be reason for it? How can I always print this date in CST format which I am getting in my local. I can't use latest java date API and have to use XML based date.

    import javax.xml.datatype.DatatypeConstants;
import javax.xml.datatype.XMLGregorianCalendar;
import java.util.Date;
import java.util.TimeZone;

import javax.xml.datatype.DatatypeConstants;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;
import java.util.Date;
import java.util.TimeZone;

public class Demo {
    public static final TimeZone TIMEZONE = TimeZone.getTimeZone("America/Chicago");

    public static Date convertDate(XMLGregorianCalendar value) {
        if (value == null) {
            return null;
        } else {
            if (value.getXMLSchemaType() == DatatypeConstants.DATE) {
                return value.toGregorianCalendar().getTime();
            } else {
                return value.toGregorianCalendar(TIMEZONE, null, null).getTime();
            }
        }
    }

    public static void main(String[] args) {
        try {
            // Parse the XML date string into XMLGregorianCalendar
            XMLGregorianCalendar xmlDate = DatatypeFactory.newInstance().newXMLGregorianCalendar("2024-02-09T01:57:33.240-06:00");
            // Convert XMLGregorianCalendar to Date
            Date date = convertDate(xmlDate);
            // Print the result
            System.out.println("Date: " + date);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Date: Fri Feb 09 01:57:33 CST 2024 - local output(Intellij - my mac time zone CST)

Date: Fri Feb 09 07:57:33 UTC 2024 - server output(https://www.jdoodle.com/online-java-compiler)

I need to store the date in america/chicago timezone irrespective of the user location and I have no control over the database query

Upvotes: 1

Views: 874

Answers (3)

Anonymous
Anonymous

Reputation: 86139

Use timestamp with time zone in UTC in your database

For a point in time in your database use UTC, universal time. And to make sure that your database knows that it is in UTC, use timestamp with time zone data type.

You may think that storing time in your local time zone, America/Chicago, is simpler and more suited to your purpose. Think again. While your app is designed for that time zone at the moment, who can guarantee that it won’t be used by users in other locations of the US at some point in the future? That the component you’re coding won’t be reused in some other program some other time? If this is for a school project to be thrown out after exams, a school project is a very good place to train best practices. Best practice here is to store UTC time.

If furthermore using timestamp without time zone or datetime in the database, a possibly minor problem is that times in America/Chicago time zone are occasionally ambiguous. Every year in the beginning of November summer time (DST) ends and the clocks are turned an hour back. Last 5 November at 2 AM, for example, clocks were turned back to 1 even though it had already been 1 AM an hour earlier. So when you retrieve a time between 1 and 2 in the night from the database, you have no way of knowing whether summer time (CDT) or standard time (CST) was intended. Also sorting such times is bound to mix them between each other. UTC does not use summer time, hence hasn’t got this problem.

Different RDBMSs have different notions of timestamp with time zone. Your DB2 stores a UTC offset with the timestamp. Others store time in UTC and when necessary convert to and from some local time zone on storing and retrieving, really only giving us "timestamp with UTC time zone", which is fine too. I suggest you always store offset 0 (zero). Yet a few allow you to store a time zone ID such as America/Chicago. Use Etc/UTC here. In all cases the type is fine for your purpose.

java.time

I understand that your requirement to store time in America/Chicago time zone is fixed. So store an OffsetDateTime with the correct UTC offset (-6 or -5 hours) into your DB2 timestamp with time zone column. If timestamp without time zone (or datetime in other RDBMSs) is dictated from others, store a LocalDateTime. First let’s get the conversion from XMLGregorianCalendar right:

public static final ZoneId ZONE_ID = ZoneId.of("America/Chicago");

/**
 * For this code example I am assuming that a date without time is stored as the start of day
 * and a date with time requires a UTC offset.
 * In both cases I am converting to America/Chicago time zone.
 */
public static OffsetDateTime convertXmlgcToOdt(XMLGregorianCalendar value) {
    if (value.getHour() == DatatypeConstants.FIELD_UNDEFINED
            && value.getMinute() == DatatypeConstants.FIELD_UNDEFINED
            && value.getSecond() == DatatypeConstants.FIELD_UNDEFINED
            && value.getFractionalSecond() == null) { // Date only
        return value.toGregorianCalendar()
                .toZonedDateTime()
                .toLocalDate()
                .atStartOfDay(ZONE_ID)
                .toOffsetDateTime();
    } else { // date and time
        if (value.getTimezone() == DatatypeConstants.FIELD_UNDEFINED) {
            throw new IllegalArgumentException("value must have UTC offset (\"timezone\")");
        }
        return value.toGregorianCalendar()
                .toZonedDateTime()
                .withZoneSameInstant(ZONE_ID)
                .toOffsetDateTime();
    }
}

Caveat: My conversion has milliseconds precision even though both XMLGregorianCalendar and OffsetDateTime have finer precision. If you need to preserve any finer precision than milliseconds from XML, I suggest:

        OffsetDateTime converted = OffsetDateTime.parse(value.toString());
        return converted.atZoneSameInstant(ZONE_ID)
                .toOffsetDateTime();

Let’s try the conversion method out:

    XMLGregorianCalendar xmlDate = DatatypeFactory.newInstance()
            .newXMLGregorianCalendar("2024-02-09T01:57:33.240-06:00");
    System.out.println(convertXmlgcToOdt(xmlDate));
    xmlDate = DatatypeFactory.newInstance()
            .newXMLGregorianCalendar("2024-02-09");
    System.out.println(convertXmlgcToOdt(xmlDate));

Output:

2024-02-09T01:57:33.240-06:00
2024-02-09T00:00-06:00

If instead you need to convert to LocalDateTime, replace each of the two lines calling toOffsetDeteTime() with .toLocalDateTime();.

To store in your database use something like the following, assuming you are using JDBC:

    OffsetDateTime odt = convertXmlgcToOdt(xmlDate);
    PreparedStatement pStmt = yourDatabaseConnection
            .prepareStatement("insert into your_table(your_tstz_column) values(?);");
    pStmt.setObject(1, odt);
    pStmt.executeUpdate();

If using timestamp wthout time zone and therefore LocalDateTime, just pass your LocalDateTime to setObject() in exactly the same way.

For really old Java versions

If using Java 6 or 7, use the backport of java.time named ThreeTen Backport, link at the bottom. In this case you can’t directly pass the java.time objects to your PreparedStatement. One option is to convert to an old-fashioned java.sql.Timestamp and pass it to PreparedStatement.setTimestmap(). The Timestamp class was a true hack on top of the already poorly designed Date class. Another optoin is to format into strings that you pass to PreparedStatement.setString(). If using Java 5, use Joda-Time in a similar manner.

Control the time zone via the database session

If for some reason I can’t guess you are forced to use one of the problematic and outdated Date classes, use the conversion you already have. Configure your database session to use America/Chicago time zone. I don’t understand the logic in this configuration belonging to the session, to me it rather belongs to the database column; but most SQL databases offer it as a setting on the session. Search for how it’s done with your brand of database engine/RDBMS. I include a link for DB2 below to get you started. The DB2 “session time zone” is a mere offset from UTC, so when dealing with times in both standard time and summer time, you will need to set it for each value stored.

Or via the JVM default time zone: The really dirty solution is to set the default time zone of your JVM independent of the server OS time zone. Beware that this will affect the entire Java program and may have unwanted effects in other parts of it. I expect it to control the time zone used to converting Date to either of the database timestamp data types, though (I have not tested).

Either start your server program with a command line option for the purpose, like for example:

java -Duser.timezone=America/Chicago YourApp

Or set the property from within your code, before doing any date and time work:

    System.setProperty("user.timezone", "America/Chicago");

If all else fails, tell a lie

If none of the above is viable in your setup, you need a way to compensate for the fact that your server JVM and your database disagree about time zone and the Date is therefore converted incorrectly when stored. The trick that I think you used in your own answer seems to work, only please be aware that we are doing a deliberately incorrect conversion to compensate for the opposite incorrect conversion taking place out of our control when storing the Date. If that were me, I would want to make this very clear in the code, or anyone reading it in the future may be severely confused and may also try to “correct” what they may take to be an unintentional error. You can use your own conversion or mine or mix them somehow. Here’s mine:

/**
 * Convert to America/Chicago time zone
 * and then convert further to a {@code Date} that when interpreted
 * in the default time zone of the JVM
 * has the same date and time of day (wall-clock time)
 * no matter if this makes the point in time wrong.
 * We are doing a <em>deliberately incorrect conversion</em> to compensate
 * for the opposite incorrect conversion taking place out of our control when
 * storing the {@code Date}.
 * That incorrect conversion happens because the JVM and the database
 * disagree about time zone:
 * the JVM uses UTC (GMT), but the database column must be in America/Chicago time zone.
 */
public static Date convertDate(XMLGregorianCalendar value) {
    if (value == null) {
        return null;
    } else {
        ZonedDateTime dateTimeWithDeliberatelyWrongTimeZone;
        if (value.getHour() == DatatypeConstants.FIELD_UNDEFINED
                && value.getMinute() == DatatypeConstants.FIELD_UNDEFINED
                && value.getSecond() == DatatypeConstants.FIELD_UNDEFINED
                && value.getFractionalSecond() == null) { // Date only
            dateTimeWithDeliberatelyWrongTimeZone = value.toGregorianCalendar()
                    .toZonedDateTime()
                    .toLocalDate()
                    .atStartOfDay(ZoneId.systemDefault());
        } else { // date and time
            if (value.getTimezone() == DatatypeConstants.FIELD_UNDEFINED) { // No UTC offset
                // In this case use UTC as default
                dateTimeWithDeliberatelyWrongTimeZone = value
                        .toGregorianCalendar(TimeZone.getTimeZone(ZoneOffset.UTC), null, null)
                        .toZonedDateTime()
                        .withZoneSameInstant(ZONE_ID)
                        // Preserve date and time of day
                        // when converting to default time zone
                        .withZoneSameLocal(ZoneId.systemDefault());
            } else {
                dateTimeWithDeliberatelyWrongTimeZone = value.toGregorianCalendar()
                        .toZonedDateTime()
                        .withZoneSameInstant(ZONE_ID)
                        // Preserve date and time of day
                        // when converting to default time zone
                        .withZoneSameLocal(ZoneId.systemDefault());
            }
        }
        return Date.from(dateTimeWithDeliberatelyWrongTimeZone.toInstant());
    }
}

Converting the same two XMLGregorianCalendar objects as before in UTC time zone gives the following results:

Fri Feb 09 01:57:33 UTC 2024
Fri Feb 09 00:00:00 UTC 2024

You see that the Date objects are printed in UTC. Since the times are as expected, this means that the Dates are 6 hours off from CST, so incorrect as they should. The date and the times printed are also the ones that are stored to the database, which is what you wanted.

The case where we pass no offset in the XMLGregorianCalendar:

    xmlDate = DatatypeFactory.newInstance()
            .newXMLGregorianCalendar("2024-02-09T01:57:33.240");
Thu Feb 08 19:57:33 UTC 2024

The date and time have been converted from UTC to CST so the CST date and time will be saved to the database. This time too the Date object is really 6 hours off.

When running the same three examples on a JVM with America/Chicago as default time zone (as on your local computer), the conversion is actually correct, which is also what we want because then there is no time zone conflict between local JVM and database:

Fri Feb 09 01:57:33 CST 2024
Fri Feb 09 00:00:00 CST 2024
Thu Feb 08 19:57:33 CST 2024

You see that in all three examples the dates and times are the same as before, only the time zone abbreviation is now CST instead of UTC.

Links

Upvotes: 5

Carlene12War
Carlene12War

Reputation: 37

import javax.xml.datatype.DatatypeConstants;
import javax.xml.datatype.XMLGregorianCalendar;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.*;

import javax.xml.datatype.DatatypeConstants;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;
import java.util.Date;
import java.util.TimeZone;

import javax.xml.datatype.DatatypeConstants;
import javax.xml.datatype.XMLGregorianCalendar;

import java.sql.Timestamp;
import java.time.ZoneId;

import javax.xml.datatype.DatatypeConstants;
import javax.xml.datatype.XMLGregorianCalendar;

public class Demo {



    public static final ZoneId ZONE_ID = ZoneId.of("America/Chicago");
    public static Date convertDate2(XMLGregorianCalendar myXmlGregCal) {
        ZonedDateTime zdt = myXmlGregCal.toGregorianCalendar().toZonedDateTime();
        ZoneId zChicago = ZoneId.of("America/Chicago");
        ZonedDateTime zdtChicago = zdt.withZoneSameInstant(zChicago);
        Locale locale = Locale.US;
        DateTimeFormatter f = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).withLocale(locale);
        String output = zdtChicago.format(f);

        // Parse the output string to LocalDateTime
        LocalDateTime localDateTime = LocalDateTime.parse(output, f);

        // Convert LocalDateTime to Date
        Instant instant = localDateTime.atZone(ZoneId.systemDefault()).toInstant();
        return Date.from(instant);
    }

    public static Date convertDate(XMLGregorianCalendar value) {
        if (value == null) {
            return null;
        } else {
            if (value.getXMLSchemaType() == DatatypeConstants.DATE) {
                return value.toGregorianCalendar().getTime();
            } else {
                LocalDateTime localDateTime = value.toGregorianCalendar().toZonedDateTime().withZoneSameInstant(ZONE_ID).toLocalDateTime();
                return Timestamp.valueOf(localDateTime);
            }
        }
    }

    public static void main(String[] args) {
        try {
            // Parse the XML date string into XMLGregorianCalendar
            XMLGregorianCalendar xmlDate = DatatypeFactory.newInstance().newXMLGregorianCalendar("2024-02-09T01:57:33.240-06:00");
            // Convert XMLGregorianCalendar to Date
            Date date = convertDate(xmlDate);
            // Print the result
          //  System.out.println("Date: " + date);
            System.out.println(convertDate2(xmlDate));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Upvotes: 0

Basil Bourque
Basil Bourque

Reputation: 338181

tl;dr

If handed a XMLGregorianCalendar, convert to modern class.

myXmlGregCal
    .toGregorianCalendar()
    .toZonedDateTime()
    .withZoneSameInstant( 
        ZoneId.of( "America/Edmonton" ) 
    )
    .format (
        DateTimeFormatter
            .ofLocalizedDateTime( FormatStyle.FULL )
            .withLocale( Locale.GERMANY )
    )

If handed text in standard ISO 8601 format with offset-from-UTC, parse as an OffsetDateTime object.

OffsetDateTime
    .parse( "2024-02-09T01:57:33.240-06:00" ) 
    .atZoneSameInstant( 
        ZoneId.of( "Pacific/Auckland" )
    )
    .format (
        DateTimeFormatter
            .ofLocalizedDateTime( FormatStyle.FULL )
            .withLocale( Locale.GERMANY )
    )

Convert a XMLGregorianCalendar to a java.util.Date by way of java.time:

java.util.Date
    .from(
        myXmlGregCal
            .toGregorianCalendar()
            .toZonedDateTime()
            .toInstant()
    )

Details

You are using terribly flawed date-time classes that are now legacy. Avoid XMLGregorianCalendar, GregorianCalendar, Calendar, SimpleDateFormat, and both Date classes. They were years ago supplanted by the modern java.time classes defined in JSR 310.

Among its many flaws is that java.util.Date#toString lies to you. That method interjects the JVM’s current default time zone while generating text to represent the UTC value of the object.

When handed a XMLGregorianCalendar object, immediately convert to its modern replacement, java.time.ZonedDateTime by way of java.util.GregorianCalendar.

ZonedDateTime zdt = myXmlGregCal.toGregorianCalendar().toZonedDateTime() ;

If you want to adjust that moment to be seen in a different time zone, apply a ZoneId to produce another ZonedDateTime. The java.time classes are immutable, so you get a new object rather than changing the original.

ZoneId zTokyo = ZoneId.of( "Asia/Tokyo" ) ;
ZonedDateTime zdtTokyo = zdt.withZoneSameInstant( zTokyo ) ;

Generate localized text.

Locale locale = Locale.CANADA_FRENCH ;
DateTimeFormatter f = DateTimeFormatter.ofLocalizedDateTime( FormatStyle.FULL ).withLocale( locale ) ;
String output = zdtTokyo.format( f ) ;

If you need to represent the moment in a ZonedDateTime as a java.util.Date to interoperate with old code not yet updated to java.time, extract an Instant. Pass that to java.util.Date.from. Both Instant and java.util.Date represent a point on the timeline as seen with an offset from UTC of zero hours-minutes-seconds. The legacy class resolves to milliseconds while the modern class resolves to nanoseconds.

Upvotes: 3

Related Questions