1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 package org.apache.commons.configuration2; 18 19 import java.io.IOException; 20 import java.io.Reader; 21 import java.io.Writer; 22 import java.net.URL; 23 import java.util.ArrayDeque; 24 import java.util.LinkedHashMap; 25 import java.util.List; 26 import java.util.Map; 27 import java.util.Objects; 28 import java.util.Properties; 29 import java.util.Set; 30 import java.util.concurrent.atomic.AtomicInteger; 31 32 import org.apache.commons.configuration2.event.ConfigurationEvent; 33 import org.apache.commons.configuration2.event.EventListener; 34 import org.apache.commons.configuration2.ex.ConfigurationException; 35 import org.apache.commons.configuration2.ex.ConfigurationRuntimeException; 36 import org.apache.commons.lang3.StringUtils; 37 38 /** 39 * <p> 40 * A helper class used by {@link PropertiesConfiguration} to keep the layout of a properties file. 41 * </p> 42 * <p> 43 * Instances of this class are associated with a {@code PropertiesConfiguration} object. They are responsible for 44 * analyzing properties files and for extracting as much information about the file layout (e.g. empty lines, comments) 45 * as possible. When the properties file is written back again it should be close to the original. 46 * </p> 47 * <p> 48 * The {@code PropertiesConfigurationLayout} object associated with a {@code PropertiesConfiguration} object can be 49 * obtained using the {@code getLayout()} method of the configuration. Then the methods provided by this class can be 50 * used to alter the properties file's layout. 51 * </p> 52 * <p> 53 * Implementation note: This is a very simple implementation, which is far away from being perfect, i.e. the original 54 * layout of a properties file won't be reproduced in all cases. One limitation is that comments for multi-valued 55 * property keys are concatenated. Maybe this implementation can later be improved. 56 * </p> 57 * <p> 58 * To get an impression how this class works consider the following properties file: 59 * </p> 60 * 61 * <pre> 62 * # A demo configuration file 63 * # for Demo App 1.42 64 * 65 * # Application name 66 * AppName=Demo App 67 * 68 * # Application vendor 69 * AppVendor=DemoSoft 70 * 71 * 72 * # GUI properties 73 * # Window Color 74 * windowColors=0xFFFFFF,0x000000 75 * 76 * # Include some setting 77 * include=settings.properties 78 * # Another vendor 79 * AppVendor=TestSoft 80 * </pre> 81 * 82 * <p> 83 * For this example the following points are relevant: 84 * </p> 85 * <ul> 86 * <li>The first two lines are set as header comment. The header comment is determined by the last blank line before the 87 * first property definition.</li> 88 * <li>For the property {@code AppName} one comment line and one leading blank line is stored.</li> 89 * <li>For the property {@code windowColors} two comment lines and two leading blank lines are stored.</li> 90 * <li>Include files is something this class cannot deal with well. When saving the properties configuration back, the 91 * included properties are simply contained in the original file. The comment before the include property is 92 * skipped.</li> 93 * <li>For all properties except for {@code AppVendor} the "single line" flag is set. This is relevant only 94 * for {@code windowColors}, which has multiple values defined in one line using the separator character.</li> 95 * <li>The {@code AppVendor} property appears twice. The comment lines are concatenated, so that 96 * {@code layout.getComment("AppVendor");} will result in {@code Application vendor<CR>Another vendor}, with 97 * {@code <CR>} meaning the line separator. In addition the "single line" flag is set to <b>false</b> 98 * for this property. When the file is saved, two property definitions will be written (in series).</li> 99 * </ul> 100 * 101 * @since 1.3 102 */ 103 public class PropertiesConfigurationLayout implements EventListener<ConfigurationEvent> { 104 /** Constant for the line break character. */ 105 private static final String CR = "\n"; 106 107 /** Constant for the default comment prefix. */ 108 private static final String COMMENT_PREFIX = "# "; 109 110 /** Stores a map with the contained layout information. */ 111 private final Map<String, PropertyLayoutData> layoutData; 112 113 /** Stores the header comment. */ 114 private String headerComment; 115 116 /** Stores the footer comment. */ 117 private String footerComment; 118 119 /** The global separator that will be used for all properties. */ 120 private String globalSeparator; 121 122 /** The line separator. */ 123 private String lineSeparator; 124 125 /** A counter for determining nested load calls. */ 126 private final AtomicInteger loadCounter; 127 128 /** Stores the force single line flag. */ 129 private boolean forceSingleLine; 130 131 /** Seen includes. */ 132 private final ArrayDeque<URL> seenStack = new ArrayDeque<>(); 133 134 /** 135 * Creates a new, empty instance of {@code PropertiesConfigurationLayout}. 136 */ 137 public PropertiesConfigurationLayout() { 138 this(null); 139 } 140 141 /** 142 * Creates a new instance of {@code PropertiesConfigurationLayout} and copies the data of the specified layout object. 143 * 144 * @param c the layout object to be copied 145 */ 146 public PropertiesConfigurationLayout(final PropertiesConfigurationLayout c) { 147 loadCounter = new AtomicInteger(); 148 layoutData = new LinkedHashMap<>(); 149 150 if (c != null) { 151 copyFrom(c); 152 } 153 } 154 155 /** 156 * Gets the comment for the specified property key in a canonical form. "Canonical" means that either all 157 * lines start with a comment character or none. If the {@code commentChar} parameter is <b>false</b>, all comment 158 * characters are removed, so that the result is only the plain text of the comment. Otherwise it is ensured that each 159 * line of the comment starts with a comment character. Also, line breaks in the comment are normalized to the line 160 * separator "\n". 161 * 162 * @param key the key of the property 163 * @param commentChar determines whether all lines should start with comment characters or not 164 * @return the canonical comment for this key (can be <b>null</b>) 165 */ 166 public String getCanonicalComment(final String key, final boolean commentChar) { 167 return constructCanonicalComment(getComment(key), commentChar); 168 } 169 170 /** 171 * Gets the comment for the specified property key. The comment is returned as it was set (either manually by calling 172 * {@code setComment()} or when it was loaded from a properties file). No modifications are performed. 173 * 174 * @param key the key of the property 175 * @return the comment for this key (can be <b>null</b>) 176 */ 177 public String getComment(final String key) { 178 return fetchLayoutData(key).getComment(); 179 } 180 181 /** 182 * Sets the comment for the specified property key. The comment (or its single lines if it is a multi-line comment) can 183 * start with a comment character. If this is the case, it will be written without changes. Otherwise a default comment 184 * character is added automatically. 185 * 186 * @param key the key of the property 187 * @param comment the comment for this key (can be <b>null</b>, then the comment will be removed) 188 */ 189 public void setComment(final String key, final String comment) { 190 fetchLayoutData(key).setComment(comment); 191 } 192 193 /** 194 * Gets the number of blank lines before this property key. If this key does not exist, 0 will be returned. 195 * 196 * @param key the property key 197 * @return the number of blank lines before the property definition for this key 198 * @deprecated Use {@link #getBlankLinesBefore(String)}. 199 */ 200 @Deprecated 201 public int getBlancLinesBefore(final String key) { 202 return getBlankLinesBefore(key); 203 } 204 205 /** 206 * Gets the number of blank lines before this property key. If this key does not exist, 0 will be returned. 207 * 208 * @param key the property key 209 * @return the number of blank lines before the property definition for this key 210 */ 211 public int getBlankLinesBefore(final String key) { 212 return fetchLayoutData(key).getBlankLines(); 213 } 214 215 /** 216 * Sets the number of blank lines before the given property key. This can be used for a logical grouping of properties. 217 * 218 * @param key the property key 219 * @param number the number of blank lines to add before this property definition 220 * @deprecated use {@link PropertiesConfigurationLayout#setBlankLinesBefore(String, int)}. 221 */ 222 @Deprecated 223 public void setBlancLinesBefore(final String key, final int number) { 224 setBlankLinesBefore(key, number); 225 } 226 227 /** 228 * Sets the number of blank lines before the given property key. This can be used for a logical grouping of properties. 229 * 230 * @param key the property key 231 * @param number the number of blank lines to add before this property definition 232 * @since 2.8.0 233 */ 234 public void setBlankLinesBefore(final String key, final int number) { 235 fetchLayoutData(key).setBlankLines(number); 236 } 237 238 /** 239 * Gets the header comment of the represented properties file in a canonical form. With the {@code commentChar} 240 * parameter it can be specified whether comment characters should be stripped or be always present. 241 * 242 * @param commentChar determines the presence of comment characters 243 * @return the header comment (can be <b>null</b>) 244 */ 245 public String getCanonicalHeaderComment(final boolean commentChar) { 246 return constructCanonicalComment(getHeaderComment(), commentChar); 247 } 248 249 /** 250 * Gets the header comment of the represented properties file. This method returns the header comment exactly as it 251 * was set using {@code setHeaderComment()} or extracted from the loaded properties file. 252 * 253 * @return the header comment (can be <b>null</b>) 254 */ 255 public String getHeaderComment() { 256 return headerComment; 257 } 258 259 /** 260 * Sets the header comment for the represented properties file. This comment will be output on top of the file. 261 * 262 * @param comment the comment 263 */ 264 public void setHeaderComment(final String comment) { 265 headerComment = comment; 266 } 267 268 /** 269 * Gets the footer comment of the represented properties file in a canonical form. This method works like 270 * {@code getCanonicalHeaderComment()}, but reads the footer comment. 271 * 272 * @param commentChar determines the presence of comment characters 273 * @return the footer comment (can be <b>null</b>) 274 * @see #getCanonicalHeaderComment(boolean) 275 * @since 2.0 276 */ 277 public String getCanonicalFooterCooment(final boolean commentChar) { 278 return constructCanonicalComment(getFooterComment(), commentChar); 279 } 280 281 /** 282 * Gets the footer comment of the represented properties file. This method returns the footer comment exactly as it 283 * was set using {@code setFooterComment()} or extracted from the loaded properties file. 284 * 285 * @return the footer comment (can be <b>null</b>) 286 * @since 2.0 287 */ 288 public String getFooterComment() { 289 return footerComment; 290 } 291 292 /** 293 * Sets the footer comment for the represented properties file. This comment will be output at the bottom of the file. 294 * 295 * @param footerComment the footer comment 296 * @since 2.0 297 */ 298 public void setFooterComment(final String footerComment) { 299 this.footerComment = footerComment; 300 } 301 302 /** 303 * Returns a flag whether the specified property is defined on a single line. This is meaningful only if this property 304 * has multiple values. 305 * 306 * @param key the property key 307 * @return a flag if this property is defined on a single line 308 */ 309 public boolean isSingleLine(final String key) { 310 return fetchLayoutData(key).isSingleLine(); 311 } 312 313 /** 314 * Sets the "single line flag" for the specified property key. This flag is evaluated if the property has 315 * multiple values (i.e. if it is a list property). In this case, if the flag is set, all values will be written in a 316 * single property definition using the list delimiter as separator. Otherwise multiple lines will be written for this 317 * property, each line containing one property value. 318 * 319 * @param key the property key 320 * @param f the single line flag 321 */ 322 public void setSingleLine(final String key, final boolean f) { 323 fetchLayoutData(key).setSingleLine(f); 324 } 325 326 /** 327 * Returns the "force single line" flag. 328 * 329 * @return the force single line flag 330 * @see #setForceSingleLine(boolean) 331 */ 332 public boolean isForceSingleLine() { 333 return forceSingleLine; 334 } 335 336 /** 337 * Sets the "force single line" flag. If this flag is set, all properties with multiple values are written on 338 * single lines. This mode provides more compatibility with {@link Properties}, which cannot deal with 339 * multiple definitions of a single property. This mode has no effect if the list delimiter parsing is disabled. 340 * 341 * @param f the force single line flag 342 */ 343 public void setForceSingleLine(final boolean f) { 344 forceSingleLine = f; 345 } 346 347 /** 348 * Gets the separator for the property with the given key. 349 * 350 * @param key the property key 351 * @return the property separator for this property 352 * @since 1.7 353 */ 354 public String getSeparator(final String key) { 355 return fetchLayoutData(key).getSeparator(); 356 } 357 358 /** 359 * Sets the separator to be used for the property with the given key. The separator is the string between the property 360 * key and its value. For new properties " = " is used. When a properties file is read, the layout tries to 361 * determine the separator for each property. With this method the separator can be changed. To be compatible with the 362 * properties format only the characters {@code =} and {@code :} (with or without whitespace) should be used, but this 363 * method does not enforce this - it accepts arbitrary strings. If the key refers to a property with multiple values 364 * that are written on multiple lines, this separator will be used on all lines. 365 * 366 * @param key the key for the property 367 * @param sep the separator to be used for this property 368 * @since 1.7 369 */ 370 public void setSeparator(final String key, final String sep) { 371 fetchLayoutData(key).setSeparator(sep); 372 } 373 374 /** 375 * Gets the global separator. 376 * 377 * @return the global properties separator 378 * @since 1.7 379 */ 380 public String getGlobalSeparator() { 381 return globalSeparator; 382 } 383 384 /** 385 * Sets the global separator for properties. With this method a separator can be set that will be used for all 386 * properties when writing the configuration. This is an easy way of determining the properties separator globally. To 387 * be compatible with the properties format only the characters {@code =} and {@code :} (with or without whitespace) 388 * should be used, but this method does not enforce this - it accepts arbitrary strings. If the global separator is set 389 * to <b>null</b>, property separators are not changed. This is the default behavior as it produces results that are 390 * closer to the original properties file. 391 * 392 * @param globalSeparator the separator to be used for all properties 393 * @since 1.7 394 */ 395 public void setGlobalSeparator(final String globalSeparator) { 396 this.globalSeparator = globalSeparator; 397 } 398 399 /** 400 * Gets the line separator. 401 * 402 * @return the line separator 403 * @since 1.7 404 */ 405 public String getLineSeparator() { 406 return lineSeparator; 407 } 408 409 /** 410 * Sets the line separator. When writing the properties configuration, all lines are terminated with this separator. If 411 * no separator was set, the platform-specific default line separator is used. 412 * 413 * @param lineSeparator the line separator 414 * @since 1.7 415 */ 416 public void setLineSeparator(final String lineSeparator) { 417 this.lineSeparator = lineSeparator; 418 } 419 420 /** 421 * Gets a set with all property keys managed by this object. 422 * 423 * @return a set with all contained property keys 424 */ 425 public Set<String> getKeys() { 426 return layoutData.keySet(); 427 } 428 429 /** 430 * Reads a properties file and stores its internal structure. The found properties will be added to the specified 431 * configuration object. 432 * 433 * @param config the associated configuration object 434 * @param reader the reader to the properties file 435 * @throws ConfigurationException if an error occurs 436 */ 437 public void load(final PropertiesConfiguration config, final Reader reader) throws ConfigurationException { 438 loadCounter.incrementAndGet(); 439 @SuppressWarnings("resource") // createPropertiesReader wraps the reader. 440 final PropertiesConfiguration.PropertiesReader pReader = config.getIOFactory().createPropertiesReader(reader); 441 442 try { 443 while (pReader.nextProperty()) { 444 if (config.propertyLoaded(pReader.getPropertyName(), pReader.getPropertyValue(), seenStack)) { 445 final boolean contained = layoutData.containsKey(pReader.getPropertyName()); 446 int blankLines = 0; 447 int idx = checkHeaderComment(pReader.getCommentLines()); 448 while (idx < pReader.getCommentLines().size() && StringUtils.isEmpty(pReader.getCommentLines().get(idx))) { 449 idx++; 450 blankLines++; 451 } 452 final String comment = extractComment(pReader.getCommentLines(), idx, pReader.getCommentLines().size() - 1); 453 final PropertyLayoutData data = fetchLayoutData(pReader.getPropertyName()); 454 if (contained) { 455 data.addComment(comment); 456 data.setSingleLine(false); 457 } else { 458 data.setComment(comment); 459 data.setBlankLines(blankLines); 460 data.setSeparator(pReader.getPropertySeparator()); 461 } 462 } 463 } 464 465 setFooterComment(extractComment(pReader.getCommentLines(), 0, pReader.getCommentLines().size() - 1)); 466 } catch (final IOException ioex) { 467 throw new ConfigurationException(ioex); 468 } finally { 469 loadCounter.decrementAndGet(); 470 } 471 } 472 473 /** 474 * Writes the properties file to the given writer, preserving as much of its structure as possible. 475 * 476 * @param config the associated configuration object 477 * @param writer the writer 478 * @throws ConfigurationException if an error occurs 479 */ 480 public void save(final PropertiesConfiguration config, final Writer writer) throws ConfigurationException { 481 try { 482 @SuppressWarnings("resource") // createPropertiesReader wraps the writer. 483 final PropertiesConfiguration.PropertiesWriter pWriter = config.getIOFactory().createPropertiesWriter(writer, config.getListDelimiterHandler()); 484 pWriter.setGlobalSeparator(getGlobalSeparator()); 485 if (getLineSeparator() != null) { 486 pWriter.setLineSeparator(getLineSeparator()); 487 } 488 489 if (headerComment != null) { 490 writeComment(pWriter, getCanonicalHeaderComment(true)); 491 } 492 493 boolean firstKey = true; 494 for (final String key : getKeys()) { 495 if (config.containsKeyInternal(key)) { 496 // preset header comment needs to be separated from key 497 if (firstKey && headerComment != null && getBlankLinesBefore(key) == 0) { 498 pWriter.writeln(null); 499 } 500 501 // Output blank lines before property 502 for (int i = 0; i < getBlankLinesBefore(key); i++) { 503 pWriter.writeln(null); 504 } 505 506 // Output the comment 507 writeComment(pWriter, getCanonicalComment(key, true)); 508 509 // Output the property and its value 510 final boolean singleLine = isForceSingleLine() || isSingleLine(key); 511 pWriter.setCurrentSeparator(getSeparator(key)); 512 pWriter.writeProperty(key, config.getPropertyInternal(key), singleLine); 513 } 514 firstKey = false; 515 } 516 517 writeComment(pWriter, getCanonicalFooterCooment(true)); 518 pWriter.flush(); 519 } catch (final IOException ioex) { 520 throw new ConfigurationException(ioex); 521 } 522 } 523 524 /** 525 * The event listener callback. Here event notifications of the configuration object are processed to update the layout 526 * object properly. 527 * 528 * @param event the event object 529 */ 530 @Override 531 public void onEvent(final ConfigurationEvent event) { 532 if (!event.isBeforeUpdate() && loadCounter.get() == 0) { 533 if (ConfigurationEvent.ADD_PROPERTY.equals(event.getEventType())) { 534 final boolean contained = layoutData.containsKey(event.getPropertyName()); 535 final PropertyLayoutData data = fetchLayoutData(event.getPropertyName()); 536 data.setSingleLine(!contained); 537 } else if (ConfigurationEvent.CLEAR_PROPERTY.equals(event.getEventType())) { 538 layoutData.remove(event.getPropertyName()); 539 } else if (ConfigurationEvent.CLEAR.equals(event.getEventType())) { 540 clear(); 541 } else if (ConfigurationEvent.SET_PROPERTY.equals(event.getEventType())) { 542 fetchLayoutData(event.getPropertyName()); 543 } 544 } 545 } 546 547 /** 548 * Returns a layout data object for the specified key. If this is a new key, a new object is created and initialized 549 * with default values. 550 * 551 * @param key the key 552 * @return the corresponding layout data object 553 */ 554 private PropertyLayoutData fetchLayoutData(final String key) { 555 if (key == null) { 556 throw new IllegalArgumentException("Property key must not be null!"); 557 } 558 559 // PropertyLayoutData defaults to singleLine = true 560 return layoutData.computeIfAbsent(key, k -> new PropertyLayoutData()); 561 } 562 563 /** 564 * Removes all content from this layout object. 565 */ 566 private void clear() { 567 seenStack.clear(); 568 layoutData.clear(); 569 setHeaderComment(null); 570 setFooterComment(null); 571 } 572 573 /** 574 * Tests whether a line is a comment, i.e. whether it starts with a comment character. 575 * 576 * @param line the line 577 * @return a flag if this is a comment line 578 */ 579 static boolean isCommentLine(final String line) { 580 return PropertiesConfiguration.isCommentLine(line); 581 } 582 583 /** 584 * Trims a comment. This method either removes all comment characters from the given string, leaving only the plain 585 * comment text or ensures that every line starts with a valid comment character. 586 * 587 * @param s the string to be processed 588 * @param comment if <b>true</b>, a comment character will always be enforced; if <b>false</b>, it will be removed 589 * @return the trimmed comment 590 */ 591 static String trimComment(final String s, final boolean comment) { 592 final StringBuilder buf = new StringBuilder(s.length()); 593 int lastPos = 0; 594 int pos; 595 596 do { 597 pos = s.indexOf(CR, lastPos); 598 if (pos >= 0) { 599 final String line = s.substring(lastPos, pos); 600 buf.append(stripCommentChar(line, comment)).append(CR); 601 lastPos = pos + CR.length(); 602 } 603 } while (pos >= 0); 604 605 if (lastPos < s.length()) { 606 buf.append(stripCommentChar(s.substring(lastPos), comment)); 607 } 608 return buf.toString(); 609 } 610 611 /** 612 * Either removes the comment character from the given comment line or ensures that the line starts with a comment 613 * character. 614 * 615 * @param s the comment line 616 * @param comment if <b>true</b>, a comment character will always be enforced; if <b>false</b>, it will be removed 617 * @return the line without comment character 618 */ 619 static String stripCommentChar(final String s, final boolean comment) { 620 if (StringUtils.isBlank(s) || isCommentLine(s) == comment) { 621 return s; 622 } 623 if (!comment) { 624 int pos = 0; 625 // find first comment character 626 while (PropertiesConfiguration.COMMENT_CHARS.indexOf(s.charAt(pos)) < 0) { 627 pos++; 628 } 629 630 // Remove leading spaces 631 pos++; 632 while (pos < s.length() && Character.isWhitespace(s.charAt(pos))) { 633 pos++; 634 } 635 636 return pos < s.length() ? s.substring(pos) : StringUtils.EMPTY; 637 } 638 return COMMENT_PREFIX + s; 639 } 640 641 /** 642 * Extracts a comment string from the given range of the specified comment lines. The single lines are added using a 643 * line feed as separator. 644 * 645 * @param commentLines a list with comment lines 646 * @param from the start index 647 * @param to the end index (inclusive) 648 * @return the comment string (<b>null</b> if it is undefined) 649 */ 650 private String extractComment(final List<String> commentLines, final int from, final int to) { 651 if (to < from) { 652 return null; 653 } 654 final StringBuilder buf = new StringBuilder(commentLines.get(from)); 655 for (int i = from + 1; i <= to; i++) { 656 buf.append(CR); 657 buf.append(commentLines.get(i)); 658 } 659 return buf.toString(); 660 } 661 662 /** 663 * Checks if parts of the passed in comment can be used as header comment. This method checks whether a header comment 664 * can be defined (i.e. whether this is the first comment in the loaded file). If this is the case, it is searched for 665 * the latest blank line. This line will mark the end of the header comment. The return value is the index of the first 666 * line in the passed in list, which does not belong to the header comment. 667 * 668 * @param commentLines the comment lines 669 * @return the index of the next line after the header comment 670 */ 671 private int checkHeaderComment(final List<String> commentLines) { 672 if (loadCounter.get() == 1 && layoutData.isEmpty()) { 673 int index = commentLines.size() - 1; 674 // strip comments that belong to first key 675 while (index >= 0 && StringUtils.isNotEmpty(commentLines.get(index))) { 676 index--; 677 } 678 // strip blank lines 679 while (index >= 0 && StringUtils.isEmpty(commentLines.get(index))) { 680 index--; 681 } 682 if (getHeaderComment() == null) { 683 setHeaderComment(extractComment(commentLines, 0, index)); 684 } 685 return index + 1; 686 } 687 return 0; 688 } 689 690 /** 691 * Copies the data from the given layout object. 692 * 693 * @param c the layout object to copy 694 */ 695 private void copyFrom(final PropertiesConfigurationLayout c) { 696 c.getKeys().forEach(key -> layoutData.put(key, c.layoutData.get(key).clone())); 697 698 setHeaderComment(c.getHeaderComment()); 699 setFooterComment(c.getFooterComment()); 700 } 701 702 /** 703 * Helper method for writing a comment line. This method ensures that the correct line separator is used if the comment 704 * spans multiple lines. 705 * 706 * @param writer the writer 707 * @param comment the comment to write 708 * @throws IOException if an IO error occurs 709 */ 710 private static void writeComment(final PropertiesConfiguration.PropertiesWriter writer, final String comment) throws IOException { 711 if (comment != null) { 712 writer.writeln(StringUtils.replace(comment, CR, writer.getLineSeparator())); 713 } 714 } 715 716 /** 717 * Helper method for generating a comment string. Depending on the boolean argument the resulting string either has no 718 * comment characters or a leading comment character at each line. 719 * 720 * @param comment the comment string to be processed 721 * @param commentChar determines the presence of comment characters 722 * @return the canonical comment string (can be <b>null</b>) 723 */ 724 private static String constructCanonicalComment(final String comment, final boolean commentChar) { 725 return comment == null ? null : trimComment(comment, commentChar); 726 } 727 728 /** 729 * A helper class for storing all layout related information for a configuration property. 730 */ 731 static class PropertyLayoutData implements Cloneable { 732 /** Stores the comment for the property. */ 733 private StringBuffer comment; 734 735 /** The separator to be used for this property. */ 736 private String separator; 737 738 /** Stores the number of blank lines before this property. */ 739 private int blankLines; 740 741 /** Stores the single line property. */ 742 private boolean singleLine; 743 744 /** 745 * Creates a new instance of {@code PropertyLayoutData}. 746 */ 747 public PropertyLayoutData() { 748 singleLine = true; 749 separator = PropertiesConfiguration.DEFAULT_SEPARATOR; 750 } 751 752 /** 753 * Gets the number of blank lines before this property. 754 * 755 * @return the number of blank lines before this property 756 * @deprecated Use {#link {@link #getBlankLines()}}. 757 */ 758 @Deprecated 759 public int getBlancLines() { 760 return getBlankLines(); 761 } 762 763 /** 764 * Gets the number of blank lines before this property. 765 * 766 * @return the number of blank lines before this property 767 * @since 2.8.0 768 */ 769 public int getBlankLines() { 770 return blankLines; 771 } 772 773 /** 774 * Sets the number of properties before this property. 775 * 776 * @param blankLines the number of properties before this property 777 * @deprecated Use {@link #setBlankLines(int)}. 778 */ 779 @Deprecated 780 public void setBlancLines(final int blankLines) { 781 setBlankLines(blankLines); 782 } 783 784 /** 785 * Sets the number of properties before this property. 786 * 787 * @param blankLines the number of properties before this property 788 * @since 2.8.0 789 */ 790 public void setBlankLines(final int blankLines) { 791 this.blankLines = blankLines; 792 } 793 794 /** 795 * Returns the single line flag. 796 * 797 * @return the single line flag 798 */ 799 public boolean isSingleLine() { 800 return singleLine; 801 } 802 803 /** 804 * Sets the single line flag. 805 * 806 * @param singleLine the single line flag 807 */ 808 public void setSingleLine(final boolean singleLine) { 809 this.singleLine = singleLine; 810 } 811 812 /** 813 * Adds a comment for this property. If already a comment exists, the new comment is added (separated by a newline). 814 * 815 * @param s the comment to add 816 */ 817 public void addComment(final String s) { 818 if (s != null) { 819 if (comment == null) { 820 comment = new StringBuffer(s); 821 } else { 822 comment.append(CR).append(s); 823 } 824 } 825 } 826 827 /** 828 * Sets the comment for this property. 829 * 830 * @param s the new comment (can be <b>null</b>) 831 */ 832 public void setComment(final String s) { 833 if (s == null) { 834 comment = null; 835 } else { 836 comment = new StringBuffer(s); 837 } 838 } 839 840 /** 841 * Gets the comment for this property. The comment is returned as it is, without processing of comment characters. 842 * 843 * @return the comment (can be <b>null</b>) 844 */ 845 public String getComment() { 846 return Objects.toString(comment, null); 847 } 848 849 /** 850 * Gets the separator that was used for this property. 851 * 852 * @return the property separator 853 */ 854 public String getSeparator() { 855 return separator; 856 } 857 858 /** 859 * Sets the separator to be used for the represented property. 860 * 861 * @param separator the property separator 862 */ 863 public void setSeparator(final String separator) { 864 this.separator = separator; 865 } 866 867 /** 868 * Creates a copy of this object. 869 * 870 * @return the copy 871 */ 872 @Override 873 public PropertyLayoutData clone() { 874 try { 875 final PropertyLayoutData copy = (PropertyLayoutData) super.clone(); 876 if (comment != null) { 877 // must copy string buffer, too 878 copy.comment = new StringBuffer(getComment()); 879 } 880 return copy; 881 } catch (final CloneNotSupportedException cnex) { 882 // This cannot happen! 883 throw new ConfigurationRuntimeException(cnex); 884 } 885 } 886 } 887 }