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 "mobileNumberPortableRegion")); 83 84 private final TreeMap<String, TreeSet<String>> blacklist; 85 86 // Note: If changing the blacklist here or the name of the method, update documentation about 87 // affected methods at the same time: 88 // https://github.com/google/libphonenumber/blob/master/FAQ.md#what-is-the-metadatalitejsmetadata_lite-option forLiteBuild()89 static MetadataFilter forLiteBuild() { 90 // "exampleNumber" is a blacklist. 91 return new MetadataFilter(parseFieldMapFromString("exampleNumber")); 92 } 93 forSpecialBuild()94 static MetadataFilter forSpecialBuild() { 95 // "mobile" is a whitelist. 96 return new MetadataFilter(computeComplement(parseFieldMapFromString("mobile"))); 97 } 98 emptyFilter()99 static MetadataFilter emptyFilter() { 100 // Empty blacklist, meaning we filter nothing. 101 return new MetadataFilter(new TreeMap<String, TreeSet<String>>()); 102 } 103 104 // @VisibleForTesting MetadataFilter(TreeMap<String, TreeSet<String>> blacklist)105 MetadataFilter(TreeMap<String, TreeSet<String>> blacklist) { 106 this.blacklist = blacklist; 107 } 108 109 @Override equals(Object obj)110 public boolean equals(Object obj) { 111 return blacklist.equals(((MetadataFilter) obj).blacklist); 112 } 113 114 @Override hashCode()115 public int hashCode() { 116 return blacklist.hashCode(); 117 } 118 119 /** 120 * Clears certain fields in {@code metadata} as defined by the {@code MetadataFilter} instance. 121 * Note that this changes the mutable {@code metadata} object, and is not thread-safe. If this 122 * method does not return successfully, do not assume {@code metadata} has not changed. 123 * 124 * @param metadata The {@code PhoneMetadata} object to be filtered 125 */ filterMetadata(PhoneMetadata.Builder metadata)126 void filterMetadata(PhoneMetadata.Builder metadata) { 127 // TODO: Consider clearing if the filtered PhoneNumberDesc is empty. 128 if (metadata.hasFixedLine()) { 129 metadata.setFixedLine(getFiltered("fixedLine", metadata.getFixedLine())); 130 } 131 if (metadata.hasMobile()) { 132 metadata.setMobile(getFiltered("mobile", metadata.getMobile())); 133 } 134 if (metadata.hasTollFree()) { 135 metadata.setTollFree(getFiltered("tollFree", metadata.getTollFree())); 136 } 137 if (metadata.hasPremiumRate()) { 138 metadata.setPremiumRate(getFiltered("premiumRate", metadata.getPremiumRate())); 139 } 140 if (metadata.hasSharedCost()) { 141 metadata.setSharedCost(getFiltered("sharedCost", metadata.getSharedCost())); 142 } 143 if (metadata.hasPersonalNumber()) { 144 metadata.setPersonalNumber(getFiltered("personalNumber", metadata.getPersonalNumber())); 145 } 146 if (metadata.hasVoip()) { 147 metadata.setVoip(getFiltered("voip", metadata.getVoip())); 148 } 149 if (metadata.hasPager()) { 150 metadata.setPager(getFiltered("pager", metadata.getPager())); 151 } 152 if (metadata.hasUan()) { 153 metadata.setUan(getFiltered("uan", metadata.getUan())); 154 } 155 if (metadata.hasEmergency()) { 156 metadata.setEmergency(getFiltered("emergency", metadata.getEmergency())); 157 } 158 if (metadata.hasVoicemail()) { 159 metadata.setVoicemail(getFiltered("voicemail", metadata.getVoicemail())); 160 } 161 if (metadata.hasShortCode()) { 162 metadata.setShortCode(getFiltered("shortCode", metadata.getShortCode())); 163 } 164 if (metadata.hasStandardRate()) { 165 metadata.setStandardRate(getFiltered("standardRate", metadata.getStandardRate())); 166 } 167 if (metadata.hasCarrierSpecific()) { 168 metadata.setCarrierSpecific(getFiltered("carrierSpecific", metadata.getCarrierSpecific())); 169 } 170 if (metadata.hasSmsServices()) { 171 metadata.setSmsServices(getFiltered("smsServices", metadata.getSmsServices())); 172 } 173 if (metadata.hasNoInternationalDialling()) { 174 metadata.setNoInternationalDialling(getFiltered("noInternationalDialling", 175 metadata.getNoInternationalDialling())); 176 } 177 178 if (shouldDrop("preferredInternationalPrefix")) { 179 metadata.clearPreferredInternationalPrefix(); 180 } 181 if (shouldDrop("nationalPrefix")) { 182 metadata.clearNationalPrefix(); 183 } 184 if (shouldDrop("preferredExtnPrefix")) { 185 metadata.clearPreferredExtnPrefix(); 186 } 187 if (shouldDrop("nationalPrefixTransformRule")) { 188 metadata.clearNationalPrefixTransformRule(); 189 } 190 if (shouldDrop("sameMobileAndFixedLinePattern")) { 191 metadata.clearSameMobileAndFixedLinePattern(); 192 } 193 if (shouldDrop("mainCountryForCode")) { 194 metadata.clearMainCountryForCode(); 195 } 196 if (shouldDrop("mobileNumberPortableRegion")) { 197 metadata.clearMobileNumberPortableRegion(); 198 } 199 } 200 201 /** 202 * The input blacklist or whitelist string is expected to be of the form "a(b,c):d(e):f", where 203 * b and c are children of a, e is a child of d, and f is either a parent field, a child field, or 204 * a childless field. Order and whitespace don't matter. We throw RuntimeException for any 205 * duplicates, malformed strings, or strings where field tokens do not correspond to strings in 206 * the sets of excludable fields. We also throw RuntimeException for empty strings since such 207 * strings should be treated as a special case by the flag checking code and not passed here. 208 */ 209 // @VisibleForTesting parseFieldMapFromString(String string)210 static TreeMap<String, TreeSet<String>> parseFieldMapFromString(String string) { 211 if (string == null) { 212 throw new RuntimeException("Null string should not be passed to parseFieldMapFromString"); 213 } 214 // Remove whitespace. 215 string = string.replaceAll("\\s", ""); 216 if (string.isEmpty()) { 217 throw new RuntimeException("Empty string should not be passed to parseFieldMapFromString"); 218 } 219 220 TreeMap<String, TreeSet<String>> fieldMap = new TreeMap<String, TreeSet<String>>(); 221 TreeSet<String> wildcardChildren = new TreeSet<String>(); 222 for (String group : string.split(":", -1)) { 223 int leftParenIndex = group.indexOf('('); 224 int rightParenIndex = group.indexOf(')'); 225 if (leftParenIndex < 0 && rightParenIndex < 0) { 226 if (excludableParentFields.contains(group)) { 227 if (fieldMap.containsKey(group)) { 228 throw new RuntimeException(group + " given more than once in " + string); 229 } 230 fieldMap.put(group, new TreeSet<String>(excludableChildFields)); 231 } else if (excludableChildlessFields.contains(group)) { 232 if (fieldMap.containsKey(group)) { 233 throw new RuntimeException(group + " given more than once in " + string); 234 } 235 fieldMap.put(group, new TreeSet<String>()); 236 } else if (excludableChildFields.contains(group)) { 237 if (wildcardChildren.contains(group)) { 238 throw new RuntimeException(group + " given more than once in " + string); 239 } 240 wildcardChildren.add(group); 241 } else { 242 throw new RuntimeException(group + " is not a valid token"); 243 } 244 } else if (leftParenIndex > 0 && rightParenIndex == group.length() - 1) { 245 // We don't check for duplicate parentheses or illegal characters since these will be caught 246 // as not being part of valid field tokens. 247 String parent = group.substring(0, leftParenIndex); 248 if (!excludableParentFields.contains(parent)) { 249 throw new RuntimeException(parent + " is not a valid parent token"); 250 } 251 if (fieldMap.containsKey(parent)) { 252 throw new RuntimeException(parent + " given more than once in " + string); 253 } 254 TreeSet<String> children = new TreeSet<String>(); 255 for (String child : group.substring(leftParenIndex + 1, rightParenIndex).split(",", -1)) { 256 if (!excludableChildFields.contains(child)) { 257 throw new RuntimeException(child + " is not a valid child token"); 258 } 259 if (!children.add(child)) { 260 throw new RuntimeException(child + " given more than once in " + group); 261 } 262 } 263 fieldMap.put(parent, children); 264 } else { 265 throw new RuntimeException("Incorrect location of parantheses in " + group); 266 } 267 } 268 for (String wildcardChild : wildcardChildren) { 269 for (String parent : excludableParentFields) { 270 TreeSet<String> children = fieldMap.get(parent); 271 if (children == null) { 272 children = new TreeSet<String>(); 273 fieldMap.put(parent, children); 274 } 275 if (!children.add(wildcardChild) 276 && fieldMap.get(parent).size() != excludableChildFields.size()) { 277 // The map already contains parent -> wildcardChild but not all possible children. 278 // So wildcardChild was given explicitly as a child of parent, which is a duplication 279 // since it's also given as a wildcard child. 280 throw new RuntimeException( 281 wildcardChild + " is present by itself so remove it from " + parent + "'s group"); 282 } 283 } 284 } 285 return fieldMap; 286 } 287 288 // Does not check that legal tokens are used, assuming that fieldMap is constructed using 289 // parseFieldMapFromString(String) which does check. If fieldMap contains illegal tokens or parent 290 // fields with no children or other unexpected state, the behavior of this function is undefined. 291 // @VisibleForTesting computeComplement( TreeMap<String, TreeSet<String>> fieldMap)292 static TreeMap<String, TreeSet<String>> computeComplement( 293 TreeMap<String, TreeSet<String>> fieldMap) { 294 TreeMap<String, TreeSet<String>> complement = new TreeMap<String, TreeSet<String>>(); 295 for (String parent : excludableParentFields) { 296 if (!fieldMap.containsKey(parent)) { 297 complement.put(parent, new TreeSet<String>(excludableChildFields)); 298 } else { 299 TreeSet<String> otherChildren = fieldMap.get(parent); 300 // If the other map has all the children for this parent then we don't want to include the 301 // parent as a key. 302 if (otherChildren.size() != excludableChildFields.size()) { 303 TreeSet<String> children = new TreeSet<String>(); 304 for (String child : excludableChildFields) { 305 if (!otherChildren.contains(child)) { 306 children.add(child); 307 } 308 } 309 complement.put(parent, children); 310 } 311 } 312 } 313 for (String childlessField : excludableChildlessFields) { 314 if (!fieldMap.containsKey(childlessField)) { 315 complement.put(childlessField, new TreeSet<String>()); 316 } 317 } 318 return complement; 319 } 320 321 // @VisibleForTesting shouldDrop(String parent, String child)322 boolean shouldDrop(String parent, String child) { 323 if (!excludableParentFields.contains(parent)) { 324 throw new RuntimeException(parent + " is not an excludable parent field"); 325 } 326 if (!excludableChildFields.contains(child)) { 327 throw new RuntimeException(child + " is not an excludable child field"); 328 } 329 return blacklist.containsKey(parent) && blacklist.get(parent).contains(child); 330 } 331 332 // @VisibleForTesting shouldDrop(String childlessField)333 boolean shouldDrop(String childlessField) { 334 if (!excludableChildlessFields.contains(childlessField)) { 335 throw new RuntimeException(childlessField + " is not an excludable childless field"); 336 } 337 return blacklist.containsKey(childlessField); 338 } 339 getFiltered(String type, PhoneNumberDesc desc)340 private PhoneNumberDesc getFiltered(String type, PhoneNumberDesc desc) { 341 PhoneNumberDesc.Builder builder = PhoneNumberDesc.newBuilder().mergeFrom(desc); 342 if (shouldDrop(type, "nationalNumberPattern")) { 343 builder.clearNationalNumberPattern(); 344 } 345 if (shouldDrop(type, "possibleLength")) { 346 builder.clearPossibleLength(); 347 } 348 if (shouldDrop(type, "possibleLengthLocalOnly")) { 349 builder.clearPossibleLengthLocalOnly(); 350 } 351 if (shouldDrop(type, "exampleNumber")) { 352 builder.clearExampleNumber(); 353 } 354 return builder.build(); 355 } 356 } 357