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