    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    }