Home | History | Annotate | Download | only in geocoding
      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.android.i18n.phonenumbers.geocoding;
     18 
     19 import com.android.i18n.phonenumbers.PhoneNumberUtil;
     20 import com.android.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 string describing the geographical area the prefix
     34  * covers.
     35  *
     36  * @author Shaopeng Jia
     37  */
     38 public class AreaCodeMap implements Externalizable {
     39   private final PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
     40   private static final Logger LOGGER = Logger.getLogger(AreaCodeMap.class.getName());
     41 
     42   private AreaCodeMapStorageStrategy areaCodeMapStorage;
     43 
     44   // @VisibleForTesting
     45   AreaCodeMapStorageStrategy getAreaCodeMapStorage() {
     46     return areaCodeMapStorage;
     47   }
     48 
     49   /**
     50    * Creates an empty {@link AreaCodeMap}. The default constructor is necessary for implementing
     51    * {@link Externalizable}. The empty map could later be populated by
     52    * {@link #readAreaCodeMap(java.util.SortedMap)} or {@link #readExternal(java.io.ObjectInput)}.
     53    */
     54   public AreaCodeMap() {}
     55 
     56   /**
     57    * Gets the size of the provided area code map storage. The map storage passed-in will be filled
     58    * as a result.
     59    */
     60   private static int getSizeOfAreaCodeMapStorage(AreaCodeMapStorageStrategy mapStorage,
     61       SortedMap<Integer, String> areaCodeMap) throws IOException {
     62     mapStorage.readFromSortedMap(areaCodeMap);
     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 AreaCodeMapStorageStrategy createDefaultMapStorage() {
     73     return new DefaultMapStorage();
     74   }
     75 
     76   private AreaCodeMapStorageStrategy createFlyweightMapStorage() {
     77     return new FlyweightMapStorage();
     78   }
     79 
     80   /**
     81    * Gets the smaller area code map storage strategy according to the provided area code map. It
     82    * 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   AreaCodeMapStorageStrategy getSmallerMapStorage(SortedMap<Integer, String> areaCodeMap) {
     87     try {
     88       AreaCodeMapStorageStrategy flyweightMapStorage = createFlyweightMapStorage();
     89       int sizeOfFlyweightMapStorage = getSizeOfAreaCodeMapStorage(flyweightMapStorage, areaCodeMap);
     90 
     91       AreaCodeMapStorageStrategy defaultMapStorage = createDefaultMapStorage();
     92       int sizeOfDefaultMapStorage = getSizeOfAreaCodeMapStorage(defaultMapStorage, areaCodeMap);
     93 
     94       return sizeOfFlyweightMapStorage < sizeOfDefaultMapStorage
     95           ? flyweightMapStorage : defaultMapStorage;
     96     } catch (IOException e) {
     97       LOGGER.severe(e.getMessage());
     98       return createFlyweightMapStorage();
     99     }
    100   }
    101 
    102   /**
    103    * Creates an {@link AreaCodeMap} initialized with {@code sortedAreaCodeMap}.  Note that the
    104    * underlying implementation of this method is expensive thus should not be called by
    105    * time-critical applications.
    106    *
    107    * @param sortedAreaCodeMap  a map from phone number prefixes to descriptions of corresponding
    108    *     geographical areas, sorted in ascending order of the phone number prefixes as integers.
    109    */
    110   public void readAreaCodeMap(SortedMap<Integer, String> sortedAreaCodeMap) {
    111     areaCodeMapStorage = getSmallerMapStorage(sortedAreaCodeMap);
    112   }
    113 
    114   /**
    115    * Supports Java Serialization.
    116    */
    117   public void readExternal(ObjectInput objectInput) throws IOException {
    118     // Read the area code map storage strategy flag.
    119     boolean useFlyweightMapStorage = objectInput.readBoolean();
    120     if (useFlyweightMapStorage) {
    121       areaCodeMapStorage = new FlyweightMapStorage();
    122     } else {
    123       areaCodeMapStorage = new DefaultMapStorage();
    124     }
    125     areaCodeMapStorage.readExternal(objectInput);
    126   }
    127 
    128   /**
    129    * Supports Java Serialization.
    130    */
    131   public void writeExternal(ObjectOutput objectOutput) throws IOException {
    132     objectOutput.writeBoolean(areaCodeMapStorage instanceof FlyweightMapStorage);
    133     areaCodeMapStorage.writeExternal(objectOutput);
    134   }
    135 
    136   /**
    137    * Returns the description of the geographical area the {@code number} corresponds to. This method
    138    * distinguishes the case of an invalid prefix and a prefix for which the name is not available in
    139    * the current language. If the description is not available in the current language an empty
    140    * string is returned. If no description was found for the provided number, null is returned.
    141    *
    142    * @param number  the phone number to look up
    143    * @return  the description of the geographical area
    144    */
    145   String lookup(PhoneNumber number) {
    146     int numOfEntries = areaCodeMapStorage.getNumOfEntries();
    147     if (numOfEntries == 0) {
    148       return null;
    149     }
    150     long phonePrefix =
    151         Long.parseLong(number.getCountryCode() + phoneUtil.getNationalSignificantNumber(number));
    152     int currentIndex = numOfEntries - 1;
    153     SortedSet<Integer> currentSetOfLengths = areaCodeMapStorage.getPossibleLengths();
    154     while (currentSetOfLengths.size() > 0) {
    155       Integer possibleLength = currentSetOfLengths.last();
    156       String phonePrefixStr = String.valueOf(phonePrefix);
    157       if (phonePrefixStr.length() > possibleLength) {
    158         phonePrefix = Long.parseLong(phonePrefixStr.substring(0, possibleLength));
    159       }
    160       currentIndex = binarySearch(0, currentIndex, phonePrefix);
    161       if (currentIndex < 0) {
    162         return null;
    163       }
    164       int currentPrefix = areaCodeMapStorage.getPrefix(currentIndex);
    165       if (phonePrefix == currentPrefix) {
    166         return areaCodeMapStorage.getDescription(currentIndex);
    167       }
    168       currentSetOfLengths = currentSetOfLengths.headSet(possibleLength);
    169     }
    170     return null;
    171   }
    172 
    173   /**
    174    * Does a binary search for {@code value} in the provided array from {@code start} to {@code end}
    175    * (inclusive). Returns the position if {@code value} is found; otherwise, returns the
    176    * position which has the largest value that is less than {@code value}. This means if
    177    * {@code value} is the smallest, -1 will be returned.
    178    */
    179   private int binarySearch(int start, int end, long value) {
    180     int current = 0;
    181     while (start <= end) {
    182       current = (start + end) >>> 1;
    183       int currentValue = areaCodeMapStorage.getPrefix(current);
    184       if (currentValue == value) {
    185         return current;
    186       } else if (currentValue > value) {
    187         current--;
    188         end = current;
    189       } else {
    190         start = current + 1;
    191       }
    192     }
    193     return current;
    194   }
    195 
    196   /**
    197    * Dumps the mappings contained in the area code map.
    198    */
    199   @Override
    200   public String toString() {
    201     return areaCodeMapStorage.toString();
    202   }
    203 }
    204