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