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