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