    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). This
    066 * configuration doesn't support the binary FORMAT used in OS X 10.4.
    067 *
    068 * <p>
    069 * Example:
    070 * </p>
    071 *
    072 * <pre>
    073 * &lt;?xml version="1.0"?&gt;
    074 * &lt;!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd"&gt;
    075 * &lt;plist version="1.0"&gt;
    076 *     &lt;dict&gt;
    077 *         &lt;key&gt;string&lt;/key&gt;
    078 *         &lt;string&gt;value1&lt;/string&gt;
    079 *
    080 *         &lt;key&gt;integer&lt;/key&gt;
    081 *         &lt;integer&gt;12345&lt;/integer&gt;
    082 *
    083 *         &lt;key&gt;real&lt;/key&gt;
    084 *         &lt;real&gt;-123.45E-1&lt;/real&gt;
    085 *
    086 *         &lt;key&gt;boolean&lt;/key&gt;
    087 *         &lt;true/&gt;
    088 *
    089 *         &lt;key&gt;date&lt;/key&gt;
    090 *         &lt;date&gt;2005-01-01T12:00:00Z&lt;/date&gt;
    091 *
    092 *         &lt;key&gt;data&lt;/key&gt;
    093 *         &lt;data&gt;RHJhY28gRG9ybWllbnMgTnVucXVhbSBUaXRpbGxhbmR1cw==&lt;/data&gt;
    094 *
    095 *         &lt;key&gt;array&lt;/key&gt;
    096 *         &lt;array&gt;
    097 *             &lt;string&gt;value1&lt;/string&gt;
    098 *             &lt;string&gt;value2&lt;/string&gt;
    099 *             &lt;string&gt;value3&lt;/string&gt;
    100 *         &lt;/array&gt;
    101 *
    102 *         &lt;key&gt;dictionnary&lt;/key&gt;
    103 *         &lt;dict&gt;
    104 *             &lt;key&gt;key1&lt;/key&gt;
    105 *             &lt;string&gt;value1&lt;/string&gt;
    106 *             &lt;key&gt;key2&lt;/key&gt;
    107 *             &lt;string&gt;value2&lt;/string&gt;
    108 *             &lt;key&gt;key3&lt;/key&gt;
    109 *             &lt;string&gt;value3&lt;/string&gt;
    110 *         &lt;/dict&gt;
    111 *
    112 *         &lt;key&gt;nested&lt;/key&gt;
    113 *         &lt;dict&gt;
    114 *             &lt;key&gt;node1&lt;/key&gt;
    115 *             &lt;dict&gt;
    116 *                 &lt;key&gt;node2&lt;/key&gt;
    117 *                 &lt;dict&gt;
    118 *                     &lt;key&gt;node3&lt;/key&gt;
    119 *                     &lt;string&gt;value&lt;/string&gt;
    120 *                 &lt;/dict&gt;
    121 *             &lt;/dict&gt;
    122 *         &lt;/dict&gt;
    123 *
    124 *     &lt;/dict&gt;
    125 * &lt;/plist&gt;
    126 * </pre>
    127 *
    128 * @since 1.2
    129 */
    130public class XMLPropertyListConfiguration extends BaseHierarchicalConfiguration implements FileBasedConfiguration, FileLocatorAware {
    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 used to synthesize a new plist file by adding
    142     * values and then saving().
    143     */
    144    public XMLPropertyListConfiguration() {
    145    }
    146
    147    /**
    148     * Creates a new instance of {@code XMLPropertyListConfiguration} and copies the content of the specified configuration
    149     * into this object.
    150     *
    151     * @param configuration the configuration to copy
    152     * @since 1.4
    153     */
    154    public XMLPropertyListConfiguration(final HierarchicalConfiguration<ImmutableNode> configuration) {
    155        super(configuration);
    156    }
    157
    158    /**
    159     * Creates a new instance of {@code XMLPropertyConfiguration} with the given root node.
    160     *
    161     * @param root the root node
    162     */
    163    XMLPropertyListConfiguration(final ImmutableNode root) {
    164        super(new InMemoryNodeModel(root));
    165    }
    166
    167    private void setPropertyDirect(final String key, final Object value) {
    168        setDetailEvents(false);
    169        try {
    170            clearProperty(key);
    171            addPropertyDirect(key, value);
    172        } finally {
    173            setDetailEvents(true);
    174        }
    175    }
    176
    177    @Override
    178    protected void setPropertyInternal(final String key, final Object value) {
    179        // special case for byte arrays, they must be stored as is in the configuration
    180        if (value instanceof byte[] || value instanceof List) {
    181            setPropertyDirect(key, value);
    182        } else if (value instanceof Object[]) {
    183            setPropertyDirect(key, Arrays.asList((Object[]) value));
    184        } else {
    185            super.setPropertyInternal(key, value);
    186        }
    187    }
    188
    189    @Override
    190    protected void addPropertyInternal(final String key, final Object value) {
    191        if (value instanceof byte[] || value instanceof List) {
    192            addPropertyDirect(key, value);
    193        } else if (value instanceof Object[]) {
    194            addPropertyDirect(key, Arrays.asList((Object[]) value));
    195        } else {
    196            super.addPropertyInternal(key, value);
    197        }
    198    }
    199
    200    /**
    201     * Stores the current file locator. This method is called before I/O operations.
    202     *
    203     * @param locator the current {@code FileLocator}
    204     */
    205    @Override
    206    public void initFileLocator(final FileLocator locator) {
    207        this.locator = locator;
    208    }
    209
    210    @Override
    211    public void read(final Reader in) throws ConfigurationException {
    212        // set up the DTD validation
    213        final EntityResolver resolver = (publicId, systemId) -> new InputSource(getClass().getClassLoader().getResourceAsStream("PropertyList-1.0.dtd"));
    214
    215        // parse the file
    216        final XMLPropertyListHandler handler = new XMLPropertyListHandler();
    217        try {
    218            final SAXParserFactory factory = SAXParserFactory.newInstance();
    219            factory.setValidating(true);
    220
    221            final SAXParser parser = factory.newSAXParser();
    222            parser.getXMLReader().setEntityResolver(resolver);
    223            parser.getXMLReader().setContentHandler(handler);
    224            parser.getXMLReader().parse(new InputSource(in));
    225
    226            getNodeModel().mergeRoot(handler.getResultBuilder().createNode(), null, null, null, this);
    227        } catch (final Exception e) {
    228            throw new ConfigurationException("Unable to parse the configuration file", e);
    229        }
    230    }
    231
    232    @Override
    233    public void write(final Writer out) throws ConfigurationException {
    234        if (locator == null) {
    235            throw new ConfigurationException(
    236                "Save operation not properly " + "initialized! Do not call write(Writer) directly," + " but use a FileHandler to save a configuration.");
    237        }
    238        final PrintWriter writer = new PrintWriter(out);
    239
    240        if (locator.getEncoding() != null) {
    241            writer.println("<?xml version=\"1.0\" encoding=\"" + locator.getEncoding() + "\"?>");
    242        } else {
    243            writer.println("<?xml version=\"1.0\"?>");
    244        }
    245
    246        writer.println("<!DOCTYPE plist SYSTEM \"file://localhost/System/Library/DTDs/PropertyList.dtd\">");
    247        writer.println("<plist version=\"1.0\">");
    248
    249        printNode(writer, 1, getNodeModel().getNodeHandler().getRootNode());
    250
    251        writer.println("</plist>");
    252        writer.flush();
    253    }
    254
    255    /**
    256     * Append a node to the writer, indented according to a specific level.
    257     */
    258    private void printNode(final PrintWriter out, final int indentLevel, final ImmutableNode node) {
    259        final String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
    260
    261        if (node.getNodeName() != null) {
    262            out.println(padding + "<key>" + StringEscapeUtils.escapeXml10(node.getNodeName()) + "</key>");
    263        }
    264
    265        final List<ImmutableNode> children = node.getChildren();
    266        if (!children.isEmpty()) {
    267            out.println(padding + "<dict>");
    268
    269            final Iterator<ImmutableNode> it = children.iterator();
    270            while (it.hasNext()) {
    271                final ImmutableNode child = it.next();
    272                printNode(out, indentLevel + 1, child);
    273
    274                if (it.hasNext()) {
    275                    out.println();
    276                }
    277            }
    278
    279            out.println(padding + "</dict>");
    280        } else if (node.getValue() == null) {
    281            out.println(padding + "<dict/>");
    282        } else {
    283            final Object value = node.getValue();
    284            printValue(out, indentLevel, value);
    285        }
    286    }
    287
    288    /**
    289     * Append a value to the writer, indented according to a specific level.
    290     */
    291    private void printValue(final PrintWriter out, final int indentLevel, final Object value) {
    292        final String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
    293
    294        if (value instanceof Date) {
    295            synchronized (PListNodeBuilder.FORMAT) {
    296                out.println(padding + "<date>" + PListNodeBuilder.FORMAT.format((Date) value) + "</date>");
    297            }
    298        } else if (value instanceof Calendar) {
    299            printValue(out, indentLevel, ((Calendar) value).getTime());
    300        } else if (value instanceof Number) {
    301            if (value instanceof Double || value instanceof Float || value instanceof BigDecimal) {
    302                out.println(padding + "<real>" + value.toString() + "</real>");
    303            } else {
    304                out.println(padding + "<integer>" + value.toString() + "</integer>");
    305            }
    306        } else if (value instanceof Boolean) {
    307            if (((Boolean) value).booleanValue()) {
    308                out.println(padding + "<true/>");
    309            } else {
    310                out.println(padding + "<false/>");
    311            }
    312        } else if (value instanceof List) {
    313            out.println(padding + "<array>");
    314            ((List<?>) value).forEach(o -> printValue(out, indentLevel + 1, o));
    315            out.println(padding + "</array>");
    316        } else if (value instanceof HierarchicalConfiguration) {
    317            // This is safe because we have created this configuration
    318            @SuppressWarnings("unchecked")
    319            final HierarchicalConfiguration<ImmutableNode> config = (HierarchicalConfiguration<ImmutableNode>) value;
    320            printNode(out, indentLevel, config.getNodeModel().getNodeHandler().getRootNode());
    321        } else if (value instanceof ImmutableConfiguration) {
    322            // display a flat Configuration as a dictionary
    323            out.println(padding + "<dict>");
    324
    325            final ImmutableConfiguration config = (ImmutableConfiguration) value;
    326            final Iterator<String> it = config.getKeys();
    327            while (it.hasNext()) {
    328                // create a node for each property
    329                final String key = it.next();
    330                final ImmutableNode node = new ImmutableNode.Builder().name(key).value(config.getProperty(key)).create();
    331
    332                // print the node
    333                printNode(out, indentLevel + 1, node);
    334
    335                if (it.hasNext()) {
    336                    out.println();
    337                }
    338            }
    339            out.println(padding + "</dict>");
    340        } else if (value instanceof Map) {
    341            // display a Map as a dictionary
    342            final Map<String, Object> map = transformMap((Map<?, ?>) value);
    343            printValue(out, indentLevel, new MapConfiguration(map));
    344        } else if (value instanceof byte[]) {
    345            final String base64;
    346            try {
    347                base64 = new String(Base64.encodeBase64((byte[]) value), DATA_ENCODING);
    348            } catch (final UnsupportedEncodingException e) {
    349                // Cannot happen as UTF-8 is a standard encoding
    350                throw new AssertionError(e);
    351            }
    352            out.println(padding + "<data>" + StringEscapeUtils.escapeXml10(base64) + "</data>");
    353        } else if (value != null) {
    354            out.println(padding + "<string>" + StringEscapeUtils.escapeXml10(String.valueOf(value)) + "</string>");
    355        } else {
    356            out.println(padding + "<string/>");
    357        }
    358    }
    359
    360    /**
    361     * Transform a map of arbitrary types into a map with string keys and object values. All keys of the source map which
    362     * are not of type String are dropped.
    363     *
    364     * @param src the map to be converted
    365     * @return the resulting map
    366     */
    367    private static Map<String, Object> transformMap(final Map<?, ?> src) {
    368        final Map<String, Object> dest = new HashMap<>();
    369        for (final Map.Entry<?, ?> e : src.entrySet()) {
    370            if (e.getKey() instanceof String) {
    371                dest.put((String) e.getKey(), e.getValue());
    372            }
    373        }
    374        return dest;
    375    }
    376
    377    /**
    378     * SAX Handler to build the configuration nodes while the document is being parsed.
    379     */
    380    private class XMLPropertyListHandler extends DefaultHandler {
    381        /** The buffer containing the text node being read */
    382        private final StringBuilder buffer = new StringBuilder();
    383
    384        /** The stack of configuration nodes */
    385        private final List<PListNodeBuilder> stack = new ArrayList<>();
    386
    387        /** The builder for the resulting node. */
    388        private final PListNodeBuilder resultBuilder;
    389
    390        public XMLPropertyListHandler() {
    391            resultBuilder = new PListNodeBuilder();
    392            push(resultBuilder);
    393        }
    394
    395        /**
    396         * Gets the builder for the result node.
    397         *
    398         * @return the result node builder
    399         */
    400        public PListNodeBuilder getResultBuilder() {
    401            return resultBuilder;
    402        }
    403
    404        /**
    405         * Return the node on the top of the stack.
    406         */
    407        private PListNodeBuilder peek() {
    408            if (!stack.isEmpty()) {
    409                return stack.get(stack.size() - 1);
    410            }
    411            return null;
    412        }
    413
    414        /**
    415         * Returns the node on top of the non-empty stack. Throws an exception if the stack is empty.
    416         *
    417         * @return the top node of the stack
    418         * @throws ConfigurationRuntimeException if the stack is empty
    419         */
    420        private PListNodeBuilder peekNE() {
    421            final PListNodeBuilder result = peek();
    422            if (result == null) {
    423                throw new ConfigurationRuntimeException("Access to empty stack!");
    424            }
    425            return result;
    426        }
    427
    428        /**
    429         * Remove and return the node on the top of the stack.
    430         */
    431        private PListNodeBuilder pop() {
    432            if (!stack.isEmpty()) {
    433                return stack.remove(stack.size() - 1);
    434            }
    435            return null;
    436        }
    437
    438        /**
    439         * Put a node on the top of the stack.
    440         */
    441        private void push(final PListNodeBuilder node) {
    442            stack.add(node);
    443        }
    444
    445        @Override
    446        public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException {
    447            if ("array".equals(qName)) {
    448                push(new ArrayNodeBuilder());
    449            } else if ("dict".equals(qName) && peek() instanceof ArrayNodeBuilder) {
    450                // push the new root builder on the stack
    451                push(new PListNodeBuilder());
    452            }
    453        }
    454
    455        @Override
    456        public void endElement(final String uri, final String localName, final String qName) throws SAXException {
    457            if ("key".equals(qName)) {
    458                // create a new node, link it to its parent and push it on the stack
    459                final PListNodeBuilder node = new PListNodeBuilder();
    460                node.setName(buffer.toString());
    461                peekNE().addChild(node);
    462                push(node);
    463            } else if ("dict".equals(qName)) {
    464                // remove the root of the XMLPropertyListConfiguration previously pushed on the stack
    465                final PListNodeBuilder builder = pop();
    466                assert builder != null : "Stack was empty!";
    467                if (peek() instanceof ArrayNodeBuilder) {
    468                    // create the configuration
    469                    final XMLPropertyListConfiguration config = new XMLPropertyListConfiguration(builder.createNode());
    470
    471                    // add it to the ArrayNodeBuilder
    472                    final ArrayNodeBuilder node = (ArrayNodeBuilder) peekNE();
    473                    node.addValue(config);
    474                }
    475            } else {
    476                if ("string".equals(qName)) {
    477                    peekNE().addValue(buffer.toString());
    478                } else if ("integer".equals(qName)) {
    479                    peekNE().addIntegerValue(buffer.toString());
    480                } else if ("real".equals(qName)) {
    481                    peekNE().addRealValue(buffer.toString());
    482                } else if ("true".equals(qName)) {
    483                    peekNE().addTrueValue();
    484                } else if ("false".equals(qName)) {
    485                    peekNE().addFalseValue();
    486                } else if ("data".equals(qName)) {
    487                    peekNE().addDataValue(buffer.toString());
    488                } else if ("date".equals(qName)) {
    489                    try {
    490                        peekNE().addDateValue(buffer.toString());
    491                    } catch (final IllegalArgumentException iex) {
    492                        getLogger().warn("Ignoring invalid date property " + buffer);
    493                    }
    494                } else if ("array".equals(qName)) {
    495                    final ArrayNodeBuilder array = (ArrayNodeBuilder) pop();
    496                    peekNE().addList(array);
    497                }
    498
    499                // remove the plist node on the stack once the value has been parsed,
    500                // array nodes remains on the stack for the next values in the list
    501                if (!(peek() instanceof ArrayNodeBuilder)) {
    502                    pop();
    503                }
    504            }
    505
    506            buffer.setLength(0);
    507        }
    508
    509        @Override
    510        public void characters(final char[] ch, final int start, final int length) throws SAXException {
    511            buffer.append(ch, start, length);
    512        }
    513    }
    514
    515    /**
    516     * A specialized builder class with addXXX methods to parse the typed data passed by the SAX handler. It is used for
    517     * creating the nodes of the configuration.
    518     */
    519    private static class PListNodeBuilder {
    520        /**
    521         * The MacOS FORMAT of dates in plist files. Note: Because {@code SimpleDateFormat} is not thread-safe, each access has
    522         * to be synchronized.
    523         */
    524        private static final DateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
    525        static {
    526            FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
    527        }
    528
    529        /**
    530         * The GNUstep FORMAT of dates in plist files. Note: Because {@code SimpleDateFormat} is not thread-safe, each access
    531         * has to be synchronized.
    532         */
    533        private static final DateFormat GNUSTEP_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
    534
    535        /** A collection with child builders of this builder. */
    536        private final Collection<PListNodeBuilder> childBuilders = new LinkedList<>();
    537
    538        /** The name of the represented node. */
    539        private String name;
    540
    541        /** The current value of the represented node. */
    542        private Object value;
    543
    544        /**
    545         * Update the value of the node. If the existing value is null, it's replaced with the new value. If the existing value
    546         * is a list, the specified value is appended to the list. If the existing value is not null, a list with the two values
    547         * is built.
    548         *
    549         * @param v the value to be added
    550         */
    551        public void addValue(final Object v) {
    552            if (value == null) {
    553                value = v;
    554            } else if (value instanceof Collection) {
    555                // This is safe because we create the collections ourselves
    556                @SuppressWarnings("unchecked")
    557                final Collection<Object> collection = (Collection<Object>) value;
    558                collection.add(v);
    559            } else {
    560                final List<Object> list = new ArrayList<>();
    561                list.add(value);
    562                list.add(v);
    563                value = list;
    564            }
    565        }
    566
    567        /**
    568         * Parse the specified string as a date and add it to the values of the node.
    569         *
    570         * @param value the value to be added
    571         * @throws IllegalArgumentException if the date string cannot be parsed
    572         */
    573        public void addDateValue(final String value) {
    574            try {
    575                if (value.indexOf(' ') != -1) {
    576                    // parse the date using the GNUstep FORMAT
    577                    synchronized (GNUSTEP_FORMAT) {
    578                        addValue(GNUSTEP_FORMAT.parse(value));
    579                    }
    580                } else {
    581                    // parse the date using the MacOS X FORMAT
    582                    synchronized (FORMAT) {
    583                        addValue(FORMAT.parse(value));
    584                    }
    585                }
    586            } catch (final ParseException e) {
    587                throw new IllegalArgumentException(String.format("'%s' cannot be parsed to a date!", value), e);
    588            }
    589        }
    590
    591        /**
    592         * Parse the specified string as a byte array in base 64 FORMAT and add it to the values of the node.
    593         *
    594         * @param value the value to be added
    595         */
    596        public void addDataValue(final String value) {
    597            try {
    598                addValue(Base64.decodeBase64(value.getBytes(DATA_ENCODING)));
    599            } catch (final UnsupportedEncodingException e) {
    600                // Cannot happen as UTF-8 is a default encoding
    601                throw new AssertionError(e);
    602            }
    603        }
    604
    605        /**
    606         * Parse the specified string as an Interger and add it to the values of the node.
    607         *
    608         * @param value the value to be added
    609         */
    610        public void addIntegerValue(final String value) {
    611            addValue(new BigInteger(value));
    612        }
    613
    614        /**
    615         * Parse the specified string as a Double and add it to the values of the node.
    616         *
    617         * @param value the value to be added
    618         */
    619        public void addRealValue(final String value) {
    620            addValue(new BigDecimal(value));
    621        }
    622
    623        /**
    624         * Add a boolean value 'true' to the values of the node.
    625         */
    626        public void addTrueValue() {
    627            addValue(Boolean.TRUE);
    628        }
    629
    630        /**
    631         * Add a boolean value 'false' to the values of the node.
    632         */
    633        public void addFalseValue() {
    634            addValue(Boolean.FALSE);
    635        }
    636
    637        /**
    638         * Add a sublist to the values of the node.
    639         *
    640         * @param node the node whose value will be added to the current node value
    641         */
    642        public void addList(final ArrayNodeBuilder node) {
    643            addValue(node.getNodeValue());
    644        }
    645
    646        /**
    647         * Sets the name of the represented node.
    648         *
    649         * @param nodeName the node name
    650         */
    651        public void setName(final String nodeName) {
    652            name = nodeName;
    653        }
    654
    655        /**
    656         * Adds the given child builder to this builder.
    657         *
    658         * @param child the child builder to be added
    659         */
    660        public void addChild(final PListNodeBuilder child) {
    661            childBuilders.add(child);
    662        }
    663
    664        /**
    665         * Creates the configuration node defined by this builder.
    666         *
    667         * @return the newly created configuration node
    668         */
    669        public ImmutableNode createNode() {
    670            final ImmutableNode.Builder nodeBuilder = new ImmutableNode.Builder(childBuilders.size());
    671            childBuilders.forEach(child -> nodeBuilder.addChild(child.createNode()));
    672            return nodeBuilder.name(name).value(getNodeValue()).create();
    673        }
    674
    675        /**
    676         * Gets the final value for the node to be created. This method is called when the represented configuration node is
    677         * actually created.
    678         *
    679         * @return the value of the resulting configuration node
    680         */
    681        protected Object getNodeValue() {
    682            return value;
    683        }
    684    }
    685
    686    /**
    687     * Container for array elements. <b>Do not use this class !</b> It is used internally by XMLPropertyConfiguration to
    688     * parse the configuration file, it may be removed at any moment in the future.
    689     */
    690    private static class ArrayNodeBuilder extends PListNodeBuilder {
    691        /** The list of values in the array. */
    692        private final List<Object> list = new ArrayList<>();
    693
    694        /**
    695         * Add an object to the array.
    696         *
    697         * @param value the value to be added
    698         */
    699        @Override
    700        public void addValue(final Object value) {
    701            list.add(value);
    702        }
    703
    704        /**
    705         * Return the list of values in the array.
    706         *
    707         * @return the {@link List} of values
    708         */
    709        @Override
    710        protected Object getNodeValue() {
    711            return list;
    712        }
    713    }
    714}