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.configuration2.plist;
19  
20  import java.io.PrintWriter;
21  import java.io.Reader;
22  import java.io.Writer;
23  import java.util.ArrayList;
24  import java.util.Calendar;
25  import java.util.Date;
26  import java.util.HashMap;
27  import java.util.Iterator;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.TimeZone;
31  
32  import org.apache.commons.codec.binary.Hex;
33  import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
34  import org.apache.commons.configuration2.Configuration;
35  import org.apache.commons.configuration2.FileBasedConfiguration;
36  import org.apache.commons.configuration2.HierarchicalConfiguration;
37  import org.apache.commons.configuration2.ImmutableConfiguration;
38  import org.apache.commons.configuration2.MapConfiguration;
39  import org.apache.commons.configuration2.ex.ConfigurationException;
40  import org.apache.commons.configuration2.tree.ImmutableNode;
41  import org.apache.commons.configuration2.tree.InMemoryNodeModel;
42  import org.apache.commons.configuration2.tree.NodeHandler;
43  import org.apache.commons.lang3.StringUtils;
44  
45  /**
46   * NeXT / OpenStep style configuration. This configuration can read and write ASCII plist files. It supports the GNUStep
47   * extension to specify date objects.
48   * <p>
49   * References:
50   * <ul>
51   * <li><a href=
52   * "https://developer.apple.com/documentation/Cocoa/Conceptual/PropertyLists/OldStylePlists/OldStylePLists.html"> Apple
53   * Documentation - Old-Style ASCII Property Lists</a></li>
54   * <li><a href="http://www.gnustep.org/resources/documentation/Developer/Base/Reference/NSPropertyList.html"> GNUStep
55   * Documentation</a></li>
56   * </ul>
57   *
58   * <p>
59   * Example:
60   * </p>
61   *
62   * <pre>
63   * {
64   *     foo = "bar";
65   *
66   *     array = ( value1, value2, value3 );
67   *
68   *     data = &lt;4f3e0145ab&gt;;
69   *
70   *     date = &lt;*D2007-05-05 20:05:00 +0100&gt;;
71   *
72   *     nested =
73   *     {
74   *         key1 = value1;
75   *         key2 = value;
76   *         nested =
77   *         {
78   *             foo = bar
79   *         }
80   *     }
81   * }
82   * </pre>
83   *
84   * @since 1.2
85   */
86  public class PropertyListConfiguration extends BaseHierarchicalConfiguration implements FileBasedConfiguration {
87      /** Constant for the separator parser for the date part. */
88      private static final DateComponentParser DATE_SEPARATOR_PARSER = new DateSeparatorParser("-");
89  
90      /** Constant for the separator parser for the time part. */
91      private static final DateComponentParser TIME_SEPARATOR_PARSER = new DateSeparatorParser(":");
92  
93      /** Constant for the separator parser for blanks between the parts. */
94      private static final DateComponentParser BLANK_SEPARATOR_PARSER = new DateSeparatorParser(" ");
95  
96      /** An array with the component parsers for dealing with dates. */
97      private static final DateComponentParser[] DATE_PARSERS = {new DateSeparatorParser("<*D"), new DateFieldParser(Calendar.YEAR, 4), DATE_SEPARATOR_PARSER,
98          new DateFieldParser(Calendar.MONTH, 2, 1), DATE_SEPARATOR_PARSER, new DateFieldParser(Calendar.DATE, 2), BLANK_SEPARATOR_PARSER,
99          new DateFieldParser(Calendar.HOUR_OF_DAY, 2), TIME_SEPARATOR_PARSER, new DateFieldParser(Calendar.MINUTE, 2), TIME_SEPARATOR_PARSER,
100         new DateFieldParser(Calendar.SECOND, 2), BLANK_SEPARATOR_PARSER, new DateTimeZoneParser(), new DateSeparatorParser(">")};
101 
102     /** Constant for the ID prefix for GMT time zones. */
103     private static final String TIME_ZONE_PREFIX = "GMT";
104 
105     /** Constant for the milliseconds of a minute. */
106     private static final int MILLIS_PER_MINUTE = 1000 * 60;
107 
108     /** Constant for the minutes per hour. */
109     private static final int MINUTES_PER_HOUR = 60;
110 
111     /** Size of the indentation for the generated file. */
112     private static final int INDENT_SIZE = 4;
113 
114     /** Constant for the length of a time zone. */
115     private static final int TIME_ZONE_LENGTH = 5;
116 
117     /** Constant for the padding character in the date format. */
118     private static final char PAD_CHAR = '0';
119 
120     /**
121      * Creates an empty PropertyListConfiguration object which can be used to synthesize a new plist file by adding values
122      * and then saving().
123      */
124     public PropertyListConfiguration() {
125     }
126 
127     /**
128      * Creates a new instance of {@code PropertyListConfiguration} and copies the content of the specified configuration
129      * into this object.
130      *
131      * @param c the configuration to copy
132      * @since 1.4
133      */
134     public PropertyListConfiguration(final HierarchicalConfiguration<ImmutableNode> c) {
135         super(c);
136     }
137 
138     /**
139      * Creates a new instance of {@code PropertyListConfiguration} with the given root node.
140      *
141      * @param root the root node
142      */
143     PropertyListConfiguration(final ImmutableNode root) {
144         super(new InMemoryNodeModel(root));
145     }
146 
147     @Override
148     protected void setPropertyInternal(final String key, final Object value) {
149         // special case for byte arrays, they must be stored as is in the configuration
150         if (value instanceof byte[]) {
151             setDetailEvents(false);
152             try {
153                 clearProperty(key);
154                 addPropertyDirect(key, value);
155             } finally {
156                 setDetailEvents(true);
157             }
158         } else {
159             super.setPropertyInternal(key, value);
160         }
161     }
162 
163     @Override
164     protected void addPropertyInternal(final String key, final Object value) {
165         if (value instanceof byte[]) {
166             addPropertyDirect(key, value);
167         } else {
168             super.addPropertyInternal(key, value);
169         }
170     }
171 
172     @Override
173     public void read(final Reader in) throws ConfigurationException {
174         final PropertyListParser parser = new PropertyListParser(in);
175         try {
176             final PropertyListConfiguration config = parser.parse();
177             getModel().setRootNode(config.getNodeModel().getNodeHandler().getRootNode());
178         } catch (final ParseException e) {
179             throw new ConfigurationException(e);
180         }
181     }
182 
183     @Override
184     public void write(final Writer out) throws ConfigurationException {
185         final PrintWriter writer = new PrintWriter(out);
186         final NodeHandler<ImmutableNode> handler = getModel().getNodeHandler();
187         printNode(writer, 0, handler.getRootNode(), handler);
188         writer.flush();
189     }
190 
191     /**
192      * Append a node to the writer, indented according to a specific level.
193      */
194     private void printNode(final PrintWriter out, final int indentLevel, final ImmutableNode node, final NodeHandler<ImmutableNode> handler) {
195         final String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
196 
197         if (node.getNodeName() != null) {
198             out.print(padding + quoteString(node.getNodeName()) + " = ");
199         }
200 
201         final List<ImmutableNode> children = new ArrayList<>(node.getChildren());
202         if (!children.isEmpty()) {
203             // skip a line, except for the root dictionary
204             if (indentLevel > 0) {
205                 out.println();
206             }
207 
208             out.println(padding + "{");
209 
210             // display the children
211             final Iterator<ImmutableNode> it = children.iterator();
212             while (it.hasNext()) {
213                 final ImmutableNode child = it.next();
214 
215                 printNode(out, indentLevel + 1, child, handler);
216 
217                 // add a semi colon for elements that are not dictionaries
218                 final Object value = child.getValue();
219                 if (value != null && !(value instanceof Map) && !(value instanceof Configuration)) {
220                     out.println(";");
221                 }
222 
223                 // skip a line after arrays and dictionaries
224                 if (it.hasNext() && (value == null || value instanceof List)) {
225                     out.println();
226                 }
227             }
228 
229             out.print(padding + "}");
230 
231             // line feed if the dictionary is not in an array
232             if (handler.getParent(node) != null) {
233                 out.println();
234             }
235         } else if (node.getValue() == null) {
236             out.println();
237             out.print(padding + "{ };");
238 
239             // line feed if the dictionary is not in an array
240             if (handler.getParent(node) != null) {
241                 out.println();
242             }
243         } else {
244             // display the leaf value
245             final Object value = node.getValue();
246             printValue(out, indentLevel, value);
247         }
248     }
249 
250     /**
251      * Append a value to the writer, indented according to a specific level.
252      */
253     private void printValue(final PrintWriter out, final int indentLevel, final Object value) {
254         final String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
255 
256         if (value instanceof List) {
257             out.print("( ");
258             final Iterator<?> it = ((List<?>) value).iterator();
259             while (it.hasNext()) {
260                 printValue(out, indentLevel + 1, it.next());
261                 if (it.hasNext()) {
262                     out.print(", ");
263                 }
264             }
265             out.print(" )");
266         } else if (value instanceof PropertyListConfiguration) {
267             final NodeHandler<ImmutableNode> handler = ((PropertyListConfiguration) value).getModel().getNodeHandler();
268             printNode(out, indentLevel, handler.getRootNode(), handler);
269         } else if (value instanceof ImmutableConfiguration) {
270             // display a flat Configuration as a dictionary
271             out.println();
272             out.println(padding + "{");
273 
274             final ImmutableConfiguration config = (ImmutableConfiguration) value;
275             final Iterator<String> it = config.getKeys();
276             while (it.hasNext()) {
277                 final String key = it.next();
278                 final ImmutableNode node = new ImmutableNode.Builder().name(key).value(config.getProperty(key)).create();
279                 final InMemoryNodeModel tempModel = new InMemoryNodeModel(node);
280                 printNode(out, indentLevel + 1, node, tempModel.getNodeHandler());
281                 out.println(";");
282             }
283             out.println(padding + "}");
284         } else if (value instanceof Map) {
285             // display a Map as a dictionary
286             final Map<String, Object> map = transformMap((Map<?, ?>) value);
287             printValue(out, indentLevel, new MapConfiguration(map));
288         } else if (value instanceof byte[]) {
289             out.print("<" + new String(Hex.encodeHex((byte[]) value)) + ">");
290         } else if (value instanceof Date) {
291             out.print(formatDate((Date) value));
292         } else if (value != null) {
293             out.print(quoteString(String.valueOf(value)));
294         }
295     }
296 
297     /**
298      * Quote the specified string if necessary, that's if the string contains:
299      * <ul>
300      * <li>a space character (' ', '\t', '\r', '\n')</li>
301      * <li>a quote '"'</li>
302      * <li>special characters in plist files ('(', ')', '{', '}', '=', ';', ',')</li>
303      * </ul>
304      * Quotes within the string are escaped.
305      *
306      * <p>
307      * Examples:
308      * </p>
309      * <ul>
310      * <li>abcd -> abcd</li>
311      * <li>ab cd -> "ab cd"</li>
312      * <li>foo"bar -> "foo\"bar"</li>
313      * <li>foo;bar -> "foo;bar"</li>
314      * </ul>
315      */
316     String quoteString(String s) {
317         if (s == null) {
318             return null;
319         }
320 
321         if (s.indexOf(' ') != -1 || s.indexOf('\t') != -1 || s.indexOf('\r') != -1 || s.indexOf('\n') != -1 || s.indexOf('"') != -1 || s.indexOf('(') != -1
322             || s.indexOf(')') != -1 || s.indexOf('{') != -1 || s.indexOf('}') != -1 || s.indexOf('=') != -1 || s.indexOf(',') != -1 || s.indexOf(';') != -1) {
323             s = s.replace("\"", "\\\"");
324             s = "\"" + s + "\"";
325         }
326 
327         return s;
328     }
329 
330     /**
331      * Parses a date in a format like {@code <*D2002-03-22 11:30:00 +0100>}.
332      *
333      * @param s the string with the date to be parsed
334      * @return the parsed date
335      * @throws ParseException if an error occurred while parsing the string
336      */
337     static Date parseDate(final String s) throws ParseException {
338         final Calendar cal = Calendar.getInstance();
339         cal.clear();
340         int index = 0;
341 
342         for (final DateComponentParser parser : DATE_PARSERS) {
343             index += parser.parseComponent(s, index, cal);
344         }
345 
346         return cal.getTime();
347     }
348 
349     /**
350      * Returns a string representation for the date specified by the given calendar.
351      *
352      * @param cal the calendar with the initialized date
353      * @return a string for this date
354      */
355     static String formatDate(final Calendar cal) {
356         final StringBuilder buf = new StringBuilder();
357 
358         for (final DateComponentParser element : DATE_PARSERS) {
359             element.formatComponent(buf, cal);
360         }
361 
362         return buf.toString();
363     }
364 
365     /**
366      * Returns a string representation for the specified date.
367      *
368      * @param date the date
369      * @return a string for this date
370      */
371     static String formatDate(final Date date) {
372         final Calendar cal = Calendar.getInstance();
373         cal.setTime(date);
374         return formatDate(cal);
375     }
376 
377     /**
378      * Transform a map of arbitrary types into a map with string keys and object values. All keys of the source map which
379      * are not of type String are dropped.
380      *
381      * @param src the map to be converted
382      * @return the resulting map
383      */
384     private static Map<String, Object> transformMap(final Map<?, ?> src) {
385         final Map<String, Object> dest = new HashMap<>();
386         src.forEach((k, v) -> {
387             if (k instanceof String) {
388                 dest.put((String) k, v);
389             }
390         });
391         return dest;
392     }
393 
394     /**
395      * A helper class for parsing and formatting date literals. Usually we would use {@code SimpleDateFormat} for this
396      * purpose, but in Java 1.3 the functionality of this class is limited. So we have a hierarchy of parser classes instead
397      * that deal with the different components of a date literal.
398      */
399     private abstract static class DateComponentParser {
400         /**
401          * Parses a component from the given input string.
402          *
403          * @param s the string to be parsed
404          * @param index the current parsing position
405          * @param cal the calendar where to store the result
406          * @return the length of the processed component
407          * @throws ParseException if the component cannot be extracted
408          */
409         public abstract int parseComponent(String s, int index, Calendar cal) throws ParseException;
410 
411         /**
412          * Formats a date component. This method is used for converting a date in its internal representation into a string
413          * literal.
414          *
415          * @param buf the target buffer
416          * @param cal the calendar with the current date
417          */
418         public abstract void formatComponent(StringBuilder buf, Calendar cal);
419 
420         /**
421          * Checks whether the given string has at least {@code length} characters starting from the given parsing position. If
422          * this is not the case, an exception will be thrown.
423          *
424          * @param s the string to be tested
425          * @param index the current index
426          * @param length the minimum length after the index
427          * @throws ParseException if the string is too short
428          */
429         protected void checkLength(final String s, final int index, final int length) throws ParseException {
430             final int len = s == null ? 0 : s.length();
431             if (index + length > len) {
432                 throw new ParseException("Input string too short: " + s + ", index: " + index);
433             }
434         }
435 
436         /**
437          * Adds a number to the given string buffer and adds leading '0' characters until the given length is reached.
438          *
439          * @param buf the target buffer
440          * @param num the number to add
441          * @param length the required length
442          */
443         protected void padNum(final StringBuilder buf, final int num, final int length) {
444             buf.append(StringUtils.leftPad(String.valueOf(num), length, PAD_CHAR));
445         }
446     }
447 
448     /**
449      * A specialized date component parser implementation that deals with numeric calendar fields. The class is able to
450      * extract fields from a string literal and to format a literal from a calendar.
451      */
452     private static final class DateFieldParser extends DateComponentParser {
453         /** Stores the calendar field to be processed. */
454         private final int calendarField;
455 
456         /** Stores the length of this field. */
457         private final int length;
458 
459         /** An optional offset to add to the calendar field. */
460         private final int offset;
461 
462         /**
463          * Creates a new instance of {@code DateFieldParser}.
464          *
465          * @param calFld the calendar field code
466          * @param len the length of this field
467          */
468         public DateFieldParser(final int calFld, final int len) {
469             this(calFld, len, 0);
470         }
471 
472         /**
473          * Creates a new instance of {@code DateFieldParser} and fully initializes it.
474          *
475          * @param calFld the calendar field code
476          * @param len the length of this field
477          * @param ofs an offset to add to the calendar field
478          */
479         public DateFieldParser(final int calFld, final int len, final int ofs) {
480             calendarField = calFld;
481             length = len;
482             offset = ofs;
483         }
484 
485         @Override
486         public void formatComponent(final StringBuilder buf, final Calendar cal) {
487             padNum(buf, cal.get(calendarField) + offset, length);
488         }
489 
490         @Override
491         public int parseComponent(final String s, final int index, final Calendar cal) throws ParseException {
492             checkLength(s, index, length);
493             try {
494                 cal.set(calendarField, Integer.parseInt(s.substring(index, index + length)) - offset);
495                 return length;
496             } catch (final NumberFormatException nfex) {
497                 throw new ParseException("Invalid number: " + s + ", index " + index);
498             }
499         }
500     }
501 
502     /**
503      * A specialized date component parser implementation that deals with separator characters.
504      */
505     private static final class DateSeparatorParser extends DateComponentParser {
506         /** Stores the separator. */
507         private final String separator;
508 
509         /**
510          * Creates a new instance of {@code DateSeparatorParser} and sets the separator string.
511          *
512          * @param sep the separator string
513          */
514         public DateSeparatorParser(final String sep) {
515             separator = sep;
516         }
517 
518         @Override
519         public void formatComponent(final StringBuilder buf, final Calendar cal) {
520             buf.append(separator);
521         }
522 
523         @Override
524         public int parseComponent(final String s, final int index, final Calendar cal) throws ParseException {
525             checkLength(s, index, separator.length());
526             if (!s.startsWith(separator, index)) {
527                 throw new ParseException("Invalid input: " + s + ", index " + index + ", expected " + separator);
528             }
529             return separator.length();
530         }
531     }
532 
533     /**
534      * A specialized date component parser implementation that deals with the time zone part of a date component.
535      */
536     private static final class DateTimeZoneParser extends DateComponentParser {
537         @Override
538         public void formatComponent(final StringBuilder buf, final Calendar cal) {
539             final TimeZone tz = cal.getTimeZone();
540             int ofs = tz.getRawOffset() / MILLIS_PER_MINUTE;
541             if (ofs < 0) {
542                 buf.append('-');
543                 ofs = -ofs;
544             } else {
545                 buf.append('+');
546             }
547             final int hour = ofs / MINUTES_PER_HOUR;
548             final int min = ofs % MINUTES_PER_HOUR;
549             padNum(buf, hour, 2);
550             padNum(buf, min, 2);
551         }
552 
553         @Override
554         public int parseComponent(final String s, final int index, final Calendar cal) throws ParseException {
555             checkLength(s, index, TIME_ZONE_LENGTH);
556             final TimeZone tz = TimeZone.getTimeZone(TIME_ZONE_PREFIX + s.substring(index, index + TIME_ZONE_LENGTH));
557             cal.setTimeZone(tz);
558             return TIME_ZONE_LENGTH;
559         }
560     }
561 }