/*
* Copyright (c) 2003, PostgreSQL Global Development Group
* See the LICENSE file in the project root for more information.
*/
package org.postgresql.jdbc;
import org.postgresql.
PGStatement;
import org.postgresql.core.
JavaVersion;
import org.postgresql.core.
Oid;
import org.postgresql.core.
Provider;
import org.postgresql.util.
ByteConverter;
import org.postgresql.util.
GT;
import org.postgresql.util.
PSQLException;
import org.postgresql.util.
PSQLState;
import java.lang.reflect.
Field;
import java.sql.
Date;
import java.sql.
SQLException;
import java.sql.
Time;
import java.sql.
Timestamp;
//JCP! if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.2"
import java.time.
Duration;
import java.time.
LocalDate;
import java.time.
LocalDateTime;
import java.time.
LocalTime;
import java.time.
OffsetDateTime;
import java.time.
ZoneOffset;
import java.time.
ZonedDateTime;
import java.time.chrono.
IsoEra;
import java.time.format.
DateTimeParseException;
import java.time.temporal.
ChronoField;
//JCP! endif
import java.util.
Calendar;
import java.util.
GregorianCalendar;
import java.util.
HashMap;
import java.util.
SimpleTimeZone;
import java.util.
TimeZone;
/**
* Misc utils for handling time and date values.
*/
public class
TimestampUtils {
/**
* Number of milliseconds in one day.
*/
private static final int
ONEDAY = 24 * 3600 * 1000;
private static final char[]
ZEROS = {'0', '0', '0', '0', '0', '0', '0', '0', '0'};
private static final char[][]
NUMBERS;
private static final
HashMap<
String,
TimeZone>
GMT_ZONES = new
HashMap<
String,
TimeZone>();
private static final int
MAX_NANOS_BEFORE_WRAP_ON_ROUND = 999999500;
//JCP! if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.2"
private static final
Duration ONE_MICROSECOND =
Duration.
ofNanos(1000);
// LocalTime.MAX is 23:59:59.999_999_999, and it wraps to 24:00:00 when nanos exceed 999_999_499
// since PostgreSQL has microsecond resolution only
private static final
LocalTime MAX_TIME =
LocalTime.
MAX.
minus(
Duration.
ofMillis(500));
private static final
OffsetDateTime MAX_OFFSET_DATETIME =
OffsetDateTime.
MAX.
minus(
Duration.
ofMillis(500));
private static final
LocalDateTime MAX_LOCAL_DATETIME =
LocalDateTime.
MAX.
minus(
Duration.
ofMillis(500));
//JCP! endif
private static final
Field DEFAULT_TIME_ZONE_FIELD;
private
TimeZone prevDefaultZoneFieldValue;
private
TimeZone defaultTimeZoneCache;
static {
// The expected maximum value is 60 (seconds), so 64 is used "just in case"
NUMBERS = new char[64][];
for (int
i = 0;
i <
NUMBERS.length;
i++) {
NUMBERS[
i] = ((
i < 10 ? "0" : "") +
Integer.
toString(
i)).
toCharArray();
}
// Backend's gmt-3 means GMT+03 in Java. Here a map is created so gmt-3 can be converted to
// java TimeZone
for (int
i = -12;
i <= 14;
i++) {
TimeZone timeZone;
String pgZoneName;
if (
i == 0) {
timeZone =
TimeZone.
getTimeZone("GMT");
pgZoneName = "GMT";
} else {
timeZone =
TimeZone.
getTimeZone("GMT" + (
i <= 0 ? "+" : "-") +
Math.
abs(
i));
pgZoneName = "GMT" + (
i >= 0 ? "+" : "-");
}
if (
i == 0) {
GMT_ZONES.
put(
pgZoneName,
timeZone);
continue;
}
GMT_ZONES.
put(
pgZoneName +
Math.
abs(
i),
timeZone);
GMT_ZONES.
put(
pgZoneName + new
String(
NUMBERS[
Math.
abs(
i)]),
timeZone);
}
// Fast path to getting the default timezone.
// Accessing the default timezone over and over creates a clone with regular API.
// Because we don't mutate that object in our use of it, we can access the field directly.
// This saves the creation of a clone everytime, and the memory associated to all these clones.
Field tzField;
try {
tzField = null;
// Avoid reflective access in Java 9+
if (
JavaVersion.
getRuntimeVersion().
compareTo(
JavaVersion.
v1_8) <= 0) {
tzField =
TimeZone.class.
getDeclaredField("defaultTimeZone");
tzField.
setAccessible(true);
TimeZone defaultTz =
TimeZone.
getDefault();
Object tzFromField =
tzField.
get(null);
if (
defaultTz == null || !
defaultTz.
equals(
tzFromField)) {
tzField = null;
}
}
} catch (
Exception e) {
tzField = null;
}
DEFAULT_TIME_ZONE_FIELD =
tzField;
}
private final
StringBuilder sbuf = new
StringBuilder();
// This calendar is used when user provides calendar in setX(, Calendar) method.
// It ensures calendar is Gregorian.
private final
Calendar calendarWithUserTz = new
GregorianCalendar();
private final
TimeZone utcTz =
TimeZone.
getTimeZone("UTC");
private
Calendar calCache;
private int
calCacheZone;
/**
* True if the backend uses doubles for time values. False if long is used.
*/
private final boolean
usesDouble;
private final
Provider<
TimeZone>
timeZoneProvider;
TimestampUtils(boolean
usesDouble,
Provider<
TimeZone>
timeZoneProvider) {
this.
usesDouble =
usesDouble;
this.
timeZoneProvider =
timeZoneProvider;
}
private
Calendar getCalendar(int
sign, int
hr, int
min, int
sec) {
int
rawOffset =
sign * (((
hr * 60 +
min) * 60 +
sec) * 1000);
if (
calCache != null &&
calCacheZone ==
rawOffset) {
return
calCache;
}
StringBuilder zoneID = new
StringBuilder("GMT");
zoneID.
append(
sign < 0 ? '-' : '+');
if (
hr < 10) {
zoneID.
append('0');
}
zoneID.
append(
hr);
if (
min < 10) {
zoneID.
append('0');
}
zoneID.
append(
min);
if (
sec < 10) {
zoneID.
append('0');
}
zoneID.
append(
sec);
TimeZone syntheticTZ = new
SimpleTimeZone(
rawOffset,
zoneID.
toString());
calCache = new
GregorianCalendar(
syntheticTZ);
calCacheZone =
rawOffset;
return
calCache;
}
private static class
ParsedTimestamp {
boolean
hasDate = false;
int
era =
GregorianCalendar.
AD;
int
year = 1970;
int
month = 1;
boolean
hasTime = false;
int
day = 1;
int
hour = 0;
int
minute = 0;
int
second = 0;
int
nanos = 0;
Calendar tz = null;
}
private static class
ParsedBinaryTimestamp {
Infinity infinity = null;
long
millis = 0;
int
nanos = 0;
}
enum
Infinity {
POSITIVE,
NEGATIVE;
}
/**
* Load date/time information into the provided calendar returning the fractional seconds.
*/
private
ParsedTimestamp parseBackendTimestamp(
String str) throws
SQLException {
char[]
s =
str.
toCharArray();
int
slen =
s.length;
// This is pretty gross..
ParsedTimestamp result = new
ParsedTimestamp();
// We try to parse these fields in order; all are optional
// (but some combinations don't make sense, e.g. if you have
// both date and time then they must be whitespace-separated).
// At least one of date and time must be present.
// leading whitespace
// yyyy-mm-dd
// whitespace
// hh:mm:ss
// whitespace
// timezone in one of the formats: +hh, -hh, +hh:mm, -hh:mm
// whitespace
// if date is present, an era specifier: AD or BC
// trailing whitespace
try {
int
start =
skipWhitespace(
s, 0); // Skip leading whitespace
int
end =
firstNonDigit(
s,
start);
int
num;
char
sep;
// Possibly read date.
if (
charAt(
s,
end) == '-') {
//
// Date
//
result.
hasDate = true;
// year
result.
year =
number(
s,
start,
end);
start =
end + 1; // Skip '-'
// month
end =
firstNonDigit(
s,
start);
result.
month =
number(
s,
start,
end);
sep =
charAt(
s,
end);
if (
sep != '-') {
throw new
NumberFormatException("Expected date to be dash-separated, got '" +
sep + "'");
}
start =
end + 1; // Skip '-'
// day of month
end =
firstNonDigit(
s,
start);
result.
day =
number(
s,
start,
end);
start =
skipWhitespace(
s,
end); // Skip trailing whitespace
}
// Possibly read time.
if (
Character.
isDigit(
charAt(
s,
start))) {
//
// Time.
//
result.
hasTime = true;
// Hours
end =
firstNonDigit(
s,
start);
result.
hour =
number(
s,
start,
end);
sep =
charAt(
s,
end);
if (
sep != ':') {
throw new
NumberFormatException("Expected time to be colon-separated, got '" +
sep + "'");
}
start =
end + 1; // Skip ':'
// minutes
end =
firstNonDigit(
s,
start);
result.
minute =
number(
s,
start,
end);
sep =
charAt(
s,
end);
if (
sep != ':') {
throw new
NumberFormatException("Expected time to be colon-separated, got '" +
sep + "'");
}
start =
end + 1; // Skip ':'
// seconds
end =
firstNonDigit(
s,
start);
result.
second =
number(
s,
start,
end);
start =
end;
// Fractional seconds.
if (
charAt(
s,
start) == '.') {
end =
firstNonDigit(
s,
start + 1); // Skip '.'
num =
number(
s,
start + 1,
end);
for (int
numlength = (
end - (
start + 1));
numlength < 9; ++
numlength) {
num *= 10;
}
result.
nanos =
num;
start =
end;
}
start =
skipWhitespace(
s,
start); // Skip trailing whitespace
}
// Possibly read timezone.
sep =
charAt(
s,
start);
if (
sep == '-' ||
sep == '+') {
int
tzsign = (
sep == '-') ? -1 : 1;
int
tzhr;
int
tzmin;
int
tzsec;
end =
firstNonDigit(
s,
start + 1); // Skip +/-
tzhr =
number(
s,
start + 1,
end);
start =
end;
sep =
charAt(
s,
start);
if (
sep == ':') {
end =
firstNonDigit(
s,
start + 1); // Skip ':'
tzmin =
number(
s,
start + 1,
end);
start =
end;
} else {
tzmin = 0;
}
tzsec = 0;
sep =
charAt(
s,
start);
if (
sep == ':') {
end =
firstNonDigit(
s,
start + 1); // Skip ':'
tzsec =
number(
s,
start + 1,
end);
start =
end;
}
// Setting offset does not seem to work correctly in all
// cases.. So get a fresh calendar for a synthetic timezone
// instead
result.
tz =
getCalendar(
tzsign,
tzhr,
tzmin,
tzsec);
start =
skipWhitespace(
s,
start); // Skip trailing whitespace
}
if (
result.
hasDate &&
start <
slen) {
String eraString = new
String(
s,
start,
slen -
start);
if (
eraString.
startsWith("AD")) {
result.
era =
GregorianCalendar.
AD;
start += 2;
} else if (
eraString.
startsWith("BC")) {
result.
era =
GregorianCalendar.
BC;
start += 2;
}
}
if (
start <
slen) {
throw new
NumberFormatException(
"Trailing junk on timestamp: '" + new
String(
s,
start,
slen -
start) + "'");
}
if (!
result.
hasTime && !
result.
hasDate) {
throw new
NumberFormatException("Timestamp has neither date nor time");
}
} catch (
NumberFormatException nfe) {
throw new
PSQLException(
GT.
tr("Bad value for type timestamp/date/time: {1}",
str),
PSQLState.
BAD_DATETIME_FORMAT,
nfe);
}
return
result;
}
/**
* Parse a string and return a timestamp representing its value.
*
* @param cal calendar to be used to parse the input string
* @param s The ISO formated date string to parse.
* @return null if s is null or a timestamp of the parsed string s.
* @throws SQLException if there is a problem parsing s.
*/
public synchronized
Timestamp toTimestamp(
Calendar cal,
String s) throws
SQLException {
if (
s == null) {
return null;
}
int
slen =
s.
length();
// convert postgres's infinity values to internal infinity magic value
if (
slen == 8 &&
s.
equals("infinity")) {
return new
Timestamp(
PGStatement.
DATE_POSITIVE_INFINITY);
}
if (
slen == 9 &&
s.
equals("-infinity")) {
return new
Timestamp(
PGStatement.
DATE_NEGATIVE_INFINITY);
}
ParsedTimestamp ts =
parseBackendTimestamp(
s);
Calendar useCal =
ts.
tz != null ?
ts.
tz :
setupCalendar(
cal);
useCal.
set(
Calendar.
ERA,
ts.
era);
useCal.
set(
Calendar.
YEAR,
ts.
year);
useCal.
set(
Calendar.
MONTH,
ts.
month - 1);
useCal.
set(
Calendar.
DAY_OF_MONTH,
ts.
day);
useCal.
set(
Calendar.
HOUR_OF_DAY,
ts.
hour);
useCal.
set(
Calendar.
MINUTE,
ts.
minute);
useCal.
set(
Calendar.
SECOND,
ts.
second);
useCal.
set(
Calendar.
MILLISECOND, 0);
Timestamp result = new
Timestamp(
useCal.
getTimeInMillis());
result.
setNanos(
ts.
nanos);
return
result;
}
//JCP! if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.2"
/**
* Parse a string and return a LocalTime representing its value.
*
* @param s The ISO formated time string to parse.
* @return null if s is null or a LocalTime of the parsed string s.
* @throws SQLException if there is a problem parsing s.
*/
public
LocalTime toLocalTime(
String s) throws
SQLException {
if (
s == null) {
return null;
}
if (
s.
equals("24:00:00")) {
return
LocalTime.
MAX;
}
try {
return
LocalTime.
parse(
s);
} catch (
DateTimeParseException nfe) {
throw new
PSQLException(
GT.
tr("Bad value for type timestamp/date/time: {1}",
s),
PSQLState.
BAD_DATETIME_FORMAT,
nfe);
}
}
/**
* Parse a string and return a LocalDateTime representing its value.
*
* @param s The ISO formated date string to parse.
* @return null if s is null or a LocalDateTime of the parsed string s.
* @throws SQLException if there is a problem parsing s.
*/
public
LocalDateTime toLocalDateTime(
String s) throws
SQLException {
if (
s == null) {
return null;
}
int
slen =
s.
length();
// convert postgres's infinity values to internal infinity magic value
if (
slen == 8 &&
s.
equals("infinity")) {
return
LocalDateTime.
MAX;
}
if (
slen == 9 &&
s.
equals("-infinity")) {
return
LocalDateTime.
MIN;
}
ParsedTimestamp ts =
parseBackendTimestamp(
s);
// intentionally ignore time zone
// 2004-10-19 10:23:54+03:00 is 2004-10-19 10:23:54 locally
LocalDateTime result =
LocalDateTime.
of(
ts.
year,
ts.
month,
ts.
day,
ts.
hour,
ts.
minute,
ts.
second,
ts.
nanos);
if (
ts.
era ==
GregorianCalendar.
BC) {
return
result.
with(
ChronoField.
ERA,
IsoEra.
BCE.
getValue());
} else {
return
result;
}
}
//JCP! endif
public synchronized
Time toTime(
Calendar cal,
String s) throws
SQLException {
// 1) Parse backend string
if (
s == null) {
return null;
}
ParsedTimestamp ts =
parseBackendTimestamp(
s);
Calendar useCal =
ts.
tz != null ?
ts.
tz :
setupCalendar(
cal);
if (
ts.
tz == null) {
// When no time zone provided (e.g. time or timestamp)
// We get the year-month-day from the string, then truncate the day to 1970-01-01
// This is used for timestamp -> time conversion
// Note: this cannot be merged with "else" branch since
// timestamps at which the time flips to/from DST depend on the date
// For instance, 2000-03-26 02:00:00 is invalid timestamp in Europe/Moscow time zone
// and the valid one is 2000-03-26 03:00:00. That is why we parse full timestamp
// then set year to 1970 later
useCal.
set(
Calendar.
ERA,
ts.
era);
useCal.
set(
Calendar.
YEAR,
ts.
year);
useCal.
set(
Calendar.
MONTH,
ts.
month - 1);
useCal.
set(
Calendar.
DAY_OF_MONTH,
ts.
day);
} else {
// When time zone is given, we just pick the time part and assume date to be 1970-01-01
// this is used for time, timez, and timestamptz parsing
useCal.
set(
Calendar.
ERA,
GregorianCalendar.
AD);
useCal.
set(
Calendar.
YEAR, 1970);
useCal.
set(
Calendar.
MONTH,
Calendar.
JANUARY);
useCal.
set(
Calendar.
DAY_OF_MONTH, 1);
}
useCal.
set(
Calendar.
HOUR_OF_DAY,
ts.
hour);
useCal.
set(
Calendar.
MINUTE,
ts.
minute);
useCal.
set(
Calendar.
SECOND,
ts.
second);
useCal.
set(
Calendar.
MILLISECOND, 0);
long
timeMillis =
useCal.
getTimeInMillis() +
ts.
nanos / 1000000;
if (
ts.
tz != null || (
ts.
year == 1970 &&
ts.
era ==
GregorianCalendar.
AD)) {
// time with time zone has proper time zone, so the value can be returned as is
return new
Time(
timeMillis);
}
// 2) Truncate date part so in given time zone the date would be formatted as 01/01/1970
return
convertToTime(
timeMillis,
useCal == null ? null :
useCal.
getTimeZone());
}
public synchronized
Date toDate(
Calendar cal,
String s) throws
SQLException {
// 1) Parse backend string
Timestamp timestamp =
toTimestamp(
cal,
s);
if (
timestamp == null) {
return null;
}
// Note: infinite dates are handled in convertToDate
// 2) Truncate date part so in given time zone the date would be formatted as 00:00
return
convertToDate(
timestamp.
getTime(),
cal == null ? null :
cal.
getTimeZone());
}
private
Calendar setupCalendar(
Calendar cal) {
TimeZone timeZone =
cal == null ? null :
cal.
getTimeZone();
return
getSharedCalendar(
timeZone);
}
/**
* Get a shared calendar, applying the supplied time zone or the default time zone if null.
*
* @param timeZone time zone to be set for the calendar
* @return The shared calendar.
*/
public
Calendar getSharedCalendar(
TimeZone timeZone) {
if (
timeZone == null) {
timeZone =
getDefaultTz();
}
Calendar tmp =
calendarWithUserTz;
tmp.
setTimeZone(
timeZone);
return
tmp;
}
/**
* Returns true when microsecond part of the time should be increased
* when rounding to microseconds
* @param nanos nanosecond part of the time
* @return true when microsecond part of the time should be increased when rounding to microseconds
*/
private static boolean
nanosExceed499(int
nanos) {
return
nanos % 1000 > 499;
}
public synchronized
String toString(
Calendar cal,
Timestamp x) {
return
toString(
cal,
x, true);
}
public synchronized
String toString(
Calendar cal,
Timestamp x,
boolean
withTimeZone) {
if (
x.
getTime() ==
PGStatement.
DATE_POSITIVE_INFINITY) {
return "infinity";
} else if (
x.
getTime() ==
PGStatement.
DATE_NEGATIVE_INFINITY) {
return "-infinity";
}
cal =
setupCalendar(
cal);
long
timeMillis =
x.
getTime();
// Round to microseconds
int
nanos =
x.
getNanos();
if (
nanos >=
MAX_NANOS_BEFORE_WRAP_ON_ROUND) {
nanos = 0;
timeMillis++;
} else if (
nanosExceed499(
nanos)) {
// PostgreSQL does not support nanosecond resolution yet, and appendTime will just ignore
// 0..999 part of the nanoseconds, however we subtract nanos % 1000 to make the value
// a little bit saner for debugging reasons
nanos += 1000 -
nanos % 1000;
}
cal.
setTimeInMillis(
timeMillis);
sbuf.
setLength(0);
appendDate(
sbuf,
cal);
sbuf.
append(' ');
appendTime(
sbuf,
cal,
nanos);
if (
withTimeZone) {
appendTimeZone(
sbuf,
cal);
}
appendEra(
sbuf,
cal);
return
sbuf.
toString();
}
public synchronized
String toString(
Calendar cal,
Date x) {
return
toString(
cal,
x, true);
}
public synchronized
String toString(
Calendar cal,
Date x,
boolean
withTimeZone) {
if (
x.
getTime() ==
PGStatement.
DATE_POSITIVE_INFINITY) {
return "infinity";
} else if (
x.
getTime() ==
PGStatement.
DATE_NEGATIVE_INFINITY) {
return "-infinity";
}
cal =
setupCalendar(
cal);
cal.
setTime(
x);
sbuf.
setLength(0);
appendDate(
sbuf,
cal);
appendEra(
sbuf,
cal);
if (
withTimeZone) {
sbuf.
append(' ');
appendTimeZone(
sbuf,
cal);
}
return
sbuf.
toString();
}
public synchronized
String toString(
Calendar cal,
Time x) {
return
toString(
cal,
x, true);
}
public synchronized
String toString(
Calendar cal,
Time x,
boolean
withTimeZone) {
cal =
setupCalendar(
cal);
cal.
setTime(
x);
sbuf.
setLength(0);
appendTime(
sbuf,
cal,
cal.
get(
Calendar.
MILLISECOND) * 1000000);
// The 'time' parser for <= 7.3 doesn't like timezones.
if (
withTimeZone) {
appendTimeZone(
sbuf,
cal);
}
return
sbuf.
toString();
}
private static void
appendDate(
StringBuilder sb,
Calendar cal) {
int
l_year =
cal.
get(
Calendar.
YEAR);
int
l_month =
cal.
get(
Calendar.
MONTH) + 1;
int
l_day =
cal.
get(
Calendar.
DAY_OF_MONTH);
appendDate(
sb,
l_year,
l_month,
l_day);
}
private static void
appendDate(
StringBuilder sb, int
year, int
month, int
day) {
// always use at least four digits for the year so very
// early years, like 2, don't get misinterpreted
//
int
prevLength =
sb.
length();
sb.
append(
year);
int
leadingZerosForYear = 4 - (
sb.
length() -
prevLength);
if (
leadingZerosForYear > 0) {
sb.
insert(
prevLength,
ZEROS, 0,
leadingZerosForYear);
}
sb.
append('-');
sb.
append(
NUMBERS[
month]);
sb.
append('-');
sb.
append(
NUMBERS[
day]);
}
private static void
appendTime(
StringBuilder sb,
Calendar cal, int
nanos) {
int
hours =
cal.
get(
Calendar.
HOUR_OF_DAY);
int
minutes =
cal.
get(
Calendar.
MINUTE);
int
seconds =
cal.
get(
Calendar.
SECOND);
appendTime(
sb,
hours,
minutes,
seconds,
nanos);
}
/**
* Appends time part to the {@code StringBuilder} in PostgreSQL-compatible format.
* The function truncates {@param nanos} to microseconds. The value is expected to be rounded
* beforehand.
* @param sb destination
* @param hours hours
* @param minutes minutes
* @param seconds seconds
* @param nanos nanoseconds
*/
private static void
appendTime(
StringBuilder sb, int
hours, int
minutes, int
seconds, int
nanos) {
sb.
append(
NUMBERS[
hours]);
sb.
append(':');
sb.
append(
NUMBERS[
minutes]);
sb.
append(':');
sb.
append(
NUMBERS[
seconds]);
// Add nanoseconds.
// This won't work for server versions < 7.2 which only want
// a two digit fractional second, but we don't need to support 7.1
// anymore and getting the version number here is difficult.
//
if (
nanos < 1000) {
return;
}
sb.
append('.');
int
len =
sb.
length();
sb.
append(
nanos / 1000); // append microseconds
int
needZeros = 6 - (
sb.
length() -
len);
if (
needZeros > 0) {
sb.
insert(
len,
ZEROS, 0,
needZeros);
}
int
end =
sb.
length() - 1;
while (
sb.
charAt(
end) == '0') {
sb.
deleteCharAt(
end);
end--;
}
}
private void
appendTimeZone(
StringBuilder sb, java.util.
Calendar cal) {
int
offset = (
cal.
get(
Calendar.
ZONE_OFFSET) +
cal.
get(
Calendar.
DST_OFFSET)) / 1000;
appendTimeZone(
sb,
offset);
}
private void
appendTimeZone(
StringBuilder sb, int
offset) {
int
absoff =
Math.
abs(
offset);
int
hours =
absoff / 60 / 60;
int
mins = (
absoff -
hours * 60 * 60) / 60;
int
secs =
absoff -
hours * 60 * 60 -
mins * 60;
sb.
append((
offset >= 0) ? "+" : "-");
sb.
append(
NUMBERS[
hours]);
if (
mins == 0 &&
secs == 0) {
return;
}
sb.
append(':');
sb.
append(
NUMBERS[
mins]);
if (
secs != 0) {
sb.
append(':');
sb.
append(
NUMBERS[
secs]);
}
}
private static void
appendEra(
StringBuilder sb,
Calendar cal) {
if (
cal.
get(
Calendar.
ERA) ==
GregorianCalendar.
BC) {
sb.
append(" BC");
}
}
//JCP! if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.2"
public synchronized
String toString(
LocalDate localDate) {
if (
LocalDate.
MAX.
equals(
localDate)) {
return "infinity";
} else if (
LocalDate.
MIN.
equals(
localDate)) {
return "-infinity";
}
sbuf.
setLength(0);
appendDate(
sbuf,
localDate);
appendEra(
sbuf,
localDate);
return
sbuf.
toString();
}
public synchronized
String toString(
LocalTime localTime) {
sbuf.
setLength(0);
if (
localTime.
isAfter(
MAX_TIME)) {
return "24:00:00";
}
int
nano =
localTime.
getNano();
if (
nanosExceed499(
nano)) {
// Technically speaking this is not a proper rounding, however
// it relies on the fact that appendTime just truncates 000..999 nanosecond part
localTime =
localTime.
plus(
ONE_MICROSECOND);
}
appendTime(
sbuf,
localTime);
return
sbuf.
toString();
}
public synchronized
String toString(
OffsetDateTime offsetDateTime) {
if (
offsetDateTime.
isAfter(
MAX_OFFSET_DATETIME)) {
return "infinity";
} else if (
OffsetDateTime.
MIN.
equals(
offsetDateTime)) {
return "-infinity";
}
sbuf.
setLength(0);
int
nano =
offsetDateTime.
getNano();
if (
nanosExceed499(
nano)) {
// Technically speaking this is not a proper rounding, however
// it relies on the fact that appendTime just truncates 000..999 nanosecond part
offsetDateTime =
offsetDateTime.
plus(
ONE_MICROSECOND);
}
LocalDateTime localDateTime =
offsetDateTime.
toLocalDateTime();
LocalDate localDate =
localDateTime.
toLocalDate();
appendDate(
sbuf,
localDate);
sbuf.
append(' ');
appendTime(
sbuf,
localDateTime.
toLocalTime());
appendTimeZone(
sbuf,
offsetDateTime.
getOffset());
appendEra(
sbuf,
localDate);
return
sbuf.
toString();
}
/**
* Formats {@link LocalDateTime} to be sent to the backend, thus it adds time zone.
* Do not use this method in {@link java.sql.ResultSet#getString(int)}
* @param localDateTime The local date to format as a String
* @return The formatted local date
*/
public synchronized
String toString(
LocalDateTime localDateTime) {
if (
localDateTime.
isAfter(
MAX_LOCAL_DATETIME)) {
return "infinity";
} else if (
LocalDateTime.
MIN.
equals(
localDateTime)) {
return "-infinity";
}
// LocalDateTime is always passed with time zone so backend can decide between timestamp and timestamptz
ZonedDateTime zonedDateTime =
localDateTime.
atZone(
getDefaultTz().
toZoneId());
return
toString(
zonedDateTime.
toOffsetDateTime());
}
private static void
appendDate(
StringBuilder sb,
LocalDate localDate) {
int
year =
Math.
abs(
localDate.
getYear()); // year is negative for BC dates
int
month =
localDate.
getMonthValue();
int
day =
localDate.
getDayOfMonth();
appendDate(
sb,
year,
month,
day);
}
private static void
appendTime(
StringBuilder sb,
LocalTime localTime) {
int
hours =
localTime.
getHour();
int
minutes =
localTime.
getMinute();
int
seconds =
localTime.
getSecond();
int
nanos =
localTime.
getNano();
appendTime(
sb,
hours,
minutes,
seconds,
nanos);
}
private void
appendTimeZone(
StringBuilder sb,
ZoneOffset offset) {
int
offsetSeconds =
offset.
getTotalSeconds();
appendTimeZone(
sb,
offsetSeconds);
}
private static void
appendEra(
StringBuilder sb,
LocalDate localDate) {
if (
localDate.
get(
ChronoField.
ERA) ==
IsoEra.
BCE.
getValue()) {
sb.
append(" BC");
}
}
//JCP! endif
private static int
skipWhitespace(char[]
s, int
start) {
int
slen =
s.length;
for (int
i =
start;
i <
slen;
i++) {
if (!
Character.
isSpace(
s[
i])) {
return
i;
}
}
return
slen;
}
private static int
firstNonDigit(char[]
s, int
start) {
int
slen =
s.length;
for (int
i =
start;
i <
slen;
i++) {
if (!
Character.
isDigit(
s[
i])) {
return
i;
}
}
return
slen;
}
private static int
number(char[]
s, int
start, int
end) {
if (
start >=
end) {
throw new
NumberFormatException();
}
int
n = 0;
for (int
i =
start;
i <
end;
i++) {
n = 10 *
n + (
s[
i] - '0');
}
return
n;
}
private static char
charAt(char[]
s, int
pos) {
if (
pos >= 0 &&
pos <
s.length) {
return
s[
pos];
}
return '\0';
}
/**
* Returns the SQL Date object matching the given bytes with {@link Oid#DATE}.
*
* @param tz The timezone used.
* @param bytes The binary encoded date value.
* @return The parsed date object.
* @throws PSQLException If binary format could not be parsed.
*/
public
Date toDateBin(
TimeZone tz, byte[]
bytes) throws
PSQLException {
if (
bytes.length != 4) {
throw new
PSQLException(
GT.
tr("Unsupported binary encoding of {0}.", "date"),
PSQLState.
BAD_DATETIME_FORMAT);
}
int
days =
ByteConverter.
int4(
bytes, 0);
if (
tz == null) {
tz =
getDefaultTz();
}
long
secs =
toJavaSecs(
days * 86400L);
long
millis =
secs * 1000L;
if (
millis <=
PGStatement.
DATE_NEGATIVE_SMALLER_INFINITY) {
millis =
PGStatement.
DATE_NEGATIVE_INFINITY;
} else if (
millis >=
PGStatement.
DATE_POSITIVE_SMALLER_INFINITY) {
millis =
PGStatement.
DATE_POSITIVE_INFINITY;
} else {
// Here be dragons: backend did not provide us the timezone, so we guess the actual point in
// time
millis =
guessTimestamp(
millis,
tz);
}
return new
Date(
millis);
}
private
TimeZone getDefaultTz() {
// Fast path to getting the default timezone.
if (
DEFAULT_TIME_ZONE_FIELD != null) {
try {
TimeZone defaultTimeZone = (
TimeZone)
DEFAULT_TIME_ZONE_FIELD.
get(null);
if (
defaultTimeZone ==
prevDefaultZoneFieldValue) {
return
defaultTimeZoneCache;
}
prevDefaultZoneFieldValue =
defaultTimeZone;
} catch (
Exception e) {
// If this were to fail, fallback on slow method.
}
}
TimeZone tz =
TimeZone.
getDefault();
defaultTimeZoneCache =
tz;
return
tz;
}
public boolean
hasFastDefaultTimeZone() {
return
DEFAULT_TIME_ZONE_FIELD != null;
}
/**
* Returns the SQL Time object matching the given bytes with {@link Oid#TIME} or
* {@link Oid#TIMETZ}.
*
* @param tz The timezone used when received data is {@link Oid#TIME}, ignored if data already
* contains {@link Oid#TIMETZ}.
* @param bytes The binary encoded time value.
* @return The parsed time object.
* @throws PSQLException If binary format could not be parsed.
*/
public
Time toTimeBin(
TimeZone tz, byte[]
bytes) throws
PSQLException {
if ((
bytes.length != 8 &&
bytes.length != 12)) {
throw new
PSQLException(
GT.
tr("Unsupported binary encoding of {0}.", "time"),
PSQLState.
BAD_DATETIME_FORMAT);
}
long
millis;
int
timeOffset;
if (
usesDouble) {
double
time =
ByteConverter.
float8(
bytes, 0);
millis = (long) (
time * 1000);
} else {
long
time =
ByteConverter.
int8(
bytes, 0);
millis =
time / 1000;
}
if (
bytes.length == 12) {
timeOffset =
ByteConverter.
int4(
bytes, 8);
timeOffset *= -1000;
millis -=
timeOffset;
return new
Time(
millis);
}
if (
tz == null) {
tz =
getDefaultTz();
}
// Here be dragons: backend did not provide us the timezone, so we guess the actual point in
// time
millis =
guessTimestamp(
millis,
tz);
return
convertToTime(
millis,
tz); // Ensure date part is 1970-01-01
}
//JCP! if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.2"
/**
* Returns the SQL Time object matching the given bytes with {@link Oid#TIME}.
*
* @param bytes The binary encoded time value.
* @return The parsed time object.
* @throws PSQLException If binary format could not be parsed.
*/
public
LocalTime toLocalTimeBin(byte[]
bytes) throws
PSQLException {
if (
bytes.length != 8) {
throw new
PSQLException(
GT.
tr("Unsupported binary encoding of {0}.", "time"),
PSQLState.
BAD_DATETIME_FORMAT);
}
long
micros;
if (
usesDouble) {
double
seconds =
ByteConverter.
float8(
bytes, 0);
micros = (long) (
seconds * 1000000d);
} else {
micros =
ByteConverter.
int8(
bytes, 0);
}
return
LocalTime.
ofNanoOfDay(
micros * 1000);
}
//JCP! endif
/**
* Returns the SQL Timestamp object matching the given bytes with {@link Oid#TIMESTAMP} or
* {@link Oid#TIMESTAMPTZ}.
*
* @param tz The timezone used when received data is {@link Oid#TIMESTAMP}, ignored if data
* already contains {@link Oid#TIMESTAMPTZ}.
* @param bytes The binary encoded timestamp value.
* @param timestamptz True if the binary is in GMT.
* @return The parsed timestamp object.
* @throws PSQLException If binary format could not be parsed.
*/
public
Timestamp toTimestampBin(
TimeZone tz, byte[]
bytes, boolean
timestamptz)
throws
PSQLException {
ParsedBinaryTimestamp parsedTimestamp = this.
toParsedTimestampBin(
tz,
bytes,
timestamptz);
if (
parsedTimestamp.
infinity ==
Infinity.
POSITIVE) {
return new
Timestamp(
PGStatement.
DATE_POSITIVE_INFINITY);
} else if (
parsedTimestamp.
infinity ==
Infinity.
NEGATIVE) {
return new
Timestamp(
PGStatement.
DATE_NEGATIVE_INFINITY);
}
Timestamp ts = new
Timestamp(
parsedTimestamp.
millis);
ts.
setNanos(
parsedTimestamp.
nanos);
return
ts;
}
private
ParsedBinaryTimestamp toParsedTimestampBin(
TimeZone tz, byte[]
bytes, boolean
timestamptz)
throws
PSQLException {
if (
bytes.length != 8) {
throw new
PSQLException(
GT.
tr("Unsupported binary encoding of {0}.", "timestamp"),
PSQLState.
BAD_DATETIME_FORMAT);
}
long
secs;
int
nanos;
if (
usesDouble) {
double
time =
ByteConverter.
float8(
bytes, 0);
if (
time ==
Double.
POSITIVE_INFINITY) {
ParsedBinaryTimestamp ts = new
ParsedBinaryTimestamp();
ts.
infinity =
Infinity.
POSITIVE;
return
ts;
} else if (
time ==
Double.
NEGATIVE_INFINITY) {
ParsedBinaryTimestamp ts = new
ParsedBinaryTimestamp();
ts.
infinity =
Infinity.
NEGATIVE;
return
ts;
}
secs = (long)
time;
nanos = (int) ((
time -
secs) * 1000000);
} else {
long
time =
ByteConverter.
int8(
bytes, 0);
// compatibility with text based receiving, not strictly necessary
// and can actually be confusing because there are timestamps
// that are larger than infinite
if (
time ==
Long.
MAX_VALUE) {
ParsedBinaryTimestamp ts = new
ParsedBinaryTimestamp();
ts.
infinity =
Infinity.
POSITIVE;
return
ts;
} else if (
time ==
Long.
MIN_VALUE) {
ParsedBinaryTimestamp ts = new
ParsedBinaryTimestamp();
ts.
infinity =
Infinity.
NEGATIVE;
return
ts;
}
secs =
time / 1000000;
nanos = (int) (
time -
secs * 1000000);
}
if (
nanos < 0) {
secs--;
nanos += 1000000;
}
nanos *= 1000;
secs =
toJavaSecs(
secs);
long
millis =
secs * 1000L;
if (!
timestamptz) {
// Here be dragons: backend did not provide us the timezone, so we guess the actual point in
// time
millis =
guessTimestamp(
millis,
tz);
}
ParsedBinaryTimestamp ts = new
ParsedBinaryTimestamp();
ts.
millis =
millis;
ts.
nanos =
nanos;
return
ts;
}
//JCP! if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.2"
/**
* Returns the local date time object matching the given bytes with {@link Oid#TIMESTAMP} or
* {@link Oid#TIMESTAMPTZ}.
*
* @param tz time zone to use
* @param bytes The binary encoded local date time value.
* @return The parsed local date time object.
* @throws PSQLException If binary format could not be parsed.
*/
public
LocalDateTime toLocalDateTimeBin(
TimeZone tz, byte[]
bytes) throws
PSQLException {
ParsedBinaryTimestamp parsedTimestamp = this.
toParsedTimestampBin(
tz,
bytes, true);
if (
parsedTimestamp.
infinity ==
Infinity.
POSITIVE) {
return
LocalDateTime.
MAX;
} else if (
parsedTimestamp.
infinity ==
Infinity.
NEGATIVE) {
return
LocalDateTime.
MIN;
}
return
LocalDateTime.
ofEpochSecond(
parsedTimestamp.
millis / 1000L,
parsedTimestamp.
nanos,
ZoneOffset.
UTC);
}
//JCP! endif
/**
* <p>Given a UTC timestamp {@code millis} finds another point in time that is rendered in given time
* zone {@code tz} exactly as "millis in UTC".</p>
*
* <p>For instance, given 7 Jan 16:00 UTC and tz=GMT+02:00 it returns 7 Jan 14:00 UTC == 7 Jan 16:00
* GMT+02:00 Note that is not trivial for timestamps near DST change. For such cases, we rely on
* {@link Calendar} to figure out the proper timestamp.</p>
*
* @param millis source timestamp
* @param tz desired time zone
* @return timestamp that would be rendered in {@code tz} like {@code millis} in UTC
*/
private long
guessTimestamp(long
millis,
TimeZone tz) {
if (
tz == null) {
// If client did not provide us with time zone, we use system default time zone
tz =
getDefaultTz();
}
// The story here:
// Backend provided us with something like '2015-10-04 13:40' and it did NOT provide us with a
// time zone.
// On top of that, user asked us to treat the timestamp as if it were in GMT+02:00.
//
// The code below creates such a timestamp that is rendered as '2015-10-04 13:40 GMT+02:00'
// In other words, its UTC value should be 11:40 UTC == 13:40 GMT+02:00.
// It is not sufficient to just subtract offset as you might cross DST change as you subtract.
//
// For instance, on 2000-03-26 02:00:00 Moscow went to DST, thus local time became 03:00:00
// Suppose we deal with 2000-03-26 02:00:01
// If you subtract offset from the timestamp, the time will be "a hour behind" since
// "just a couple of hours ago the OFFSET was different"
//
// To make a long story short: we have UTC timestamp that looks like "2000-03-26 02:00:01" when
// rendered in UTC tz.
// We want to know another timestamp that will look like "2000-03-26 02:00:01" in Europe/Moscow
// time zone.
if (
isSimpleTimeZone(
tz.
getID())) {
// For well-known non-DST time zones, just subtract offset
return
millis -
tz.
getRawOffset();
}
// For all the other time zones, enjoy debugging Calendar API
// Here we do a straight-forward implementation that splits original timestamp into pieces and
// composes it back.
// Note: cal.setTimeZone alone is not sufficient as it would alter hour (it will try to keep the
// same time instant value)
Calendar cal =
calendarWithUserTz;
cal.
setTimeZone(
utcTz);
cal.
setTimeInMillis(
millis);
int
era =
cal.
get(
Calendar.
ERA);
int
year =
cal.
get(
Calendar.
YEAR);
int
month =
cal.
get(
Calendar.
MONTH);
int
day =
cal.
get(
Calendar.
DAY_OF_MONTH);
int
hour =
cal.
get(
Calendar.
HOUR_OF_DAY);
int
min =
cal.
get(
Calendar.
MINUTE);
int
sec =
cal.
get(
Calendar.
SECOND);
int
ms =
cal.
get(
Calendar.
MILLISECOND);
cal.
setTimeZone(
tz);
cal.
set(
Calendar.
ERA,
era);
cal.
set(
Calendar.
YEAR,
year);
cal.
set(
Calendar.
MONTH,
month);
cal.
set(
Calendar.
DAY_OF_MONTH,
day);
cal.
set(
Calendar.
HOUR_OF_DAY,
hour);
cal.
set(
Calendar.
MINUTE,
min);
cal.
set(
Calendar.
SECOND,
sec);
cal.
set(
Calendar.
MILLISECOND,
ms);
return
cal.
getTimeInMillis();
}
private static boolean
isSimpleTimeZone(
String id) {
return
id.
startsWith("GMT") ||
id.
startsWith("UTC");
}
/**
* Extracts the date part from a timestamp.
*
* @param millis The timestamp from which to extract the date.
* @param tz The time zone of the date.
* @return The extracted date.
*/
public
Date convertToDate(long
millis,
TimeZone tz) {
// no adjustments for the inifity hack values
if (
millis <=
PGStatement.
DATE_NEGATIVE_INFINITY
||
millis >=
PGStatement.
DATE_POSITIVE_INFINITY) {
return new
Date(
millis);
}
if (
tz == null) {
tz =
getDefaultTz();
}
if (
isSimpleTimeZone(
tz.
getID())) {
// Truncate to 00:00 of the day.
// Suppose the input date is 7 Jan 15:40 GMT+02:00 (that is 13:40 UTC)
// We want it to become 7 Jan 00:00 GMT+02:00
// 1) Make sure millis becomes 15:40 in UTC, so add offset
int
offset =
tz.
getRawOffset();
millis +=
offset;
// 2) Truncate hours, minutes, etc. Day is always 86400 seconds, no matter what leap seconds
// are
millis =
millis /
ONEDAY *
ONEDAY;
// 2) Now millis is 7 Jan 00:00 UTC, however we need that in GMT+02:00, so subtract some
// offset
millis -=
offset;
// Now we have brand-new 7 Jan 00:00 GMT+02:00
return new
Date(
millis);
}
Calendar cal =
calendarWithUserTz;
cal.
setTimeZone(
tz);
cal.
setTimeInMillis(
millis);
cal.
set(
Calendar.
HOUR_OF_DAY, 0);
cal.
set(
Calendar.
MINUTE, 0);
cal.
set(
Calendar.
SECOND, 0);
cal.
set(
Calendar.
MILLISECOND, 0);
return new
Date(
cal.
getTimeInMillis());
}
/**
* Extracts the time part from a timestamp. This method ensures the date part of output timestamp
* looks like 1970-01-01 in given timezone.
*
* @param millis The timestamp from which to extract the time.
* @param tz timezone to use.
* @return The extracted time.
*/
public
Time convertToTime(long
millis,
TimeZone tz) {
if (
tz == null) {
tz =
getDefaultTz();
}
if (
isSimpleTimeZone(
tz.
getID())) {
// Leave just time part of the day.
// Suppose the input date is 2015 7 Jan 15:40 GMT+02:00 (that is 13:40 UTC)
// We want it to become 1970 1 Jan 15:40 GMT+02:00
// 1) Make sure millis becomes 15:40 in UTC, so add offset
int
offset =
tz.
getRawOffset();
millis +=
offset;
// 2) Truncate year, month, day. Day is always 86400 seconds, no matter what leap seconds are
millis =
millis %
ONEDAY;
// 2) Now millis is 1970 1 Jan 15:40 UTC, however we need that in GMT+02:00, so subtract some
// offset
millis -=
offset;
// Now we have brand-new 1970 1 Jan 15:40 GMT+02:00
return new
Time(
millis);
}
Calendar cal =
calendarWithUserTz;
cal.
setTimeZone(
tz);
cal.
setTimeInMillis(
millis);
cal.
set(
Calendar.
ERA,
GregorianCalendar.
AD);
cal.
set(
Calendar.
YEAR, 1970);
cal.
set(
Calendar.
MONTH, 0);
cal.
set(
Calendar.
DAY_OF_MONTH, 1);
return new
Time(
cal.
getTimeInMillis());
}
/**
* Returns the given time value as String matching what the current postgresql server would send
* in text mode.
*
* @param time time value
* @param withTimeZone whether timezone should be added
* @return given time value as String
*/
public
String timeToString(java.util.
Date time, boolean
withTimeZone) {
Calendar cal = null;
if (
withTimeZone) {
cal =
calendarWithUserTz;
cal.
setTimeZone(
timeZoneProvider.
get());
}
if (
time instanceof
Timestamp) {
return
toString(
cal, (
Timestamp)
time,
withTimeZone);
}
if (
time instanceof
Time) {
return
toString(
cal, (
Time)
time,
withTimeZone);
}
return
toString(
cal, (
Date)
time,
withTimeZone);
}
/**
* Converts the given postgresql seconds to java seconds. Reverse engineered by inserting varying
* dates to postgresql and tuning the formula until the java dates matched. See {@link #toPgSecs}
* for the reverse operation.
*
* @param secs Postgresql seconds.
* @return Java seconds.
*/
private static long
toJavaSecs(long
secs) {
// postgres epoc to java epoc
secs += 946684800L;
// Julian/Gregorian calendar cutoff point
if (
secs < -12219292800L) { // October 4, 1582 -> October 15, 1582
secs += 86400 * 10;
if (
secs < -14825808000L) { // 1500-02-28 -> 1500-03-01
int
extraLeaps = (int) ((
secs + 14825808000L) / 3155760000L);
extraLeaps--;
extraLeaps -=
extraLeaps / 4;
secs +=
extraLeaps * 86400L;
}
}
return
secs;
}
/**
* Converts the given java seconds to postgresql seconds. See {@link #toJavaSecs} for the reverse
* operation. The conversion is valid for any year 100 BC onwards.
*
* @param secs Postgresql seconds.
* @return Java seconds.
*/
private static long
toPgSecs(long
secs) {
// java epoc to postgres epoc
secs -= 946684800L;
// Julian/Greagorian calendar cutoff point
if (
secs < -13165977600L) { // October 15, 1582 -> October 4, 1582
secs -= 86400 * 10;
if (
secs < -15773356800L) { // 1500-03-01 -> 1500-02-28
int
years = (int) ((
secs + 15773356800L) / -3155823050L);
years++;
years -=
years / 4;
secs +=
years * 86400;
}
}
return
secs;
}
/**
* Converts the SQL Date to binary representation for {@link Oid#DATE}.
*
* @param tz The timezone used.
* @param bytes The binary encoded date value.
* @param value value
* @throws PSQLException If binary format could not be parsed.
*/
public void
toBinDate(
TimeZone tz, byte[]
bytes,
Date value) throws
PSQLException {
long
millis =
value.
getTime();
if (
tz == null) {
tz =
getDefaultTz();
}
// It "getOffset" is UNTESTED
// See org.postgresql.jdbc.AbstractJdbc2Statement.setDate(int, java.sql.Date,
// java.util.Calendar)
// The problem is we typically do not know for sure what is the exact required date/timestamp
// type
// Thus pgjdbc sticks to text transfer.
millis +=
tz.
getOffset(
millis);
long
secs =
toPgSecs(
millis / 1000);
ByteConverter.
int4(
bytes, 0, (int) (
secs / 86400));
}
/**
* Converts backend's TimeZone parameter to java format.
* Notable difference: backend's gmt-3 is GMT+03 in Java.
*
* @param timeZone time zone to use
* @return java TimeZone
*/
public static
TimeZone parseBackendTimeZone(
String timeZone) {
if (
timeZone.
startsWith("GMT")) {
TimeZone tz =
GMT_ZONES.
get(
timeZone);
if (
tz != null) {
return
tz;
}
}
return
TimeZone.
getTimeZone(
timeZone);
}
}