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