    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    }