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