| /* |
| * Copyright (C) 2009 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.internal.util; |
| |
| import java.io.IOException; |
| import java.io.Reader; |
| import java.io.StreamTokenizer; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.regex.Pattern; |
| |
| /** |
| * A {@code Map} that publishes a set of typed properties, defined by |
| * zero or more {@code Reader}s containing textual definitions and assignments. |
| */ |
| public class TypedProperties extends HashMap<String, Object> { |
| /** |
| * Instantiates a {@link java.io.StreamTokenizer} and sets its syntax tables |
| * appropriately for the {@code TypedProperties} file format. |
| * |
| * @param r The {@code Reader} that the {@code StreamTokenizer} will read from |
| * @return a newly-created and initialized {@code StreamTokenizer} |
| */ |
| static StreamTokenizer initTokenizer(Reader r) { |
| StreamTokenizer st = new StreamTokenizer(r); |
| |
| // Treat everything we don't specify as "ordinary". |
| st.resetSyntax(); |
| |
| /* The only non-quoted-string words we'll be reading are: |
| * - property names: [._$a-zA-Z0-9] |
| * - type names: [a-zS] |
| * - number literals: [-0-9.eExXA-Za-z] ('x' for 0xNNN hex literals. "NaN", "Infinity") |
| * - "true" or "false" (case insensitive): [a-zA-Z] |
| */ |
| st.wordChars('0', '9'); |
| st.wordChars('A', 'Z'); |
| st.wordChars('a', 'z'); |
| st.wordChars('_', '_'); |
| st.wordChars('$', '$'); |
| st.wordChars('.', '.'); |
| st.wordChars('-', '-'); |
| st.wordChars('+', '+'); |
| |
| // Single-character tokens |
| st.ordinaryChar('='); |
| |
| // Other special characters |
| st.whitespaceChars(' ', ' '); |
| st.whitespaceChars('\t', '\t'); |
| st.whitespaceChars('\n', '\n'); |
| st.whitespaceChars('\r', '\r'); |
| st.quoteChar('"'); |
| |
| // Java-style comments |
| st.slashStarComments(true); |
| st.slashSlashComments(true); |
| |
| return st; |
| } |
| |
| |
| /** |
| * An unchecked exception that is thrown when encountering a syntax |
| * or semantic error in the input. |
| */ |
| public static class ParseException extends IllegalArgumentException { |
| ParseException(StreamTokenizer state, String expected) { |
| super("expected " + expected + ", saw " + state.toString()); |
| } |
| } |
| |
| // A sentinel instance used to indicate a null string. |
| static final String NULL_STRING = new String("<TypedProperties:NULL_STRING>"); |
| |
| // Constants used to represent the supported types. |
| static final int TYPE_UNSET = 'x'; |
| static final int TYPE_BOOLEAN = 'Z'; |
| static final int TYPE_BYTE = 'I' | 1 << 8; |
| // TYPE_CHAR: character literal syntax not supported; use short. |
| static final int TYPE_SHORT = 'I' | 2 << 8; |
| static final int TYPE_INT = 'I' | 4 << 8; |
| static final int TYPE_LONG = 'I' | 8 << 8; |
| static final int TYPE_FLOAT = 'F' | 4 << 8; |
| static final int TYPE_DOUBLE = 'F' | 8 << 8; |
| static final int TYPE_STRING = 'L' | 's' << 8; |
| static final int TYPE_ERROR = -1; |
| |
| /** |
| * Converts a string to an internal type constant. |
| * |
| * @param typeName the type name to convert |
| * @return the type constant that corresponds to {@code typeName}, |
| * or {@code TYPE_ERROR} if the type is unknown |
| */ |
| static int interpretType(String typeName) { |
| if ("unset".equals(typeName)) { |
| return TYPE_UNSET; |
| } else if ("boolean".equals(typeName)) { |
| return TYPE_BOOLEAN; |
| } else if ("byte".equals(typeName)) { |
| return TYPE_BYTE; |
| } else if ("short".equals(typeName)) { |
| return TYPE_SHORT; |
| } else if ("int".equals(typeName)) { |
| return TYPE_INT; |
| } else if ("long".equals(typeName)) { |
| return TYPE_LONG; |
| } else if ("float".equals(typeName)) { |
| return TYPE_FLOAT; |
| } else if ("double".equals(typeName)) { |
| return TYPE_DOUBLE; |
| } else if ("String".equals(typeName)) { |
| return TYPE_STRING; |
| } |
| return TYPE_ERROR; |
| } |
| |
| /** |
| * Parses the data in the reader. |
| * |
| * @param r The {@code Reader} containing input data to parse |
| * @param map The {@code Map} to insert parameter values into |
| * @throws ParseException if the input data is malformed |
| * @throws IOException if there is a problem reading from the {@code Reader} |
| */ |
| static void parse(Reader r, Map<String, Object> map) throws ParseException, IOException { |
| final StreamTokenizer st = initTokenizer(r); |
| |
| /* A property name must be a valid fully-qualified class + package name. |
| * We don't support Unicode, though. |
| */ |
| final String identifierPattern = "[a-zA-Z_$][0-9a-zA-Z_$]*"; |
| final Pattern propertyNamePattern = |
| Pattern.compile("(" + identifierPattern + "\\.)*" + identifierPattern); |
| |
| |
| while (true) { |
| int token; |
| |
| // Read the next token, which is either the type or EOF. |
| token = st.nextToken(); |
| if (token == StreamTokenizer.TT_EOF) { |
| break; |
| } |
| if (token != StreamTokenizer.TT_WORD) { |
| throw new ParseException(st, "type name"); |
| } |
| final int type = interpretType(st.sval); |
| if (type == TYPE_ERROR) { |
| throw new ParseException(st, "valid type name"); |
| } |
| st.sval = null; |
| |
| if (type == TYPE_UNSET) { |
| // Expect '('. |
| token = st.nextToken(); |
| if (token != '(') { |
| throw new ParseException(st, "'('"); |
| } |
| } |
| |
| // Read the property name. |
| token = st.nextToken(); |
| if (token != StreamTokenizer.TT_WORD) { |
| throw new ParseException(st, "property name"); |
| } |
| final String propertyName = st.sval; |
| if (!propertyNamePattern.matcher(propertyName).matches()) { |
| throw new ParseException(st, "valid property name"); |
| } |
| st.sval = null; |
| |
| if (type == TYPE_UNSET) { |
| // Expect ')'. |
| token = st.nextToken(); |
| if (token != ')') { |
| throw new ParseException(st, "')'"); |
| } |
| map.remove(propertyName); |
| } else { |
| // Expect '='. |
| token = st.nextToken(); |
| if (token != '=') { |
| throw new ParseException(st, "'='"); |
| } |
| |
| // Read a value of the appropriate type, and insert into the map. |
| final Object value = parseValue(st, type); |
| final Object oldValue = map.remove(propertyName); |
| if (oldValue != null) { |
| // TODO: catch the case where a string is set to null and then |
| // the same property is defined with a different type. |
| if (value.getClass() != oldValue.getClass()) { |
| throw new ParseException(st, |
| "(property previously declared as a different type)"); |
| } |
| } |
| map.put(propertyName, value); |
| } |
| |
| // Expect ';'. |
| token = st.nextToken(); |
| if (token != ';') { |
| throw new ParseException(st, "';'"); |
| } |
| } |
| } |
| |
| /** |
| * Parses the next token in the StreamTokenizer as the specified type. |
| * |
| * @param st The token source |
| * @param type The type to interpret next token as |
| * @return a Boolean, Number subclass, or String representing the value. |
| * Null strings are represented by the String instance NULL_STRING |
| * @throws IOException if there is a problem reading from the {@code StreamTokenizer} |
| */ |
| static Object parseValue(StreamTokenizer st, final int type) throws IOException { |
| final int token = st.nextToken(); |
| |
| if (type == TYPE_BOOLEAN) { |
| if (token != StreamTokenizer.TT_WORD) { |
| throw new ParseException(st, "boolean constant"); |
| } |
| |
| if ("true".equals(st.sval)) { |
| return Boolean.TRUE; |
| } else if ("false".equals(st.sval)) { |
| return Boolean.FALSE; |
| } |
| |
| throw new ParseException(st, "boolean constant"); |
| } else if ((type & 0xff) == 'I') { |
| if (token != StreamTokenizer.TT_WORD) { |
| throw new ParseException(st, "integer constant"); |
| } |
| |
| /* Parse the string. Long.decode() handles C-style integer constants |
| * ("0x" -> hex, "0" -> octal). It also treats numbers with a prefix of "#" as |
| * hex, but our syntax intentionally does not list '#' as a word character. |
| */ |
| long value; |
| try { |
| value = Long.decode(st.sval); |
| } catch (NumberFormatException ex) { |
| throw new ParseException(st, "integer constant"); |
| } |
| |
| // Ensure that the type can hold this value, and return. |
| int width = (type >> 8) & 0xff; |
| switch (width) { |
| case 1: |
| if (value < Byte.MIN_VALUE || value > Byte.MAX_VALUE) { |
| throw new ParseException(st, "8-bit integer constant"); |
| } |
| return new Byte((byte)value); |
| case 2: |
| if (value < Short.MIN_VALUE || value > Short.MAX_VALUE) { |
| throw new ParseException(st, "16-bit integer constant"); |
| } |
| return new Short((short)value); |
| case 4: |
| if (value < Integer.MIN_VALUE || value > Integer.MAX_VALUE) { |
| throw new ParseException(st, "32-bit integer constant"); |
| } |
| return new Integer((int)value); |
| case 8: |
| if (value < Long.MIN_VALUE || value > Long.MAX_VALUE) { |
| throw new ParseException(st, "64-bit integer constant"); |
| } |
| return new Long(value); |
| default: |
| throw new IllegalStateException( |
| "Internal error; unexpected integer type width " + width); |
| } |
| } else if ((type & 0xff) == 'F') { |
| if (token != StreamTokenizer.TT_WORD) { |
| throw new ParseException(st, "float constant"); |
| } |
| |
| // Parse the string. |
| /* TODO: Maybe just parse as float or double, losing precision if necessary. |
| * Parsing as double and converting to float can change the value |
| * compared to just parsing as float. |
| */ |
| double value; |
| try { |
| /* TODO: detect if the string representation loses precision |
| * when being converted to a double. |
| */ |
| value = Double.parseDouble(st.sval); |
| } catch (NumberFormatException ex) { |
| throw new ParseException(st, "float constant"); |
| } |
| |
| // Ensure that the type can hold this value, and return. |
| if (((type >> 8) & 0xff) == 4) { |
| // This property is a float; make sure the value fits. |
| double absValue = Math.abs(value); |
| if (absValue != 0.0 && !Double.isInfinite(value) && !Double.isNaN(value)) { |
| if (absValue < Float.MIN_VALUE || absValue > Float.MAX_VALUE) { |
| throw new ParseException(st, "32-bit float constant"); |
| } |
| } |
| return new Float((float)value); |
| } else { |
| // This property is a double; no need to truncate. |
| return new Double(value); |
| } |
| } else if (type == TYPE_STRING) { |
| // Expect a quoted string or the word "null". |
| if (token == '"') { |
| return st.sval; |
| } else if (token == StreamTokenizer.TT_WORD && "null".equals(st.sval)) { |
| return NULL_STRING; |
| } |
| throw new ParseException(st, "double-quoted string or 'null'"); |
| } |
| |
| throw new IllegalStateException("Internal error; unknown type " + type); |
| } |
| |
| |
| /** |
| * Creates an empty TypedProperties instance. |
| */ |
| public TypedProperties() { |
| super(); |
| } |
| |
| /** |
| * Loads zero or more properties from the specified Reader. |
| * Properties that have already been loaded are preserved unless |
| * the new Reader overrides or unsets earlier values for the |
| * same properties. |
| * <p> |
| * File syntax: |
| * <blockquote> |
| * <tt> |
| * <type> <property-name> = <value> ; |
| * <br /> |
| * unset ( <property-name> ) ; |
| * </tt> |
| * <p> |
| * "//" comments everything until the end of the line. |
| * "/a;" comments everything until the next appearance of "a;/". |
| * <p> |
| * Blank lines are ignored. |
| * <p> |
| * The only required whitespace is between the type and |
| * the property name. |
| * <p> |
| * <type> is one of {boolean, byte, short, int, long, |
| * float, double, String}, and is case-sensitive. |
| * <p> |
| * <property-name> is a valid fully-qualified class name |
| * (one or more valid identifiers separated by dot characters). |
| * <p> |
| * <value> depends on the type: |
| * <ul> |
| * <li> boolean: one of {true, false} (case-sensitive) |
| * <li> byte, short, int, long: a valid Java integer constant |
| * (including non-base-10 constants like 0xabc and 074) |
| * whose value does not overflow the type. NOTE: these are |
| * interpreted as Java integer values, so they are all signed. |
| * <li> float, double: a valid Java floating-point constant. |
| * If the type is float, the value must fit in 32 bits. |
| * <li> String: a double-quoted string value, or the word {@code null}. |
| * NOTE: the contents of the string must be 7-bit clean ASCII; |
| * C-style octal escapes are recognized, but Unicode escapes are not. |
| * </ul> |
| * <p> |
| * Passing a property-name to {@code unset()} will unset the property, |
| * removing its value and type information, as if it had never been |
| * defined. |
| * </blockquote> |
| * |
| * @param r The Reader to load properties from |
| * @throws IOException if an error occurs when reading the data |
| * @throws IllegalArgumentException if the data is malformed |
| */ |
| public void load(Reader r) throws IOException { |
| parse(r, this); |
| } |
| |
| @Override |
| public Object get(Object key) { |
| Object value = super.get(key); |
| if (value == NULL_STRING) { |
| return null; |
| } |
| return value; |
| } |
| |
| /* |
| * Getters with explicit defaults |
| */ |
| |
| /** |
| * An unchecked exception that is thrown if a {@code get<TYPE>()} method |
| * is used to retrieve a parameter whose type does not match the method name. |
| */ |
| public static class TypeException extends IllegalArgumentException { |
| TypeException(String property, Object value, String requestedType) { |
| super(property + " has type " + value.getClass().getName() + |
| ", not " + requestedType); |
| } |
| } |
| |
| /** |
| * Returns the value of a boolean property, or the default if the property |
| * has not been defined. |
| * |
| * @param property The name of the property to return |
| * @param def The default value to return if the property is not set |
| * @return the value of the property |
| * @throws TypeException if the property is set and is not a boolean |
| */ |
| public boolean getBoolean(String property, boolean def) { |
| Object value = super.get(property); |
| if (value == null) { |
| return def; |
| } |
| if (value instanceof Boolean) { |
| return ((Boolean)value).booleanValue(); |
| } |
| throw new TypeException(property, value, "boolean"); |
| } |
| |
| /** |
| * Returns the value of a byte property, or the default if the property |
| * has not been defined. |
| * |
| * @param property The name of the property to return |
| * @param def The default value to return if the property is not set |
| * @return the value of the property |
| * @throws TypeException if the property is set and is not a byte |
| */ |
| public byte getByte(String property, byte def) { |
| Object value = super.get(property); |
| if (value == null) { |
| return def; |
| } |
| if (value instanceof Byte) { |
| return ((Byte)value).byteValue(); |
| } |
| throw new TypeException(property, value, "byte"); |
| } |
| |
| /** |
| * Returns the value of a short property, or the default if the property |
| * has not been defined. |
| * |
| * @param property The name of the property to return |
| * @param def The default value to return if the property is not set |
| * @return the value of the property |
| * @throws TypeException if the property is set and is not a short |
| */ |
| public short getShort(String property, short def) { |
| Object value = super.get(property); |
| if (value == null) { |
| return def; |
| } |
| if (value instanceof Short) { |
| return ((Short)value).shortValue(); |
| } |
| throw new TypeException(property, value, "short"); |
| } |
| |
| /** |
| * Returns the value of an integer property, or the default if the property |
| * has not been defined. |
| * |
| * @param property The name of the property to return |
| * @param def The default value to return if the property is not set |
| * @return the value of the property |
| * @throws TypeException if the property is set and is not an integer |
| */ |
| public int getInt(String property, int def) { |
| Object value = super.get(property); |
| if (value == null) { |
| return def; |
| } |
| if (value instanceof Integer) { |
| return ((Integer)value).intValue(); |
| } |
| throw new TypeException(property, value, "int"); |
| } |
| |
| /** |
| * Returns the value of a long property, or the default if the property |
| * has not been defined. |
| * |
| * @param property The name of the property to return |
| * @param def The default value to return if the property is not set |
| * @return the value of the property |
| * @throws TypeException if the property is set and is not a long |
| */ |
| public long getLong(String property, long def) { |
| Object value = super.get(property); |
| if (value == null) { |
| return def; |
| } |
| if (value instanceof Long) { |
| return ((Long)value).longValue(); |
| } |
| throw new TypeException(property, value, "long"); |
| } |
| |
| /** |
| * Returns the value of a float property, or the default if the property |
| * has not been defined. |
| * |
| * @param property The name of the property to return |
| * @param def The default value to return if the property is not set |
| * @return the value of the property |
| * @throws TypeException if the property is set and is not a float |
| */ |
| public float getFloat(String property, float def) { |
| Object value = super.get(property); |
| if (value == null) { |
| return def; |
| } |
| if (value instanceof Float) { |
| return ((Float)value).floatValue(); |
| } |
| throw new TypeException(property, value, "float"); |
| } |
| |
| /** |
| * Returns the value of a double property, or the default if the property |
| * has not been defined. |
| * |
| * @param property The name of the property to return |
| * @param def The default value to return if the property is not set |
| * @return the value of the property |
| * @throws TypeException if the property is set and is not a double |
| */ |
| public double getDouble(String property, double def) { |
| Object value = super.get(property); |
| if (value == null) { |
| return def; |
| } |
| if (value instanceof Double) { |
| return ((Double)value).doubleValue(); |
| } |
| throw new TypeException(property, value, "double"); |
| } |
| |
| /** |
| * Returns the value of a string property, or the default if the property |
| * has not been defined. |
| * |
| * @param property The name of the property to return |
| * @param def The default value to return if the property is not set |
| * @return the value of the property |
| * @throws TypeException if the property is set and is not a string |
| */ |
| public String getString(String property, String def) { |
| Object value = super.get(property); |
| if (value == null) { |
| return def; |
| } |
| if (value == NULL_STRING) { |
| return null; |
| } else if (value instanceof String) { |
| return (String)value; |
| } |
| throw new TypeException(property, value, "string"); |
| } |
| |
| /* |
| * Getters with implicit defaults |
| */ |
| |
| /** |
| * Returns the value of a boolean property, or false |
| * if the property has not been defined. |
| * |
| * @param property The name of the property to return |
| * @return the value of the property |
| * @throws TypeException if the property is set and is not a boolean |
| */ |
| public boolean getBoolean(String property) { |
| return getBoolean(property, false); |
| } |
| |
| /** |
| * Returns the value of a byte property, or 0 |
| * if the property has not been defined. |
| * |
| * @param property The name of the property to return |
| * @return the value of the property |
| * @throws TypeException if the property is set and is not a byte |
| */ |
| public byte getByte(String property) { |
| return getByte(property, (byte)0); |
| } |
| |
| /** |
| * Returns the value of a short property, or 0 |
| * if the property has not been defined. |
| * |
| * @param property The name of the property to return |
| * @return the value of the property |
| * @throws TypeException if the property is set and is not a short |
| */ |
| public short getShort(String property) { |
| return getShort(property, (short)0); |
| } |
| |
| /** |
| * Returns the value of an integer property, or 0 |
| * if the property has not been defined. |
| * |
| * @param property The name of the property to return |
| * @return the value of the property |
| * @throws TypeException if the property is set and is not an integer |
| */ |
| public int getInt(String property) { |
| return getInt(property, 0); |
| } |
| |
| /** |
| * Returns the value of a long property, or 0 |
| * if the property has not been defined. |
| * |
| * @param property The name of the property to return |
| * @return the value of the property |
| * @throws TypeException if the property is set and is not a long |
| */ |
| public long getLong(String property) { |
| return getLong(property, 0L); |
| } |
| |
| /** |
| * Returns the value of a float property, or 0.0 |
| * if the property has not been defined. |
| * |
| * @param property The name of the property to return |
| * @return the value of the property |
| * @throws TypeException if the property is set and is not a float |
| */ |
| public float getFloat(String property) { |
| return getFloat(property, 0.0f); |
| } |
| |
| /** |
| * Returns the value of a double property, or 0.0 |
| * if the property has not been defined. |
| * |
| * @param property The name of the property to return |
| * @return the value of the property |
| * @throws TypeException if the property is set and is not a double |
| */ |
| public double getDouble(String property) { |
| return getDouble(property, 0.0); |
| } |
| |
| /** |
| * Returns the value of a String property, or "" |
| * if the property has not been defined. |
| * |
| * @param property The name of the property to return |
| * @return the value of the property |
| * @throws TypeException if the property is set and is not a string |
| */ |
| public String getString(String property) { |
| return getString(property, ""); |
| } |
| |
| // Values returned by getStringInfo() |
| public static final int STRING_TYPE_MISMATCH = -2; |
| public static final int STRING_NOT_SET = -1; |
| public static final int STRING_NULL = 0; |
| public static final int STRING_SET = 1; |
| |
| /** |
| * Provides string type information about a property. |
| * |
| * @param property the property to check |
| * @return STRING_SET if the property is a string and is non-null. |
| * STRING_NULL if the property is a string and is null. |
| * STRING_NOT_SET if the property is not set (no type or value). |
| * STRING_TYPE_MISMATCH if the property is set but is not a string. |
| */ |
| public int getStringInfo(String property) { |
| Object value = super.get(property); |
| if (value == null) { |
| return STRING_NOT_SET; |
| } |
| if (value == NULL_STRING) { |
| return STRING_NULL; |
| } else if (value instanceof String) { |
| return STRING_SET; |
| } |
| return STRING_TYPE_MISMATCH; |
| } |
| } |