org.firebirdsql.gds.ng.tz.TimeZoneMapping Maven / Gradle / Ivy
Show all versions of jaybird Show documentation
/*
* Firebird Open Source JDBC Driver
*
* Distributable under LGPL license.
* You may obtain a copy of the License at http://www.gnu.org/copyleft/lgpl.html
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* LGPL License for more details.
*
* This file was created by members of the firebird development team.
* All individual contributions remain the Copyright (C) of those
* individuals. Contributors to this file are either listed here or
* can be obtained from a source control history command.
*
* All rights reserved.
*/
package org.firebirdsql.gds.ng.tz;
import org.firebirdsql.jaybird.util.StringUtils;
import java.io.IOException;
import java.io.InputStream;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* Mapping of Firebird time zone ids.
*
* @author Mark Rotteveel
* @since 4.0
*/
public final class TimeZoneMapping {
private final System.Logger logger = System.getLogger(TimeZoneMapping.class.getName());
private static final TimeZoneMapping INSTANCE = new TimeZoneMapping();
private static final int MAX_ZONE_ID = 65535;
private static final int MAX_OFFSET = 1439; // 23h:59m
private static final int MIN_OFFSET = -MAX_OFFSET;
private static final int MAX_OFFSET_SUPPORTED = 1080; // 18h:00m; derived from ZoneOffset limitations
private static final int MIN_OFFSET_SUPPORTED = -MAX_OFFSET_SUPPORTED;
private static final int MAX_OFFSET_ENCODED = 2878;
private static final int OFFSET_CORRECTION = 1439;
private static final int OFFSET_UTC = 0;
private static final int OFFSET_UTC_ENCODED = OFFSET_UTC + OFFSET_CORRECTION;
private static final int MAX_OFFSET_SUPPORTED_ENCODED = MAX_OFFSET_SUPPORTED + OFFSET_CORRECTION;
private static final int MIN_OFFSET_SUPPORTED_ENCODED = MIN_OFFSET_SUPPORTED + OFFSET_CORRECTION;
/**
* Maximum number of zone offsets cached. If a 25th needs to be cached, we clear the cache and start afresh.
*
* Given the general practice of having full hour offsets and the limited number of zones per applications, this
* should be more than sufficient for most applications.
*
*
* @see #cacheOffset(Integer, ZoneOffset)
*/
private static final int MAX_CACHED_ZONE_OFFSETS = 24;
/**
* Maximum number of named zones cached. If an 11th needs to be cached, we clear the cache and start afresh.
*
* Applies to both {@code namedZoneCache} and {@code namedZoneIdCache}.
*
*
* @see #cacheOffset(Integer, ZoneOffset)
*/
private static final int MAX_CACHED_NAMED_ZONES = 10;
private static final String FALLBACK_ZONE = "UTC";
private static final ZoneId FALLBACK_ZONE_ID = ZoneOffset.UTC;
static final String KEY_MIN_ZONE_ID = "min_zone_id";
private static final String FIREBIRD_TIME_ZONE_MAPPING_PROPERTIES = "firebird_time_zone_mapping.properties";
/**
* List of time zone names so that the index in the list corresponds to {@code 0xFFFF - RDB$TIME_ZONE_ID} (see
* {@link #internalId(int)}).
*
* When updating the list, make sure to update {@code min_zone_id} in the properties file and the
* {@code TimeZoneByNameMappingTest}. Also verify if all instances are mapped (or otherwise where necessary, update
* names to equivalent names). See also file {@code firebird_time_zone_mapping.properties}.
*
*/
private final List timeZoneNameById = loadTimeZoneNameById();
private final Map offsetCache = new ConcurrentHashMap<>(MAX_CACHED_ZONE_OFFSETS);
private final Map namedZoneCache = new ConcurrentHashMap<>(MAX_CACHED_NAMED_ZONES);
private final Map namedZoneIdCache = new ConcurrentHashMap<>(MAX_CACHED_NAMED_ZONES);
public static TimeZoneMapping getInstance() {
return INSTANCE;
}
/**
* Maps a Firebird time zone id to a suitable Java {@link java.time.ZoneId}.
*
* The returned value is either a named time zone (zone region), or an offset zone ({@link java.time.ZoneOffset}).
*
*
* @param timeZoneId
* Firebird time zone id (valid between between 0 and 65535)
* @return {@code java.time.Zone} equivalent (out of range values or unmapped ids will return {@code UTC}).
*/
public ZoneId timeZoneById(final int timeZoneId) {
if (timeZoneId < 0 || timeZoneId > MAX_ZONE_ID) {
return defaultForOutOfRange(timeZoneId);
}
return timeZoneId > MAX_OFFSET_ENCODED
? namedTimeZoneById(timeZoneId)
: offsetTimeZoneFromId(timeZoneId);
}
/**
* Determines if {@code timeZoneId} is an offset time zone or - possibly - a named time zone.
*
* @param timeZoneId
* Firebird time zone id
* @return {@code true} if this is an offset time zone, {@code false} otherwise
*/
public boolean isOffsetTimeZone(final int timeZoneId) {
return timeZoneId >= 0 && timeZoneId <= MAX_OFFSET_ENCODED;
}
/**
* Determines if {@code timeZoneId} is an offset time zone in the range supported by Jaybird [-18:00, +18:00].
*
* @param timeZoneId
* Firebird time zone id
* @return {@code true} if this is an offset time zone in the range [-18:00, +18:00]
*/
public boolean isSupportedOffsetTimezone(final int timeZoneId) {
return timeZoneId >= MIN_OFFSET_SUPPORTED_ENCODED && timeZoneId <= MAX_OFFSET_SUPPORTED_ENCODED;
}
/**
* Return the offset in minutes for an offset time zone id
*
* @param timeZoneId
* Offset time zone id (in range [0, 2878])
* @return Offset in minutes, out of range values for {@code timeZoneId} will return {@code 0} for UTC/GMT.
*/
public int toOffsetMinutes(final int timeZoneId) {
if (isOffsetTimeZone(timeZoneId)) {
return getOffsetMinutesUnchecked(timeZoneId);
}
logInvalidOffsetTimeZoneId(timeZoneId);
return OFFSET_UTC;
}
private void logInvalidOffsetTimeZoneId(int timeZoneId) {
if (logger.isLoggable(System.Logger.Level.WARNING)) {
String message = "Provided timezone id " + timeZoneId + " is not a valid offset time zone. "
+ "Valid range is [0, " + MAX_OFFSET_ENCODED + "]. Returning offset 0 (UTC) instead.";
logger.log(System.Logger.Level.WARNING, message + " See debug level for location.");
if (logger.isLoggable(System.Logger.Level.DEBUG)) {
logger.log(System.Logger.Level.DEBUG, message, new RuntimeException("debugging stacktrace"));
}
}
}
/**
* Returns the Firebird time zone id for a {@link java.time.ZoneOffset}.
*
* @param zoneOffset
* The zone offset
* @return Firebird time zone id
* @see #toTimeZoneId(ZoneId)
*/
public int toTimeZoneId(ZoneOffset zoneOffset) {
return toTimeZoneId(zoneOffset.getTotalSeconds() / 60);
}
/**
* Returns the Firebird time zone id for a {@link java.time.ZoneId}.
*
* This method handles both {@link ZoneOffset} and named regions.
*
*
* @param zoneId
* The zone id
* @return Firebird time zone id
* @since 4.0.1
*/
public int toTimeZoneId(ZoneId zoneId) {
if (zoneId instanceof ZoneOffset zoneOffset) {
return toTimeZoneId(zoneOffset);
}
Integer firebirdId = namedZoneIdCache.get(zoneId);
if (firebirdId != null) {
return firebirdId;
}
String zoneIdName = zoneId.getId();
for (int internalId = 0; internalId < timeZoneNameById.size(); internalId++) {
String candidateName = timeZoneNameById.get(internalId);
if (Objects.equals(zoneIdName, candidateName)) {
firebirdId = timeZoneId(internalId);
break;
}
}
if (firebirdId == null) {
// fallback to offset encoded at UTC
firebirdId = OFFSET_UTC_ENCODED;
}
cacheNamedZoneId(zoneId, firebirdId);
return firebirdId;
}
/**
* Return the Firebird time zone id for an offset in minutes.
*
* @param offsetMinutes
* Offset in minutes valid range [-1439, 1439] (or [-23:59, +23:59])
* @return Time zone id encoding the specified offset, out of range values will return id for offset {@code 0} (UTC)
*/
public int toTimeZoneId(final int offsetMinutes) {
if (offsetMinutes < MIN_OFFSET || offsetMinutes > MAX_OFFSET) {
logInvalidOffsetMinutes(offsetMinutes);
return OFFSET_UTC_ENCODED;
}
return OFFSET_CORRECTION + offsetMinutes;
}
private void logInvalidOffsetMinutes(int offsetMinutes) {
if (logger.isLoggable(System.Logger.Level.WARNING)) {
String message = "Offset value " + offsetMinutes + " out of range [" + MIN_OFFSET + ", " + MAX_OFFSET
+ "]. Returning id for offset 0 instead.";
logger.log(System.Logger.Level.WARNING, message + " See debug level for location");
if (logger.isLoggable(System.Logger.Level.DEBUG)) {
logger.log(System.Logger.Level.DEBUG, message, new RuntimeException("debugging stacktrace"));
}
}
}
private ZoneId offsetTimeZoneFromId(final int timeZoneId) {
if (timeZoneId < MIN_OFFSET_SUPPORTED_ENCODED || timeZoneId > MAX_OFFSET_SUPPORTED_ENCODED) {
return FALLBACK_ZONE_ID;
}
final Integer key = timeZoneId;
ZoneOffset offsetId = offsetCache.get(key);
if (offsetId != null) {
return offsetId;
}
offsetId = calculateOffsetTimeZone(timeZoneId);
cacheOffset(key, offsetId);
return offsetId;
}
private void cacheOffset(final Integer key, final ZoneOffset offsetId) {
if (offsetCache.size() >= MAX_CACHED_ZONE_OFFSETS) {
offsetCache.clear();
}
offsetCache.put(key, offsetId);
}
private ZoneOffset calculateOffsetTimeZone(final int timeZoneId) {
int offset = Math.abs(getOffsetMinutesUnchecked(timeZoneId));
int negative = timeZoneId < OFFSET_CORRECTION ? -1 : 1;
int hours = negative * (offset / 60);
int minutes = negative * (offset % 60);
return ZoneOffset.ofHoursMinutes(hours, minutes);
}
private int getOffsetMinutesUnchecked(final int timeZoneId) {
return timeZoneId - OFFSET_CORRECTION;
}
private ZoneId namedTimeZoneById(final int timeZoneId) {
final Integer key = timeZoneId;
ZoneId zoneId = namedZoneCache.get(timeZoneId);
if (zoneId != null) {
return zoneId;
}
final int internalId = internalId(timeZoneId);
if (internalId < 0 || internalId >= timeZoneNameById.size()) {
return defaultForOutOfRange(timeZoneId);
}
final String zoneIdName = timeZoneNameById.get(internalId);
zoneId = zoneIdName != null ? ZoneId.of(zoneIdName, ZoneId.SHORT_IDS) : defaultForOutOfRange(timeZoneId);
cacheNamedZone(key, zoneId);
return zoneId;
}
private void cacheNamedZone(final Integer key, final ZoneId zoneId) {
if (namedZoneCache.size() >= MAX_CACHED_NAMED_ZONES) {
namedZoneCache.clear();
}
namedZoneCache.put(key, zoneId);
}
private void cacheNamedZoneId(final ZoneId key, final Integer firebirdId) {
if (namedZoneIdCache.size() >= MAX_CACHED_NAMED_ZONES) {
namedZoneIdCache.clear();
}
namedZoneIdCache.put(key, firebirdId);
}
private ZoneId defaultForOutOfRange(final int timeZoneId) {
logOutOfRange(timeZoneId);
return FALLBACK_ZONE_ID;
}
private void logOutOfRange(int timeZoneId) {
if (logger.isLoggable(System.Logger.Level.WARNING)) {
String message = "Unmapped or out of range timezone id received, defaulting to " + FALLBACK_ZONE
+ ", was id: " + timeZoneId + ".";
logger.log(System.Logger.Level.WARNING, message + " See debug level for location.");
if (logger.isLoggable(System.Logger.Level.DEBUG)) {
logger.log(System.Logger.Level.DEBUG, message, new RuntimeException("debugging stacktrace"));
}
}
}
/**
* Maps the Firebird time zone id to index for lookup in {@link #timeZoneNameById}.
*
* @param timeZoneId
* Firebird time zone id
* @return Index suitable for lookup in {@link #timeZoneNameById} (value may be out of range for invalid ids)
*/
private static int internalId(final int timeZoneId) {
return MAX_ZONE_ID - timeZoneId;
}
/**
* Maps an internal id to the Firebird time zone id.
*
* Inverse of {@link #internalId(int)}.
*
*
* @param internalId
* Internal id (index for lookup)
* @return Firebird time zone id.
*/
private static int timeZoneId(final int internalId) {
// if internalId = MAX_ZONE_ID - timeZoneId then timeZoneId = MAX_ZONE_ID - internalId
return MAX_ZONE_ID - internalId;
}
private static List loadTimeZoneNameById() {
// TODO Just hard code it instead?
try {
final Properties timeZoneMapping = loadTimeZoneMapping();
final int minZoneId = Integer.parseInt(timeZoneMapping.getProperty(KEY_MIN_ZONE_ID));
final int zoneIdCount = internalId(minZoneId) + 1;
final String[] zonesById = new String[zoneIdCount];
for (int internalId = 0; internalId < zoneIdCount; internalId++) {
final String timeZoneId = String.valueOf(timeZoneId(internalId));
final String timeZoneName = StringUtils.trimToNull(timeZoneMapping.getProperty(timeZoneId));
zonesById[internalId] = timeZoneName != null ? timeZoneName : FALLBACK_ZONE;
}
return List.of(zonesById);
} catch (Exception e) {
System.getLogger(TimeZoneMapping.class.getName()).log(System.Logger.Level.WARNING,
"Unable to load Firebird time zone id to name mapping, only offset timezone support will be "
+ "available ", e);
// Populating with 65535 (internalId 0) == GMT
return Collections.singletonList("GMT");
}
}
/**
* Loads the time zone mapping resource as a {@link Properties} object.
*
* @return Properties object with the time zone mapping
* @throws IOException
* For issues loading the time zone mapping
*/
static Properties loadTimeZoneMapping() throws IOException {
try (InputStream in = TimeZoneMapping.class.getResourceAsStream(FIREBIRD_TIME_ZONE_MAPPING_PROPERTIES)) {
Properties timeZoneMapping = new Properties();
timeZoneMapping.load(in);
return timeZoneMapping;
}
}
}