    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}