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