1 /* 2 * Copyright (C) 2012 The Libphonenumber Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.i18n.phonenumbers; 18 19 import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberType; 20 import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; 21 import com.google.i18n.phonenumbers.prefixmapper.PrefixTimeZonesMap; 22 23 import java.io.IOException; 24 import java.io.InputStream; 25 import java.io.ObjectInputStream; 26 import java.util.ArrayList; 27 import java.util.Collections; 28 import java.util.List; 29 import java.util.logging.Level; 30 import java.util.logging.Logger; 31 32 /** 33 * An offline mapper from phone numbers to time zones. 34 */ 35 public class PhoneNumberToTimeZonesMapper { 36 private static final String MAPPING_DATA_DIRECTORY = 37 "/com/google/i18n/phonenumbers/timezones/data/"; 38 private static final String MAPPING_DATA_FILE_NAME = "map_data"; 39 // This is defined by ICU as the unknown time zone. 40 private static final String UNKNOWN_TIMEZONE = "Etc/Unknown"; 41 // A list with the ICU unknown time zone as single element. 42 // @VisibleForTesting 43 static final List<String> UNKNOWN_TIME_ZONE_LIST = new ArrayList<String>(1); 44 static { 45 UNKNOWN_TIME_ZONE_LIST.add(UNKNOWN_TIMEZONE); 46 } 47 48 private static final Logger LOGGER = 49 Logger.getLogger(PhoneNumberToTimeZonesMapper.class.getName()); 50 51 private PrefixTimeZonesMap prefixTimeZonesMap = null; 52 53 // @VisibleForTesting PhoneNumberToTimeZonesMapper(String prefixTimeZonesMapDataDirectory)54 PhoneNumberToTimeZonesMapper(String prefixTimeZonesMapDataDirectory) { 55 this.prefixTimeZonesMap = loadPrefixTimeZonesMapFromFile( 56 prefixTimeZonesMapDataDirectory + MAPPING_DATA_FILE_NAME); 57 } 58 PhoneNumberToTimeZonesMapper(PrefixTimeZonesMap prefixTimeZonesMap)59 private PhoneNumberToTimeZonesMapper(PrefixTimeZonesMap prefixTimeZonesMap) { 60 this.prefixTimeZonesMap = prefixTimeZonesMap; 61 } 62 loadPrefixTimeZonesMapFromFile(String path)63 private static PrefixTimeZonesMap loadPrefixTimeZonesMapFromFile(String path) { 64 InputStream source = PhoneNumberToTimeZonesMapper.class.getResourceAsStream(path); 65 ObjectInputStream in = null; 66 PrefixTimeZonesMap map = new PrefixTimeZonesMap(); 67 try { 68 in = new ObjectInputStream(source); 69 map.readExternal(in); 70 } catch (IOException e) { 71 LOGGER.log(Level.WARNING, e.toString()); 72 } finally { 73 close(in); 74 } 75 return map; 76 } 77 close(InputStream in)78 private static void close(InputStream in) { 79 if (in != null) { 80 try { 81 in.close(); 82 } catch (IOException e) { 83 LOGGER.log(Level.WARNING, e.toString()); 84 } 85 } 86 } 87 88 /** 89 * Helper class used for lazy instantiation of a PhoneNumberToTimeZonesMapper. This also loads the 90 * map data in a thread-safe way. 91 */ 92 private static class LazyHolder { 93 private static final PhoneNumberToTimeZonesMapper INSTANCE; 94 static { 95 PrefixTimeZonesMap map = 96 loadPrefixTimeZonesMapFromFile(MAPPING_DATA_DIRECTORY + MAPPING_DATA_FILE_NAME); 97 INSTANCE = new PhoneNumberToTimeZonesMapper(map); 98 } 99 } 100 101 /** 102 * Gets a {@link PhoneNumberToTimeZonesMapper} instance. 103 * 104 * <p> The {@link PhoneNumberToTimeZonesMapper} is implemented as a singleton. Therefore, calling 105 * this method multiple times will only result in one instance being created. 106 * 107 * @return a {@link PhoneNumberToTimeZonesMapper} instance 108 */ getInstance()109 public static synchronized PhoneNumberToTimeZonesMapper getInstance() { 110 return LazyHolder.INSTANCE; 111 } 112 113 /** 114 * Returns a list of time zones to which a phone number belongs. 115 * 116 * <p>This method assumes the validity of the number passed in has already been checked, and that 117 * the number is geo-localizable. We consider fixed-line and mobile numbers possible candidates 118 * for geo-localization. 119 * 120 * @param number a valid phone number for which we want to get the time zones to which it belongs 121 * @return a list of the corresponding time zones or a single element list with the default 122 * unknown time zone if no other time zone was found or if the number was invalid 123 */ getTimeZonesForGeographicalNumber(PhoneNumber number)124 public List<String> getTimeZonesForGeographicalNumber(PhoneNumber number) { 125 return getTimeZonesForGeocodableNumber(number); 126 } 127 128 /** 129 * As per {@link #getTimeZonesForGeographicalNumber(PhoneNumber)} but explicitly checks 130 * the validity of the number passed in. 131 * 132 * @param number the phone number for which we want to get the time zones to which it belongs 133 * @return a list of the corresponding time zones or a single element list with the default 134 * unknown time zone if no other time zone was found or if the number was invalid 135 */ getTimeZonesForNumber(PhoneNumber number)136 public List<String> getTimeZonesForNumber(PhoneNumber number) { 137 PhoneNumberType numberType = PhoneNumberUtil.getInstance().getNumberType(number); 138 if (numberType == PhoneNumberType.UNKNOWN) { 139 return UNKNOWN_TIME_ZONE_LIST; 140 } else if (!canBeGeocoded(numberType)) { 141 return getCountryLevelTimeZonesforNumber(number); 142 } 143 return getTimeZonesForGeographicalNumber(number); 144 } 145 146 /** 147 * A similar method is implemented as PhoneNumberUtil.isNumberGeographical, which performs a 148 * stricter check, as it determines if a number has a geographical association. Also, if new 149 * phone number types were added, we should check if this other method should be updated too. 150 * TODO: Remove duplication by completing the logic in the method in PhoneNumberUtil. 151 * For more information, see the comments in that method. 152 */ canBeGeocoded(PhoneNumberType numberType)153 private boolean canBeGeocoded(PhoneNumberType numberType) { 154 return (numberType == PhoneNumberType.FIXED_LINE || 155 numberType == PhoneNumberType.MOBILE || 156 numberType == PhoneNumberType.FIXED_LINE_OR_MOBILE); 157 } 158 159 /** 160 * Returns a String with the ICU unknown time zone. 161 */ getUnknownTimeZone()162 public static String getUnknownTimeZone() { 163 return UNKNOWN_TIMEZONE; 164 } 165 166 /** 167 * Returns a list of time zones to which a geocodable phone number belongs. 168 * 169 * @param number the phone number for which we want to get the time zones to which it belongs 170 * @return the list of corresponding time zones or a single element list with the default 171 * unknown time zone if no other time zone was found or if the number was invalid 172 */ getTimeZonesForGeocodableNumber(PhoneNumber number)173 private List<String> getTimeZonesForGeocodableNumber(PhoneNumber number) { 174 List<String> timezones = prefixTimeZonesMap.lookupTimeZonesForNumber(number); 175 return Collections.unmodifiableList(timezones.isEmpty() ? UNKNOWN_TIME_ZONE_LIST 176 : timezones); 177 } 178 179 /** 180 * Returns the list of time zones corresponding to the country calling code of {@code number}. 181 * 182 * @param number the phone number to look up 183 * @return the list of corresponding time zones or a single element list with the default 184 * unknown time zone if no other time zone was found 185 */ getCountryLevelTimeZonesforNumber(PhoneNumber number)186 private List<String> getCountryLevelTimeZonesforNumber(PhoneNumber number) { 187 List<String> timezones = prefixTimeZonesMap.lookupCountryLevelTimeZonesForNumber(number); 188 return Collections.unmodifiableList(timezones.isEmpty() ? UNKNOWN_TIME_ZONE_LIST 189 : timezones); 190 } 191 } 192