• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.Phonemetadata.PhoneMetadata;
20 import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadataCollection;
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.io.ObjectInputStream;
24 import java.util.Collections;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Set;
29 import java.util.concurrent.ConcurrentHashMap;
30 import java.util.concurrent.atomic.AtomicReference;
31 import java.util.logging.Level;
32 import java.util.logging.Logger;
33 
34 /**
35  * Manager for loading metadata for alternate formats and short numbers. We also declare some
36  * constants for phone number metadata loading, to more easily maintain all three types of metadata
37  * together.
38  * TODO: Consider managing phone number metadata loading here too.
39  */
40 final class MetadataManager {
41   static final String MULTI_FILE_PHONE_NUMBER_METADATA_FILE_PREFIX =
42       "/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto";
43   static final String SINGLE_FILE_PHONE_NUMBER_METADATA_FILE_NAME =
44       "/com/google/i18n/phonenumbers/data/SingleFilePhoneNumberMetadataProto";
45   private static final String ALTERNATE_FORMATS_FILE_PREFIX =
46       "/com/google/i18n/phonenumbers/data/PhoneNumberAlternateFormatsProto";
47   private static final String SHORT_NUMBER_METADATA_FILE_PREFIX =
48       "/com/google/i18n/phonenumbers/data/ShortNumberMetadataProto";
49 
50   static final MetadataLoader DEFAULT_METADATA_LOADER = new MetadataLoader() {
51     @Override
52     public InputStream loadMetadata(String metadataFileName) {
53       return MetadataManager.class.getResourceAsStream(metadataFileName);
54     }
55   };
56 
57   private static final Logger logger = Logger.getLogger(MetadataManager.class.getName());
58 
59   // A mapping from a country calling code to the alternate formats for that country calling code.
60   private static final ConcurrentHashMap<Integer, PhoneMetadata> alternateFormatsMap =
61       new ConcurrentHashMap<Integer, PhoneMetadata>();
62 
63   // A mapping from a region code to the short number metadata for that region code.
64   private static final ConcurrentHashMap<String, PhoneMetadata> shortNumberMetadataMap =
65       new ConcurrentHashMap<String, PhoneMetadata>();
66 
67   // The set of country calling codes for which there are alternate formats. For every country
68   // calling code in this set there should be metadata linked into the resources.
69   private static final Set<Integer> alternateFormatsCountryCodes =
70       AlternateFormatsCountryCodeSet.getCountryCodeSet();
71 
72   // The set of region codes for which there are short number metadata. For every region code in
73   // this set there should be metadata linked into the resources.
74   private static final Set<String> shortNumberMetadataRegionCodes =
75       ShortNumbersRegionCodeSet.getRegionCodeSet();
76 
MetadataManager()77   private MetadataManager() {}
78 
getAlternateFormatsForCountry(int countryCallingCode)79   static PhoneMetadata getAlternateFormatsForCountry(int countryCallingCode) {
80     if (!alternateFormatsCountryCodes.contains(countryCallingCode)) {
81       return null;
82     }
83     return getMetadataFromMultiFilePrefix(countryCallingCode, alternateFormatsMap,
84         ALTERNATE_FORMATS_FILE_PREFIX, DEFAULT_METADATA_LOADER);
85   }
86 
getShortNumberMetadataForRegion(String regionCode)87   static PhoneMetadata getShortNumberMetadataForRegion(String regionCode) {
88     if (!shortNumberMetadataRegionCodes.contains(regionCode)) {
89       return null;
90     }
91     return getMetadataFromMultiFilePrefix(regionCode, shortNumberMetadataMap,
92         SHORT_NUMBER_METADATA_FILE_PREFIX, DEFAULT_METADATA_LOADER);
93   }
94 
getSupportedShortNumberRegions()95   static Set<String> getSupportedShortNumberRegions() {
96     return Collections.unmodifiableSet(shortNumberMetadataRegionCodes);
97   }
98 
99   /**
100    * @param key  the lookup key for the provided map, typically a region code or a country calling
101    *     code
102    * @param map  the map containing mappings of already loaded metadata from their {@code key}. If
103    *     this {@code key}'s metadata isn't already loaded, it will be added to this map after
104    *     loading
105    * @param filePrefix  the prefix of the file to load metadata from
106    * @param metadataLoader  the metadata loader used to inject alternative metadata sources
107    */
getMetadataFromMultiFilePrefix(T key, ConcurrentHashMap<T, PhoneMetadata> map, String filePrefix, MetadataLoader metadataLoader)108   static <T> PhoneMetadata getMetadataFromMultiFilePrefix(T key,
109       ConcurrentHashMap<T, PhoneMetadata> map, String filePrefix, MetadataLoader metadataLoader) {
110     PhoneMetadata metadata = map.get(key);
111     if (metadata != null) {
112       return metadata;
113     }
114     // We assume key.toString() is well-defined.
115     String fileName = filePrefix + "_" + key;
116     List<PhoneMetadata> metadataList = getMetadataFromSingleFileName(fileName, metadataLoader);
117     if (metadataList.size() > 1) {
118       logger.log(Level.WARNING, "more than one metadata in file " + fileName);
119     }
120     metadata = metadataList.get(0);
121     PhoneMetadata oldValue = map.putIfAbsent(key, metadata);
122     return (oldValue != null) ? oldValue : metadata;
123   }
124 
125   // Loader and holder for the metadata maps loaded from a single file.
126   static class SingleFileMetadataMaps {
load(String fileName, MetadataLoader metadataLoader)127     static SingleFileMetadataMaps load(String fileName, MetadataLoader metadataLoader) {
128       List<PhoneMetadata> metadataList = getMetadataFromSingleFileName(fileName, metadataLoader);
129       Map<String, PhoneMetadata> regionCodeToMetadata = new HashMap<String, PhoneMetadata>();
130       Map<Integer, PhoneMetadata> countryCallingCodeToMetadata =
131           new HashMap<Integer, PhoneMetadata>();
132       for (PhoneMetadata metadata : metadataList) {
133         String regionCode = metadata.getId();
134         if (PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY.equals(regionCode)) {
135           // regionCode belongs to a non-geographical entity.
136           countryCallingCodeToMetadata.put(metadata.getCountryCode(), metadata);
137         } else {
138           regionCodeToMetadata.put(regionCode, metadata);
139         }
140       }
141       return new SingleFileMetadataMaps(regionCodeToMetadata, countryCallingCodeToMetadata);
142     }
143 
144     // A map from a region code to the PhoneMetadata for that region.
145     // For phone number metadata, the region code "001" is excluded, since that is used for the
146     // non-geographical phone number entities.
147     private final Map<String, PhoneMetadata> regionCodeToMetadata;
148 
149     // A map from a country calling code to the PhoneMetadata for that country calling code.
150     // Examples of the country calling codes include 800 (International Toll Free Service) and 808
151     // (International Shared Cost Service).
152     // For phone number metadata, only the non-geographical phone number entities' country calling
153     // codes are present.
154     private final Map<Integer, PhoneMetadata> countryCallingCodeToMetadata;
155 
SingleFileMetadataMaps(Map<String, PhoneMetadata> regionCodeToMetadata, Map<Integer, PhoneMetadata> countryCallingCodeToMetadata)156     private SingleFileMetadataMaps(Map<String, PhoneMetadata> regionCodeToMetadata,
157         Map<Integer, PhoneMetadata> countryCallingCodeToMetadata) {
158       this.regionCodeToMetadata = Collections.unmodifiableMap(regionCodeToMetadata);
159       this.countryCallingCodeToMetadata = Collections.unmodifiableMap(countryCallingCodeToMetadata);
160     }
161 
get(String regionCode)162     PhoneMetadata get(String regionCode) {
163       return regionCodeToMetadata.get(regionCode);
164     }
165 
get(int countryCallingCode)166     PhoneMetadata get(int countryCallingCode) {
167       return countryCallingCodeToMetadata.get(countryCallingCode);
168     }
169   }
170 
171   // Manages the atomic reference lifecycle of a SingleFileMetadataMaps encapsulation.
getSingleFileMetadataMaps( AtomicReference<SingleFileMetadataMaps> ref, String fileName, MetadataLoader metadataLoader)172   static SingleFileMetadataMaps getSingleFileMetadataMaps(
173       AtomicReference<SingleFileMetadataMaps> ref, String fileName, MetadataLoader metadataLoader) {
174     SingleFileMetadataMaps maps = ref.get();
175     if (maps != null) {
176       return maps;
177     }
178     maps = SingleFileMetadataMaps.load(fileName, metadataLoader);
179     ref.compareAndSet(null, maps);
180     return ref.get();
181   }
182 
getMetadataFromSingleFileName(String fileName, MetadataLoader metadataLoader)183   private static List<PhoneMetadata> getMetadataFromSingleFileName(String fileName,
184       MetadataLoader metadataLoader) {
185     InputStream source = metadataLoader.loadMetadata(fileName);
186     if (source == null) {
187       // Sanity check; this would only happen if we packaged jars incorrectly.
188       throw new IllegalStateException("missing metadata: " + fileName);
189     }
190     PhoneMetadataCollection metadataCollection = loadMetadataAndCloseInput(source);
191     List<PhoneMetadata> metadataList = metadataCollection.getMetadataList();
192     if (metadataList.size() == 0) {
193       // Sanity check; this should not happen since we build with non-empty metadata.
194       throw new IllegalStateException("empty metadata: " + fileName);
195     }
196     return metadataList;
197   }
198 
199   /**
200    * Loads and returns the metadata from the given stream and closes the stream.
201    *
202    * @param source  the non-null stream from which metadata is to be read
203    * @return  the loaded metadata
204    */
loadMetadataAndCloseInput(InputStream source)205   private static PhoneMetadataCollection loadMetadataAndCloseInput(InputStream source) {
206     ObjectInputStream ois = null;
207     try {
208       try {
209         ois = new ObjectInputStream(source);
210       } catch (IOException e) {
211         throw new RuntimeException("cannot load/parse metadata", e);
212       }
213       PhoneMetadataCollection metadataCollection = new PhoneMetadataCollection();
214       try {
215         metadataCollection.readExternal(ois);
216       } catch (IOException e) {
217         throw new RuntimeException("cannot load/parse metadata", e);
218       }
219       return metadataCollection;
220     } finally {
221       try {
222         if (ois != null) {
223           // This will close all underlying streams as well, including source.
224           ois.close();
225         } else {
226           source.close();
227         }
228       } catch (IOException e) {
229         logger.log(Level.WARNING, "error closing input stream (ignored)", e);
230       }
231     }
232   }
233 }
234