    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
    018    package org.apache.commons.configuration.plist;
    019
    020    import java.io.File;
    021    import java.io.PrintWriter;
    022    import java.io.Reader;
    023    import java.io.Writer;
    024    import java.net.URL;
    025    import java.util.ArrayList;
    026    import java.util.Calendar;
    027    import java.util.Date;
    028    import java.util.HashMap;
    029    import java.util.Iterator;
    030    import java.util.List;
    031    import java.util.Map;
    032    import java.util.TimeZone;
    033
    034    import org.apache.commons.codec.binary.Hex;
    035    import org.apache.commons.configuration.AbstractHierarchicalFileConfiguration;
    036    import org.apache.commons.configuration.Configuration;
    037    import org.apache.commons.configuration.ConfigurationException;
    038    import org.apache.commons.configuration.HierarchicalConfiguration;
    039    import org.apache.commons.configuration.MapConfiguration;
    040    import org.apache.commons.configuration.tree.ConfigurationNode;
    041    import org.apache.commons.lang.StringUtils;
    042
    043    /**
    044     * NeXT / OpenStep style configuration. This configuration can read and write
    045     * ASCII plist files. It supports the GNUStep extension to specify date objects.
    046     * <p>
    047     * References:
    048     * <ul>
    049     *   <li><a
    050     * href="http://developer.apple.com/documentation/Cocoa/Conceptual/PropertyLists/OldStylePlists/OldStylePLists.html">
    051     * Apple Documentation - Old-Style ASCII Property Lists</a></li>
    052     *   <li><a
    053     * href="http://www.gnustep.org/resources/documentation/Developer/Base/Reference/NSPropertyList.html">
    054     * GNUStep Documentation</a></li>
    055     * </ul>
    056     *
    057     * <p>Example:</p>
    058     * <pre>
    059     * {
    060     *     foo = "bar";
    061     *
    062     *     array = ( value1, value2, value3 );
    063     *
    064     *     data = &lt;4f3e0145ab>;
    065     *
    066     *     date = &lt;*D2007-05-05 20:05:00 +0100>;
    067     *
    068     *     nested =
    069     *     {
    070     *         key1 = value1;
    071     *         key2 = value;
    072     *         nested =
    073     *         {
    074     *             foo = bar
    075     *         }
    076     *     }
    077     * }
    078     * </pre>
    079     *
    080     * @since 1.2
    081     *
    082     * @author Emmanuel Bourg
    083     * @version $Id: PropertyListConfiguration.java 1210637 2011-12-05 21:12:12Z oheger $
    084     */
    085    public class PropertyListConfiguration extends AbstractHierarchicalFileConfiguration
    086    {
    087        /** Constant for the separator parser for the date part. */
    088        private static final DateComponentParser DATE_SEPARATOR_PARSER = new DateSeparatorParser(
    089                "-");
    090
    091        /** Constant for the separator parser for the time part. */
    092        private static final DateComponentParser TIME_SEPARATOR_PARSER = new DateSeparatorParser(
    093                ":");
    094
    095        /** Constant for the separator parser for blanks between the parts. */
    096        private static final DateComponentParser BLANK_SEPARATOR_PARSER = new DateSeparatorParser(
    097                " ");
    098
    099        /** An array with the component parsers for dealing with dates. */
    100        private static final DateComponentParser[] DATE_PARSERS =
    101        {new DateSeparatorParser("<*D"), new DateFieldParser(Calendar.YEAR, 4),
    102                DATE_SEPARATOR_PARSER, new DateFieldParser(Calendar.MONTH, 2, 1),
    103                DATE_SEPARATOR_PARSER, new DateFieldParser(Calendar.DATE, 2),
    104                BLANK_SEPARATOR_PARSER,
    105                new DateFieldParser(Calendar.HOUR_OF_DAY, 2),
    106                TIME_SEPARATOR_PARSER, new DateFieldParser(Calendar.MINUTE, 2),
    107                TIME_SEPARATOR_PARSER, new DateFieldParser(Calendar.SECOND, 2),
    108                BLANK_SEPARATOR_PARSER, new DateTimeZoneParser(),
    109                new DateSeparatorParser(">")};
    110
    111        /** Constant for the ID prefix for GMT time zones. */
    112        private static final String TIME_ZONE_PREFIX = "GMT";
    113
    114        /** The serial version UID. */
    115        private static final long serialVersionUID = 3227248503779092127L;
    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(HierarchicalConfiguration c)
    149        {
    150            super(c);
    151        }
    152
    153        /**
    154         * Creates and loads the property list from the specified file.
    155         *
    156         * @param fileName The name of the plist file to load.
    157         * @throws ConfigurationException Error while loading the plist file
    158         */
    159        public PropertyListConfiguration(String fileName) throws ConfigurationException
    160        {
    161            super(fileName);
    162        }
    163
    164        /**
    165         * Creates and loads the property list from the specified file.
    166         *
    167         * @param file The plist file to load.
    168         * @throws ConfigurationException Error while loading the plist file
    169         */
    170        public PropertyListConfiguration(File file) throws ConfigurationException
    171        {
    172            super(file);
    173        }
    174
    175        /**
    176         * Creates and loads the property list from the specified URL.
    177         *
    178         * @param url The location of the plist file to load.
    179         * @throws ConfigurationException Error while loading the plist file
    180         */
    181        public PropertyListConfiguration(URL url) throws ConfigurationException
    182        {
    183            super(url);
    184        }
    185
    186        @Override
    187        public void setProperty(String key, Object value)
    188        {
    189            // special case for byte arrays, they must be stored as is in the configuration
    190            if (value instanceof byte[])
    191            {
    192                fireEvent(EVENT_SET_PROPERTY, key, value, true);
    193                setDetailEvents(false);
    194                try
    195                {
    196                    clearProperty(key);
    197                    addPropertyDirect(key, value);
    198                }
    199                finally
    200                {
    201                    setDetailEvents(true);
    202                }
    203                fireEvent(EVENT_SET_PROPERTY, key, value, false);
    204            }
    205            else
    206            {
    207                super.setProperty(key, value);
    208            }
    209        }
    210
    211        @Override
    212        public void addProperty(String key, Object value)
    213        {
    214            if (value instanceof byte[])
    215            {
    216                fireEvent(EVENT_ADD_PROPERTY, key, value, true);
    217                addPropertyDirect(key, value);
    218                fireEvent(EVENT_ADD_PROPERTY, key, value, false);
    219            }
    220            else
    221            {
    222                super.addProperty(key, value);
    223            }
    224        }
    225
    226        public void load(Reader in) throws ConfigurationException
    227        {
    228            PropertyListParser parser = new PropertyListParser(in);
    229            try
    230            {
    231                HierarchicalConfiguration config = parser.parse();
    232                setRoot(config.getRoot());
    233            }
    234            catch (ParseException e)
    235            {
    236                throw new ConfigurationException(e);
    237            }
    238        }
    239
    240        public void save(Writer out) throws ConfigurationException
    241        {
    242            PrintWriter writer = new PrintWriter(out);
    243            printNode(writer, 0, getRoot());
    244            writer.flush();
    245        }
    246
    247        /**
    248         * Append a node to the writer, indented according to a specific level.
    249         */
    250        private void printNode(PrintWriter out, int indentLevel, ConfigurationNode node)
    251        {
    252            String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
    253
    254            if (node.getName() != null)
    255            {
    256                out.print(padding + quoteString(node.getName()) + " = ");
    257            }
    258
    259            List<ConfigurationNode> children = new ArrayList<ConfigurationNode>(node.getChildren());
    260            if (!children.isEmpty())
    261            {
    262                // skip a line, except for the root dictionary
    263                if (indentLevel > 0)
    264                {
    265                    out.println();
    266                }
    267
    268                out.println(padding + "{");
    269
    270                // display the children
    271                Iterator<ConfigurationNode> it = children.iterator();
    272                while (it.hasNext())
    273                {
    274                    ConfigurationNode child = it.next();
    275
    276                    printNode(out, indentLevel + 1, child);
    277
    278                    // add a semi colon for elements that are not dictionaries
    279                    Object value = child.getValue();
    280                    if (value != null && !(value instanceof Map) && !(value instanceof Configuration))
    281                    {
    282                        out.println(";");
    283                    }
    284
    285                    // skip a line after arrays and dictionaries
    286                    if (it.hasNext() && (value == null || value instanceof List))
    287                    {
    288                        out.println();
    289                    }
    290                }
    291
    292                out.print(padding + "}");
    293
    294                // line feed if the dictionary is not in an array
    295                if (node.getParentNode() != null)
    296                {
    297                    out.println();
    298                }
    299            }
    300            else if (node.getValue() == null)
    301            {
    302                out.println();
    303                out.print(padding + "{ };");
    304
    305                // line feed if the dictionary is not in an array
    306                if (node.getParentNode() != null)
    307                {
    308                    out.println();
    309                }
    310            }
    311            else
    312            {
    313                // display the leaf value
    314                Object value = node.getValue();
    315                printValue(out, indentLevel, value);
    316            }
    317        }
    318
    319        /**
    320         * Append a value to the writer, indented according to a specific level.
    321         */
    322        private void printValue(PrintWriter out, int indentLevel, Object value)
    323        {
    324            String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
    325
    326            if (value instanceof List)
    327            {
    328                out.print("( ");
    329                Iterator<?> it = ((List<?>) value).iterator();
    330                while (it.hasNext())
    331                {
    332                    printValue(out, indentLevel + 1, it.next());
    333                    if (it.hasNext())
    334                    {
    335                        out.print(", ");
    336                    }
    337                }
    338                out.print(" )");
    339            }
    340            else if (value instanceof HierarchicalConfiguration)
    341            {
    342                printNode(out, indentLevel, ((HierarchicalConfiguration) value).getRoot());
    343            }
    344            else if (value instanceof Configuration)
    345            {
    346                // display a flat Configuration as a dictionary
    347                out.println();
    348                out.println(padding + "{");
    349
    350                Configuration config = (Configuration) value;
    351                Iterator<String> it = config.getKeys();
    352                while (it.hasNext())
    353                {
    354                    String key = it.next();
    355                    Node node = new Node(key);
    356                    node.setValue(config.getProperty(key));
    357
    358                    printNode(out, indentLevel + 1, node);
    359                    out.println(";");
    360                }
    361                out.println(padding + "}");
    362            }
    363            else if (value instanceof Map)
    364            {
    365                // display a Map as a dictionary
    366                Map<String, Object> map = transformMap((Map<?, ?>) value);
    367                printValue(out, indentLevel, new MapConfiguration(map));
    368            }
    369            else if (value instanceof byte[])
    370            {
    371                out.print("<" + new String(Hex.encodeHex((byte[]) value)) + ">");
    372            }
    373            else if (value instanceof Date)
    374            {
    375                out.print(formatDate((Date) value));
    376            }
    377            else if (value != null)
    378            {
    379                out.print(quoteString(String.valueOf(value)));
    380            }
    381        }
    382
    383        /**
    384         * Quote the specified string if necessary, that's if the string contains:
    385         * <ul>
    386         *   <li>a space character (' ', '\t', '\r', '\n')</li>
    387         *   <li>a quote '"'</li>
    388         *   <li>special characters in plist files ('(', ')', '{', '}', '=', ';', ',')</li>
    389         * </ul>
    390         * Quotes within the string are escaped.
    391         *
    392         * <p>Examples:</p>
    393         * <ul>
    394         *   <li>abcd -> abcd</li>
    395         *   <li>ab cd -> "ab cd"</li>
    396         *   <li>foo"bar -> "foo\"bar"</li>
    397         *   <li>foo;bar -> "foo;bar"</li>
    398         * </ul>
    399         */
    400        String quoteString(String s)
    401        {
    402            if (s == null)
    403            {
    404                return null;
    405            }
    406
    407            if (s.indexOf(' ') != -1
    408                    || s.indexOf('\t') != -1
    409                    || s.indexOf('\r') != -1
    410                    || s.indexOf('\n') != -1
    411                    || s.indexOf('"') != -1
    412                    || s.indexOf('(') != -1
    413                    || s.indexOf(')') != -1
    414                    || s.indexOf('{') != -1
    415                    || s.indexOf('}') != -1
    416                    || s.indexOf('=') != -1
    417                    || s.indexOf(',') != -1
    418                    || s.indexOf(';') != -1)
    419            {
    420                s = s.replaceAll("\"", "\\\\\\\"");
    421                s = "\"" + s + "\"";
    422            }
    423
    424            return s;
    425        }
    426
    427        /**
    428         * Parses a date in a format like
    429         * {@code <*D2002-03-22 11:30:00 +0100>}.
    430         *
    431         * @param s the string with the date to be parsed
    432         * @return the parsed date
    433         * @throws ParseException if an error occurred while parsing the string
    434         */
    435        static Date parseDate(String s) throws ParseException
    436        {
    437            Calendar cal = Calendar.getInstance();
    438            cal.clear();
    439            int index = 0;
    440
    441            for (DateComponentParser parser : DATE_PARSERS)
    442            {
    443                index += parser.parseComponent(s, index, cal);
    444            }
    445
    446            return cal.getTime();
    447        }
    448
    449        /**
    450         * Returns a string representation for the date specified by the given
    451         * calendar.
    452         *
    453         * @param cal the calendar with the initialized date
    454         * @return a string for this date
    455         */
    456        static String formatDate(Calendar cal)
    457        {
    458            StringBuilder buf = new StringBuilder();
    459
    460            for (int i = 0; i < DATE_PARSERS.length; i++)
    461            {
    462                DATE_PARSERS[i].formatComponent(buf, cal);
    463            }
    464
    465            return buf.toString();
    466        }
    467
    468        /**
    469         * Returns a string representation for the specified date.
    470         *
    471         * @param date the date
    472         * @return a string for this date
    473         */
    474        static String formatDate(Date date)
    475        {
    476            Calendar cal = Calendar.getInstance();
    477            cal.setTime(date);
    478            return formatDate(cal);
    479        }
    480
    481        /**
    482         * Transform a map of arbitrary types into a map with string keys and object
    483         * values. All keys of the source map which are not of type String are
    484         * dropped.
    485         *
    486         * @param src the map to be converted
    487         * @return the resulting map
    488         */
    489        private static Map<String, Object> transformMap(Map<?, ?> src)
    490        {
    491            Map<String, Object> dest = new HashMap<String, Object>();
    492            for (Map.Entry<?, ?> e : src.entrySet())
    493            {
    494                if (e.getKey() instanceof String)
    495                {
    496                    dest.put((String) e.getKey(), e.getValue());
    497                }
    498            }
    499            return dest;
    500        }
    501
    502        /**
    503         * A helper class for parsing and formatting date literals. Usually we would
    504         * use {@code SimpleDateFormat} for this purpose, but in Java 1.3 the
    505         * functionality of this class is limited. So we have a hierarchy of parser
    506         * classes instead that deal with the different components of a date
    507         * literal.
    508         */
    509        private abstract static class DateComponentParser
    510        {
    511            /**
    512             * Parses a component from the given input string.
    513             *
    514             * @param s the string to be parsed
    515             * @param index the current parsing position
    516             * @param cal the calendar where to store the result
    517             * @return the length of the processed component
    518             * @throws ParseException if the component cannot be extracted
    519             */
    520            public abstract int parseComponent(String s, int index, Calendar cal)
    521                    throws ParseException;
    522
    523            /**
    524             * Formats a date component. This method is used for converting a date
    525             * in its internal representation into a string literal.
    526             *
    527             * @param buf the target buffer
    528             * @param cal the calendar with the current date
    529             */
    530            public abstract void formatComponent(StringBuilder buf, Calendar cal);
    531
    532            /**
    533             * Checks whether the given string has at least {@code length}
    534             * characters starting from the given parsing position. If this is not
    535             * the case, an exception will be thrown.
    536             *
    537             * @param s the string to be tested
    538             * @param index the current index
    539             * @param length the minimum length after the index
    540             * @throws ParseException if the string is too short
    541             */
    542            protected void checkLength(String s, int index, int length)
    543                    throws ParseException
    544            {
    545                int len = (s == null) ? 0 : s.length();
    546                if (index + length > len)
    547                {
    548                    throw new ParseException("Input string too short: " + s
    549                            + ", index: " + index);
    550                }
    551            }
    552
    553            /**
    554             * Adds a number to the given string buffer and adds leading '0'
    555             * characters until the given length is reached.
    556             *
    557             * @param buf the target buffer
    558             * @param num the number to add
    559             * @param length the required length
    560             */
    561            protected void padNum(StringBuilder buf, int num, int length)
    562            {
    563                buf.append(StringUtils.leftPad(String.valueOf(num), length,
    564                        PAD_CHAR));
    565            }
    566        }
    567
    568        /**
    569         * A specialized date component parser implementation that deals with
    570         * numeric calendar fields. The class is able to extract fields from a
    571         * string literal and to format a literal from a calendar.
    572         */
    573        private static class DateFieldParser extends DateComponentParser
    574        {
    575            /** Stores the calendar field to be processed. */
    576            private int calendarField;
    577
    578            /** Stores the length of this field. */
    579            private int length;
    580
    581            /** An optional offset to add to the calendar field. */
    582            private int offset;
    583
    584            /**
    585             * Creates a new instance of {@code DateFieldParser}.
    586             *
    587             * @param calFld the calendar field code
    588             * @param len the length of this field
    589             */
    590            public DateFieldParser(int calFld, int len)
    591            {
    592                this(calFld, len, 0);
    593            }
    594
    595            /**
    596             * Creates a new instance of {@code DateFieldParser} and fully
    597             * initializes it.
    598             *
    599             * @param calFld the calendar field code
    600             * @param len the length of this field
    601             * @param ofs an offset to add to the calendar field
    602             */
    603            public DateFieldParser(int calFld, int len, int ofs)
    604            {
    605                calendarField = calFld;
    606                length = len;
    607                offset = ofs;
    608            }
    609
    610            @Override
    611            public void formatComponent(StringBuilder buf, Calendar cal)
    612            {
    613                padNum(buf, cal.get(calendarField) + offset, length);
    614            }
    615
    616            @Override
    617            public int parseComponent(String s, int index, Calendar cal)
    618                    throws ParseException
    619            {
    620                checkLength(s, index, length);
    621                try
    622                {
    623                    cal.set(calendarField, Integer.parseInt(s.substring(index,
    624                            index + length))
    625                            - offset);
    626                    return length;
    627                }
    628                catch (NumberFormatException nfex)
    629                {
    630                    throw new ParseException("Invalid number: " + s + ", index "
    631                            + index);
    632                }
    633            }
    634        }
    635
    636        /**
    637         * A specialized date component parser implementation that deals with
    638         * separator characters.
    639         */
    640        private static class DateSeparatorParser extends DateComponentParser
    641        {
    642            /** Stores the separator. */
    643            private String separator;
    644
    645            /**
    646             * Creates a new instance of {@code DateSeparatorParser} and sets
    647             * the separator string.
    648             *
    649             * @param sep the separator string
    650             */
    651            public DateSeparatorParser(String sep)
    652            {
    653                separator = sep;
    654            }
    655
    656            @Override
    657            public void formatComponent(StringBuilder buf, Calendar cal)
    658            {
    659                buf.append(separator);
    660            }
    661
    662            @Override
    663            public int parseComponent(String s, int index, Calendar cal)
    664                    throws ParseException
    665            {
    666                checkLength(s, index, separator.length());
    667                if (!s.startsWith(separator, index))
    668                {
    669                    throw new ParseException("Invalid input: " + s + ", index "
    670                            + index + ", expected " + separator);
    671                }
    672                return separator.length();
    673            }
    674        }
    675
    676        /**
    677         * A specialized date component parser implementation that deals with the
    678         * time zone part of a date component.
    679         */
    680        private static class DateTimeZoneParser extends DateComponentParser
    681        {
    682            @Override
    683            public void formatComponent(StringBuilder buf, Calendar cal)
    684            {
    685                TimeZone tz = cal.getTimeZone();
    686                int ofs = tz.getRawOffset() / MILLIS_PER_MINUTE;
    687                if (ofs < 0)
    688                {
    689                    buf.append('-');
    690                    ofs = -ofs;
    691                }
    692                else
    693                {
    694                    buf.append('+');
    695                }
    696                int hour = ofs / MINUTES_PER_HOUR;
    697                int min = ofs % MINUTES_PER_HOUR;
    698                padNum(buf, hour, 2);
    699                padNum(buf, min, 2);
    700            }
    701
    702            @Override
    703            public int parseComponent(String s, int index, Calendar cal)
    704                    throws ParseException
    705            {
    706                checkLength(s, index, TIME_ZONE_LENGTH);
    707                TimeZone tz = TimeZone.getTimeZone(TIME_ZONE_PREFIX
    708                        + s.substring(index, index + TIME_ZONE_LENGTH));
    709                cal.setTimeZone(tz);
    710                return TIME_ZONE_LENGTH;
    711            }
    712        }
    713    }