1 /* 2 * Copyright (C) 2016 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; 18 19 import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata; 20 import com.google.i18n.phonenumbers.Phonemetadata.PhoneNumberDesc; 21 import java.util.Arrays; 22 import java.util.TreeMap; 23 import java.util.TreeSet; 24 25 /** 26 * Class to encapsulate the metadata filtering logic and restrict visibility into raw data 27 * structures. 28 * 29 * <p> 30 * WARNING: This is an internal API which is under development and subject to backwards-incompatible 31 * changes without notice. Any changes are not guaranteed to be reflected in the versioning scheme 32 * of the public API, nor in release notes. 33 */ 34 final class MetadataFilter { 35 // The following 3 sets comprise all the PhoneMetadata fields as defined at phonemetadata.proto 36 // which may be excluded from customized serializations of the binary metadata. Fields that are 37 // core to the library functionality may not be listed here. 38 // excludableParentFields are PhoneMetadata fields of type PhoneNumberDesc. 39 // excludableChildFields are PhoneNumberDesc fields of primitive type. 40 // excludableChildlessFields are PhoneMetadata fields of primitive type. 41 // Currently we support only one non-primitive type and the depth of the "family tree" is 2, 42 // meaning a field may have only direct descendants, who may not have descendants of their own. If 43 // this changes, the blacklist handling in this class should also change. 44 // @VisibleForTesting 45 static final TreeSet<String> excludableParentFields = new TreeSet<String>(Arrays.asList( 46 "fixedLine", 47 "mobile", 48 "tollFree", 49 "premiumRate", 50 "sharedCost", 51 "personalNumber", 52 "voip", 53 "pager", 54 "uan", 55 "emergency", 56 "voicemail", 57 "shortCode", 58 "standardRate", 59 "carrierSpecific", 60 "smsServices", 61 "noInternationalDialling")); 62 63 // Note: If this set changes, the descHasData implementation must change in PhoneNumberUtil. 64 // The current implementation assumes that all PhoneNumberDesc fields are present here, since it 65 // "clears" a PhoneNumberDesc field by simply clearing all of the fields under it. See the comment 66 // above, about all 3 sets, for more about these fields. 67 // @VisibleForTesting 68 static final TreeSet<String> excludableChildFields = new TreeSet<String>(Arrays.asList( 69 "nationalNumberPattern", 70 "possibleLength", 71 "possibleLengthLocalOnly", 72 "exampleNumber")); 73 74 // @VisibleForTesting 75 static final TreeSet<String> excludableChildlessFields = new TreeSet<String>(Arrays.asList( 76 "preferredInternationalPrefix", 77 "nationalPrefix", 78 "preferredExtnPrefix", 79 "nationalPrefixTransformRule", 80 "sameMobileAndFixedLinePattern", 81 "mainCountryForCode", 82 "leadingZeroPossible", 83 "mobileNumberPortableRegion")); 84 85 private final TreeMap<String, TreeSet<String>> blacklist; 86 87 // Note: If changing the blacklist here or the name of the method, update documentation about 88 // affected methods at the same time: 89 // https://github.com/google/libphonenumber/blob/master/FAQ.md#what-is-the-metadatalitejsmetadata_lite-option forLiteBuild()90 static MetadataFilter forLiteBuild() { 91 // "exampleNumber" is a blacklist. 92 return new MetadataFilter(parseFieldMapFromString("exampleNumber")); 93 } 94 forSpecialBuild()95 static MetadataFilter forSpecialBuild() { 96 // "mobile" is a whitelist. 97 return new MetadataFilter(computeComplement(parseFieldMapFromString("mobile"))); 98 } 99 emptyFilter()100 static MetadataFilter emptyFilter() { 101 // Empty blacklist, meaning we filter nothing. 102 return new MetadataFilter(new TreeMap<String, TreeSet<String>>()); 103 } 104 105 // @VisibleForTesting MetadataFilter(TreeMap<String, TreeSet<String>> blacklist)106 MetadataFilter(TreeMap<String, TreeSet<String>> blacklist) { 107 this.blacklist = blacklist; 108 } 109 110 @Override equals(Object obj)111 public boolean equals(Object obj) { 112 return blacklist.equals(((MetadataFilter) obj).blacklist); 113 } 114 115 @Override hashCode()116 public int hashCode() { 117 return blacklist.hashCode(); 118 } 119 120 /** 121 * Clears certain fields in {@code metadata} as defined by the {@code MetadataFilter} instance. 122 * Note that this changes the mutable {@code metadata} object, and is not thread-safe. If this 123 * method does not return successfully, do not assume {@code metadata} has not changed. 124 * 125 * @param metadata The {@code PhoneMetadata} object to be filtered 126 */ filterMetadata(PhoneMetadata.Builder metadata)127 void filterMetadata(PhoneMetadata.Builder metadata) { 128 // TODO: Consider clearing if the filtered PhoneNumberDesc is empty. 129 if (metadata.hasFixedLine()) { 130 metadata.setFixedLine(getFiltered("fixedLine", metadata.getFixedLine())); 131 } 132 if (metadata.hasMobile()) { 133 metadata.setMobile(getFiltered("mobile", metadata.getMobile())); 134 } 135 if (metadata.hasTollFree()) { 136 metadata.setTollFree(getFiltered("tollFree", metadata.getTollFree())); 137 } 138 if (metadata.hasPremiumRate()) { 139 metadata.setPremiumRate(getFiltered("premiumRate", metadata.getPremiumRate())); 140 } 141 if (metadata.hasSharedCost()) { 142 metadata.setSharedCost(getFiltered("sharedCost", metadata.getSharedCost())); 143 } 144 if (metadata.hasPersonalNumber()) { 145 metadata.setPersonalNumber(getFiltered("personalNumber", metadata.getPersonalNumber())); 146 } 147 if (metadata.hasVoip()) { 148 metadata.setVoip(getFiltered("voip", metadata.getVoip())); 149 } 150 if (metadata.hasPager()) { 151 metadata.setPager(getFiltered("pager", metadata.getPager())); 152 } 153 if (metadata.hasUan()) { 154 metadata.setUan(getFiltered("uan", metadata.getUan())); 155 } 156 if (metadata.hasEmergency()) { 157 metadata.setEmergency(getFiltered("emergency", metadata.getEmergency())); 158 } 159 if (metadata.hasVoicemail()) { 160 metadata.setVoicemail(getFiltered("voicemail", metadata.getVoicemail())); 161 } 162 if (metadata.hasShortCode()) { 163 metadata.setShortCode(getFiltered("shortCode", metadata.getShortCode())); 164 } 165 if (metadata.hasStandardRate()) { 166 metadata.setStandardRate(getFiltered("standardRate", metadata.getStandardRate())); 167 } 168 if (metadata.hasCarrierSpecific()) { 169 metadata.setCarrierSpecific(getFiltered("carrierSpecific", metadata.getCarrierSpecific())); 170 } 171 if (metadata.hasSmsServices()) { 172 metadata.setSmsServices(getFiltered("smsServices", metadata.getSmsServices())); 173 } 174 if (metadata.hasNoInternationalDialling()) { 175 metadata.setNoInternationalDialling(getFiltered("noInternationalDialling", 176 metadata.getNoInternationalDialling())); 177 } 178 179 if (shouldDrop("preferredInternationalPrefix")) { 180 metadata.clearPreferredInternationalPrefix(); 181 } 182 if (shouldDrop("nationalPrefix")) { 183 metadata.clearNationalPrefix(); 184 } 185 if (shouldDrop("preferredExtnPrefix")) { 186 metadata.clearPreferredExtnPrefix(); 187 } 188 if (shouldDrop("nationalPrefixTransformRule")) { 189 metadata.clearNationalPrefixTransformRule(); 190 } 191 if (shouldDrop("sameMobileAndFixedLinePattern")) { 192 metadata.clearSameMobileAndFixedLinePattern(); 193 } 194 if (shouldDrop("mainCountryForCode")) { 195 metadata.clearMainCountryForCode(); 196 } 197 if (shouldDrop("leadingZeroPossible")) { 198 metadata.clearLeadingZeroPossible(); 199 } 200 if (shouldDrop("mobileNumberPortableRegion")) { 201 metadata.clearMobileNumberPortableRegion(); 202 } 203 } 204 205 /** 206 * The input blacklist or whitelist string is expected to be of the form "a(b,c):d(e):f", where 207 * b and c are children of a, e is a child of d, and f is either a parent field, a child field, or 208 * a childless field. Order and whitespace don't matter. We throw RuntimeException for any 209 * duplicates, malformed strings, or strings where field tokens do not correspond to strings in 210 * the sets of excludable fields. We also throw RuntimeException for empty strings since such 211 * strings should be treated as a special case by the flag checking code and not passed here. 212 */ 213 // @VisibleForTesting parseFieldMapFromString(String string)214 static TreeMap<String, TreeSet<String>> parseFieldMapFromString(String string) { 215 if (string == null) { 216 throw new RuntimeException("Null string should not be passed to parseFieldMapFromString"); 217 } 218 // Remove whitespace. 219 string = string.replaceAll("\\s", ""); 220 if (string.isEmpty()) { 221 throw new RuntimeException("Empty string should not be passed to parseFieldMapFromString"); 222 } 223 224 TreeMap<String, TreeSet<String>> fieldMap = new TreeMap<String, TreeSet<String>>(); 225 TreeSet<String> wildcardChildren = new TreeSet<String>(); 226 for (String group : string.split(":", -1)) { 227 int leftParenIndex = group.indexOf('('); 228 int rightParenIndex = group.indexOf(')'); 229 if (leftParenIndex < 0 && rightParenIndex < 0) { 230 if (excludableParentFields.contains(group)) { 231 if (fieldMap.containsKey(group)) { 232 throw new RuntimeException(group + " given more than once in " + string); 233 } 234 fieldMap.put(group, new TreeSet<String>(excludableChildFields)); 235 } else if (excludableChildlessFields.contains(group)) { 236 if (fieldMap.containsKey(group)) { 237 throw new RuntimeException(group + " given more than once in " + string); 238 } 239 fieldMap.put(group, new TreeSet<String>()); 240 } else if (excludableChildFields.contains(group)) { 241 if (wildcardChildren.contains(group)) { 242 throw new RuntimeException(group + " given more than once in " + string); 243 } 244 wildcardChildren.add(group); 245 } else { 246 throw new RuntimeException(group + " is not a valid token"); 247 } 248 } else if (leftParenIndex > 0 && rightParenIndex == group.length() - 1) { 249 // We don't check for duplicate parentheses or illegal characters since these will be caught 250 // as not being part of valid field tokens. 251 String parent = group.substring(0, leftParenIndex); 252 if (!excludableParentFields.contains(parent)) { 253 throw new RuntimeException(parent + " is not a valid parent token"); 254 } 255 if (fieldMap.containsKey(parent)) { 256 throw new RuntimeException(parent + " given more than once in " + string); 257 } 258 TreeSet<String> children = new TreeSet<String>(); 259 for (String child : group.substring(leftParenIndex + 1, rightParenIndex).split(",", -1)) { 260 if (!excludableChildFields.contains(child)) { 261 throw new RuntimeException(child + " is not a valid child token"); 262 } 263 if (!children.add(child)) { 264 throw new RuntimeException(child + " given more than once in " + group); 265 } 266 } 267 fieldMap.put(parent, children); 268 } else { 269 throw new RuntimeException("Incorrect location of parantheses in " + group); 270 } 271 } 272 for (String wildcardChild : wildcardChildren) { 273 for (String parent : excludableParentFields) { 274 TreeSet<String> children = fieldMap.get(parent); 275 if (children == null) { 276 children = new TreeSet<String>(); 277 fieldMap.put(parent, children); 278 } 279 if (!children.add(wildcardChild) 280 && fieldMap.get(parent).size() != excludableChildFields.size()) { 281 // The map already contains parent -> wildcardChild but not all possible children. 282 // So wildcardChild was given explicitly as a child of parent, which is a duplication 283 // since it's also given as a wildcard child. 284 throw new RuntimeException( 285 wildcardChild + " is present by itself so remove it from " + parent + "'s group"); 286 } 287 } 288 } 289 return fieldMap; 290 } 291 292 // Does not check that legal tokens are used, assuming that fieldMap is constructed using 293 // parseFieldMapFromString(String) which does check. If fieldMap contains illegal tokens or parent 294 // fields with no children or other unexpected state, the behavior of this function is undefined. 295 // @VisibleForTesting computeComplement( TreeMap<String, TreeSet<String>> fieldMap)296 static TreeMap<String, TreeSet<String>> computeComplement( 297 TreeMap<String, TreeSet<String>> fieldMap) { 298 TreeMap<String, TreeSet<String>> complement = new TreeMap<String, TreeSet<String>>(); 299 for (String parent : excludableParentFields) { 300 if (!fieldMap.containsKey(parent)) { 301 complement.put(parent, new TreeSet<String>(excludableChildFields)); 302 } else { 303 TreeSet<String> otherChildren = fieldMap.get(parent); 304 // If the other map has all the children for this parent then we don't want to include the 305 // parent as a key. 306 if (otherChildren.size() != excludableChildFields.size()) { 307 TreeSet<String> children = new TreeSet<String>(); 308 for (String child : excludableChildFields) { 309 if (!otherChildren.contains(child)) { 310 children.add(child); 311 } 312 } 313 complement.put(parent, children); 314 } 315 } 316 } 317 for (String childlessField : excludableChildlessFields) { 318 if (!fieldMap.containsKey(childlessField)) { 319 complement.put(childlessField, new TreeSet<String>()); 320 } 321 } 322 return complement; 323 } 324 325 // @VisibleForTesting shouldDrop(String parent, String child)326 boolean shouldDrop(String parent, String child) { 327 if (!excludableParentFields.contains(parent)) { 328 throw new RuntimeException(parent + " is not an excludable parent field"); 329 } 330 if (!excludableChildFields.contains(child)) { 331 throw new RuntimeException(child + " is not an excludable child field"); 332 } 333 return blacklist.containsKey(parent) && blacklist.get(parent).contains(child); 334 } 335 336 // @VisibleForTesting shouldDrop(String childlessField)337 boolean shouldDrop(String childlessField) { 338 if (!excludableChildlessFields.contains(childlessField)) { 339 throw new RuntimeException(childlessField + " is not an excludable childless field"); 340 } 341 return blacklist.containsKey(childlessField); 342 } 343 getFiltered(String type, PhoneNumberDesc desc)344 private PhoneNumberDesc getFiltered(String type, PhoneNumberDesc desc) { 345 PhoneNumberDesc.Builder builder = PhoneNumberDesc.newBuilder().mergeFrom(desc); 346 if (shouldDrop(type, "nationalNumberPattern")) { 347 builder.clearNationalNumberPattern(); 348 } 349 if (shouldDrop(type, "possibleLength")) { 350 builder.clearPossibleLength(); 351 } 352 if (shouldDrop(type, "possibleLengthLocalOnly")) { 353 builder.clearPossibleLengthLocalOnly(); 354 } 355 if (shouldDrop(type, "exampleNumber")) { 356 builder.clearExampleNumber(); 357 } 358 return builder.build(); 359 } 360 } 361