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