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
17package com.android.i18n.phonenumbers.prefixmapper;
18
19import com.android.i18n.phonenumbers.PhoneNumberUtil;
20import com.android.i18n.phonenumbers.Phonenumber.PhoneNumber;
21
22import java.io.ByteArrayOutputStream;
23import java.io.Externalizable;
24import java.io.IOException;
25import java.io.ObjectInput;
26import java.io.ObjectOutput;
27import java.io.ObjectOutputStream;
28import java.util.SortedMap;
29import java.util.SortedSet;
30import 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 */
38public 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