Browse Source

Refactoring to read from multiple file metadata source.

pull/759/head
Miruna Barbu 11 years ago
parent
commit
9d640d946b
6 changed files with 277 additions and 131 deletions
  1. +38
    -0
      java/libphonenumber/src/com/google/i18n/phonenumbers/MetadataSource.java
  2. +155
    -0
      java/libphonenumber/src/com/google/i18n/phonenumbers/MultiFileMetadataSourceImpl.java
  3. +31
    -109
      java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberUtil.java
  4. +50
    -0
      java/libphonenumber/test/com/google/i18n/phonenumbers/MultiFileMetadataSourceImplTest.java
  5. +0
    -20
      java/libphonenumber/test/com/google/i18n/phonenumbers/PhoneNumberUtilTest.java
  6. +3
    -2
      java/libphonenumber/test/com/google/i18n/phonenumbers/TestMetadataTestCase.java

+ 38
- 0
java/libphonenumber/src/com/google/i18n/phonenumbers/MetadataSource.java View File

@ -0,0 +1,38 @@
/*
* 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;
/**
* A source for phone metadata from resources.
*/
interface MetadataSource {
/**
* Gets phone metadata for a region.
* @param regionCode the region code.
* @return the phone metadata for that region, or null if there is none.
*/
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);
}

+ 155
- 0
java/libphonenumber/src/com/google/i18n/phonenumbers/MultiFileMetadataSourceImpl.java View File

@ -0,0 +1,155 @@
/*
* 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 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.logging.Level;
import java.util.logging.Logger;
/**
* Implementation of {@link MetadataSource} that reads from multiple resource files.
*/
final class MultiFileMetadataSourceImpl implements MetadataSource {
private static final Logger logger =
Logger.getLogger(MultiFileMetadataSourceImpl.class.getName());
private static final String META_DATA_FILE_PREFIX =
"/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto";
// A mapping from a region code to the PhoneMetadata for that region.
// Note: Synchronization, though only needed for the Android version of the library, is used in
// all versions for consistency.
private final Map<String, PhoneMetadata> regionToMetadataMap =
Collections.synchronizedMap(new HashMap<String, PhoneMetadata>());
// A mapping from a country calling code for a non-geographical entity 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).
// Note: Synchronization, though only needed for the Android version of the library, is used in
// all versions for consistency.
private final Map<Integer, PhoneMetadata> countryCodeToNonGeographicalMetadataMap =
Collections.synchronizedMap(new HashMap<Integer, PhoneMetadata>());
// The prefix of the metadata files from which region data is loaded.
private final String currentFilePrefix;
// The metadata loader used to inject alternative metadata sources.
private final MetadataLoader metadataLoader;
// It is assumed that metadataLoader is not null.
public MultiFileMetadataSourceImpl(String currentFilePrefix, MetadataLoader metadataLoader) {
this.currentFilePrefix = currentFilePrefix;
this.metadataLoader = metadataLoader;
}
// It is assumed that metadataLoader is not null.
public MultiFileMetadataSourceImpl(MetadataLoader metadataLoader) {
this(META_DATA_FILE_PREFIX, metadataLoader);
}
@Override
public PhoneMetadata getMetadataForRegion(String regionCode) {
synchronized (regionToMetadataMap) {
if (!regionToMetadataMap.containsKey(regionCode)) {
// The regionCode here will be valid and won't be '001', so we don't need to worry about
// what to pass in for the country calling code.
loadMetadataFromFile(currentFilePrefix, regionCode, 0, metadataLoader);
}
}
return regionToMetadataMap.get(regionCode);
}
@Override
public PhoneMetadata getMetadataForNonGeographicalRegion(int countryCallingCode) {
synchronized (countryCodeToNonGeographicalMetadataMap) {
if (!countryCodeToNonGeographicalMetadataMap.containsKey(countryCallingCode)) {
loadMetadataFromFile(currentFilePrefix, PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY,
countryCallingCode, metadataLoader);
}
}
return countryCodeToNonGeographicalMetadataMap.get(countryCallingCode);
}
// @VisibleForTesting
void loadMetadataFromFile(String filePrefix, String regionCode, int countryCallingCode,
MetadataLoader metadataLoader) {
boolean isNonGeoRegion = PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY.equals(regionCode);
String fileName = filePrefix + "_" +
(isNonGeoRegion ? String.valueOf(countryCallingCode) : regionCode);
InputStream source = metadataLoader.loadMetadata(fileName);
if (source == null) {
logger.log(Level.SEVERE, "missing metadata: " + fileName);
throw new IllegalStateException("missing metadata: " + fileName);
}
ObjectInputStream in = null;
try {
in = new ObjectInputStream(source);
PhoneMetadataCollection metadataCollection = loadMetadataAndCloseInput(in);
List<PhoneMetadata> metadataList = metadataCollection.getMetadataList();
if (metadataList.isEmpty()) {
logger.log(Level.SEVERE, "empty metadata: " + fileName);
throw new IllegalStateException("empty metadata: " + fileName);
}
if (metadataList.size() > 1) {
logger.log(Level.WARNING, "invalid metadata (too many entries): " + fileName);
}
PhoneMetadata metadata = metadataList.get(0);
if (isNonGeoRegion) {
countryCodeToNonGeographicalMetadataMap.put(countryCallingCode, metadata);
} else {
regionToMetadataMap.put(regionCode, metadata);
}
} catch (IOException e) {
logger.log(Level.SEVERE, "cannot load/parse metadata: " + fileName, e);
throw new RuntimeException("cannot load/parse metadata: " + fileName, e);
}
}
/**
* Loads the metadata protocol buffer from the given stream and closes the stream afterwards. Any
* exceptions that occur while reading the stream are propagated (though exceptions that occur
* when the stream is closed will be ignored).
*
* @param source the non-null stream from which metadata is to be read.
* @return the loaded metadata protocol buffer.
*/
private static PhoneMetadataCollection loadMetadataAndCloseInput(ObjectInputStream source) {
PhoneMetadataCollection metadataCollection = new PhoneMetadataCollection();
try {
metadataCollection.readExternal(source);
} catch (IOException e) {
logger.log(Level.WARNING, "error reading input (ignored)", e);
} finally {
try {
source.close();
} catch (IOException e) {
logger.log(Level.WARNING, "error closing input stream (ignored)", e);
}
}
return metadataCollection;
}
}

+ 31
- 109
java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberUtil.java View File

@ -18,15 +18,11 @@ package com.google.i18n.phonenumbers;
import com.google.i18n.phonenumbers.Phonemetadata.NumberFormat;
import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata;
import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadataCollection;
import com.google.i18n.phonenumbers.Phonemetadata.PhoneNumberDesc;
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber.CountryCodeSource;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -78,9 +74,6 @@ public class PhoneNumberUtil {
// input from overflowing the regular-expression engine.
private static final int MAX_INPUT_STRING_LENGTH = 250;
private static final String META_DATA_FILE_PREFIX =
"/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto";
// Region-code for the unknown region.
private static final String UNKNOWN_REGION = "ZZ";
@ -535,6 +528,9 @@ public class PhoneNumberUtil {
abstract boolean verify(PhoneNumber number, String candidate, PhoneNumberUtil util);
}
// A source of metadata for different regions.
private final MetadataSource metadataSource;
// A mapping from a country calling code to the region codes which denote the region represented
// by that country calling code. In the case of multiple regions sharing a calling code, such as
// the NANPA regions, the one indicated with "isMainCountryForCode" in the metadata should be
@ -546,20 +542,6 @@ public class PhoneNumberUtil {
// We set the initial capacity of the HashSet to 35 to offer a load factor of roughly 0.75.
private final Set<String> nanpaRegions = new HashSet<String>(35);
// A mapping from a region code to the PhoneMetadata for that region.
// Note: Synchronization, though only needed for the Android version of the library, is used in
// all versions for consistency.
private final Map<String, PhoneMetadata> regionToMetadataMap =
Collections.synchronizedMap(new HashMap<String, PhoneMetadata>());
// A mapping from a country calling code for a non-geographical entity 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).
// Note: Synchronization, though only needed for the Android version of the library, is used in
// all versions for consistency.
private final Map<Integer, PhoneMetadata> countryCodeToNonGeographicalMetadataMap =
Collections.synchronizedMap(new HashMap<Integer, PhoneMetadata>());
// 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
// performance measurements.
@ -574,19 +556,13 @@ public class PhoneNumberUtil {
// currently contains < 12 elements so the default capacity of 16 (load factor=0.75) is fine.
private final Set<Integer> countryCodesForNonGeographicalRegion = new HashSet<Integer>();
// The prefix of the metadata files from which region data is loaded.
private final String currentFilePrefix;
// The metadata loader used to inject alternative metadata sources.
private final MetadataLoader metadataLoader;
/**
* This class implements a singleton, the constructor is only visible to facilitate testing.
*/
// @VisibleForTesting
PhoneNumberUtil(String filePrefix, MetadataLoader metadataLoader,
PhoneNumberUtil(MetadataSource metadataSource,
Map<Integer, List<String>> countryCallingCodeToRegionCodeMap) {
this.currentFilePrefix = filePrefix;
this.metadataLoader = metadataLoader;
this.metadataSource = metadataSource;
this.countryCallingCodeToRegionCodeMap = countryCallingCodeToRegionCodeMap;
for (Map.Entry<Integer, List<String>> entry : countryCallingCodeToRegionCodeMap.entrySet()) {
List<String> regionCodes = entry.getValue();
@ -610,65 +586,6 @@ public class PhoneNumberUtil {
nanpaRegions.addAll(countryCallingCodeToRegionCodeMap.get(NANPA_COUNTRY_CODE));
}
// @VisibleForTesting
void loadMetadataFromFile(String filePrefix, String regionCode, int countryCallingCode,
MetadataLoader metadataLoader) {
boolean isNonGeoRegion = REGION_CODE_FOR_NON_GEO_ENTITY.equals(regionCode);
String fileName = filePrefix + "_" +
(isNonGeoRegion ? String.valueOf(countryCallingCode) : regionCode);
InputStream source = metadataLoader.loadMetadata(fileName);
if (source == null) {
logger.log(Level.SEVERE, "missing metadata: " + fileName);
throw new IllegalStateException("missing metadata: " + fileName);
}
ObjectInputStream in = null;
try {
in = new ObjectInputStream(source);
PhoneMetadataCollection metadataCollection = loadMetadataAndCloseInput(in);
List<PhoneMetadata> metadataList = metadataCollection.getMetadataList();
if (metadataList.isEmpty()) {
logger.log(Level.SEVERE, "empty metadata: " + fileName);
throw new IllegalStateException("empty metadata: " + fileName);
}
if (metadataList.size() > 1) {
logger.log(Level.WARNING, "invalid metadata (too many entries): " + fileName);
}
PhoneMetadata metadata = metadataList.get(0);
if (isNonGeoRegion) {
countryCodeToNonGeographicalMetadataMap.put(countryCallingCode, metadata);
} else {
regionToMetadataMap.put(regionCode, metadata);
}
} catch (IOException e) {
logger.log(Level.SEVERE, "cannot load/parse metadata: " + fileName, e);
throw new RuntimeException("cannot load/parse metadata: " + fileName, e);
}
}
/**
* Loads the metadata protocol buffer from the given stream and closes the stream afterwards. Any
* exceptions that occur while reading the stream are propagated (though exceptions that occur
* when the stream is closed will be ignored).
*
* @param source the non-null stream from which metadata is to be read.
* @return the loaded metadata protocol buffer.
*/
private static PhoneMetadataCollection loadMetadataAndCloseInput(ObjectInputStream source) {
PhoneMetadataCollection metadataCollection = new PhoneMetadataCollection();
try {
metadataCollection.readExternal(source);
} catch (IOException e) {
logger.log(Level.WARNING, "error reading input (ignored)", e);
} finally {
try {
source.close();
} catch (IOException e) {
logger.log(Level.WARNING, "error closing input stream (ignored)", e);
}
}
return metadataCollection;
}
/**
* Attempts to extract a possible number from the string passed in. This currently strips all
* leading characters that cannot be used to start a phone number. Characters that can be used to
@ -1019,6 +936,26 @@ public class PhoneNumberUtil {
return instance;
}
/**
* Create a new {@link PhoneNumberUtil} instance to carry out international phone number
* formatting, parsing, or validation. The instance is loaded with all metadata by
* using the metadataSource specified.
*
* This method should only be used in the rare case in which you want to manage your own
* metadata loading. Calling this method multiple times is very expensive, as each time
* a new instance is created from scratch. When in doubt, use {@link #getInstance}.
*
* @param metadataSource Customized metadata source. This should not be null.
* @return a PhoneNumberUtil instance
*/
public static PhoneNumberUtil createInstance(MetadataSource metadataSource) {
if (metadataSource == null) {
throw new IllegalArgumentException("metadataSource could not be null.");
}
return new PhoneNumberUtil(metadataSource,
CountryCodeToRegionCodeMap.getCountryCodeToRegionCodeMap());
}
/**
* Create a new {@link PhoneNumberUtil} instance to carry out international phone number
* formatting, parsing, or validation. The instance is loaded with all metadata by
@ -1028,16 +965,14 @@ public class PhoneNumberUtil {
* metadata loading. Calling this method multiple times is very expensive, as each time
* a new instance is created from scratch. When in doubt, use {@link #getInstance}.
*
* @param metadataLoader Customized metadata loader. If null, default metadata loader will
* be used. This should not be null.
* @param metadataLoader Customized metadata loader. This should not be null.
* @return a PhoneNumberUtil instance
*/
public static PhoneNumberUtil createInstance(MetadataLoader metadataLoader) {
if (metadataLoader == null) {
throw new IllegalArgumentException("metadataLoader could not be null.");
}
return new PhoneNumberUtil(META_DATA_FILE_PREFIX, metadataLoader,
CountryCodeToRegionCodeMap.getCountryCodeToRegionCodeMap());
return createInstance(new MultiFileMetadataSourceImpl(metadataLoader));
}
/**
@ -2038,27 +1973,14 @@ public class PhoneNumberUtil {
if (!isValidRegionCode(regionCode)) {
return null;
}
synchronized (regionToMetadataMap) {
if (!regionToMetadataMap.containsKey(regionCode)) {
// The regionCode here will be valid and won't be '001', so we don't need to worry about
// what to pass in for the country calling code.
loadMetadataFromFile(currentFilePrefix, regionCode, 0, metadataLoader);
}
}
return regionToMetadataMap.get(regionCode);
return metadataSource.getMetadataForRegion(regionCode);
}
PhoneMetadata getMetadataForNonGeographicalRegion(int countryCallingCode) {
synchronized (countryCodeToNonGeographicalMetadataMap) {
if (!countryCallingCodeToRegionCodeMap.containsKey(countryCallingCode)) {
return null;
}
if (!countryCodeToNonGeographicalMetadataMap.containsKey(countryCallingCode)) {
loadMetadataFromFile(
currentFilePrefix, REGION_CODE_FOR_NON_GEO_ENTITY, countryCallingCode, metadataLoader);
}
if (!countryCallingCodeToRegionCodeMap.containsKey(countryCallingCode)) {
return null;
}
return countryCodeToNonGeographicalMetadataMap.get(countryCallingCode);
return metadataSource.getMetadataForNonGeographicalRegion(countryCallingCode);
}
boolean isNumberPossibleForDesc(String nationalNumber, PhoneNumberDesc numberDesc) {


+ 50
- 0
java/libphonenumber/test/com/google/i18n/phonenumbers/MultiFileMetadataSourceImplTest.java View File

@ -0,0 +1,50 @@
/*
* 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;
/**
* Unit tests for MultiFileMetadataSourceImpl.java.
*/
public class MultiFileMetadataSourceImplTest extends TestMetadataTestCase {
private final MultiFileMetadataSourceImpl multiFileMetadataSource;
public MultiFileMetadataSourceImplTest() {
multiFileMetadataSource = new MultiFileMetadataSourceImpl(TEST_META_DATA_FILE_PREFIX,
PhoneNumberUtil.DEFAULT_METADATA_LOADER);
}
public void testMissingMetadataFileThrowsRuntimeException() {
// 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 {
multiFileMetadataSource.loadMetadataFromFile(
"no/such/file", "XX", -1, PhoneNumberUtil.DEFAULT_METADATA_LOADER);
fail("expected exception");
} catch (RuntimeException e) {
assertTrue("Unexpected error: " + e, e.toString().contains("no/such/file_XX"));
}
try {
multiFileMetadataSource.loadMetadataFromFile("no/such/file", PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY,
123, PhoneNumberUtil.DEFAULT_METADATA_LOADER);
fail("expected exception");
} catch (RuntimeException e) {
assertTrue("Unexpected error: " + e, e.getMessage().contains("no/such/file_123"));
}
}
}

+ 0
- 20
java/libphonenumber/test/com/google/i18n/phonenumbers/PhoneNumberUtilTest.java View File

@ -132,26 +132,6 @@ public class PhoneNumberUtilTest extends TestMetadataTestCase {
assertNull(phoneUtil.getMetadataForNonGeographicalRegion(-1));
}
public void testMissingMetadataFileThrowsRuntimeException() {
// 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 {
phoneUtil.loadMetadataFromFile(
"no/such/file", "XX", -1, PhoneNumberUtil.DEFAULT_METADATA_LOADER);
fail("expected exception");
} catch (RuntimeException e) {
assertTrue("Unexpected error: " + e, e.toString().contains("no/such/file_XX"));
}
try {
phoneUtil.loadMetadataFromFile("no/such/file", PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY,
123, PhoneNumberUtil.DEFAULT_METADATA_LOADER);
fail("expected exception");
} catch (RuntimeException e) {
assertTrue("Unexpected error: " + e, e.getMessage().contains("no/such/file_123"));
}
}
public void testGetInstanceLoadUSMetadata() {
PhoneMetadata metadata = phoneUtil.getMetadataForRegion(RegionCode.US);
assertEquals("US", metadata.getId());


+ 3
- 2
java/libphonenumber/test/com/google/i18n/phonenumbers/TestMetadataTestCase.java View File

@ -27,7 +27,7 @@ import junit.framework.TestCase;
* @author Shaopeng Jia
*/
public class TestMetadataTestCase extends TestCase {
private static final String TEST_META_DATA_FILE_PREFIX =
protected static final String TEST_META_DATA_FILE_PREFIX =
"/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProtoForTesting";
protected final PhoneNumberUtil phoneUtil;
@ -38,7 +38,8 @@ public class TestMetadataTestCase extends TestCase {
static PhoneNumberUtil initializePhoneUtilForTesting() {
PhoneNumberUtil phoneUtil = new PhoneNumberUtil(
TEST_META_DATA_FILE_PREFIX, PhoneNumberUtil.DEFAULT_METADATA_LOADER,
new MultiFileMetadataSourceImpl(TEST_META_DATA_FILE_PREFIX,
PhoneNumberUtil.DEFAULT_METADATA_LOADER),
CountryCodeToRegionCodeMapForTesting.getCountryCodeToRegionCodeMap());
PhoneNumberUtil.setInstance(phoneUtil);
return phoneUtil;


Loading…
Cancel
Save