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