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