Home | History | Annotate | Download | only in util
      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      */
     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 {
     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      */
    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      */
    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      */
    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      */
    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      *     &lt;type&gt; &lt;property-name&gt; = &lt;value&gt; ;
    356      *     <br />
    357      *     unset ( &lt;property-name&gt; ) ;
    358      *     </tt>
    359      *     <p>
    360      *     "//" comments everything until the end of the line.
    361      *     "/&#2a;" comments everything until the next appearance of "&#2a;/".
    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      *     &lt;type&gt; is one of {boolean, byte, short, int, long,
    369      *     float, double, String}, and is case-sensitive.
    370      *     <p>
    371      *     &lt;property-name&gt; is a valid fully-qualified class name
    372      *     (one or more valid identifiers separated by dot characters).
    373      *     <p>
    374      *     &lt;value&gt; 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      */
    397     public void load(Reader r) throws IOException {
    398         parse(r, this);
    399     }
    400 
    401     @Override
    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 {
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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