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