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 * <?xml version="1.0"?> 070 * <!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd"> 071 * <plist version="1.0"> 072 * <dict> 073 * <key>string</key> 074 * <string>value1</string> 075 * 076 * <key>integer</key> 077 * <integer>12345</integer> 078 * 079 * <key>real</key> 080 * <real>-123.45E-1</real> 081 * 082 * <key>boolean</key> 083 * <true/> 084 * 085 * <key>date</key> 086 * <date>2005-01-01T12:00:00Z</date> 087 * 088 * <key>data</key> 089 * <data>RHJhY28gRG9ybWllbnMgTnVucXVhbSBUaXRpbGxhbmR1cw==</data> 090 * 091 * <key>array</key> 092 * <array> 093 * <string>value1</string> 094 * <string>value2</string> 095 * <string>value3</string> 096 * </array> 097 * 098 * <key>dictionnary</key> 099 * <dict> 100 * <key>key1</key> 101 * <string>value1</string> 102 * <key>key2</key> 103 * <string>value2</string> 104 * <key>key3</key> 105 * <string>value3</string> 106 * </dict> 107 * 108 * <key>nested</key> 109 * <dict> 110 * <key>node1</key> 111 * <dict> 112 * <key>node2</key> 113 * <dict> 114 * <key>node3</key> 115 * <string>value</string> 116 * </dict> 117 * </dict> 118 * </dict> 119 * 120 * </dict> 121 * </plist> 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}