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