1 /* 2 * Copyright (C) 2015 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 22 import java.io.IOException; 23 import java.io.InputStream; 24 import java.io.ObjectInputStream; 25 import java.util.Collections; 26 import java.util.HashMap; 27 import java.util.List; 28 import java.util.Map; 29 import java.util.logging.Level; 30 import java.util.logging.Logger; 31 32 /** 33 * Implementation of {@link MetadataSource} that reads from multiple resource files. 34 */ 35 final class MultiFileMetadataSourceImpl implements MetadataSource { 36 37 private static final Logger logger = 38 Logger.getLogger(MultiFileMetadataSourceImpl.class.getName()); 39 40 private static final String META_DATA_FILE_PREFIX = 41 "/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto"; 42 43 // A mapping from a region code to the PhoneMetadata for that region. 44 // Note: Synchronization, though only needed for the Android version of the library, is used in 45 // all versions for consistency. 46 private final Map<String, PhoneMetadata> regionToMetadataMap = 47 Collections.synchronizedMap(new HashMap<String, PhoneMetadata>()); 48 49 // A mapping from a country calling code for a non-geographical entity to the PhoneMetadata for 50 // that country calling code. Examples of the country calling codes include 800 (International 51 // Toll Free Service) and 808 (International Shared Cost Service). 52 // Note: Synchronization, though only needed for the Android version of the library, is used in 53 // all versions for consistency. 54 private final Map<Integer, PhoneMetadata> countryCodeToNonGeographicalMetadataMap = 55 Collections.synchronizedMap(new HashMap<Integer, PhoneMetadata>()); 56 57 // The prefix of the metadata files from which region data is loaded. 58 private final String currentFilePrefix; 59 60 // The metadata loader used to inject alternative metadata sources. 61 private final MetadataLoader metadataLoader; 62 63 // It is assumed that metadataLoader is not null. 64 public MultiFileMetadataSourceImpl(String currentFilePrefix, MetadataLoader metadataLoader) { 65 this.currentFilePrefix = currentFilePrefix; 66 this.metadataLoader = metadataLoader; 67 } 68 69 // It is assumed that metadataLoader is not null. 70 public MultiFileMetadataSourceImpl(MetadataLoader metadataLoader) { 71 this(META_DATA_FILE_PREFIX, metadataLoader); 72 } 73 74 @Override 75 public PhoneMetadata getMetadataForRegion(String regionCode) { 76 synchronized (regionToMetadataMap) { 77 if (!regionToMetadataMap.containsKey(regionCode)) { 78 // The regionCode here will be valid and won't be '001', so we don't need to worry about 79 // what to pass in for the country calling code. 80 loadMetadataFromFile(currentFilePrefix, regionCode, 0, metadataLoader); 81 } 82 } 83 return regionToMetadataMap.get(regionCode); 84 } 85 86 @Override 87 public PhoneMetadata getMetadataForNonGeographicalRegion(int countryCallingCode) { 88 synchronized (countryCodeToNonGeographicalMetadataMap) { 89 if (!countryCodeToNonGeographicalMetadataMap.containsKey(countryCallingCode)) { 90 loadMetadataFromFile(currentFilePrefix, PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY, 91 countryCallingCode, metadataLoader); 92 } 93 } 94 return countryCodeToNonGeographicalMetadataMap.get(countryCallingCode); 95 } 96 97 // @VisibleForTesting 98 void loadMetadataFromFile(String filePrefix, String regionCode, int countryCallingCode, 99 MetadataLoader metadataLoader) { 100 boolean isNonGeoRegion = PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY.equals(regionCode); 101 String fileName = filePrefix + "_" + 102 (isNonGeoRegion ? String.valueOf(countryCallingCode) : regionCode); 103 InputStream source = metadataLoader.loadMetadata(fileName); 104 if (source == null) { 105 logger.log(Level.SEVERE, "missing metadata: " + fileName); 106 throw new IllegalStateException("missing metadata: " + fileName); 107 } 108 ObjectInputStream in = null; 109 try { 110 in = new ObjectInputStream(source); 111 PhoneMetadataCollection metadataCollection = loadMetadataAndCloseInput(in); 112 List<PhoneMetadata> metadataList = metadataCollection.getMetadataList(); 113 if (metadataList.isEmpty()) { 114 logger.log(Level.SEVERE, "empty metadata: " + fileName); 115 throw new IllegalStateException("empty metadata: " + fileName); 116 } 117 if (metadataList.size() > 1) { 118 logger.log(Level.WARNING, "invalid metadata (too many entries): " + fileName); 119 } 120 PhoneMetadata metadata = metadataList.get(0); 121 if (isNonGeoRegion) { 122 countryCodeToNonGeographicalMetadataMap.put(countryCallingCode, metadata); 123 } else { 124 regionToMetadataMap.put(regionCode, metadata); 125 } 126 } catch (IOException e) { 127 logger.log(Level.SEVERE, "cannot load/parse metadata: " + fileName, e); 128 throw new RuntimeException("cannot load/parse metadata: " + fileName, e); 129 } 130 } 131 132 /** 133 * Loads the metadata protocol buffer from the given stream and closes the stream afterwards. Any 134 * exceptions that occur while reading the stream are propagated (though exceptions that occur 135 * when the stream is closed will be ignored). 136 * 137 * @param source the non-null stream from which metadata is to be read. 138 * @return the loaded metadata protocol buffer. 139 */ 140 private static PhoneMetadataCollection loadMetadataAndCloseInput(ObjectInputStream source) { 141 PhoneMetadataCollection metadataCollection = new PhoneMetadataCollection(); 142 try { 143 metadataCollection.readExternal(source); 144 } catch (IOException e) { 145 logger.log(Level.WARNING, "error reading input (ignored)", e); 146 } finally { 147 try { 148 source.close(); 149 } catch (IOException e) { 150 logger.log(Level.WARNING, "error closing input stream (ignored)", e); 151 } 152 } 153 return metadataCollection; 154 } 155 } 156