001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018 package org.apache.commons.configuration.plist;
019
020 import java.io.File;
021 import java.io.PrintWriter;
022 import java.io.Reader;
023 import java.io.Writer;
024 import java.net.URL;
025 import java.util.ArrayList;
026 import java.util.Calendar;
027 import java.util.Date;
028 import java.util.Iterator;
029 import java.util.List;
030 import java.util.Map;
031 import java.util.TimeZone;
032
033 import org.apache.commons.codec.binary.Hex;
034 import org.apache.commons.configuration.AbstractHierarchicalFileConfiguration;
035 import org.apache.commons.configuration.Configuration;
036 import org.apache.commons.configuration.ConfigurationException;
037 import org.apache.commons.configuration.HierarchicalConfiguration;
038 import org.apache.commons.configuration.MapConfiguration;
039 import org.apache.commons.lang.StringUtils;
040
041 /**
042 * NeXT / OpenStep style configuration. This configuration can read and write
043 * ASCII plist files. It supports the GNUStep extension to specify date objects.
044 * <p>
045 * References:
046 * <ul>
047 * <li><a
048 * href="http://developer.apple.com/documentation/Cocoa/Conceptual/PropertyLists/OldStylePlists/OldStylePLists.html">
049 * Apple Documentation - Old-Style ASCII Property Lists</a></li>
050 * <li><a
051 * href="http://www.gnustep.org/resources/documentation/Developer/Base/Reference/NSPropertyList.html">
052 * GNUStep Documentation</a></li>
053 * </ul>
054 *
055 * <p>Example:</p>
056 * <pre>
057 * {
058 * foo = "bar";
059 *
060 * array = ( value1, value2, value3 );
061 *
062 * data = <4f3e0145ab>;
063 *
064 * date = <*D2007-05-05 20:05:00 +0100>;
065 *
066 * nested =
067 * {
068 * key1 = value1;
069 * key2 = value;
070 * nested =
071 * {
072 * foo = bar
073 * }
074 * }
075 * }
076 * </pre>
077 *
078 * @since 1.2
079 *
080 * @author Emmanuel Bourg
081 * @version $Revision: 797282 $, $Date: 2009-07-24 02:39:29 +0200 (Fr, 24. Jul 2009) $
082 */
083 public class PropertyListConfiguration extends AbstractHierarchicalFileConfiguration
084 {
085 /** Constant for the separator parser for the date part. */
086 private static final DateComponentParser DATE_SEPARATOR_PARSER = new DateSeparatorParser(
087 "-");
088
089 /** Constant for the separator parser for the time part. */
090 private static final DateComponentParser TIME_SEPARATOR_PARSER = new DateSeparatorParser(
091 ":");
092
093 /** Constant for the separator parser for blanks between the parts. */
094 private static final DateComponentParser BLANK_SEPARATOR_PARSER = new DateSeparatorParser(
095 " ");
096
097 /** An array with the component parsers for dealing with dates. */
098 private static final DateComponentParser[] DATE_PARSERS =
099 {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 // special case for byte arrays, they must be stored as is in the configuration
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 List children = new ArrayList(node.getChildren());
256 if (!children.isEmpty())
257 {
258 // skip a line, except for the root dictionary
259 if (indentLevel > 0)
260 {
261 out.println();
262 }
263
264 out.println(padding + "{");
265
266 // display the children
267 Iterator it = children.iterator();
268 while (it.hasNext())
269 {
270 Node child = (Node) it.next();
271
272 printNode(out, indentLevel + 1, child);
273
274 // add a semi colon for elements that are not dictionaries
275 Object value = child.getValue();
276 if (value != null && !(value instanceof Map) && !(value instanceof Configuration))
277 {
278 out.println(";");
279 }
280
281 // skip a line after arrays and dictionaries
282 if (it.hasNext() && (value == null || value instanceof List))
283 {
284 out.println();
285 }
286 }
287
288 out.print(padding + "}");
289
290 // line feed if the dictionary is not in an array
291 if (node.getParent() != null)
292 {
293 out.println();
294 }
295 }
296 else if (node.getValue() == null)
297 {
298 out.println();
299 out.print(padding + "{ };");
300
301 // line feed if the dictionary is not in an array
302 if (node.getParentNode() != null)
303 {
304 out.println();
305 }
306 }
307 else
308 {
309 // display the leaf value
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 // display a flat Configuration as a dictionary
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 // display a Map as a dictionary
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 }