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