1 /* 2 * Copyright (C) 2009 The Android Open Source Project 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.android.internal.util; 18 19 import java.io.IOException; 20 import java.io.Reader; 21 import java.io.StreamTokenizer; 22 import java.util.HashMap; 23 import java.util.Map; 24 import java.util.regex.Pattern; 25 26 /** 27 * A {@code Map} that publishes a set of typed properties, defined by 28 * zero or more {@code Reader}s containing textual definitions and assignments. 29 */ 30 public class TypedProperties extends HashMap<String, Object> { 31 /** 32 * Instantiates a {@link java.io.StreamTokenizer} and sets its syntax tables 33 * appropriately for the {@code TypedProperties} file format. 34 * 35 * @param r The {@code Reader} that the {@code StreamTokenizer} will read from 36 * @return a newly-created and initialized {@code StreamTokenizer} 37 */ initTokenizer(Reader r)38 static StreamTokenizer initTokenizer(Reader r) { 39 StreamTokenizer st = new StreamTokenizer(r); 40 41 // Treat everything we don't specify as "ordinary". 42 st.resetSyntax(); 43 44 /* The only non-quoted-string words we'll be reading are: 45 * - property names: [._$a-zA-Z0-9] 46 * - type names: [a-zS] 47 * - number literals: [-0-9.eExXA-Za-z] ('x' for 0xNNN hex literals. "NaN", "Infinity") 48 * - "true" or "false" (case insensitive): [a-zA-Z] 49 */ 50 st.wordChars('0', '9'); 51 st.wordChars('A', 'Z'); 52 st.wordChars('a', 'z'); 53 st.wordChars('_', '_'); 54 st.wordChars('$', '$'); 55 st.wordChars('.', '.'); 56 st.wordChars('-', '-'); 57 st.wordChars('+', '+'); 58 59 // Single-character tokens 60 st.ordinaryChar('='); 61 62 // Other special characters 63 st.whitespaceChars(' ', ' '); 64 st.whitespaceChars('\t', '\t'); 65 st.whitespaceChars('\n', '\n'); 66 st.whitespaceChars('\r', '\r'); 67 st.quoteChar('"'); 68 69 // Java-style comments 70 st.slashStarComments(true); 71 st.slashSlashComments(true); 72 73 return st; 74 } 75 76 77 /** 78 * An unchecked exception that is thrown when encountering a syntax 79 * or semantic error in the input. 80 */ 81 public static class ParseException extends IllegalArgumentException { ParseException(StreamTokenizer state, String expected)82 ParseException(StreamTokenizer state, String expected) { 83 super("expected " + expected + ", saw " + state.toString()); 84 } 85 } 86 87 // A sentinel instance used to indicate a null string. 88 static final String NULL_STRING = new String("<TypedProperties:NULL_STRING>"); 89 90 // Constants used to represent the supported types. 91 static final int TYPE_UNSET = 'x'; 92 static final int TYPE_BOOLEAN = 'Z'; 93 static final int TYPE_BYTE = 'I' | 1 << 8; 94 // TYPE_CHAR: character literal syntax not supported; use short. 95 static final int TYPE_SHORT = 'I' | 2 << 8; 96 static final int TYPE_INT = 'I' | 4 << 8; 97 static final int TYPE_LONG = 'I' | 8 << 8; 98 static final int TYPE_FLOAT = 'F' | 4 << 8; 99 static final int TYPE_DOUBLE = 'F' | 8 << 8; 100 static final int TYPE_STRING = 'L' | 's' << 8; 101 static final int TYPE_ERROR = -1; 102 103 /** 104 * Converts a string to an internal type constant. 105 * 106 * @param typeName the type name to convert 107 * @return the type constant that corresponds to {@code typeName}, 108 * or {@code TYPE_ERROR} if the type is unknown 109 */ interpretType(String typeName)110 static int interpretType(String typeName) { 111 if ("unset".equals(typeName)) { 112 return TYPE_UNSET; 113 } else if ("boolean".equals(typeName)) { 114 return TYPE_BOOLEAN; 115 } else if ("byte".equals(typeName)) { 116 return TYPE_BYTE; 117 } else if ("short".equals(typeName)) { 118 return TYPE_SHORT; 119 } else if ("int".equals(typeName)) { 120 return TYPE_INT; 121 } else if ("long".equals(typeName)) { 122 return TYPE_LONG; 123 } else if ("float".equals(typeName)) { 124 return TYPE_FLOAT; 125 } else if ("double".equals(typeName)) { 126 return TYPE_DOUBLE; 127 } else if ("String".equals(typeName)) { 128 return TYPE_STRING; 129 } 130 return TYPE_ERROR; 131 } 132 133 /** 134 * Parses the data in the reader. 135 * 136 * @param r The {@code Reader} containing input data to parse 137 * @param map The {@code Map} to insert parameter values into 138 * @throws ParseException if the input data is malformed 139 * @throws IOException if there is a problem reading from the {@code Reader} 140 */ parse(Reader r, Map<String, Object> map)141 static void parse(Reader r, Map<String, Object> map) throws ParseException, IOException { 142 final StreamTokenizer st = initTokenizer(r); 143 144 /* A property name must be a valid fully-qualified class + package name. 145 * We don't support Unicode, though. 146 */ 147 final String identifierPattern = "[a-zA-Z_$][0-9a-zA-Z_$]*"; 148 final Pattern propertyNamePattern = 149 Pattern.compile("(" + identifierPattern + "\\.)*" + identifierPattern); 150 151 152 while (true) { 153 int token; 154 155 // Read the next token, which is either the type or EOF. 156 token = st.nextToken(); 157 if (token == StreamTokenizer.TT_EOF) { 158 break; 159 } 160 if (token != StreamTokenizer.TT_WORD) { 161 throw new ParseException(st, "type name"); 162 } 163 final int type = interpretType(st.sval); 164 if (type == TYPE_ERROR) { 165 throw new ParseException(st, "valid type name"); 166 } 167 st.sval = null; 168 169 if (type == TYPE_UNSET) { 170 // Expect '('. 171 token = st.nextToken(); 172 if (token != '(') { 173 throw new ParseException(st, "'('"); 174 } 175 } 176 177 // Read the property name. 178 token = st.nextToken(); 179 if (token != StreamTokenizer.TT_WORD) { 180 throw new ParseException(st, "property name"); 181 } 182 final String propertyName = st.sval; 183 if (!propertyNamePattern.matcher(propertyName).matches()) { 184 throw new ParseException(st, "valid property name"); 185 } 186 st.sval = null; 187 188 if (type == TYPE_UNSET) { 189 // Expect ')'. 190 token = st.nextToken(); 191 if (token != ')') { 192 throw new ParseException(st, "')'"); 193 } 194 map.remove(propertyName); 195 } else { 196 // Expect '='. 197 token = st.nextToken(); 198 if (token != '=') { 199 throw new ParseException(st, "'='"); 200 } 201 202 // Read a value of the appropriate type, and insert into the map. 203 final Object value = parseValue(st, type); 204 final Object oldValue = map.remove(propertyName); 205 if (oldValue != null) { 206 // TODO: catch the case where a string is set to null and then 207 // the same property is defined with a different type. 208 if (value.getClass() != oldValue.getClass()) { 209 throw new ParseException(st, 210 "(property previously declared as a different type)"); 211 } 212 } 213 map.put(propertyName, value); 214 } 215 216 // Expect ';'. 217 token = st.nextToken(); 218 if (token != ';') { 219 throw new ParseException(st, "';'"); 220 } 221 } 222 } 223 224 /** 225 * Parses the next token in the StreamTokenizer as the specified type. 226 * 227 * @param st The token source 228 * @param type The type to interpret next token as 229 * @return a Boolean, Number subclass, or String representing the value. 230 * Null strings are represented by the String instance NULL_STRING 231 * @throws IOException if there is a problem reading from the {@code StreamTokenizer} 232 */ parseValue(StreamTokenizer st, final int type)233 static Object parseValue(StreamTokenizer st, final int type) throws IOException { 234 final int token = st.nextToken(); 235 236 if (type == TYPE_BOOLEAN) { 237 if (token != StreamTokenizer.TT_WORD) { 238 throw new ParseException(st, "boolean constant"); 239 } 240 241 if ("true".equals(st.sval)) { 242 return Boolean.TRUE; 243 } else if ("false".equals(st.sval)) { 244 return Boolean.FALSE; 245 } 246 247 throw new ParseException(st, "boolean constant"); 248 } else if ((type & 0xff) == 'I') { 249 if (token != StreamTokenizer.TT_WORD) { 250 throw new ParseException(st, "integer constant"); 251 } 252 253 /* Parse the string. Long.decode() handles C-style integer constants 254 * ("0x" -> hex, "0" -> octal). It also treats numbers with a prefix of "#" as 255 * hex, but our syntax intentionally does not list '#' as a word character. 256 */ 257 long value; 258 try { 259 value = Long.decode(st.sval); 260 } catch (NumberFormatException ex) { 261 throw new ParseException(st, "integer constant"); 262 } 263 264 // Ensure that the type can hold this value, and return. 265 int width = (type >> 8) & 0xff; 266 switch (width) { 267 case 1: 268 if (value < Byte.MIN_VALUE || value > Byte.MAX_VALUE) { 269 throw new ParseException(st, "8-bit integer constant"); 270 } 271 return new Byte((byte)value); 272 case 2: 273 if (value < Short.MIN_VALUE || value > Short.MAX_VALUE) { 274 throw new ParseException(st, "16-bit integer constant"); 275 } 276 return new Short((short)value); 277 case 4: 278 if (value < Integer.MIN_VALUE || value > Integer.MAX_VALUE) { 279 throw new ParseException(st, "32-bit integer constant"); 280 } 281 return new Integer((int)value); 282 case 8: 283 if (value < Long.MIN_VALUE || value > Long.MAX_VALUE) { 284 throw new ParseException(st, "64-bit integer constant"); 285 } 286 return new Long(value); 287 default: 288 throw new IllegalStateException( 289 "Internal error; unexpected integer type width " + width); 290 } 291 } else if ((type & 0xff) == 'F') { 292 if (token != StreamTokenizer.TT_WORD) { 293 throw new ParseException(st, "float constant"); 294 } 295 296 // Parse the string. 297 /* TODO: Maybe just parse as float or double, losing precision if necessary. 298 * Parsing as double and converting to float can change the value 299 * compared to just parsing as float. 300 */ 301 double value; 302 try { 303 /* TODO: detect if the string representation loses precision 304 * when being converted to a double. 305 */ 306 value = Double.parseDouble(st.sval); 307 } catch (NumberFormatException ex) { 308 throw new ParseException(st, "float constant"); 309 } 310 311 // Ensure that the type can hold this value, and return. 312 if (((type >> 8) & 0xff) == 4) { 313 // This property is a float; make sure the value fits. 314 double absValue = Math.abs(value); 315 if (absValue != 0.0 && !Double.isInfinite(value) && !Double.isNaN(value)) { 316 if (absValue < Float.MIN_VALUE || absValue > Float.MAX_VALUE) { 317 throw new ParseException(st, "32-bit float constant"); 318 } 319 } 320 return new Float((float)value); 321 } else { 322 // This property is a double; no need to truncate. 323 return new Double(value); 324 } 325 } else if (type == TYPE_STRING) { 326 // Expect a quoted string or the word "null". 327 if (token == '"') { 328 return st.sval; 329 } else if (token == StreamTokenizer.TT_WORD && "null".equals(st.sval)) { 330 return NULL_STRING; 331 } 332 throw new ParseException(st, "double-quoted string or 'null'"); 333 } 334 335 throw new IllegalStateException("Internal error; unknown type " + type); 336 } 337 338 339 /** 340 * Creates an empty TypedProperties instance. 341 */ TypedProperties()342 public TypedProperties() { 343 super(); 344 } 345 346 /** 347 * Loads zero or more properties from the specified Reader. 348 * Properties that have already been loaded are preserved unless 349 * the new Reader overrides or unsets earlier values for the 350 * same properties. 351 * <p> 352 * File syntax: 353 * <blockquote> 354 * <tt> 355 * <type> <property-name> = <value> ; 356 * <br /> 357 * unset ( <property-name> ) ; 358 * </tt> 359 * <p> 360 * "//" comments everything until the end of the line. 361 * "/a;" comments everything until the next appearance of "a;/". 362 * <p> 363 * Blank lines are ignored. 364 * <p> 365 * The only required whitespace is between the type and 366 * the property name. 367 * <p> 368 * <type> is one of {boolean, byte, short, int, long, 369 * float, double, String}, and is case-sensitive. 370 * <p> 371 * <property-name> is a valid fully-qualified class name 372 * (one or more valid identifiers separated by dot characters). 373 * <p> 374 * <value> depends on the type: 375 * <ul> 376 * <li> boolean: one of {true, false} (case-sensitive) 377 * <li> byte, short, int, long: a valid Java integer constant 378 * (including non-base-10 constants like 0xabc and 074) 379 * whose value does not overflow the type. NOTE: these are 380 * interpreted as Java integer values, so they are all signed. 381 * <li> float, double: a valid Java floating-point constant. 382 * If the type is float, the value must fit in 32 bits. 383 * <li> String: a double-quoted string value, or the word {@code null}. 384 * NOTE: the contents of the string must be 7-bit clean ASCII; 385 * C-style octal escapes are recognized, but Unicode escapes are not. 386 * </ul> 387 * <p> 388 * Passing a property-name to {@code unset()} will unset the property, 389 * removing its value and type information, as if it had never been 390 * defined. 391 * </blockquote> 392 * 393 * @param r The Reader to load properties from 394 * @throws IOException if an error occurs when reading the data 395 * @throws IllegalArgumentException if the data is malformed 396 */ load(Reader r)397 public void load(Reader r) throws IOException { 398 parse(r, this); 399 } 400 401 @Override get(Object key)402 public Object get(Object key) { 403 Object value = super.get(key); 404 if (value == NULL_STRING) { 405 return null; 406 } 407 return value; 408 } 409 410 /* 411 * Getters with explicit defaults 412 */ 413 414 /** 415 * An unchecked exception that is thrown if a {@code get<TYPE>()} method 416 * is used to retrieve a parameter whose type does not match the method name. 417 */ 418 public static class TypeException extends IllegalArgumentException { TypeException(String property, Object value, String requestedType)419 TypeException(String property, Object value, String requestedType) { 420 super(property + " has type " + value.getClass().getName() + 421 ", not " + requestedType); 422 } 423 } 424 425 /** 426 * Returns the value of a boolean property, or the default if the property 427 * has not been defined. 428 * 429 * @param property The name of the property to return 430 * @param def The default value to return if the property is not set 431 * @return the value of the property 432 * @throws TypeException if the property is set and is not a boolean 433 */ getBoolean(String property, boolean def)434 public boolean getBoolean(String property, boolean def) { 435 Object value = super.get(property); 436 if (value == null) { 437 return def; 438 } 439 if (value instanceof Boolean) { 440 return ((Boolean)value).booleanValue(); 441 } 442 throw new TypeException(property, value, "boolean"); 443 } 444 445 /** 446 * Returns the value of a byte property, or the default if the property 447 * has not been defined. 448 * 449 * @param property The name of the property to return 450 * @param def The default value to return if the property is not set 451 * @return the value of the property 452 * @throws TypeException if the property is set and is not a byte 453 */ getByte(String property, byte def)454 public byte getByte(String property, byte def) { 455 Object value = super.get(property); 456 if (value == null) { 457 return def; 458 } 459 if (value instanceof Byte) { 460 return ((Byte)value).byteValue(); 461 } 462 throw new TypeException(property, value, "byte"); 463 } 464 465 /** 466 * Returns the value of a short property, or the default if the property 467 * has not been defined. 468 * 469 * @param property The name of the property to return 470 * @param def The default value to return if the property is not set 471 * @return the value of the property 472 * @throws TypeException if the property is set and is not a short 473 */ getShort(String property, short def)474 public short getShort(String property, short def) { 475 Object value = super.get(property); 476 if (value == null) { 477 return def; 478 } 479 if (value instanceof Short) { 480 return ((Short)value).shortValue(); 481 } 482 throw new TypeException(property, value, "short"); 483 } 484 485 /** 486 * Returns the value of an integer property, or the default if the property 487 * has not been defined. 488 * 489 * @param property The name of the property to return 490 * @param def The default value to return if the property is not set 491 * @return the value of the property 492 * @throws TypeException if the property is set and is not an integer 493 */ getInt(String property, int def)494 public int getInt(String property, int def) { 495 Object value = super.get(property); 496 if (value == null) { 497 return def; 498 } 499 if (value instanceof Integer) { 500 return ((Integer)value).intValue(); 501 } 502 throw new TypeException(property, value, "int"); 503 } 504 505 /** 506 * Returns the value of a long property, or the default if the property 507 * has not been defined. 508 * 509 * @param property The name of the property to return 510 * @param def The default value to return if the property is not set 511 * @return the value of the property 512 * @throws TypeException if the property is set and is not a long 513 */ getLong(String property, long def)514 public long getLong(String property, long def) { 515 Object value = super.get(property); 516 if (value == null) { 517 return def; 518 } 519 if (value instanceof Long) { 520 return ((Long)value).longValue(); 521 } 522 throw new TypeException(property, value, "long"); 523 } 524 525 /** 526 * Returns the value of a float property, or the default if the property 527 * has not been defined. 528 * 529 * @param property The name of the property to return 530 * @param def The default value to return if the property is not set 531 * @return the value of the property 532 * @throws TypeException if the property is set and is not a float 533 */ getFloat(String property, float def)534 public float getFloat(String property, float def) { 535 Object value = super.get(property); 536 if (value == null) { 537 return def; 538 } 539 if (value instanceof Float) { 540 return ((Float)value).floatValue(); 541 } 542 throw new TypeException(property, value, "float"); 543 } 544 545 /** 546 * Returns the value of a double property, or the default if the property 547 * has not been defined. 548 * 549 * @param property The name of the property to return 550 * @param def The default value to return if the property is not set 551 * @return the value of the property 552 * @throws TypeException if the property is set and is not a double 553 */ getDouble(String property, double def)554 public double getDouble(String property, double def) { 555 Object value = super.get(property); 556 if (value == null) { 557 return def; 558 } 559 if (value instanceof Double) { 560 return ((Double)value).doubleValue(); 561 } 562 throw new TypeException(property, value, "double"); 563 } 564 565 /** 566 * Returns the value of a string property, or the default if the property 567 * has not been defined. 568 * 569 * @param property The name of the property to return 570 * @param def The default value to return if the property is not set 571 * @return the value of the property 572 * @throws TypeException if the property is set and is not a string 573 */ getString(String property, String def)574 public String getString(String property, String def) { 575 Object value = super.get(property); 576 if (value == null) { 577 return def; 578 } 579 if (value == NULL_STRING) { 580 return null; 581 } else if (value instanceof String) { 582 return (String)value; 583 } 584 throw new TypeException(property, value, "string"); 585 } 586 587 /* 588 * Getters with implicit defaults 589 */ 590 591 /** 592 * Returns the value of a boolean property, or false 593 * if the property has not been defined. 594 * 595 * @param property The name of the property to return 596 * @return the value of the property 597 * @throws TypeException if the property is set and is not a boolean 598 */ getBoolean(String property)599 public boolean getBoolean(String property) { 600 return getBoolean(property, false); 601 } 602 603 /** 604 * Returns the value of a byte property, or 0 605 * if the property has not been defined. 606 * 607 * @param property The name of the property to return 608 * @return the value of the property 609 * @throws TypeException if the property is set and is not a byte 610 */ getByte(String property)611 public byte getByte(String property) { 612 return getByte(property, (byte)0); 613 } 614 615 /** 616 * Returns the value of a short property, or 0 617 * if the property has not been defined. 618 * 619 * @param property The name of the property to return 620 * @return the value of the property 621 * @throws TypeException if the property is set and is not a short 622 */ getShort(String property)623 public short getShort(String property) { 624 return getShort(property, (short)0); 625 } 626 627 /** 628 * Returns the value of an integer property, or 0 629 * if the property has not been defined. 630 * 631 * @param property The name of the property to return 632 * @return the value of the property 633 * @throws TypeException if the property is set and is not an integer 634 */ getInt(String property)635 public int getInt(String property) { 636 return getInt(property, 0); 637 } 638 639 /** 640 * Returns the value of a long property, or 0 641 * if the property has not been defined. 642 * 643 * @param property The name of the property to return 644 * @return the value of the property 645 * @throws TypeException if the property is set and is not a long 646 */ getLong(String property)647 public long getLong(String property) { 648 return getLong(property, 0L); 649 } 650 651 /** 652 * Returns the value of a float property, or 0.0 653 * if the property has not been defined. 654 * 655 * @param property The name of the property to return 656 * @return the value of the property 657 * @throws TypeException if the property is set and is not a float 658 */ getFloat(String property)659 public float getFloat(String property) { 660 return getFloat(property, 0.0f); 661 } 662 663 /** 664 * Returns the value of a double property, or 0.0 665 * if the property has not been defined. 666 * 667 * @param property The name of the property to return 668 * @return the value of the property 669 * @throws TypeException if the property is set and is not a double 670 */ getDouble(String property)671 public double getDouble(String property) { 672 return getDouble(property, 0.0); 673 } 674 675 /** 676 * Returns the value of a String property, or "" 677 * if the property has not been defined. 678 * 679 * @param property The name of the property to return 680 * @return the value of the property 681 * @throws TypeException if the property is set and is not a string 682 */ getString(String property)683 public String getString(String property) { 684 return getString(property, ""); 685 } 686 687 // Values returned by getStringInfo() 688 public static final int STRING_TYPE_MISMATCH = -2; 689 public static final int STRING_NOT_SET = -1; 690 public static final int STRING_NULL = 0; 691 public static final int STRING_SET = 1; 692 693 /** 694 * Provides string type information about a property. 695 * 696 * @param property the property to check 697 * @return STRING_SET if the property is a string and is non-null. 698 * STRING_NULL if the property is a string and is null. 699 * STRING_NOT_SET if the property is not set (no type or value). 700 * STRING_TYPE_MISMATCH if the property is set but is not a string. 701 */ getStringInfo(String property)702 public int getStringInfo(String property) { 703 Object value = super.get(property); 704 if (value == null) { 705 return STRING_NOT_SET; 706 } 707 if (value == NULL_STRING) { 708 return STRING_NULL; 709 } else if (value instanceof String) { 710 return STRING_SET; 711 } 712 return STRING_TYPE_MISMATCH; 713 } 714 } 715