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