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.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 supports 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 = &lt;4f3e0145ab>;
63   *
64   *     date = &lt;*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: 628705 $, $Date: 2008-02-18 13:37:19 +0100 (Mo, 18 Feb 2008) $
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         // 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         // get all non trivial nodes
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             // skip a line, except for the root dictionary
270             if (indentLevel > 0)
271             {
272                 out.println();
273             }
274 
275             out.println(padding + "{");
276 
277             // display the children
278             it = children.iterator();
279             while (it.hasNext())
280             {
281                 Node child = (Node) it.next();
282 
283                 printNode(out, indentLevel + 1, child);
284 
285                 // add a semi colon for elements that are not dictionaries
286                 Object value = child.getValue();
287                 if (value != null && !(value instanceof Map) && !(value instanceof Configuration))
288                 {
289                     out.println(";");
290                 }
291 
292                 // skip a line after arrays and dictionaries
293                 if (it.hasNext() && (value == null || value instanceof List))
294                 {
295                     out.println();
296                 }
297             }
298 
299             out.print(padding + "}");
300 
301             // line feed if the dictionary is not in an array
302             if (node.getParent() != 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>&lt;*D2002-03-22 11:30:00 +0100&gt;</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 }