9281a13ce168f457a5473432bfa0ad9bc6ddcc37
[platform/upstream/libphonenumber.git] / java / geocoder / src / com / google / i18n / phonenumbers / geocoding / PhoneNumberOfflineGeocoder.java
1 /*
2  * Copyright (C) 2011 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.geocoding;
18
19 import com.google.i18n.phonenumbers.PhoneNumberUtil;
20 import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberType;
21 import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
22
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.io.ObjectInputStream;
26 import java.util.HashMap;
27 import java.util.Locale;
28 import java.util.Map;
29 import java.util.logging.Level;
30 import java.util.logging.Logger;
31
32 /**
33  * An offline geocoder which provides geographical information related to a phone number.
34  *
35  * @author Shaopeng Jia
36  */
37 public class PhoneNumberOfflineGeocoder {
38   private static PhoneNumberOfflineGeocoder instance = null;
39   private static final String MAPPING_DATA_DIRECTORY =
40       "/com/google/i18n/phonenumbers/geocoding/data/";
41   private static final Logger LOGGER = Logger.getLogger(PhoneNumberOfflineGeocoder.class.getName());
42
43   private final PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
44   private final String phonePrefixDataDirectory;
45
46   // The mappingFileProvider knows for which combination of countryCallingCode and language a phone
47   // prefix mapping file is available in the file system, so that a file can be loaded when needed.
48   private MappingFileProvider mappingFileProvider = new MappingFileProvider();
49
50   // A mapping from countryCallingCode_lang to the corresponding phone prefix map that has been
51   // loaded.
52   private Map<String, AreaCodeMap> availablePhonePrefixMaps = new HashMap<String, AreaCodeMap>();
53
54   // @VisibleForTesting
55   PhoneNumberOfflineGeocoder(String phonePrefixDataDirectory) {
56     this.phonePrefixDataDirectory = phonePrefixDataDirectory;
57     loadMappingFileProvider();
58   }
59
60   private void loadMappingFileProvider() {
61     InputStream source =
62         PhoneNumberOfflineGeocoder.class.getResourceAsStream(phonePrefixDataDirectory + "config");
63     ObjectInputStream in = null;
64     try {
65       in = new ObjectInputStream(source);
66       mappingFileProvider.readExternal(in);
67     } catch (IOException e) {
68       LOGGER.log(Level.WARNING, e.toString());
69     } finally {
70       close(in);
71     }
72   }
73
74   private AreaCodeMap getPhonePrefixDescriptions(
75       int prefixMapKey, String language, String script, String region) {
76     String fileName = mappingFileProvider.getFileName(prefixMapKey, language, script, region);
77     if (fileName.length() == 0) {
78       return null;
79     }
80     if (!availablePhonePrefixMaps.containsKey(fileName)) {
81       loadAreaCodeMapFromFile(fileName);
82     }
83     return availablePhonePrefixMaps.get(fileName);
84   }
85
86   private void loadAreaCodeMapFromFile(String fileName) {
87     InputStream source =
88         PhoneNumberOfflineGeocoder.class.getResourceAsStream(phonePrefixDataDirectory + fileName);
89     ObjectInputStream in = null;
90     try {
91       in = new ObjectInputStream(source);
92       AreaCodeMap map = new AreaCodeMap();
93       map.readExternal(in);
94       availablePhonePrefixMaps.put(fileName, map);
95     } catch (IOException e) {
96       LOGGER.log(Level.WARNING, e.toString());
97     } finally {
98       close(in);
99     }
100   }
101
102   private static void close(InputStream in) {
103     if (in != null) {
104       try {
105         in.close();
106       } catch (IOException e) {
107         LOGGER.log(Level.WARNING, e.toString());
108       }
109     }
110   }
111
112   /**
113    * Gets a {@link PhoneNumberOfflineGeocoder} instance to carry out international phone number
114    * geocoding.
115    *
116    * <p> The {@link PhoneNumberOfflineGeocoder} is implemented as a singleton. Therefore, calling
117    * this method multiple times will only result in one instance being created.
118    *
119    * @return  a {@link PhoneNumberOfflineGeocoder} instance
120    */
121   public static synchronized PhoneNumberOfflineGeocoder getInstance() {
122     if (instance == null) {
123       instance = new PhoneNumberOfflineGeocoder(MAPPING_DATA_DIRECTORY);
124     }
125     return instance;
126   }
127
128   /**
129    * Returns the customary display name in the given language for the given territory the phone
130    * number is from.
131    */
132   private String getCountryNameForNumber(PhoneNumber number, Locale language) {
133     String regionCode = phoneUtil.getRegionCodeForNumber(number);
134     return getRegionDisplayName(regionCode, language);
135   }
136
137   /**
138    * Returns the customary display name in the given language for the given region.
139    */
140   private String getRegionDisplayName(String regionCode, Locale language) {
141     return (regionCode == null || regionCode.equals("ZZ") ||
142             regionCode.equals(PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY))
143         ? "" : new Locale("", regionCode).getDisplayCountry(language);
144   }
145
146   /**
147    * Returns a text description for the given phone number, in the language provided. The
148    * description might consist of the name of the country where the phone number is from, or the
149    * name of the geographical area the phone number is from if more detailed information is
150    * available.
151    *
152    * <p>This method assumes the validity of the number passed in has already been checked, and that
153    * the number is suitable for geocoding. We consider fixed-line and mobile numbers possible
154    * candidates for geocoding.
155    *
156    * @param number  a valid phone number for which we want to get a text description
157    * @param languageCode  the language code for which the description should be written
158    * @return  a text description for the given language code for the given phone number
159    */
160   public String getDescriptionForValidNumber(PhoneNumber number, Locale languageCode) {
161     String langStr = languageCode.getLanguage();
162     String scriptStr = "";  // No script is specified
163     String regionStr = languageCode.getCountry();
164
165     String areaDescription =
166         getAreaDescriptionForNumber(number, langStr, scriptStr, regionStr);
167     return (areaDescription.length() > 0)
168         ? areaDescription : getCountryNameForNumber(number, languageCode);
169   }
170
171   /**
172    * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale)} but also considers the
173    * region of the user. If the phone number is from the same region as the user, only a lower-level
174    * description will be returned, if one exists. Otherwise, the phone number's region will be
175    * returned, with optionally some more detailed information.
176    *
177    * <p>For example, for a user from the region "US" (United States), we would show "Mountain View,
178    * CA" for a particular number, omitting the United States from the description. For a user from
179    * the United Kingdom (region "GB"), for the same number we may show "Mountain View, CA, United
180    * States" or even just "United States".
181    *
182    * <p>This method assumes the validity of the number passed in has already been checked.
183    *
184    * @param number  the phone number for which we want to get a text description
185    * @param languageCode  the language code for which the description should be written
186    * @param userRegion  the region code for a given user. This region will be omitted from the
187    *     description if the phone number comes from this region. It is a two-letter uppercase ISO
188    *     country code as defined by ISO 3166-1.
189    * @return  a text description for the given language code for the given phone number, or empty
190    *     string if the number passed in is invalid
191    */
192   public String getDescriptionForValidNumber(PhoneNumber number, Locale languageCode,
193                                              String userRegion) {
194     // If the user region matches the number's region, then we just show the lower-level
195     // description, if one exists - if no description exists, we will show the region(country) name
196     // for the number.
197     String regionCode = phoneUtil.getRegionCodeForNumber(number);
198     if (userRegion.equals(regionCode)) {
199       return getDescriptionForValidNumber(number, languageCode);
200     }
201     // Otherwise, we just show the region(country) name for now.
202     return getRegionDisplayName(regionCode, languageCode);
203     // TODO: Concatenate the lower-level and country-name information in an appropriate
204     // way for each language.
205   }
206
207   /**
208    * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale)} but explicitly checks
209    * the validity of the number passed in.
210    *
211    * @param number  the phone number for which we want to get a text description
212    * @param languageCode  the language code for which the description should be written
213    * @return  a text description for the given language code for the given phone number, or empty
214    *     string if the number passed in is invalid
215    */
216   public String getDescriptionForNumber(PhoneNumber number, Locale languageCode) {
217     PhoneNumberType numberType = phoneUtil.getNumberType(number);
218     if (numberType == PhoneNumberType.UNKNOWN) {
219       return "";
220     } else if (!canBeGeocoded(numberType)) {
221       return getCountryNameForNumber(number, languageCode);
222     }
223     return getDescriptionForValidNumber(number, languageCode);
224   }
225
226   /**
227    * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale, String)} but
228    * explicitly checks the validity of the number passed in.
229    *
230    * @param number  the phone number for which we want to get a text description
231    * @param languageCode  the language code for which the description should be written
232    * @param userRegion  the region code for a given user. This region will be omitted from the
233    *     description if the phone number comes from this region. It is a two-letter uppercase ISO
234    *     country code as defined by ISO 3166-1.
235    * @return  a text description for the given language code for the given phone number, or empty
236    *     string if the number passed in is invalid
237    */
238   public String getDescriptionForNumber(PhoneNumber number, Locale languageCode,
239                                         String userRegion) {
240     PhoneNumberType numberType = phoneUtil.getNumberType(number);
241     if (numberType == PhoneNumberType.UNKNOWN) {
242       return "";
243     } else if (!canBeGeocoded(numberType)) {
244       return getCountryNameForNumber(number, languageCode);
245     }
246     return getDescriptionForValidNumber(number, languageCode, userRegion);
247   }
248
249   /**
250    * A similar method is implemented as PhoneNumberUtil.isNumberGeographical, which performs a
251    * stricter check, as it determines if a number has a geographical association. Also, if new
252    * phone number types were added, we should check if this other method should be updated too.
253    */
254   private boolean canBeGeocoded(PhoneNumberType numberType) {
255     return (numberType == PhoneNumberType.FIXED_LINE ||
256             numberType == PhoneNumberType.MOBILE ||
257             numberType == PhoneNumberType.FIXED_LINE_OR_MOBILE);
258   }
259
260   /**
261    * Returns an area-level text description in the given language for the given phone number.
262    *
263    * @param number  the phone number for which we want to get a text description
264    * @param lang  two-letter lowercase ISO language codes as defined by ISO 639-1
265    * @param script  four-letter titlecase (the first letter is uppercase and the rest of the letters
266    *     are lowercase) ISO script codes as defined in ISO 15924
267    * @param region  two-letter uppercase ISO country codes as defined by ISO 3166-1
268    * @return  an area-level text description in the given language for the given phone number, or an
269    *     empty string if such a description is not available
270    */
271   private String getAreaDescriptionForNumber(
272       PhoneNumber number, String lang, String script, String region) {
273     int countryCallingCode = number.getCountryCode();
274     // As the NANPA data is split into multiple files covering 3-digit areas, use a phone number
275     // prefix of 4 digits for NANPA instead, e.g. 1650.
276     int phonePrefix = (countryCallingCode != 1) ?
277         countryCallingCode : (1000 + (int) (number.getNationalNumber() / 10000000));
278     AreaCodeMap phonePrefixDescriptions =
279         getPhonePrefixDescriptions(phonePrefix, lang, script, region);
280     String description = (phonePrefixDescriptions != null)
281         ? phonePrefixDescriptions.lookup(number)
282         : null;
283     // When a location is not available in the requested language, fall back to English.
284     if ((description == null || description.length() == 0) && mayFallBackToEnglish(lang)) {
285       AreaCodeMap defaultMap = getPhonePrefixDescriptions(phonePrefix, "en", "", "");
286       if (defaultMap == null) {
287         return "";
288       }
289       description = defaultMap.lookup(number);
290     }
291     return description != null ? description : "";
292   }
293
294   private boolean mayFallBackToEnglish(String lang) {
295     // Don't fall back to English if the requested language is among the following:
296     // - Chinese
297     // - Japanese
298     // - Korean
299     return !lang.equals("zh") && !lang.equals("ja") && !lang.equals("ko");
300   }
301 }