|
|
|
@ -24,48 +24,187 @@ import java.io.InputStream; |
|
|
|
import java.io.ObjectInputStream; |
|
|
|
import java.util.Collections; |
|
|
|
import java.util.HashMap; |
|
|
|
import java.util.List; |
|
|
|
import java.util.Map; |
|
|
|
import java.util.Set; |
|
|
|
import java.util.concurrent.ConcurrentHashMap; |
|
|
|
import java.util.concurrent.atomic.AtomicReference; |
|
|
|
import java.util.logging.Level; |
|
|
|
import java.util.logging.Logger; |
|
|
|
|
|
|
|
/** |
|
|
|
* Class encapsulating loading of PhoneNumber Metadata information. Currently this is used only for |
|
|
|
* additional data files such as PhoneNumberAlternateFormats, but in the future it is envisaged it |
|
|
|
* would handle the main metadata file (PhoneNumberMetadata.xml) as well. |
|
|
|
* Manager for loading metadata for alternate formats and short numbers. We also declare some |
|
|
|
* constants for phone number metadata loading, to more easily maintain all three types of metadata |
|
|
|
* together. |
|
|
|
* TODO: Consider managing phone number metadata loading here too. |
|
|
|
*/ |
|
|
|
final class MetadataManager { |
|
|
|
static final String MULTI_FILE_PHONE_NUMBER_METADATA_FILE_PREFIX = |
|
|
|
"/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto"; |
|
|
|
static final String SINGLE_FILE_PHONE_NUMBER_METADATA_FILE_NAME = |
|
|
|
"/com/google/i18n/phonenumbers/data/SingleFilePhoneNumberMetadataProto"; |
|
|
|
private static final String ALTERNATE_FORMATS_FILE_PREFIX = |
|
|
|
"/com/google/i18n/phonenumbers/data/PhoneNumberAlternateFormatsProto"; |
|
|
|
private static final String SHORT_NUMBER_METADATA_FILE_PREFIX = |
|
|
|
"/com/google/i18n/phonenumbers/data/ShortNumberMetadataProto"; |
|
|
|
|
|
|
|
private static final MetadataLoader METADATA_LOADER = new MetadataLoader() { |
|
|
|
@Override |
|
|
|
public InputStream loadMetadata(String metadataFileName) { |
|
|
|
return MetadataManager.class.getResourceAsStream(metadataFileName); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
private static final Logger logger = Logger.getLogger(MetadataManager.class.getName()); |
|
|
|
|
|
|
|
private static final Map<Integer, PhoneMetadata> callingCodeToAlternateFormatsMap = |
|
|
|
Collections.synchronizedMap(new HashMap<Integer, PhoneMetadata>()); |
|
|
|
private static final Map<String, PhoneMetadata> regionCodeToShortNumberMetadataMap = |
|
|
|
Collections.synchronizedMap(new HashMap<String, PhoneMetadata>()); |
|
|
|
// A mapping from a country calling code to the alternate formats for that country calling code. |
|
|
|
private static final ConcurrentHashMap<Integer, PhoneMetadata> alternateFormatsMap = |
|
|
|
new ConcurrentHashMap<Integer, PhoneMetadata>(); |
|
|
|
|
|
|
|
// A mapping from a region code to the short number metadata for that region code. |
|
|
|
private static final ConcurrentHashMap<String, PhoneMetadata> shortNumberMetadataMap = |
|
|
|
new ConcurrentHashMap<String, PhoneMetadata>(); |
|
|
|
|
|
|
|
// A set of which country calling codes there are alternate format data for. If the set has an |
|
|
|
// entry for a code, then there should be data for that code linked into the resources. |
|
|
|
private static final Set<Integer> countryCodeSet = |
|
|
|
// The set of country calling codes for which there are alternate formats. For every country |
|
|
|
// calling code in this set there should be metadata linked into the resources. |
|
|
|
private static final Set<Integer> alternateFormatsCountryCodes = |
|
|
|
AlternateFormatsCountryCodeSet.getCountryCodeSet(); |
|
|
|
|
|
|
|
// A set of which region codes there are short number data for. If the set has an entry for a |
|
|
|
// code, then there should be data for that code linked into the resources. |
|
|
|
private static final Set<String> regionCodeSet = ShortNumbersRegionCodeSet.getRegionCodeSet(); |
|
|
|
// The set of region codes for which there are short number metadata. For every region code in |
|
|
|
// this set there should be metadata linked into the resources. |
|
|
|
private static final Set<String> shortNumberMetadataRegionCodes = |
|
|
|
ShortNumbersRegionCodeSet.getRegionCodeSet(); |
|
|
|
|
|
|
|
private MetadataManager() {} |
|
|
|
|
|
|
|
static PhoneMetadata getAlternateFormatsForCountry(int countryCallingCode) { |
|
|
|
if (!alternateFormatsCountryCodes.contains(countryCallingCode)) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
return getMultiFileMetadata(countryCallingCode, alternateFormatsMap, |
|
|
|
ALTERNATE_FORMATS_FILE_PREFIX, METADATA_LOADER); |
|
|
|
} |
|
|
|
|
|
|
|
static PhoneMetadata getShortNumberMetadataForRegion(String regionCode) { |
|
|
|
if (!shortNumberMetadataRegionCodes.contains(regionCode)) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
return getMultiFileMetadata(regionCode, shortNumberMetadataMap, |
|
|
|
SHORT_NUMBER_METADATA_FILE_PREFIX, METADATA_LOADER); |
|
|
|
} |
|
|
|
|
|
|
|
static Set<String> getSupportedShortNumberRegions() { |
|
|
|
return Collections.unmodifiableSet(shortNumberMetadataRegionCodes); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* @param key the lookup key for the provided map, typically a region code or a country calling |
|
|
|
* code |
|
|
|
* @param map the map containing mappings of already loaded metadata from their {@code key}. If |
|
|
|
* this {@code key}'s metadata isn't already loaded, it will be added to this map after |
|
|
|
* loading |
|
|
|
* @param filePrefix the prefix of the file to load metadata from |
|
|
|
* @param metadataLoader the metadata loader used to inject alternative metadata sources |
|
|
|
*/ |
|
|
|
static <T> PhoneMetadata getMultiFileMetadata(T key, ConcurrentHashMap<T, PhoneMetadata> map, |
|
|
|
String filePrefix, MetadataLoader metadataLoader) { |
|
|
|
PhoneMetadata metadata = map.get(key); |
|
|
|
if (metadata != null) { |
|
|
|
return metadata; |
|
|
|
} |
|
|
|
// We assume key.toString() is well-defined. |
|
|
|
String fileName = filePrefix + "_" + key; |
|
|
|
List<PhoneMetadata> metadataList = getMetadataFromFileName(fileName, metadataLoader); |
|
|
|
if (metadataList.size() > 1) { |
|
|
|
logger.log(Level.WARNING, "more than one metadata in file " + fileName); |
|
|
|
} |
|
|
|
metadata = metadataList.get(0); |
|
|
|
PhoneMetadata oldValue = map.putIfAbsent(key, metadata); |
|
|
|
return (oldValue != null) ? oldValue : metadata; |
|
|
|
} |
|
|
|
|
|
|
|
// Loader and holder for the metadata maps loaded from a single file. |
|
|
|
static class SingleFileMetadataMaps { |
|
|
|
static SingleFileMetadataMaps load(String fileName, MetadataLoader metadataLoader) { |
|
|
|
List<PhoneMetadata> metadataList = getMetadataFromFileName(fileName, metadataLoader); |
|
|
|
Map<String, PhoneMetadata> regionCodeToMetadata = new HashMap<String, PhoneMetadata>(); |
|
|
|
Map<Integer, PhoneMetadata> countryCallingCodeToMetadata = new HashMap<Integer, PhoneMetadata>(); |
|
|
|
for (PhoneMetadata metadata : metadataList) { |
|
|
|
String regionCode = metadata.getId(); |
|
|
|
if (PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY.equals(regionCode)) { |
|
|
|
// regionCode belongs to a non-geographical entity. |
|
|
|
countryCallingCodeToMetadata.put(metadata.getCountryCode(), metadata); |
|
|
|
} else { |
|
|
|
regionCodeToMetadata.put(regionCode, metadata); |
|
|
|
} |
|
|
|
} |
|
|
|
return new SingleFileMetadataMaps(regionCodeToMetadata, countryCallingCodeToMetadata); |
|
|
|
} |
|
|
|
|
|
|
|
// A map from a region code to the PhoneMetadata for that region. |
|
|
|
// For phone number metadata, the region code "001" is excluded, since that is used for the |
|
|
|
// non-geographical phone number entities. |
|
|
|
private final Map<String, PhoneMetadata> regionCodeToMetadata; |
|
|
|
|
|
|
|
// A map from a country calling code to the PhoneMetadata for that country calling code. |
|
|
|
// Examples of the country calling codes include 800 (International Toll Free Service) and 808 |
|
|
|
// (International Shared Cost Service). |
|
|
|
// For phone number metadata, only the non-geographical phone number entities' country calling |
|
|
|
// codes are present. |
|
|
|
private final Map<Integer, PhoneMetadata> countryCallingCodeToMetadata; |
|
|
|
|
|
|
|
private SingleFileMetadataMaps(Map<String, PhoneMetadata> regionCodeToMetadata, |
|
|
|
Map<Integer, PhoneMetadata> countryCallingCodeToMetadata) { |
|
|
|
this.regionCodeToMetadata = Collections.unmodifiableMap(regionCodeToMetadata); |
|
|
|
this.countryCallingCodeToMetadata = Collections.unmodifiableMap(countryCallingCodeToMetadata); |
|
|
|
} |
|
|
|
|
|
|
|
PhoneMetadata get(String regionCode) { |
|
|
|
return regionCodeToMetadata.get(regionCode); |
|
|
|
} |
|
|
|
|
|
|
|
PhoneMetadata get(int countryCallingCode) { |
|
|
|
return countryCallingCodeToMetadata.get(countryCallingCode); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Manages the atomic reference lifecycle of a SingleFileMetadataMaps encapsulation. |
|
|
|
static SingleFileMetadataMaps getSingleFileMetadataMaps( |
|
|
|
AtomicReference<SingleFileMetadataMaps> ref, String fileName, MetadataLoader metadataLoader) { |
|
|
|
SingleFileMetadataMaps maps = ref.get(); |
|
|
|
if (maps == null) { |
|
|
|
maps = SingleFileMetadataMaps.load(fileName, metadataLoader); |
|
|
|
SingleFileMetadataMaps existingValue = ref.getAndSet(maps); |
|
|
|
if (existingValue != null) { |
|
|
|
maps = existingValue; |
|
|
|
} |
|
|
|
} |
|
|
|
return maps; |
|
|
|
} |
|
|
|
|
|
|
|
private MetadataManager() { |
|
|
|
private static List<PhoneMetadata> getMetadataFromFileName(String fileName, |
|
|
|
MetadataLoader metadataLoader) { |
|
|
|
InputStream source = metadataLoader.loadMetadata(fileName); |
|
|
|
if (source == null) { |
|
|
|
// Sanity check; this would only happen if we packaged jars incorrectly. |
|
|
|
throw new IllegalStateException("missing metadata: " + fileName); |
|
|
|
} |
|
|
|
PhoneMetadataCollection metadataCollection = loadMetadataAndCloseInput(source); |
|
|
|
List<PhoneMetadata> metadataList = metadataCollection.getMetadataList(); |
|
|
|
if (metadataList.size() == 0) { |
|
|
|
// Sanity check; this should not happen since we build with non-empty metadata. |
|
|
|
throw new IllegalStateException("empty metadata: " + fileName); |
|
|
|
} |
|
|
|
return metadataList; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Loads and returns the metadata object from the given stream and closes the stream. |
|
|
|
* Loads and returns the metadata from the given stream and closes the stream. |
|
|
|
* |
|
|
|
* @param source the non-null stream from which metadata is to be read |
|
|
|
* @return the loaded metadata object |
|
|
|
* @return the loaded metadata |
|
|
|
*/ |
|
|
|
static PhoneMetadataCollection loadMetadataAndCloseInput(InputStream source) { |
|
|
|
private static PhoneMetadataCollection loadMetadataAndCloseInput(InputStream source) { |
|
|
|
ObjectInputStream ois = null; |
|
|
|
try { |
|
|
|
try { |
|
|
|
@ -93,61 +232,4 @@ final class MetadataManager { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private static void loadAlternateFormatsMetadataFromFile(int countryCallingCode) { |
|
|
|
String fileName = ALTERNATE_FORMATS_FILE_PREFIX + "_" + countryCallingCode; |
|
|
|
InputStream source = MetadataManager.class.getResourceAsStream(fileName); |
|
|
|
if (source == null) { |
|
|
|
// Sanity check; this should not happen since we only load things based on the expectation |
|
|
|
// that they are present, by checking the map of available data first. |
|
|
|
throw new IllegalStateException("missing metadata: " + fileName); |
|
|
|
} |
|
|
|
PhoneMetadataCollection alternateFormats = loadMetadataAndCloseInput(source); |
|
|
|
for (PhoneMetadata metadata : alternateFormats.getMetadataList()) { |
|
|
|
callingCodeToAlternateFormatsMap.put(metadata.getCountryCode(), metadata); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
static PhoneMetadata getAlternateFormatsForCountry(int countryCallingCode) { |
|
|
|
if (!countryCodeSet.contains(countryCallingCode)) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
synchronized (callingCodeToAlternateFormatsMap) { |
|
|
|
if (!callingCodeToAlternateFormatsMap.containsKey(countryCallingCode)) { |
|
|
|
loadAlternateFormatsMetadataFromFile(countryCallingCode); |
|
|
|
} |
|
|
|
} |
|
|
|
return callingCodeToAlternateFormatsMap.get(countryCallingCode); |
|
|
|
} |
|
|
|
|
|
|
|
private static void loadShortNumberMetadataFromFile(String regionCode) { |
|
|
|
String fileName = SHORT_NUMBER_METADATA_FILE_PREFIX + "_" + regionCode; |
|
|
|
InputStream source = MetadataManager.class.getResourceAsStream(fileName); |
|
|
|
if (source == null) { |
|
|
|
// Sanity check; this should not happen since we only load things based on the expectation |
|
|
|
// that they are present, by checking the map of available data first. |
|
|
|
throw new IllegalStateException("missing metadata: " + fileName); |
|
|
|
} |
|
|
|
PhoneMetadataCollection shortNumberMetadata = loadMetadataAndCloseInput(source); |
|
|
|
for (PhoneMetadata metadata : shortNumberMetadata.getMetadataList()) { |
|
|
|
regionCodeToShortNumberMetadataMap.put(regionCode, metadata); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// @VisibleForTesting |
|
|
|
static Set<String> getShortNumberMetadataSupportedRegions() { |
|
|
|
return regionCodeSet; |
|
|
|
} |
|
|
|
|
|
|
|
static PhoneMetadata getShortNumberMetadataForRegion(String regionCode) { |
|
|
|
if (!regionCodeSet.contains(regionCode)) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
synchronized (regionCodeToShortNumberMetadataMap) { |
|
|
|
if (!regionCodeToShortNumberMetadataMap.containsKey(regionCode)) { |
|
|
|
loadShortNumberMetadataFromFile(regionCode); |
|
|
|
} |
|
|
|
} |
|
|
|
return regionCodeToShortNumberMetadataMap.get(regionCode); |
|
|
|
} |
|
|
|
} |