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      */
initTokenizer(Reader r)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 {
ParseException(StreamTokenizer state, String expected)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      */
interpretType(String typeName)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      */
parse(Reader r, Map<String, Object> map)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      */
parseValue(StreamTokenizer st, final int type)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      */
TypedProperties()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      */
load(Reader r)397     public void load(Reader r) throws IOException {
398         parse(r, this);
399     }
400 
401     @Override
get(Object key)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 {
TypeException(String property, Object value, String requestedType)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      */
getBoolean(String property, boolean def)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      */
getByte(String property, byte def)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      */
getShort(String property, short def)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      */
getInt(String property, int def)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      */
getLong(String property, long def)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      */
getFloat(String property, float def)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      */
getDouble(String property, double def)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      */
getString(String property, String def)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      */
getBoolean(String property)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      */
getByte(String property)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      */
getShort(String property)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      */
getInt(String property)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      */
getLong(String property)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      */
getFloat(String property)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      */
getDouble(String property)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      */
getString(String property)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      */
getStringInfo(String property)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