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 1368665 2012-08-02 19:48:26Z 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 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    }