/* * Licensed 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 com.facebook.presto.spi.type; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; import java.io.IOException; import java.io.InputStream; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Properties; import java.util.Set; import java.util.TreeMap; import static java.lang.Character.isDigit; import static java.lang.Math.abs; import static java.lang.Math.max; import static java.util.Locale.ENGLISH; import static java.util.Objects.requireNonNull; public final class TimeZoneKey { public static final TimeZoneKey UTC_KEY = new TimeZoneKey("UTC", (short) 0); public static final short MAX_TIME_ZONE_KEY; private static final Map<String, TimeZoneKey> ZONE_ID_TO_KEY; private static final Set<TimeZoneKey> ZONE_KEYS; private static final TimeZoneKey[] TIME_ZONE_KEYS; private static final short OFFSET_TIME_ZONE_MIN = -14 * 60; private static final short OFFSET_TIME_ZONE_MAX = 14 * 60; private static final TimeZoneKey[] OFFSET_TIME_ZONE_KEYS = new TimeZoneKey[OFFSET_TIME_ZONE_MAX - OFFSET_TIME_ZONE_MIN + 1]; static { try (InputStream in = TimeZoneIndex.class.getResourceAsStream("zone-index.properties")) { // load zone file // todo parse file by hand since Properties ignores duplicate entries Properties data = new Properties() { @Override public synchronized Object put(Object key, Object value) { Object existingEntry = super.put(key, value); if (existingEntry != null) { throw new AssertionError("Zone file has duplicate entries for " + key); } return null; } }; data.load(in); if (data.containsKey("0")) { throw new AssertionError("Zone file should not contain a mapping for key 0"); } Map<String, TimeZoneKey> zoneIdToKey = new TreeMap<>(); zoneIdToKey.put(UTC_KEY.getId().toLowerCase(ENGLISH), UTC_KEY); short maxZoneKey = 0; for (Entry<Object, Object> entry : data.entrySet()) { short zoneKey = Short.valueOf(((String) entry.getKey()).trim()); String zoneId = ((String) entry.getValue()).trim(); maxZoneKey = (short) max(maxZoneKey, zoneKey); zoneIdToKey.put(zoneId.toLowerCase(ENGLISH), new TimeZoneKey(zoneId, zoneKey)); } MAX_TIME_ZONE_KEY = maxZoneKey; ZONE_ID_TO_KEY = Collections.unmodifiableMap(new LinkedHashMap<>(zoneIdToKey)); ZONE_KEYS = Collections.unmodifiableSet(new LinkedHashSet<>(zoneIdToKey.values())); TIME_ZONE_KEYS = new TimeZoneKey[maxZoneKey + 1]; for (TimeZoneKey timeZoneKey : zoneIdToKey.values()) { TIME_ZONE_KEYS[timeZoneKey.getKey()] = timeZoneKey; } for (short offset = OFFSET_TIME_ZONE_MIN; offset <= OFFSET_TIME_ZONE_MAX; offset++) { if (offset == 0) { continue; } String zoneId = zoneIdForOffset(offset); TimeZoneKey zoneKey = ZONE_ID_TO_KEY.get(zoneId); OFFSET_TIME_ZONE_KEYS[offset - OFFSET_TIME_ZONE_MIN] = zoneKey; } } catch (IOException e) { throw new AssertionError("Error loading time zone index file", e); } } public static Set<TimeZoneKey> getTimeZoneKeys() { return ZONE_KEYS; } @JsonCreator public static TimeZoneKey getTimeZoneKey(short timeZoneKey) { checkArgument(timeZoneKey < TIME_ZONE_KEYS.length && TIME_ZONE_KEYS[timeZoneKey] != null, "Invalid time zone key %d", timeZoneKey); return TIME_ZONE_KEYS[timeZoneKey]; } public static TimeZoneKey getTimeZoneKey(String zoneId) { requireNonNull(zoneId, "Zone id is null"); checkArgument(!zoneId.isEmpty(), "Zone id is an empty string"); TimeZoneKey zoneKey = ZONE_ID_TO_KEY.get(zoneId.toLowerCase(ENGLISH)); if (zoneKey == null) { zoneKey = ZONE_ID_TO_KEY.get(normalizeZoneId(zoneId)); } if (zoneKey == null) { throw new TimeZoneNotSupportedException(zoneId); } return zoneKey; } public static TimeZoneKey getTimeZoneKeyForOffset(long offsetMinutes) { if (offsetMinutes == 0) { return UTC_KEY; } checkArgument(offsetMinutes >= OFFSET_TIME_ZONE_MIN && offsetMinutes <= OFFSET_TIME_ZONE_MAX, "Invalid offset minutes %s", offsetMinutes); TimeZoneKey timeZoneKey = OFFSET_TIME_ZONE_KEYS[((int) offsetMinutes) - OFFSET_TIME_ZONE_MIN]; if (timeZoneKey == null) { throw new TimeZoneNotSupportedException(zoneIdForOffset(offsetMinutes)); } return timeZoneKey; } private final String id; private final short key; TimeZoneKey(String id, short key) { this.id = requireNonNull(id, "id is null"); if (key < 0) { throw new IllegalArgumentException("key is negative"); } this.key = key; } public String getId() { return id; } @JsonValue public short getKey() { return key; } @Override public int hashCode() { return Objects.hash(id, key); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } TimeZoneKey other = (TimeZoneKey) obj; return Objects.equals(this.id, other.id) && Objects.equals(this.key, other.key); } @Override public String toString() { return id; } public static boolean isUtcZoneId(String zoneId) { return normalizeZoneId(zoneId).equals("utc"); } private static String normalizeZoneId(String originalZoneId) { String zoneId = originalZoneId.toLowerCase(ENGLISH); if (zoneId.startsWith("etc/")) { zoneId = zoneId.substring(4); } if (isUtcEquivalentName(zoneId)) { return "utc"; } // // Normalize fixed offset time zones. // // In some zones systems, these will start with UTC, GMT or UT. int length = zoneId.length(); if (length > 3 && (zoneId.startsWith("utc") || zoneId.startsWith("gmt"))) { zoneId = zoneId.substring(3); length = zoneId.length(); } else if (length > 2 && zoneId.startsWith("ut")) { zoneId = zoneId.substring(2); length = zoneId.length(); } // (+/-)00:00 is UTC if ("+00:00".equals(zoneId) || "-00:00".equals(zoneId)) { return "utc"; } // if zoneId matches XXX:XX, it is likely +HH:mm, so just return it // since only offset time zones will contain a `:` character if (length == 6 && zoneId.charAt(3) == ':') { return zoneId; } // // Rewrite (+/-)H[H] to (+/-)HH:00 // if (length != 2 && length != 3) { return originalZoneId; } // zone must start with a plus or minus sign char signChar = zoneId.charAt(0); if (signChar != '+' && signChar != '-') { return originalZoneId; } // extract the tens and ones characters for the hour char hourTens; char hourOnes; if (length == 2) { hourTens = '0'; hourOnes = zoneId.charAt(1); } else { hourTens = zoneId.charAt(1); hourOnes = zoneId.charAt(2); } // do we have a valid hours offset time zone? if (!isDigit(hourTens) || !isDigit(hourOnes)) { return originalZoneId; } // is this offset 0 (e.g., UTC)? if (hourTens == '0' && hourOnes == '0') { return "utc"; } return "" + signChar + hourTens + hourOnes + ":00"; } private static boolean isUtcEquivalentName(String zoneId) { return zoneId.equals("utc") || zoneId.equals("z") || zoneId.equals("ut") || zoneId.equals("uct") || zoneId.equals("ut") || zoneId.equals("gmt") || zoneId.equals("gmt0") || zoneId.equals("greenwich") || zoneId.equals("universal") || zoneId.equals("zulu"); } private static String zoneIdForOffset(long offset) { return String.format("%s%02d:%02d", offset < 0 ? "-" : "+", abs(offset / 60), abs(offset % 60)); } private static void checkArgument(boolean check, String message, Object... args) { if (!check) { throw new IllegalArgumentException(String.format(message, args)); } } }