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 */ 017package org.apache.commons.configuration2; 018 019import java.io.IOException; 020import java.io.Reader; 021import java.io.Writer; 022import java.net.URL; 023import java.util.ArrayDeque; 024import java.util.LinkedHashMap; 025import java.util.List; 026import java.util.Map; 027import java.util.Objects; 028import java.util.Set; 029import java.util.concurrent.atomic.AtomicInteger; 030 031import org.apache.commons.configuration2.event.ConfigurationEvent; 032import org.apache.commons.configuration2.event.EventListener; 033import org.apache.commons.configuration2.ex.ConfigurationException; 034import org.apache.commons.configuration2.ex.ConfigurationRuntimeException; 035import org.apache.commons.lang3.StringUtils; 036 037/** 038 * <p> 039 * A helper class used by {@link PropertiesConfiguration} to keep 040 * the layout of a properties file. 041 * </p> 042 * <p> 043 * Instances of this class are associated with a 044 * {@code PropertiesConfiguration} object. They are responsible for 045 * analyzing properties files and for extracting as much information about the 046 * file layout (e.g. empty lines, comments) as possible. When the properties 047 * file is written back again it should be close to the original. 048 * </p> 049 * <p> 050 * The {@code PropertiesConfigurationLayout} object associated with a 051 * {@code PropertiesConfiguration} object can be obtained using the 052 * {@code getLayout()} method of the configuration. Then the methods 053 * provided by this class can be used to alter the properties file's layout. 054 * </p> 055 * <p> 056 * Implementation note: This is a very simple implementation, which is far away 057 * from being perfect, i.e. the original layout of a properties file won't be 058 * reproduced in all cases. One limitation is that comments for multi-valued 059 * property keys are concatenated. Maybe this implementation can later be 060 * improved. 061 * </p> 062 * <p> 063 * To get an impression how this class works consider the following properties 064 * file: 065 * </p> 066 * 067 * <pre> 068 * # A demo configuration file 069 * # for Demo App 1.42 070 * 071 * # Application name 072 * AppName=Demo App 073 * 074 * # Application vendor 075 * AppVendor=DemoSoft 076 * 077 * 078 * # GUI properties 079 * # Window Color 080 * windowColors=0xFFFFFF,0x000000 081 * 082 * # Include some setting 083 * include=settings.properties 084 * # Another vendor 085 * AppVendor=TestSoft 086 * </pre> 087 * 088 * <p> 089 * For this example the following points are relevant: 090 * </p> 091 * <ul> 092 * <li>The first two lines are set as header comment. The header comment is 093 * determined by the last blanc line before the first property definition.</li> 094 * <li>For the property {@code AppName} one comment line and one 095 * leading blanc line is stored.</li> 096 * <li>For the property {@code windowColors} two comment lines and two 097 * leading blanc lines are stored.</li> 098 * <li>Include files is something this class cannot deal with well. When saving 099 * the properties configuration back, the included properties are simply 100 * contained in the original file. The comment before the include property is 101 * skipped.</li> 102 * <li>For all properties except for {@code AppVendor} the "single 103 * line" flag is set. This is relevant only for {@code windowColors}, 104 * which has multiple values defined in one line using the separator character.</li> 105 * <li>The {@code AppVendor} property appears twice. The comment lines 106 * are concatenated, so that {@code layout.getComment("AppVendor");} will 107 * result in {@code Application vendor<CR>Another vendor}, with 108 * {@code <CR>} meaning the line separator. In addition the 109 * "single line" flag is set to <b>false</b> for this property. When 110 * the file is saved, two property definitions will be written (in series).</li> 111 * </ul> 112 * 113 * @since 1.3 114 */ 115public class PropertiesConfigurationLayout implements EventListener<ConfigurationEvent> 116{ 117 /** Constant for the line break character. */ 118 private static final String CR = "\n"; 119 120 /** Constant for the default comment prefix. */ 121 private static final String COMMENT_PREFIX = "# "; 122 123 /** Stores a map with the contained layout information. */ 124 private final Map<String, PropertyLayoutData> layoutData; 125 126 /** Stores the header comment. */ 127 private String headerComment; 128 129 /** Stores the footer comment. */ 130 private String footerComment; 131 132 /** The global separator that will be used for all properties. */ 133 private String globalSeparator; 134 135 /** The line separator.*/ 136 private String lineSeparator; 137 138 /** A counter for determining nested load calls. */ 139 private final AtomicInteger loadCounter; 140 141 /** Stores the force single line flag. */ 142 private boolean forceSingleLine; 143 144 /** Seen includes. */ 145 private final ArrayDeque<URL> seenStack = new ArrayDeque<>(); 146 147 /** 148 * Creates a new, empty instance of {@code PropertiesConfigurationLayout}. 149 */ 150 public PropertiesConfigurationLayout() 151 { 152 this(null); 153 } 154 155 /** 156 * Creates a new instance of {@code PropertiesConfigurationLayout} and 157 * copies the data of the specified layout object. 158 * 159 * @param c the layout object to be copied 160 */ 161 public PropertiesConfigurationLayout(final PropertiesConfigurationLayout c) 162 { 163 loadCounter = new AtomicInteger(); 164 layoutData = new LinkedHashMap<>(); 165 166 if (c != null) 167 { 168 copyFrom(c); 169 } 170 } 171 172 /** 173 * Returns the comment for the specified property key in a canonical form. 174 * "Canonical" means that either all lines start with a comment 175 * character or none. If the {@code commentChar} parameter is <b>false</b>, 176 * all comment characters are removed, so that the result is only the plain 177 * text of the comment. Otherwise it is ensured that each line of the 178 * comment starts with a comment character. Also, line breaks in the comment 179 * are normalized to the line separator "\n". 180 * 181 * @param key the key of the property 182 * @param commentChar determines whether all lines should start with comment 183 * characters or not 184 * @return the canonical comment for this key (can be <b>null</b>) 185 */ 186 public String getCanonicalComment(final String key, final boolean commentChar) 187 { 188 return constructCanonicalComment(getComment(key), commentChar); 189 } 190 191 /** 192 * Returns the comment for the specified property key. The comment is 193 * returned as it was set (either manually by calling 194 * {@code setComment()} or when it was loaded from a properties 195 * file). No modifications are performed. 196 * 197 * @param key the key of the property 198 * @return the comment for this key (can be <b>null</b>) 199 */ 200 public String getComment(final String key) 201 { 202 return fetchLayoutData(key).getComment(); 203 } 204 205 /** 206 * Sets the comment for the specified property key. The comment (or its 207 * single lines if it is a multi-line comment) can start with a comment 208 * character. If this is the case, it will be written without changes. 209 * Otherwise a default comment character is added automatically. 210 * 211 * @param key the key of the property 212 * @param comment the comment for this key (can be <b>null</b>, then the 213 * comment will be removed) 214 */ 215 public void setComment(final String key, final String comment) 216 { 217 fetchLayoutData(key).setComment(comment); 218 } 219 220 /** 221 * Returns the number of blanc lines before this property key. If this key 222 * does not exist, 0 will be returned. 223 * 224 * @param key the property key 225 * @return the number of blanc lines before the property definition for this 226 * key 227 */ 228 public int getBlancLinesBefore(final String key) 229 { 230 return fetchLayoutData(key).getBlancLines(); 231 } 232 233 /** 234 * Sets the number of blanc lines before the given property key. This can be 235 * used for a logical grouping of properties. 236 * 237 * @param key the property key 238 * @param number the number of blanc lines to add before this property 239 * definition 240 */ 241 public void setBlancLinesBefore(final String key, final int number) 242 { 243 fetchLayoutData(key).setBlancLines(number); 244 } 245 246 /** 247 * Returns the header comment of the represented properties file in a 248 * canonical form. With the {@code commentChar} parameter it can be 249 * specified whether comment characters should be stripped or be always 250 * present. 251 * 252 * @param commentChar determines the presence of comment characters 253 * @return the header comment (can be <b>null</b>) 254 */ 255 public String getCanonicalHeaderComment(final boolean commentChar) 256 { 257 return constructCanonicalComment(getHeaderComment(), commentChar); 258 } 259 260 /** 261 * Returns the header comment of the represented properties file. This 262 * method returns the header comment exactly as it was set using 263 * {@code setHeaderComment()} or extracted from the loaded properties 264 * file. 265 * 266 * @return the header comment (can be <b>null</b>) 267 */ 268 public String getHeaderComment() 269 { 270 return headerComment; 271 } 272 273 /** 274 * Sets the header comment for the represented properties file. This comment 275 * will be output on top of the file. 276 * 277 * @param comment the comment 278 */ 279 public void setHeaderComment(final String comment) 280 { 281 headerComment = comment; 282 } 283 284 /** 285 * Returns the footer comment of the represented properties file in a 286 * canonical form. This method works like 287 * {@code getCanonicalHeaderComment()}, but reads the footer comment. 288 * 289 * @param commentChar determines the presence of comment characters 290 * @return the footer comment (can be <b>null</b>) 291 * @see #getCanonicalHeaderComment(boolean) 292 * @since 2.0 293 */ 294 public String getCanonicalFooterCooment(final boolean commentChar) 295 { 296 return constructCanonicalComment(getFooterComment(), commentChar); 297 } 298 299 /** 300 * Returns the footer comment of the represented properties file. This 301 * method returns the footer comment exactly as it was set using 302 * {@code setFooterComment()} or extracted from the loaded properties 303 * file. 304 * 305 * @return the footer comment (can be <b>null</b>) 306 * @since 2.0 307 */ 308 public String getFooterComment() 309 { 310 return footerComment; 311 } 312 313 /** 314 * Sets the footer comment for the represented properties file. This comment 315 * will be output at the bottom of the file. 316 * 317 * @param footerComment the footer comment 318 * @since 2.0 319 */ 320 public void setFooterComment(final String footerComment) 321 { 322 this.footerComment = footerComment; 323 } 324 325 /** 326 * Returns a flag whether the specified property is defined on a single 327 * line. This is meaningful only if this property has multiple values. 328 * 329 * @param key the property key 330 * @return a flag if this property is defined on a single line 331 */ 332 public boolean isSingleLine(final String key) 333 { 334 return fetchLayoutData(key).isSingleLine(); 335 } 336 337 /** 338 * Sets the "single line flag" for the specified property key. 339 * This flag is evaluated if the property has multiple values (i.e. if it is 340 * a list property). In this case, if the flag is set, all values will be 341 * written in a single property definition using the list delimiter as 342 * separator. Otherwise multiple lines will be written for this property, 343 * each line containing one property value. 344 * 345 * @param key the property key 346 * @param f the single line flag 347 */ 348 public void setSingleLine(final String key, final boolean f) 349 { 350 fetchLayoutData(key).setSingleLine(f); 351 } 352 353 /** 354 * Returns the "force single line" flag. 355 * 356 * @return the force single line flag 357 * @see #setForceSingleLine(boolean) 358 */ 359 public boolean isForceSingleLine() 360 { 361 return forceSingleLine; 362 } 363 364 /** 365 * Sets the "force single line" flag. If this flag is set, all 366 * properties with multiple values are written on single lines. This mode 367 * provides more compatibility with {@code java.lang.Properties}, 368 * which cannot deal with multiple definitions of a single property. This 369 * mode has no effect if the list delimiter parsing is disabled. 370 * 371 * @param f the force single line flag 372 */ 373 public void setForceSingleLine(final boolean f) 374 { 375 forceSingleLine = f; 376 } 377 378 /** 379 * Returns the separator for the property with the given key. 380 * 381 * @param key the property key 382 * @return the property separator for this property 383 * @since 1.7 384 */ 385 public String getSeparator(final String key) 386 { 387 return fetchLayoutData(key).getSeparator(); 388 } 389 390 /** 391 * Sets the separator to be used for the property with the given key. The 392 * separator is the string between the property key and its value. For new 393 * properties " = " is used. When a properties file is read, the 394 * layout tries to determine the separator for each property. With this 395 * method the separator can be changed. To be compatible with the properties 396 * format only the characters {@code =} and {@code :} (with or 397 * without whitespace) should be used, but this method does not enforce this 398 * - it accepts arbitrary strings. If the key refers to a property with 399 * multiple values that are written on multiple lines, this separator will 400 * be used on all lines. 401 * 402 * @param key the key for the property 403 * @param sep the separator to be used for this property 404 * @since 1.7 405 */ 406 public void setSeparator(final String key, final String sep) 407 { 408 fetchLayoutData(key).setSeparator(sep); 409 } 410 411 /** 412 * Returns the global separator. 413 * 414 * @return the global properties separator 415 * @since 1.7 416 */ 417 public String getGlobalSeparator() 418 { 419 return globalSeparator; 420 } 421 422 /** 423 * Sets the global separator for properties. With this method a separator 424 * can be set that will be used for all properties when writing the 425 * configuration. This is an easy way of determining the properties 426 * separator globally. To be compatible with the properties format only the 427 * characters {@code =} and {@code :} (with or without whitespace) 428 * should be used, but this method does not enforce this - it accepts 429 * arbitrary strings. If the global separator is set to <b>null</b>, 430 * property separators are not changed. This is the default behavior as it 431 * produces results that are closer to the original properties file. 432 * 433 * @param globalSeparator the separator to be used for all properties 434 * @since 1.7 435 */ 436 public void setGlobalSeparator(final String globalSeparator) 437 { 438 this.globalSeparator = globalSeparator; 439 } 440 441 /** 442 * Returns the line separator. 443 * 444 * @return the line separator 445 * @since 1.7 446 */ 447 public String getLineSeparator() 448 { 449 return lineSeparator; 450 } 451 452 /** 453 * Sets the line separator. When writing the properties configuration, all 454 * lines are terminated with this separator. If no separator was set, the 455 * platform-specific default line separator is used. 456 * 457 * @param lineSeparator the line separator 458 * @since 1.7 459 */ 460 public void setLineSeparator(final String lineSeparator) 461 { 462 this.lineSeparator = lineSeparator; 463 } 464 465 /** 466 * Returns a set with all property keys managed by this object. 467 * 468 * @return a set with all contained property keys 469 */ 470 public Set<String> getKeys() 471 { 472 return layoutData.keySet(); 473 } 474 475 /** 476 * Reads a properties file and stores its internal structure. The found 477 * properties will be added to the specified configuration object. 478 * 479 * @param config the associated configuration object 480 * @param in the reader to the properties file 481 * @throws ConfigurationException if an error occurs 482 */ 483 public void load(final PropertiesConfiguration config, final Reader in) 484 throws ConfigurationException 485 { 486 loadCounter.incrementAndGet(); 487 final PropertiesConfiguration.PropertiesReader reader = 488 config.getIOFactory().createPropertiesReader(in); 489 490 try 491 { 492 while (reader.nextProperty()) 493 { 494 if (config.propertyLoaded(reader.getPropertyName(), 495 reader.getPropertyValue(), seenStack)) 496 { 497 final boolean contained = layoutData.containsKey(reader 498 .getPropertyName()); 499 int blancLines = 0; 500 int idx = checkHeaderComment(reader.getCommentLines()); 501 while (idx < reader.getCommentLines().size() 502 && reader.getCommentLines().get(idx).length() < 1) 503 { 504 idx++; 505 blancLines++; 506 } 507 final String comment = extractComment(reader.getCommentLines(), 508 idx, reader.getCommentLines().size() - 1); 509 final PropertyLayoutData data = fetchLayoutData(reader 510 .getPropertyName()); 511 if (contained) 512 { 513 data.addComment(comment); 514 data.setSingleLine(false); 515 } 516 else 517 { 518 data.setComment(comment); 519 data.setBlancLines(blancLines); 520 data.setSeparator(reader.getPropertySeparator()); 521 } 522 } 523 } 524 525 setFooterComment(extractComment(reader.getCommentLines(), 0, reader 526 .getCommentLines().size() - 1)); 527 } 528 catch (final IOException ioex) 529 { 530 throw new ConfigurationException(ioex); 531 } 532 finally 533 { 534 loadCounter.decrementAndGet(); 535 } 536 } 537 538 /** 539 * Writes the properties file to the given writer, preserving as much of its 540 * structure as possible. 541 * 542 * @param config the associated configuration object 543 * @param out the writer 544 * @throws ConfigurationException if an error occurs 545 */ 546 public void save(final PropertiesConfiguration config, final Writer out) throws ConfigurationException 547 { 548 try 549 { 550 final PropertiesConfiguration.PropertiesWriter writer = 551 config.getIOFactory().createPropertiesWriter(out, 552 config.getListDelimiterHandler()); 553 writer.setGlobalSeparator(getGlobalSeparator()); 554 if (getLineSeparator() != null) 555 { 556 writer.setLineSeparator(getLineSeparator()); 557 } 558 559 if (headerComment != null) 560 { 561 writeComment(writer, getCanonicalHeaderComment(true)); 562 writer.writeln(null); 563 } 564 565 for (final String key : getKeys()) 566 { 567 if (config.containsKeyInternal(key)) 568 { 569 570 // Output blank lines before property 571 for (int i = 0; i < getBlancLinesBefore(key); i++) 572 { 573 writer.writeln(null); 574 } 575 576 // Output the comment 577 writeComment(writer, getCanonicalComment(key, true)); 578 579 // Output the property and its value 580 final boolean singleLine = isForceSingleLine() || isSingleLine(key); 581 writer.setCurrentSeparator(getSeparator(key)); 582 writer.writeProperty(key, config.getPropertyInternal( 583 key), singleLine); 584 } 585 } 586 587 writeComment(writer, getCanonicalFooterCooment(true)); 588 writer.flush(); 589 } 590 catch (final IOException ioex) 591 { 592 throw new ConfigurationException(ioex); 593 } 594 } 595 596 /** 597 * The event listener callback. Here event notifications of the 598 * configuration object are processed to update the layout object properly. 599 * 600 * @param event the event object 601 */ 602 @Override 603 public void onEvent(final ConfigurationEvent event) 604 { 605 if (!event.isBeforeUpdate() && loadCounter.get() == 0) 606 { 607 if (ConfigurationEvent.ADD_PROPERTY.equals(event.getEventType())) 608 { 609 final boolean contained = 610 layoutData.containsKey(event.getPropertyName()); 611 final PropertyLayoutData data = 612 fetchLayoutData(event.getPropertyName()); 613 data.setSingleLine(!contained); 614 } 615 else if (ConfigurationEvent.CLEAR_PROPERTY.equals(event 616 .getEventType())) 617 { 618 layoutData.remove(event.getPropertyName()); 619 } 620 else if (ConfigurationEvent.CLEAR.equals(event.getEventType())) 621 { 622 clear(); 623 } 624 else if (ConfigurationEvent.SET_PROPERTY.equals(event 625 .getEventType())) 626 { 627 fetchLayoutData(event.getPropertyName()); 628 } 629 } 630 } 631 632 /** 633 * Returns a layout data object for the specified key. If this is a new key, 634 * a new object is created and initialized with default values. 635 * 636 * @param key the key 637 * @return the corresponding layout data object 638 */ 639 private PropertyLayoutData fetchLayoutData(final String key) 640 { 641 if (key == null) 642 { 643 throw new IllegalArgumentException("Property key must not be null!"); 644 } 645 646 PropertyLayoutData data = layoutData.get(key); 647 if (data == null) 648 { 649 data = new PropertyLayoutData(); 650 data.setSingleLine(true); 651 layoutData.put(key, data); 652 } 653 654 return data; 655 } 656 657 /** 658 * Removes all content from this layout object. 659 */ 660 private void clear() 661 { 662 seenStack.clear(); 663 layoutData.clear(); 664 setHeaderComment(null); 665 setFooterComment(null); 666 } 667 668 /** 669 * Tests whether a line is a comment, i.e. whether it starts with a comment 670 * character. 671 * 672 * @param line the line 673 * @return a flag if this is a comment line 674 */ 675 static boolean isCommentLine(final String line) 676 { 677 return PropertiesConfiguration.isCommentLine(line); 678 } 679 680 /** 681 * Trims a comment. This method either removes all comment characters from 682 * the given string, leaving only the plain comment text or ensures that 683 * every line starts with a valid comment character. 684 * 685 * @param s the string to be processed 686 * @param comment if <b>true</b>, a comment character will always be 687 * enforced; if <b>false</b>, it will be removed 688 * @return the trimmed comment 689 */ 690 static String trimComment(final String s, final boolean comment) 691 { 692 final StringBuilder buf = new StringBuilder(s.length()); 693 int lastPos = 0; 694 int pos; 695 696 do 697 { 698 pos = s.indexOf(CR, lastPos); 699 if (pos >= 0) 700 { 701 final String line = s.substring(lastPos, pos); 702 buf.append(stripCommentChar(line, comment)).append(CR); 703 lastPos = pos + CR.length(); 704 } 705 } while (pos >= 0); 706 707 if (lastPos < s.length()) 708 { 709 buf.append(stripCommentChar(s.substring(lastPos), comment)); 710 } 711 return buf.toString(); 712 } 713 714 /** 715 * Either removes the comment character from the given comment line or 716 * ensures that the line starts with a comment character. 717 * 718 * @param s the comment line 719 * @param comment if <b>true</b>, a comment character will always be 720 * enforced; if <b>false</b>, it will be removed 721 * @return the line without comment character 722 */ 723 static String stripCommentChar(final String s, final boolean comment) 724 { 725 if (StringUtils.isBlank(s) || (isCommentLine(s) == comment)) 726 { 727 return s; 728 } 729 if (!comment) 730 { 731 int pos = 0; 732 // find first comment character 733 while (PropertiesConfiguration.COMMENT_CHARS.indexOf(s 734 .charAt(pos)) < 0) 735 { 736 pos++; 737 } 738 739 // Remove leading spaces 740 pos++; 741 while (pos < s.length() 742 && Character.isWhitespace(s.charAt(pos))) 743 { 744 pos++; 745 } 746 747 return pos < s.length() ? s.substring(pos) 748 : StringUtils.EMPTY; 749 } 750 return COMMENT_PREFIX + s; 751 } 752 753 /** 754 * Extracts a comment string from the given range of the specified comment 755 * lines. The single lines are added using a line feed as separator. 756 * 757 * @param commentLines a list with comment lines 758 * @param from the start index 759 * @param to the end index (inclusive) 760 * @return the comment string (<b>null</b> if it is undefined) 761 */ 762 private String extractComment(final List<String> commentLines, final int from, final int to) 763 { 764 if (to < from) 765 { 766 return null; 767 } 768 final StringBuilder buf = new StringBuilder(commentLines.get(from)); 769 for (int i = from + 1; i <= to; i++) 770 { 771 buf.append(CR); 772 buf.append(commentLines.get(i)); 773 } 774 return buf.toString(); 775 } 776 777 /** 778 * Checks if parts of the passed in comment can be used as header comment. 779 * This method checks whether a header comment can be defined (i.e. whether 780 * this is the first comment in the loaded file). If this is the case, it is 781 * searched for the latest blanc line. This line will mark the end of the 782 * header comment. The return value is the index of the first line in the 783 * passed in list, which does not belong to the header comment. 784 * 785 * @param commentLines the comment lines 786 * @return the index of the next line after the header comment 787 */ 788 private int checkHeaderComment(final List<String> commentLines) 789 { 790 if (loadCounter.get() == 1 && layoutData.isEmpty()) 791 { 792 // This is the first comment. Search for blanc lines. 793 int index = commentLines.size() - 1; 794 while (index >= 0 795 && commentLines.get(index).length() > 0) 796 { 797 index--; 798 } 799 if (getHeaderComment() == null) 800 { 801 setHeaderComment(extractComment(commentLines, 0, index - 1)); 802 } 803 return index + 1; 804 } 805 return 0; 806 } 807 808 /** 809 * Copies the data from the given layout object. 810 * 811 * @param c the layout object to copy 812 */ 813 private void copyFrom(final PropertiesConfigurationLayout c) 814 { 815 for (final String key : c.getKeys()) 816 { 817 final PropertyLayoutData data = c.layoutData.get(key); 818 layoutData.put(key, data.clone()); 819 } 820 821 setHeaderComment(c.getHeaderComment()); 822 setFooterComment(c.getFooterComment()); 823 } 824 825 /** 826 * Helper method for writing a comment line. This method ensures that the 827 * correct line separator is used if the comment spans multiple lines. 828 * 829 * @param writer the writer 830 * @param comment the comment to write 831 * @throws IOException if an IO error occurs 832 */ 833 private static void writeComment( 834 final PropertiesConfiguration.PropertiesWriter writer, final String comment) 835 throws IOException 836 { 837 if (comment != null) 838 { 839 writer.writeln(StringUtils.replace(comment, CR, writer 840 .getLineSeparator())); 841 } 842 } 843 844 /** 845 * Helper method for generating a comment string. Depending on the boolean 846 * argument the resulting string either has no comment characters or a 847 * leading comment character at each line. 848 * 849 * @param comment the comment string to be processed 850 * @param commentChar determines the presence of comment characters 851 * @return the canonical comment string (can be <b>null</b>) 852 */ 853 private static String constructCanonicalComment(final String comment, 854 final boolean commentChar) 855 { 856 return comment == null ? null : trimComment(comment, commentChar); 857 } 858 859 /** 860 * A helper class for storing all layout related information for a 861 * configuration property. 862 */ 863 static class PropertyLayoutData implements Cloneable 864 { 865 /** Stores the comment for the property. */ 866 private StringBuffer comment; 867 868 /** The separator to be used for this property. */ 869 private String separator; 870 871 /** Stores the number of blanc lines before this property. */ 872 private int blancLines; 873 874 /** Stores the single line property. */ 875 private boolean singleLine; 876 877 /** 878 * Creates a new instance of {@code PropertyLayoutData}. 879 */ 880 public PropertyLayoutData() 881 { 882 singleLine = true; 883 separator = PropertiesConfiguration.DEFAULT_SEPARATOR; 884 } 885 886 /** 887 * Returns the number of blanc lines before this property. 888 * 889 * @return the number of blanc lines before this property 890 */ 891 public int getBlancLines() 892 { 893 return blancLines; 894 } 895 896 /** 897 * Sets the number of properties before this property. 898 * 899 * @param blancLines the number of properties before this property 900 */ 901 public void setBlancLines(final int blancLines) 902 { 903 this.blancLines = blancLines; 904 } 905 906 /** 907 * Returns the single line flag. 908 * 909 * @return the single line flag 910 */ 911 public boolean isSingleLine() 912 { 913 return singleLine; 914 } 915 916 /** 917 * Sets the single line flag. 918 * 919 * @param singleLine the single line flag 920 */ 921 public void setSingleLine(final boolean singleLine) 922 { 923 this.singleLine = singleLine; 924 } 925 926 /** 927 * Adds a comment for this property. If already a comment exists, the 928 * new comment is added (separated by a newline). 929 * 930 * @param s the comment to add 931 */ 932 public void addComment(final String s) 933 { 934 if (s != null) 935 { 936 if (comment == null) 937 { 938 comment = new StringBuffer(s); 939 } 940 else 941 { 942 comment.append(CR).append(s); 943 } 944 } 945 } 946 947 /** 948 * Sets the comment for this property. 949 * 950 * @param s the new comment (can be <b>null</b>) 951 */ 952 public void setComment(final String s) 953 { 954 if (s == null) 955 { 956 comment = null; 957 } 958 else 959 { 960 comment = new StringBuffer(s); 961 } 962 } 963 964 /** 965 * Returns the comment for this property. The comment is returned as it 966 * is, without processing of comment characters. 967 * 968 * @return the comment (can be <b>null</b>) 969 */ 970 public String getComment() 971 { 972 return Objects.toString(comment, null); 973 } 974 975 /** 976 * Returns the separator that was used for this property. 977 * 978 * @return the property separator 979 */ 980 public String getSeparator() 981 { 982 return separator; 983 } 984 985 /** 986 * Sets the separator to be used for the represented property. 987 * 988 * @param separator the property separator 989 */ 990 public void setSeparator(final String separator) 991 { 992 this.separator = separator; 993 } 994 995 /** 996 * Creates a copy of this object. 997 * 998 * @return the copy 999 */ 1000 @Override 1001 public PropertyLayoutData clone() 1002 { 1003 try 1004 { 1005 final PropertyLayoutData copy = (PropertyLayoutData) super.clone(); 1006 if (comment != null) 1007 { 1008 // must copy string buffer, too 1009 copy.comment = new StringBuffer(getComment()); 1010 } 1011 return copy; 1012 } 1013 catch (final CloneNotSupportedException cnex) 1014 { 1015 // This cannot happen! 1016 throw new ConfigurationRuntimeException(cnex); 1017 } 1018 } 1019 } 1020}