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