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