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