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