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