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 &gt; 0 if an EOL token was seen, &lt; 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     *     &lt;property-name&gt; &lt;type&gt; = &lt;value&gt;
368     *     &lt;property-name&gt; 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     *     &lt;property-name&gt; is a valid fully-qualified class name
381     *     (one or more valid identifiers separated by dot characters).
382     *
383     *     &lt;type&gt; is one of {boolean, byte, short, int, long,
384     *     float, double, string}, and is case-insensitive.
385     *
386     *     &lt;value&gt; 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&lt;TYPE&gt;()} 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