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