1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.apache.commons.configuration.plist;
19
20 import java.io.File;
21 import java.io.PrintWriter;
22 import java.io.Reader;
23 import java.io.Writer;
24 import java.net.URL;
25 import java.util.ArrayList;
26 import java.util.Calendar;
27 import java.util.Date;
28 import java.util.Iterator;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.TimeZone;
32
33 import org.apache.commons.codec.binary.Hex;
34 import org.apache.commons.configuration.AbstractHierarchicalFileConfiguration;
35 import org.apache.commons.configuration.Configuration;
36 import org.apache.commons.configuration.ConfigurationException;
37 import org.apache.commons.configuration.HierarchicalConfiguration;
38 import org.apache.commons.configuration.MapConfiguration;
39 import org.apache.commons.lang.StringUtils;
40
41 /***
42 * NeXT / OpenStep style configuration. This configuration can read and write
43 * ASCII plist files. It support the GNUStep extension to specify date objects.
44 * <p>
45 * References:
46 * <ul>
47 * <li><a
48 * href="http://developer.apple.com/documentation/Cocoa/Conceptual/PropertyLists/Articles/OldStylePListsConcept.html">
49 * Apple Documentation - Old-Style ASCII Property Lists</a></li>
50 * <li><a
51 * href="http://www.gnustep.org/resources/documentation/Developer/Base/Reference/NSPropertyList.html">
52 * GNUStep Documentation</a></li>
53 * </ul>
54 *
55 * <p>Example:</p>
56 * <pre>
57 * {
58 * foo = "bar";
59 *
60 * array = ( value1, value2, value3 );
61 *
62 * data = <4f3e0145ab>;
63 *
64 * date = <*D2007-05-05 20:05:00 +0100>;
65 *
66 * nested =
67 * {
68 * key1 = value1;
69 * key2 = value;
70 * nested =
71 * {
72 * foo = bar
73 * }
74 * }
75 * }
76 * </pre>
77 *
78 * @since 1.2
79 *
80 * @author Emmanuel Bourg
81 * @version $Revision: 590474 $, $Date: 2007-10-30 22:35:11 +0100 (Di, 30 Okt 2007) $
82 */
83 public class PropertyListConfiguration extends AbstractHierarchicalFileConfiguration
84 {
85 /*** Constant for the separator parser for the date part. */
86 private static final DateComponentParser DATE_SEPARATOR_PARSER = new DateSeparatorParser(
87 "-");
88
89 /*** Constant for the separator parser for the time part. */
90 private static final DateComponentParser TIME_SEPARATOR_PARSER = new DateSeparatorParser(
91 ":");
92
93 /*** Constant for the separator parser for blanks between the parts. */
94 private static final DateComponentParser BLANK_SEPARATOR_PARSER = new DateSeparatorParser(
95 " ");
96
97 /*** An array with the component parsers for dealing with dates. */
98 private static final DateComponentParser[] DATE_PARSERS =
99 {new DateSeparatorParser("<*D"), new DateFieldParser(Calendar.YEAR, 4),
100 DATE_SEPARATOR_PARSER, new DateFieldParser(Calendar.MONTH, 2, 1),
101 DATE_SEPARATOR_PARSER, new DateFieldParser(Calendar.DATE, 2),
102 BLANK_SEPARATOR_PARSER,
103 new DateFieldParser(Calendar.HOUR_OF_DAY, 2),
104 TIME_SEPARATOR_PARSER, new DateFieldParser(Calendar.MINUTE, 2),
105 TIME_SEPARATOR_PARSER, new DateFieldParser(Calendar.SECOND, 2),
106 BLANK_SEPARATOR_PARSER, new DateTimeZoneParser(),
107 new DateSeparatorParser(">")};
108
109 /*** Constant for the ID prefix for GMT time zones. */
110 private static final String TIME_ZONE_PREFIX = "GMT";
111
112 /*** The serial version UID. */
113 private static final long serialVersionUID = 3227248503779092127L;
114
115 /*** Constant for the milliseconds of a minute.*/
116 private static final int MILLIS_PER_MINUTE = 1000 * 60;
117
118 /*** Constant for the minutes per hour.*/
119 private static final int MINUTES_PER_HOUR = 60;
120
121 /*** Size of the indentation for the generated file. */
122 private static final int INDENT_SIZE = 4;
123
124 /*** Constant for the length of a time zone.*/
125 private static final int TIME_ZONE_LENGTH = 5;
126
127 /*** Constant for the padding character in the date format.*/
128 private static final char PAD_CHAR = '0';
129
130 /***
131 * Creates an empty PropertyListConfiguration object which can be
132 * used to synthesize a new plist file by adding values and
133 * then saving().
134 */
135 public PropertyListConfiguration()
136 {
137 }
138
139 /***
140 * Creates a new instance of <code>PropertyListConfiguration</code> and
141 * copies the content of the specified configuration into this object.
142 *
143 * @param c the configuration to copy
144 * @since 1.4
145 */
146 public PropertyListConfiguration(HierarchicalConfiguration c)
147 {
148 super(c);
149 }
150
151 /***
152 * Creates and loads the property list from the specified file.
153 *
154 * @param fileName The name of the plist file to load.
155 * @throws ConfigurationException Error while loading the plist file
156 */
157 public PropertyListConfiguration(String fileName) throws ConfigurationException
158 {
159 super(fileName);
160 }
161
162 /***
163 * Creates and loads the property list from the specified file.
164 *
165 * @param file The plist file to load.
166 * @throws ConfigurationException Error while loading the plist file
167 */
168 public PropertyListConfiguration(File file) throws ConfigurationException
169 {
170 super(file);
171 }
172
173 /***
174 * Creates and loads the property list from the specified URL.
175 *
176 * @param url The location of the plist file to load.
177 * @throws ConfigurationException Error while loading the plist file
178 */
179 public PropertyListConfiguration(URL url) throws ConfigurationException
180 {
181 super(url);
182 }
183
184 public void setProperty(String key, Object value)
185 {
186
187 if (value instanceof byte[])
188 {
189 fireEvent(EVENT_SET_PROPERTY, key, value, true);
190 setDetailEvents(false);
191 try
192 {
193 clearProperty(key);
194 addPropertyDirect(key, value);
195 }
196 finally
197 {
198 setDetailEvents(true);
199 }
200 fireEvent(EVENT_SET_PROPERTY, key, value, false);
201 }
202 else
203 {
204 super.setProperty(key, value);
205 }
206 }
207
208 public void addProperty(String key, Object value)
209 {
210 if (value instanceof byte[])
211 {
212 fireEvent(EVENT_ADD_PROPERTY, key, value, true);
213 addPropertyDirect(key, value);
214 fireEvent(EVENT_ADD_PROPERTY, key, value, false);
215 }
216 else
217 {
218 super.addProperty(key, value);
219 }
220 }
221
222 public void load(Reader in) throws ConfigurationException
223 {
224 PropertyListParser parser = new PropertyListParser(in);
225 try
226 {
227 HierarchicalConfiguration config = parser.parse();
228 setRoot(config.getRoot());
229 }
230 catch (ParseException e)
231 {
232 throw new ConfigurationException(e);
233 }
234 }
235
236 public void save(Writer out) throws ConfigurationException
237 {
238 PrintWriter writer = new PrintWriter(out);
239 printNode(writer, 0, getRoot());
240 writer.flush();
241 }
242
243 /***
244 * Append a node to the writer, indented according to a specific level.
245 */
246 private void printNode(PrintWriter out, int indentLevel, Node node)
247 {
248 String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
249
250 if (node.getName() != null)
251 {
252 out.print(padding + quoteString(node.getName()) + " = ");
253 }
254
255
256 List children = new ArrayList(node.getChildren());
257 Iterator it = children.iterator();
258 while (it.hasNext())
259 {
260 Node child = (Node) it.next();
261 if (child.getValue() == null && (child.getChildren() == null || child.getChildren().isEmpty()))
262 {
263 it.remove();
264 }
265 }
266
267 if (!children.isEmpty())
268 {
269
270 if (indentLevel > 0)
271 {
272 out.println();
273 }
274
275 out.println(padding + "{");
276
277
278 it = children.iterator();
279 while (it.hasNext())
280 {
281 Node child = (Node) it.next();
282
283 printNode(out, indentLevel + 1, child);
284
285
286 Object value = child.getValue();
287 if (value != null && !(value instanceof Map) && !(value instanceof Configuration))
288 {
289 out.println(";");
290 }
291
292
293 if (it.hasNext() && (value == null || value instanceof List))
294 {
295 out.println();
296 }
297 }
298
299 out.print(padding + "}");
300
301
302 if (node.getParent() != null)
303 {
304 out.println();
305 }
306 }
307 else
308 {
309
310 Object value = node.getValue();
311 printValue(out, indentLevel, value);
312 }
313 }
314
315 /***
316 * Append a value to the writer, indented according to a specific level.
317 */
318 private void printValue(PrintWriter out, int indentLevel, Object value)
319 {
320 String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
321
322 if (value instanceof List)
323 {
324 out.print("( ");
325 Iterator it = ((List) value).iterator();
326 while (it.hasNext())
327 {
328 printValue(out, indentLevel + 1, it.next());
329 if (it.hasNext())
330 {
331 out.print(", ");
332 }
333 }
334 out.print(" )");
335 }
336 else if (value instanceof HierarchicalConfiguration)
337 {
338 printNode(out, indentLevel, ((HierarchicalConfiguration) value).getRoot());
339 }
340 else if (value instanceof Configuration)
341 {
342
343 out.println();
344 out.println(padding + "{");
345
346 Configuration config = (Configuration) value;
347 Iterator it = config.getKeys();
348 while (it.hasNext())
349 {
350 String key = (String) it.next();
351 Node node = new Node(key);
352 node.setValue(config.getProperty(key));
353
354 printNode(out, indentLevel + 1, node);
355 out.println(";");
356 }
357 out.println(padding + "}");
358 }
359 else if (value instanceof Map)
360 {
361
362 Map map = (Map) value;
363 printValue(out, indentLevel, new MapConfiguration(map));
364 }
365 else if (value instanceof byte[])
366 {
367 out.print("<" + new String(Hex.encodeHex((byte[]) value)) + ">");
368 }
369 else if (value instanceof Date)
370 {
371 out.print(formatDate((Date) value));
372 }
373 else if (value != null)
374 {
375 out.print(quoteString(String.valueOf(value)));
376 }
377 }
378
379 /***
380 * Quote the specified string if necessary, that's if the string contains:
381 * <ul>
382 * <li>a space character (' ', '\t', '\r', '\n')</li>
383 * <li>a quote '"'</li>
384 * <li>special characters in plist files ('(', ')', '{', '}', '=', ';', ',')</li>
385 * </ul>
386 * Quotes within the string are escaped.
387 *
388 * <p>Examples:</p>
389 * <ul>
390 * <li>abcd -> abcd</li>
391 * <li>ab cd -> "ab cd"</li>
392 * <li>foo"bar -> "foo\"bar"</li>
393 * <li>foo;bar -> "foo;bar"</li>
394 * </ul>
395 */
396 String quoteString(String s)
397 {
398 if (s == null)
399 {
400 return null;
401 }
402
403 if (s.indexOf(' ') != -1
404 || s.indexOf('\t') != -1
405 || s.indexOf('\r') != -1
406 || s.indexOf('\n') != -1
407 || s.indexOf('"') != -1
408 || s.indexOf('(') != -1
409 || s.indexOf(')') != -1
410 || s.indexOf('{') != -1
411 || s.indexOf('}') != -1
412 || s.indexOf('=') != -1
413 || s.indexOf(',') != -1
414 || s.indexOf(';') != -1)
415 {
416 s = StringUtils.replace(s, "\"", "//\"");
417 s = "\"" + s + "\"";
418 }
419
420 return s;
421 }
422
423 /***
424 * Parses a date in a format like
425 * <code><*D2002-03-22 11:30:00 +0100></code>.
426 *
427 * @param s the string with the date to be parsed
428 * @return the parsed date
429 * @throws ParseException if an error occurred while parsing the string
430 */
431 static Date parseDate(String s) throws ParseException
432 {
433 Calendar cal = Calendar.getInstance();
434 cal.clear();
435 int index = 0;
436
437 for (int i = 0; i < DATE_PARSERS.length; i++)
438 {
439 index += DATE_PARSERS[i].parseComponent(s, index, cal);
440 }
441
442 return cal.getTime();
443 }
444
445 /***
446 * Returns a string representation for the date specified by the given
447 * calendar.
448 *
449 * @param cal the calendar with the initialized date
450 * @return a string for this date
451 */
452 static String formatDate(Calendar cal)
453 {
454 StringBuffer buf = new StringBuffer();
455
456 for (int i = 0; i < DATE_PARSERS.length; i++)
457 {
458 DATE_PARSERS[i].formatComponent(buf, cal);
459 }
460
461 return buf.toString();
462 }
463
464 /***
465 * Returns a string representation for the specified date.
466 *
467 * @param date the date
468 * @return a string for this date
469 */
470 static String formatDate(Date date)
471 {
472 Calendar cal = Calendar.getInstance();
473 cal.setTime(date);
474 return formatDate(cal);
475 }
476
477 /***
478 * A helper class for parsing and formatting date literals. Usually we would
479 * use <code>SimpleDateFormat</code> for this purpose, but in Java 1.3 the
480 * functionality of this class is limited. So we have a hierarchy of parser
481 * classes instead that deal with the different components of a date
482 * literal.
483 */
484 private abstract static class DateComponentParser
485 {
486 /***
487 * Parses a component from the given input string.
488 *
489 * @param s the string to be parsed
490 * @param index the current parsing position
491 * @param cal the calendar where to store the result
492 * @return the length of the processed component
493 * @throws ParseException if the component cannot be extracted
494 */
495 public abstract int parseComponent(String s, int index, Calendar cal)
496 throws ParseException;
497
498 /***
499 * Formats a date component. This method is used for converting a date
500 * in its internal representation into a string literal.
501 *
502 * @param buf the target buffer
503 * @param cal the calendar with the current date
504 */
505 public abstract void formatComponent(StringBuffer buf, Calendar cal);
506
507 /***
508 * Checks whether the given string has at least <code>length</code>
509 * characters starting from the given parsing position. If this is not
510 * the case, an exception will be thrown.
511 *
512 * @param s the string to be tested
513 * @param index the current index
514 * @param length the minimum length after the index
515 * @throws ParseException if the string is too short
516 */
517 protected void checkLength(String s, int index, int length)
518 throws ParseException
519 {
520 int len = (s == null) ? 0 : s.length();
521 if (index + length > len)
522 {
523 throw new ParseException("Input string too short: " + s
524 + ", index: " + index);
525 }
526 }
527
528 /***
529 * Adds a number to the given string buffer and adds leading '0'
530 * characters until the given length is reached.
531 *
532 * @param buf the target buffer
533 * @param num the number to add
534 * @param length the required length
535 */
536 protected void padNum(StringBuffer buf, int num, int length)
537 {
538 buf.append(StringUtils.leftPad(String.valueOf(num), length,
539 PAD_CHAR));
540 }
541 }
542
543 /***
544 * A specialized date component parser implementation that deals with
545 * numeric calendar fields. The class is able to extract fields from a
546 * string literal and to format a literal from a calendar.
547 */
548 private static class DateFieldParser extends DateComponentParser
549 {
550 /*** Stores the calendar field to be processed. */
551 private int calendarField;
552
553 /*** Stores the length of this field. */
554 private int length;
555
556 /*** An optional offset to add to the calendar field. */
557 private int offset;
558
559 /***
560 * Creates a new instance of <code>DateFieldParser</code>.
561 *
562 * @param calFld the calendar field code
563 * @param len the length of this field
564 */
565 public DateFieldParser(int calFld, int len)
566 {
567 this(calFld, len, 0);
568 }
569
570 /***
571 * Creates a new instance of <code>DateFieldParser</code> and fully
572 * initializes it.
573 *
574 * @param calFld the calendar field code
575 * @param len the length of this field
576 * @param ofs an offset to add to the calendar field
577 */
578 public DateFieldParser(int calFld, int len, int ofs)
579 {
580 calendarField = calFld;
581 length = len;
582 offset = ofs;
583 }
584
585 public void formatComponent(StringBuffer buf, Calendar cal)
586 {
587 padNum(buf, cal.get(calendarField) + offset, length);
588 }
589
590 public int parseComponent(String s, int index, Calendar cal)
591 throws ParseException
592 {
593 checkLength(s, index, length);
594 try
595 {
596 cal.set(calendarField, Integer.parseInt(s.substring(index,
597 index + length))
598 - offset);
599 return length;
600 }
601 catch (NumberFormatException nfex)
602 {
603 throw new ParseException("Invalid number: " + s + ", index "
604 + index);
605 }
606 }
607 }
608
609 /***
610 * A specialized date component parser implementation that deals with
611 * separator characters.
612 */
613 private static class DateSeparatorParser extends DateComponentParser
614 {
615 /*** Stores the separator. */
616 private String separator;
617
618 /***
619 * Creates a new instance of <code>DateSeparatorParser</code> and sets
620 * the separator string.
621 *
622 * @param sep the separator string
623 */
624 public DateSeparatorParser(String sep)
625 {
626 separator = sep;
627 }
628
629 public void formatComponent(StringBuffer buf, Calendar cal)
630 {
631 buf.append(separator);
632 }
633
634 public int parseComponent(String s, int index, Calendar cal)
635 throws ParseException
636 {
637 checkLength(s, index, separator.length());
638 if (!s.startsWith(separator, index))
639 {
640 throw new ParseException("Invalid input: " + s + ", index "
641 + index + ", expected " + separator);
642 }
643 return separator.length();
644 }
645 }
646
647 /***
648 * A specialized date component parser implementation that deals with the
649 * time zone part of a date component.
650 */
651 private static class DateTimeZoneParser extends DateComponentParser
652 {
653 public void formatComponent(StringBuffer buf, Calendar cal)
654 {
655 TimeZone tz = cal.getTimeZone();
656 int ofs = tz.getRawOffset() / MILLIS_PER_MINUTE;
657 if (ofs < 0)
658 {
659 buf.append('-');
660 ofs = -ofs;
661 }
662 else
663 {
664 buf.append('+');
665 }
666 int hour = ofs / MINUTES_PER_HOUR;
667 int min = ofs % MINUTES_PER_HOUR;
668 padNum(buf, hour, 2);
669 padNum(buf, min, 2);
670 }
671
672 public int parseComponent(String s, int index, Calendar cal)
673 throws ParseException
674 {
675 checkLength(s, index, TIME_ZONE_LENGTH);
676 TimeZone tz = TimeZone.getTimeZone(TIME_ZONE_PREFIX
677 + s.substring(index, index + TIME_ZONE_LENGTH));
678 cal.setTimeZone(tz);
679 return TIME_ZONE_LENGTH;
680 }
681 }
682 }