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