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