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.math.BigDecimal;
025    import java.math.BigInteger;
026    import java.net.URL;
027    import java.text.DateFormat;
028    import java.text.ParseException;
029    import java.text.SimpleDateFormat;
030    import java.util.ArrayList;
031    import java.util.Calendar;
032    import java.util.Collection;
033    import java.util.Date;
034    import java.util.HashMap;
035    import java.util.Iterator;
036    import java.util.List;
037    import java.util.Map;
038    import java.util.TimeZone;
039    
040    import javax.xml.parsers.SAXParser;
041    import javax.xml.parsers.SAXParserFactory;
042    
043    import org.apache.commons.codec.binary.Base64;
044    import org.apache.commons.configuration.AbstractHierarchicalFileConfiguration;
045    import org.apache.commons.configuration.Configuration;
046    import org.apache.commons.configuration.ConfigurationException;
047    import org.apache.commons.configuration.HierarchicalConfiguration;
048    import org.apache.commons.configuration.MapConfiguration;
049    import org.apache.commons.configuration.tree.ConfigurationNode;
050    import org.apache.commons.lang.StringEscapeUtils;
051    import org.apache.commons.lang.StringUtils;
052    import org.xml.sax.Attributes;
053    import org.xml.sax.EntityResolver;
054    import org.xml.sax.InputSource;
055    import org.xml.sax.SAXException;
056    import 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 1210644 2011-12-05 21:20:39Z oheger $
123     */
124    public 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 static 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                        ((PListNode) peek()).addDateValue(buffer.toString());
582                    }
583                    else if ("array".equals(qName))
584                    {
585                        ArrayNode array = (ArrayNode) pop();
586                        ((PListNode) peek()).addList(array);
587                    }
588    
589                    // remove the plist node on the stack once the value has been parsed,
590                    // array nodes remains on the stack for the next values in the list
591                    if (!(peek() instanceof ArrayNode))
592                    {
593                        pop();
594                    }
595                }
596    
597                buffer.setLength(0);
598            }
599    
600            @Override
601            public void characters(char[] ch, int start, int length) throws SAXException
602            {
603                buffer.append(ch, start, length);
604            }
605        }
606    
607        /**
608         * Node extension with addXXX methods to parse the typed data passed by the SAX handler.
609         * <b>Do not use this class !</b> It is used internally by XMLPropertyConfiguration
610         * to parse the configuration file, it may be removed at any moment in the future.
611         */
612        public static class PListNode extends Node
613        {
614            /**
615             * The serial version UID.
616             */
617            private static final long serialVersionUID = -7614060264754798317L;
618    
619            /** The MacOS format of dates in plist files. */
620            private static DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
621            static
622            {
623                format.setTimeZone(TimeZone.getTimeZone("UTC"));
624            }
625    
626            /** The GNUstep format of dates in plist files. */
627            private static DateFormat gnustepFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
628    
629            /**
630             * Update the value of the node. If the existing value is null, it's
631             * replaced with the new value. If the existing value is a list, the
632             * specified value is appended to the list. If the existing value is
633             * not null, a list with the two values is built.
634             *
635             * @param value the value to be added
636             */
637            public void addValue(Object value)
638            {
639                if (getValue() == null)
640                {
641                    setValue(value);
642                }
643                else if (getValue() instanceof Collection)
644                {
645                    // This is safe because we create the collections ourselves
646                    @SuppressWarnings("unchecked")
647                    Collection<Object> collection = (Collection<Object>) getValue();
648                    collection.add(value);
649                }
650                else
651                {
652                    List<Object> list = new ArrayList<Object>();
653                    list.add(getValue());
654                    list.add(value);
655                    setValue(list);
656                }
657            }
658    
659            /**
660             * Parse the specified string as a date and add it to the values of the node.
661             *
662             * @param value the value to be added
663             */
664            public void addDateValue(String value)
665            {
666                try
667                {
668                    if (value.indexOf(' ') != -1)
669                    {
670                        // parse the date using the GNUstep format
671                        synchronized (gnustepFormat)
672                        {
673                            addValue(gnustepFormat.parse(value));
674                        }
675                    }
676                    else
677                    {
678                        // parse the date using the MacOS X format
679                        synchronized (format)
680                        {
681                            addValue(format.parse(value));
682                        }
683                    }
684                }
685                catch (ParseException e)
686                {
687                    // ignore
688                    ;
689                }
690            }
691    
692            /**
693             * Parse the specified string as a byte array in base 64 format
694             * and add it to the values of the node.
695             *
696             * @param value the value to be added
697             */
698            public void addDataValue(String value)
699            {
700                addValue(Base64.decodeBase64(value.getBytes()));
701            }
702    
703            /**
704             * Parse the specified string as an Interger and add it to the values of the node.
705             *
706             * @param value the value to be added
707             */
708            public void addIntegerValue(String value)
709            {
710                addValue(new BigInteger(value));
711            }
712    
713            /**
714             * Parse the specified string as a Double and add it to the values of the node.
715             *
716             * @param value the value to be added
717             */
718            public void addRealValue(String value)
719            {
720                addValue(new BigDecimal(value));
721            }
722    
723            /**
724             * Add a boolean value 'true' to the values of the node.
725             */
726            public void addTrueValue()
727            {
728                addValue(Boolean.TRUE);
729            }
730    
731            /**
732             * Add a boolean value 'false' to the values of the node.
733             */
734            public void addFalseValue()
735            {
736                addValue(Boolean.FALSE);
737            }
738    
739            /**
740             * Add a sublist to the values of the node.
741             *
742             * @param node the node whose value will be added to the current node value
743             */
744            public void addList(ArrayNode node)
745            {
746                addValue(node.getValue());
747            }
748        }
749    
750        /**
751         * Container for array elements. <b>Do not use this class !</b>
752         * It is used internally by XMLPropertyConfiguration to parse the
753         * configuration file, it may be removed at any moment in the future.
754         */
755        public static class ArrayNode extends PListNode
756        {
757            /**
758             * The serial version UID.
759             */
760            private static final long serialVersionUID = 5586544306664205835L;
761    
762            /** The list of values in the array. */
763            private List<Object> list = new ArrayList<Object>();
764    
765            /**
766             * Add an object to the array.
767             *
768             * @param value the value to be added
769             */
770            @Override
771            public void addValue(Object value)
772            {
773                list.add(value);
774            }
775    
776            /**
777             * Return the list of values in the array.
778             *
779             * @return the {@link List} of values
780             */
781            @Override
782            public Object getValue()
783            {
784                return list;
785            }
786        }
787    }