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