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/Articles/OldStylePListsConcept.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: 628705 $, $Date: 2008-02-18 13:37:19 +0100 (Mo, 18 Feb 2008) $
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            // get all non trivial nodes
256            List children = new ArrayList(node.getChildren());
257            Iterator it = children.iterator();
258            while (it.hasNext())
259            {
260                Node child = (Node) it.next();
261                if (child.getValue() == null && (child.getChildren() == null || child.getChildren().isEmpty()))
262                {
263                    it.remove();
264                }
265            }
266    
267            if (!children.isEmpty())
268            {
269                // skip a line, except for the root dictionary
270                if (indentLevel > 0)
271                {
272                    out.println();
273                }
274    
275                out.println(padding + "{");
276    
277                // display the children
278                it = children.iterator();
279                while (it.hasNext())
280                {
281                    Node child = (Node) it.next();
282    
283                    printNode(out, indentLevel + 1, child);
284    
285                    // add a semi colon for elements that are not dictionaries
286                    Object value = child.getValue();
287                    if (value != null && !(value instanceof Map) && !(value instanceof Configuration))
288                    {
289                        out.println(";");
290                    }
291    
292                    // skip a line after arrays and dictionaries
293                    if (it.hasNext() && (value == null || value instanceof List))
294                    {
295                        out.println();
296                    }
297                }
298    
299                out.print(padding + "}");
300    
301                // line feed if the dictionary is not in an array
302                if (node.getParent() != 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    }