View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.configuration2.plist;
19  
20  import java.io.PrintWriter;
21  import java.io.Reader;
22  import java.io.Writer;
23  import java.math.BigDecimal;
24  import java.math.BigInteger;
25  import java.nio.charset.Charset;
26  import java.nio.charset.StandardCharsets;
27  import java.text.DateFormat;
28  import java.text.ParseException;
29  import java.text.SimpleDateFormat;
30  import java.util.ArrayList;
31  import java.util.Arrays;
32  import java.util.Calendar;
33  import java.util.Collection;
34  import java.util.Date;
35  import java.util.HashMap;
36  import java.util.Iterator;
37  import java.util.LinkedList;
38  import java.util.List;
39  import java.util.Map;
40  import java.util.TimeZone;
41  
42  import javax.xml.parsers.SAXParser;
43  import javax.xml.parsers.SAXParserFactory;
44  
45  import org.apache.commons.codec.binary.Base64;
46  import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
47  import org.apache.commons.configuration2.FileBasedConfiguration;
48  import org.apache.commons.configuration2.HierarchicalConfiguration;
49  import org.apache.commons.configuration2.ImmutableConfiguration;
50  import org.apache.commons.configuration2.MapConfiguration;
51  import org.apache.commons.configuration2.ex.ConfigurationException;
52  import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
53  import org.apache.commons.configuration2.io.FileLocator;
54  import org.apache.commons.configuration2.io.FileLocatorAware;
55  import org.apache.commons.configuration2.tree.ImmutableNode;
56  import org.apache.commons.configuration2.tree.InMemoryNodeModel;
57  import org.apache.commons.lang3.StringUtils;
58  import org.apache.commons.text.StringEscapeUtils;
59  import org.xml.sax.Attributes;
60  import org.xml.sax.EntityResolver;
61  import org.xml.sax.InputSource;
62  import org.xml.sax.SAXException;
63  import org.xml.sax.helpers.DefaultHandler;
64  
65  /**
66   * Property list file (plist) in XML FORMAT as used by macOS X (http://www.apple.com/DTDs/PropertyList-1.0.dtd). This
67   * configuration doesn't support the binary FORMAT used in OS X 10.4.
68   *
69   * <p>
70   * Example:
71   * </p>
72   *
73   * <pre>
74   * &lt;?xml version="1.0"?&gt;
75   * &lt;!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd"&gt;
76   * &lt;plist version="1.0"&gt;
77   *     &lt;dict&gt;
78   *         &lt;key&gt;string&lt;/key&gt;
79   *         &lt;string&gt;value1&lt;/string&gt;
80   *
81   *         &lt;key&gt;integer&lt;/key&gt;
82   *         &lt;integer&gt;12345&lt;/integer&gt;
83   *
84   *         &lt;key&gt;real&lt;/key&gt;
85   *         &lt;real&gt;-123.45E-1&lt;/real&gt;
86   *
87   *         &lt;key&gt;boolean&lt;/key&gt;
88   *         &lt;true/&gt;
89   *
90   *         &lt;key&gt;date&lt;/key&gt;
91   *         &lt;date&gt;2005-01-01T12:00:00Z&lt;/date&gt;
92   *
93   *         &lt;key&gt;data&lt;/key&gt;
94   *         &lt;data&gt;RHJhY28gRG9ybWllbnMgTnVucXVhbSBUaXRpbGxhbmR1cw==&lt;/data&gt;
95   *
96   *         &lt;key&gt;array&lt;/key&gt;
97   *         &lt;array&gt;
98   *             &lt;string&gt;value1&lt;/string&gt;
99   *             &lt;string&gt;value2&lt;/string&gt;
100  *             &lt;string&gt;value3&lt;/string&gt;
101  *         &lt;/array&gt;
102  *
103  *         &lt;key&gt;dictionnary&lt;/key&gt;
104  *         &lt;dict&gt;
105  *             &lt;key&gt;key1&lt;/key&gt;
106  *             &lt;string&gt;value1&lt;/string&gt;
107  *             &lt;key&gt;key2&lt;/key&gt;
108  *             &lt;string&gt;value2&lt;/string&gt;
109  *             &lt;key&gt;key3&lt;/key&gt;
110  *             &lt;string&gt;value3&lt;/string&gt;
111  *         &lt;/dict&gt;
112  *
113  *         &lt;key&gt;nested&lt;/key&gt;
114  *         &lt;dict&gt;
115  *             &lt;key&gt;node1&lt;/key&gt;
116  *             &lt;dict&gt;
117  *                 &lt;key&gt;node2&lt;/key&gt;
118  *                 &lt;dict&gt;
119  *                     &lt;key&gt;node3&lt;/key&gt;
120  *                     &lt;string&gt;value&lt;/string&gt;
121  *                 &lt;/dict&gt;
122  *             &lt;/dict&gt;
123  *         &lt;/dict&gt;
124  *
125  *     &lt;/dict&gt;
126  * &lt;/plist&gt;
127  * </pre>
128  *
129  * @since 1.2
130  */
131 public 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 Charset DATA_ENCODING = StandardCharsets.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             ((List<?>) value).forEach(o -> printValue(out, indentLevel + 1, o));
316             out.println(padding + "</array>");
317         } else if (value instanceof HierarchicalConfiguration) {
318             // This is safe because we have created this configuration
319             @SuppressWarnings("unchecked")
320             final HierarchicalConfiguration<ImmutableNode> config = (HierarchicalConfiguration<ImmutableNode>) value;
321             printNode(out, indentLevel, config.getNodeModel().getNodeHandler().getRootNode());
322         } else if (value instanceof ImmutableConfiguration) {
323             // display a flat Configuration as a dictionary
324             out.println(padding + "<dict>");
325 
326             final ImmutableConfiguration config = (ImmutableConfiguration) value;
327             final Iterator<String> it = config.getKeys();
328             while (it.hasNext()) {
329                 // create a node for each property
330                 final String key = it.next();
331                 final ImmutableNode node = new ImmutableNode.Builder().name(key).value(config.getProperty(key)).create();
332 
333                 // print the node
334                 printNode(out, indentLevel + 1, node);
335 
336                 if (it.hasNext()) {
337                     out.println();
338                 }
339             }
340             out.println(padding + "</dict>");
341         } else if (value instanceof Map) {
342             // display a Map as a dictionary
343             final Map<String, Object> map = transformMap((Map<?, ?>) value);
344             printValue(out, indentLevel, new MapConfiguration(map));
345         } else if (value instanceof byte[]) {
346             final String base64 = new String(Base64.encodeBase64((byte[]) value), DATA_ENCODING);
347             out.println(padding + "<data>" + StringEscapeUtils.escapeXml10(base64) + "</data>");
348         } else if (value != null) {
349             out.println(padding + "<string>" + StringEscapeUtils.escapeXml10(String.valueOf(value)) + "</string>");
350         } else {
351             out.println(padding + "<string/>");
352         }
353     }
354 
355     /**
356      * Transform a map of arbitrary types into a map with string keys and object values. All keys of the source map which
357      * are not of type String are dropped.
358      *
359      * @param src the map to be converted
360      * @return the resulting map
361      */
362     private static Map<String, Object> transformMap(final Map<?, ?> src) {
363         final Map<String, Object> dest = new HashMap<>();
364         for (final Map.Entry<?, ?> e : src.entrySet()) {
365             if (e.getKey() instanceof String) {
366                 dest.put((String) e.getKey(), e.getValue());
367             }
368         }
369         return dest;
370     }
371 
372     /**
373      * SAX Handler to build the configuration nodes while the document is being parsed.
374      */
375     private final class XMLPropertyListHandler extends DefaultHandler {
376         /** The buffer containing the text node being read */
377         private final StringBuilder buffer = new StringBuilder();
378 
379         /** The stack of configuration nodes */
380         private final List<PListNodeBuilder> stack = new ArrayList<>();
381 
382         /** The builder for the resulting node. */
383         private final PListNodeBuilder resultBuilder;
384 
385         public XMLPropertyListHandler() {
386             resultBuilder = new PListNodeBuilder();
387             push(resultBuilder);
388         }
389 
390         /**
391          * Gets the builder for the result node.
392          *
393          * @return the result node builder
394          */
395         public PListNodeBuilder getResultBuilder() {
396             return resultBuilder;
397         }
398 
399         /**
400          * Return the node on the top of the stack.
401          */
402         private PListNodeBuilder peek() {
403             if (!stack.isEmpty()) {
404                 return stack.get(stack.size() - 1);
405             }
406             return null;
407         }
408 
409         /**
410          * Returns the node on top of the non-empty stack. Throws an exception if the stack is empty.
411          *
412          * @return the top node of the stack
413          * @throws ConfigurationRuntimeException if the stack is empty
414          */
415         private PListNodeBuilder peekNE() {
416             final PListNodeBuilder result = peek();
417             if (result == null) {
418                 throw new ConfigurationRuntimeException("Access to empty stack!");
419             }
420             return result;
421         }
422 
423         /**
424          * Remove and return the node on the top of the stack.
425          */
426         private PListNodeBuilder pop() {
427             if (!stack.isEmpty()) {
428                 return stack.remove(stack.size() - 1);
429             }
430             return null;
431         }
432 
433         /**
434          * Put a node on the top of the stack.
435          */
436         private void push(final PListNodeBuilder node) {
437             stack.add(node);
438         }
439 
440         @Override
441         public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException {
442             if ("array".equals(qName)) {
443                 push(new ArrayNodeBuilder());
444             } else if ("dict".equals(qName) && peek() instanceof ArrayNodeBuilder) {
445                 // push the new root builder on the stack
446                 push(new PListNodeBuilder());
447             }
448         }
449 
450         @Override
451         public void endElement(final String uri, final String localName, final String qName) throws SAXException {
452             if ("key".equals(qName)) {
453                 // create a new node, link it to its parent and push it on the stack
454                 final PListNodeBuilder node = new PListNodeBuilder();
455                 node.setName(buffer.toString());
456                 peekNE().addChild(node);
457                 push(node);
458             } else if ("dict".equals(qName)) {
459                 // remove the root of the XMLPropertyListConfiguration previously pushed on the stack
460                 final PListNodeBuilder builder = pop();
461                 assert builder != null : "Stack was empty!";
462                 if (peek() instanceof ArrayNodeBuilder) {
463                     // create the configuration
464                     final XMLPropertyListConfiguration config = new XMLPropertyListConfiguration(builder.createNode());
465 
466                     // add it to the ArrayNodeBuilder
467                     final ArrayNodeBuilder node = (ArrayNodeBuilder) peekNE();
468                     node.addValue(config);
469                 }
470             } else {
471                 if ("string".equals(qName)) {
472                     peekNE().addValue(buffer.toString());
473                 } else if ("integer".equals(qName)) {
474                     peekNE().addIntegerValue(buffer.toString());
475                 } else if ("real".equals(qName)) {
476                     peekNE().addRealValue(buffer.toString());
477                 } else if ("true".equals(qName)) {
478                     peekNE().addTrueValue();
479                 } else if ("false".equals(qName)) {
480                     peekNE().addFalseValue();
481                 } else if ("data".equals(qName)) {
482                     peekNE().addDataValue(buffer.toString());
483                 } else if ("date".equals(qName)) {
484                     try {
485                         peekNE().addDateValue(buffer.toString());
486                     } catch (final IllegalArgumentException iex) {
487                         getLogger().warn("Ignoring invalid date property " + buffer);
488                     }
489                 } else if ("array".equals(qName)) {
490                     final ArrayNodeBuilder array = (ArrayNodeBuilder) pop();
491                     peekNE().addList(array);
492                 }
493 
494                 // remove the plist node on the stack once the value has been parsed,
495                 // array nodes remains on the stack for the next values in the list
496                 if (!(peek() instanceof ArrayNodeBuilder)) {
497                     pop();
498                 }
499             }
500 
501             buffer.setLength(0);
502         }
503 
504         @Override
505         public void characters(final char[] ch, final int start, final int length) throws SAXException {
506             buffer.append(ch, start, length);
507         }
508     }
509 
510     /**
511      * A specialized builder class with addXXX methods to parse the typed data passed by the SAX handler. It is used for
512      * creating the nodes of the configuration.
513      */
514     private static class PListNodeBuilder {
515         /**
516          * The MacOS FORMAT of dates in plist files. Note: Because {@code SimpleDateFormat} is not thread-safe, each access has
517          * to be synchronized.
518          */
519         private static final DateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
520         static {
521             FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
522         }
523 
524         /**
525          * The GNUstep FORMAT of dates in plist files. Note: Because {@code SimpleDateFormat} is not thread-safe, each access
526          * has to be synchronized.
527          */
528         private static final DateFormat GNUSTEP_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
529 
530         /** A collection with child builders of this builder. */
531         private final Collection<PListNodeBuilder> childBuilders = new LinkedList<>();
532 
533         /** The name of the represented node. */
534         private String name;
535 
536         /** The current value of the represented node. */
537         private Object value;
538 
539         /**
540          * Update the value of the node. If the existing value is null, it's replaced with the new value. If the existing value
541          * is a list, the specified value is appended to the list. If the existing value is not null, a list with the two values
542          * is built.
543          *
544          * @param v the value to be added
545          */
546         public void addValue(final Object v) {
547             if (value == null) {
548                 value = v;
549             } else if (value instanceof Collection) {
550                 // This is safe because we create the collections ourselves
551                 @SuppressWarnings("unchecked")
552                 final Collection<Object> collection = (Collection<Object>) value;
553                 collection.add(v);
554             } else {
555                 final List<Object> list = new ArrayList<>();
556                 list.add(value);
557                 list.add(v);
558                 value = list;
559             }
560         }
561 
562         /**
563          * Parse the specified string as a date and add it to the values of the node.
564          *
565          * @param value the value to be added
566          * @throws IllegalArgumentException if the date string cannot be parsed
567          */
568         public void addDateValue(final String value) {
569             try {
570                 if (value.indexOf(' ') != -1) {
571                     // parse the date using the GNUstep FORMAT
572                     synchronized (GNUSTEP_FORMAT) {
573                         addValue(GNUSTEP_FORMAT.parse(value));
574                     }
575                 } else {
576                     // parse the date using the MacOS X FORMAT
577                     synchronized (FORMAT) {
578                         addValue(FORMAT.parse(value));
579                     }
580                 }
581             } catch (final ParseException e) {
582                 throw new IllegalArgumentException(String.format("'%s' cannot be parsed to a date!", value), e);
583             }
584         }
585 
586         /**
587          * Parse the specified string as a byte array in base 64 FORMAT and add it to the values of the node.
588          *
589          * @param value the value to be added
590          */
591         public void addDataValue(final String value) {
592             addValue(Base64.decodeBase64(value.getBytes(DATA_ENCODING)));
593         }
594 
595         /**
596          * Parse the specified string as an Interger and add it to the values of the node.
597          *
598          * @param value the value to be added
599          */
600         public void addIntegerValue(final String value) {
601             addValue(new BigInteger(value));
602         }
603 
604         /**
605          * Parse the specified string as a Double and add it to the values of the node.
606          *
607          * @param value the value to be added
608          */
609         public void addRealValue(final String value) {
610             addValue(new BigDecimal(value));
611         }
612 
613         /**
614          * Add a boolean value 'true' to the values of the node.
615          */
616         public void addTrueValue() {
617             addValue(Boolean.TRUE);
618         }
619 
620         /**
621          * Add a boolean value 'false' to the values of the node.
622          */
623         public void addFalseValue() {
624             addValue(Boolean.FALSE);
625         }
626 
627         /**
628          * Add a sublist to the values of the node.
629          *
630          * @param node the node whose value will be added to the current node value
631          */
632         public void addList(final ArrayNodeBuilder node) {
633             addValue(node.getNodeValue());
634         }
635 
636         /**
637          * Sets the name of the represented node.
638          *
639          * @param nodeName the node name
640          */
641         public void setName(final String nodeName) {
642             name = nodeName;
643         }
644 
645         /**
646          * Adds the given child builder to this builder.
647          *
648          * @param child the child builder to be added
649          */
650         public void addChild(final PListNodeBuilder child) {
651             childBuilders.add(child);
652         }
653 
654         /**
655          * Creates the configuration node defined by this builder.
656          *
657          * @return the newly created configuration node
658          */
659         public ImmutableNode createNode() {
660             final ImmutableNode.Builder nodeBuilder = new ImmutableNode.Builder(childBuilders.size());
661             childBuilders.forEach(child -> nodeBuilder.addChild(child.createNode()));
662             return nodeBuilder.name(name).value(getNodeValue()).create();
663         }
664 
665         /**
666          * Gets the final value for the node to be created. This method is called when the represented configuration node is
667          * actually created.
668          *
669          * @return the value of the resulting configuration node
670          */
671         protected Object getNodeValue() {
672             return value;
673         }
674     }
675 
676     /**
677      * Container for array elements. <b>Do not use this class !</b> It is used internally by XMLPropertyConfiguration to
678      * parse the configuration file, it may be removed at any moment in the future.
679      */
680     private static final class ArrayNodeBuilder extends PListNodeBuilder {
681         /** The list of values in the array. */
682         private final List<Object> list = new ArrayList<>();
683 
684         /**
685          * Add an object to the array.
686          *
687          * @param value the value to be added
688          */
689         @Override
690         public void addValue(final Object value) {
691             list.add(value);
692         }
693 
694         /**
695          * Return the list of values in the array.
696          *
697          * @return the {@link List} of values
698          */
699         @Override
700         protected Object getNodeValue() {
701             return list;
702         }
703     }
704 }