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 018 package org.apache.commons.configuration.plist; 019 020 import java.io.File; 021 import java.io.PrintWriter; 022 import java.io.Reader; 023 import java.io.Writer; 024 import java.math.BigDecimal; 025 import java.math.BigInteger; 026 import java.net.URL; 027 import java.text.DateFormat; 028 import java.text.ParseException; 029 import java.text.SimpleDateFormat; 030 import java.util.ArrayList; 031 import java.util.Calendar; 032 import java.util.Collection; 033 import java.util.Date; 034 import java.util.HashMap; 035 import java.util.Iterator; 036 import java.util.List; 037 import java.util.Map; 038 import java.util.TimeZone; 039 040 import javax.xml.parsers.SAXParser; 041 import javax.xml.parsers.SAXParserFactory; 042 043 import org.apache.commons.codec.binary.Base64; 044 import org.apache.commons.configuration.AbstractHierarchicalFileConfiguration; 045 import org.apache.commons.configuration.Configuration; 046 import org.apache.commons.configuration.ConfigurationException; 047 import org.apache.commons.configuration.HierarchicalConfiguration; 048 import org.apache.commons.configuration.MapConfiguration; 049 import org.apache.commons.configuration.tree.ConfigurationNode; 050 import org.apache.commons.lang.StringEscapeUtils; 051 import org.apache.commons.lang.StringUtils; 052 import org.xml.sax.Attributes; 053 import org.xml.sax.EntityResolver; 054 import org.xml.sax.InputSource; 055 import org.xml.sax.SAXException; 056 import 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 1210644 2011-12-05 21:20:39Z oheger $ 123 */ 124 public 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 static 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 ((PListNode) peek()).addDateValue(buffer.toString()); 582 } 583 else if ("array".equals(qName)) 584 { 585 ArrayNode array = (ArrayNode) pop(); 586 ((PListNode) peek()).addList(array); 587 } 588 589 // remove the plist node on the stack once the value has been parsed, 590 // array nodes remains on the stack for the next values in the list 591 if (!(peek() instanceof ArrayNode)) 592 { 593 pop(); 594 } 595 } 596 597 buffer.setLength(0); 598 } 599 600 @Override 601 public void characters(char[] ch, int start, int length) throws SAXException 602 { 603 buffer.append(ch, start, length); 604 } 605 } 606 607 /** 608 * Node extension with addXXX methods to parse the typed data passed by the SAX handler. 609 * <b>Do not use this class !</b> It is used internally by XMLPropertyConfiguration 610 * to parse the configuration file, it may be removed at any moment in the future. 611 */ 612 public static class PListNode extends Node 613 { 614 /** 615 * The serial version UID. 616 */ 617 private static final long serialVersionUID = -7614060264754798317L; 618 619 /** The MacOS format of dates in plist files. */ 620 private static DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); 621 static 622 { 623 format.setTimeZone(TimeZone.getTimeZone("UTC")); 624 } 625 626 /** The GNUstep format of dates in plist files. */ 627 private static DateFormat gnustepFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z"); 628 629 /** 630 * Update the value of the node. If the existing value is null, it's 631 * replaced with the new value. If the existing value is a list, the 632 * specified value is appended to the list. If the existing value is 633 * not null, a list with the two values is built. 634 * 635 * @param value the value to be added 636 */ 637 public void addValue(Object value) 638 { 639 if (getValue() == null) 640 { 641 setValue(value); 642 } 643 else if (getValue() instanceof Collection) 644 { 645 // This is safe because we create the collections ourselves 646 @SuppressWarnings("unchecked") 647 Collection<Object> collection = (Collection<Object>) getValue(); 648 collection.add(value); 649 } 650 else 651 { 652 List<Object> list = new ArrayList<Object>(); 653 list.add(getValue()); 654 list.add(value); 655 setValue(list); 656 } 657 } 658 659 /** 660 * Parse the specified string as a date and add it to the values of the node. 661 * 662 * @param value the value to be added 663 */ 664 public void addDateValue(String value) 665 { 666 try 667 { 668 if (value.indexOf(' ') != -1) 669 { 670 // parse the date using the GNUstep format 671 synchronized (gnustepFormat) 672 { 673 addValue(gnustepFormat.parse(value)); 674 } 675 } 676 else 677 { 678 // parse the date using the MacOS X format 679 synchronized (format) 680 { 681 addValue(format.parse(value)); 682 } 683 } 684 } 685 catch (ParseException e) 686 { 687 // ignore 688 ; 689 } 690 } 691 692 /** 693 * Parse the specified string as a byte array in base 64 format 694 * and add it to the values of the node. 695 * 696 * @param value the value to be added 697 */ 698 public void addDataValue(String value) 699 { 700 addValue(Base64.decodeBase64(value.getBytes())); 701 } 702 703 /** 704 * Parse the specified string as an Interger and add it to the values of the node. 705 * 706 * @param value the value to be added 707 */ 708 public void addIntegerValue(String value) 709 { 710 addValue(new BigInteger(value)); 711 } 712 713 /** 714 * Parse the specified string as a Double and add it to the values of the node. 715 * 716 * @param value the value to be added 717 */ 718 public void addRealValue(String value) 719 { 720 addValue(new BigDecimal(value)); 721 } 722 723 /** 724 * Add a boolean value 'true' to the values of the node. 725 */ 726 public void addTrueValue() 727 { 728 addValue(Boolean.TRUE); 729 } 730 731 /** 732 * Add a boolean value 'false' to the values of the node. 733 */ 734 public void addFalseValue() 735 { 736 addValue(Boolean.FALSE); 737 } 738 739 /** 740 * Add a sublist to the values of the node. 741 * 742 * @param node the node whose value will be added to the current node value 743 */ 744 public void addList(ArrayNode node) 745 { 746 addValue(node.getValue()); 747 } 748 } 749 750 /** 751 * Container for array elements. <b>Do not use this class !</b> 752 * It is used internally by XMLPropertyConfiguration to parse the 753 * configuration file, it may be removed at any moment in the future. 754 */ 755 public static class ArrayNode extends PListNode 756 { 757 /** 758 * The serial version UID. 759 */ 760 private static final long serialVersionUID = 5586544306664205835L; 761 762 /** The list of values in the array. */ 763 private List<Object> list = new ArrayList<Object>(); 764 765 /** 766 * Add an object to the array. 767 * 768 * @param value the value to be added 769 */ 770 @Override 771 public void addValue(Object value) 772 { 773 list.add(value); 774 } 775 776 /** 777 * Return the list of values in the array. 778 * 779 * @return the {@link List} of values 780 */ 781 @Override 782 public Object getValue() 783 { 784 return list; 785 } 786 } 787 }