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 * https://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 ASCII plist files. It supports the GNUStep 047 * extension to specify date objects. 048 * <p> 049 * References: 050 * <ul> 051 * <li><a href= 052 * "https://developer.apple.com/documentation/Cocoa/Conceptual/PropertyLists/OldStylePlists/OldStylePLists.html"> Apple 053 * Documentation - Old-Style ASCII Property Lists</a></li> 054 * <li><a href="https://www.gnustep.org/resources/documentation/Developer/Base/Reference/NSPropertyList.html"> GNUStep 055 * Documentation</a></li> 056 * </ul> 057 * 058 * <p> 059 * Example: 060 * </p> 061 * 062 * <pre> 063 * { 064 * foo = "bar"; 065 * 066 * array = ( value1, value2, value3 ); 067 * 068 * data = <4f3e0145ab>; 069 * 070 * date = <*D2007-05-05 20:05:00 +0100>; 071 * 072 * nested = 073 * { 074 * key1 = value1; 075 * key2 = value; 076 * nested = 077 * { 078 * foo = bar 079 * } 080 * } 081 * } 082 * </pre> 083 * 084 * @since 1.2 085 */ 086public class PropertyListConfiguration extends BaseHierarchicalConfiguration implements FileBasedConfiguration { 087 088 /** 089 * A helper class for parsing and formatting date literals. Usually we would use {@code SimpleDateFormat} for this 090 * purpose, but in Java 1.3 the functionality of this class is limited. So we have a hierarchy of parser classes instead 091 * that deal with the different components of a date literal. 092 */ 093 private abstract static class DateComponentParser { 094 095 /** 096 * Checks whether the given string has at least {@code length} characters starting from the given parsing position. If 097 * this is not the case, an exception will be thrown. 098 * 099 * @param s the string to be tested 100 * @param index the current index 101 * @param length the minimum length after the index 102 * @throws ParseException if the string is too short 103 */ 104 protected void checkLength(final String s, final int index, final int length) throws ParseException { 105 final int len = s == null ? 0 : s.length(); 106 if (index + length > len) { 107 throw new ParseException("Input string too short: " + s + ", index: " + index); 108 } 109 } 110 111 /** 112 * Formats a date component. This method is used for converting a date in its internal representation into a string 113 * literal. 114 * 115 * @param buf the target buffer 116 * @param cal the calendar with the current date 117 */ 118 public abstract void formatComponent(StringBuilder buf, Calendar cal); 119 120 /** 121 * Adds a number to the given string buffer and adds leading '0' characters until the given length is reached. 122 * 123 * @param buf the target buffer 124 * @param num the number to add 125 * @param length the required length 126 */ 127 protected void padNum(final StringBuilder buf, final int num, final int length) { 128 buf.append(StringUtils.leftPad(String.valueOf(num), length, PAD_CHAR)); 129 } 130 131 /** 132 * Parses a component from the given input string. 133 * 134 * @param s the string to be parsed 135 * @param index the current parsing position 136 * @param cal the calendar where to store the result 137 * @return the length of the processed component 138 * @throws ParseException if the component cannot be extracted 139 */ 140 public abstract int parseComponent(String s, int index, Calendar cal) throws ParseException; 141 } 142 143 /** 144 * A specialized date component parser implementation that deals with numeric calendar fields. The class is able to 145 * extract fields from a string literal and to format a literal from a calendar. 146 */ 147 private static final class DateFieldParser extends DateComponentParser { 148 149 /** Stores the calendar field to be processed. */ 150 private final int calendarField; 151 152 /** Stores the length of this field. */ 153 private final int length; 154 155 /** An optional offset to add to the calendar field. */ 156 private final int offset; 157 158 /** 159 * Creates a new instance of {@code DateFieldParser}. 160 * 161 * @param calFld the calendar field code 162 * @param len the length of this field 163 */ 164 public DateFieldParser(final int calFld, final int len) { 165 this(calFld, len, 0); 166 } 167 168 /** 169 * Creates a new instance of {@code DateFieldParser} and fully initializes it. 170 * 171 * @param calFld the calendar field code 172 * @param len the length of this field 173 * @param ofs an offset to add to the calendar field 174 */ 175 public DateFieldParser(final int calFld, final int len, final int ofs) { 176 calendarField = calFld; 177 length = len; 178 offset = ofs; 179 } 180 181 @Override 182 public void formatComponent(final StringBuilder buf, final Calendar cal) { 183 padNum(buf, cal.get(calendarField) + offset, length); 184 } 185 186 @Override 187 public int parseComponent(final String s, final int index, final Calendar cal) throws ParseException { 188 checkLength(s, index, length); 189 try { 190 cal.set(calendarField, Integer.parseInt(s.substring(index, index + length)) - offset); 191 return length; 192 } catch (final NumberFormatException nfex) { 193 throw new ParseException("Invalid number: " + s + ", index " + index); 194 } 195 } 196 } 197 198 /** 199 * A specialized date component parser implementation that deals with separator characters. 200 */ 201 private static final class DateSeparatorParser extends DateComponentParser { 202 203 /** Stores the separator. */ 204 private final String separator; 205 206 /** 207 * Creates a new instance of {@code DateSeparatorParser} and sets the separator string. 208 * 209 * @param sep the separator string 210 */ 211 public DateSeparatorParser(final String sep) { 212 separator = sep; 213 } 214 215 @Override 216 public void formatComponent(final StringBuilder buf, final Calendar cal) { 217 buf.append(separator); 218 } 219 220 @Override 221 public int parseComponent(final String s, final int index, final Calendar cal) throws ParseException { 222 checkLength(s, index, separator.length()); 223 if (!s.startsWith(separator, index)) { 224 throw new ParseException("Invalid input: " + s + ", index " + index + ", expected " + separator); 225 } 226 return separator.length(); 227 } 228 } 229 230 /** 231 * A specialized date component parser implementation that deals with the time zone part of a date component. 232 */ 233 private static final class DateTimeZoneParser extends DateComponentParser { 234 @Override 235 public void formatComponent(final StringBuilder buf, final Calendar cal) { 236 final TimeZone tz = cal.getTimeZone(); 237 int ofs = tz.getRawOffset() / MILLIS_PER_MINUTE; 238 if (ofs < 0) { 239 buf.append('-'); 240 ofs = -ofs; 241 } else { 242 buf.append('+'); 243 } 244 final int hour = ofs / MINUTES_PER_HOUR; 245 final int min = ofs % MINUTES_PER_HOUR; 246 padNum(buf, hour, 2); 247 padNum(buf, min, 2); 248 } 249 250 @Override 251 public int parseComponent(final String s, final int index, final Calendar cal) throws ParseException { 252 checkLength(s, index, TIME_ZONE_LENGTH); 253 final TimeZone tz = TimeZone.getTimeZone(TIME_ZONE_PREFIX + s.substring(index, index + TIME_ZONE_LENGTH)); 254 cal.setTimeZone(tz); 255 return TIME_ZONE_LENGTH; 256 } 257 } 258 259 /** Constant for the separator parser for the date part. */ 260 private static final DateComponentParser DATE_SEPARATOR_PARSER = new DateSeparatorParser("-"); 261 262 /** Constant for the separator parser for the time part. */ 263 private static final DateComponentParser TIME_SEPARATOR_PARSER = new DateSeparatorParser(":"); 264 265 /** Constant for the separator parser for blanks between the parts. */ 266 private static final DateComponentParser BLANK_SEPARATOR_PARSER = new DateSeparatorParser(" "); 267 268 /** An array with the component parsers for dealing with dates. */ 269 private static final DateComponentParser[] DATE_PARSERS = {new DateSeparatorParser("<*D"), new DateFieldParser(Calendar.YEAR, 4), DATE_SEPARATOR_PARSER, 270 new DateFieldParser(Calendar.MONTH, 2, 1), DATE_SEPARATOR_PARSER, new DateFieldParser(Calendar.DATE, 2), BLANK_SEPARATOR_PARSER, 271 new DateFieldParser(Calendar.HOUR_OF_DAY, 2), TIME_SEPARATOR_PARSER, new DateFieldParser(Calendar.MINUTE, 2), TIME_SEPARATOR_PARSER, 272 new DateFieldParser(Calendar.SECOND, 2), BLANK_SEPARATOR_PARSER, new DateTimeZoneParser(), new DateSeparatorParser(">")}; 273 274 /** Constant for the ID prefix for GMT time zones. */ 275 private static final String TIME_ZONE_PREFIX = "GMT"; 276 277 /** Constant for the milliseconds of a minute. */ 278 private static final int MILLIS_PER_MINUTE = 1000 * 60; 279 280 /** Constant for the minutes per hour. */ 281 private static final int MINUTES_PER_HOUR = 60; 282 283 /** Size of the indentation for the generated file. */ 284 private static final int INDENT_SIZE = 4; 285 286 /** Constant for the length of a time zone. */ 287 private static final int TIME_ZONE_LENGTH = 5; 288 289 /** Constant for the padding character in the date format. */ 290 private static final char PAD_CHAR = '0'; 291 292 /** 293 * Returns a string representation for the date specified by the given calendar. 294 * 295 * @param cal the calendar with the initialized date 296 * @return a string for this date 297 */ 298 static String formatDate(final Calendar cal) { 299 final StringBuilder buf = new StringBuilder(); 300 301 for (final DateComponentParser element : DATE_PARSERS) { 302 element.formatComponent(buf, cal); 303 } 304 305 return buf.toString(); 306 } 307 308 /** 309 * Returns a string representation for the specified date. 310 * 311 * @param date the date 312 * @return a string for this date 313 */ 314 static String formatDate(final Date date) { 315 final Calendar cal = Calendar.getInstance(); 316 cal.setTime(date); 317 return formatDate(cal); 318 } 319 320 /** 321 * Parses a date in a format like {@code <*D2002-03-22 11:30:00 +0100>}. 322 * 323 * @param s the string with the date to be parsed 324 * @return the parsed date 325 * @throws ParseException if an error occurred while parsing the string 326 */ 327 static Date parseDate(final String s) throws ParseException { 328 final Calendar cal = Calendar.getInstance(); 329 cal.clear(); 330 int index = 0; 331 332 for (final DateComponentParser parser : DATE_PARSERS) { 333 index += parser.parseComponent(s, index, cal); 334 } 335 336 return cal.getTime(); 337 } 338 339 /** 340 * Transform a map of arbitrary types into a map with string keys and object values. All keys of the source map which 341 * are not of type String are dropped. 342 * 343 * @param src the map to be converted 344 * @return the resulting map 345 */ 346 private static Map<String, Object> transformMap(final Map<?, ?> src) { 347 final Map<String, Object> dest = new HashMap<>(); 348 src.forEach((k, v) -> { 349 if (k instanceof String) { 350 dest.put((String) k, v); 351 } 352 }); 353 return dest; 354 } 355 356 /** 357 * Creates an empty PropertyListConfiguration object which can be used to synthesize a new plist file by adding values 358 * and then saving(). 359 */ 360 public PropertyListConfiguration() { 361 } 362 363 /** 364 * Creates a new instance of {@code PropertyListConfiguration} and copies the content of the specified configuration 365 * into this object. 366 * 367 * @param c the configuration to copy 368 * @since 1.4 369 */ 370 public PropertyListConfiguration(final HierarchicalConfiguration<ImmutableNode> c) { 371 super(c); 372 } 373 374 /** 375 * Creates a new instance of {@code PropertyListConfiguration} with the given root node. 376 * 377 * @param root the root node 378 */ 379 PropertyListConfiguration(final ImmutableNode root) { 380 super(new InMemoryNodeModel(root)); 381 } 382 383 @Override 384 protected void addPropertyInternal(final String key, final Object value) { 385 if (value instanceof byte[]) { 386 addPropertyDirect(key, value); 387 } else { 388 super.addPropertyInternal(key, value); 389 } 390 } 391 392 /** 393 * Append a node to the writer, indented according to a specific level. 394 */ 395 private void printNode(final PrintWriter out, final int indentLevel, final ImmutableNode node, final NodeHandler<ImmutableNode> handler) { 396 final String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE); 397 398 if (node.getNodeName() != null) { 399 out.print(padding + quoteString(node.getNodeName()) + " = "); 400 } 401 402 final List<ImmutableNode> children = new ArrayList<>(node.getChildren()); 403 if (!children.isEmpty()) { 404 // skip a line, except for the root dictionary 405 if (indentLevel > 0) { 406 out.println(); 407 } 408 409 out.println(padding + "{"); 410 411 // display the children 412 final Iterator<ImmutableNode> it = children.iterator(); 413 while (it.hasNext()) { 414 final ImmutableNode child = it.next(); 415 416 printNode(out, indentLevel + 1, child, handler); 417 418 // add a semi colon for elements that are not dictionaries 419 final Object value = child.getValue(); 420 if (value != null && !(value instanceof Map) && !(value instanceof Configuration)) { 421 out.println(";"); 422 } 423 424 // skip a line after arrays and dictionaries 425 if (it.hasNext() && (value == null || value instanceof List)) { 426 out.println(); 427 } 428 } 429 430 out.print(padding + "}"); 431 432 // line feed if the dictionary is not in an array 433 if (handler.getParent(node) != null) { 434 out.println(); 435 } 436 } else if (node.getValue() == null) { 437 out.println(); 438 out.print(padding + "{ };"); 439 440 // line feed if the dictionary is not in an array 441 if (handler.getParent(node) != null) { 442 out.println(); 443 } 444 } else { 445 // display the leaf value 446 final Object value = node.getValue(); 447 printValue(out, indentLevel, value); 448 } 449 } 450 451 /** 452 * Append a value to the writer, indented according to a specific level. 453 */ 454 private void printValue(final PrintWriter out, final int indentLevel, final Object value) { 455 final String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE); 456 if (value instanceof List) { 457 out.print("( "); 458 final Iterator<?> it = ((List<?>) value).iterator(); 459 while (it.hasNext()) { 460 printValue(out, indentLevel + 1, it.next()); 461 if (it.hasNext()) { 462 out.print(", "); 463 } 464 } 465 out.print(" )"); 466 } else if (value instanceof PropertyListConfiguration) { 467 final NodeHandler<ImmutableNode> handler = ((PropertyListConfiguration) value).getModel().getNodeHandler(); 468 printNode(out, indentLevel, handler.getRootNode(), handler); 469 } else if (value instanceof ImmutableConfiguration) { 470 // display a flat Configuration as a dictionary 471 out.println(); 472 out.println(padding + "{"); 473 final ImmutableConfiguration config = (ImmutableConfiguration) value; 474 config.forEach((k, v) -> { 475 final ImmutableNode node = new ImmutableNode.Builder().name(k).value(v).create(); 476 final InMemoryNodeModel tempModel = new InMemoryNodeModel(node); 477 printNode(out, indentLevel + 1, node, tempModel.getNodeHandler()); 478 out.println(";"); 479 }); 480 out.println(padding + "}"); 481 } else if (value instanceof Map) { 482 // display a Map as a dictionary 483 final Map<String, Object> map = transformMap((Map<?, ?>) value); 484 printValue(out, indentLevel, new MapConfiguration(map)); 485 } else if (value instanceof byte[]) { 486 out.print("<" + new String(Hex.encodeHex((byte[]) value)) + ">"); 487 } else if (value instanceof Date) { 488 out.print(formatDate((Date) value)); 489 } else if (value != null) { 490 out.print(quoteString(String.valueOf(value))); 491 } 492 } 493 494 /** 495 * Quote the specified string if necessary, that's if the string contains: 496 * <ul> 497 * <li>a space character (' ', '\t', '\r', '\n')</li> 498 * <li>a quote '"'</li> 499 * <li>special characters in plist files ('(', ')', '{', '}', '=', ';', ',')</li> 500 * </ul> 501 * Quotes within the string are escaped. 502 * 503 * <p> 504 * Examples: 505 * </p> 506 * <ul> 507 * <li>abcd -> abcd</li> 508 * <li>ab cd -> "ab cd"</li> 509 * <li>foo"bar -> "foo\"bar"</li> 510 * <li>foo;bar -> "foo;bar"</li> 511 * </ul> 512 */ 513 String quoteString(String s) { 514 if (s == null) { 515 return null; 516 } 517 518 if (s.indexOf(' ') != -1 || s.indexOf('\t') != -1 || s.indexOf('\r') != -1 || s.indexOf('\n') != -1 || s.indexOf('"') != -1 || s.indexOf('(') != -1 519 || s.indexOf(')') != -1 || s.indexOf('{') != -1 || s.indexOf('}') != -1 || s.indexOf('=') != -1 || s.indexOf(',') != -1 || s.indexOf(';') != -1) { 520 s = s.replace("\"", "\\\""); 521 s = "\"" + s + "\""; 522 } 523 524 return s; 525 } 526 527 @Override 528 public void read(final Reader in) throws ConfigurationException { 529 final PropertyListParser parser = new PropertyListParser(in); 530 try { 531 final PropertyListConfiguration config = parser.parse(); 532 getModel().setRootNode(config.getNodeModel().getNodeHandler().getRootNode()); 533 } catch (final ParseException e) { 534 throw new ConfigurationException(e); 535 } 536 } 537 538 @Override 539 protected void setPropertyInternal(final String key, final Object value) { 540 // special case for byte arrays, they must be stored as is in the configuration 541 if (value instanceof byte[]) { 542 setDetailEvents(false); 543 try { 544 clearProperty(key); 545 addPropertyDirect(key, value); 546 } finally { 547 setDetailEvents(true); 548 } 549 } else { 550 super.setPropertyInternal(key, value); 551 } 552 } 553 554 @Override 555 public void write(final Writer out) throws ConfigurationException { 556 final PrintWriter writer = new PrintWriter(out); 557 final NodeHandler<ImmutableNode> handler = getModel().getNodeHandler(); 558 printNode(writer, 0, handler.getRootNode(), handler); 559 writer.flush(); 560 } 561}