blob: ed081ca8c3e8d4c7977a2d7c15f1877ebf9fed81 [file] [log] [blame]
/*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.sis.util.privy;
import java.util.Date;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.LocalTime;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.temporal.Temporal;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAccessor;
import java.time.chrono.ChronoZonedDateTime;
import org.apache.sis.util.resources.Errors;
/**
* A date which is wrapping a {@code java.time} temporal object.
* This is used for interoperability in a situation where we are mixing
* legacy API working on {@link Date} with newer API working on {@link Temporal}.
*
* <h2>Design note</h2>
* This class intentionally don't implement {@link Temporal} in order to force unwrapping
* if a temporal object is desired.
*
* @author Martin Desruisseaux (Geomatys)
*/
public final class TemporalDate extends Date { // Intentionally do not implement Temporal.
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = 8239300258490556354L;
/**
* The temporal object wrapped by this date.
*/
@SuppressWarnings("serial") // Most implementations are serializable.
private final Temporal temporal;
/**
* Creates a new date for the given instant.
*
* @param temporal the temporal object to wrap in a new date.
* @throws ArithmeticException if numeric overflow occurs.
*/
private TemporalDate(final Instant temporal) {
super(temporal.toEpochMilli());
this.temporal = temporal;
}
/**
* Creates a new date for the given temporal.
*
* @param temporal the temporal object to wrap in a new date.
* @throws ArithmeticException if numeric overflow occurs.
*/
private TemporalDate(final Temporal temporal) {
super(toInstant(temporal, ZoneOffset.UTC).toEpochMilli());
this.temporal = temporal;
}
/**
* Returns the given temporal object as a date.
* Used for interoperability in situations where old and new Java API are mixed.
*
* @param time the temporal object to return as a date, or {@code null}.
* @return the given temporal object as a date, or {@code null} if the given argument was null.
* @throws ArithmeticException if numeric overflow occurs.
*/
public static Date toDate(final Temporal time) {
return (time == null) ? null : new TemporalDate(time);
}
/**
* Returns the given temporal object as a date.
* Used for interoperability in situations where old and new Java API are mixed.
*
* @param time the temporal object to return as a date, or {@code null}.
* @return the given temporal object as a date, or {@code null} if the given argument was null.
* @throws ArithmeticException if numeric overflow occurs.
*/
public static Date toDate(final Instant time) {
return (time == null) ? null : new TemporalDate(time);
}
/**
* Returns the given date as a temporal object.
* Used for interoperability in situations where old and new Java API are mixed.
*
* @param time the date to return as a temporal object, or {@code null}.
* @return the given date as a temporal object, or {@code null} if the given argument was null.
*/
public static Temporal toTemporal(final Date time) {
return (time == null) ? null : (time instanceof TemporalDate) ? ((TemporalDate) time).temporal : time.toInstant();
}
/**
* Returns the given date as an instant object.
* Used for interoperability in situations where old and new Java API are mixed.
*
* @param time the date to return as an instant object, or {@code null}.
* @return the given date as an instant object, or {@code null} if the given argument was null.
*/
public static Instant toInstant(final Date time) {
return (time == null) ? null : time.toInstant();
}
/**
* Converts the given temporal object into an instant.
* If the timezone is unspecified, then UTC is assumed.
*
* @param date the temporal object to convert, or {@code null}.
* @param zone the timezone to use if the time is local, or {@code null} if none.
* @return the instant for the given temporal object, or {@code null} if the argument was null.
* @throws DateTimeException if the given date does not support a field required by this method.
*/
public static Instant toInstant(final TemporalAccessor date, final ZoneId zone) {
if (date == null) {
return null;
}
if (date instanceof Instant) {
return (Instant) date;
} else if (date instanceof OffsetDateTime) {
return ((OffsetDateTime) date).toInstant();
} else if (date instanceof ChronoZonedDateTime) {
return ((ChronoZonedDateTime) date).toInstant();
} else if (zone != null) {
if (date instanceof LocalDateTime) {
final var t = (LocalDateTime) date;
if (zone instanceof ZoneOffset) {
return t.atOffset((ZoneOffset) zone).toInstant();
} else {
return t.atZone(zone).toInstant();
}
} else if (date instanceof LocalDate) {
final var t = (LocalDate) date;
return t.atStartOfDay(zone).toInstant();
}
}
Instant time;
final ChronoField nano;
if (zone == null || date.isSupported(ChronoField.INSTANT_SECONDS)) {
time = Instant.ofEpochSecond(date.getLong(ChronoField.INSTANT_SECONDS));
nano = ChronoField.NANO_OF_SECOND;
} else if (zone.equals(ZoneOffset.UTC)) {
// Note that the timezone of the temporal value is unknown here. We assume UTC.
time = Instant.ofEpochSecond(Math.multiplyExact(date.getLong(ChronoField.EPOCH_DAY), Constants.SECONDS_PER_DAY));
nano = ChronoField.NANO_OF_DAY;
} else {
throw new DateTimeException(Errors.format(Errors.Keys.CanNotConvertFromType_2, date.getClass(), Instant.class));
}
if (date.isSupported(nano)) {
time = time.plusNanos(date.getLong(nano));
}
return time;
}
/**
* Returns this date as an instant.
*/
@Override
public Instant toInstant() {
if (temporal instanceof Instant) {
return (Instant) temporal;
}
return super.toInstant();
}
/**
* Adds the given amount of seconds to the given instant.
*
* @param time the instant to which to add seconds, or {@code null}.
* @param value number of seconds.
* @return the shifted time, or {@code null} if the given instant was null or the given value was NaN.
*/
public static Instant addSeconds(final Instant time, final double value) {
if (time == null || Double.isNaN(value)) {
return null;
}
final long r = Math.round(value);
return time.plusSeconds(r).plusNanos(Math.round((value - r) * Constants.NANOS_PER_SECOND));
}
/**
* Returns {@code true} if objects of the given class have day, month and hour fields.
* This method is defined here for having a single class where to concentrate such heuristic rules.
* Note that {@link Instant} does not have date fields.
*
* @param date class of object to test (may be {@code null}).
* @return whether the given class is {@link LocalDate} or one of the classes with date + time.
* This list may be expanded in future versions.
*/
public static boolean hasDateFields(final Class<?> date) {
return date == LocalDate.class
|| date == LocalDateTime.class
|| date == OffsetDateTime.class
|| date == ZonedDateTime.class;
}
/**
* Returns {@code true} if objects of the given class have time fields.
* This method is defined here for having a single class where to concentrate such heuristic rules.
* Note that {@link Instant} does not have hour fields.
*
* @param date class of object to test (may be {@code null}).
* @return whether the given class is {@link LocalTime}, {@link OffsetTime} or one of the classes with date + time.
* This list may be expanded in future versions.
*/
public static boolean hasTimeFields(final Class<?> date) {
return date == LocalTime.class
|| date == OffsetTime.class
|| date == LocalDateTime.class
|| date == OffsetDateTime.class
|| date == ZonedDateTime.class;
}
}