001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.configuration.plist;
019
020import java.io.File;
021import java.io.PrintWriter;
022import java.io.Reader;
023import java.io.Writer;
024import java.net.URL;
025import java.util.ArrayList;
026import java.util.Calendar;
027import java.util.Date;
028import java.util.HashMap;
029import java.util.Iterator;
030import java.util.List;
031import java.util.Map;
032import java.util.TimeZone;
033
034import org.apache.commons.codec.binary.Hex;
035import org.apache.commons.configuration.AbstractHierarchicalFileConfiguration;
036import org.apache.commons.configuration.Configuration;
037import org.apache.commons.configuration.ConfigurationException;
038import org.apache.commons.configuration.HierarchicalConfiguration;
039import org.apache.commons.configuration.MapConfiguration;
040import org.apache.commons.configuration.tree.ConfigurationNode;
041import 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 */
085public 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}