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