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