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