/* * Copyright (C) 2011 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.i18n.phonenumbers.geocoding; import com.android.i18n.phonenumbers.PhoneNumberUtil; import com.android.i18n.phonenumbers.Phonenumber.PhoneNumber; import java.io.ByteArrayOutputStream; import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; import java.io.ObjectOutputStream; import java.util.SortedMap; import java.util.SortedSet; import java.util.logging.Logger; /** * A utility that maps phone number prefixes to a string describing the geographical area the prefix * covers. * * @author Shaopeng Jia */ public class AreaCodeMap implements Externalizable { private final PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance(); private static final Logger LOGGER = Logger.getLogger(AreaCodeMap.class.getName()); private AreaCodeMapStorageStrategy areaCodeMapStorage; // @VisibleForTesting AreaCodeMapStorageStrategy getAreaCodeMapStorage() { return areaCodeMapStorage; } /** * Creates an empty {@link AreaCodeMap}. The default constructor is necessary for implementing * {@link Externalizable}. The empty map could later be populated by * {@link #readAreaCodeMap(java.util.SortedMap)} or {@link #readExternal(java.io.ObjectInput)}. */ public AreaCodeMap() {} /** * Gets the size of the provided area code map storage. The map storage passed-in will be filled * as a result. */ private static int getSizeOfAreaCodeMapStorage(AreaCodeMapStorageStrategy mapStorage, SortedMap<Integer, String> areaCodeMap) throws IOException { mapStorage.readFromSortedMap(areaCodeMap); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); mapStorage.writeExternal(objectOutputStream); objectOutputStream.flush(); int sizeOfStorage = byteArrayOutputStream.size(); objectOutputStream.close(); return sizeOfStorage; } private AreaCodeMapStorageStrategy createDefaultMapStorage() { return new DefaultMapStorage(); } private AreaCodeMapStorageStrategy createFlyweightMapStorage() { return new FlyweightMapStorage(); } /** * Gets the smaller area code map storage strategy according to the provided area code map. It * actually uses (outputs the data to a stream) both strategies and retains the best one which * make this method quite expensive. */ // @VisibleForTesting AreaCodeMapStorageStrategy getSmallerMapStorage(SortedMap<Integer, String> areaCodeMap) { try { AreaCodeMapStorageStrategy flyweightMapStorage = createFlyweightMapStorage(); int sizeOfFlyweightMapStorage = getSizeOfAreaCodeMapStorage(flyweightMapStorage, areaCodeMap); AreaCodeMapStorageStrategy defaultMapStorage = createDefaultMapStorage(); int sizeOfDefaultMapStorage = getSizeOfAreaCodeMapStorage(defaultMapStorage, areaCodeMap); return sizeOfFlyweightMapStorage < sizeOfDefaultMapStorage ? flyweightMapStorage : defaultMapStorage; } catch (IOException e) { LOGGER.severe(e.getMessage()); return createFlyweightMapStorage(); } } /** * Creates an {@link AreaCodeMap} initialized with {@code sortedAreaCodeMap}. Note that the * underlying implementation of this method is expensive thus should not be called by * time-critical applications. * * @param sortedAreaCodeMap a map from phone number prefixes to descriptions of corresponding * geographical areas, sorted in ascending order of the phone number prefixes as integers. */ public void readAreaCodeMap(SortedMap<Integer, String> sortedAreaCodeMap) { areaCodeMapStorage = getSmallerMapStorage(sortedAreaCodeMap); } /** * Supports Java Serialization. */ public void readExternal(ObjectInput objectInput) throws IOException { // Read the area code map storage strategy flag. boolean useFlyweightMapStorage = objectInput.readBoolean(); if (useFlyweightMapStorage) { areaCodeMapStorage = new FlyweightMapStorage(); } else { areaCodeMapStorage = new DefaultMapStorage(); } areaCodeMapStorage.readExternal(objectInput); } /** * Supports Java Serialization. */ public void writeExternal(ObjectOutput objectOutput) throws IOException { objectOutput.writeBoolean(areaCodeMapStorage.isFlyweight()); areaCodeMapStorage.writeExternal(objectOutput); } /** * Returns the description of the geographical area the {@code number} corresponds to. * * @param number the phone number to look up * @return the description of the geographical area */ String lookup(PhoneNumber number) { int numOfEntries = areaCodeMapStorage.getNumOfEntries(); if (numOfEntries == 0) { return ""; } long phonePrefix = Long.parseLong(number.getCountryCode() + phoneUtil.getNationalSignificantNumber(number)); int currentIndex = numOfEntries - 1; SortedSet<Integer> currentSetOfLengths = areaCodeMapStorage.getPossibleLengths(); while (currentSetOfLengths.size() > 0) { Integer possibleLength = currentSetOfLengths.last(); String phonePrefixStr = String.valueOf(phonePrefix); if (phonePrefixStr.length() > possibleLength) { phonePrefix = Long.parseLong(phonePrefixStr.substring(0, possibleLength)); } currentIndex = binarySearch(0, currentIndex, phonePrefix); if (currentIndex < 0) { return ""; } int currentPrefix = areaCodeMapStorage.getPrefix(currentIndex); if (phonePrefix == currentPrefix) { return areaCodeMapStorage.getDescription(currentIndex); } currentSetOfLengths = currentSetOfLengths.headSet(possibleLength); } return ""; } /** * Does a binary search for {@code value} in the provided array from {@code start} to {@code end} * (inclusive). Returns the position if {@code value} is found; otherwise, returns the * position which has the largest value that is less than {@code value}. This means if * {@code value} is the smallest, -1 will be returned. */ private int binarySearch(int start, int end, long value) { int current = 0; while (start <= end) { current = (start + end) >>> 1; int currentValue = areaCodeMapStorage.getPrefix(current); if (currentValue == value) { return current; } else if (currentValue > value) { current--; end = current; } else { start = current + 1; } } return current; } /** * Dumps the mappings contained in the area code map. */ @Override public String toString() { return areaCodeMapStorage.toString(); } }