Reputation: 37
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
Reputation: 86139
timestamp with time zone
in UTC in your databaseFor 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.
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.
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.
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 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 Date
s 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
.
timestamp with time zone
in DB2set session time zone
in DB2Upvotes: 5
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
Reputation: 338181
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()
)
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