View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
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.HashMap;
29  import java.util.Iterator;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.TimeZone;
33  
34  import org.apache.commons.codec.binary.Hex;
35  import org.apache.commons.configuration.AbstractHierarchicalFileConfiguration;
36  import org.apache.commons.configuration.Configuration;
37  import org.apache.commons.configuration.ConfigurationException;
38  import org.apache.commons.configuration.HierarchicalConfiguration;
39  import org.apache.commons.configuration.MapConfiguration;
40  import org.apache.commons.configuration.tree.ConfigurationNode;
41  import org.apache.commons.lang.StringUtils;
42  
43  /**
44   * NeXT / OpenStep style configuration. This configuration can read and write
45   * ASCII plist files. It supports the GNUStep extension to specify date objects.
46   * <p>
47   * References:
48   * <ul>
49   *   <li><a
50   * href="http://developer.apple.com/documentation/Cocoa/Conceptual/PropertyLists/OldStylePlists/OldStylePLists.html">
51   * Apple Documentation - Old-Style ASCII Property Lists</a></li>
52   *   <li><a
53   * href="http://www.gnustep.org/resources/documentation/Developer/Base/Reference/NSPropertyList.html">
54   * GNUStep Documentation</a></li>
55   * </ul>
56   *
57   * <p>Example:</p>
58   * <pre>
59   * {
60   *     foo = "bar";
61   *
62   *     array = ( value1, value2, value3 );
63   *
64   *     data = &lt;4f3e0145ab>;
65   *
66   *     date = &lt;*D2007-05-05 20:05:00 +0100>;
67   *
68   *     nested =
69   *     {
70   *         key1 = value1;
71   *         key2 = value;
72   *         nested =
73   *         {
74   *             foo = bar
75   *         }
76   *     }
77   * }
78   * </pre>
79   *
80   * @since 1.2
81   *
82   * @author Emmanuel Bourg
83   * @version $Id: PropertyListConfiguration.java 1210637 2011-12-05 21:12:12Z oheger $
84   */
85  public class PropertyListConfiguration extends AbstractHierarchicalFileConfiguration
86  {
87      /** Constant for the separator parser for the date part. */
88      private static final DateComponentParser DATE_SEPARATOR_PARSER = new DateSeparatorParser(
89              "-");
90  
91      /** Constant for the separator parser for the time part. */
92      private static final DateComponentParser TIME_SEPARATOR_PARSER = new DateSeparatorParser(
93              ":");
94  
95      /** Constant for the separator parser for blanks between the parts. */
96      private static final DateComponentParser BLANK_SEPARATOR_PARSER = new DateSeparatorParser(
97              " ");
98  
99      /** 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 }