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