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