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 * @author Emmanuel Bourg 128 * @version $Id: XMLPropertyListConfiguration.java 1842194 2018-09-27 22:24:23Z ggregory $ 129 */ 130public class XMLPropertyListConfiguration extends BaseHierarchicalConfiguration 131 implements FileBasedConfiguration, FileLocatorAware 132{ 133 /** Size of the indentation for the generated file. */ 134 private static final int INDENT_SIZE = 4; 135 136 /** Constant for the encoding for binary data. */ 137 private static final String DATA_ENCODING = "UTF-8"; 138 139 /** Temporarily stores the current file location. */ 140 private FileLocator locator; 141 142 /** 143 * Creates an empty XMLPropertyListConfiguration object which can be 144 * used to synthesize a new plist file by adding values and 145 * then saving(). 146 */ 147 public XMLPropertyListConfiguration() 148 { 149 } 150 151 /** 152 * Creates a new instance of {@code XMLPropertyListConfiguration} and 153 * copies the content of the specified configuration into this object. 154 * 155 * @param configuration the configuration to copy 156 * @since 1.4 157 */ 158 public XMLPropertyListConfiguration(final HierarchicalConfiguration<ImmutableNode> configuration) 159 { 160 super(configuration); 161 } 162 163 /** 164 * Creates a new instance of {@code XMLPropertyConfiguration} with the given 165 * root node. 166 * 167 * @param root the root node 168 */ 169 XMLPropertyListConfiguration(final ImmutableNode root) 170 { 171 super(new InMemoryNodeModel(root)); 172 } 173 174 @Override 175 protected void setPropertyInternal(final String key, final Object value) 176 { 177 // special case for byte arrays, they must be stored as is in the configuration 178 if (value instanceof byte[]) 179 { 180 setDetailEvents(false); 181 try 182 { 183 clearProperty(key); 184 addPropertyDirect(key, value); 185 } 186 finally 187 { 188 setDetailEvents(true); 189 } 190 } 191 else 192 { 193 super.setPropertyInternal(key, value); 194 } 195 } 196 197 @Override 198 protected void addPropertyInternal(final String key, final Object value) 199 { 200 if (value instanceof byte[] || value instanceof List) 201 { 202 addPropertyDirect(key, value); 203 } 204 else if (value instanceof Object[]) 205 { 206 addPropertyDirect(key, Arrays.asList((Object[]) value)); 207 } 208 else 209 { 210 super.addPropertyInternal(key, value); 211 } 212 } 213 214 /** 215 * Stores the current file locator. This method is called before I/O 216 * operations. 217 * 218 * @param locator the current {@code FileLocator} 219 */ 220 @Override 221 public void initFileLocator(final FileLocator locator) 222 { 223 this.locator = locator; 224 } 225 226 @Override 227 public void read(final Reader in) throws ConfigurationException 228 { 229 // set up the DTD validation 230 final EntityResolver resolver = new EntityResolver() 231 { 232 @Override 233 public InputSource resolveEntity(final String publicId, final String systemId) 234 { 235 return new InputSource(getClass().getClassLoader() 236 .getResourceAsStream("PropertyList-1.0.dtd")); 237 } 238 }; 239 240 // parse the file 241 final XMLPropertyListHandler handler = new XMLPropertyListHandler(); 242 try 243 { 244 final SAXParserFactory factory = SAXParserFactory.newInstance(); 245 factory.setValidating(true); 246 247 final SAXParser parser = factory.newSAXParser(); 248 parser.getXMLReader().setEntityResolver(resolver); 249 parser.getXMLReader().setContentHandler(handler); 250 parser.getXMLReader().parse(new InputSource(in)); 251 252 getNodeModel().mergeRoot(handler.getResultBuilder().createNode(), 253 null, null, null, this); 254 } 255 catch (final Exception e) 256 { 257 throw new ConfigurationException( 258 "Unable to parse the configuration file", e); 259 } 260 } 261 262 @Override 263 public void write(final Writer out) throws ConfigurationException 264 { 265 if (locator == null) 266 { 267 throw new ConfigurationException("Save operation not properly " 268 + "initialized! Do not call write(Writer) directly," 269 + " but use a FileHandler to save a configuration."); 270 } 271 final PrintWriter writer = new PrintWriter(out); 272 273 if (locator.getEncoding() != null) 274 { 275 writer.println("<?xml version=\"1.0\" encoding=\"" + locator.getEncoding() + "\"?>"); 276 } 277 else 278 { 279 writer.println("<?xml version=\"1.0\"?>"); 280 } 281 282 writer.println("<!DOCTYPE plist SYSTEM \"file://localhost/System/Library/DTDs/PropertyList.dtd\">"); 283 writer.println("<plist version=\"1.0\">"); 284 285 printNode(writer, 1, getNodeModel().getNodeHandler().getRootNode()); 286 287 writer.println("</plist>"); 288 writer.flush(); 289 } 290 291 /** 292 * Append a node to the writer, indented according to a specific level. 293 */ 294 private void printNode(final PrintWriter out, final int indentLevel, final ImmutableNode node) 295 { 296 final String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE); 297 298 if (node.getNodeName() != null) 299 { 300 out.println(padding + "<key>" + StringEscapeUtils.escapeXml10(node.getNodeName()) + "</key>"); 301 } 302 303 final List<ImmutableNode> children = node.getChildren(); 304 if (!children.isEmpty()) 305 { 306 out.println(padding + "<dict>"); 307 308 final Iterator<ImmutableNode> it = children.iterator(); 309 while (it.hasNext()) 310 { 311 final ImmutableNode child = it.next(); 312 printNode(out, indentLevel + 1, child); 313 314 if (it.hasNext()) 315 { 316 out.println(); 317 } 318 } 319 320 out.println(padding + "</dict>"); 321 } 322 else if (node.getValue() == null) 323 { 324 out.println(padding + "<dict/>"); 325 } 326 else 327 { 328 final Object value = node.getValue(); 329 printValue(out, indentLevel, value); 330 } 331 } 332 333 /** 334 * Append a value to the writer, indented according to a specific level. 335 */ 336 private void printValue(final PrintWriter out, final int indentLevel, final Object value) 337 { 338 final String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE); 339 340 if (value instanceof Date) 341 { 342 synchronized (PListNodeBuilder.FORMAT) 343 { 344 out.println(padding + "<date>" + PListNodeBuilder.FORMAT.format((Date) value) + "</date>"); 345 } 346 } 347 else if (value instanceof Calendar) 348 { 349 printValue(out, indentLevel, ((Calendar) value).getTime()); 350 } 351 else if (value instanceof Number) 352 { 353 if (value instanceof Double || value instanceof Float || value instanceof BigDecimal) 354 { 355 out.println(padding + "<real>" + value.toString() + "</real>"); 356 } 357 else 358 { 359 out.println(padding + "<integer>" + value.toString() + "</integer>"); 360 } 361 } 362 else if (value instanceof Boolean) 363 { 364 if (((Boolean) value).booleanValue()) 365 { 366 out.println(padding + "<true/>"); 367 } 368 else 369 { 370 out.println(padding + "<false/>"); 371 } 372 } 373 else if (value instanceof List) 374 { 375 out.println(padding + "<array>"); 376 for (final Object o : (List<?>) value) 377 { 378 printValue(out, indentLevel + 1, o); 379 } 380 out.println(padding + "</array>"); 381 } 382 else if (value instanceof HierarchicalConfiguration) 383 { 384 // This is safe because we have created this configuration 385 @SuppressWarnings("unchecked") 386 final 387 HierarchicalConfiguration<ImmutableNode> config = 388 (HierarchicalConfiguration<ImmutableNode>) value; 389 printNode(out, indentLevel, config.getNodeModel().getNodeHandler() 390 .getRootNode()); 391 } 392 else if (value instanceof ImmutableConfiguration) 393 { 394 // display a flat Configuration as a dictionary 395 out.println(padding + "<dict>"); 396 397 final ImmutableConfiguration config = (ImmutableConfiguration) value; 398 final Iterator<String> it = config.getKeys(); 399 while (it.hasNext()) 400 { 401 // create a node for each property 402 final String key = it.next(); 403 final ImmutableNode node = 404 new ImmutableNode.Builder().name(key) 405 .value(config.getProperty(key)).create(); 406 407 // print the node 408 printNode(out, indentLevel + 1, node); 409 410 if (it.hasNext()) 411 { 412 out.println(); 413 } 414 } 415 out.println(padding + "</dict>"); 416 } 417 else if (value instanceof Map) 418 { 419 // display a Map as a dictionary 420 final Map<String, Object> map = transformMap((Map<?, ?>) value); 421 printValue(out, indentLevel, new MapConfiguration(map)); 422 } 423 else if (value instanceof byte[]) 424 { 425 String base64; 426 try 427 { 428 base64 = new String(Base64.encodeBase64((byte[]) value), DATA_ENCODING); 429 } 430 catch (final UnsupportedEncodingException e) 431 { 432 // Cannot happen as UTF-8 is a standard encoding 433 throw new AssertionError(e); 434 } 435 out.println(padding + "<data>" + StringEscapeUtils.escapeXml10(base64) + "</data>"); 436 } 437 else if (value != null) 438 { 439 out.println(padding + "<string>" + StringEscapeUtils.escapeXml10(String.valueOf(value)) + "</string>"); 440 } 441 else 442 { 443 out.println(padding + "<string/>"); 444 } 445 } 446 447 /** 448 * Transform a map of arbitrary types into a map with string keys and object 449 * values. All keys of the source map which are not of type String are 450 * dropped. 451 * 452 * @param src the map to be converted 453 * @return the resulting map 454 */ 455 private static Map<String, Object> transformMap(final Map<?, ?> src) 456 { 457 final Map<String, Object> dest = new HashMap<>(); 458 for (final Map.Entry<?, ?> e : src.entrySet()) 459 { 460 if (e.getKey() instanceof String) 461 { 462 dest.put((String) e.getKey(), e.getValue()); 463 } 464 } 465 return dest; 466 } 467 468 /** 469 * SAX Handler to build the configuration nodes while the document is being parsed. 470 */ 471 private class XMLPropertyListHandler extends DefaultHandler 472 { 473 /** The buffer containing the text node being read */ 474 private final StringBuilder buffer = new StringBuilder(); 475 476 /** The stack of configuration nodes */ 477 private final List<PListNodeBuilder> stack = new ArrayList<>(); 478 479 /** The builder for the resulting node. */ 480 private final PListNodeBuilder resultBuilder; 481 482 public XMLPropertyListHandler() 483 { 484 resultBuilder = new PListNodeBuilder(); 485 push(resultBuilder); 486 } 487 488 /** 489 * Returns the builder for the result node. 490 * 491 * @return the result node builder 492 */ 493 public PListNodeBuilder getResultBuilder() 494 { 495 return resultBuilder; 496 } 497 498 /** 499 * Return the node on the top of the stack. 500 */ 501 private PListNodeBuilder peek() 502 { 503 if (!stack.isEmpty()) 504 { 505 return stack.get(stack.size() - 1); 506 } 507 return null; 508 } 509 510 /** 511 * Returns the node on top of the non-empty stack. Throws an exception if the 512 * stack is empty. 513 * 514 * @return the top node of the stack 515 * @throws ConfigurationRuntimeException if the stack is empty 516 */ 517 private PListNodeBuilder peekNE() 518 { 519 final PListNodeBuilder result = peek(); 520 if (result == null) 521 { 522 throw new ConfigurationRuntimeException("Access to empty stack!"); 523 } 524 return result; 525 } 526 527 /** 528 * Remove and return the node on the top of the stack. 529 */ 530 private PListNodeBuilder pop() 531 { 532 if (!stack.isEmpty()) 533 { 534 return stack.remove(stack.size() - 1); 535 } 536 return null; 537 } 538 539 /** 540 * Put a node on the top of the stack. 541 */ 542 private void push(final PListNodeBuilder node) 543 { 544 stack.add(node); 545 } 546 547 @Override 548 public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException 549 { 550 if ("array".equals(qName)) 551 { 552 push(new ArrayNodeBuilder()); 553 } 554 else if ("dict".equals(qName)) 555 { 556 if (peek() instanceof ArrayNodeBuilder) 557 { 558 // push the new root builder on the stack 559 push(new PListNodeBuilder()); 560 } 561 } 562 } 563 564 @Override 565 public void endElement(final String uri, final String localName, final String qName) throws SAXException 566 { 567 if ("key".equals(qName)) 568 { 569 // create a new node, link it to its parent and push it on the stack 570 final PListNodeBuilder node = new PListNodeBuilder(); 571 node.setName(buffer.toString()); 572 peekNE().addChild(node); 573 push(node); 574 } 575 else if ("dict".equals(qName)) 576 { 577 // remove the root of the XMLPropertyListConfiguration previously pushed on the stack 578 final PListNodeBuilder builder = pop(); 579 assert builder != null : "Stack was empty!"; 580 if (peek() instanceof ArrayNodeBuilder) 581 { 582 // create the configuration 583 final XMLPropertyListConfiguration config = new XMLPropertyListConfiguration(builder.createNode()); 584 585 // add it to the ArrayNodeBuilder 586 final ArrayNodeBuilder node = (ArrayNodeBuilder) peekNE(); 587 node.addValue(config); 588 } 589 } 590 else 591 { 592 if ("string".equals(qName)) 593 { 594 peekNE().addValue(buffer.toString()); 595 } 596 else if ("integer".equals(qName)) 597 { 598 peekNE().addIntegerValue(buffer.toString()); 599 } 600 else if ("real".equals(qName)) 601 { 602 peekNE().addRealValue(buffer.toString()); 603 } 604 else if ("true".equals(qName)) 605 { 606 peekNE().addTrueValue(); 607 } 608 else if ("false".equals(qName)) 609 { 610 peekNE().addFalseValue(); 611 } 612 else if ("data".equals(qName)) 613 { 614 peekNE().addDataValue(buffer.toString()); 615 } 616 else if ("date".equals(qName)) 617 { 618 try 619 { 620 peekNE().addDateValue(buffer.toString()); 621 } 622 catch (final IllegalArgumentException iex) 623 { 624 getLogger().warn( 625 "Ignoring invalid date property " + buffer); 626 } 627 } 628 else if ("array".equals(qName)) 629 { 630 final ArrayNodeBuilder array = (ArrayNodeBuilder) pop(); 631 peekNE().addList(array); 632 } 633 634 // remove the plist node on the stack once the value has been parsed, 635 // array nodes remains on the stack for the next values in the list 636 if (!(peek() instanceof ArrayNodeBuilder)) 637 { 638 pop(); 639 } 640 } 641 642 buffer.setLength(0); 643 } 644 645 @Override 646 public void characters(final char[] ch, final int start, final int length) throws SAXException 647 { 648 buffer.append(ch, start, length); 649 } 650 } 651 652 /** 653 * A specialized builder class with addXXX methods to parse the typed data passed by the SAX handler. 654 * It is used for creating the nodes of the configuration. 655 */ 656 private static class PListNodeBuilder 657 { 658 /** 659 * The MacOS FORMAT of dates in plist files. Note: Because 660 * {@code SimpleDateFormat} is not thread-safe, each access has to be 661 * synchronized. 662 */ 663 private static final DateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); 664 static 665 { 666 FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); 667 } 668 669 /** 670 * The GNUstep FORMAT of dates in plist files. Note: Because 671 * {@code SimpleDateFormat} is not thread-safe, each access has to be 672 * synchronized. 673 */ 674 private static final DateFormat GNUSTEP_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z"); 675 676 /** A collection with child builders of this builder. */ 677 private final Collection<PListNodeBuilder> childBuilders = 678 new LinkedList<>(); 679 680 /** The name of the represented node. */ 681 private String name; 682 683 /** The current value of the represented node. */ 684 private Object value; 685 686 /** 687 * Update the value of the node. If the existing value is null, it's 688 * replaced with the new value. If the existing value is a list, the 689 * specified value is appended to the list. If the existing value is 690 * not null, a list with the two values is built. 691 * 692 * @param v the value to be added 693 */ 694 public void addValue(final Object v) 695 { 696 if (value == null) 697 { 698 value = v; 699 } 700 else if (value instanceof Collection) 701 { 702 // This is safe because we create the collections ourselves 703 @SuppressWarnings("unchecked") 704 final 705 Collection<Object> collection = (Collection<Object>) value; 706 collection.add(v); 707 } 708 else 709 { 710 final List<Object> list = new ArrayList<>(); 711 list.add(value); 712 list.add(v); 713 value = list; 714 } 715 } 716 717 /** 718 * Parse the specified string as a date and add it to the values of the node. 719 * 720 * @param value the value to be added 721 * @throws IllegalArgumentException if the date string cannot be parsed 722 */ 723 public void addDateValue(final String value) 724 { 725 try 726 { 727 if (value.indexOf(' ') != -1) 728 { 729 // parse the date using the GNUstep FORMAT 730 synchronized (GNUSTEP_FORMAT) 731 { 732 addValue(GNUSTEP_FORMAT.parse(value)); 733 } 734 } 735 else 736 { 737 // parse the date using the MacOS X FORMAT 738 synchronized (FORMAT) 739 { 740 addValue(FORMAT.parse(value)); 741 } 742 } 743 } 744 catch (final ParseException e) 745 { 746 throw new IllegalArgumentException(String.format( 747 "'%s' cannot be parsed to a date!", value), e); 748 } 749 } 750 751 /** 752 * Parse the specified string as a byte array in base 64 FORMAT 753 * and add it to the values of the node. 754 * 755 * @param value the value to be added 756 */ 757 public void addDataValue(final String value) 758 { 759 try 760 { 761 addValue(Base64.decodeBase64(value.getBytes(DATA_ENCODING))); 762 } 763 catch (final UnsupportedEncodingException e) 764 { 765 //Cannot happen as UTF-8 is a default encoding 766 throw new AssertionError(e); 767 } 768 } 769 770 /** 771 * Parse the specified string as an Interger and add it to the values of the node. 772 * 773 * @param value the value to be added 774 */ 775 public void addIntegerValue(final String value) 776 { 777 addValue(new BigInteger(value)); 778 } 779 780 /** 781 * Parse the specified string as a Double and add it to the values of the node. 782 * 783 * @param value the value to be added 784 */ 785 public void addRealValue(final String value) 786 { 787 addValue(new BigDecimal(value)); 788 } 789 790 /** 791 * Add a boolean value 'true' to the values of the node. 792 */ 793 public void addTrueValue() 794 { 795 addValue(Boolean.TRUE); 796 } 797 798 /** 799 * Add a boolean value 'false' to the values of the node. 800 */ 801 public void addFalseValue() 802 { 803 addValue(Boolean.FALSE); 804 } 805 806 /** 807 * Add a sublist to the values of the node. 808 * 809 * @param node the node whose value will be added to the current node value 810 */ 811 public void addList(final ArrayNodeBuilder node) 812 { 813 addValue(node.getNodeValue()); 814 } 815 816 /** 817 * Sets the name of the represented node. 818 * 819 * @param nodeName the node name 820 */ 821 public void setName(final String nodeName) 822 { 823 name = nodeName; 824 } 825 826 /** 827 * Adds the given child builder to this builder. 828 * 829 * @param child the child builder to be added 830 */ 831 public void addChild(final PListNodeBuilder child) 832 { 833 childBuilders.add(child); 834 } 835 836 /** 837 * Creates the configuration node defined by this builder. 838 * 839 * @return the newly created configuration node 840 */ 841 public ImmutableNode createNode() 842 { 843 final ImmutableNode.Builder nodeBuilder = 844 new ImmutableNode.Builder(childBuilders.size()); 845 for (final PListNodeBuilder child : childBuilders) 846 { 847 nodeBuilder.addChild(child.createNode()); 848 } 849 return nodeBuilder.name(name).value(getNodeValue()).create(); 850 } 851 852 /** 853 * Returns the final value for the node to be created. This method is 854 * called when the represented configuration node is actually created. 855 * 856 * @return the value of the resulting configuration node 857 */ 858 protected Object getNodeValue() 859 { 860 return value; 861 } 862 } 863 864 /** 865 * Container for array elements. <b>Do not use this class !</b> 866 * It is used internally by XMLPropertyConfiguration to parse the 867 * configuration file, it may be removed at any moment in the future. 868 */ 869 private static class ArrayNodeBuilder extends PListNodeBuilder 870 { 871 /** The list of values in the array. */ 872 private final List<Object> list = new ArrayList<>(); 873 874 /** 875 * Add an object to the array. 876 * 877 * @param value the value to be added 878 */ 879 @Override 880 public void addValue(final Object value) 881 { 882 list.add(value); 883 } 884 885 /** 886 * Return the list of values in the array. 887 * 888 * @return the {@link List} of values 889 */ 890 @Override 891 protected Object getNodeValue() 892 { 893 return list; 894 } 895 } 896}