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