    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
    018package org.apache.commons.configuration2.plist;
    019
    020import java.io.PrintWriter;
    021import java.io.Reader;
    022import java.io.UnsupportedEncodingException;
    023import java.io.Writer;
    024import java.math.BigDecimal;
    025import java.math.BigInteger;
    026import java.text.DateFormat;
    027import java.text.ParseException;
    028import java.text.SimpleDateFormat;
    029import java.util.ArrayList;
    030import java.util.Arrays;
    031import java.util.Calendar;
    032import java.util.Collection;
    033import java.util.Date;
    034import java.util.HashMap;
    035import java.util.Iterator;
    036import java.util.LinkedList;
    037import java.util.List;
    038import java.util.Map;
    039import java.util.TimeZone;
    040
    041import javax.xml.parsers.SAXParser;
    042import javax.xml.parsers.SAXParserFactory;
    043
    044import org.apache.commons.codec.binary.Base64;
    045import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
    046import org.apache.commons.configuration2.FileBasedConfiguration;
    047import org.apache.commons.configuration2.HierarchicalConfiguration;
    048import org.apache.commons.configuration2.ImmutableConfiguration;
    049import org.apache.commons.configuration2.MapConfiguration;
    050import org.apache.commons.configuration2.ex.ConfigurationException;
    051import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
    052import org.apache.commons.configuration2.io.FileLocator;
    053import org.apache.commons.configuration2.io.FileLocatorAware;
    054import org.apache.commons.configuration2.tree.ImmutableNode;
    055import org.apache.commons.configuration2.tree.InMemoryNodeModel;
    056import org.apache.commons.lang3.StringUtils;
    057import org.apache.commons.text.StringEscapeUtils;
    058import org.xml.sax.Attributes;
    059import org.xml.sax.EntityResolver;
    060import org.xml.sax.InputSource;
    061import org.xml.sax.SAXException;
    062import org.xml.sax.helpers.DefaultHandler;
    063
    064/**
    065 * Property list file (plist) in XML FORMAT as used by Mac OS X (http://www.apple.com/DTDs/PropertyList-1.0.dtd).
    066 * This configuration doesn't support the binary FORMAT used in OS X 10.4.
    067 *
    068 * <p>Example:</p>
    069 * <pre>
    070 * &lt;?xml version="1.0"?&gt;
    071 * &lt;!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd"&gt;
    072 * &lt;plist version="1.0"&gt;
    073 *     &lt;dict&gt;
    074 *         &lt;key&gt;string&lt;/key&gt;
    075 *         &lt;string&gt;value1&lt;/string&gt;
    076 *
    077 *         &lt;key&gt;integer&lt;/key&gt;
    078 *         &lt;integer&gt;12345&lt;/integer&gt;
    079 *
    080 *         &lt;key&gt;real&lt;/key&gt;
    081 *         &lt;real&gt;-123.45E-1&lt;/real&gt;
    082 *
    083 *         &lt;key&gt;boolean&lt;/key&gt;
    084 *         &lt;true/&gt;
    085 *
    086 *         &lt;key&gt;date&lt;/key&gt;
    087 *         &lt;date&gt;2005-01-01T12:00:00Z&lt;/date&gt;
    088 *
    089 *         &lt;key&gt;data&lt;/key&gt;
    090 *         &lt;data&gt;RHJhY28gRG9ybWllbnMgTnVucXVhbSBUaXRpbGxhbmR1cw==&lt;/data&gt;
    091 *
    092 *         &lt;key&gt;array&lt;/key&gt;
    093 *         &lt;array&gt;
    094 *             &lt;string&gt;value1&lt;/string&gt;
    095 *             &lt;string&gt;value2&lt;/string&gt;
    096 *             &lt;string&gt;value3&lt;/string&gt;
    097 *         &lt;/array&gt;
    098 *
    099 *         &lt;key&gt;dictionnary&lt;/key&gt;
    100 *         &lt;dict&gt;
    101 *             &lt;key&gt;key1&lt;/key&gt;
    102 *             &lt;string&gt;value1&lt;/string&gt;
    103 *             &lt;key&gt;key2&lt;/key&gt;
    104 *             &lt;string&gt;value2&lt;/string&gt;
    105 *             &lt;key&gt;key3&lt;/key&gt;
    106 *             &lt;string&gt;value3&lt;/string&gt;
    107 *         &lt;/dict&gt;
    108 *
    109 *         &lt;key&gt;nested&lt;/key&gt;
    110 *         &lt;dict&gt;
    111 *             &lt;key&gt;node1&lt;/key&gt;
    112 *             &lt;dict&gt;
    113 *                 &lt;key&gt;node2&lt;/key&gt;
    114 *                 &lt;dict&gt;
    115 *                     &lt;key&gt;node3&lt;/key&gt;
    116 *                     &lt;string&gt;value&lt;/string&gt;
    117 *                 &lt;/dict&gt;
    118 *             &lt;/dict&gt;
    119 *         &lt;/dict&gt;
    120 *
    121 *     &lt;/dict&gt;
    122 * &lt;/plist&gt;
    123 * </pre>
    124 *
    125 * @since 1.2
    126 *
    127 * @author Emmanuel Bourg
    128 * @version $Id: XMLPropertyListConfiguration.java 1842194 2018-09-27 22:24:23Z ggregory $
    129 */
    130public class XMLPropertyListConfiguration extends BaseHierarchicalConfiguration
    131    implements FileBasedConfiguration, FileLocatorAware
    132{
    133    /** Size of the indentation for the generated file. */
    134    private static final int INDENT_SIZE = 4;
    135
    136    /** Constant for the encoding for binary data. */
    137    private static final String DATA_ENCODING = "UTF-8";
    138
    139    /** Temporarily stores the current file location. */
    140    private FileLocator locator;
    141
    142    /**
    143     * Creates an empty XMLPropertyListConfiguration object which can be
    144     * used to synthesize a new plist file by adding values and
    145     * then saving().
    146     */
    147    public XMLPropertyListConfiguration()
    148    {
    149    }
    150
    151    /**
    152     * Creates a new instance of {@code XMLPropertyListConfiguration} and
    153     * copies the content of the specified configuration into this object.
    154     *
    155     * @param configuration the configuration to copy
    156     * @since 1.4
    157     */
    158    public XMLPropertyListConfiguration(final HierarchicalConfiguration<ImmutableNode> configuration)
    159    {
    160        super(configuration);
    161    }
    162
    163    /**
    164     * Creates a new instance of {@code XMLPropertyConfiguration} with the given
    165     * root node.
    166     *
    167     * @param root the root node
    168     */
    169    XMLPropertyListConfiguration(final ImmutableNode root)
    170    {
    171        super(new InMemoryNodeModel(root));
    172    }
    173
    174    @Override
    175    protected void setPropertyInternal(final String key, final Object value)
    176    {
    177        // special case for byte arrays, they must be stored as is in the configuration
    178        if (value instanceof byte[])
    179        {
    180            setDetailEvents(false);
    181            try
    182            {
    183                clearProperty(key);
    184                addPropertyDirect(key, value);
    185            }
    186            finally
    187            {
    188                setDetailEvents(true);
    189            }
    190        }
    191        else
    192        {
    193            super.setPropertyInternal(key, value);
    194        }
    195    }
    196
    197    @Override
    198    protected void addPropertyInternal(final String key, final Object value)
    199    {
    200        if (value instanceof byte[] || value instanceof List)
    201        {
    202            addPropertyDirect(key, value);
    203        }
    204        else if (value instanceof Object[])
    205        {
    206            addPropertyDirect(key, Arrays.asList((Object[]) value));
    207        }
    208        else
    209        {
    210            super.addPropertyInternal(key, value);
    211        }
    212    }
    213
    214    /**
    215     * Stores the current file locator. This method is called before I/O
    216     * operations.
    217     *
    218     * @param locator the current {@code FileLocator}
    219     */
    220    @Override
    221    public void initFileLocator(final FileLocator locator)
    222    {
    223        this.locator = locator;
    224    }
    225
    226    @Override
    227    public void read(final Reader in) throws ConfigurationException
    228    {
    229        // set up the DTD validation
    230        final EntityResolver resolver = new EntityResolver()
    231        {
    232            @Override
    233            public InputSource resolveEntity(final String publicId, final String systemId)
    234            {
    235                return new InputSource(getClass().getClassLoader()
    236                        .getResourceAsStream("PropertyList-1.0.dtd"));
    237            }
    238        };
    239
    240        // parse the file
    241        final XMLPropertyListHandler handler = new XMLPropertyListHandler();
    242        try
    243        {
    244            final SAXParserFactory factory = SAXParserFactory.newInstance();
    245            factory.setValidating(true);
    246
    247            final SAXParser parser = factory.newSAXParser();
    248            parser.getXMLReader().setEntityResolver(resolver);
    249            parser.getXMLReader().setContentHandler(handler);
    250            parser.getXMLReader().parse(new InputSource(in));
    251
    252            getNodeModel().mergeRoot(handler.getResultBuilder().createNode(),
    253                    null, null, null, this);
    254        }
    255        catch (final Exception e)
    256        {
    257            throw new ConfigurationException(
    258                    "Unable to parse the configuration file", e);
    259        }
    260    }
    261
    262    @Override
    263    public void write(final Writer out) throws ConfigurationException
    264    {
    265        if (locator == null)
    266        {
    267            throw new ConfigurationException("Save operation not properly "
    268                    + "initialized! Do not call write(Writer) directly,"
    269                    + " but use a FileHandler to save a configuration.");
    270        }
    271        final PrintWriter writer = new PrintWriter(out);
    272
    273        if (locator.getEncoding() != null)
    274        {
    275            writer.println("<?xml version=\"1.0\" encoding=\"" + locator.getEncoding() + "\"?>");
    276        }
    277        else
    278        {
    279            writer.println("<?xml version=\"1.0\"?>");
    280        }
    281
    282        writer.println("<!DOCTYPE plist SYSTEM \"file://localhost/System/Library/DTDs/PropertyList.dtd\">");
    283        writer.println("<plist version=\"1.0\">");
    284
    285        printNode(writer, 1, getNodeModel().getNodeHandler().getRootNode());
    286
    287        writer.println("</plist>");
    288        writer.flush();
    289    }
    290
    291    /**
    292     * Append a node to the writer, indented according to a specific level.
    293     */
    294    private void printNode(final PrintWriter out, final int indentLevel, final ImmutableNode node)
    295    {
    296        final String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
    297
    298        if (node.getNodeName() != null)
    299        {
    300            out.println(padding + "<key>" + StringEscapeUtils.escapeXml10(node.getNodeName()) + "</key>");
    301        }
    302
    303        final List<ImmutableNode> children = node.getChildren();
    304        if (!children.isEmpty())
    305        {
    306            out.println(padding + "<dict>");
    307
    308            final Iterator<ImmutableNode> it = children.iterator();
    309            while (it.hasNext())
    310            {
    311                final ImmutableNode child = it.next();
    312                printNode(out, indentLevel + 1, child);
    313
    314                if (it.hasNext())
    315                {
    316                    out.println();
    317                }
    318            }
    319
    320            out.println(padding + "</dict>");
    321        }
    322        else if (node.getValue() == null)
    323        {
    324            out.println(padding + "<dict/>");
    325        }
    326        else
    327        {
    328            final Object value = node.getValue();
    329            printValue(out, indentLevel, value);
    330        }
    331    }
    332
    333    /**
    334     * Append a value to the writer, indented according to a specific level.
    335     */
    336    private void printValue(final PrintWriter out, final int indentLevel, final Object value)
    337    {
    338        final String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
    339
    340        if (value instanceof Date)
    341        {
    342            synchronized (PListNodeBuilder.FORMAT)
    343            {
    344                out.println(padding + "<date>" + PListNodeBuilder.FORMAT.format((Date) value) + "</date>");
    345            }
    346        }
    347        else if (value instanceof Calendar)
    348        {
    349            printValue(out, indentLevel, ((Calendar) value).getTime());
    350        }
    351        else if (value instanceof Number)
    352        {
    353            if (value instanceof Double || value instanceof Float || value instanceof BigDecimal)
    354            {
    355                out.println(padding + "<real>" + value.toString() + "</real>");
    356            }
    357            else
    358            {
    359                out.println(padding + "<integer>" + value.toString() + "</integer>");
    360            }
    361        }
    362        else if (value instanceof Boolean)
    363        {
    364            if (((Boolean) value).booleanValue())
    365            {
    366                out.println(padding + "<true/>");
    367            }
    368            else
    369            {
    370                out.println(padding + "<false/>");
    371            }
    372        }
    373        else if (value instanceof List)
    374        {
    375            out.println(padding + "<array>");
    376            for (final Object o : (List<?>) value)
    377            {
    378                printValue(out, indentLevel + 1, o);
    379            }
    380            out.println(padding + "</array>");
    381        }
    382        else if (value instanceof HierarchicalConfiguration)
    383        {
    384            // This is safe because we have created this configuration
    385            @SuppressWarnings("unchecked")
    386            final
    387            HierarchicalConfiguration<ImmutableNode> config =
    388                    (HierarchicalConfiguration<ImmutableNode>) value;
    389            printNode(out, indentLevel, config.getNodeModel().getNodeHandler()
    390                    .getRootNode());
    391        }
    392        else if (value instanceof ImmutableConfiguration)
    393        {
    394            // display a flat Configuration as a dictionary
    395            out.println(padding + "<dict>");
    396
    397            final ImmutableConfiguration config = (ImmutableConfiguration) value;
    398            final Iterator<String> it = config.getKeys();
    399            while (it.hasNext())
    400            {
    401                // create a node for each property
    402                final String key = it.next();
    403                final ImmutableNode node =
    404                        new ImmutableNode.Builder().name(key)
    405                                .value(config.getProperty(key)).create();
    406
    407                // print the node
    408                printNode(out, indentLevel + 1, node);
    409
    410                if (it.hasNext())
    411                {
    412                    out.println();
    413                }
    414            }
    415            out.println(padding + "</dict>");
    416        }
    417        else if (value instanceof Map)
    418        {
    419            // display a Map as a dictionary
    420            final Map<String, Object> map = transformMap((Map<?, ?>) value);
    421            printValue(out, indentLevel, new MapConfiguration(map));
    422        }
    423        else if (value instanceof byte[])
    424        {
    425            String base64;
    426            try
    427            {
    428                base64 = new String(Base64.encodeBase64((byte[]) value), DATA_ENCODING);
    429            }
    430            catch (final UnsupportedEncodingException e)
    431            {
    432                // Cannot happen as UTF-8 is a standard encoding
    433                throw new AssertionError(e);
    434            }
    435            out.println(padding + "<data>" + StringEscapeUtils.escapeXml10(base64) + "</data>");
    436        }
    437        else if (value != null)
    438        {
    439            out.println(padding + "<string>" + StringEscapeUtils.escapeXml10(String.valueOf(value)) + "</string>");
    440        }
    441        else
    442        {
    443            out.println(padding + "<string/>");
    444        }
    445    }
    446
    447    /**
    448     * Transform a map of arbitrary types into a map with string keys and object
    449     * values. All keys of the source map which are not of type String are
    450     * dropped.
    451     *
    452     * @param src the map to be converted
    453     * @return the resulting map
    454     */
    455    private static Map<String, Object> transformMap(final Map<?, ?> src)
    456    {
    457        final Map<String, Object> dest = new HashMap<>();
    458        for (final Map.Entry<?, ?> e : src.entrySet())
    459        {
    460            if (e.getKey() instanceof String)
    461            {
    462                dest.put((String) e.getKey(), e.getValue());
    463            }
    464        }
    465        return dest;
    466    }
    467
    468    /**
    469     * SAX Handler to build the configuration nodes while the document is being parsed.
    470     */
    471    private class XMLPropertyListHandler extends DefaultHandler
    472    {
    473        /** The buffer containing the text node being read */
    474        private final StringBuilder buffer = new StringBuilder();
    475
    476        /** The stack of configuration nodes */
    477        private final List<PListNodeBuilder> stack = new ArrayList<>();
    478
    479        /** The builder for the resulting node. */
    480        private final PListNodeBuilder resultBuilder;
    481
    482        public XMLPropertyListHandler()
    483        {
    484            resultBuilder = new PListNodeBuilder();
    485            push(resultBuilder);
    486        }
    487
    488        /**
    489         * Returns the builder for the result node.
    490         *
    491         * @return the result node builder
    492         */
    493        public PListNodeBuilder getResultBuilder()
    494        {
    495            return resultBuilder;
    496        }
    497
    498        /**
    499         * Return the node on the top of the stack.
    500         */
    501        private PListNodeBuilder peek()
    502        {
    503            if (!stack.isEmpty())
    504            {
    505                return stack.get(stack.size() - 1);
    506            }
    507            return null;
    508        }
    509
    510        /**
    511         * Returns the node on top of the non-empty stack. Throws an exception if the
    512         * stack is empty.
    513         *
    514         * @return the top node of the stack
    515         * @throws ConfigurationRuntimeException if the stack is empty
    516         */
    517        private PListNodeBuilder peekNE()
    518        {
    519            final PListNodeBuilder result = peek();
    520            if (result == null)
    521            {
    522                throw new ConfigurationRuntimeException("Access to empty stack!");
    523            }
    524            return result;
    525        }
    526
    527        /**
    528         * Remove and return the node on the top of the stack.
    529         */
    530        private PListNodeBuilder pop()
    531        {
    532            if (!stack.isEmpty())
    533            {
    534                return stack.remove(stack.size() - 1);
    535            }
    536            return null;
    537        }
    538
    539        /**
    540         * Put a node on the top of the stack.
    541         */
    542        private void push(final PListNodeBuilder node)
    543        {
    544            stack.add(node);
    545        }
    546
    547        @Override
    548        public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException
    549        {
    550            if ("array".equals(qName))
    551            {
    552                push(new ArrayNodeBuilder());
    553            }
    554            else if ("dict".equals(qName))
    555            {
    556                if (peek() instanceof ArrayNodeBuilder)
    557                {
    558                    // push the new root builder on the stack
    559                    push(new PListNodeBuilder());
    560                }
    561            }
    562        }
    563
    564        @Override
    565        public void endElement(final String uri, final String localName, final String qName) throws SAXException
    566        {
    567            if ("key".equals(qName))
    568            {
    569                // create a new node, link it to its parent and push it on the stack
    570                final PListNodeBuilder node = new PListNodeBuilder();
    571                node.setName(buffer.toString());
    572                peekNE().addChild(node);
    573                push(node);
    574            }
    575            else if ("dict".equals(qName))
    576            {
    577                // remove the root of the XMLPropertyListConfiguration previously pushed on the stack
    578                final PListNodeBuilder builder = pop();
    579                assert builder != null : "Stack was empty!";
    580                if (peek() instanceof ArrayNodeBuilder)
    581                {
    582                    // create the configuration
    583                    final XMLPropertyListConfiguration config = new XMLPropertyListConfiguration(builder.createNode());
    584
    585                    // add it to the ArrayNodeBuilder
    586                    final ArrayNodeBuilder node = (ArrayNodeBuilder) peekNE();
    587                    node.addValue(config);
    588                }
    589            }
    590            else
    591            {
    592                if ("string".equals(qName))
    593                {
    594                    peekNE().addValue(buffer.toString());
    595                }
    596                else if ("integer".equals(qName))
    597                {
    598                    peekNE().addIntegerValue(buffer.toString());
    599                }
    600                else if ("real".equals(qName))
    601                {
    602                    peekNE().addRealValue(buffer.toString());
    603                }
    604                else if ("true".equals(qName))
    605                {
    606                    peekNE().addTrueValue();
    607                }
    608                else if ("false".equals(qName))
    609                {
    610                    peekNE().addFalseValue();
    611                }
    612                else if ("data".equals(qName))
    613                {
    614                    peekNE().addDataValue(buffer.toString());
    615                }
    616                else if ("date".equals(qName))
    617                {
    618                    try
    619                    {
    620                        peekNE().addDateValue(buffer.toString());
    621                    }
    622                    catch (final IllegalArgumentException iex)
    623                    {
    624                        getLogger().warn(
    625                                "Ignoring invalid date property " + buffer);
    626                    }
    627                }
    628                else if ("array".equals(qName))
    629                {
    630                    final ArrayNodeBuilder array = (ArrayNodeBuilder) pop();
    631                    peekNE().addList(array);
    632                }
    633
    634                // remove the plist node on the stack once the value has been parsed,
    635                // array nodes remains on the stack for the next values in the list
    636                if (!(peek() instanceof ArrayNodeBuilder))
    637                {
    638                    pop();
    639                }
    640            }
    641
    642            buffer.setLength(0);
    643        }
    644
    645        @Override
    646        public void characters(final char[] ch, final int start, final int length) throws SAXException
    647        {
    648            buffer.append(ch, start, length);
    649        }
    650    }
    651
    652    /**
    653     * A specialized builder class with addXXX methods to parse the typed data passed by the SAX handler.
    654     * It is used for creating the nodes of the configuration.
    655     */
    656    private static class PListNodeBuilder
    657    {
    658        /**
    659         * The MacOS FORMAT of dates in plist files. Note: Because
    660         * {@code SimpleDateFormat} is not thread-safe, each access has to be
    661         * synchronized.
    662         */
    663        private static final DateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
    664        static
    665        {
    666            FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
    667        }
    668
    669        /**
    670         * The GNUstep FORMAT of dates in plist files. Note: Because
    671         * {@code SimpleDateFormat} is not thread-safe, each access has to be
    672         * synchronized.
    673         */
    674        private static final DateFormat GNUSTEP_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
    675
    676        /** A collection with child builders of this builder. */
    677        private final Collection<PListNodeBuilder> childBuilders =
    678                new LinkedList<>();
    679
    680        /** The name of the represented node. */
    681        private String name;
    682
    683        /** The current value of the represented node. */
    684        private Object value;
    685
    686        /**
    687         * Update the value of the node. If the existing value is null, it's
    688         * replaced with the new value. If the existing value is a list, the
    689         * specified value is appended to the list. If the existing value is
    690         * not null, a list with the two values is built.
    691         *
    692         * @param v the value to be added
    693         */
    694        public void addValue(final Object v)
    695        {
    696            if (value == null)
    697            {
    698                value = v;
    699            }
    700            else if (value instanceof Collection)
    701            {
    702                // This is safe because we create the collections ourselves
    703                @SuppressWarnings("unchecked")
    704                final
    705                Collection<Object> collection = (Collection<Object>) value;
    706                collection.add(v);
    707            }
    708            else
    709            {
    710                final List<Object> list = new ArrayList<>();
    711                list.add(value);
    712                list.add(v);
    713                value = list;
    714            }
    715        }
    716
    717        /**
    718         * Parse the specified string as a date and add it to the values of the node.
    719         *
    720         * @param value the value to be added
    721         * @throws IllegalArgumentException if the date string cannot be parsed
    722         */
    723        public void addDateValue(final String value)
    724        {
    725            try
    726            {
    727                if (value.indexOf(' ') != -1)
    728                {
    729                    // parse the date using the GNUstep FORMAT
    730                    synchronized (GNUSTEP_FORMAT)
    731                    {
    732                        addValue(GNUSTEP_FORMAT.parse(value));
    733                    }
    734                }
    735                else
    736                {
    737                    // parse the date using the MacOS X FORMAT
    738                    synchronized (FORMAT)
    739                    {
    740                        addValue(FORMAT.parse(value));
    741                    }
    742                }
    743            }
    744            catch (final ParseException e)
    745            {
    746                throw new IllegalArgumentException(String.format(
    747                        "'%s' cannot be parsed to a date!", value), e);
    748            }
    749        }
    750
    751        /**
    752         * Parse the specified string as a byte array in base 64 FORMAT
    753         * and add it to the values of the node.
    754         *
    755         * @param value the value to be added
    756         */
    757        public void addDataValue(final String value)
    758        {
    759            try
    760            {
    761                addValue(Base64.decodeBase64(value.getBytes(DATA_ENCODING)));
    762            }
    763            catch (final UnsupportedEncodingException e)
    764            {
    765                //Cannot happen as UTF-8 is a default encoding
    766                throw new AssertionError(e);
    767            }
    768        }
    769
    770        /**
    771         * Parse the specified string as an Interger and add it to the values of the node.
    772         *
    773         * @param value the value to be added
    774         */
    775        public void addIntegerValue(final String value)
    776        {
    777            addValue(new BigInteger(value));
    778        }
    779
    780        /**
    781         * Parse the specified string as a Double and add it to the values of the node.
    782         *
    783         * @param value the value to be added
    784         */
    785        public void addRealValue(final String value)
    786        {
    787            addValue(new BigDecimal(value));
    788        }
    789
    790        /**
    791         * Add a boolean value 'true' to the values of the node.
    792         */
    793        public void addTrueValue()
    794        {
    795            addValue(Boolean.TRUE);
    796        }
    797
    798        /**
    799         * Add a boolean value 'false' to the values of the node.
    800         */
    801        public void addFalseValue()
    802        {
    803            addValue(Boolean.FALSE);
    804        }
    805
    806        /**
    807         * Add a sublist to the values of the node.
    808         *
    809         * @param node the node whose value will be added to the current node value
    810         */
    811        public void addList(final ArrayNodeBuilder node)
    812        {
    813            addValue(node.getNodeValue());
    814        }
    815
    816        /**
    817         * Sets the name of the represented node.
    818         *
    819         * @param nodeName the node name
    820         */
    821        public void setName(final String nodeName)
    822        {
    823            name = nodeName;
    824        }
    825
    826        /**
    827         * Adds the given child builder to this builder.
    828         *
    829         * @param child the child builder to be added
    830         */
    831        public void addChild(final PListNodeBuilder child)
    832        {
    833            childBuilders.add(child);
    834        }
    835
    836        /**
    837         * Creates the configuration node defined by this builder.
    838         *
    839         * @return the newly created configuration node
    840         */
    841        public ImmutableNode createNode()
    842        {
    843            final ImmutableNode.Builder nodeBuilder =
    844                    new ImmutableNode.Builder(childBuilders.size());
    845            for (final PListNodeBuilder child : childBuilders)
    846            {
    847                nodeBuilder.addChild(child.createNode());
    848            }
    849            return nodeBuilder.name(name).value(getNodeValue()).create();
    850        }
    851
    852        /**
    853         * Returns the final value for the node to be created. This method is
    854         * called when the represented configuration node is actually created.
    855         *
    856         * @return the value of the resulting configuration node
    857         */
    858        protected Object getNodeValue()
    859        {
    860            return value;
    861        }
    862    }
    863
    864    /**
    865     * Container for array elements. <b>Do not use this class !</b>
    866     * It is used internally by XMLPropertyConfiguration to parse the
    867     * configuration file, it may be removed at any moment in the future.
    868     */
    869    private static class ArrayNodeBuilder extends PListNodeBuilder
    870    {
    871        /** The list of values in the array. */
    872        private final List<Object> list = new ArrayList<>();
    873
    874        /**
    875         * Add an object to the array.
    876         *
    877         * @param value the value to be added
    878         */
    879        @Override
    880        public void addValue(final Object value)
    881        {
    882            list.add(value);
    883        }
    884
    885        /**
    886         * Return the list of values in the array.
    887         *
    888         * @return the {@link List} of values
    889         */
    890        @Override
    891        protected Object getNodeValue()
    892        {
    893            return list;
    894        }
    895    }
    896}