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}