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    }