    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}