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 * 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 String DATA_ENCODING = "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 for (final Object o : (List<?>) value) { 316 printValue(out, indentLevel + 1, o); 317 } 318 out.println(padding + "</array>"); 319 } else if (value instanceof HierarchicalConfiguration) { 320 // This is safe because we have created this configuration 321 @SuppressWarnings("unchecked") 322 final HierarchicalConfiguration<ImmutableNode> config = (HierarchicalConfiguration<ImmutableNode>) value; 323 printNode(out, indentLevel, config.getNodeModel().getNodeHandler().getRootNode()); 324 } else if (value instanceof ImmutableConfiguration) { 325 // display a flat Configuration as a dictionary 326 out.println(padding + "<dict>"); 327 328 final ImmutableConfiguration config = (ImmutableConfiguration) value; 329 final Iterator<String> it = config.getKeys(); 330 while (it.hasNext()) { 331 // create a node for each property 332 final String key = it.next(); 333 final ImmutableNode node = new ImmutableNode.Builder().name(key).value(config.getProperty(key)).create(); 334 335 // print the node 336 printNode(out, indentLevel + 1, node); 337 338 if (it.hasNext()) { 339 out.println(); 340 } 341 } 342 out.println(padding + "</dict>"); 343 } else if (value instanceof Map) { 344 // display a Map as a dictionary 345 final Map<String, Object> map = transformMap((Map<?, ?>) value); 346 printValue(out, indentLevel, new MapConfiguration(map)); 347 } else if (value instanceof byte[]) { 348 final String base64; 349 try { 350 base64 = new String(Base64.encodeBase64((byte[]) value), DATA_ENCODING); 351 } catch (final UnsupportedEncodingException e) { 352 // Cannot happen as UTF-8 is a standard encoding 353 throw new AssertionError(e); 354 } 355 out.println(padding + "<data>" + StringEscapeUtils.escapeXml10(base64) + "</data>"); 356 } else if (value != null) { 357 out.println(padding + "<string>" + StringEscapeUtils.escapeXml10(String.valueOf(value)) + "</string>"); 358 } else { 359 out.println(padding + "<string/>"); 360 } 361 } 362 363 /** 364 * Transform a map of arbitrary types into a map with string keys and object values. All keys of the source map which 365 * are not of type String are dropped. 366 * 367 * @param src the map to be converted 368 * @return the resulting map 369 */ 370 private static Map<String, Object> transformMap(final Map<?, ?> src) { 371 final Map<String, Object> dest = new HashMap<>(); 372 for (final Map.Entry<?, ?> e : src.entrySet()) { 373 if (e.getKey() instanceof String) { 374 dest.put((String) e.getKey(), e.getValue()); 375 } 376 } 377 return dest; 378 } 379 380 /** 381 * SAX Handler to build the configuration nodes while the document is being parsed. 382 */ 383 private class XMLPropertyListHandler extends DefaultHandler { 384 /** The buffer containing the text node being read */ 385 private final StringBuilder buffer = new StringBuilder(); 386 387 /** The stack of configuration nodes */ 388 private final List<PListNodeBuilder> stack = new ArrayList<>(); 389 390 /** The builder for the resulting node. */ 391 private final PListNodeBuilder resultBuilder; 392 393 public XMLPropertyListHandler() { 394 resultBuilder = new PListNodeBuilder(); 395 push(resultBuilder); 396 } 397 398 /** 399 * Returns the builder for the result node. 400 * 401 * @return the result node builder 402 */ 403 public PListNodeBuilder getResultBuilder() { 404 return resultBuilder; 405 } 406 407 /** 408 * Return the node on the top of the stack. 409 */ 410 private PListNodeBuilder peek() { 411 if (!stack.isEmpty()) { 412 return stack.get(stack.size() - 1); 413 } 414 return null; 415 } 416 417 /** 418 * Returns the node on top of the non-empty stack. Throws an exception if the stack is empty. 419 * 420 * @return the top node of the stack 421 * @throws ConfigurationRuntimeException if the stack is empty 422 */ 423 private PListNodeBuilder peekNE() { 424 final PListNodeBuilder result = peek(); 425 if (result == null) { 426 throw new ConfigurationRuntimeException("Access to empty stack!"); 427 } 428 return result; 429 } 430 431 /** 432 * Remove and return the node on the top of the stack. 433 */ 434 private PListNodeBuilder pop() { 435 if (!stack.isEmpty()) { 436 return stack.remove(stack.size() - 1); 437 } 438 return null; 439 } 440 441 /** 442 * Put a node on the top of the stack. 443 */ 444 private void push(final PListNodeBuilder node) { 445 stack.add(node); 446 } 447 448 @Override 449 public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException { 450 if ("array".equals(qName)) { 451 push(new ArrayNodeBuilder()); 452 } else if ("dict".equals(qName) && peek() instanceof ArrayNodeBuilder) { 453 // push the new root builder on the stack 454 push(new PListNodeBuilder()); 455 } 456 } 457 458 @Override 459 public void endElement(final String uri, final String localName, final String qName) throws SAXException { 460 if ("key".equals(qName)) { 461 // create a new node, link it to its parent and push it on the stack 462 final PListNodeBuilder node = new PListNodeBuilder(); 463 node.setName(buffer.toString()); 464 peekNE().addChild(node); 465 push(node); 466 } else if ("dict".equals(qName)) { 467 // remove the root of the XMLPropertyListConfiguration previously pushed on the stack 468 final PListNodeBuilder builder = pop(); 469 assert builder != null : "Stack was empty!"; 470 if (peek() instanceof ArrayNodeBuilder) { 471 // create the configuration 472 final XMLPropertyListConfiguration config = new XMLPropertyListConfiguration(builder.createNode()); 473 474 // add it to the ArrayNodeBuilder 475 final ArrayNodeBuilder node = (ArrayNodeBuilder) peekNE(); 476 node.addValue(config); 477 } 478 } else { 479 if ("string".equals(qName)) { 480 peekNE().addValue(buffer.toString()); 481 } else if ("integer".equals(qName)) { 482 peekNE().addIntegerValue(buffer.toString()); 483 } else if ("real".equals(qName)) { 484 peekNE().addRealValue(buffer.toString()); 485 } else if ("true".equals(qName)) { 486 peekNE().addTrueValue(); 487 } else if ("false".equals(qName)) { 488 peekNE().addFalseValue(); 489 } else if ("data".equals(qName)) { 490 peekNE().addDataValue(buffer.toString()); 491 } else if ("date".equals(qName)) { 492 try { 493 peekNE().addDateValue(buffer.toString()); 494 } catch (final IllegalArgumentException iex) { 495 getLogger().warn("Ignoring invalid date property " + buffer); 496 } 497 } else if ("array".equals(qName)) { 498 final ArrayNodeBuilder array = (ArrayNodeBuilder) pop(); 499 peekNE().addList(array); 500 } 501 502 // remove the plist node on the stack once the value has been parsed, 503 // array nodes remains on the stack for the next values in the list 504 if (!(peek() instanceof ArrayNodeBuilder)) { 505 pop(); 506 } 507 } 508 509 buffer.setLength(0); 510 } 511 512 @Override 513 public void characters(final char[] ch, final int start, final int length) throws SAXException { 514 buffer.append(ch, start, length); 515 } 516 } 517 518 /** 519 * A specialized builder class with addXXX methods to parse the typed data passed by the SAX handler. It is used for 520 * creating the nodes of the configuration. 521 */ 522 private static class PListNodeBuilder { 523 /** 524 * The MacOS FORMAT of dates in plist files. Note: Because {@code SimpleDateFormat} is not thread-safe, each access has 525 * to be synchronized. 526 */ 527 private static final DateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); 528 static { 529 FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); 530 } 531 532 /** 533 * The GNUstep FORMAT of dates in plist files. Note: Because {@code SimpleDateFormat} is not thread-safe, each access 534 * has to be synchronized. 535 */ 536 private static final DateFormat GNUSTEP_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z"); 537 538 /** A collection with child builders of this builder. */ 539 private final Collection<PListNodeBuilder> childBuilders = new LinkedList<>(); 540 541 /** The name of the represented node. */ 542 private String name; 543 544 /** The current value of the represented node. */ 545 private Object value; 546 547 /** 548 * Update the value of the node. If the existing value is null, it's replaced with the new value. If the existing value 549 * is a list, the specified value is appended to the list. If the existing value is not null, a list with the two values 550 * is built. 551 * 552 * @param v the value to be added 553 */ 554 public void addValue(final Object v) { 555 if (value == null) { 556 value = v; 557 } else if (value instanceof Collection) { 558 // This is safe because we create the collections ourselves 559 @SuppressWarnings("unchecked") 560 final Collection<Object> collection = (Collection<Object>) value; 561 collection.add(v); 562 } else { 563 final List<Object> list = new ArrayList<>(); 564 list.add(value); 565 list.add(v); 566 value = list; 567 } 568 } 569 570 /** 571 * Parse the specified string as a date and add it to the values of the node. 572 * 573 * @param value the value to be added 574 * @throws IllegalArgumentException if the date string cannot be parsed 575 */ 576 public void addDateValue(final String value) { 577 try { 578 if (value.indexOf(' ') != -1) { 579 // parse the date using the GNUstep FORMAT 580 synchronized (GNUSTEP_FORMAT) { 581 addValue(GNUSTEP_FORMAT.parse(value)); 582 } 583 } else { 584 // parse the date using the MacOS X FORMAT 585 synchronized (FORMAT) { 586 addValue(FORMAT.parse(value)); 587 } 588 } 589 } catch (final ParseException e) { 590 throw new IllegalArgumentException(String.format("'%s' cannot be parsed to a date!", value), e); 591 } 592 } 593 594 /** 595 * Parse the specified string as a byte array in base 64 FORMAT and add it to the values of the node. 596 * 597 * @param value the value to be added 598 */ 599 public void addDataValue(final String value) { 600 try { 601 addValue(Base64.decodeBase64(value.getBytes(DATA_ENCODING))); 602 } catch (final UnsupportedEncodingException e) { 603 // Cannot happen as UTF-8 is a default encoding 604 throw new AssertionError(e); 605 } 606 } 607 608 /** 609 * Parse the specified string as an Interger and add it to the values of the node. 610 * 611 * @param value the value to be added 612 */ 613 public void addIntegerValue(final String value) { 614 addValue(new BigInteger(value)); 615 } 616 617 /** 618 * Parse the specified string as a Double and add it to the values of the node. 619 * 620 * @param value the value to be added 621 */ 622 public void addRealValue(final String value) { 623 addValue(new BigDecimal(value)); 624 } 625 626 /** 627 * Add a boolean value 'true' to the values of the node. 628 */ 629 public void addTrueValue() { 630 addValue(Boolean.TRUE); 631 } 632 633 /** 634 * Add a boolean value 'false' to the values of the node. 635 */ 636 public void addFalseValue() { 637 addValue(Boolean.FALSE); 638 } 639 640 /** 641 * Add a sublist to the values of the node. 642 * 643 * @param node the node whose value will be added to the current node value 644 */ 645 public void addList(final ArrayNodeBuilder node) { 646 addValue(node.getNodeValue()); 647 } 648 649 /** 650 * Sets the name of the represented node. 651 * 652 * @param nodeName the node name 653 */ 654 public void setName(final String nodeName) { 655 name = nodeName; 656 } 657 658 /** 659 * Adds the given child builder to this builder. 660 * 661 * @param child the child builder to be added 662 */ 663 public void addChild(final PListNodeBuilder child) { 664 childBuilders.add(child); 665 } 666 667 /** 668 * Creates the configuration node defined by this builder. 669 * 670 * @return the newly created configuration node 671 */ 672 public ImmutableNode createNode() { 673 final ImmutableNode.Builder nodeBuilder = new ImmutableNode.Builder(childBuilders.size()); 674 for (final PListNodeBuilder child : childBuilders) { 675 nodeBuilder.addChild(child.createNode()); 676 } 677 return nodeBuilder.name(name).value(getNodeValue()).create(); 678 } 679 680 /** 681 * Returns the final value for the node to be created. This method is called when the represented configuration node is 682 * actually created. 683 * 684 * @return the value of the resulting configuration node 685 */ 686 protected Object getNodeValue() { 687 return value; 688 } 689 } 690 691 /** 692 * Container for array elements. <b>Do not use this class !</b> It is used internally by XMLPropertyConfiguration to 693 * parse the configuration file, it may be removed at any moment in the future. 694 */ 695 private static class ArrayNodeBuilder extends PListNodeBuilder { 696 /** The list of values in the array. */ 697 private final List<Object> list = new ArrayList<>(); 698 699 /** 700 * Add an object to the array. 701 * 702 * @param value the value to be added 703 */ 704 @Override 705 public void addValue(final Object value) { 706 list.add(value); 707 } 708 709 /** 710 * Return the list of values in the array. 711 * 712 * @return the {@link List} of values 713 */ 714 @Override 715 protected Object getNodeValue() { 716 return list; 717 } 718 } 719}