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