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.geocoding;
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 string describing the geographical area the prefix
34 * covers.
35 *
36 * @author Shaopeng Jia
37 */
38public 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