diff --git a/java/carrier/src/com/google/i18n/phonenumbers/PhoneNumberToCarrierMapper.java b/java/carrier/src/com/google/i18n/phonenumbers/PhoneNumberToCarrierMapper.java index b99abe6e3..ed3d6d88e 100644 --- a/java/carrier/src/com/google/i18n/phonenumbers/PhoneNumberToCarrierMapper.java +++ b/java/carrier/src/com/google/i18n/phonenumbers/PhoneNumberToCarrierMapper.java @@ -16,11 +16,10 @@ package com.google.i18n.phonenumbers; -import com.google.i18n.phonenumbers.PhoneNumberUtil; import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberType; import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; +import com.google.i18n.phonenumbers.metadata.DefaultMetadataDependenciesProvider; import com.google.i18n.phonenumbers.prefixmapper.PrefixFileReader; - import java.util.Locale; /** @@ -30,9 +29,7 @@ import java.util.Locale; */ public class PhoneNumberToCarrierMapper { private static PhoneNumberToCarrierMapper instance = null; - private static final String MAPPING_DATA_DIRECTORY = - "/com/google/i18n/phonenumbers/carrier/data/"; - private PrefixFileReader prefixFileReader = null; + private final PrefixFileReader prefixFileReader; private final PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance(); @@ -51,7 +48,8 @@ public class PhoneNumberToCarrierMapper { */ public static synchronized PhoneNumberToCarrierMapper getInstance() { if (instance == null) { - instance = new PhoneNumberToCarrierMapper(MAPPING_DATA_DIRECTORY); + instance = new PhoneNumberToCarrierMapper(DefaultMetadataDependenciesProvider.getInstance() + .getCarrierDataDirectory()); } return instance; } diff --git a/java/geocoder/src/com/google/i18n/phonenumbers/geocoding/PhoneNumberOfflineGeocoder.java b/java/geocoder/src/com/google/i18n/phonenumbers/geocoding/PhoneNumberOfflineGeocoder.java index a48ed903d..f26cc1038 100644 --- a/java/geocoder/src/com/google/i18n/phonenumbers/geocoding/PhoneNumberOfflineGeocoder.java +++ b/java/geocoder/src/com/google/i18n/phonenumbers/geocoding/PhoneNumberOfflineGeocoder.java @@ -20,6 +20,7 @@ import com.google.i18n.phonenumbers.NumberParseException; import com.google.i18n.phonenumbers.PhoneNumberUtil; import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberType; import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; +import com.google.i18n.phonenumbers.metadata.DefaultMetadataDependenciesProvider; import com.google.i18n.phonenumbers.prefixmapper.PrefixFileReader; import java.util.List; @@ -32,9 +33,7 @@ import java.util.Locale; */ public class PhoneNumberOfflineGeocoder { private static PhoneNumberOfflineGeocoder instance = null; - private static final String MAPPING_DATA_DIRECTORY = - "/com/google/i18n/phonenumbers/geocoding/data/"; - private PrefixFileReader prefixFileReader = null; + private final PrefixFileReader prefixFileReader; private final PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance(); @@ -54,7 +53,8 @@ public class PhoneNumberOfflineGeocoder { */ public static synchronized PhoneNumberOfflineGeocoder getInstance() { if (instance == null) { - instance = new PhoneNumberOfflineGeocoder(MAPPING_DATA_DIRECTORY); + instance = new PhoneNumberOfflineGeocoder(DefaultMetadataDependenciesProvider.getInstance() + .getGeocodingDataDirectory()); } return instance; } diff --git a/java/internal/prefixmapper/src/com/google/i18n/phonenumbers/prefixmapper/PrefixFileReader.java b/java/internal/prefixmapper/src/com/google/i18n/phonenumbers/prefixmapper/PrefixFileReader.java index 8a4fde70f..6033b6785 100644 --- a/java/internal/prefixmapper/src/com/google/i18n/phonenumbers/prefixmapper/PrefixFileReader.java +++ b/java/internal/prefixmapper/src/com/google/i18n/phonenumbers/prefixmapper/PrefixFileReader.java @@ -16,8 +16,10 @@ package com.google.i18n.phonenumbers.prefixmapper; +import com.google.i18n.phonenumbers.MetadataLoader; import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; +import com.google.i18n.phonenumbers.metadata.DefaultMetadataDependenciesProvider; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; @@ -40,17 +42,17 @@ public class PrefixFileReader { private MappingFileProvider mappingFileProvider = new MappingFileProvider(); // A mapping from countryCallingCode_lang to the corresponding phone prefix map that has been // loaded. - private Map availablePhonePrefixMaps = - new HashMap(); + private Map availablePhonePrefixMaps = new HashMap<>(); + private final MetadataLoader metadataLoader; public PrefixFileReader(String phonePrefixDataDirectory) { this.phonePrefixDataDirectory = phonePrefixDataDirectory; + this.metadataLoader = DefaultMetadataDependenciesProvider.getInstance().getMetadataLoader(); loadMappingFileProvider(); } private void loadMappingFileProvider() { - InputStream source = - PrefixFileReader.class.getResourceAsStream(phonePrefixDataDirectory + "config"); + InputStream source = metadataLoader.loadMetadata(phonePrefixDataDirectory + "config"); ObjectInputStream in = null; try { in = new ObjectInputStream(source); @@ -75,8 +77,7 @@ public class PrefixFileReader { } private void loadPhonePrefixMapFromFile(String fileName) { - InputStream source = - PrefixFileReader.class.getResourceAsStream(phonePrefixDataDirectory + fileName); + InputStream source = metadataLoader.loadMetadata(phonePrefixDataDirectory + fileName); ObjectInputStream in = null; try { in = new ObjectInputStream(source); diff --git a/java/lib/mockito-all-1.10.19.jar b/java/lib/mockito-all-1.10.19.jar new file mode 100644 index 000000000..c831489cd Binary files /dev/null and b/java/lib/mockito-all-1.10.19.jar differ diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/CountryCodeToRegionCodeMap.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/CountryCodeToRegionCodeMap.java index 159f940db..06571d15f 100644 --- a/java/libphonenumber/src/com/google/i18n/phonenumbers/CountryCodeToRegionCodeMap.java +++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/CountryCodeToRegionCodeMap.java @@ -30,7 +30,7 @@ public class CountryCodeToRegionCodeMap { // country/region represented by that country code. In the case of multiple // countries sharing a calling code, such as the NANPA countries, the one // indicated with "isMainCountryForCode" in the metadata should be first. - static Map> getCountryCodeToRegionCodeMap() { + public static Map> getCountryCodeToRegionCodeMap() { // The capacity is set to 286 as there are 215 different entries, // and this offers a load factor of roughly 0.75. Map> countryCodeToRegionCodeMap = diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/MetadataManager.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/MetadataManager.java deleted file mode 100644 index 5c072d6ab..000000000 --- a/java/libphonenumber/src/com/google/i18n/phonenumbers/MetadataManager.java +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Copyright (C) 2012 The Libphonenumber Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.i18n.phonenumbers; - -import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata; -import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadataCollection; -import java.io.IOException; -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; - -/** - * 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"; - - static final MetadataLoader DEFAULT_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()); - - // A mapping from a country calling code to the alternate formats for that country calling code. - private static final ConcurrentHashMap alternateFormatsMap = - new ConcurrentHashMap(); - - // A mapping from a region code to the short number metadata for that region code. - private static final ConcurrentHashMap shortNumberMetadataMap = - new ConcurrentHashMap(); - - // 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 alternateFormatsCountryCodes = - AlternateFormatsCountryCodeSet.getCountryCodeSet(); - - // 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 shortNumberMetadataRegionCodes = - ShortNumbersRegionCodeSet.getRegionCodeSet(); - - private MetadataManager() {} - - static PhoneMetadata getAlternateFormatsForCountry(int countryCallingCode) { - if (!alternateFormatsCountryCodes.contains(countryCallingCode)) { - return null; - } - return getMetadataFromMultiFilePrefix(countryCallingCode, alternateFormatsMap, - ALTERNATE_FORMATS_FILE_PREFIX, DEFAULT_METADATA_LOADER); - } - - static PhoneMetadata getShortNumberMetadataForRegion(String regionCode) { - if (!shortNumberMetadataRegionCodes.contains(regionCode)) { - return null; - } - return getMetadataFromMultiFilePrefix(regionCode, shortNumberMetadataMap, - SHORT_NUMBER_METADATA_FILE_PREFIX, DEFAULT_METADATA_LOADER); - } - - static Set 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 PhoneMetadata getMetadataFromMultiFilePrefix(T key, - ConcurrentHashMap 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 metadataList = getMetadataFromSingleFileName(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 metadataList = getMetadataFromSingleFileName(fileName, metadataLoader); - Map regionCodeToMetadata = new HashMap(); - Map countryCallingCodeToMetadata = - new HashMap(); - 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 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 countryCallingCodeToMetadata; - - private SingleFileMetadataMaps(Map regionCodeToMetadata, - Map 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 ref, String fileName, MetadataLoader metadataLoader) { - SingleFileMetadataMaps maps = ref.get(); - if (maps != null) { - return maps; - } - maps = SingleFileMetadataMaps.load(fileName, metadataLoader); - ref.compareAndSet(null, maps); - return ref.get(); - } - - private static List getMetadataFromSingleFileName(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 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 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 - */ - private static PhoneMetadataCollection loadMetadataAndCloseInput(InputStream source) { - ObjectInputStream ois = null; - try { - try { - ois = new ObjectInputStream(source); - } catch (IOException e) { - throw new RuntimeException("cannot load/parse metadata", e); - } - PhoneMetadataCollection metadataCollection = new PhoneMetadataCollection(); - try { - metadataCollection.readExternal(ois); - } catch (IOException e) { - throw new RuntimeException("cannot load/parse metadata", e); - } - return metadataCollection; - } finally { - try { - if (ois != null) { - // This will close all underlying streams as well, including source. - ois.close(); - } else { - source.close(); - } - } catch (IOException e) { - logger.log(Level.WARNING, "error closing input stream (ignored)", e); - } - } - } -} diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/MissingMetadataException.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/MissingMetadataException.java new file mode 100644 index 000000000..c2a8544ca --- /dev/null +++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/MissingMetadataException.java @@ -0,0 +1,9 @@ +package com.google.i18n.phonenumbers; + +/** Exception class for cases when expected metadata cannot be found. */ +public final class MissingMetadataException extends IllegalStateException { + + public MissingMetadataException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/MultiFileMetadataSourceImpl.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/MultiFileMetadataSourceImpl.java deleted file mode 100644 index 9a0b8e690..000000000 --- a/java/libphonenumber/src/com/google/i18n/phonenumbers/MultiFileMetadataSourceImpl.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2015 The Libphonenumber Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.i18n.phonenumbers; - -import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Implementation of {@link MetadataSource} that reads from multiple resource files. - */ -final class MultiFileMetadataSourceImpl implements MetadataSource { - // The prefix of the binary files containing phone number metadata for different regions. - // This enables us to set up with different metadata, such as for testing. - private final String phoneNumberMetadataFilePrefix; - - // The {@link MetadataLoader} used to inject alternative metadata sources. - private final MetadataLoader metadataLoader; - - // A mapping from a region code to the phone number metadata for that region code. - // Unlike the mappings for alternate formats and short number metadata, the phone number metadata - // is loaded from a non-statically determined file prefix; therefore this map is bound to the - // instance and not static. - private final ConcurrentHashMap geographicalRegions = - new ConcurrentHashMap(); - - // A mapping from a country calling code for a non-geographical entity to the phone number - // metadata for that country calling code. Examples of the country calling codes include 800 - // (International Toll Free Service) and 808 (International Shared Cost Service). - // Unlike the mappings for alternate formats and short number metadata, the phone number metadata - // is loaded from a non-statically determined file prefix; therefore this map is bound to the - // instance and not static. - private final ConcurrentHashMap nonGeographicalRegions = - new ConcurrentHashMap(); - - // It is assumed that metadataLoader is not null. Checks should happen before passing it in here. - // @VisibleForTesting - MultiFileMetadataSourceImpl(String phoneNumberMetadataFilePrefix, MetadataLoader metadataLoader) { - this.phoneNumberMetadataFilePrefix = phoneNumberMetadataFilePrefix; - this.metadataLoader = metadataLoader; - } - - // It is assumed that metadataLoader is not null. Checks should happen before passing it in here. - MultiFileMetadataSourceImpl(MetadataLoader metadataLoader) { - this(MetadataManager.MULTI_FILE_PHONE_NUMBER_METADATA_FILE_PREFIX, metadataLoader); - } - - @Override - public PhoneMetadata getMetadataForRegion(String regionCode) { - return MetadataManager.getMetadataFromMultiFilePrefix(regionCode, geographicalRegions, - phoneNumberMetadataFilePrefix, metadataLoader); - } - - @Override - public PhoneMetadata getMetadataForNonGeographicalRegion(int countryCallingCode) { - if (!isNonGeographical(countryCallingCode)) { - // The given country calling code was for a geographical region. - return null; - } - return MetadataManager.getMetadataFromMultiFilePrefix(countryCallingCode, nonGeographicalRegions, - phoneNumberMetadataFilePrefix, metadataLoader); - } - - // A country calling code is non-geographical if it only maps to the non-geographical region code, - // i.e. "001". - private boolean isNonGeographical(int countryCallingCode) { - List regionCodes = - CountryCodeToRegionCodeMap.getCountryCodeToRegionCodeMap().get(countryCallingCode); - return (regionCodes.size() == 1 - && PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY.equals(regionCodes.get(0))); - } -} diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberMatcher.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberMatcher.java index c7bde8e13..b812551db 100644 --- a/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberMatcher.java +++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberMatcher.java @@ -24,6 +24,7 @@ import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata; import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber.CountryCodeSource; import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; import com.google.i18n.phonenumbers.internal.RegexCache; +import com.google.i18n.phonenumbers.metadata.DefaultMetadataDependenciesProvider; import java.lang.Character.UnicodeBlock; import java.util.Iterator; import java.util.NoSuchElementException; @@ -575,7 +576,9 @@ final class PhoneNumberMatcher implements Iterator { } // If this didn't pass, see if there are any alternate formats that match, and try them instead. PhoneMetadata alternateFormats = - MetadataManager.getAlternateFormatsForCountry(number.getCountryCode()); + DefaultMetadataDependenciesProvider.getInstance() + .getAlternateFormatsMetadataSource() + .getFormattingMetadataForCountryCallingCode(number.getCountryCode()); String nationalSignificantNumber = util.getNationalSignificantNumber(number); if (alternateFormats != null) { for (NumberFormat alternateFormat : alternateFormats.getNumberFormatList()) { diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberUtil.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberUtil.java index 646b13450..677110d32 100644 --- a/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberUtil.java +++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberUtil.java @@ -24,11 +24,12 @@ import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber.CountryCodeSource; import com.google.i18n.phonenumbers.internal.MatcherApi; import com.google.i18n.phonenumbers.internal.RegexBasedMatcher; import com.google.i18n.phonenumbers.internal.RegexCache; - +import com.google.i18n.phonenumbers.metadata.DefaultMetadataDependenciesProvider; +import com.google.i18n.phonenumbers.metadata.source.MetadataSource; +import com.google.i18n.phonenumbers.metadata.source.MetadataSourceImpl; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -121,16 +122,16 @@ public class PhoneNumberUtil { private static final Map ALL_PLUS_NUMBER_GROUPING_SYMBOLS; static { - HashMap mobileTokenMap = new HashMap(); + HashMap mobileTokenMap = new HashMap<>(); mobileTokenMap.put(54, "9"); MOBILE_TOKEN_MAPPINGS = Collections.unmodifiableMap(mobileTokenMap); - HashSet geoMobileCountriesWithoutMobileAreaCodes = new HashSet(); + HashSet geoMobileCountriesWithoutMobileAreaCodes = new HashSet<>(); geoMobileCountriesWithoutMobileAreaCodes.add(86); // China GEO_MOBILE_COUNTRIES_WITHOUT_MOBILE_AREA_CODES = Collections.unmodifiableSet(geoMobileCountriesWithoutMobileAreaCodes); - HashSet geoMobileCountries = new HashSet(); + HashSet geoMobileCountries = new HashSet<>(); geoMobileCountries.add(52); // Mexico geoMobileCountries.add(54); // Argentina geoMobileCountries.add(55); // Brazil @@ -140,7 +141,7 @@ public class PhoneNumberUtil { // Simple ASCII digits map used to populate ALPHA_PHONE_MAPPINGS and // ALL_PLUS_NUMBER_GROUPING_SYMBOLS. - HashMap asciiDigitMappings = new HashMap(); + HashMap asciiDigitMappings = new HashMap<>(); asciiDigitMappings.put('0', '0'); asciiDigitMappings.put('1', '1'); asciiDigitMappings.put('2', '2'); @@ -152,7 +153,7 @@ public class PhoneNumberUtil { asciiDigitMappings.put('8', '8'); asciiDigitMappings.put('9', '9'); - HashMap alphaMap = new HashMap(40); + HashMap alphaMap = new HashMap<>(40); alphaMap.put('A', '2'); alphaMap.put('B', '2'); alphaMap.put('C', '2'); @@ -181,19 +182,19 @@ public class PhoneNumberUtil { alphaMap.put('Z', '9'); ALPHA_MAPPINGS = Collections.unmodifiableMap(alphaMap); - HashMap combinedMap = new HashMap(100); + HashMap combinedMap = new HashMap<>(100); combinedMap.putAll(ALPHA_MAPPINGS); combinedMap.putAll(asciiDigitMappings); ALPHA_PHONE_MAPPINGS = Collections.unmodifiableMap(combinedMap); - HashMap diallableCharMap = new HashMap(); + HashMap diallableCharMap = new HashMap<>(); diallableCharMap.putAll(asciiDigitMappings); diallableCharMap.put(PLUS_SIGN, PLUS_SIGN); diallableCharMap.put('*', '*'); diallableCharMap.put('#', '#'); DIALLABLE_CHAR_MAPPINGS = Collections.unmodifiableMap(diallableCharMap); - HashMap allPlusNumberGroupings = new HashMap(); + HashMap allPlusNumberGroupings = new HashMap<>(); // Put (lower letter -> upper letter) and (upper letter -> upper letter) mappings. for (char c : ALPHA_MAPPINGS.keySet()) { allPlusNumberGroupings.put(Character.toLowerCase(c), c); @@ -308,8 +309,8 @@ public class PhoneNumberUtil { // version. private static final String EXTN_PATTERNS_FOR_PARSING = createExtnPattern(true); static final String EXTN_PATTERNS_FOR_MATCHING = createExtnPattern(false); - - /** + + /** * Helper method for constructing regular expressions for parsing. Creates an expression that * captures up to maxLength digits. */ @@ -659,7 +660,7 @@ public class PhoneNumberUtil { // The set of regions that share country calling code 1. // There are roughly 26 regions. // We set the initial capacity of the HashSet to 35 to offer a load factor of roughly 0.75. - private final Set nanpaRegions = new HashSet(35); + private final Set nanpaRegions = new HashSet<>(35); // A cache for frequently used region-specific regular expressions. // The initial capacity is set to 100 as this seems to be an optimal value for Android, based on @@ -669,11 +670,11 @@ public class PhoneNumberUtil { // The set of regions the library supports. // There are roughly 240 of them and we set the initial capacity of the HashSet to 320 to offer a // load factor of roughly 0.75. - private final Set supportedRegions = new HashSet(320); + private final Set supportedRegions = new HashSet<>(320); // The set of country calling codes that map to the non-geo entity region ("001"). This set // currently contains < 12 elements so the default capacity of 16 (load factor=0.75) is fine. - private final Set countryCodesForNonGeographicalRegion = new HashSet(); + private final Set countryCodesForNonGeographicalRegion = new HashSet<>(); /** * This class implements a singleton, the constructor is only visible to facilitate testing. @@ -1089,7 +1090,7 @@ public class PhoneNumberUtil { * be non-null. */ private Set getSupportedTypesForMetadata(PhoneMetadata metadata) { - Set types = new TreeSet(); + Set types = new TreeSet<>(); for (PhoneNumberType type : PhoneNumberType.values()) { if (type == PhoneNumberType.FIXED_LINE_OR_MOBILE || type == PhoneNumberType.UNKNOWN) { // Never return FIXED_LINE_OR_MOBILE (it is a convenience type, and represents that a @@ -1149,7 +1150,9 @@ public class PhoneNumberUtil { */ public static synchronized PhoneNumberUtil getInstance() { if (instance == null) { - setInstance(createInstance(MetadataManager.DEFAULT_METADATA_LOADER)); + MetadataLoader metadataLoader = DefaultMetadataDependenciesProvider.getInstance() + .getMetadataLoader(); + setInstance(createInstance(metadataLoader)); } return instance; } @@ -1170,7 +1173,11 @@ public class PhoneNumberUtil { if (metadataLoader == null) { throw new IllegalArgumentException("metadataLoader could not be null."); } - return createInstance(new MultiFileMetadataSourceImpl(metadataLoader)); + return createInstance(new MetadataSourceImpl( + DefaultMetadataDependenciesProvider.getInstance().getPhoneNumberMetadataFileNameProvider(), + metadataLoader, + DefaultMetadataDependenciesProvider.getInstance().getMetadataParser() + )); } /** @@ -1699,7 +1706,7 @@ public class PhoneNumberUtil { NumberFormat.Builder numFormatCopy = NumberFormat.newBuilder(); numFormatCopy.mergeFrom(formatRule); numFormatCopy.clearNationalPrefixFormattingRule(); - List numberFormats = new ArrayList(1); + List numberFormats = new ArrayList<>(1); numberFormats.add(numFormatCopy.build()); formattedNumber = formatByPattern(number, PhoneNumberFormat.NATIONAL, numberFormats); break; @@ -2275,21 +2282,42 @@ public class PhoneNumberUtil { } /** - * Returns the metadata for the given region code or {@code null} if the region code is invalid - * or unknown. + * Returns the metadata for the given region code or {@code null} if the region code is invalid or + * unknown. + * + * @throws MissingMetadataException if the region code is valid, but metadata cannot be found. */ PhoneMetadata getMetadataForRegion(String regionCode) { if (!isValidRegionCode(regionCode)) { return null; } - return metadataSource.getMetadataForRegion(regionCode); + PhoneMetadata phoneMetadata = metadataSource.getMetadataForRegion(regionCode); + ensureMetadataIsNonNull(phoneMetadata, "Missing metadata for region code " + regionCode); + return phoneMetadata; } + /** + * Returns the metadata for the given country calling code or {@code null} if the country calling + * code is invalid or unknown. + * + * @throws MissingMetadataException if the country calling code is valid, but metadata cannot be + * found. + */ PhoneMetadata getMetadataForNonGeographicalRegion(int countryCallingCode) { - if (!countryCallingCodeToRegionCodeMap.containsKey(countryCallingCode)) { + if (!countryCodesForNonGeographicalRegion.contains(countryCallingCode)) { return null; } - return metadataSource.getMetadataForNonGeographicalRegion(countryCallingCode); + PhoneMetadata phoneMetadata = metadataSource.getMetadataForNonGeographicalRegion( + countryCallingCode); + ensureMetadataIsNonNull(phoneMetadata, + "Missing metadata for country code " + countryCallingCode); + return phoneMetadata; + } + + private static void ensureMetadataIsNonNull(PhoneMetadata phoneMetadata, String message) { + if (phoneMetadata == null) { + throw new MissingMetadataException(message); + } } boolean isNumberMatchingDesc(String nationalNumber, PhoneNumberDesc numberDesc) { @@ -2585,7 +2613,7 @@ public class PhoneNumberUtil { PhoneNumberDesc mobileDesc = getNumberDescByType(metadata, PhoneNumberType.MOBILE); if (descHasPossibleNumberData(mobileDesc)) { // Merge the mobile data in if there was any. We have to make a copy to do this. - possibleLengths = new ArrayList(possibleLengths); + possibleLengths = new ArrayList<>(possibleLengths); // Note that when adding the possible lengths from mobile, we have to again check they // aren't empty since if they are this indicates they are the same as the general desc and // should be obtained from there. @@ -2599,7 +2627,7 @@ public class PhoneNumberUtil { if (localLengths.isEmpty()) { localLengths = mobileDesc.getPossibleLengthLocalOnlyList(); } else { - localLengths = new ArrayList(localLengths); + localLengths = new ArrayList<>(localLengths); localLengths.addAll(mobileDesc.getPossibleLengthLocalOnlyList()); Collections.sort(localLengths); } diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/ShortNumberInfo.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/ShortNumberInfo.java index 3e7df59e9..7ce2972ba 100644 --- a/java/libphonenumber/src/com/google/i18n/phonenumbers/ShortNumberInfo.java +++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/ShortNumberInfo.java @@ -22,6 +22,8 @@ import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata; import com.google.i18n.phonenumbers.Phonemetadata.PhoneNumberDesc; import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; +import com.google.i18n.phonenumbers.metadata.DefaultMetadataDependenciesProvider; +import com.google.i18n.phonenumbers.metadata.source.RegionMetadataSource; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -44,12 +46,13 @@ public class ShortNumberInfo { private static final Logger logger = Logger.getLogger(ShortNumberInfo.class.getName()); private static final ShortNumberInfo INSTANCE = - new ShortNumberInfo(RegexBasedMatcher.create()); + new ShortNumberInfo( + RegexBasedMatcher.create(), + DefaultMetadataDependenciesProvider.getInstance().getShortNumberMetadataSource()); // In these countries, if extra digits are added to an emergency number, it no longer connects // to the emergency service. - private static final Set REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT = - new HashSet(); + private static final Set REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT = new HashSet<>(); static { REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT.add("BR"); REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT.add("CL"); @@ -61,7 +64,7 @@ public class ShortNumberInfo { TOLL_FREE, STANDARD_RATE, PREMIUM_RATE, - UNKNOWN_COST; + UNKNOWN_COST } /** Returns the singleton instance of the ShortNumberInfo. */ @@ -79,9 +82,13 @@ public class ShortNumberInfo { // first. private final Map> countryCallingCodeToRegionCodeMap; + private final RegionMetadataSource shortNumberMetadataSource; + // @VisibleForTesting - ShortNumberInfo(MatcherApi matcherApi) { + ShortNumberInfo(MatcherApi matcherApi, + RegionMetadataSource shortNumberMetadataSource) { this.matcherApi = matcherApi; + this.shortNumberMetadataSource = shortNumberMetadataSource; // TODO: Create ShortNumberInfo for a given map this.countryCallingCodeToRegionCodeMap = CountryCodeToRegionCodeMap.getCountryCodeToRegionCodeMap(); @@ -108,6 +115,21 @@ public class ShortNumberInfo { return regionCodes.contains(regionDialingFrom); } + /** + * A thin wrapper around {@code shortNumberMetadataSource} which catches {@link + * IllegalArgumentException} for invalid region code and instead returns {@code null} + */ + private PhoneMetadata getShortNumberMetadataForRegion(String regionCode) { + if (regionCode == null) { + return null; + } + try { + return shortNumberMetadataSource.getMetadataForRegion(regionCode); + } catch (IllegalArgumentException e) { + return null; + } + } + /** * Check whether a short number is a possible number when dialed from the given region. This * provides a more lenient check than {@link #isValidShortNumberForRegion}. @@ -120,8 +142,7 @@ public class ShortNumberInfo { if (!regionDialingFromMatchesNumber(number, regionDialingFrom)) { return false; } - PhoneMetadata phoneMetadata = - MetadataManager.getShortNumberMetadataForRegion(regionDialingFrom); + PhoneMetadata phoneMetadata = getShortNumberMetadataForRegion(regionDialingFrom); if (phoneMetadata == null) { return false; } @@ -142,7 +163,7 @@ public class ShortNumberInfo { List regionCodes = getRegionCodesForCountryCode(number.getCountryCode()); int shortNumberLength = getNationalSignificantNumber(number).length(); for (String region : regionCodes) { - PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(region); + PhoneMetadata phoneMetadata = getShortNumberMetadataForRegion(region); if (phoneMetadata == null) { continue; } @@ -166,8 +187,7 @@ public class ShortNumberInfo { if (!regionDialingFromMatchesNumber(number, regionDialingFrom)) { return false; } - PhoneMetadata phoneMetadata = - MetadataManager.getShortNumberMetadataForRegion(regionDialingFrom); + PhoneMetadata phoneMetadata = getShortNumberMetadataForRegion(regionDialingFrom); if (phoneMetadata == null) { return false; } @@ -228,8 +248,7 @@ public class ShortNumberInfo { return ShortNumberCost.UNKNOWN_COST; } // Note that regionDialingFrom may be null, in which case phoneMetadata will also be null. - PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion( - regionDialingFrom); + PhoneMetadata phoneMetadata = getShortNumberMetadataForRegion(regionDialingFrom); if (phoneMetadata == null) { return ShortNumberCost.UNKNOWN_COST; } @@ -326,7 +345,7 @@ public class ShortNumberInfo { } String nationalNumber = getNationalSignificantNumber(number); for (String regionCode : regionCodes) { - PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionCode); + PhoneMetadata phoneMetadata = getShortNumberMetadataForRegion(regionCode); if (phoneMetadata != null && matchesPossibleNumberAndNationalNumber(nationalNumber, phoneMetadata.getShortCode())) { // The number is valid for this region. @@ -336,13 +355,6 @@ public class ShortNumberInfo { return null; } - /** - * Convenience method to get a list of what regions the library has metadata for. - */ - Set getSupportedRegions() { - return MetadataManager.getSupportedShortNumberRegions(); - } - /** * Gets a valid short number for the specified region. * @@ -352,7 +364,7 @@ public class ShortNumberInfo { */ // @VisibleForTesting String getExampleShortNumber(String regionCode) { - PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionCode); + PhoneMetadata phoneMetadata = getShortNumberMetadataForRegion(regionCode); if (phoneMetadata == null) { return ""; } @@ -373,7 +385,7 @@ public class ShortNumberInfo { */ // @VisibleForTesting String getExampleShortNumberForCost(String regionCode, ShortNumberCost cost) { - PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionCode); + PhoneMetadata phoneMetadata = getShortNumberMetadataForRegion(regionCode); if (phoneMetadata == null) { return ""; } @@ -441,7 +453,7 @@ public class ShortNumberInfo { // add additional logic here to handle it. return false; } - PhoneMetadata metadata = MetadataManager.getShortNumberMetadataForRegion(regionCode); + PhoneMetadata metadata = getShortNumberMetadataForRegion(regionCode); if (metadata == null || !metadata.hasEmergency()) { return false; } @@ -468,7 +480,7 @@ public class ShortNumberInfo { List regionCodes = getRegionCodesForCountryCode(number.getCountryCode()); String regionCode = getRegionCodeForShortNumberFromRegionList(number, regionCodes); String nationalNumber = getNationalSignificantNumber(number); - PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionCode); + PhoneMetadata phoneMetadata = getShortNumberMetadataForRegion(regionCode); return (phoneMetadata != null) && (matchesPossibleNumberAndNationalNumber(nationalNumber, phoneMetadata.getCarrierSpecific())); @@ -492,8 +504,7 @@ public class ShortNumberInfo { return false; } String nationalNumber = getNationalSignificantNumber(number); - PhoneMetadata phoneMetadata = - MetadataManager.getShortNumberMetadataForRegion(regionDialingFrom); + PhoneMetadata phoneMetadata = getShortNumberMetadataForRegion(regionDialingFrom); return (phoneMetadata != null) && (matchesPossibleNumberAndNationalNumber(nationalNumber, phoneMetadata.getCarrierSpecific())); @@ -516,8 +527,7 @@ public class ShortNumberInfo { if (!regionDialingFromMatchesNumber(number, regionDialingFrom)) { return false; } - PhoneMetadata phoneMetadata = - MetadataManager.getShortNumberMetadataForRegion(regionDialingFrom); + PhoneMetadata phoneMetadata = getShortNumberMetadataForRegion(regionDialingFrom); return phoneMetadata != null && matchesPossibleNumberAndNationalNumber(getNationalSignificantNumber(number), phoneMetadata.getSmsServices()); diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/SingleFileMetadataSourceImpl.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/SingleFileMetadataSourceImpl.java deleted file mode 100644 index a988a543e..000000000 --- a/java/libphonenumber/src/com/google/i18n/phonenumbers/SingleFileMetadataSourceImpl.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2015 The Libphonenumber Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.i18n.phonenumbers; - -import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata; -import java.util.concurrent.atomic.AtomicReference; - -/** - * Implementation of {@link MetadataSource} that reads from a single resource file. - */ -final class SingleFileMetadataSourceImpl implements MetadataSource { - // The name of the binary file containing phone number metadata for different regions. - // This enables us to set up with different metadata, such as for testing. - private final String phoneNumberMetadataFileName; - - // The {@link MetadataLoader} used to inject alternative metadata sources. - private final MetadataLoader metadataLoader; - - private final AtomicReference phoneNumberMetadataRef = - new AtomicReference(); - - // It is assumed that metadataLoader is not null. Checks should happen before passing it in here. - // @VisibleForTesting - SingleFileMetadataSourceImpl(String phoneNumberMetadataFileName, MetadataLoader metadataLoader) { - this.phoneNumberMetadataFileName = phoneNumberMetadataFileName; - this.metadataLoader = metadataLoader; - } - - // It is assumed that metadataLoader is not null. Checks should happen before passing it in here. - SingleFileMetadataSourceImpl(MetadataLoader metadataLoader) { - this(MetadataManager.SINGLE_FILE_PHONE_NUMBER_METADATA_FILE_NAME, metadataLoader); - } - - @Override - public PhoneMetadata getMetadataForRegion(String regionCode) { - return MetadataManager.getSingleFileMetadataMaps(phoneNumberMetadataRef, - phoneNumberMetadataFileName, metadataLoader).get(regionCode); - } - - @Override - public PhoneMetadata getMetadataForNonGeographicalRegion(int countryCallingCode) { - // A country calling code is non-geographical if it only maps to the non-geographical region - // code, i.e. "001". If this is not true of the given country calling code, then we will return - // null here. If not for the atomic reference, such as if we were loading in multiple stages, we - // would check that the passed in country calling code was indeed non-geographical to avoid - // loading costs for a null result. Here though we do not check this since the entire data must - // be loaded anyway if any of it is needed at some point in the life cycle of this class. - return MetadataManager.getSingleFileMetadataMaps(phoneNumberMetadataRef, - phoneNumberMetadataFileName, metadataLoader).get(countryCallingCode); - } -} diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/internal/GeoEntityUtility.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/internal/GeoEntityUtility.java new file mode 100644 index 000000000..ef0cf67fa --- /dev/null +++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/internal/GeoEntityUtility.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2022 The Libphonenumber Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.i18n.phonenumbers.internal; + +import com.google.i18n.phonenumbers.CountryCodeToRegionCodeMap; +import java.util.List; + +/** + * Utility class for checking whether identifiers region code and country calling code belong + * to geographical entities. For more information about geo vs. non-geo entities see {@link + * com.google.i18n.phonenumbers.metadata.source.RegionMetadataSource} and {@link + * com.google.i18n.phonenumbers.metadata.source.NonGeographicalEntityMetadataSource} + */ +public final class GeoEntityUtility { + + /** Region code with a special meaning, used to mark non-geographical entities */ + public static final String REGION_CODE_FOR_NON_GEO_ENTITIES = "001"; + + /** Determines whether {@code regionCode} belongs to a geographical entity. */ + public static boolean isGeoEntity(String regionCode) { + return !regionCode.equals(REGION_CODE_FOR_NON_GEO_ENTITIES); + } + + /** + * Determines whether {@code countryCallingCode} belongs to a geographical entity. + * + *

A single country calling code could map to several different regions. It is considered that + * {@code countryCallingCode} belongs to a geo entity if all of these regions are geo entities + * + *

Note that this method will not throw an exception even when the underlying mapping for the + * {@code countryCallingCode} does not exist, instead it will return {@code false} + */ + public static boolean isGeoEntity(int countryCallingCode) { + List regionCodesForCountryCallingCode = + CountryCodeToRegionCodeMap.getCountryCodeToRegionCodeMap().get(countryCallingCode); + + return regionCodesForCountryCallingCode != null + && !regionCodesForCountryCallingCode.contains(REGION_CODE_FOR_NON_GEO_ENTITIES); + } + + private GeoEntityUtility() {} +} diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/DefaultMetadataDependenciesProvider.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/DefaultMetadataDependenciesProvider.java new file mode 100644 index 000000000..ab818acdc --- /dev/null +++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/DefaultMetadataDependenciesProvider.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2022 The Libphonenumber Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.i18n.phonenumbers.metadata; + +import com.google.i18n.phonenumbers.MetadataLoader; +import com.google.i18n.phonenumbers.metadata.init.ClassPathResourceMetadataLoader; +import com.google.i18n.phonenumbers.metadata.init.MetadataParser; +import com.google.i18n.phonenumbers.metadata.source.FormattingMetadataSource; +import com.google.i18n.phonenumbers.metadata.source.FormattingMetadataSourceImpl; +import com.google.i18n.phonenumbers.metadata.source.MetadataSource; +import com.google.i18n.phonenumbers.metadata.source.MetadataSourceImpl; +import com.google.i18n.phonenumbers.metadata.source.MultiFileModeFileNameProvider; +import com.google.i18n.phonenumbers.metadata.source.PhoneMetadataFileNameProvider; +import com.google.i18n.phonenumbers.metadata.source.RegionMetadataSource; +import com.google.i18n.phonenumbers.metadata.source.RegionMetadataSourceImpl; + +/** + * Provides metadata init and source dependencies when metadata is stored in multi-file mode and + * loaded as a classpath resource. + */ +public final class DefaultMetadataDependenciesProvider { + + private static final DefaultMetadataDependenciesProvider INSTANCE = new DefaultMetadataDependenciesProvider(); + + public static DefaultMetadataDependenciesProvider getInstance() { + return INSTANCE; + } + + private DefaultMetadataDependenciesProvider() { + } + + private final MetadataParser metadataParser = MetadataParser.newLenientParser(); + private final MetadataLoader metadataLoader = new ClassPathResourceMetadataLoader(); + + private final PhoneMetadataFileNameProvider phoneNumberMetadataFileNameProvider = + new MultiFileModeFileNameProvider( + "/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto"); + private final MetadataSource phoneNumberMetadataSource = + new MetadataSourceImpl( + phoneNumberMetadataFileNameProvider, + metadataLoader, + metadataParser); + + private final PhoneMetadataFileNameProvider shortNumberMetadataFileNameProvider = + new MultiFileModeFileNameProvider( + "/com/google/i18n/phonenumbers/data/ShortNumberMetadataProto"); + private final RegionMetadataSource shortNumberMetadataSource = + new RegionMetadataSourceImpl( + shortNumberMetadataFileNameProvider, + metadataLoader, + metadataParser); + + private final PhoneMetadataFileNameProvider alternateFormatsMetadataFileNameProvider = + new MultiFileModeFileNameProvider( + "/com/google/i18n/phonenumbers/data/PhoneNumberAlternateFormatsProto"); + private final FormattingMetadataSource alternateFormatsMetadataSource = + new FormattingMetadataSourceImpl( + alternateFormatsMetadataFileNameProvider, + metadataLoader, + metadataParser); + + public MetadataParser getMetadataParser() { + return metadataParser; + } + + public MetadataLoader getMetadataLoader() { + return metadataLoader; + } + + public PhoneMetadataFileNameProvider getPhoneNumberMetadataFileNameProvider() { + return phoneNumberMetadataFileNameProvider; + } + + public MetadataSource getPhoneNumberMetadataSource() { + return phoneNumberMetadataSource; + } + + public PhoneMetadataFileNameProvider getShortNumberMetadataFileNameProvider() { + return shortNumberMetadataFileNameProvider; + } + + public RegionMetadataSource getShortNumberMetadataSource() { + return shortNumberMetadataSource; + } + + public PhoneMetadataFileNameProvider getAlternateFormatsMetadataFileNameProvider() { + return alternateFormatsMetadataFileNameProvider; + } + + public FormattingMetadataSource getAlternateFormatsMetadataSource() { + return alternateFormatsMetadataSource; + } + + public String getCarrierDataDirectory() { + return "/com/google/i18n/phonenumbers/buildtools/carrier_data/"; + } + + public String getGeocodingDataDirectory() { + return "/com/google/i18n/phonenumbers/buildtools/geocoding_data/"; + } +} diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/init/ClassPathResourceMetadataLoader.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/init/ClassPathResourceMetadataLoader.java new file mode 100644 index 000000000..76122b8ad --- /dev/null +++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/init/ClassPathResourceMetadataLoader.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2022 The Libphonenumber Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.i18n.phonenumbers.metadata.init; + +import com.google.i18n.phonenumbers.MetadataLoader; +import java.io.InputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A {@link MetadataLoader} implementation that reads phone number metadata files as classpath + * resources. + */ +public final class ClassPathResourceMetadataLoader implements MetadataLoader { + + private static final Logger logger = + Logger.getLogger(ClassPathResourceMetadataLoader.class.getName()); + + @Override + public InputStream loadMetadata(String metadataFileName) { + InputStream inputStream = + ClassPathResourceMetadataLoader.class.getResourceAsStream(metadataFileName); + if (inputStream == null) { + logger.log(Level.WARNING, String.format("File %s not found", metadataFileName)); + } + return inputStream; + } +} \ No newline at end of file diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/init/MetadataParser.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/init/MetadataParser.java new file mode 100644 index 000000000..e923dfadf --- /dev/null +++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/init/MetadataParser.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2022 The Libphonenumber Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.i18n.phonenumbers.metadata.init; + +import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata; +import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadataCollection; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Exposes single method for parsing {@link InputStream} content into {@link Collection} of {@link + * PhoneMetadata} + */ +public final class MetadataParser { + + private static final Logger logger = Logger.getLogger(MetadataParser.class.getName()); + + /** + * Creates new instance in lenient mode, see {@link MetadataParser#parse(InputStream)} for more + * info. + */ + public static MetadataParser newLenientParser() { + return new MetadataParser(false); + } + + /** + * Creates new instance in strict mode, see {@link MetadataParser#parse(InputStream)} for more + * info + */ + public static MetadataParser newStrictParser() { + return new MetadataParser(true); + } + + private final boolean strictMode; + + private MetadataParser(boolean strictMode) { + this.strictMode = strictMode; + } + + /** + * Parses given {@link InputStream} into a {@link Collection} of {@link PhoneMetadata}. + * + * @throws IllegalArgumentException if {@code source} is {@code null} and strict mode is on + * @return parsed {@link PhoneMetadata}, or empty {@link Collection} if {@code source} is {@code + * null} and lenient mode is on + */ + public Collection parse(InputStream source) { + if (source == null) { + return handleNullSource(); + } + ObjectInputStream ois = null; + try { + ois = new ObjectInputStream(source); + PhoneMetadataCollection phoneMetadataCollection = new PhoneMetadataCollection(); + phoneMetadataCollection.readExternal(ois); + List phoneMetadata = phoneMetadataCollection.getMetadataList(); + // Sanity check; this should not happen if provided InputStream is valid + if (phoneMetadata.isEmpty()) { + throw new IllegalStateException("Empty metadata"); + } + return phoneMetadataCollection.getMetadataList(); + } catch (IOException e) { + throw new IllegalStateException("Unable to parse metadata file", e); + } finally { + if (ois != null) { + // This will close all underlying streams as well, including source. + close(ois); + } else { + close(source); + } + } + } + + private List handleNullSource() { + if (strictMode) { + throw new IllegalArgumentException("Source cannot be null"); + } + return Collections.emptyList(); + } + + private void close(InputStream inputStream) { + try { + inputStream.close(); + } catch (IOException e) { + logger.log(Level.WARNING, "Error closing input stream (ignored)", e); + } + } +} \ No newline at end of file diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/BlockingMetadataBootstrappingGuard.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/BlockingMetadataBootstrappingGuard.java new file mode 100644 index 000000000..a3ff68248 --- /dev/null +++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/BlockingMetadataBootstrappingGuard.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2022 The Libphonenumber Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.i18n.phonenumbers.metadata.source; + +import com.google.i18n.phonenumbers.MetadataLoader; +import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata; +import com.google.i18n.phonenumbers.metadata.init.MetadataParser; +import java.io.InputStream; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +/** + * A blocking implementation of {@link MetadataBootstrappingGuard}. Can be used for both single-file + * (bulk) and multi-file metadata + * + * @param needs to extend {@link MetadataContainer} + */ +final class BlockingMetadataBootstrappingGuard + implements MetadataBootstrappingGuard { + + private final MetadataLoader metadataLoader; + private final MetadataParser metadataParser; + private final T metadataContainer; + private final Set loadedFiles; + + BlockingMetadataBootstrappingGuard( + MetadataLoader metadataLoader, MetadataParser metadataParser, T metadataContainer) { + this.metadataLoader = metadataLoader; + this.metadataParser = metadataParser; + this.metadataContainer = metadataContainer; + this.loadedFiles = new HashSet<>(); + } + + @Override + public T getOrBootstrap(String phoneMetadataFile) { + if (!loadedFiles.contains(phoneMetadataFile)) { + bootstrapMetadata(phoneMetadataFile); + } + return metadataContainer; + } + + private synchronized void bootstrapMetadata(String phoneMetadataFile) { + // Additional check is needed because multiple threads could pass the first check when calling + // getOrBootstrap() at the same time for unloaded metadata file + if (loadedFiles.contains(phoneMetadataFile)) { + return; + } + Collection phoneMetadata = read(phoneMetadataFile); + for (PhoneMetadata metadata : phoneMetadata) { + metadataContainer.accept(metadata); + } + loadedFiles.add(phoneMetadataFile); + } + + private Collection read(String phoneMetadataFile) { + try { + InputStream metadataStream = metadataLoader.loadMetadata(phoneMetadataFile); + return metadataParser.parse(metadataStream); + } catch (IllegalArgumentException | IllegalStateException e) { + throw new IllegalStateException("Failed to read file " + phoneMetadataFile, e); + } + } +} diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/CompositeMetadataContainer.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/CompositeMetadataContainer.java new file mode 100644 index 000000000..727574957 --- /dev/null +++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/CompositeMetadataContainer.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2022 The Libphonenumber Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.i18n.phonenumbers.metadata.source; + +import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata; +import com.google.i18n.phonenumbers.internal.GeoEntityUtility; + +/** + * Implementation of {@link MetadataContainer} which is a composition of different {@link + * MapBackedMetadataContainer}s. It adds items to a single simpler container at a time depending on + * the content of {@link PhoneMetadata}. + */ +final class CompositeMetadataContainer implements MetadataContainer { + + private final MapBackedMetadataContainer metadataByCountryCode = + MapBackedMetadataContainer.byCountryCallingCode(); + private final MapBackedMetadataContainer metadataByRegionCode = + MapBackedMetadataContainer.byRegionCode(); + + /** + * Intended to be called for geographical regions only. For non-geographical entities, use {@link + * CompositeMetadataContainer#getMetadataBy(int)} + */ + PhoneMetadata getMetadataBy(String regionCode) { + return metadataByRegionCode.getMetadataBy(regionCode); + } + + /** + * Intended to be called for non-geographical entities only, such as 800 (country code assigned to + * the Universal International Freephone Service). For geographical regions, use {@link + * CompositeMetadataContainer#getMetadataBy(String)} + */ + PhoneMetadata getMetadataBy(int countryCallingCode) { + return metadataByCountryCode.getMetadataBy(countryCallingCode); + } + + /** + * If the metadata belongs to a specific geographical region (it has a region code other than + * {@link GeoEntityUtility#REGION_CODE_FOR_NON_GEO_ENTITIES}), it will be added to a {@link + * MapBackedMetadataContainer} which stores metadata by region code. Otherwise, it will be added + * to a {@link MapBackedMetadataContainer} which stores metadata by country calling code. This + * means that {@link CompositeMetadataContainer#getMetadataBy(int)} will not work for country + * calling codes such as 41 (country calling code for Switzerland), only for country calling codes + * such as 800 (country code assigned to the Universal International Freephone Service) + */ + @Override + public void accept(PhoneMetadata phoneMetadata) { + String regionCode = metadataByRegionCode.getKeyProvider().getKeyOf(phoneMetadata); + if (GeoEntityUtility.isGeoEntity(regionCode)) { + metadataByRegionCode.accept(phoneMetadata); + } else { + metadataByCountryCode.accept(phoneMetadata); + } + } +} diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/FormattingMetadataSource.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/FormattingMetadataSource.java new file mode 100644 index 000000000..f4f332cb2 --- /dev/null +++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/FormattingMetadataSource.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2022 The Libphonenumber Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.i18n.phonenumbers.metadata.source; + +import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata; + +/** A source of formatting phone metadata. */ +public interface FormattingMetadataSource { + + /** + * Returns formatting phone metadata for provided country calling code. + * + *

This method is similar to the one in {@link + * NonGeographicalEntityMetadataSource#getMetadataForNonGeographicalRegion(int)}, except that it + * will not fail for geographical regions, it can be used for both geo- and non-geo entities. + * + *

In case the provided {@code countryCallingCode} maps to several different regions, only one + * would contain formatting metadata. + * + * @return the phone metadata for provided {@code countryCallingCode}, or null if there is none. + */ + PhoneMetadata getFormattingMetadataForCountryCallingCode(int countryCallingCode); +} diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/FormattingMetadataSourceImpl.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/FormattingMetadataSourceImpl.java new file mode 100644 index 000000000..d6a819099 --- /dev/null +++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/FormattingMetadataSourceImpl.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 The Libphonenumber Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.i18n.phonenumbers.metadata.source; + +import com.google.i18n.phonenumbers.MetadataLoader; +import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata; +import com.google.i18n.phonenumbers.metadata.init.MetadataParser; + +/** + * Implementation of {@link FormattingMetadataSource} guarded by {@link MetadataBootstrappingGuard} + * + *

By default, a {@link BlockingMetadataBootstrappingGuard} will be used, but any custom + * implementation can be injected. + */ +public final class FormattingMetadataSourceImpl implements FormattingMetadataSource { + + private final PhoneMetadataFileNameProvider phoneMetadataFileNameProvider; + private final MetadataBootstrappingGuard> bootstrappingGuard; + + public FormattingMetadataSourceImpl( + PhoneMetadataFileNameProvider phoneMetadataFileNameProvider, + MetadataBootstrappingGuard> bootstrappingGuard) { + this.phoneMetadataFileNameProvider = phoneMetadataFileNameProvider; + this.bootstrappingGuard = bootstrappingGuard; + } + + public FormattingMetadataSourceImpl( + PhoneMetadataFileNameProvider phoneMetadataFileNameProvider, + MetadataLoader metadataLoader, + MetadataParser metadataParser) { + this( + phoneMetadataFileNameProvider, + new BlockingMetadataBootstrappingGuard<>( + metadataLoader, metadataParser, MapBackedMetadataContainer.byCountryCallingCode())); + } + + @Override + public PhoneMetadata getFormattingMetadataForCountryCallingCode(int countryCallingCode) { + return bootstrappingGuard + .getOrBootstrap(phoneMetadataFileNameProvider.getFor(countryCallingCode)) + .getMetadataBy(countryCallingCode); + } +} diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/MapBackedMetadataContainer.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/MapBackedMetadataContainer.java new file mode 100644 index 000000000..639280d87 --- /dev/null +++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/MapBackedMetadataContainer.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2022 The Libphonenumber Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.i18n.phonenumbers.metadata.source; + +import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * A {@link MetadataContainer} implementation backed by a {@link ConcurrentHashMap} with generic + * keys. + */ +final class MapBackedMetadataContainer implements MetadataContainer { + + static MapBackedMetadataContainer byRegionCode() { + return new MapBackedMetadataContainer<>( + new KeyProvider() { + @Override + public String getKeyOf(PhoneMetadata phoneMetadata) { + return phoneMetadata.getId(); + } + }); + } + + static MapBackedMetadataContainer byCountryCallingCode() { + return new MapBackedMetadataContainer<>( + new KeyProvider() { + @Override + public Integer getKeyOf(PhoneMetadata phoneMetadata) { + return phoneMetadata.getCountryCode(); + } + }); + } + + private final ConcurrentMap metadataMap; + + private final KeyProvider keyProvider; + + private MapBackedMetadataContainer(KeyProvider keyProvider) { + this.metadataMap = new ConcurrentHashMap<>(); + this.keyProvider = keyProvider; + } + + PhoneMetadata getMetadataBy(T key) { + return key != null ? metadataMap.get(key) : null; + } + + KeyProvider getKeyProvider() { + return keyProvider; + } + + @Override + public void accept(PhoneMetadata phoneMetadata) { + metadataMap.put(keyProvider.getKeyOf(phoneMetadata), phoneMetadata); + } + + interface KeyProvider { + T getKeyOf(PhoneMetadata phoneMetadata); + } +} diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/MetadataBootstrappingGuard.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/MetadataBootstrappingGuard.java new file mode 100644 index 000000000..9380c5954 --- /dev/null +++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/MetadataBootstrappingGuard.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 The Libphonenumber Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.i18n.phonenumbers.metadata.source; + +/** + * Guard that ensures that metadata bootstrapping process (loading and parsing) is triggered only + * once per metadata file. + * + * @param needs to extend {@link MetadataContainer} + */ +public interface MetadataBootstrappingGuard { + + /** + * If metadata from the provided file has not yet been read, invokes loading and parsing from the + * provided file and adds the result to guarded {@link MetadataContainer}. + * + * @param phoneMetadataFile to read from + * @return guarded {@link MetadataContainer} + */ + T getOrBootstrap(String phoneMetadataFile); +} diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/MetadataSource.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/MetadataContainer.java similarity index 50% rename from java/libphonenumber/src/com/google/i18n/phonenumbers/MetadataSource.java rename to java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/MetadataContainer.java index f5ffcad8d..3f6b21ed0 100644 --- a/java/libphonenumber/src/com/google/i18n/phonenumbers/MetadataSource.java +++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/MetadataContainer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 The Libphonenumber Authors + * Copyright (C) 2022 The Libphonenumber Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,26 +14,19 @@ * limitations under the License. */ -package com.google.i18n.phonenumbers; +package com.google.i18n.phonenumbers.metadata.source; import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata; /** - * A source for phone metadata for all regions. + * A container for {@link PhoneMetadata} */ -interface MetadataSource { +interface MetadataContainer { /** - * Gets phone metadata for a region. - * @param regionCode the region code. - * @return the phone metadata for that region, or null if there is none. + * Adds {@link PhoneMetadata} to the container. It depends on the implementation of the interface + * what this means, for example {@link MapBackedMetadataContainer} simply adds the provided + * metadata into the backing map. Implementing classes should ensure thread-safety. */ - PhoneMetadata getMetadataForRegion(String regionCode); - - /** - * Gets phone metadata for a non-geographical region. - * @param countryCallingCode the country calling code. - * @return the phone metadata for that region, or null if there is none. - */ - PhoneMetadata getMetadataForNonGeographicalRegion(int countryCallingCode); + void accept(PhoneMetadata phoneMetadata); } diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/MetadataSource.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/MetadataSource.java new file mode 100644 index 000000000..d353ce969 --- /dev/null +++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/MetadataSource.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2022 The Libphonenumber Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.i18n.phonenumbers.metadata.source; + +/** A source of phone metadata split by different regions. */ +public interface MetadataSource extends RegionMetadataSource, NonGeographicalEntityMetadataSource { +} diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/MetadataSourceImpl.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/MetadataSourceImpl.java new file mode 100644 index 000000000..c3d1c7360 --- /dev/null +++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/MetadataSourceImpl.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2022 The Libphonenumber Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.i18n.phonenumbers.metadata.source; + +import com.google.i18n.phonenumbers.MetadataLoader; +import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata; +import com.google.i18n.phonenumbers.internal.GeoEntityUtility; +import com.google.i18n.phonenumbers.metadata.init.MetadataParser; + +/** + * Implementation of {@link MetadataSource} guarded by {@link MetadataBootstrappingGuard}. + * + *

By default, a {@link BlockingMetadataBootstrappingGuard} will be used, but any custom + * implementation can be injected. + */ +public final class MetadataSourceImpl implements MetadataSource { + + private final PhoneMetadataFileNameProvider phoneMetadataFileNameProvider; + private final MetadataBootstrappingGuard bootstrappingGuard; + + public MetadataSourceImpl( + PhoneMetadataFileNameProvider phoneMetadataFileNameProvider, + MetadataBootstrappingGuard bootstrappingGuard) { + this.phoneMetadataFileNameProvider = phoneMetadataFileNameProvider; + this.bootstrappingGuard = bootstrappingGuard; + } + + public MetadataSourceImpl( + PhoneMetadataFileNameProvider phoneMetadataFileNameProvider, + MetadataLoader metadataLoader, + MetadataParser metadataParser) { + this( + phoneMetadataFileNameProvider, + new BlockingMetadataBootstrappingGuard<>( + metadataLoader, metadataParser, new CompositeMetadataContainer())); + } + + @Override + public PhoneMetadata getMetadataForNonGeographicalRegion(int countryCallingCode) { + if (GeoEntityUtility.isGeoEntity(countryCallingCode)) { + throw new IllegalArgumentException( + countryCallingCode + " calling code belongs to a geo entity"); + } + return bootstrappingGuard + .getOrBootstrap(phoneMetadataFileNameProvider.getFor(countryCallingCode)) + .getMetadataBy(countryCallingCode); + } + + @Override + public PhoneMetadata getMetadataForRegion(String regionCode) { + if (!GeoEntityUtility.isGeoEntity(regionCode)) { + throw new IllegalArgumentException(regionCode + " region code is a non-geo entity"); + } + return bootstrappingGuard + .getOrBootstrap(phoneMetadataFileNameProvider.getFor(regionCode)) + .getMetadataBy(regionCode); + } +} diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/MultiFileModeFileNameProvider.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/MultiFileModeFileNameProvider.java new file mode 100644 index 000000000..0d9adb5ee --- /dev/null +++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/MultiFileModeFileNameProvider.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2022 The Libphonenumber Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.i18n.phonenumbers.metadata.source; + +import java.util.regex.Pattern; + +/** + * {@link PhoneMetadataFileNameProvider} implementation which appends key as a suffix to the + * predefined metadata file name base. + */ +public final class MultiFileModeFileNameProvider implements PhoneMetadataFileNameProvider { + + private final String phoneMetadataFileNamePrefix; + private static final Pattern ALPHANUMERIC = Pattern.compile("^[\\p{L}\\p{N}]+$"); + + public MultiFileModeFileNameProvider(String phoneMetadataFileNameBase) { + this.phoneMetadataFileNamePrefix = phoneMetadataFileNameBase + "_"; + } + + @Override + public String getFor(Object key) { + String keyAsString = key.toString(); + if (!ALPHANUMERIC.matcher(keyAsString).matches()) { + throw new IllegalArgumentException("Invalid key: " + keyAsString); + } + return phoneMetadataFileNamePrefix + key; + } +} diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/NonGeographicalEntityMetadataSource.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/NonGeographicalEntityMetadataSource.java new file mode 100644 index 000000000..70db06df0 --- /dev/null +++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/NonGeographicalEntityMetadataSource.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2022 The Libphonenumber Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.i18n.phonenumbers.metadata.source; + +import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata; + +/** + * A source of phone metadata for non-geographical entities. + * + *

Non-geographical entities are phone number ranges that have a country calling code, but either + * do not belong to an actual country (some international services), or belong to a region which has + * a different country calling code from the country it is part of. Examples of such ranges are + * those starting with: + * + *

    + *
  • 800 - country code assigned to the Universal International Freephone Service + *
  • 808 - country code assigned to the International Shared Cost Service + *
  • 870 - country code assigned to the Pitcairn Islands + *
  • ... + *
+ */ +public interface NonGeographicalEntityMetadataSource { + + /** + * Gets phone metadata for a non-geographical entity. + * + * @param countryCallingCode the country calling code. + * @return the phone metadata for that entity, or null if there is none. + * @throws IllegalArgumentException if provided {@code countryCallingCode} does not belong to a + * non-geographical entity + */ + PhoneMetadata getMetadataForNonGeographicalRegion(int countryCallingCode); +} diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/PhoneMetadataFileNameProvider.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/PhoneMetadataFileNameProvider.java new file mode 100644 index 000000000..c3d16887f --- /dev/null +++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/PhoneMetadataFileNameProvider.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2022 The Libphonenumber Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.i18n.phonenumbers.metadata.source; + +/** + * Abstraction responsible for inferring the metadata file name. + * + *

Two implementations are available: + * + *

    + *
  • {@link SingleFileModeFileNameProvider} for single-file metadata. + *
  • {@link MultiFileModeFileNameProvider} for multi-file metadata. + *
+ */ +public interface PhoneMetadataFileNameProvider { + + /** + * Returns phone metadata file path for the given key. Assumes that key.toString() is + * well-defined. + */ + String getFor(Object key); +} diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/RegionMetadataSource.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/RegionMetadataSource.java new file mode 100644 index 000000000..3cf15c206 --- /dev/null +++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/RegionMetadataSource.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2022 The Libphonenumber Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.i18n.phonenumbers.metadata.source; + +import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata; +import com.google.i18n.phonenumbers.internal.GeoEntityUtility; + +/** + * A source of phone metadata split by geographical regions. + */ +public interface RegionMetadataSource { + + /** + * Returns phone metadata for provided geographical region. + * + *

The {@code regionCode} must be different from {@link + * GeoEntityUtility#REGION_CODE_FOR_NON_GEO_ENTITIES}, which has a special meaning and is used to + * mark non-geographical regions (see {@link NonGeographicalEntityMetadataSource} for more + * information). + * + * @return the phone metadata for provided {@code regionCode}, or null if there is none. + * @throws IllegalArgumentException if provided {@code regionCode} is {@link + * GeoEntityUtility#REGION_CODE_FOR_NON_GEO_ENTITIES} + */ + PhoneMetadata getMetadataForRegion(String regionCode); +} diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/RegionMetadataSourceImpl.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/RegionMetadataSourceImpl.java new file mode 100644 index 000000000..0078dd945 --- /dev/null +++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/RegionMetadataSourceImpl.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2022 The Libphonenumber Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.i18n.phonenumbers.metadata.source; + +import com.google.i18n.phonenumbers.MetadataLoader; +import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata; +import com.google.i18n.phonenumbers.internal.GeoEntityUtility; +import com.google.i18n.phonenumbers.metadata.init.MetadataParser; + +/** + * Implementation of {@link RegionMetadataSource} guarded by {@link MetadataBootstrappingGuard} + * + *

By default, a {@link BlockingMetadataBootstrappingGuard} will be used, but any custom + * implementation can be injected. + */ +public final class RegionMetadataSourceImpl implements RegionMetadataSource { + + private final PhoneMetadataFileNameProvider phoneMetadataFileNameProvider; + private final MetadataBootstrappingGuard> + bootstrappingGuard; + + public RegionMetadataSourceImpl( + PhoneMetadataFileNameProvider phoneMetadataFileNameProvider, + MetadataBootstrappingGuard> bootstrappingGuard) { + this.phoneMetadataFileNameProvider = phoneMetadataFileNameProvider; + this.bootstrappingGuard = bootstrappingGuard; + } + + public RegionMetadataSourceImpl( + PhoneMetadataFileNameProvider phoneMetadataFileNameProvider, + MetadataLoader metadataLoader, + MetadataParser metadataParser) { + this( + phoneMetadataFileNameProvider, + new BlockingMetadataBootstrappingGuard<>( + metadataLoader, metadataParser, MapBackedMetadataContainer.byRegionCode())); + } + + @Override + public PhoneMetadata getMetadataForRegion(String regionCode) { + if (!GeoEntityUtility.isGeoEntity(regionCode)) { + throw new IllegalArgumentException(regionCode + " region code is a non-geo entity"); + } + return bootstrappingGuard + .getOrBootstrap(phoneMetadataFileNameProvider.getFor(regionCode)) + .getMetadataBy(regionCode); + } +} diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/SingleFileModeFileNameProvider.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/SingleFileModeFileNameProvider.java new file mode 100644 index 000000000..1d3d1eb81 --- /dev/null +++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/SingleFileModeFileNameProvider.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 The Libphonenumber Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.i18n.phonenumbers.metadata.source; + +/** + * {@link PhoneMetadataFileNameProvider} implementation that returns the same metadata file name for + * each key + */ +public final class SingleFileModeFileNameProvider implements PhoneMetadataFileNameProvider { + + private final String phoneMetadataFileName; + + public SingleFileModeFileNameProvider(String phoneMetadataFileName) { + this.phoneMetadataFileName = phoneMetadataFileName; + } + + @Override + public String getFor(Object key) { + return phoneMetadataFileName; + } +} diff --git a/java/libphonenumber/test/com/google/i18n/phonenumbers/ExampleNumbersTest.java b/java/libphonenumber/test/com/google/i18n/phonenumbers/ExampleNumbersTest.java index ebea7b7d3..e98470bfa 100644 --- a/java/libphonenumber/test/com/google/i18n/phonenumbers/ExampleNumbersTest.java +++ b/java/libphonenumber/test/com/google/i18n/phonenumbers/ExampleNumbersTest.java @@ -19,15 +19,15 @@ package com.google.i18n.phonenumbers; import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberType; import com.google.i18n.phonenumbers.Phonemetadata.PhoneNumberDesc; import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; - -import junit.framework.TestCase; - +import com.google.i18n.phonenumbers.metadata.DefaultMetadataDependenciesProvider; +import com.google.i18n.phonenumbers.metadata.source.RegionMetadataSource; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; +import junit.framework.TestCase; /** * Verifies all of the example numbers in the metadata are valid and of the correct type. If no @@ -37,10 +37,14 @@ import java.util.logging.Logger; */ public class ExampleNumbersTest extends TestCase { private static final Logger logger = Logger.getLogger(ExampleNumbersTest.class.getName()); - private PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance(); - private ShortNumberInfo shortNumberInfo = ShortNumberInfo.getInstance(); - private List invalidCases = new ArrayList(); - private List wrongTypeCases = new ArrayList(); + private final PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance(); + private final ShortNumberInfo shortNumberInfo = ShortNumberInfo.getInstance(); + private final RegionMetadataSource shortNumberMetadataSource = + DefaultMetadataDependenciesProvider.getInstance().getShortNumberMetadataSource(); + + private final List invalidCases = new ArrayList<>(); + private final List wrongTypeCases = new ArrayList<>(); + private final Set shortNumberSupportedRegions = ShortNumbersRegionCodeSet.getRegionCodeSet(); /** * @param exampleNumberRequestedType type we are requesting an example number for @@ -55,14 +59,14 @@ public class ExampleNumbersTest extends TestCase { if (exampleNumber != null) { if (!phoneNumberUtil.isValidNumber(exampleNumber)) { invalidCases.add(exampleNumber); - logger.log(Level.SEVERE, "Failed validation for " + exampleNumber.toString()); + logger.log(Level.SEVERE, "Failed validation for " + exampleNumber); } else { // We know the number is valid, now we check the type. PhoneNumberType exampleNumberType = phoneNumberUtil.getNumberType(exampleNumber); if (!possibleExpectedTypes.contains(exampleNumberType)) { wrongTypeCases.add(exampleNumber); logger.log(Level.SEVERE, "Wrong type for " - + exampleNumber.toString() + + exampleNumber + ": got " + exampleNumberType); logger.log(Level.WARNING, "Expected types: "); for (PhoneNumberType type : possibleExpectedTypes) { @@ -74,7 +78,7 @@ public class ExampleNumbersTest extends TestCase { } } - public void testFixedLine() throws Exception { + public void testFixedLine() { Set fixedLineTypes = EnumSet.of(PhoneNumberType.FIXED_LINE, PhoneNumberType.FIXED_LINE_OR_MOBILE); checkNumbersValidAndCorrectType(PhoneNumberType.FIXED_LINE, fixedLineTypes); @@ -82,7 +86,7 @@ public class ExampleNumbersTest extends TestCase { assertEquals(0, wrongTypeCases.size()); } - public void testMobile() throws Exception { + public void testMobile() { Set mobileTypes = EnumSet.of(PhoneNumberType.MOBILE, PhoneNumberType.FIXED_LINE_OR_MOBILE); checkNumbersValidAndCorrectType(PhoneNumberType.MOBILE, mobileTypes); @@ -90,56 +94,56 @@ public class ExampleNumbersTest extends TestCase { assertEquals(0, wrongTypeCases.size()); } - public void testTollFree() throws Exception { + public void testTollFree() { Set tollFreeTypes = EnumSet.of(PhoneNumberType.TOLL_FREE); checkNumbersValidAndCorrectType(PhoneNumberType.TOLL_FREE, tollFreeTypes); assertEquals(0, invalidCases.size()); assertEquals(0, wrongTypeCases.size()); } - public void testPremiumRate() throws Exception { + public void testPremiumRate() { Set premiumRateTypes = EnumSet.of(PhoneNumberType.PREMIUM_RATE); checkNumbersValidAndCorrectType(PhoneNumberType.PREMIUM_RATE, premiumRateTypes); assertEquals(0, invalidCases.size()); assertEquals(0, wrongTypeCases.size()); } - public void testVoip() throws Exception { + public void testVoip() { Set voipTypes = EnumSet.of(PhoneNumberType.VOIP); checkNumbersValidAndCorrectType(PhoneNumberType.VOIP, voipTypes); assertEquals(0, invalidCases.size()); assertEquals(0, wrongTypeCases.size()); } - public void testPager() throws Exception { + public void testPager() { Set pagerTypes = EnumSet.of(PhoneNumberType.PAGER); checkNumbersValidAndCorrectType(PhoneNumberType.PAGER, pagerTypes); assertEquals(0, invalidCases.size()); assertEquals(0, wrongTypeCases.size()); } - public void testUan() throws Exception { + public void testUan() { Set uanTypes = EnumSet.of(PhoneNumberType.UAN); checkNumbersValidAndCorrectType(PhoneNumberType.UAN, uanTypes); assertEquals(0, invalidCases.size()); assertEquals(0, wrongTypeCases.size()); } - public void testVoicemail() throws Exception { + public void testVoicemail() { Set voicemailTypes = EnumSet.of(PhoneNumberType.VOICEMAIL); checkNumbersValidAndCorrectType(PhoneNumberType.VOICEMAIL, voicemailTypes); assertEquals(0, invalidCases.size()); assertEquals(0, wrongTypeCases.size()); } - public void testSharedCost() throws Exception { + public void testSharedCost() { Set sharedCostTypes = EnumSet.of(PhoneNumberType.SHARED_COST); checkNumbersValidAndCorrectType(PhoneNumberType.SHARED_COST, sharedCostTypes); assertEquals(0, invalidCases.size()); assertEquals(0, wrongTypeCases.size()); } - public void testCanBeInternationallyDialled() throws Exception { + public void testCanBeInternationallyDialled() { for (String regionCode : phoneNumberUtil.getSupportedRegions()) { PhoneNumber exampleNumber = null; PhoneNumberDesc desc = @@ -153,41 +157,41 @@ public class ExampleNumbersTest extends TestCase { } if (exampleNumber != null && phoneNumberUtil.canBeInternationallyDialled(exampleNumber)) { wrongTypeCases.add(exampleNumber); - logger.log(Level.SEVERE, "Number " + exampleNumber.toString() + logger.log(Level.SEVERE, "Number " + exampleNumber + " should not be internationally diallable"); } } assertEquals(0, wrongTypeCases.size()); } - public void testGlobalNetworkNumbers() throws Exception { + public void testGlobalNetworkNumbers() { for (Integer callingCode : phoneNumberUtil.getSupportedGlobalNetworkCallingCodes()) { PhoneNumber exampleNumber = phoneNumberUtil.getExampleNumberForNonGeoEntity(callingCode); assertNotNull("No example phone number for calling code " + callingCode, exampleNumber); if (!phoneNumberUtil.isValidNumber(exampleNumber)) { invalidCases.add(exampleNumber); - logger.log(Level.SEVERE, "Failed validation for " + exampleNumber.toString()); + logger.log(Level.SEVERE, "Failed validation for " + exampleNumber); } } assertEquals(0, invalidCases.size()); } - public void testEveryRegionHasAnExampleNumber() throws Exception { + public void testEveryRegionHasAnExampleNumber() { for (String regionCode : phoneNumberUtil.getSupportedRegions()) { PhoneNumber exampleNumber = phoneNumberUtil.getExampleNumber(regionCode); assertNotNull("No example number found for region " + regionCode, exampleNumber); } } - public void testEveryRegionHasAnInvalidExampleNumber() throws Exception { + public void testEveryRegionHasAnInvalidExampleNumber() { for (String regionCode : phoneNumberUtil.getSupportedRegions()) { PhoneNumber exampleNumber = phoneNumberUtil.getInvalidExampleNumber(regionCode); assertNotNull("No invalid example number found for region " + regionCode, exampleNumber); } } - public void testEveryTypeHasAnExampleNumber() throws Exception { + public void testEveryTypeHasAnExampleNumber() { for (PhoneNumberUtil.PhoneNumberType type : PhoneNumberUtil.PhoneNumberType.values()) { if (type == PhoneNumberType.UNKNOWN) { continue; @@ -198,8 +202,8 @@ public class ExampleNumbersTest extends TestCase { } public void testShortNumbersValidAndCorrectCost() throws Exception { - List invalidStringCases = new ArrayList(); - for (String regionCode : shortNumberInfo.getSupportedRegions()) { + List invalidStringCases = new ArrayList<>(); + for (String regionCode : shortNumberSupportedRegions) { String exampleShortNumber = shortNumberInfo.getExampleShortNumber(regionCode); if (!shortNumberInfo.isValidShortNumberForRegion( phoneNumberUtil.parse(exampleShortNumber, regionCode), regionCode)) { @@ -211,7 +215,7 @@ public class ExampleNumbersTest extends TestCase { PhoneNumber phoneNumber = phoneNumberUtil.parse(exampleShortNumber, regionCode); if (!shortNumberInfo.isValidShortNumber(phoneNumber)) { invalidCases.add(phoneNumber); - logger.log(Level.SEVERE, "Failed validation for " + phoneNumber.toString()); + logger.log(Level.SEVERE, "Failed validation for " + phoneNumber); } for (ShortNumberInfo.ShortNumberCost cost : ShortNumberInfo.ShortNumberCost.values()) { @@ -236,9 +240,8 @@ public class ExampleNumbersTest extends TestCase { public void testEmergency() throws Exception { int wrongTypeCounter = 0; - for (String regionCode : shortNumberInfo.getSupportedRegions()) { - PhoneNumberDesc desc = - MetadataManager.getShortNumberMetadataForRegion(regionCode).getEmergency(); + for (String regionCode : shortNumberSupportedRegions) { + PhoneNumberDesc desc = shortNumberMetadataSource.getMetadataForRegion(regionCode).getEmergency(); if (desc.hasExampleNumber()) { String exampleNumber = desc.getExampleNumber(); PhoneNumber phoneNumber = phoneNumberUtil.parse(exampleNumber, regionCode); @@ -258,9 +261,8 @@ public class ExampleNumbersTest extends TestCase { public void testCarrierSpecificShortNumbers() throws Exception { int wrongTagCounter = 0; - for (String regionCode : shortNumberInfo.getSupportedRegions()) { - PhoneNumberDesc desc = - MetadataManager.getShortNumberMetadataForRegion(regionCode).getCarrierSpecific(); + for (String regionCode : shortNumberSupportedRegions) { + PhoneNumberDesc desc = shortNumberMetadataSource.getMetadataForRegion(regionCode).getCarrierSpecific(); if (desc.hasExampleNumber()) { String exampleNumber = desc.getExampleNumber(); PhoneNumber carrierSpecificNumber = phoneNumberUtil.parse(exampleNumber, regionCode); @@ -276,9 +278,8 @@ public class ExampleNumbersTest extends TestCase { public void testSmsServiceShortNumbers() throws Exception { int wrongTagCounter = 0; - for (String regionCode : shortNumberInfo.getSupportedRegions()) { - PhoneNumberDesc desc = - MetadataManager.getShortNumberMetadataForRegion(regionCode).getSmsServices(); + for (String regionCode : shortNumberSupportedRegions) { + PhoneNumberDesc desc = shortNumberMetadataSource.getMetadataForRegion(regionCode).getSmsServices(); if (desc.hasExampleNumber()) { String exampleNumber = desc.getExampleNumber(); PhoneNumber smsServiceNumber = phoneNumberUtil.parse(exampleNumber, regionCode); diff --git a/java/libphonenumber/test/com/google/i18n/phonenumbers/MetadataManagerTest.java b/java/libphonenumber/test/com/google/i18n/phonenumbers/MetadataManagerTest.java deleted file mode 100644 index 91b2f3950..000000000 --- a/java/libphonenumber/test/com/google/i18n/phonenumbers/MetadataManagerTest.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (C) 2012 The Libphonenumber Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.i18n.phonenumbers; - -import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata; -import java.util.concurrent.ConcurrentHashMap; -import junit.framework.TestCase; - -/** - * Some basic tests to check that metadata can be correctly loaded. - */ -public class MetadataManagerTest extends TestCase { - public void testAlternateFormatsLoadCorrectly() { - // We should have some data for Germany. - PhoneMetadata germanyMetadata = MetadataManager.getAlternateFormatsForCountry(49); - assertNotNull(germanyMetadata); - assertTrue(germanyMetadata.getNumberFormatCount() > 0); - } - - public void testAlternateFormatsFailsGracefully() throws Exception { - PhoneMetadata noAlternateFormats = MetadataManager.getAlternateFormatsForCountry(999); - assertNull(noAlternateFormats); - } - - public void testShortNumberMetadataLoadCorrectly() throws Exception { - // We should have some data for France. - PhoneMetadata franceMetadata = MetadataManager.getShortNumberMetadataForRegion("FR"); - assertNotNull(franceMetadata); - assertTrue(franceMetadata.hasShortCode()); - } - - public void testShortNumberMetadataFailsGracefully() throws Exception { - PhoneMetadata noShortNumberMetadata = MetadataManager.getShortNumberMetadataForRegion("XXX"); - assertNull(noShortNumberMetadata); - } - - public void testGetMetadataFromMultiFilePrefix_regionCode() { - ConcurrentHashMap map = new ConcurrentHashMap(); - PhoneMetadata metadata = MetadataManager.getMetadataFromMultiFilePrefix("CA", map, - "/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProtoForTesting", - MetadataManager.DEFAULT_METADATA_LOADER); - assertEquals(metadata, map.get("CA")); - } - - public void testGetMetadataFromMultiFilePrefix_countryCallingCode() { - ConcurrentHashMap map = new ConcurrentHashMap(); - PhoneMetadata metadata = MetadataManager.getMetadataFromMultiFilePrefix(800, map, - "/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProtoForTesting", - MetadataManager.DEFAULT_METADATA_LOADER); - assertEquals(metadata, map.get(800)); - } - - public void testGetMetadataFromMultiFilePrefix_missingMetadataFileThrowsRuntimeException() { - // In normal usage we should never get a state where we are asking to load metadata that doesn't - // exist. However if the library is packaged incorrectly in the jar, this could happen and the - // best we can do is make sure the exception has the file name in it. - try { - MetadataManager.getMetadataFromMultiFilePrefix("XX", - new ConcurrentHashMap(), "no/such/file", - MetadataManager.DEFAULT_METADATA_LOADER); - fail("expected exception"); - } catch (RuntimeException e) { - assertTrue("Unexpected error: " + e, e.getMessage().contains("no/such/file_XX")); - } - try { - MetadataManager.getMetadataFromMultiFilePrefix(123, - new ConcurrentHashMap(), "no/such/file", - MetadataManager.DEFAULT_METADATA_LOADER); - fail("expected exception"); - } catch (RuntimeException e) { - assertTrue("Unexpected error: " + e, e.getMessage().contains("no/such/file_123")); - } - } -} diff --git a/java/libphonenumber/test/com/google/i18n/phonenumbers/MultiFileMetadataSourceImplTest.java b/java/libphonenumber/test/com/google/i18n/phonenumbers/MultiFileMetadataSourceImplTest.java deleted file mode 100644 index 7c9b0fafe..000000000 --- a/java/libphonenumber/test/com/google/i18n/phonenumbers/MultiFileMetadataSourceImplTest.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (C) 2015 The Libphonenumber Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.i18n.phonenumbers; - -import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata; -import junit.framework.TestCase; - -/** - * Unit tests for MultiFileMetadataSourceImpl.java. - */ -public class MultiFileMetadataSourceImplTest extends TestCase { - private static final MultiFileMetadataSourceImpl SOURCE = - new MultiFileMetadataSourceImpl(MetadataManager.DEFAULT_METADATA_LOADER); - private static final MultiFileMetadataSourceImpl MISSING_FILE_SOURCE = - new MultiFileMetadataSourceImpl("no/such/file", MetadataManager.DEFAULT_METADATA_LOADER); - - public void testGeoPhoneNumberMetadataLoadCorrectly() { - // We should have some data for the UAE. - PhoneMetadata uaeMetadata = SOURCE.getMetadataForRegion("AE"); - assertEquals(uaeMetadata.getCountryCode(), 971); - assertTrue(uaeMetadata.hasGeneralDesc()); - } - - public void testGeoPhoneNumberMetadataLoadFromMissingFileThrowsException() throws Exception { - try { - MISSING_FILE_SOURCE.getMetadataForRegion("AE"); - fail("expected exception"); - } catch (RuntimeException e) { - assertTrue("Unexpected error: " + e, e.getMessage().contains("no/such/file")); - } - } - - public void testNonGeoPhoneNumberMetadataLoadCorrectly() { - // We should have some data for international toll-free numbers. - PhoneMetadata intlMetadata = SOURCE.getMetadataForNonGeographicalRegion(800); - assertEquals(intlMetadata.getId(), "001"); - assertTrue(intlMetadata.hasGeneralDesc()); - } - - public void testNonGeoPhoneNumberMetadataLoadFromMissingFileThrowsException() throws Exception { - try { - MISSING_FILE_SOURCE.getMetadataForNonGeographicalRegion(800); - fail("expected exception"); - } catch (RuntimeException e) { - assertTrue("Unexpected error: " + e, e.getMessage().contains("no/such/file")); - } - } -} diff --git a/java/libphonenumber/test/com/google/i18n/phonenumbers/PhoneNumberUtilTest.java b/java/libphonenumber/test/com/google/i18n/phonenumbers/PhoneNumberUtilTest.java index 641bd77d3..b133d7f14 100644 --- a/java/libphonenumber/test/com/google/i18n/phonenumbers/PhoneNumberUtilTest.java +++ b/java/libphonenumber/test/com/google/i18n/phonenumbers/PhoneNumberUtilTest.java @@ -16,6 +16,8 @@ package com.google.i18n.phonenumbers; +import static org.junit.Assert.assertThrows; + import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat; import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberType; import com.google.i18n.phonenumbers.PhoneNumberUtil.ValidationResult; @@ -25,9 +27,13 @@ import com.google.i18n.phonenumbers.Phonemetadata.PhoneNumberDesc; import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber.CountryCodeSource; +import com.google.i18n.phonenumbers.metadata.source.MetadataSource; import java.util.ArrayList; import java.util.List; import java.util.Set; +import org.junit.Assert; +import org.junit.function.ThrowingRunnable; +import org.mockito.Mockito; /** * Unit tests for PhoneNumberUtil.java @@ -119,6 +125,11 @@ public class PhoneNumberUtilTest extends TestMetadataTestCase { private static final PhoneNumber UNKNOWN_COUNTRY_CODE_NO_RAW_INPUT = new PhoneNumber().setCountryCode(2).setNationalNumber(12345L); + private final MetadataSource mockedMetadataSource = Mockito.mock(MetadataSource.class); + private final PhoneNumberUtil phoneNumberUtilWithMissingMetadata = + new PhoneNumberUtil(mockedMetadataSource, + CountryCodeToRegionCodeMapForTesting.getCountryCodeToRegionCodeMap()); + public void testGetSupportedRegions() { assertTrue(phoneUtil.getSupportedRegions().size() > 0); } @@ -3160,4 +3171,38 @@ public class PhoneNumberUtilTest extends TestMetadataTestCase { assertFalse(phoneUtil.isMobileNumberPortableRegion(RegionCode.AE)); assertFalse(phoneUtil.isMobileNumberPortableRegion(RegionCode.BS)); } + + public void testGetMetadataForRegionForNonGeoEntity_shouldBeNull() { + assertNull(phoneUtil.getMetadataForRegion(RegionCode.UN001)); + } + + public void testGetMetadataForRegionForUnknownRegion_shouldBeNull() { + assertNull(phoneUtil.getMetadataForRegion(RegionCode.ZZ)); + } + + public void testGetMetadataForNonGeographicalRegionForGeoRegion_shouldBeNull() { + assertNull(phoneUtil.getMetadataForNonGeographicalRegion(/* countryCallingCode = */ 1)); + } + + public void testGetMetadataForRegionForMissingMetadata() { + assertThrows( + MissingMetadataException.class, + new ThrowingRunnable() { + @Override + public void run() { + phoneNumberUtilWithMissingMetadata.getMetadataForRegion(RegionCode.US); + } + }); + } + + public void testGetMetadataForNonGeographicalRegionForMissingMetadata() { + assertThrows( + MissingMetadataException.class, + new ThrowingRunnable() { + @Override + public void run() { + phoneNumberUtilWithMissingMetadata.getMetadataForNonGeographicalRegion(800); + } + }); + } } diff --git a/java/libphonenumber/test/com/google/i18n/phonenumbers/SingleFileMetadataSourceImplTest.java b/java/libphonenumber/test/com/google/i18n/phonenumbers/SingleFileMetadataSourceImplTest.java deleted file mode 100644 index 664fc52d5..000000000 --- a/java/libphonenumber/test/com/google/i18n/phonenumbers/SingleFileMetadataSourceImplTest.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2015 The Libphonenumber Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.i18n.phonenumbers; - -import junit.framework.TestCase; - -/** - * Unit tests for SingleFileMetadataSourceImpl.java. - * - *

- * We do not package single file metadata files, so it is only possible to test failures here. - */ -public class SingleFileMetadataSourceImplTest extends TestCase { - private static final SingleFileMetadataSourceImpl MISSING_FILE_SOURCE = - new SingleFileMetadataSourceImpl("no/such/file", MetadataManager.DEFAULT_METADATA_LOADER); - - public void testGeoPhoneNumberMetadataLoadFromMissingFileThrowsException() throws Exception { - try { - MISSING_FILE_SOURCE.getMetadataForRegion("AE"); - fail("expected exception"); - } catch (RuntimeException e) { - assertTrue("Unexpected error: " + e, e.getMessage().contains("no/such/file")); - } - } - - public void testNonGeoPhoneNumberMetadataLoadFromMissingFileThrowsException() throws Exception { - try { - MISSING_FILE_SOURCE.getMetadataForNonGeographicalRegion(800); - fail("expected exception"); - } catch (RuntimeException e) { - assertTrue("Unexpected error: " + e, e.getMessage().contains("no/such/file")); - } - } -} diff --git a/java/libphonenumber/test/com/google/i18n/phonenumbers/TestMetadataTestCase.java b/java/libphonenumber/test/com/google/i18n/phonenumbers/TestMetadataTestCase.java index 51360d71a..5dfb56e96 100644 --- a/java/libphonenumber/test/com/google/i18n/phonenumbers/TestMetadataTestCase.java +++ b/java/libphonenumber/test/com/google/i18n/phonenumbers/TestMetadataTestCase.java @@ -16,6 +16,9 @@ package com.google.i18n.phonenumbers; +import com.google.i18n.phonenumbers.metadata.DefaultMetadataDependenciesProvider; +import com.google.i18n.phonenumbers.metadata.source.MetadataSourceImpl; +import com.google.i18n.phonenumbers.metadata.source.MultiFileModeFileNameProvider; import junit.framework.TestCase; /** @@ -33,15 +36,20 @@ import junit.framework.TestCase; * @author Shaopeng Jia */ public class TestMetadataTestCase extends TestCase { + private static final String TEST_METADATA_FILE_PREFIX = "/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProtoForTesting"; - /** An instance of PhoneNumberUtil that uses test metadata. */ + /** + * An instance of PhoneNumberUtil that uses test metadata. + */ protected final PhoneNumberUtil phoneUtil; public TestMetadataTestCase() { - phoneUtil = new PhoneNumberUtil(new MultiFileMetadataSourceImpl(TEST_METADATA_FILE_PREFIX, - MetadataManager.DEFAULT_METADATA_LOADER), + phoneUtil = new PhoneNumberUtil( + new MetadataSourceImpl(new MultiFileModeFileNameProvider(TEST_METADATA_FILE_PREFIX), + DefaultMetadataDependenciesProvider.getInstance().getMetadataLoader(), + DefaultMetadataDependenciesProvider.getInstance().getMetadataParser()), CountryCodeToRegionCodeMapForTesting.getCountryCodeToRegionCodeMap()); } diff --git a/java/libphonenumber/test/com/google/i18n/phonenumbers/internal/GeoEntityUtilityTest.java b/java/libphonenumber/test/com/google/i18n/phonenumbers/internal/GeoEntityUtilityTest.java new file mode 100644 index 000000000..300c5f531 --- /dev/null +++ b/java/libphonenumber/test/com/google/i18n/phonenumbers/internal/GeoEntityUtilityTest.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2022 The Libphonenumber Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.i18n.phonenumbers.internal; + +import junit.framework.TestCase; + +public class GeoEntityUtilityTest extends TestCase { + + public void test_isGeoEntity_shouldReturnTrueForCountryRegionCode() { + assertTrue(GeoEntityUtility.isGeoEntity("DE")); + } + + public void test_isGeoEntity_shouldReturnFalseForWorldRegionCode() { + assertFalse(GeoEntityUtility.isGeoEntity("001")); + } + + public void test_isGeoEntity_shouldReturnTrueForCountryCallingCode() { + assertTrue(GeoEntityUtility.isGeoEntity(41)); + } + + public void test_isGeoEntity_shouldReturnFalseForInternationalSharedCostServiceCallingCode() { + assertFalse(GeoEntityUtility.isGeoEntity(808)); + } + + public void test_isGeoEntity_shouldReturnFalseForNonExistingCountryCallingCode() { + assertFalse(GeoEntityUtility.isGeoEntity(111111111)); + } +} diff --git a/java/libphonenumber/test/com/google/i18n/phonenumbers/metadata/PhoneMetadataCollectionUtil.java b/java/libphonenumber/test/com/google/i18n/phonenumbers/metadata/PhoneMetadataCollectionUtil.java new file mode 100644 index 000000000..57fab1914 --- /dev/null +++ b/java/libphonenumber/test/com/google/i18n/phonenumbers/metadata/PhoneMetadataCollectionUtil.java @@ -0,0 +1,21 @@ +package com.google.i18n.phonenumbers.metadata; + +import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadataCollection; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectOutputStream; + +public class PhoneMetadataCollectionUtil { + + public static InputStream toInputStream(PhoneMetadataCollection metadata) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream); + metadata.writeExternal(objectOutputStream); + objectOutputStream.flush(); + InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); + objectOutputStream.close(); + return inputStream; + } +} diff --git a/java/libphonenumber/test/com/google/i18n/phonenumbers/metadata/init/MetadataParserTest.java b/java/libphonenumber/test/com/google/i18n/phonenumbers/metadata/init/MetadataParserTest.java new file mode 100644 index 000000000..22c52e29a --- /dev/null +++ b/java/libphonenumber/test/com/google/i18n/phonenumbers/metadata/init/MetadataParserTest.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2022 The Libphonenumber Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.i18n.phonenumbers.metadata.init; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertThrows; + +import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata; +import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadataCollection; +import com.google.i18n.phonenumbers.metadata.PhoneMetadataCollectionUtil; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; +import junit.framework.TestCase; +import org.junit.function.ThrowingRunnable; + +public final class MetadataParserTest extends TestCase { + + private static final MetadataParser metadataParser = MetadataParser.newStrictParser(); + + public void test_parse_shouldThrowExceptionForNullInput() { + assertThrows( + IllegalArgumentException.class, + new ThrowingRunnable() { + @Override + public void run() { + metadataParser.parse(null); + } + }); + } + + public void test_parse_shouldThrowExceptionForEmptyInput() { + final InputStream emptyInput = new ByteArrayInputStream(new byte[0]); + + assertThrows( + IllegalStateException.class, + new ThrowingRunnable() { + @Override + public void run() { + metadataParser.parse(emptyInput); + } + }); + } + + public void test_parse_shouldThrowExceptionForInvalidInput() { + final InputStream invalidInput = new ByteArrayInputStream("Some random input".getBytes(UTF_8)); + + assertThrows( + IllegalStateException.class, + new ThrowingRunnable() { + @Override + public void run() { + metadataParser.parse(invalidInput); + } + }); + } + + public void test_parse_shouldParseValidInput() throws IOException { + InputStream input = PhoneMetadataCollectionUtil.toInputStream( + PhoneMetadataCollection.newBuilder() + .addMetadata(PhoneMetadata.newBuilder().setId("id").build())); + + Collection actual = metadataParser.parse(input); + + assertEquals(1, actual.size()); + } + + public void test_parse_shouldReturnEmptyCollectionForNullInput() { + Collection actual = MetadataParser.newLenientParser().parse(null); + + assertTrue(actual.isEmpty()); + } +} \ No newline at end of file diff --git a/java/libphonenumber/test/com/google/i18n/phonenumbers/metadata/source/BlockingMetadataBootstrappingGuardTest.java b/java/libphonenumber/test/com/google/i18n/phonenumbers/metadata/source/BlockingMetadataBootstrappingGuardTest.java new file mode 100644 index 000000000..c29177089 --- /dev/null +++ b/java/libphonenumber/test/com/google/i18n/phonenumbers/metadata/source/BlockingMetadataBootstrappingGuardTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2022 The Libphonenumber Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.i18n.phonenumbers.metadata.source; + +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.i18n.phonenumbers.MetadataLoader; +import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata; +import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadataCollection; +import com.google.i18n.phonenumbers.metadata.PhoneMetadataCollectionUtil; +import com.google.i18n.phonenumbers.metadata.init.MetadataParser; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import junit.framework.TestCase; +import org.junit.Assert; +import org.junit.function.ThrowingRunnable; +import org.mockito.Mockito; + +public class BlockingMetadataBootstrappingGuardTest extends TestCase { + + private static final String PHONE_METADATA_FILE = "some metadata file"; + private static final PhoneMetadataCollection PHONE_METADATA = + PhoneMetadataCollection.newBuilder() + .addMetadata(PhoneMetadata.newBuilder().setId("id").build()); + + private final MetadataLoader metadataLoader = Mockito.mock(MetadataLoader.class); + private final MetadataContainer metadataContainer = Mockito.mock(MetadataContainer.class); + + private BlockingMetadataBootstrappingGuard bootstrappingGuard; + + @Override + public void setUp() throws IOException { + when(metadataLoader.loadMetadata(PHONE_METADATA_FILE)) + .thenReturn(PhoneMetadataCollectionUtil.toInputStream(PHONE_METADATA)); + bootstrappingGuard = + new BlockingMetadataBootstrappingGuard<>( + metadataLoader, MetadataParser.newStrictParser(), metadataContainer); + } + + public void test_getOrBootstrap_shouldInvokeBootstrappingOnlyOnce() { + bootstrappingGuard.getOrBootstrap(PHONE_METADATA_FILE); + bootstrappingGuard.getOrBootstrap(PHONE_METADATA_FILE); + + verify(metadataLoader, times(1)).loadMetadata(PHONE_METADATA_FILE); + } + + public void test_getOrBootstrap_shouldIncludeFileNameInExceptionOnFailure() { + when(metadataLoader.loadMetadata(PHONE_METADATA_FILE)).thenReturn(null); + + ThrowingRunnable throwingRunnable = + new ThrowingRunnable() { + @Override + public void run() { + bootstrappingGuard.getOrBootstrap(PHONE_METADATA_FILE); + } + }; + + IllegalStateException exception = assertThrows(IllegalStateException.class, throwingRunnable); + Assert.assertTrue(exception.getMessage().contains(PHONE_METADATA_FILE)); + } + + public void test_getOrBootstrap_shouldInvokeBootstrappingOnlyOnceWhenThreadsCallItAtTheSameTime() + throws InterruptedException { + ExecutorService executorService = Executors.newFixedThreadPool(2); + + List runnables = new ArrayList<>(); + runnables.add(new BootstrappingRunnable()); + runnables.add(new BootstrappingRunnable()); + executorService.invokeAll(runnables); + + verify(metadataLoader, times(1)).loadMetadata(PHONE_METADATA_FILE); + } + + private class BootstrappingRunnable implements Callable { + + @Override + public MetadataContainer call() { + return bootstrappingGuard.getOrBootstrap(PHONE_METADATA_FILE); + } + } +} diff --git a/java/libphonenumber/test/com/google/i18n/phonenumbers/metadata/source/CompositeMetadataContainerTest.java b/java/libphonenumber/test/com/google/i18n/phonenumbers/metadata/source/CompositeMetadataContainerTest.java new file mode 100644 index 000000000..75b66c49f --- /dev/null +++ b/java/libphonenumber/test/com/google/i18n/phonenumbers/metadata/source/CompositeMetadataContainerTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2022 The Libphonenumber Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.i18n.phonenumbers.metadata.source; + +import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata; +import com.google.i18n.phonenumbers.internal.GeoEntityUtility; +import junit.framework.TestCase; + +public class CompositeMetadataContainerTest extends TestCase { + + private static final String REGION_CODE = "US"; + private static final Integer COUNTRY_CODE = 1; + private static final PhoneMetadata PHONE_METADATA_WITH_REGION_CODE = + PhoneMetadata.newBuilder().setId(REGION_CODE).setCountryCode(COUNTRY_CODE); + private static final PhoneMetadata PHONE_METADATA_WITH_COUNTRY_CODE = + PhoneMetadata.newBuilder() + .setId(GeoEntityUtility.REGION_CODE_FOR_NON_GEO_ENTITIES) + .setCountryCode(COUNTRY_CODE); + + private CompositeMetadataContainer metadataContainer; + + @Override + public void setUp() { + metadataContainer = new CompositeMetadataContainer(); + } + + public void test_getMetadataBy_shouldReturnNullForNonExistingRegionCode() { + assertNull(metadataContainer.getMetadataBy(REGION_CODE)); + } + + public void test_getMetadataBy_shouldReturnMetadataForExistingRegionCode() { + metadataContainer.accept(PHONE_METADATA_WITH_REGION_CODE); + + assertSame(PHONE_METADATA_WITH_REGION_CODE, metadataContainer.getMetadataBy(REGION_CODE)); + } + + public void test_getMetadataBy_shouldReturnNullForNonExistingCountryCode() { + assertNull(metadataContainer.getMetadataBy(COUNTRY_CODE)); + } + + public void test_getMetadataBy_shouldReturnMetadataForExistingCountryCode() { + metadataContainer.accept(PHONE_METADATA_WITH_COUNTRY_CODE); + + assertSame(PHONE_METADATA_WITH_COUNTRY_CODE, metadataContainer.getMetadataBy(COUNTRY_CODE)); + } + + public void test_getMetadataBy_shouldReturnNullForExistingCountryCodeOfGeoRegion() { + metadataContainer.accept(PHONE_METADATA_WITH_REGION_CODE); + + assertNull(metadataContainer.getMetadataBy(COUNTRY_CODE)); + } +} diff --git a/java/libphonenumber/test/com/google/i18n/phonenumbers/metadata/source/MapBackedMetadataContainerTest.java b/java/libphonenumber/test/com/google/i18n/phonenumbers/metadata/source/MapBackedMetadataContainerTest.java new file mode 100644 index 000000000..21fbea648 --- /dev/null +++ b/java/libphonenumber/test/com/google/i18n/phonenumbers/metadata/source/MapBackedMetadataContainerTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2022 The Libphonenumber Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.i18n.phonenumbers.metadata.source; + +import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata; +import junit.framework.TestCase; + +public class MapBackedMetadataContainerTest extends TestCase { + + private static final String REGION_CODE = "US"; + private static final Integer COUNTRY_CODE = 41; + private static final PhoneMetadata PHONE_METADATA = + PhoneMetadata.newBuilder().setId(REGION_CODE).setCountryCode(COUNTRY_CODE); + + public void test_getMetadataBy_shouldReturnNullForNullRegionCode() { + assertNull(MapBackedMetadataContainer.byRegionCode().getMetadataBy(null)); + } + + public void test_getMetadataBy_shouldReturnNullForNonExistingRegionCode() { + assertNull(MapBackedMetadataContainer.byRegionCode().getMetadataBy(REGION_CODE)); + } + + public void test_getMetadataBy_shouldReturnMetadataForExistingRegionCode() { + MapBackedMetadataContainer metadataContainer = + MapBackedMetadataContainer.byRegionCode(); + + metadataContainer.accept(PHONE_METADATA); + + assertSame(PHONE_METADATA, metadataContainer.getMetadataBy(REGION_CODE)); + } + + public void test_getMetadataBy_shouldReturnNullForNullCountryCode() { + assertNull(MapBackedMetadataContainer.byCountryCallingCode().getMetadataBy(null)); + } + + public void test_getMetadataBy_shouldReturnNullForNonExistingCountryCode() { + assertNull(MapBackedMetadataContainer.byCountryCallingCode().getMetadataBy(COUNTRY_CODE)); + } + + public void test_getMetadataBy_shouldReturnMetadataForExistingCountryCode() { + MapBackedMetadataContainer metadataContainer = + MapBackedMetadataContainer.byCountryCallingCode(); + + metadataContainer.accept(PHONE_METADATA); + + assertSame(PHONE_METADATA, metadataContainer.getMetadataBy(COUNTRY_CODE)); + } +} diff --git a/java/libphonenumber/test/com/google/i18n/phonenumbers/metadata/source/MultiFileModeFileNameProviderTest.java b/java/libphonenumber/test/com/google/i18n/phonenumbers/metadata/source/MultiFileModeFileNameProviderTest.java new file mode 100644 index 000000000..c7ad7ddcd --- /dev/null +++ b/java/libphonenumber/test/com/google/i18n/phonenumbers/metadata/source/MultiFileModeFileNameProviderTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2022 The Libphonenumber Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.i18n.phonenumbers.metadata.source; + +import static org.junit.Assert.assertThrows; + +import junit.framework.TestCase; +import org.junit.function.ThrowingRunnable; + +public final class MultiFileModeFileNameProviderTest extends TestCase { + + private final PhoneMetadataFileNameProvider metadataFileNameProvider = + new MultiFileModeFileNameProvider("some/file"); + + public void test_getFor_shouldAppendKeyToTheBase() { + String metadataFileName = metadataFileNameProvider.getFor("key1"); + + assertEquals("some/file_key1", metadataFileName); + } + + public void test_getFor_shouldThrowExceptionForNonAlphanumericKey() { + assertThrows( + IllegalArgumentException.class, + new ThrowingRunnable() { + @Override + public void run() { + metadataFileNameProvider.getFor("\tkey1\n"); + } + }); + } +} diff --git a/java/libphonenumber/test/com/google/i18n/phonenumbers/metadata/source/SingleFileModeFileNameProviderTest.java b/java/libphonenumber/test/com/google/i18n/phonenumbers/metadata/source/SingleFileModeFileNameProviderTest.java new file mode 100644 index 000000000..21d3bf759 --- /dev/null +++ b/java/libphonenumber/test/com/google/i18n/phonenumbers/metadata/source/SingleFileModeFileNameProviderTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2022 The Libphonenumber Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.i18n.phonenumbers.metadata.source; + +import junit.framework.TestCase; + +public final class SingleFileModeFileNameProviderTest extends TestCase { + + private final PhoneMetadataFileNameProvider metadataFileNameProvider = + new SingleFileModeFileNameProvider("some/file"); + + public void test_getFor_shouldReturnTheFileNameBase() { + String metadataFileName = metadataFileNameProvider.getFor("key1"); + + assertEquals("some/file", metadataFileName); + } +} diff --git a/java/pom.xml b/java/pom.xml index 6e7cbd660..7f2c63494 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -235,6 +235,12 @@ 4.13.1 test + + org.mockito + mockito-all + 1.10.19 + test + diff --git a/tools/java/java-build/src/com/google/i18n/phonenumbers/BuildMetadataProtoFromXml.java b/tools/java/java-build/src/com/google/i18n/phonenumbers/BuildMetadataProtoFromXml.java index f7db4c82a..8e4fc3b59 100644 --- a/tools/java/java-build/src/com/google/i18n/phonenumbers/BuildMetadataProtoFromXml.java +++ b/tools/java/java-build/src/com/google/i18n/phonenumbers/BuildMetadataProtoFromXml.java @@ -254,7 +254,7 @@ public class BuildMetadataProtoFromXml extends Command { writer.addToImports("java.util.List"); writer.addToImports("java.util.Map"); - writer.addToBody(" static Map> getCountryCodeToRegionCodeMap() {\n"); + writer.addToBody(" public static Map> getCountryCodeToRegionCodeMap() {\n"); writer.formatToBody(CAPACITY_COMMENT, capacity, countryCodeToRegionCodeMap.size()); writer.addToBody(" Map> countryCodeToRegionCodeMap =\n"); writer.addToBody(" new HashMap>(" + capacity + ");\n"); @@ -286,7 +286,7 @@ public class BuildMetadataProtoFromXml extends Command { writer.addToImports("java.util.HashSet"); writer.addToImports("java.util.Set"); - writer.addToBody(" static Set getRegionCodeSet() {\n"); + writer.addToBody(" public static Set getRegionCodeSet() {\n"); writer.formatToBody(CAPACITY_COMMENT, capacity, regionCodeList.size()); writer.addToBody(" Set regionCodeSet = new HashSet(" + capacity + ");\n"); writer.addToBody("\n"); @@ -307,7 +307,7 @@ public class BuildMetadataProtoFromXml extends Command { writer.addToImports("java.util.HashSet"); writer.addToImports("java.util.Set"); - writer.addToBody(" static Set getCountryCodeSet() {\n"); + writer.addToBody(" public static Set getCountryCodeSet() {\n"); writer.formatToBody(CAPACITY_COMMENT, capacity, countryCodeSet.size()); writer.addToBody(" Set countryCodeSet = new HashSet(" + capacity + ");\n"); writer.addToBody("\n"); diff --git a/tools/java/java-build/src/com/google/i18n/phonenumbers/buildtools/GeneratePhonePrefixDataEntryPoint.java b/tools/java/java-build/src/com/google/i18n/phonenumbers/buildtools/GeneratePhonePrefixDataEntryPoint.java index efabbca35..449f838c7 100644 --- a/tools/java/java-build/src/com/google/i18n/phonenumbers/buildtools/GeneratePhonePrefixDataEntryPoint.java +++ b/tools/java/java-build/src/com/google/i18n/phonenumbers/buildtools/GeneratePhonePrefixDataEntryPoint.java @@ -25,11 +25,13 @@ import java.util.logging.Logger; /** * Entry point class used to invoke the generation of the binary phone prefix data files. - * - * @author Philippe Liard */ public class GeneratePhonePrefixDataEntryPoint extends Command { + private static final Logger logger = Logger.getLogger(GeneratePhonePrefixData.class.getName()); + private static final String USAGE_DESCRIPTION = + "usage: GeneratePhonePrefixData /path/to/input/directory /path/to/output/directory" + + " [outputJarName]"; @Override public String getCommandName() { @@ -40,16 +42,20 @@ public class GeneratePhonePrefixDataEntryPoint extends Command { public boolean start() { String[] args = getArgs(); - if (args.length != 3) { - logger.log(Level.SEVERE, - "usage: GeneratePhonePrefixData /path/to/input/directory " - + "/path/to/output/directory"); + if (args.length < 3 || args.length > 4) { + logger.log(Level.SEVERE, USAGE_DESCRIPTION); return false; } try { - GeneratePhonePrefixData generatePhonePrefixData = - new GeneratePhonePrefixData(new File(args[1]), new PhonePrefixDataIOHandler(new File(args[2]))); - generatePhonePrefixData.run(); + File inputPath = new File(args[1]); + File outputPath = new File(args[2]); + AbstractPhonePrefixDataIOHandler ioHandler = + args.length == 3 + ? new PhonePrefixDataIOHandler(outputPath) + : new JarPhonePrefixDataIOHandler( + outputPath, args[3], GeneratePhonePrefixData.class.getPackage()); + GeneratePhonePrefixData dataGenerator = new GeneratePhonePrefixData(inputPath, ioHandler); + dataGenerator.run(); } catch (IOException e) { logger.log(Level.SEVERE, e.getMessage()); return false; diff --git a/tools/java/java-build/src/com/google/i18n/phonenumbers/buildtools/GenerateTimeZonesMapDataEntryPoint.java b/tools/java/java-build/src/com/google/i18n/phonenumbers/buildtools/GenerateTimeZonesMapDataEntryPoint.java index 77a7e5ea6..f64a4029d 100644 --- a/tools/java/java-build/src/com/google/i18n/phonenumbers/buildtools/GenerateTimeZonesMapDataEntryPoint.java +++ b/tools/java/java-build/src/com/google/i18n/phonenumbers/buildtools/GenerateTimeZonesMapDataEntryPoint.java @@ -31,6 +31,9 @@ import java.util.logging.Logger; */ public class GenerateTimeZonesMapDataEntryPoint extends Command { private static final Logger logger = Logger.getLogger(GenerateTimeZonesMapData.class.getName()); + private static final String USAGE_DESCRIPTION = + "usage: GenerateTimeZonesMapData /path/to/input/directory /path/to/output/directory" + + " [outputJarName]"; @Override public String getCommandName() { @@ -41,15 +44,19 @@ public class GenerateTimeZonesMapDataEntryPoint extends Command { public boolean start() { String[] args = getArgs(); - if (args.length != 3) { - logger.log(Level.SEVERE, - "usage: GenerateTimeZonesMapData /path/to/input/text_file " - + "/path/to/output/directory"); + if (args.length < 3 || args.length > 4) { + logger.log(Level.SEVERE, USAGE_DESCRIPTION); return false; } try { - GenerateTimeZonesMapData generateTimeZonesMapData = new GenerateTimeZonesMapData( - new File(args[1]), new PhonePrefixDataIOHandler(new File(args[2]))); + File inputPath = new File(args[1]); + File outputPath = new File(args[2]); + AbstractPhonePrefixDataIOHandler ioHandler = + args.length == 3 + ? new PhonePrefixDataIOHandler(outputPath) + : new JarPhonePrefixDataIOHandler( + outputPath, args[3], GeneratePhonePrefixData.class.getPackage()); + GenerateTimeZonesMapData generateTimeZonesMapData = new GenerateTimeZonesMapData(inputPath, ioHandler); generateTimeZonesMapData.run(); } catch (IOException e) { logger.log(Level.SEVERE, e.getMessage()); diff --git a/tools/java/java-build/src/com/google/i18n/phonenumbers/buildtools/JarPhonePrefixDataIOHandler.java b/tools/java/java-build/src/com/google/i18n/phonenumbers/buildtools/JarPhonePrefixDataIOHandler.java new file mode 100644 index 000000000..8a87a43c3 --- /dev/null +++ b/tools/java/java-build/src/com/google/i18n/phonenumbers/buildtools/JarPhonePrefixDataIOHandler.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2012 The Libphonenumber Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.i18n.phonenumbers.buildtools; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; + +/** + * Implementation of the AbstractPhonePrefixDataIOHandler required by the GeneratePhonePrefixData + * class used here to create the output files and add them to the resulting JAR. + */ +public class JarPhonePrefixDataIOHandler extends AbstractPhonePrefixDataIOHandler { + + // Base name of the output JAR files. It also forms part of the name of the package + // containing the generated binary data. + private final String jarBase; + // The path to the output directory. + private final File outputPath; + // The JAR output stream used by the JarPhonePrefixDataIOHandler. + private final JarOutputStream jarOutputStream; + // The package that will be used to create the JAR entry file. + private final Package outputPackage; + + public JarPhonePrefixDataIOHandler(File outputPath, String outputName, Package outputPackage) + throws IOException { + if (outputPath.exists()) { + if (!outputPath.isDirectory()) { + throw new IOException("Expected directory: " + outputPath.getAbsolutePath()); + } + } else { + if (!outputPath.mkdirs()) { + throw new IOException("Could not create directory " + outputPath.getAbsolutePath()); + } + } + this.outputPath = outputPath; + this.jarBase = outputName; + this.outputPackage = outputPackage; + jarOutputStream = createJar(); + } + + private JarOutputStream createJar() throws IOException { + Manifest manifest = new java.util.jar.Manifest(); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + return new JarOutputStream(new FileOutputStream(new File(outputPath, jarBase + ".jar"))); + } + + /** + * Adds the provided file to the created JAR. + */ + @Override + public void addFileToOutput(File file) throws IOException { + JarEntry entry = + new JarEntry( + outputPackage.getName().replace('.', '/') + + String.format("/%s/", jarBase) + + file.getPath()); + entry.setTime(file.lastModified()); + jarOutputStream.putNextEntry(entry); + BufferedInputStream bufferedInputStream = null; + + try { + bufferedInputStream = new BufferedInputStream(new FileInputStream(file)); + byte[] buffer = new byte[4096]; + + for (int read; (read = bufferedInputStream.read(buffer)) > 0; ) { + jarOutputStream.write(buffer, 0, read); + } + if (!file.delete()) { + throw new IOException("Could not delete: " + file.getAbsolutePath()); + } + } finally { + jarOutputStream.closeEntry(); + closeFile(bufferedInputStream); + } + } + + @Override + public File createFile(String path) { + return new File(path); + } + + @Override + public void close() { + closeFile(jarOutputStream); + } +} diff --git a/tools/java/java-build/src/com/google/i18n/phonenumbers/buildtools/PhonePrefixDataIOHandler.java b/tools/java/java-build/src/com/google/i18n/phonenumbers/buildtools/PhonePrefixDataIOHandler.java index 72d3a626b..0584a1519 100644 --- a/tools/java/java-build/src/com/google/i18n/phonenumbers/buildtools/PhonePrefixDataIOHandler.java +++ b/tools/java/java-build/src/com/google/i18n/phonenumbers/buildtools/PhonePrefixDataIOHandler.java @@ -20,10 +20,11 @@ import java.io.File; import java.io.IOException; /** - * Implementation of the IOHandler required by the GeneratePhonePrefixData class used here to create - * the output files. + * Implementation of the AbstractPhonePrefixDataIOHandler required by the GeneratePhonePrefixData + * class used here to create the output files. */ class PhonePrefixDataIOHandler extends AbstractPhonePrefixDataIOHandler { + // The path to the output directory. private final File outputPath; @@ -40,11 +41,14 @@ class PhonePrefixDataIOHandler extends AbstractPhonePrefixDataIOHandler { this.outputPath = outputPath; } + /** + * This is a no-op. + * + *

This would be the place dealing with the addition of the provided file to the resulting JAR + * if the global output was a JAR instead of a directory containing the binary files. + */ @Override - public void addFileToOutput(File file) throws IOException { - // Do nothing. This would be the place dealing with the addition of the provided file to the - // resulting JAR if the global output was a JAR instead of a directory containing the binary - // files. + public void addFileToOutput(File file) { } @Override @@ -52,8 +56,10 @@ class PhonePrefixDataIOHandler extends AbstractPhonePrefixDataIOHandler { return new File(outputPath, path); } + /** + * This is a no-op, as no resource needs to be released. + */ @Override public void close() { - // Do nothing as no resource needs to be released. } } diff --git a/tools/java/java-build/test/com/google/i18n/phonenumbers/buildtools/JarPhonePrefixDataIOHandlerTest.java b/tools/java/java-build/test/com/google/i18n/phonenumbers/buildtools/JarPhonePrefixDataIOHandlerTest.java new file mode 100644 index 000000000..bcb8cae1e --- /dev/null +++ b/tools/java/java-build/test/com/google/i18n/phonenumbers/buildtools/JarPhonePrefixDataIOHandlerTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2012 The Libphonenumber Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.i18n.phonenumbers.buildtools; + +import java.io.File; +import java.io.IOException; +import java.util.Enumeration; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.logging.Level; +import java.util.logging.Logger; +import junit.framework.TestCase; + +/** + * Unittests for JarPhonePrefixDataIOHandler.java + */ +public class JarPhonePrefixDataIOHandlerTest extends TestCase { + + private static final String TESTING_JAR_BASE = "testing_data"; + private static final Logger logger = + Logger.getLogger(JarPhonePrefixDataIOHandlerTest.class.getName()); + + public void testAddFileToOutput() { + File outputFile = null; + + try { + // Create the output jar. + File outputPath = new File("/tmp/build"); + Package outputPackage = JarPhonePrefixDataIOHandlerTest.class.getPackage(); + + JarPhonePrefixDataIOHandler ioHandler = + new JarPhonePrefixDataIOHandler(outputPath, TESTING_JAR_BASE, outputPackage); + outputFile = File.createTempFile("outputTestFile", "txt"); + ioHandler.addFileToOutput(outputFile); + ioHandler.close(); + + JarFile outputJar = new JarFile(new File(outputPath, TESTING_JAR_BASE + ".jar")); + // Test if there is exactly one entry in the jar. + Enumeration entries = outputJar.entries(); + int entriesCount = 0; + while (entries.hasMoreElements()) { + entriesCount++; + entries.nextElement(); + } + assertEquals(1, entriesCount); + + // Test if the entry file in the jar has the expected path. + String jarEntryPath = + "com/google/i18n/phonenumbers/buildtools/" + + TESTING_JAR_BASE + + "/" + + outputFile.getPath(); + JarEntry jarEntry = outputJar.getJarEntry(jarEntryPath); + assertNotNull("Output file not found inside the jar.", jarEntry); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getMessage()); + fail(); + } finally { + if (outputFile != null && outputFile.exists()) { + outputFile.delete(); + } + } + } +} \ No newline at end of file