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