Home | History | Annotate | Download | only in prefixmapper
      1 /*
      2  * Copyright (C) 2011 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.prefixmapper;
     18 
     19 import com.google.i18n.phonenumbers.PhoneNumberUtil;
     20 import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
     21 
     22 import java.io.ByteArrayOutputStream;
     23 import java.io.Externalizable;
     24 import java.io.IOException;
     25 import java.io.ObjectInput;
     26 import java.io.ObjectOutput;
     27 import java.io.ObjectOutputStream;
     28 import java.util.SortedMap;
     29 import java.util.SortedSet;
     30 import java.util.logging.Logger;
     31 
     32 /**
     33  * A utility that maps phone number prefixes to a description string, which may be, for example,
     34  * the geographical area the prefix covers.
     35  *
     36  * @author Shaopeng Jia
     37  */
     38 public class PhonePrefixMap implements Externalizable {
     39   private final PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
     40   private static final Logger logger = Logger.getLogger(PhonePrefixMap.class.getName());
     41 
     42   private PhonePrefixMapStorageStrategy phonePrefixMapStorage;
     43 
     44   // @VisibleForTesting
     45   PhonePrefixMapStorageStrategy getPhonePrefixMapStorage() {
     46     return phonePrefixMapStorage;
     47   }
     48 
     49   /**
     50    * Creates an empty {@link PhonePrefixMap}. The default constructor is necessary for implementing
     51    * {@link Externalizable}. The empty map could later be populated by
     52    * {@link #readPhonePrefixMap(java.util.SortedMap)} or {@link #readExternal(java.io.ObjectInput)}.
     53    */
     54   public PhonePrefixMap() {}
     55 
     56   /**
     57    * Gets the size of the provided phone prefix map storage. The map storage passed-in will be
     58    * filled as a result.
     59    */
     60   private static int getSizeOfPhonePrefixMapStorage(PhonePrefixMapStorageStrategy mapStorage,
     61       SortedMap<Integer, String> phonePrefixMap) throws IOException {
     62     mapStorage.readFromSortedMap(phonePrefixMap);
     63     ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
     64     ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
     65     mapStorage.writeExternal(objectOutputStream);
     66     objectOutputStream.flush();
     67     int sizeOfStorage = byteArrayOutputStream.size();
     68     objectOutputStream.close();
     69     return sizeOfStorage;
     70   }
     71 
     72   private PhonePrefixMapStorageStrategy createDefaultMapStorage() {
     73     return new DefaultMapStorage();
     74   }
     75 
     76   private PhonePrefixMapStorageStrategy createFlyweightMapStorage() {
     77     return new FlyweightMapStorage();
     78   }
     79 
     80   /**
     81    * Gets the smaller phone prefix map storage strategy according to the provided phone prefix map.
     82    * It actually uses (outputs the data to a stream) both strategies and retains the best one which
     83    * make this method quite expensive.
     84    */
     85   // @VisibleForTesting
     86   PhonePrefixMapStorageStrategy getSmallerMapStorage(SortedMap<Integer, String> phonePrefixMap) {
     87     try {
     88       PhonePrefixMapStorageStrategy flyweightMapStorage = createFlyweightMapStorage();
     89       int sizeOfFlyweightMapStorage = getSizeOfPhonePrefixMapStorage(flyweightMapStorage,
     90                                                                      phonePrefixMap);
     91 
     92       PhonePrefixMapStorageStrategy defaultMapStorage = createDefaultMapStorage();
     93       int sizeOfDefaultMapStorage = getSizeOfPhonePrefixMapStorage(defaultMapStorage,
     94                                                                    phonePrefixMap);
     95 
     96       return sizeOfFlyweightMapStorage < sizeOfDefaultMapStorage
     97           ? flyweightMapStorage : defaultMapStorage;
     98     } catch (IOException e) {
     99       logger.severe(e.getMessage());
    100       return createFlyweightMapStorage();
    101     }
    102   }
    103 
    104   /**
    105    * Creates an {@link PhonePrefixMap} initialized with {@code sortedPhonePrefixMap}.  Note that the
    106    * underlying implementation of this method is expensive thus should not be called by
    107    * time-critical applications.
    108    *
    109    * @param sortedPhonePrefixMap  a map from phone number prefixes to descriptions of those prefixes
    110    * sorted in ascending order of the phone number prefixes as integers.
    111    */
    112   public void readPhonePrefixMap(SortedMap<Integer, String> sortedPhonePrefixMap) {
    113     phonePrefixMapStorage = getSmallerMapStorage(sortedPhonePrefixMap);
    114   }
    115 
    116   /**
    117    * Supports Java Serialization.
    118    */
    119   public void readExternal(ObjectInput objectInput) throws IOException {
    120     // Read the phone prefix map storage strategy flag.
    121     boolean useFlyweightMapStorage = objectInput.readBoolean();
    122     if (useFlyweightMapStorage) {
    123       phonePrefixMapStorage = new FlyweightMapStorage();
    124     } else {
    125       phonePrefixMapStorage = new DefaultMapStorage();
    126     }
    127     phonePrefixMapStorage.readExternal(objectInput);
    128   }
    129 
    130   /**
    131    * Supports Java Serialization.
    132    */
    133   public void writeExternal(ObjectOutput objectOutput) throws IOException {
    134     objectOutput.writeBoolean(phonePrefixMapStorage instanceof FlyweightMapStorage);
    135     phonePrefixMapStorage.writeExternal(objectOutput);
    136   }
    137 
    138   /**
    139    * Returns the description of the {@code number}. This method distinguishes the case of an invalid
    140    * prefix and a prefix for which the name is not available in the current language. If the
    141    * description is not available in the current language an empty string is returned. If no
    142    * description was found for the provided number, null is returned.
    143    *
    144    * @param number  the phone number to look up
    145    * @return  the description of the number
    146    */
    147   String lookup(long number) {
    148     int numOfEntries = phonePrefixMapStorage.getNumOfEntries();
    149     if (numOfEntries == 0) {
    150       return null;
    151     }
    152     long phonePrefix = number;
    153     int currentIndex = numOfEntries - 1;
    154     SortedSet<Integer> currentSetOfLengths = phonePrefixMapStorage.getPossibleLengths();
    155     while (currentSetOfLengths.size() > 0) {
    156       Integer possibleLength = currentSetOfLengths.last();
    157       String phonePrefixStr = String.valueOf(phonePrefix);
    158       if (phonePrefixStr.length() > possibleLength) {
    159         phonePrefix = Long.parseLong(phonePrefixStr.substring(0, possibleLength));
    160       }
    161       currentIndex = binarySearch(0, currentIndex, phonePrefix);
    162       if (currentIndex < 0) {
    163         return null;
    164       }
    165       int currentPrefix = phonePrefixMapStorage.getPrefix(currentIndex);
    166       if (phonePrefix == currentPrefix) {
    167         return phonePrefixMapStorage.getDescription(currentIndex);
    168       }
    169       currentSetOfLengths = currentSetOfLengths.headSet(possibleLength);
    170     }
    171     return null;
    172   }
    173 
    174   /**
    175    * As per {@link #lookup(long)}, but receives the number as a PhoneNumber instead of a long.
    176    *
    177    * @param number  the phone number to look up
    178    * @return  the description corresponding to the prefix that best matches this phone number
    179    */
    180   public String lookup(PhoneNumber number) {
    181     long phonePrefix =
    182         Long.parseLong(number.getCountryCode() + phoneUtil.getNationalSignificantNumber(number));
    183     return lookup(phonePrefix);
    184   }
    185 
    186   /**
    187    * Does a binary search for {@code value} in the provided array from {@code start} to {@code end}
    188    * (inclusive). Returns the position if {@code value} is found; otherwise, returns the
    189    * position which has the largest value that is less than {@code value}. This means if
    190    * {@code value} is the smallest, -1 will be returned.
    191    */
    192   private int binarySearch(int start, int end, long value) {
    193     int current = 0;
    194     while (start <= end) {
    195       current = (start + end) >>> 1;
    196       int currentValue = phonePrefixMapStorage.getPrefix(current);
    197       if (currentValue == value) {
    198         return current;
    199       } else if (currentValue > value) {
    200         current--;
    201         end = current;
    202       } else {
    203         start = current + 1;
    204       }
    205     }
    206     return current;
    207   }
    208 
    209   /**
    210    * Dumps the mappings contained in the phone prefix map.
    211    */
    212   @Override
    213   public String toString() {
    214     return phonePrefixMapStorage.toString();
    215   }
    216 }
    217