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.math.BigDecimal;
025import java.math.BigInteger;
026import java.net.URL;
027import java.text.DateFormat;
028import java.text.ParseException;
029import java.text.SimpleDateFormat;
030import java.util.ArrayList;
031import java.util.Calendar;
032import java.util.Collection;
033import java.util.Date;
034import java.util.HashMap;
035import java.util.Iterator;
036import java.util.List;
037import java.util.Map;
038import java.util.TimeZone;
039
040import javax.xml.parsers.SAXParser;
041import javax.xml.parsers.SAXParserFactory;
042
043import org.apache.commons.codec.binary.Base64;
044import org.apache.commons.configuration.AbstractHierarchicalFileConfiguration;
045import org.apache.commons.configuration.Configuration;
046import org.apache.commons.configuration.ConfigurationException;
047import org.apache.commons.configuration.HierarchicalConfiguration;
048import org.apache.commons.configuration.MapConfiguration;
049import org.apache.commons.configuration.tree.ConfigurationNode;
050import org.apache.commons.lang.StringEscapeUtils;
051import org.apache.commons.lang.StringUtils;
052import org.xml.sax.Attributes;
053import org.xml.sax.EntityResolver;
054import org.xml.sax.InputSource;
055import org.xml.sax.SAXException;
056import org.xml.sax.helpers.DefaultHandler;
057
058/**
059 * Property list file (plist) in XML FORMAT as used by Mac OS X (http://www.apple.com/DTDs/PropertyList-1.0.dtd).
060 * This configuration doesn't support the binary FORMAT used in OS X 10.4.
061 *
062 * <p>Example:</p>
063 * <pre>
064 * &lt;?xml version="1.0"?>
065 * &lt;!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
066 * &lt;plist version="1.0">
067 *     &lt;dict>
068 *         &lt;key>string&lt;/key>
069 *         &lt;string>value1&lt;/string>
070 *
071 *         &lt;key>integer&lt;/key>
072 *         &lt;integer>12345&lt;/integer>
073 *
074 *         &lt;key>real&lt;/key>
075 *         &lt;real>-123.45E-1&lt;/real>
076 *
077 *         &lt;key>boolean&lt;/key>
078 *         &lt;true/>
079 *
080 *         &lt;key>date&lt;/key>
081 *         &lt;date>2005-01-01T12:00:00Z&lt;/date>
082 *
083 *         &lt;key>data&lt;/key>
084 *         &lt;data>RHJhY28gRG9ybWllbnMgTnVucXVhbSBUaXRpbGxhbmR1cw==&lt;/data>
085 *
086 *         &lt;key>array&lt;/key>
087 *         &lt;array>
088 *             &lt;string>value1&lt;/string>
089 *             &lt;string>value2&lt;/string>
090 *             &lt;string>value3&lt;/string>
091 *         &lt;/array>
092 *
093 *         &lt;key>dictionnary&lt;/key>
094 *         &lt;dict>
095 *             &lt;key>key1&lt;/key>
096 *             &lt;string>value1&lt;/string>
097 *             &lt;key>key2&lt;/key>
098 *             &lt;string>value2&lt;/string>
099 *             &lt;key>key3&lt;/key>
100 *             &lt;string>value3&lt;/string>
101 *         &lt;/dict>
102 *
103 *         &lt;key>nested&lt;/key>
104 *         &lt;dict>
105 *             &lt;key>node1&lt;/key>
106 *             &lt;dict>
107 *                 &lt;key>node2&lt;/key>
108 *                 &lt;dict>
109 *                     &lt;key>node3&lt;/key>
110 *                     &lt;string>value&lt;/string>
111 *                 &lt;/dict>
112 *             &lt;/dict>
113 *         &lt;/dict>
114 *
115 *     &lt;/dict>
116 * &lt;/plist>
117 * </pre>
118 *
119 * @since 1.2
120 *
121 * @author Emmanuel Bourg
122 * @version $Id: XMLPropertyListConfiguration.java 1368665 2012-08-02 19:48:26Z oheger $
123 */
124public class XMLPropertyListConfiguration extends AbstractHierarchicalFileConfiguration
125{
126    /**
127     * The serial version UID.
128     */
129    private static final long serialVersionUID = -3162063751042475985L;
130
131    /** Size of the indentation for the generated file. */
132    private static final int INDENT_SIZE = 4;
133
134    /**
135     * Creates an empty XMLPropertyListConfiguration object which can be
136     * used to synthesize a new plist file by adding values and
137     * then saving().
138     */
139    public XMLPropertyListConfiguration()
140    {
141        initRoot();
142    }
143
144    /**
145     * Creates a new instance of {@code XMLPropertyListConfiguration} and
146     * copies the content of the specified configuration into this object.
147     *
148     * @param configuration the configuration to copy
149     * @since 1.4
150     */
151    public XMLPropertyListConfiguration(HierarchicalConfiguration configuration)
152    {
153        super(configuration);
154    }
155
156    /**
157     * Creates and loads the property list from the specified file.
158     *
159     * @param fileName The name of the plist file to load.
160     * @throws org.apache.commons.configuration.ConfigurationException Error
161     * while loading the plist file
162     */
163    public XMLPropertyListConfiguration(String fileName) throws ConfigurationException
164    {
165        super(fileName);
166    }
167
168    /**
169     * Creates and loads the property list from the specified file.
170     *
171     * @param file The plist file to load.
172     * @throws ConfigurationException Error while loading the plist file
173     */
174    public XMLPropertyListConfiguration(File file) throws ConfigurationException
175    {
176        super(file);
177    }
178
179    /**
180     * Creates and loads the property list from the specified URL.
181     *
182     * @param url The location of the plist file to load.
183     * @throws ConfigurationException Error while loading the plist file
184     */
185    public XMLPropertyListConfiguration(URL url) throws ConfigurationException
186    {
187        super(url);
188    }
189
190    @Override
191    public void setProperty(String key, Object value)
192    {
193        // special case for byte arrays, they must be stored as is in the configuration
194        if (value instanceof byte[])
195        {
196            fireEvent(EVENT_SET_PROPERTY, key, value, true);
197            setDetailEvents(false);
198            try
199            {
200                clearProperty(key);
201                addPropertyDirect(key, value);
202            }
203            finally
204            {
205                setDetailEvents(true);
206            }
207            fireEvent(EVENT_SET_PROPERTY, key, value, false);
208        }
209        else
210        {
211            super.setProperty(key, value);
212        }
213    }
214
215    @Override
216    public void addProperty(String key, Object value)
217    {
218        if (value instanceof byte[])
219        {
220            fireEvent(EVENT_ADD_PROPERTY, key, value, true);
221            addPropertyDirect(key, value);
222            fireEvent(EVENT_ADD_PROPERTY, key, value, false);
223        }
224        else
225        {
226            super.addProperty(key, value);
227        }
228    }
229
230    public void load(Reader in) throws ConfigurationException
231    {
232        // We have to make sure that the root node is actually a PListNode.
233        // If this object was not created using the standard constructor, the
234        // root node is a plain Node.
235        if (!(getRootNode() instanceof PListNode))
236        {
237            initRoot();
238        }
239
240        // set up the DTD validation
241        EntityResolver resolver = new EntityResolver()
242        {
243            public InputSource resolveEntity(String publicId, String systemId)
244            {
245                return new InputSource(getClass().getClassLoader().getResourceAsStream("PropertyList-1.0.dtd"));
246            }
247        };
248
249        // parse the file
250        XMLPropertyListHandler handler = new XMLPropertyListHandler(getRoot());
251        try
252        {
253            SAXParserFactory factory = SAXParserFactory.newInstance();
254            factory.setValidating(true);
255
256            SAXParser parser = factory.newSAXParser();
257            parser.getXMLReader().setEntityResolver(resolver);
258            parser.getXMLReader().setContentHandler(handler);
259            parser.getXMLReader().parse(new InputSource(in));
260        }
261        catch (Exception e)
262        {
263            throw new ConfigurationException("Unable to parse the configuration file", e);
264        }
265    }
266
267    public void save(Writer out) throws ConfigurationException
268    {
269        PrintWriter writer = new PrintWriter(out);
270
271        if (getEncoding() != null)
272        {
273            writer.println("<?xml version=\"1.0\" encoding=\"" + getEncoding() + "\"?>");
274        }
275        else
276        {
277            writer.println("<?xml version=\"1.0\"?>");
278        }
279
280        writer.println("<!DOCTYPE plist SYSTEM \"file://localhost/System/Library/DTDs/PropertyList.dtd\">");
281        writer.println("<plist version=\"1.0\">");
282
283        printNode(writer, 1, getRoot());
284
285        writer.println("</plist>");
286        writer.flush();
287    }
288
289    /**
290     * Append a node to the writer, indented according to a specific level.
291     */
292    private void printNode(PrintWriter out, int indentLevel, ConfigurationNode node)
293    {
294        String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
295
296        if (node.getName() != null)
297        {
298            out.println(padding + "<key>" + StringEscapeUtils.escapeXml(node.getName()) + "</key>");
299        }
300
301        List<ConfigurationNode> children = node.getChildren();
302        if (!children.isEmpty())
303        {
304            out.println(padding + "<dict>");
305
306            Iterator<ConfigurationNode> it = children.iterator();
307            while (it.hasNext())
308            {
309                ConfigurationNode child = it.next();
310                printNode(out, indentLevel + 1, child);
311
312                if (it.hasNext())
313                {
314                    out.println();
315                }
316            }
317
318            out.println(padding + "</dict>");
319        }
320        else if (node.getValue() == null)
321        {
322            out.println(padding + "<dict/>");
323        }
324        else
325        {
326            Object value = node.getValue();
327            printValue(out, indentLevel, value);
328        }
329    }
330
331    /**
332     * Append a value to the writer, indented according to a specific level.
333     */
334    private void printValue(PrintWriter out, int indentLevel, Object value)
335    {
336        String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
337
338        if (value instanceof Date)
339        {
340            synchronized (PListNode.FORMAT)
341            {
342                out.println(padding + "<date>" + PListNode.FORMAT.format((Date) value) + "</date>");
343            }
344        }
345        else if (value instanceof Calendar)
346        {
347            printValue(out, indentLevel, ((Calendar) value).getTime());
348        }
349        else if (value instanceof Number)
350        {
351            if (value instanceof Double || value instanceof Float || value instanceof BigDecimal)
352            {
353                out.println(padding + "<real>" + value.toString() + "</real>");
354            }
355            else
356            {
357                out.println(padding + "<integer>" + value.toString() + "</integer>");
358            }
359        }
360        else if (value instanceof Boolean)
361        {
362            if (((Boolean) value).booleanValue())
363            {
364                out.println(padding + "<true/>");
365            }
366            else
367            {
368                out.println(padding + "<false/>");
369            }
370        }
371        else if (value instanceof List)
372        {
373            out.println(padding + "<array>");
374            Iterator<?> it = ((List<?>) value).iterator();
375            while (it.hasNext())
376            {
377                printValue(out, indentLevel + 1, it.next());
378            }
379            out.println(padding + "</array>");
380        }
381        else if (value instanceof HierarchicalConfiguration)
382        {
383            printNode(out, indentLevel, ((HierarchicalConfiguration) value).getRoot());
384        }
385        else if (value instanceof Configuration)
386        {
387            // display a flat Configuration as a dictionary
388            out.println(padding + "<dict>");
389
390            Configuration config = (Configuration) value;
391            Iterator<String> it = config.getKeys();
392            while (it.hasNext())
393            {
394                // create a node for each property
395                String key = it.next();
396                Node node = new Node(key);
397                node.setValue(config.getProperty(key));
398
399                // print the node
400                printNode(out, indentLevel + 1, node);
401
402                if (it.hasNext())
403                {
404                    out.println();
405                }
406            }
407            out.println(padding + "</dict>");
408        }
409        else if (value instanceof Map)
410        {
411            // display a Map as a dictionary
412            Map<String, Object> map = transformMap((Map<?, ?>) value);
413            printValue(out, indentLevel, new MapConfiguration(map));
414        }
415        else if (value instanceof byte[])
416        {
417            String base64 = new String(Base64.encodeBase64((byte[]) value));
418            out.println(padding + "<data>" + StringEscapeUtils.escapeXml(base64) + "</data>");
419        }
420        else if (value != null)
421        {
422            out.println(padding + "<string>" + StringEscapeUtils.escapeXml(String.valueOf(value)) + "</string>");
423        }
424        else
425        {
426            out.println(padding + "<string/>");
427        }
428    }
429
430    /**
431     * Helper method for initializing the configuration's root node.
432     */
433    private void initRoot()
434    {
435        setRootNode(new PListNode());
436    }
437
438    /**
439     * Transform a map of arbitrary types into a map with string keys and object
440     * values. All keys of the source map which are not of type String are
441     * dropped.
442     *
443     * @param src the map to be converted
444     * @return the resulting map
445     */
446    private static Map<String, Object> transformMap(Map<?, ?> src)
447    {
448        Map<String, Object> dest = new HashMap<String, Object>();
449        for (Map.Entry<?, ?> e : src.entrySet())
450        {
451            if (e.getKey() instanceof String)
452            {
453                dest.put((String) e.getKey(), e.getValue());
454            }
455        }
456        return dest;
457    }
458
459    /**
460     * SAX Handler to build the configuration nodes while the document is being parsed.
461     */
462    private class XMLPropertyListHandler extends DefaultHandler
463    {
464        /** The buffer containing the text node being read */
465        private StringBuilder buffer = new StringBuilder();
466
467        /** The stack of configuration nodes */
468        private List<Node> stack = new ArrayList<Node>();
469
470        public XMLPropertyListHandler(Node root)
471        {
472            push(root);
473        }
474
475        /**
476         * Return the node on the top of the stack.
477         */
478        private Node peek()
479        {
480            if (!stack.isEmpty())
481            {
482                return stack.get(stack.size() - 1);
483            }
484            else
485            {
486                return null;
487            }
488        }
489
490        /**
491         * Remove and return the node on the top of the stack.
492         */
493        private Node pop()
494        {
495            if (!stack.isEmpty())
496            {
497                return stack.remove(stack.size() - 1);
498            }
499            else
500            {
501                return null;
502            }
503        }
504
505        /**
506         * Put a node on the top of the stack.
507         */
508        private void push(Node node)
509        {
510            stack.add(node);
511        }
512
513        @Override
514        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException
515        {
516            if ("array".equals(qName))
517            {
518                push(new ArrayNode());
519            }
520            else if ("dict".equals(qName))
521            {
522                if (peek() instanceof ArrayNode)
523                {
524                    // create the configuration
525                    XMLPropertyListConfiguration config = new XMLPropertyListConfiguration();
526
527                    // add it to the ArrayNode
528                    ArrayNode node = (ArrayNode) peek();
529                    node.addValue(config);
530
531                    // push the root on the stack
532                    push(config.getRoot());
533                }
534            }
535        }
536
537        @Override
538        public void endElement(String uri, String localName, String qName) throws SAXException
539        {
540            if ("key".equals(qName))
541            {
542                // create a new node, link it to its parent and push it on the stack
543                PListNode node = new PListNode();
544                node.setName(buffer.toString());
545                peek().addChild(node);
546                push(node);
547            }
548            else if ("dict".equals(qName))
549            {
550                // remove the root of the XMLPropertyListConfiguration previously pushed on the stack
551                pop();
552            }
553            else
554            {
555                if ("string".equals(qName))
556                {
557                    ((PListNode) peek()).addValue(buffer.toString());
558                }
559                else if ("integer".equals(qName))
560                {
561                    ((PListNode) peek()).addIntegerValue(buffer.toString());
562                }
563                else if ("real".equals(qName))
564                {
565                    ((PListNode) peek()).addRealValue(buffer.toString());
566                }
567                else if ("true".equals(qName))
568                {
569                    ((PListNode) peek()).addTrueValue();
570                }
571                else if ("false".equals(qName))
572                {
573                    ((PListNode) peek()).addFalseValue();
574                }
575                else if ("data".equals(qName))
576                {
577                    ((PListNode) peek()).addDataValue(buffer.toString());
578                }
579                else if ("date".equals(qName))
580                {
581                    try
582                    {
583                        ((PListNode) peek()).addDateValue(buffer.toString());
584                    }
585                    catch (IllegalArgumentException iex)
586                    {
587                        getLogger().warn(
588                                "Ignoring invalid date property " + buffer);
589                    }
590                }
591                else if ("array".equals(qName))
592                {
593                    ArrayNode array = (ArrayNode) pop();
594                    ((PListNode) peek()).addList(array);
595                }
596
597                // remove the plist node on the stack once the value has been parsed,
598                // array nodes remains on the stack for the next values in the list
599                if (!(peek() instanceof ArrayNode))
600                {
601                    pop();
602                }
603            }
604
605            buffer.setLength(0);
606        }
607
608        @Override
609        public void characters(char[] ch, int start, int length) throws SAXException
610        {
611            buffer.append(ch, start, length);
612        }
613    }
614
615    /**
616     * Node extension with addXXX methods to parse the typed data passed by the SAX handler.
617     * <b>Do not use this class !</b> It is used internally by XMLPropertyConfiguration
618     * to parse the configuration file, it may be removed at any moment in the future.
619     */
620    public static class PListNode extends Node
621    {
622        /**
623         * The serial version UID.
624         */
625        private static final long serialVersionUID = -7614060264754798317L;
626
627        /**
628         * The MacOS FORMAT of dates in plist files. Note: Because
629         * {@code SimpleDateFormat} is not thread-safe, each access has to be
630         * synchronized.
631         */
632        private static final DateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
633        static
634        {
635            FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
636        }
637
638        /**
639         * The GNUstep FORMAT of dates in plist files. Note: Because
640         * {@code SimpleDateFormat} is not thread-safe, each access has to be
641         * synchronized.
642         */
643        private static final DateFormat GNUSTEP_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
644
645        /**
646         * Update the value of the node. If the existing value is null, it's
647         * replaced with the new value. If the existing value is a list, the
648         * specified value is appended to the list. If the existing value is
649         * not null, a list with the two values is built.
650         *
651         * @param value the value to be added
652         */
653        public void addValue(Object value)
654        {
655            if (getValue() == null)
656            {
657                setValue(value);
658            }
659            else if (getValue() instanceof Collection)
660            {
661                // This is safe because we create the collections ourselves
662                @SuppressWarnings("unchecked")
663                Collection<Object> collection = (Collection<Object>) getValue();
664                collection.add(value);
665            }
666            else
667            {
668                List<Object> list = new ArrayList<Object>();
669                list.add(getValue());
670                list.add(value);
671                setValue(list);
672            }
673        }
674
675        /**
676         * Parse the specified string as a date and add it to the values of the node.
677         *
678         * @param value the value to be added
679         * @throws IllegalArgumentException if the date string cannot be parsed
680         */
681        public void addDateValue(String value)
682        {
683            try
684            {
685                if (value.indexOf(' ') != -1)
686                {
687                    // parse the date using the GNUstep FORMAT
688                    synchronized (GNUSTEP_FORMAT)
689                    {
690                        addValue(GNUSTEP_FORMAT.parse(value));
691                    }
692                }
693                else
694                {
695                    // parse the date using the MacOS X FORMAT
696                    synchronized (FORMAT)
697                    {
698                        addValue(FORMAT.parse(value));
699                    }
700                }
701            }
702            catch (ParseException e)
703            {
704                throw new IllegalArgumentException(String.format(
705                        "'%s' cannot be parsed to a date!", value), e);
706            }
707        }
708
709        /**
710         * Parse the specified string as a byte array in base 64 FORMAT
711         * and add it to the values of the node.
712         *
713         * @param value the value to be added
714         */
715        public void addDataValue(String value)
716        {
717            addValue(Base64.decodeBase64(value.getBytes()));
718        }
719
720        /**
721         * Parse the specified string as an Interger and add it to the values of the node.
722         *
723         * @param value the value to be added
724         */
725        public void addIntegerValue(String value)
726        {
727            addValue(new BigInteger(value));
728        }
729
730        /**
731         * Parse the specified string as a Double and add it to the values of the node.
732         *
733         * @param value the value to be added
734         */
735        public void addRealValue(String value)
736        {
737            addValue(new BigDecimal(value));
738        }
739
740        /**
741         * Add a boolean value 'true' to the values of the node.
742         */
743        public void addTrueValue()
744        {
745            addValue(Boolean.TRUE);
746        }
747
748        /**
749         * Add a boolean value 'false' to the values of the node.
750         */
751        public void addFalseValue()
752        {
753            addValue(Boolean.FALSE);
754        }
755
756        /**
757         * Add a sublist to the values of the node.
758         *
759         * @param node the node whose value will be added to the current node value
760         */
761        public void addList(ArrayNode node)
762        {
763            addValue(node.getValue());
764        }
765    }
766
767    /**
768     * Container for array elements. <b>Do not use this class !</b>
769     * It is used internally by XMLPropertyConfiguration to parse the
770     * configuration file, it may be removed at any moment in the future.
771     */
772    public static class ArrayNode extends PListNode
773    {
774        /**
775         * The serial version UID.
776         */
777        private static final long serialVersionUID = 5586544306664205835L;
778
779        /** The list of values in the array. */
780        private List<Object> list = new ArrayList<Object>();
781
782        /**
783         * Add an object to the array.
784         *
785         * @param value the value to be added
786         */
787        @Override
788        public void addValue(Object value)
789        {
790            list.add(value);
791        }
792
793        /**
794         * Return the list of values in the array.
795         *
796         * @return the {@link List} of values
797         */
798        @Override
799        public Object getValue()
800        {
801            return list;
802        }
803    }
804}