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