Merge branch 'master' into refactoring-metadata-manager
diff --git a/java/carrier/src/com/google/i18n/phonenumbers/PhoneNumberToCarrierMapper.java b/java/carrier/src/com/google/i18n/phonenumbers/PhoneNumberToCarrierMapper.java
index b99abe6..ed3d6d8 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 @@
  */
 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 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 a48ed90..f26cc10 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.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 @@
  */
 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 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 8a4fde7..6033b67 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 @@
   private MappingFileProvider mappingFileProvider = new MappingFileProvider();
   // A mapping from countryCallingCode_lang to the corresponding phone prefix map that has been
   // loaded.
-  private Map<String, PhonePrefixMap> availablePhonePrefixMaps =
-      new HashMap<String, PhonePrefixMap>();
+  private Map<String, PhonePrefixMap> 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 @@
   }
 
   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 0000000..c831489
--- /dev/null
+++ b/java/lib/mockito-all-1.10.19.jar
Binary files differ
diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/CountryCodeToRegionCodeMap.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/CountryCodeToRegionCodeMap.java
index 159f940..06571d1 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 @@
   // 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<Integer, List<String>> getCountryCodeToRegionCodeMap() {
+  public static Map<Integer, List<String>> getCountryCodeToRegionCodeMap() {
     // The capacity is set to 286 as there are 215 different entries,
     // and this offers a load factor of roughly 0.75.
     Map<Integer, List<String>> 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 5c072d6..0000000
--- 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<Integer, PhoneMetadata> alternateFormatsMap =
-      new ConcurrentHashMap<Integer, PhoneMetadata>();
-
-  // A mapping from a region code to the short number metadata for that region code.
-  private static final ConcurrentHashMap<String, PhoneMetadata> shortNumberMetadataMap =
-      new ConcurrentHashMap<String, PhoneMetadata>();
-
-  // The set of country calling codes for which there are alternate formats. For every country
-  // calling code in this set there should be metadata linked into the resources.
-  private static final Set<Integer> alternateFormatsCountryCodes =
-      AlternateFormatsCountryCodeSet.getCountryCodeSet();
-
-  // The set of region codes for which there are short number metadata. For every region code in
-  // this set there should be metadata linked into the resources.
-  private static final Set<String> shortNumberMetadataRegionCodes =
-      ShortNumbersRegionCodeSet.getRegionCodeSet();
-
-  private MetadataManager() {}
-
-  static PhoneMetadata getAlternateFormatsForCountry(int countryCallingCode) {
-    if (!alternateFormatsCountryCodes.contains(countryCallingCode)) {
-      return null;
-    }
-    return 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<String> getSupportedShortNumberRegions() {
-    return Collections.unmodifiableSet(shortNumberMetadataRegionCodes);
-  }
-
-  /**
-   * @param key  the lookup key for the provided map, typically a region code or a country calling
-   *     code
-   * @param map  the map containing mappings of already loaded metadata from their {@code key}. If
-   *     this {@code key}'s metadata isn't already loaded, it will be added to this map after
-   *     loading
-   * @param filePrefix  the prefix of the file to load metadata from
-   * @param metadataLoader  the metadata loader used to inject alternative metadata sources
-   */
-  static <T> PhoneMetadata getMetadataFromMultiFilePrefix(T key,
-      ConcurrentHashMap<T, PhoneMetadata> map, String filePrefix, MetadataLoader metadataLoader) {
-    PhoneMetadata metadata = map.get(key);
-    if (metadata != null) {
-      return metadata;
-    }
-    // We assume key.toString() is well-defined.
-    String fileName = filePrefix + "_" + key;
-    List<PhoneMetadata> metadataList = 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<PhoneMetadata> metadataList = getMetadataFromSingleFileName(fileName, metadataLoader);
-      Map<String, PhoneMetadata> regionCodeToMetadata = new HashMap<String, PhoneMetadata>();
-      Map<Integer, PhoneMetadata> countryCallingCodeToMetadata =
-          new HashMap<Integer, PhoneMetadata>();
-      for (PhoneMetadata metadata : metadataList) {
-        String regionCode = metadata.getId();
-        if (PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY.equals(regionCode)) {
-          // regionCode belongs to a non-geographical entity.
-          countryCallingCodeToMetadata.put(metadata.getCountryCode(), metadata);
-        } else {
-          regionCodeToMetadata.put(regionCode, metadata);
-        }
-      }
-      return new SingleFileMetadataMaps(regionCodeToMetadata, countryCallingCodeToMetadata);
-    }
-
-    // A map from a region code to the PhoneMetadata for that region.
-    // For phone number metadata, the region code "001" is excluded, since that is used for the
-    // non-geographical phone number entities.
-    private final Map<String, PhoneMetadata> regionCodeToMetadata;
-
-    // A map from a country calling code to the PhoneMetadata for that country calling code.
-    // Examples of the country calling codes include 800 (International Toll Free Service) and 808
-    // (International Shared Cost Service).
-    // For phone number metadata, only the non-geographical phone number entities' country calling
-    // codes are present.
-    private final Map<Integer, PhoneMetadata> countryCallingCodeToMetadata;
-
-    private SingleFileMetadataMaps(Map<String, PhoneMetadata> regionCodeToMetadata,
-        Map<Integer, PhoneMetadata> countryCallingCodeToMetadata) {
-      this.regionCodeToMetadata = Collections.unmodifiableMap(regionCodeToMetadata);
-      this.countryCallingCodeToMetadata = Collections.unmodifiableMap(countryCallingCodeToMetadata);
-    }
-
-    PhoneMetadata get(String regionCode) {
-      return regionCodeToMetadata.get(regionCode);
-    }
-
-    PhoneMetadata get(int countryCallingCode) {
-      return countryCallingCodeToMetadata.get(countryCallingCode);
-    }
-  }
-
-  // Manages the atomic reference lifecycle of a SingleFileMetadataMaps encapsulation.
-  static SingleFileMetadataMaps getSingleFileMetadataMaps(
-      AtomicReference<SingleFileMetadataMaps> ref, String fileName, MetadataLoader metadataLoader) {
-    SingleFileMetadataMaps maps = ref.get();
-    if (maps != null) {
-      return maps;
-    }
-    maps = SingleFileMetadataMaps.load(fileName, metadataLoader);
-    ref.compareAndSet(null, maps);
-    return ref.get();
-  }
-
-  private static List<PhoneMetadata> 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<PhoneMetadata> metadataList = metadataCollection.getMetadataList();
-    if (metadataList.size() == 0) {
-      // Sanity check; this should not happen since we build with non-empty metadata.
-      throw new IllegalStateException("empty metadata: " + fileName);
-    }
-    return metadataList;
-  }
-
-  /**
-   * Loads and returns the metadata 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/MetadataSource.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/MetadataSource.java
deleted file mode 100644
index f5ffcad..0000000
--- a/java/libphonenumber/src/com/google/i18n/phonenumbers/MetadataSource.java
+++ /dev/null
@@ -1,39 +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;
-
-/**
- * A source for phone metadata for all regions.
- */
-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);
-}
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 0000000..c2a8544
--- /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 9a0b8e6..0000000
--- 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<String, PhoneMetadata> geographicalRegions =
-      new ConcurrentHashMap<String, PhoneMetadata>();
-
-  // 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<Integer, PhoneMetadata> nonGeographicalRegions =
-      new ConcurrentHashMap<Integer, PhoneMetadata>();
-
-  // 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<String> 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 c7bde8e..b812551 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.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 @@
     }
     // 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 646b134..677110d 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.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 @@
   private static final Map<Character, Character> ALL_PLUS_NUMBER_GROUPING_SYMBOLS;
 
   static {
-    HashMap<Integer, String> mobileTokenMap = new HashMap<Integer, String>();
+    HashMap<Integer, String> mobileTokenMap = new HashMap<>();
     mobileTokenMap.put(54, "9");
     MOBILE_TOKEN_MAPPINGS = Collections.unmodifiableMap(mobileTokenMap);
 
-    HashSet<Integer> geoMobileCountriesWithoutMobileAreaCodes = new HashSet<Integer>();
+    HashSet<Integer> geoMobileCountriesWithoutMobileAreaCodes = new HashSet<>();
     geoMobileCountriesWithoutMobileAreaCodes.add(86);  // China
     GEO_MOBILE_COUNTRIES_WITHOUT_MOBILE_AREA_CODES =
         Collections.unmodifiableSet(geoMobileCountriesWithoutMobileAreaCodes);
 
-    HashSet<Integer> geoMobileCountries = new HashSet<Integer>();
+    HashSet<Integer> geoMobileCountries = new HashSet<>();
     geoMobileCountries.add(52);  // Mexico
     geoMobileCountries.add(54);  // Argentina
     geoMobileCountries.add(55);  // Brazil
@@ -140,7 +141,7 @@
 
     // Simple ASCII digits map used to populate ALPHA_PHONE_MAPPINGS and
     // ALL_PLUS_NUMBER_GROUPING_SYMBOLS.
-    HashMap<Character, Character> asciiDigitMappings = new HashMap<Character, Character>();
+    HashMap<Character, Character> asciiDigitMappings = new HashMap<>();
     asciiDigitMappings.put('0', '0');
     asciiDigitMappings.put('1', '1');
     asciiDigitMappings.put('2', '2');
@@ -152,7 +153,7 @@
     asciiDigitMappings.put('8', '8');
     asciiDigitMappings.put('9', '9');
 
-    HashMap<Character, Character> alphaMap = new HashMap<Character, Character>(40);
+    HashMap<Character, Character> alphaMap = new HashMap<>(40);
     alphaMap.put('A', '2');
     alphaMap.put('B', '2');
     alphaMap.put('C', '2');
@@ -181,19 +182,19 @@
     alphaMap.put('Z', '9');
     ALPHA_MAPPINGS = Collections.unmodifiableMap(alphaMap);
 
-    HashMap<Character, Character> combinedMap = new HashMap<Character, Character>(100);
+    HashMap<Character, Character> combinedMap = new HashMap<>(100);
     combinedMap.putAll(ALPHA_MAPPINGS);
     combinedMap.putAll(asciiDigitMappings);
     ALPHA_PHONE_MAPPINGS = Collections.unmodifiableMap(combinedMap);
 
-    HashMap<Character, Character> diallableCharMap = new HashMap<Character, Character>();
+    HashMap<Character, Character> diallableCharMap = new HashMap<>();
     diallableCharMap.putAll(asciiDigitMappings);
     diallableCharMap.put(PLUS_SIGN, PLUS_SIGN);
     diallableCharMap.put('*', '*');
     diallableCharMap.put('#', '#');
     DIALLABLE_CHAR_MAPPINGS = Collections.unmodifiableMap(diallableCharMap);
 
-    HashMap<Character, Character> allPlusNumberGroupings = new HashMap<Character, Character>();
+    HashMap<Character, Character> 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 @@
   // 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 @@
   // 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<String> nanpaRegions = new HashSet<String>(35);
+  private final Set<String> 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 @@
   // 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<String> supportedRegions = new HashSet<String>(320);
+  private final Set<String> 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<Integer> countryCodesForNonGeographicalRegion = new HashSet<Integer>();
+  private final Set<Integer> countryCodesForNonGeographicalRegion = new HashSet<>();
 
   /**
    * This class implements a singleton, the constructor is only visible to facilitate testing.
@@ -1089,7 +1090,7 @@
    * be non-null.
    */
   private Set<PhoneNumberType> getSupportedTypesForMetadata(PhoneMetadata metadata) {
-    Set<PhoneNumberType> types = new TreeSet<PhoneNumberType>();
+    Set<PhoneNumberType> 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 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 @@
     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 @@
         NumberFormat.Builder numFormatCopy =  NumberFormat.newBuilder();
         numFormatCopy.mergeFrom(formatRule);
         numFormatCopy.clearNationalPrefixFormattingRule();
-        List<NumberFormat> numberFormats = new ArrayList<NumberFormat>(1);
+        List<NumberFormat> numberFormats = new ArrayList<>(1);
         numberFormats.add(numFormatCopy.build());
         formattedNumber = formatByPattern(number, PhoneNumberFormat.NATIONAL, numberFormats);
         break;
@@ -2275,21 +2282,42 @@
   }
 
   /**
-   * 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 @@
         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<Integer>(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 @@
           if (localLengths.isEmpty()) {
             localLengths = mobileDesc.getPossibleLengthLocalOnlyList();
           } else {
-            localLengths = new ArrayList<Integer>(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 3e7df59..7ce2972 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.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 @@
   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<String> REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT =
-      new HashSet<String>();
+  private static final Set<String> 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 @@
     TOLL_FREE,
     STANDARD_RATE,
     PREMIUM_RATE,
-    UNKNOWN_COST;
+    UNKNOWN_COST
   }
 
   /** Returns the singleton instance of the ShortNumberInfo. */
@@ -79,9 +82,13 @@
   // first.
   private final Map<Integer, List<String>> 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();
@@ -109,6 +116,21 @@
   }
 
   /**
+   * 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 @@
     if (!regionDialingFromMatchesNumber(number, regionDialingFrom)) {
       return false;
     }
-    PhoneMetadata phoneMetadata =
-        MetadataManager.getShortNumberMetadataForRegion(regionDialingFrom);
+    PhoneMetadata phoneMetadata = getShortNumberMetadataForRegion(regionDialingFrom);
     if (phoneMetadata == null) {
       return false;
     }
@@ -142,7 +163,7 @@
     List<String> 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 @@
     if (!regionDialingFromMatchesNumber(number, regionDialingFrom)) {
       return false;
     }
-    PhoneMetadata phoneMetadata =
-        MetadataManager.getShortNumberMetadataForRegion(regionDialingFrom);
+    PhoneMetadata phoneMetadata = getShortNumberMetadataForRegion(regionDialingFrom);
     if (phoneMetadata == null) {
       return false;
     }
@@ -228,8 +248,7 @@
       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 @@
     }
     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.
@@ -337,13 +356,6 @@
   }
 
   /**
-   * Convenience method to get a list of what regions the library has metadata for.
-   */
-  Set<String> getSupportedRegions() {
-    return MetadataManager.getSupportedShortNumberRegions();
-  }
-
-  /**
    * Gets a valid short number for the specified region.
    *
    * @param regionCode the region for which an example short number is needed
@@ -352,7 +364,7 @@
    */
   // @VisibleForTesting
   String getExampleShortNumber(String regionCode) {
-    PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionCode);
+    PhoneMetadata phoneMetadata = getShortNumberMetadataForRegion(regionCode);
     if (phoneMetadata == null) {
       return "";
     }
@@ -373,7 +385,7 @@
    */
   // @VisibleForTesting
   String getExampleShortNumberForCost(String regionCode, ShortNumberCost cost) {
-    PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionCode);
+    PhoneMetadata phoneMetadata = getShortNumberMetadataForRegion(regionCode);
     if (phoneMetadata == null) {
       return "";
     }
@@ -441,7 +453,7 @@
       // 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 @@
     List<String> 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 @@
       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 @@
     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 a988a54..0000000
--- 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<MetadataManager.SingleFileMetadataMaps> phoneNumberMetadataRef =
-      new AtomicReference<MetadataManager.SingleFileMetadataMaps>();
-
-  // 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 0000000..ef0cf67
--- /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.
+   *
+   * <p>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
+   *
+   * <p>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<String> 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 0000000..ab818ac
--- /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 0000000..76122b8
--- /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 0000000..e923dfa
--- /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<PhoneMetadata> 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> 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<PhoneMetadata> 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 0000000..a3ff682
--- /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 <T> needs to extend {@link MetadataContainer}
+ */
+final class BlockingMetadataBootstrappingGuard<T extends MetadataContainer>
+    implements MetadataBootstrappingGuard<T> {
+
+  private final MetadataLoader metadataLoader;
+  private final MetadataParser metadataParser;
+  private final T metadataContainer;
+  private final Set<String> 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> phoneMetadata = read(phoneMetadataFile);
+    for (PhoneMetadata metadata : phoneMetadata) {
+      metadataContainer.accept(metadata);
+    }
+    loadedFiles.add(phoneMetadataFile);
+  }
+
+  private Collection<PhoneMetadata> 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 0000000..7275749
--- /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<Integer> metadataByCountryCode =
+      MapBackedMetadataContainer.byCountryCallingCode();
+  private final MapBackedMetadataContainer<String> 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 0000000..f4f332c
--- /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.
+   *
+   * <p>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.
+   *
+   * <p>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 0000000..d6a8190
--- /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}
+ *
+ * <p>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<MapBackedMetadataContainer<Integer>> bootstrappingGuard;
+
+  public FormattingMetadataSourceImpl(
+      PhoneMetadataFileNameProvider phoneMetadataFileNameProvider,
+      MetadataBootstrappingGuard<MapBackedMetadataContainer<Integer>> 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 0000000..639280d
--- /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<T> implements MetadataContainer {
+
+  static MapBackedMetadataContainer<String> byRegionCode() {
+    return new MapBackedMetadataContainer<>(
+        new KeyProvider<String>() {
+          @Override
+          public String getKeyOf(PhoneMetadata phoneMetadata) {
+            return phoneMetadata.getId();
+          }
+        });
+  }
+
+  static MapBackedMetadataContainer<Integer> byCountryCallingCode() {
+    return new MapBackedMetadataContainer<>(
+        new KeyProvider<Integer>() {
+          @Override
+          public Integer getKeyOf(PhoneMetadata phoneMetadata) {
+            return phoneMetadata.getCountryCode();
+          }
+        });
+  }
+
+  private final ConcurrentMap<T, PhoneMetadata> metadataMap;
+
+  private final KeyProvider<T> keyProvider;
+
+  private MapBackedMetadataContainer(KeyProvider<T> keyProvider) {
+    this.metadataMap = new ConcurrentHashMap<>();
+    this.keyProvider = keyProvider;
+  }
+
+  PhoneMetadata getMetadataBy(T key) {
+    return key != null ? metadataMap.get(key) : null;
+  }
+
+  KeyProvider<T> getKeyProvider() {
+    return keyProvider;
+  }
+
+  @Override
+  public void accept(PhoneMetadata phoneMetadata) {
+    metadataMap.put(keyProvider.getKeyOf(phoneMetadata), phoneMetadata);
+  }
+
+  interface KeyProvider<T> {
+    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 0000000..9380c59
--- /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 <T> needs to extend {@link MetadataContainer}
+ */
+public interface MetadataBootstrappingGuard<T extends MetadataContainer> {
+
+  /**
+   * 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/metadata/source/MetadataContainer.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/MetadataContainer.java
new file mode 100644
index 0000000..3f6b21e
--- /dev/null
+++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/metadata/source/MetadataContainer.java
@@ -0,0 +1,32 @@
+/*
+ * 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 container for {@link PhoneMetadata}
+ */
+interface MetadataContainer {
+
+  /**
+   * 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.
+   */
+  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 0000000..d353ce9
--- /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 0000000..c3d1c73
--- /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}.
+ *
+ * <p>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<CompositeMetadataContainer> bootstrappingGuard;
+
+  public MetadataSourceImpl(
+      PhoneMetadataFileNameProvider phoneMetadataFileNameProvider,
+      MetadataBootstrappingGuard<CompositeMetadataContainer> 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 0000000..0d9adb5
--- /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 0000000..70db06d
--- /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.
+ *
+ * <p>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:
+ *
+ * <ul>
+ *   <li>800 - country code assigned to the Universal International Freephone Service
+ *   <li>808 - country code assigned to the International Shared Cost Service
+ *   <li>870 - country code assigned to the Pitcairn Islands
+ *   <li>...
+ * </ul>
+ */
+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 0000000..c3d1688
--- /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.
+ *
+ * <p>Two implementations are available:
+ *
+ * <ul>
+ *   <li>{@link SingleFileModeFileNameProvider} for single-file metadata.
+ *   <li>{@link MultiFileModeFileNameProvider} for multi-file metadata.
+ * </ul>
+ */
+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 0000000..3cf15c2
--- /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.
+   *
+   * <p>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 0000000..0078dd9
--- /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}
+ *
+ * <p>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<MapBackedMetadataContainer<String>>
+      bootstrappingGuard;
+
+  public RegionMetadataSourceImpl(
+      PhoneMetadataFileNameProvider phoneMetadataFileNameProvider,
+      MetadataBootstrappingGuard<MapBackedMetadataContainer<String>> 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 0000000..1d3d1eb
--- /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 ebea7b7..e98470b 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 @@
 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 @@
  */
 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<PhoneNumber> invalidCases = new ArrayList<PhoneNumber>();
-  private List<PhoneNumber> wrongTypeCases = new ArrayList<PhoneNumber>();
+  private final PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance();
+  private final ShortNumberInfo shortNumberInfo = ShortNumberInfo.getInstance();
+  private final RegionMetadataSource shortNumberMetadataSource =
+      DefaultMetadataDependenciesProvider.getInstance().getShortNumberMetadataSource();
+
+  private final List<PhoneNumber> invalidCases = new ArrayList<>();
+  private final List<PhoneNumber> wrongTypeCases = new ArrayList<>();
+  private final Set<String> shortNumberSupportedRegions = ShortNumbersRegionCodeSet.getRegionCodeSet();
 
   /**
    * @param exampleNumberRequestedType  type we are requesting an example number for
@@ -55,14 +59,14 @@
       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 void testFixedLine() throws Exception {
+  public void testFixedLine() {
     Set<PhoneNumberType> fixedLineTypes = EnumSet.of(PhoneNumberType.FIXED_LINE,
                                                      PhoneNumberType.FIXED_LINE_OR_MOBILE);
     checkNumbersValidAndCorrectType(PhoneNumberType.FIXED_LINE, fixedLineTypes);
@@ -82,7 +86,7 @@
     assertEquals(0, wrongTypeCases.size());
   }
 
-  public void testMobile() throws Exception {
+  public void testMobile() {
     Set<PhoneNumberType> mobileTypes = EnumSet.of(PhoneNumberType.MOBILE,
                                                   PhoneNumberType.FIXED_LINE_OR_MOBILE);
     checkNumbersValidAndCorrectType(PhoneNumberType.MOBILE, mobileTypes);
@@ -90,56 +94,56 @@
     assertEquals(0, wrongTypeCases.size());
   }
 
-  public void testTollFree() throws Exception {
+  public void testTollFree() {
     Set<PhoneNumberType> 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<PhoneNumberType> 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<PhoneNumberType> 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<PhoneNumberType> 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<PhoneNumberType> 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<PhoneNumberType> 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<PhoneNumberType> 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 @@
       }
       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 void testShortNumbersValidAndCorrectCost() throws Exception {
-    List<String> invalidStringCases = new ArrayList<String>();
-    for (String regionCode : shortNumberInfo.getSupportedRegions()) {
+    List<String> invalidStringCases = new ArrayList<>();
+    for (String regionCode : shortNumberSupportedRegions) {
       String exampleShortNumber = shortNumberInfo.getExampleShortNumber(regionCode);
       if (!shortNumberInfo.isValidShortNumberForRegion(
           phoneNumberUtil.parse(exampleShortNumber, regionCode), regionCode)) {
@@ -211,7 +215,7 @@
       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 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 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 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 91b2f39..0000000
--- 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<String, PhoneMetadata> map = new ConcurrentHashMap<String, PhoneMetadata>();
-    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<Integer, PhoneMetadata> map = new ConcurrentHashMap<Integer, PhoneMetadata>();
-    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<String, PhoneMetadata>(), "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<Integer, PhoneMetadata>(), "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 7c9b0fa..0000000
--- 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 641bd77..b133d7f 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.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 @@
   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 @@
     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 664fc52..0000000
--- 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.
- *
- * <p>
- * 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 51360d7..5dfb56e 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 @@
  * @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 0000000..300c5f5
--- /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 0000000..57fab19
--- /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 0000000..22c52e2
--- /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<PhoneMetadata> actual = metadataParser.parse(input);
+
+    assertEquals(1, actual.size());
+  }
+
+  public void test_parse_shouldReturnEmptyCollectionForNullInput() {
+    Collection<PhoneMetadata> 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 0000000..c291770
--- /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<MetadataContainer> 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<BootstrappingRunnable> 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<MetadataContainer> {
+
+    @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 0000000..75b66c4
--- /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 0000000..21fbea6
--- /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<String> 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<Integer> 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 0000000..c7ad7dd
--- /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 0000000..21d3bf7
--- /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 6e7cbd6..7f2c634 100644
--- a/java/pom.xml
+++ b/java/pom.xml
@@ -235,6 +235,12 @@
       <version>4.13.1</version>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-all</artifactId>
+      <version>1.10.19</version>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
 </project>
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 f7db4c8..8e4fc3b 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 @@
     writer.addToImports("java.util.List");
     writer.addToImports("java.util.Map");
 
-    writer.addToBody("  static Map<Integer, List<String>> getCountryCodeToRegionCodeMap() {\n");
+    writer.addToBody("  public static Map<Integer, List<String>> getCountryCodeToRegionCodeMap() {\n");
     writer.formatToBody(CAPACITY_COMMENT, capacity, countryCodeToRegionCodeMap.size());
     writer.addToBody("    Map<Integer, List<String>> countryCodeToRegionCodeMap =\n");
     writer.addToBody("        new HashMap<Integer, List<String>>(" + capacity + ");\n");
@@ -286,7 +286,7 @@
     writer.addToImports("java.util.HashSet");
     writer.addToImports("java.util.Set");
 
-    writer.addToBody("  static Set<String> getRegionCodeSet() {\n");
+    writer.addToBody("  public static Set<String> getRegionCodeSet() {\n");
     writer.formatToBody(CAPACITY_COMMENT, capacity, regionCodeList.size());
     writer.addToBody("    Set<String> regionCodeSet = new HashSet<String>(" + capacity + ");\n");
     writer.addToBody("\n");
@@ -307,7 +307,7 @@
     writer.addToImports("java.util.HashSet");
     writer.addToImports("java.util.Set");
 
-    writer.addToBody("  static Set<Integer> getCountryCodeSet() {\n");
+    writer.addToBody("  public static Set<Integer> getCountryCodeSet() {\n");
     writer.formatToBody(CAPACITY_COMMENT, capacity, countryCodeSet.size());
     writer.addToBody("    Set<Integer> countryCodeSet = new HashSet<Integer>(" + 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 efabbca..449f838 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 @@
 
 /**
  * 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 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 77a7e5e..f64a402 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 @@
  */
 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 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 0000000..8a87a43
--- /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 72d3a62..0584a15 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.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 @@
     this.outputPath = outputPath;
   }
 
+  /**
+   * This is a <b>no-op</b>.
+   *
+   * <p>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 @@
     return new File(outputPath, path);
   }
 
+  /**
+   * This is a <b>no-op</b>, 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 0000000..bcb8cae
--- /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<JarEntry> 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