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