/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hadoop.hive.common.type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.format.TextStyle;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAccessor;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This is the internal type for Timestamp with time zone.
* A wrapper of ZonedDateTime which automatically convert the Zone to UTC.
* The full qualified input format of Timestamp with time zone is
* "yyyy-MM-dd HH:mm:ss[.SSS...] zoneid/zoneoffset", where the time and zone parts are optional.
* If time part is absent, a default '00:00:00.0' will be used.
* If zone part is absent, the system time zone will be used.
* All timestamp with time zone will be converted and stored as UTC retaining the instant.
* E.g. "2017-04-14 18:00:00 Asia/Shanghai" will be converted to
* "2017-04-14 10:00:00.0 Z".
*/
public class TimestampTZ implements Comparable<TimestampTZ> {
private static final DateTimeFormatter formatter;
private static final ZoneId UTC = ZoneOffset.UTC;
private static final ZonedDateTime EPOCH = ZonedDateTime.ofInstant(Instant.EPOCH, UTC);
private static final LocalTime DEFAULT_LOCAL_TIME = LocalTime.of(0, 0);
private static final Pattern SINGLE_DIGIT_PATTERN = Pattern.compile("[\\+-]\\d:\\d\\d");
private static final Logger LOG = LoggerFactory.getLogger(TimestampTZ.class);
private static final ThreadLocal<DateFormat> CONVERT_FORMATTER =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
static {
DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
// Date part
builder.append(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
// Time part
builder.optionalStart().appendLiteral(" ").append(DateTimeFormatter.ofPattern("HH:mm:ss")).
optionalStart().appendFraction(ChronoField.NANO_OF_SECOND, 1, 9, true).
optionalEnd().optionalEnd();
// Zone part
builder.optionalStart().appendLiteral(" ").optionalEnd();
builder.optionalStart().appendZoneText(TextStyle.NARROW).optionalEnd();
formatter = builder.toFormatter();
}
private ZonedDateTime zonedDateTime;
public TimestampTZ() {
this(EPOCH);
}
public TimestampTZ(ZonedDateTime zonedDateTime) {
setZonedDateTime(zonedDateTime);
}
public TimestampTZ(long seconds, int nanos) {
set(seconds, nanos);
}
public void set(long seconds, int nanos) {
Instant instant = Instant.ofEpochSecond(seconds, nanos);
setZonedDateTime(ZonedDateTime.ofInstant(instant, UTC));
}
public ZonedDateTime getZonedDateTime() {
return zonedDateTime;
}
public void setZonedDateTime(ZonedDateTime zonedDateTime) {
this.zonedDateTime = zonedDateTime != null ? zonedDateTime.withZoneSameInstant(UTC) : EPOCH;
}
@Override
public String toString() {
return zonedDateTime.format(formatter);
}
@Override
public int hashCode() {
return zonedDateTime.toInstant().hashCode();
}
@Override
public boolean equals(Object other) {
if (other instanceof TimestampTZ) {
return compareTo((TimestampTZ) other) == 0;
}
return false;
}
@Override
public int compareTo(TimestampTZ o) {
return zonedDateTime.toInstant().compareTo(o.zonedDateTime.toInstant());
}
public long getEpochSecond() {
return zonedDateTime.toInstant().getEpochSecond();
}
public int getNanos() {
return zonedDateTime.toInstant().getNano();
}
public static TimestampTZ parse(String s) {
// need to handle offset with single digital hour, see JDK-8066806
s = handleSingleDigitHourOffset(s);
ZonedDateTime zonedDateTime;
try {
zonedDateTime = ZonedDateTime.parse(s, formatter);
} catch (DateTimeParseException e) {
// try to be more tolerant
// if the input is invalid instead of incomplete, we'll hit exception here again
TemporalAccessor accessor = formatter.parse(s);
// LocalDate must be present
LocalDate localDate = LocalDate.from(accessor);
LocalTime localTime;
ZoneId zoneId;
try {
localTime = LocalTime.from(accessor);
} catch (DateTimeException e1) {
localTime = DEFAULT_LOCAL_TIME;
}
try {
zoneId = ZoneId.from(accessor);
} catch (DateTimeException e2) {
// TODO: in future this may come from user specified zone (via set time zone command)
zoneId = ZoneId.systemDefault();
}
zonedDateTime = ZonedDateTime.of(localDate, localTime, zoneId);
}
return new TimestampTZ(zonedDateTime);
}
private static String handleSingleDigitHourOffset(String s) {
Matcher matcher = SINGLE_DIGIT_PATTERN.matcher(s);
if (matcher.find()) {
int index = matcher.start() + 1;
s = s.substring(0, index) + "0" + s.substring(index, s.length());
}
return s;
}
public static TimestampTZ parseOrNull(String s) {
try {
return parse(s);
} catch (DateTimeParseException e) {
if (LOG.isDebugEnabled()) {
LOG.debug("Invalid string " + s + " for TIMESTAMP WITH TIME ZONE", e);
}
return null;
}
}
// Converts Date to TimestampTZ. The conversion is done text-wise since
// Date/Timestamp should be treated as description of date/time.
public static TimestampTZ convert(java.util.Date date) {
String s = date instanceof Timestamp ? date.toString() : CONVERT_FORMATTER.get().format(date);
// TODO: in future this may come from user specified zone (via set time zone command)
return parse(s + " " + ZoneId.systemDefault().getId());
}
}