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: [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