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