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