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