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