/* * Copyright (C) 2012 The Libphonenumber Authors * * 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.google.i18n.phonenumbers; import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberType; import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; import com.google.i18n.phonenumbers.prefixmapper.PrefixTimeZonesMap; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; /** * An offline mapper from phone numbers to time zones. */ public class PhoneNumberToTimeZonesMapper { private static final String MAPPING_DATA_DIRECTORY = "/com/google/i18n/phonenumbers/timezones/data/"; private static final String MAPPING_DATA_FILE_NAME = "map_data"; // This is defined by ICU as the unknown time zone. private static final String UNKNOWN_TIMEZONE = "Etc/Unknown"; // A list with the ICU unknown time zone as single element. // @VisibleForTesting static final List<String> UNKNOWN_TIME_ZONE_LIST = new ArrayList<String>(1); static { UNKNOWN_TIME_ZONE_LIST.add(UNKNOWN_TIMEZONE); } private static final Logger LOGGER = Logger.getLogger(PhoneNumberToTimeZonesMapper.class.getName()); private PrefixTimeZonesMap prefixTimeZonesMap = null; // @VisibleForTesting PhoneNumberToTimeZonesMapper(String prefixTimeZonesMapDataDirectory) { this.prefixTimeZonesMap = loadPrefixTimeZonesMapFromFile( prefixTimeZonesMapDataDirectory + MAPPING_DATA_FILE_NAME); } private PhoneNumberToTimeZonesMapper(PrefixTimeZonesMap prefixTimeZonesMap) { this.prefixTimeZonesMap = prefixTimeZonesMap; } private static PrefixTimeZonesMap loadPrefixTimeZonesMapFromFile(String path) { InputStream source = PhoneNumberToTimeZonesMapper.class.getResourceAsStream(path); ObjectInputStream in = null; PrefixTimeZonesMap map = new PrefixTimeZonesMap(); try { in = new ObjectInputStream(source); map.readExternal(in); } catch (IOException e) { LOGGER.log(Level.WARNING, e.toString()); } finally { close(in); } return map; } private static void close(InputStream in) { if (in != null) { try { in.close(); } catch (IOException e) { LOGGER.log(Level.WARNING, e.toString()); } } } /** * Helper class used for lazy instantiation of a PhoneNumberToTimeZonesMapper. This also loads the * map data in a thread-safe way. */ private static class LazyHolder { private static final PhoneNumberToTimeZonesMapper INSTANCE; static { PrefixTimeZonesMap map = loadPrefixTimeZonesMapFromFile(MAPPING_DATA_DIRECTORY + MAPPING_DATA_FILE_NAME); INSTANCE = new PhoneNumberToTimeZonesMapper(map); } } /** * Gets a {@link PhoneNumberToTimeZonesMapper} instance. * * <p> The {@link PhoneNumberToTimeZonesMapper} is implemented as a singleton. Therefore, calling * this method multiple times will only result in one instance being created. * * @return a {@link PhoneNumberToTimeZonesMapper} instance */ public static synchronized PhoneNumberToTimeZonesMapper getInstance() { return LazyHolder.INSTANCE; } /** * Returns a list of time zones to which a phone number belongs. * * <p>This method assumes the validity of the number passed in has already been checked, and that * the number is geo-localizable. We consider fixed-line and mobile numbers possible candidates * for geo-localization. * * @param number a valid phone number for which we want to get the time zones to which it belongs * @return a list of the corresponding time zones or a single element list with the default * unknown time zone if no other time zone was found or if the number was invalid */ public List<String> getTimeZonesForGeographicalNumber(PhoneNumber number) { return getTimeZonesForGeocodableNumber(number); } /** * As per {@link #getTimeZonesForGeographicalNumber(PhoneNumber)} but explicitly checks * the validity of the number passed in. * * @param number the phone number for which we want to get the time zones to which it belongs * @return a list of the corresponding time zones or a single element list with the default * unknown time zone if no other time zone was found or if the number was invalid */ public List<String> getTimeZonesForNumber(PhoneNumber number) { PhoneNumberType numberType = PhoneNumberUtil.getInstance().getNumberType(number); if (numberType == PhoneNumberType.UNKNOWN) { return UNKNOWN_TIME_ZONE_LIST; } else if (!canBeGeocoded(numberType)) { return getCountryLevelTimeZonesforNumber(number); } return getTimeZonesForGeographicalNumber(number); } /** * A similar method is implemented as PhoneNumberUtil.isNumberGeographical, which performs a * stricter check, as it determines if a number has a geographical association. Also, if new * phone number types were added, we should check if this other method should be updated too. * TODO: Remove duplication by completing the logic in the method in PhoneNumberUtil. * For more information, see the comments in that method. */ private boolean canBeGeocoded(PhoneNumberType numberType) { return (numberType == PhoneNumberType.FIXED_LINE || numberType == PhoneNumberType.MOBILE || numberType == PhoneNumberType.FIXED_LINE_OR_MOBILE); } /** * Returns a String with the ICU unknown time zone. */ public static String getUnknownTimeZone() { return UNKNOWN_TIMEZONE; } /** * Returns a list of time zones to which a geocodable phone number belongs. * * @param number the phone number for which we want to get the time zones to which it belongs * @return the list of corresponding time zones or a single element list with the default * unknown time zone if no other time zone was found or if the number was invalid */ private List<String> getTimeZonesForGeocodableNumber(PhoneNumber number) { List<String> timezones = prefixTimeZonesMap.lookupTimeZonesForNumber(number); return Collections.unmodifiableList(timezones.isEmpty() ? UNKNOWN_TIME_ZONE_LIST : timezones); } /** * Returns the list of time zones corresponding to the country calling code of {@code number}. * * @param number the phone number to look up * @return the list of corresponding time zones or a single element list with the default * unknown time zone if no other time zone was found */ private List<String> getCountryLevelTimeZonesforNumber(PhoneNumber number) { List<String> timezones = prefixTimeZonesMap.lookupCountryLevelTimeZonesForNumber(number); return Collections.unmodifiableList(timezones.isEmpty() ? UNKNOWN_TIME_ZONE_LIST : timezones); } }