Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||||||
PropertyListParser |
|
| 4.166666666666667;4.167 |
1 | /* |
|
2 | Wotonomy: OpenStep design patterns for pure Java applications. |
|
3 | Copyright (C) 2000 Blacksmith, Inc. |
|
4 | ||
5 | This library is free software; you can redistribute it and/or |
|
6 | modify it under the terms of the GNU Lesser General Public |
|
7 | License as published by the Free Software Foundation; either |
|
8 | version 2.1 of the License, or (at your option) any later version. |
|
9 | ||
10 | This library is distributed in the hope that it will be useful, |
|
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
|
13 | Lesser General Public License for more details. |
|
14 | ||
15 | You should have received a copy of the GNU Lesser General Public |
|
16 | License along with this library; if not, see http://www.gnu.org |
|
17 | */ |
|
18 | ||
19 | package net.wotonomy.foundation.internal; |
|
20 | ||
21 | import java.util.*; //collections |
|
22 | import java.io.*; |
|
23 | ||
24 | /** |
|
25 | * PropertyListParser can parse a property list (plist) file or string, and |
|
26 | * return the top-level object represented by the plist. <p> |
|
27 | * |
|
28 | * A property list is a heirarchical data structure containing only Maps, |
|
29 | * Lists, and Strings -- nothing else. In other words, a property list is |
|
30 | * either a Map, List, or String instance, with the restrictions that the |
|
31 | * collections may only contain Map, List, or String instances. <p> |
|
32 | * |
|
33 | * This class can read a particularly-formatted string or file, and create |
|
34 | * the property list structure described. It provides a convenient means |
|
35 | * for having a structured data file, letting programs simply deal with the |
|
36 | * structure rather than having to do a lot of string parsing work as well. |
|
37 | * The concept is similar to Properties files, except that the values can |
|
38 | * be nested Maps or Lists instead of only Strings. <p> |
|
39 | * |
|
40 | * A Map is specified in a file by key/value pairs surrounded by brace |
|
41 | * characters. An equal sign (=) must be between the key and value, and |
|
42 | * there must be a semicolon (;) following the value. |
|
43 | * |
|
44 | * <pre> |
|
45 | * { |
|
46 | * key1 = value1; |
|
47 | * key2 = value2; |
|
48 | * etc... |
|
49 | * } |
|
50 | * </pre> |
|
51 | * |
|
52 | * A List is specified by a comma-separated list of values surrounded by parentheses, like: |
|
53 | * <pre> |
|
54 | * ( value1, value2, value3, etc... ) |
|
55 | * </pre> |
|
56 | * |
|
57 | * A String can either be quoted in the manner of a constant string in |
|
58 | * Java, or unquoted. If unquoted, the string can only contain |
|
59 | * alphanumerics, underscores (_), periods (.), dollar signs ($), colons |
|
60 | * (:), or forward slashes (/). If any other character appears in the |
|
61 | * string, it must be quoted (i.e., surrounded by " characters). |
|
62 | * Quoted strings may also contain \n, \t, \f, \v, \b, and \a escapes, |
|
63 | * octal escapes of the form \000, and unicode escapes of the form of \U |
|
64 | * followed by four hexadecimal characters. Any other character escaped |
|
65 | * by a backslash will be treated as that character, and the escaping |
|
66 | * backslash character will be omitted. Thus, to represent an actual |
|
67 | * backslash, it must appear as \\ in the quoted string. <p> |
|
68 | * |
|
69 | * All whitespace between elements is ignored, and both //-style and |
|
70 | * /*-style comments are allowed to appear anywhere between elements. <p> |
|
71 | * |
|
72 | * If there are any syntax errors encountered while parsing, |
|
73 | * RuntimeExceptions are thrown with the line number and column of the |
|
74 | * problem. <p> |
|
75 | * |
|
76 | * Currenty, HashMaps and ArrayLists are the actual Map and List classes |
|
77 | * used when creating the property list. <p> |
|
78 | * |
|
79 | * Examples: <p><blockquote> |
|
80 | <pre> |
|
81 | // This plist file represents a Map, since it starts with a '{'. |
|
82 | { |
|
83 | Map1 = { subkey1 = "foo"; }; |
|
84 | Map2 = |
|
85 | { |
|
86 | "key1" = "This is a quoted string."; |
|
87 | "key 2" = "bar\nbaz"; // the value has a newline in it |
|
88 | key3 = ("a", b, c, "quux quux"); // a List of four Strings |
|
89 | }; // We need a semicolon here, since it's following the value of the "Map2" key |
|
90 | ||
91 | List1 = (foobar,foobaz,"foo,baz", (aa, ab, ac)); // a List of 3 Strings and a List |
|
92 | ||
93 | // And now a List of two Maps |
|
94 | List2 = ( |
|
95 | { |
|
96 | key1 = value1; |
|
97 | key2 = "value 2"; |
|
98 | key3 = (a,b,c,d); |
|
99 | key4 = (); |
|
100 | }, // We need the comma here |
|
101 | { |
|
102 | key1 = {}; // an empty Map |
|
103 | key2 = "another String value"; |
|
104 | } |
|
105 | ); |
|
106 | } |
|
107 | </pre> |
|
108 | </blockquote> |
|
109 | * For those wondering, this is essentially a re-implementation of |
|
110 | * NeXT/Apple's property lists, except that data values are not supported. |
|
111 | * |
|
112 | * @author clindberg@blacksmith.com |
|
113 | * @version $Revision: 899 $ |
|
114 | */ |
|
115 | ||
116 | 0 | public class PropertyListParser |
117 | { |
|
118 | private char buffer[]; |
|
119 | private int currIndex; |
|
120 | private int lineNumber; |
|
121 | private int currLineStartIndex; |
|
122 | ||
123 | /** Reads an object (String, List, or Map) from plistString and returns it. |
|
124 | * RuntimeExceptions are raised if there are parse problems. |
|
125 | */ |
|
126 | public static Object propertyListFromString(String plistString) |
|
127 | { |
|
128 | 0 | PropertyListParser parser = new PropertyListParser(plistString); |
129 | 0 | return parser.readTopLevelObject(); |
130 | } |
|
131 | ||
132 | /** |
|
133 | * Reads all remaining characters from the Reader, and returns the |
|
134 | * result of propertyListFromString(). RuntimeExceptions are raised if |
|
135 | * there are parse problems |
|
136 | */ |
|
137 | public static Object propertyListFromReader(Reader reader) throws IOException |
|
138 | { |
|
139 | 0 | char charBuffer[] = new char[2048]; |
140 | 0 | StringBuffer stringBuffer = new StringBuffer(); |
141 | 0 | int numRead = 0; |
142 | ||
143 | 0 | while (numRead >= 0) |
144 | { |
|
145 | 0 | numRead = reader.read(charBuffer); |
146 | 0 | if (numRead > 0) stringBuffer.append(charBuffer, 0, numRead); |
147 | } |
|
148 | ||
149 | 0 | return propertyListFromString(stringBuffer.toString()); |
150 | } |
|
151 | ||
152 | /** |
|
153 | * Reads the contents of the specified file, and parses the contents. |
|
154 | * If any error occurs, prints out a message using System.out.println() |
|
155 | * and returns null. |
|
156 | */ |
|
157 | public static Object propertyListFromFile(String filename) |
|
158 | { |
|
159 | try { |
|
160 | 0 | FileInputStream stream = new FileInputStream(filename); |
161 | 0 | return propertyListFromReader(new InputStreamReader(stream)); |
162 | 0 | } catch (Exception exception) { |
163 | 0 | String errorMessage = exception.getMessage(); |
164 | 0 | System.out.println("Error parsing property list from "+filename+": "+errorMessage); |
165 | } |
|
166 | ||
167 | 0 | return null; |
168 | } |
|
169 | ||
170 | /** |
|
171 | * Creates a new PropertyListParser to parse the contents of the |
|
172 | * specified String. |
|
173 | */ |
|
174 | public PropertyListParser(String plistString) |
|
175 | { |
|
176 | 0 | this(plistString.toCharArray()); |
177 | 0 | } |
178 | ||
179 | /** |
|
180 | * Creates a new PropertyListParser to parse the specified char array. |
|
181 | */ |
|
182 | 0 | public PropertyListParser(char[] charArray) |
183 | 0 | { |
184 | 0 | buffer = charArray; |
185 | 0 | lineNumber = 1; |
186 | 0 | currLineStartIndex = 1; |
187 | 0 | currIndex = 0; |
188 | 0 | } |
189 | ||
190 | public Object readTopLevelObject() |
|
191 | { |
|
192 | 0 | Object plist = readObject(); |
193 | ||
194 | 0 | skipCommentWhitespace(); |
195 | 0 | if (!isAtEnd()) |
196 | { |
|
197 | 0 | throwParseException("Extra characters in plist string after parsing object. A plist should only contain one top-level object."); |
198 | } |
|
199 | ||
200 | 0 | return plist; |
201 | } |
|
202 | ||
203 | private void throwParseException(String errorMessage) |
|
204 | { |
|
205 | 0 | int column = currIndex - currLineStartIndex + 1; |
206 | 0 | throw new RuntimeException(errorMessage + " (Line " + lineNumber + ", column " + column + ")"); |
207 | } |
|
208 | ||
209 | private void updateLineNumberWithIndex(int lineStartIndex) |
|
210 | { |
|
211 | 0 | lineNumber++; |
212 | 0 | currLineStartIndex = lineStartIndex; |
213 | 0 | } |
214 | ||
215 | private boolean isAtEnd() |
|
216 | { |
|
217 | 0 | return currIndex >= buffer.length; |
218 | } |
|
219 | ||
220 | private void skipDoubleslashComment() |
|
221 | { |
|
222 | 0 | while (!isAtEnd() && buffer[currIndex] != '\n') { |
223 | 0 | currIndex++; |
224 | 0 | } |
225 | 0 | } |
226 | ||
227 | private void skipStandardCComment() |
|
228 | { |
|
229 | 0 | currIndex++; //skip over the starting '/' |
230 | ||
231 | 0 | while (!isAtEnd()) |
232 | { |
|
233 | 0 | if (buffer[currIndex] == '\n') |
234 | 0 | updateLineNumberWithIndex(currIndex+1); |
235 | ||
236 | 0 | currIndex++; |
237 | ||
238 | 0 | if (buffer[currIndex-2] == '*' && buffer[currIndex-1] == '/') |
239 | { |
|
240 | 0 | return; |
241 | } |
|
242 | } |
|
243 | ||
244 | 0 | throwParseException("Input exhausted while parsing comment"); |
245 | 0 | } |
246 | ||
247 | private void skipWhitespace() |
|
248 | { |
|
249 | 0 | while (!isAtEnd() && isWhitespace(buffer[currIndex])) |
250 | { |
|
251 | 0 | if (buffer[currIndex] == '\n') |
252 | 0 | updateLineNumberWithIndex(currIndex+1); |
253 | 0 | currIndex++; |
254 | 0 | } |
255 | 0 | } |
256 | ||
257 | private void skipCommentWhitespace() |
|
258 | { |
|
259 | 0 | boolean done = false; |
260 | ||
261 | 0 | while (!done) |
262 | { |
|
263 | 0 | done = true; |
264 | ||
265 | 0 | skipWhitespace(); |
266 | 0 | if ((buffer.length - currIndex) > 1 && buffer[currIndex] == '/') |
267 | { |
|
268 | 0 | if (buffer[currIndex+1] == '/') { |
269 | 0 | done = false; //iterate again |
270 | 0 | skipDoubleslashComment(); |
271 | 0 | } |
272 | 0 | else if (buffer[currIndex+1] == '*') { |
273 | 0 | done = false; //iterate again |
274 | 0 | skipStandardCComment(); |
275 | 0 | } |
276 | } |
|
277 | } |
|
278 | 0 | } |
279 | ||
280 | private Object readObject() |
|
281 | { |
|
282 | 0 | skipCommentWhitespace(); |
283 | 0 | if (isAtEnd()) return null; |
284 | ||
285 | // Data (i.e. byte[]) not supported |
|
286 | 0 | if (buffer[currIndex] == '"') |
287 | 0 | return readQuotedString(); |
288 | 0 | if (buffer[currIndex] == '(') |
289 | 0 | return readList(); |
290 | 0 | if (buffer[currIndex] == '{') |
291 | 0 | return readMap(); |
292 | ||
293 | 0 | return readUnquotedString(); |
294 | } |
|
295 | ||
296 | private static final byte valueForHexDigit(char c) |
|
297 | { |
|
298 | 0 | if(c >= '0' && c <= '9') return (byte)(c - '0'); |
299 | 0 | if(c >= 'a' && c <= 'f') return (byte)((c - 'a') + 10); |
300 | 0 | if(c >= 'A' && c <= 'F') return (byte)((c - 'A') + 10); |
301 | ||
302 | 0 | return 0; |
303 | } |
|
304 | ||
305 | private static final boolean isOctalDigit(char c) |
|
306 | { |
|
307 | 0 | return c >= '0' && c <= '7'; |
308 | } |
|
309 | ||
310 | private static final boolean isHexDigit(char c) |
|
311 | { |
|
312 | 0 | return (c >= '0' && c <= '9') || |
313 | 0 | (c >= 'a' && c <= 'f') || |
314 | 0 | (c >= 'A' && c <= 'F'); |
315 | } |
|
316 | ||
317 | 0 | private static String unquotedStringChars = "._$:/"; // chars allowed in unquoted strings |
318 | 0 | private static String whitespaceChars = " \t\n\r\f"; |
319 | ||
320 | private static final boolean isWhitespace(char c) |
|
321 | { |
|
322 | 0 | return whitespaceChars.indexOf(c) >= 0; |
323 | } |
|
324 | ||
325 | private static final boolean isValidUnquotedStringChar(char c) |
|
326 | { |
|
327 | 0 | return ((c >= 'a' && c <= 'z') || |
328 | 0 | (c >= 'A' && c <= 'Z') || |
329 | 0 | (c >= '0' && c <= '9') || |
330 | 0 | unquotedStringChars.indexOf(c) >= 0); |
331 | } |
|
332 | ||
333 | private String readUnquotedString() |
|
334 | { |
|
335 | 0 | int startIndex = currIndex; |
336 | ||
337 | 0 | while (!isAtEnd() && isValidUnquotedStringChar(buffer[currIndex])) |
338 | 0 | currIndex++; |
339 | ||
340 | 0 | if (startIndex == currIndex) |
341 | 0 | throwParseException("No allowable characters found to parse unquoted string"); |
342 | ||
343 | 0 | return new String(buffer, startIndex, currIndex - startIndex); |
344 | } |
|
345 | ||
346 | private String readQuotedString() |
|
347 | { |
|
348 | 0 | currIndex++; //skip over '"' |
349 | ||
350 | 0 | StringBuffer stringBuffer = new StringBuffer(); |
351 | 0 | int startIndex = currIndex; |
352 | ||
353 | 0 | while (!isAtEnd() && buffer[currIndex] != '"') |
354 | { |
|
355 | 0 | if (buffer[currIndex] != '\\') |
356 | { |
|
357 | 0 | if (buffer[currIndex] == '\n') |
358 | 0 | updateLineNumberWithIndex(currIndex+1); |
359 | ||
360 | /* |
|
361 | * Just increment the index -- all these characters will be |
|
362 | * appended in chunks, either before an escape sequence or |
|
363 | * at the end. |
|
364 | */ |
|
365 | 0 | currIndex++; |
366 | 0 | } |
367 | else // it's an escape |
|
368 | { |
|
369 | /* Append anything scanned past before the '\\' */ |
|
370 | 0 | if (startIndex < currIndex) |
371 | 0 | stringBuffer.append(buffer, startIndex, currIndex - startIndex); |
372 | 0 | currIndex++; // skip over '\\' |
373 | ||
374 | 0 | if (isAtEnd()) |
375 | 0 | throwParseException("Input exhausted while parsing escape sequence"); |
376 | ||
377 | 0 | switch (buffer[currIndex]) |
378 | { |
|
379 | 0 | case 't': stringBuffer.append('\t'); currIndex++; break; // tab |
380 | 0 | case 'n': stringBuffer.append('\n'); currIndex++; break; // newline |
381 | 0 | case 'r': stringBuffer.append('\r'); currIndex++; break; // carriage return |
382 | 0 | case 'f': stringBuffer.append('\f'); currIndex++; break; // form feed |
383 | 0 | case 'b': stringBuffer.append('\b'); currIndex++; break; // backspace |
384 | 0 | case 'a': stringBuffer.append('\007'); currIndex++; break; // bell |
385 | 0 | case 'v': stringBuffer.append('\013'); currIndex++; break; // vertical tab |
386 | case 'U': |
|
387 | case 'u': |
|
388 | { |
|
389 | /* A Unicode escape. Always followed by 4 hex digits. */ |
|
390 | 0 | currIndex++; // skip past the 'U' |
391 | 0 | if ((currIndex+4) > buffer.length) |
392 | 0 | throwParseException("Not enough chars to parse \\U sequence"); |
393 | ||
394 | 0 | if(!isHexDigit(buffer[currIndex]) || !isHexDigit(buffer[currIndex+1]) || |
395 | 0 | !isHexDigit(buffer[currIndex+2]) || !isHexDigit(buffer[currIndex+3])) |
396 | { |
|
397 | 0 | throwParseException("Four hex digits not found for \\U sequence"); |
398 | } |
|
399 | ||
400 | 0 | byte byte3 = valueForHexDigit(buffer[currIndex]); |
401 | 0 | byte byte2 = valueForHexDigit(buffer[currIndex+1]); |
402 | 0 | byte byte1 = valueForHexDigit(buffer[currIndex+2]); |
403 | 0 | byte byte0 = valueForHexDigit(buffer[currIndex+3]); |
404 | 0 | char theChar = (char)((byte3 << 12) + (byte2 << 8) + (byte1 << 4) + byte0); |
405 | 0 | stringBuffer.append(theChar); |
406 | 0 | currIndex += 4; |
407 | 0 | break; |
408 | } |
|
409 | case '0': case '1': case '2': case '3': |
|
410 | case '4': case '5': case '6': case '7': |
|
411 | { |
|
412 | /* An octal escape. Expect 1, 2, or 3 octal digits. */ |
|
413 | 0 | int digits = 0; |
414 | 0 | int value = 0; |
415 | ||
416 | 0 | do { |
417 | 0 | value *= 8; |
418 | 0 | value += (int)(buffer[currIndex] - '0'); |
419 | 0 | currIndex++; |
420 | 0 | digits++; |
421 | 0 | } while (digits <= 3 && !isAtEnd() && isOctalDigit(buffer[currIndex])); |
422 | ||
423 | 0 | if (value > 255) |
424 | 0 | throwParseException("Value too large in octal escape sequence (> 0377)"); |
425 | ||
426 | // This assumes value is in ISO Latin 1 encoding |
|
427 | 0 | stringBuffer.append((char)value); |
428 | 0 | break; |
429 | } |
|
430 | /* I guess plists can't have the \x{HEX}{HEX} escapes */ |
|
431 | default: |
|
432 | { |
|
433 | // Unknown escape sequence, just add the character. |
|
434 | // GCC warns if this isn't a '"', '\'', or '\\'... |
|
435 | 0 | stringBuffer.append(buffer[currIndex]); |
436 | 0 | if (buffer[currIndex] == '\n') |
437 | 0 | updateLineNumberWithIndex(currIndex+1); |
438 | 0 | currIndex++; |
439 | break; |
|
440 | } |
|
441 | } // end case |
|
442 | ||
443 | /* Reset startIndex, so a verbatim copy will now start from this index */ |
|
444 | 0 | startIndex = currIndex; |
445 | ||
446 | } //end '\\' escape |
|
447 | 0 | } |
448 | ||
449 | 0 | if (isAtEnd()) |
450 | 0 | throwParseException("Input exhausted while parsing quoted string"); |
451 | 0 | if (startIndex < currIndex) |
452 | 0 | stringBuffer.append(buffer, startIndex, currIndex - startIndex); |
453 | 0 | currIndex++; //skip past '"' |
454 | ||
455 | 0 | return stringBuffer.toString(); |
456 | } |
|
457 | ||
458 | private List readList() |
|
459 | { |
|
460 | 0 | List newList = new ArrayList(); |
461 | ||
462 | 0 | currIndex++; //skip over '(' |
463 | 0 | skipCommentWhitespace(); |
464 | 0 | while (!isAtEnd() && buffer[currIndex] != ')') |
465 | { |
|
466 | /* A comma is required between list elements */ |
|
467 | 0 | if (newList.size() > 0) |
468 | { |
|
469 | 0 | if (buffer[currIndex] != ',') |
470 | 0 | throwParseException("List parsing failed: expecting ','"); |
471 | 0 | currIndex++; |
472 | 0 | skipCommentWhitespace(); |
473 | 0 | if (isAtEnd()) |
474 | 0 | throwParseException("Input exhausted while parsing list"); |
475 | } |
|
476 | ||
477 | 0 | if (buffer[currIndex] != ')') |
478 | { |
|
479 | 0 | Object plistObject = readObject(); |
480 | 0 | if (plistObject == null) |
481 | 0 | throwParseException("List parsing failed: could not read contained object."); |
482 | 0 | newList.add(plistObject); |
483 | 0 | skipCommentWhitespace(); |
484 | 0 | } |
485 | } |
|
486 | ||
487 | 0 | if (isAtEnd()) |
488 | 0 | throwParseException("Input exhausted while parsing list"); |
489 | 0 | currIndex++; //skip past ')' |
490 | ||
491 | 0 | return newList; |
492 | } |
|
493 | ||
494 | private Map readMap() |
|
495 | { |
|
496 | 0 | HashMap newMap = new HashMap(); |
497 | ||
498 | 0 | currIndex++; // skip over open brace |
499 | 0 | skipCommentWhitespace(); |
500 | ||
501 | 0 | while (!isAtEnd() && buffer[currIndex] != '}') |
502 | { |
|
503 | Object key; |
|
504 | Object value; |
|
505 | ||
506 | 0 | key = readObject(); |
507 | 0 | if (key == null || !(key instanceof String)) |
508 | 0 | throwParseException("Map parsing failed: could not parse key or key is not a String"); |
509 | ||
510 | 0 | skipCommentWhitespace(); |
511 | 0 | if (isAtEnd() || buffer[currIndex] != '=') |
512 | 0 | throwParseException("Map parsing failed: expecting '='"); |
513 | 0 | currIndex++; //skip over '=' |
514 | 0 | skipCommentWhitespace(); |
515 | 0 | if (isAtEnd()) |
516 | 0 | throwParseException("Input exhausted while parsing map"); |
517 | ||
518 | 0 | value = readObject(); |
519 | 0 | if (value == null) |
520 | 0 | throwParseException("Map parsing failed: could not parse value object"); |
521 | ||
522 | 0 | skipCommentWhitespace(); |
523 | 0 | if (isAtEnd() || buffer[currIndex] != ';') |
524 | 0 | throwParseException("Map parsing failed: expecting ';'"); |
525 | 0 | currIndex++; //skip over ';' |
526 | 0 | skipCommentWhitespace(); |
527 | ||
528 | 0 | newMap.put(key, value); |
529 | 0 | } |
530 | ||
531 | 0 | if (isAtEnd()) |
532 | 0 | throwParseException("Input exhausted while parsing map"); |
533 | 0 | currIndex++; //skip past '}' |
534 | ||
535 | 0 | return newMap; |
536 | } |
|
537 | ||
538 | ||
539 | public static void main(String[] args) |
|
540 | { |
|
541 | 0 | String filename = args[0]; |
542 | 0 | Object plist = PropertyListParser.propertyListFromFile(filename); |
543 | 0 | System.out.println(plist); |
544 | 0 | } |
545 | } |
|
546 |