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.net.URL; 025import java.util.ArrayList; 026import java.util.Calendar; 027import java.util.Date; 028import java.util.HashMap; 029import java.util.Iterator; 030import java.util.List; 031import java.util.Map; 032import java.util.TimeZone; 033 034import org.apache.commons.codec.binary.Hex; 035import org.apache.commons.configuration.AbstractHierarchicalFileConfiguration; 036import org.apache.commons.configuration.Configuration; 037import org.apache.commons.configuration.ConfigurationException; 038import org.apache.commons.configuration.HierarchicalConfiguration; 039import org.apache.commons.configuration.MapConfiguration; 040import org.apache.commons.configuration.tree.ConfigurationNode; 041import org.apache.commons.lang.StringUtils; 042 043/** 044 * NeXT / OpenStep style configuration. This configuration can read and write 045 * ASCII plist files. It supports the GNUStep extension to specify date objects. 046 * <p> 047 * References: 048 * <ul> 049 * <li><a 050 * href="http://developer.apple.com/documentation/Cocoa/Conceptual/PropertyLists/OldStylePlists/OldStylePLists.html"> 051 * Apple Documentation - Old-Style ASCII Property Lists</a></li> 052 * <li><a 053 * href="http://www.gnustep.org/resources/documentation/Developer/Base/Reference/NSPropertyList.html"> 054 * GNUStep Documentation</a></li> 055 * </ul> 056 * 057 * <p>Example:</p> 058 * <pre> 059 * { 060 * foo = "bar"; 061 * 062 * array = ( value1, value2, value3 ); 063 * 064 * data = <4f3e0145ab>; 065 * 066 * date = <*D2007-05-05 20:05:00 +0100>; 067 * 068 * nested = 069 * { 070 * key1 = value1; 071 * key2 = value; 072 * nested = 073 * { 074 * foo = bar 075 * } 076 * } 077 * } 078 * </pre> 079 * 080 * @since 1.2 081 * 082 * @author Emmanuel Bourg 083 * @version $Id: PropertyListConfiguration.java 1210637 2011-12-05 21:12:12Z oheger $ 084 */ 085public class PropertyListConfiguration extends AbstractHierarchicalFileConfiguration 086{ 087 /** Constant for the separator parser for the date part. */ 088 private static final DateComponentParser DATE_SEPARATOR_PARSER = new DateSeparatorParser( 089 "-"); 090 091 /** Constant for the separator parser for the time part. */ 092 private static final DateComponentParser TIME_SEPARATOR_PARSER = new DateSeparatorParser( 093 ":"); 094 095 /** Constant for the separator parser for blanks between the parts. */ 096 private static final DateComponentParser BLANK_SEPARATOR_PARSER = new DateSeparatorParser( 097 " "); 098 099 /** An array with the component parsers for dealing with dates. */ 100 private static final DateComponentParser[] DATE_PARSERS = 101 {new DateSeparatorParser("<*D"), new DateFieldParser(Calendar.YEAR, 4), 102 DATE_SEPARATOR_PARSER, new DateFieldParser(Calendar.MONTH, 2, 1), 103 DATE_SEPARATOR_PARSER, new DateFieldParser(Calendar.DATE, 2), 104 BLANK_SEPARATOR_PARSER, 105 new DateFieldParser(Calendar.HOUR_OF_DAY, 2), 106 TIME_SEPARATOR_PARSER, new DateFieldParser(Calendar.MINUTE, 2), 107 TIME_SEPARATOR_PARSER, new DateFieldParser(Calendar.SECOND, 2), 108 BLANK_SEPARATOR_PARSER, new DateTimeZoneParser(), 109 new DateSeparatorParser(">")}; 110 111 /** Constant for the ID prefix for GMT time zones. */ 112 private static final String TIME_ZONE_PREFIX = "GMT"; 113 114 /** The serial version UID. */ 115 private static final long serialVersionUID = 3227248503779092127L; 116 117 /** Constant for the milliseconds of a minute.*/ 118 private static final int MILLIS_PER_MINUTE = 1000 * 60; 119 120 /** Constant for the minutes per hour.*/ 121 private static final int MINUTES_PER_HOUR = 60; 122 123 /** Size of the indentation for the generated file. */ 124 private static final int INDENT_SIZE = 4; 125 126 /** Constant for the length of a time zone.*/ 127 private static final int TIME_ZONE_LENGTH = 5; 128 129 /** Constant for the padding character in the date format.*/ 130 private static final char PAD_CHAR = '0'; 131 132 /** 133 * Creates an empty PropertyListConfiguration object which can be 134 * used to synthesize a new plist file by adding values and 135 * then saving(). 136 */ 137 public PropertyListConfiguration() 138 { 139 } 140 141 /** 142 * Creates a new instance of {@code PropertyListConfiguration} and 143 * copies the content of the specified configuration into this object. 144 * 145 * @param c the configuration to copy 146 * @since 1.4 147 */ 148 public PropertyListConfiguration(HierarchicalConfiguration c) 149 { 150 super(c); 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 ConfigurationException Error while loading the plist file 158 */ 159 public PropertyListConfiguration(String fileName) throws ConfigurationException 160 { 161 super(fileName); 162 } 163 164 /** 165 * Creates and loads the property list from the specified file. 166 * 167 * @param file The plist file to load. 168 * @throws ConfigurationException Error while loading the plist file 169 */ 170 public PropertyListConfiguration(File file) throws ConfigurationException 171 { 172 super(file); 173 } 174 175 /** 176 * Creates and loads the property list from the specified URL. 177 * 178 * @param url The location of the plist file to load. 179 * @throws ConfigurationException Error while loading the plist file 180 */ 181 public PropertyListConfiguration(URL url) throws ConfigurationException 182 { 183 super(url); 184 } 185 186 @Override 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 @Override 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 PropertyListParser parser = new PropertyListParser(in); 229 try 230 { 231 HierarchicalConfiguration config = parser.parse(); 232 setRoot(config.getRoot()); 233 } 234 catch (ParseException e) 235 { 236 throw new ConfigurationException(e); 237 } 238 } 239 240 public void save(Writer out) throws ConfigurationException 241 { 242 PrintWriter writer = new PrintWriter(out); 243 printNode(writer, 0, getRoot()); 244 writer.flush(); 245 } 246 247 /** 248 * Append a node to the writer, indented according to a specific level. 249 */ 250 private void printNode(PrintWriter out, int indentLevel, ConfigurationNode node) 251 { 252 String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE); 253 254 if (node.getName() != null) 255 { 256 out.print(padding + quoteString(node.getName()) + " = "); 257 } 258 259 List<ConfigurationNode> children = new ArrayList<ConfigurationNode>(node.getChildren()); 260 if (!children.isEmpty()) 261 { 262 // skip a line, except for the root dictionary 263 if (indentLevel > 0) 264 { 265 out.println(); 266 } 267 268 out.println(padding + "{"); 269 270 // display the children 271 Iterator<ConfigurationNode> it = children.iterator(); 272 while (it.hasNext()) 273 { 274 ConfigurationNode child = it.next(); 275 276 printNode(out, indentLevel + 1, child); 277 278 // add a semi colon for elements that are not dictionaries 279 Object value = child.getValue(); 280 if (value != null && !(value instanceof Map) && !(value instanceof Configuration)) 281 { 282 out.println(";"); 283 } 284 285 // skip a line after arrays and dictionaries 286 if (it.hasNext() && (value == null || value instanceof List)) 287 { 288 out.println(); 289 } 290 } 291 292 out.print(padding + "}"); 293 294 // line feed if the dictionary is not in an array 295 if (node.getParentNode() != null) 296 { 297 out.println(); 298 } 299 } 300 else if (node.getValue() == null) 301 { 302 out.println(); 303 out.print(padding + "{ };"); 304 305 // line feed if the dictionary is not in an array 306 if (node.getParentNode() != null) 307 { 308 out.println(); 309 } 310 } 311 else 312 { 313 // display the leaf value 314 Object value = node.getValue(); 315 printValue(out, indentLevel, value); 316 } 317 } 318 319 /** 320 * Append a value to the writer, indented according to a specific level. 321 */ 322 private void printValue(PrintWriter out, int indentLevel, Object value) 323 { 324 String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE); 325 326 if (value instanceof List) 327 { 328 out.print("( "); 329 Iterator<?> it = ((List<?>) value).iterator(); 330 while (it.hasNext()) 331 { 332 printValue(out, indentLevel + 1, it.next()); 333 if (it.hasNext()) 334 { 335 out.print(", "); 336 } 337 } 338 out.print(" )"); 339 } 340 else if (value instanceof HierarchicalConfiguration) 341 { 342 printNode(out, indentLevel, ((HierarchicalConfiguration) value).getRoot()); 343 } 344 else if (value instanceof Configuration) 345 { 346 // display a flat Configuration as a dictionary 347 out.println(); 348 out.println(padding + "{"); 349 350 Configuration config = (Configuration) value; 351 Iterator<String> it = config.getKeys(); 352 while (it.hasNext()) 353 { 354 String key = it.next(); 355 Node node = new Node(key); 356 node.setValue(config.getProperty(key)); 357 358 printNode(out, indentLevel + 1, node); 359 out.println(";"); 360 } 361 out.println(padding + "}"); 362 } 363 else if (value instanceof Map) 364 { 365 // display a Map as a dictionary 366 Map<String, Object> map = transformMap((Map<?, ?>) value); 367 printValue(out, indentLevel, new MapConfiguration(map)); 368 } 369 else if (value instanceof byte[]) 370 { 371 out.print("<" + new String(Hex.encodeHex((byte[]) value)) + ">"); 372 } 373 else if (value instanceof Date) 374 { 375 out.print(formatDate((Date) value)); 376 } 377 else if (value != null) 378 { 379 out.print(quoteString(String.valueOf(value))); 380 } 381 } 382 383 /** 384 * Quote the specified string if necessary, that's if the string contains: 385 * <ul> 386 * <li>a space character (' ', '\t', '\r', '\n')</li> 387 * <li>a quote '"'</li> 388 * <li>special characters in plist files ('(', ')', '{', '}', '=', ';', ',')</li> 389 * </ul> 390 * Quotes within the string are escaped. 391 * 392 * <p>Examples:</p> 393 * <ul> 394 * <li>abcd -> abcd</li> 395 * <li>ab cd -> "ab cd"</li> 396 * <li>foo"bar -> "foo\"bar"</li> 397 * <li>foo;bar -> "foo;bar"</li> 398 * </ul> 399 */ 400 String quoteString(String s) 401 { 402 if (s == null) 403 { 404 return null; 405 } 406 407 if (s.indexOf(' ') != -1 408 || s.indexOf('\t') != -1 409 || s.indexOf('\r') != -1 410 || s.indexOf('\n') != -1 411 || s.indexOf('"') != -1 412 || s.indexOf('(') != -1 413 || s.indexOf(')') != -1 414 || s.indexOf('{') != -1 415 || s.indexOf('}') != -1 416 || s.indexOf('=') != -1 417 || s.indexOf(',') != -1 418 || s.indexOf(';') != -1) 419 { 420 s = s.replaceAll("\"", "\\\\\\\""); 421 s = "\"" + s + "\""; 422 } 423 424 return s; 425 } 426 427 /** 428 * Parses a date in a format like 429 * {@code <*D2002-03-22 11:30:00 +0100>}. 430 * 431 * @param s the string with the date to be parsed 432 * @return the parsed date 433 * @throws ParseException if an error occurred while parsing the string 434 */ 435 static Date parseDate(String s) throws ParseException 436 { 437 Calendar cal = Calendar.getInstance(); 438 cal.clear(); 439 int index = 0; 440 441 for (DateComponentParser parser : DATE_PARSERS) 442 { 443 index += parser.parseComponent(s, index, cal); 444 } 445 446 return cal.getTime(); 447 } 448 449 /** 450 * Returns a string representation for the date specified by the given 451 * calendar. 452 * 453 * @param cal the calendar with the initialized date 454 * @return a string for this date 455 */ 456 static String formatDate(Calendar cal) 457 { 458 StringBuilder buf = new StringBuilder(); 459 460 for (int i = 0; i < DATE_PARSERS.length; i++) 461 { 462 DATE_PARSERS[i].formatComponent(buf, cal); 463 } 464 465 return buf.toString(); 466 } 467 468 /** 469 * Returns a string representation for the specified date. 470 * 471 * @param date the date 472 * @return a string for this date 473 */ 474 static String formatDate(Date date) 475 { 476 Calendar cal = Calendar.getInstance(); 477 cal.setTime(date); 478 return formatDate(cal); 479 } 480 481 /** 482 * Transform a map of arbitrary types into a map with string keys and object 483 * values. All keys of the source map which are not of type String are 484 * dropped. 485 * 486 * @param src the map to be converted 487 * @return the resulting map 488 */ 489 private static Map<String, Object> transformMap(Map<?, ?> src) 490 { 491 Map<String, Object> dest = new HashMap<String, Object>(); 492 for (Map.Entry<?, ?> e : src.entrySet()) 493 { 494 if (e.getKey() instanceof String) 495 { 496 dest.put((String) e.getKey(), e.getValue()); 497 } 498 } 499 return dest; 500 } 501 502 /** 503 * A helper class for parsing and formatting date literals. Usually we would 504 * use {@code SimpleDateFormat} for this purpose, but in Java 1.3 the 505 * functionality of this class is limited. So we have a hierarchy of parser 506 * classes instead that deal with the different components of a date 507 * literal. 508 */ 509 private abstract static class DateComponentParser 510 { 511 /** 512 * Parses a component from the given input string. 513 * 514 * @param s the string to be parsed 515 * @param index the current parsing position 516 * @param cal the calendar where to store the result 517 * @return the length of the processed component 518 * @throws ParseException if the component cannot be extracted 519 */ 520 public abstract int parseComponent(String s, int index, Calendar cal) 521 throws ParseException; 522 523 /** 524 * Formats a date component. This method is used for converting a date 525 * in its internal representation into a string literal. 526 * 527 * @param buf the target buffer 528 * @param cal the calendar with the current date 529 */ 530 public abstract void formatComponent(StringBuilder buf, Calendar cal); 531 532 /** 533 * Checks whether the given string has at least {@code length} 534 * characters starting from the given parsing position. If this is not 535 * the case, an exception will be thrown. 536 * 537 * @param s the string to be tested 538 * @param index the current index 539 * @param length the minimum length after the index 540 * @throws ParseException if the string is too short 541 */ 542 protected void checkLength(String s, int index, int length) 543 throws ParseException 544 { 545 int len = (s == null) ? 0 : s.length(); 546 if (index + length > len) 547 { 548 throw new ParseException("Input string too short: " + s 549 + ", index: " + index); 550 } 551 } 552 553 /** 554 * Adds a number to the given string buffer and adds leading '0' 555 * characters until the given length is reached. 556 * 557 * @param buf the target buffer 558 * @param num the number to add 559 * @param length the required length 560 */ 561 protected void padNum(StringBuilder buf, int num, int length) 562 { 563 buf.append(StringUtils.leftPad(String.valueOf(num), length, 564 PAD_CHAR)); 565 } 566 } 567 568 /** 569 * A specialized date component parser implementation that deals with 570 * numeric calendar fields. The class is able to extract fields from a 571 * string literal and to format a literal from a calendar. 572 */ 573 private static class DateFieldParser extends DateComponentParser 574 { 575 /** Stores the calendar field to be processed. */ 576 private int calendarField; 577 578 /** Stores the length of this field. */ 579 private int length; 580 581 /** An optional offset to add to the calendar field. */ 582 private int offset; 583 584 /** 585 * Creates a new instance of {@code DateFieldParser}. 586 * 587 * @param calFld the calendar field code 588 * @param len the length of this field 589 */ 590 public DateFieldParser(int calFld, int len) 591 { 592 this(calFld, len, 0); 593 } 594 595 /** 596 * Creates a new instance of {@code DateFieldParser} and fully 597 * initializes it. 598 * 599 * @param calFld the calendar field code 600 * @param len the length of this field 601 * @param ofs an offset to add to the calendar field 602 */ 603 public DateFieldParser(int calFld, int len, int ofs) 604 { 605 calendarField = calFld; 606 length = len; 607 offset = ofs; 608 } 609 610 @Override 611 public void formatComponent(StringBuilder buf, Calendar cal) 612 { 613 padNum(buf, cal.get(calendarField) + offset, length); 614 } 615 616 @Override 617 public int parseComponent(String s, int index, Calendar cal) 618 throws ParseException 619 { 620 checkLength(s, index, length); 621 try 622 { 623 cal.set(calendarField, Integer.parseInt(s.substring(index, 624 index + length)) 625 - offset); 626 return length; 627 } 628 catch (NumberFormatException nfex) 629 { 630 throw new ParseException("Invalid number: " + s + ", index " 631 + index); 632 } 633 } 634 } 635 636 /** 637 * A specialized date component parser implementation that deals with 638 * separator characters. 639 */ 640 private static class DateSeparatorParser extends DateComponentParser 641 { 642 /** Stores the separator. */ 643 private String separator; 644 645 /** 646 * Creates a new instance of {@code DateSeparatorParser} and sets 647 * the separator string. 648 * 649 * @param sep the separator string 650 */ 651 public DateSeparatorParser(String sep) 652 { 653 separator = sep; 654 } 655 656 @Override 657 public void formatComponent(StringBuilder buf, Calendar cal) 658 { 659 buf.append(separator); 660 } 661 662 @Override 663 public int parseComponent(String s, int index, Calendar cal) 664 throws ParseException 665 { 666 checkLength(s, index, separator.length()); 667 if (!s.startsWith(separator, index)) 668 { 669 throw new ParseException("Invalid input: " + s + ", index " 670 + index + ", expected " + separator); 671 } 672 return separator.length(); 673 } 674 } 675 676 /** 677 * A specialized date component parser implementation that deals with the 678 * time zone part of a date component. 679 */ 680 private static class DateTimeZoneParser extends DateComponentParser 681 { 682 @Override 683 public void formatComponent(StringBuilder buf, Calendar cal) 684 { 685 TimeZone tz = cal.getTimeZone(); 686 int ofs = tz.getRawOffset() / MILLIS_PER_MINUTE; 687 if (ofs < 0) 688 { 689 buf.append('-'); 690 ofs = -ofs; 691 } 692 else 693 { 694 buf.append('+'); 695 } 696 int hour = ofs / MINUTES_PER_HOUR; 697 int min = ofs % MINUTES_PER_HOUR; 698 padNum(buf, hour, 2); 699 padNum(buf, min, 2); 700 } 701 702 @Override 703 public int parseComponent(String s, int index, Calendar cal) 704 throws ParseException 705 { 706 checkLength(s, index, TIME_ZONE_LENGTH); 707 TimeZone tz = TimeZone.getTimeZone(TIME_ZONE_PREFIX 708 + s.substring(index, index + TIME_ZONE_LENGTH)); 709 cal.setTimeZone(tz); 710 return TIME_ZONE_LENGTH; 711 } 712 } 713}