1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.commons.configuration2;
18
19 import java.io.IOException;
20 import java.io.Reader;
21 import java.io.Writer;
22 import java.net.URL;
23 import java.util.ArrayDeque;
24 import java.util.LinkedHashMap;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Objects;
28 import java.util.Properties;
29 import java.util.Set;
30 import java.util.concurrent.atomic.AtomicInteger;
31
32 import org.apache.commons.configuration2.event.ConfigurationEvent;
33 import org.apache.commons.configuration2.event.EventListener;
34 import org.apache.commons.configuration2.ex.ConfigurationException;
35 import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
36 import org.apache.commons.lang3.StringUtils;
37
38 /**
39 * <p>
40 * A helper class used by {@link PropertiesConfiguration} to keep the layout of a properties file.
41 * </p>
42 * <p>
43 * Instances of this class are associated with a {@code PropertiesConfiguration} object. They are responsible for
44 * analyzing properties files and for extracting as much information about the file layout (for example empty lines, comments)
45 * as possible. When the properties file is written back again it should be close to the original.
46 * </p>
47 * <p>
48 * The {@code PropertiesConfigurationLayout} object associated with a {@code PropertiesConfiguration} object can be
49 * obtained using the {@code getLayout()} method of the configuration. Then the methods provided by this class can be
50 * used to alter the properties file's layout.
51 * </p>
52 * <p>
53 * Implementation note: This is a very simple implementation, which is far away from being perfect, i.e. the original
54 * layout of a properties file won't be reproduced in all cases. One limitation is that comments for multi-valued
55 * property keys are concatenated. Maybe this implementation can later be improved.
56 * </p>
57 * <p>
58 * To get an impression how this class works consider the following properties file:
59 * </p>
60 *
61 * <pre>
62 * # A demo configuration file
63 * # for Demo App 1.42
64 *
65 * # Application name
66 * AppName=Demo App
67 *
68 * # Application vendor
69 * AppVendor=DemoSoft
70 *
71 *
72 * # GUI properties
73 * # Window Color
74 * windowColors=0xFFFFFF,0x000000
75 *
76 * # Include some setting
77 * include=settings.properties
78 * # Another vendor
79 * AppVendor=TestSoft
80 * </pre>
81 *
82 * <p>
83 * For this example the following points are relevant:
84 * </p>
85 * <ul>
86 * <li>The first two lines are set as header comment. The header comment is determined by the last blank line before the
87 * first property definition.</li>
88 * <li>For the property {@code AppName} one comment line and one leading blank line is stored.</li>
89 * <li>For the property {@code windowColors} two comment lines and two leading blank lines are stored.</li>
90 * <li>Include files is something this class cannot deal with well. When saving the properties configuration back, the
91 * included properties are simply contained in the original file. The comment before the include property is
92 * skipped.</li>
93 * <li>For all properties except for {@code AppVendor} the "single line" flag is set. This is relevant only
94 * for {@code windowColors}, which has multiple values defined in one line using the separator character.</li>
95 * <li>The {@code AppVendor} property appears twice. The comment lines are concatenated, so that
96 * {@code layout.getComment("AppVendor");} will result in {@code Application vendor<CR>Another vendor}, with
97 * {@code <CR>} meaning the line separator. In addition the "single line" flag is set to <strong>false</strong>
98 * for this property. When the file is saved, two property definitions will be written (in series).</li>
99 * </ul>
100 *
101 * @since 1.3
102 */
103 public 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 <strong>null</strong>)
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 <strong>null</strong>)
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 <strong>null</strong>)
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 <strong>true</strong>, a comment character will always be enforced; if <strong>false</strong>, 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 <strong>true</strong>, a comment character will always be enforced; if <strong>false</strong>, 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 volatile 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 (<strong>null</strong> 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 <strong>false</strong>, 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 <strong>null</strong>)
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 <strong>null</strong>)
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 <strong>null</strong>)
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 <strong>null</strong>)
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 <strong>null</strong>)
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 <strong>null</strong>)
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 propReader = config.getIOFactory().createPropertiesReader(reader);
662 try {
663 while (propReader.nextProperty()) {
664 if (config.propertyLoaded(propReader.getPropertyName(), propReader.getPropertyValue(), seenStack)) {
665 final boolean contained = layoutData.containsKey(propReader.getPropertyName());
666 int blankLines = 0;
667 int idx = checkHeaderComment(propReader.getCommentLines());
668 while (idx < propReader.getCommentLines().size() && StringUtils.isEmpty(propReader.getCommentLines().get(idx))) {
669 idx++;
670 blankLines++;
671 }
672 final String comment = extractComment(propReader.getCommentLines(), idx, propReader.getCommentLines().size() - 1);
673 final PropertyLayoutData data = fetchLayoutData(propReader.getPropertyName());
674 if (contained) {
675 data.addComment(comment);
676 data.setSingleLine(false);
677 } else {
678 data.setComment(comment);
679 data.setBlankLines(blankLines);
680 data.setSeparator(propReader.getPropertySeparator());
681 }
682 }
683 }
684 setFooterComment(extractComment(propReader.getCommentLines(), 0, propReader.getCommentLines().size() - 1));
685 } catch (final IOException ioex) {
686 throw new ConfigurationException(ioex);
687 } finally {
688 loadCounter.decrementAndGet();
689 }
690 }
691
692 /**
693 * The event listener callback. Here event notifications of the configuration object are processed to update the layout
694 * object properly.
695 *
696 * @param event the event object
697 */
698 @Override
699 public void onEvent(final ConfigurationEvent event) {
700 if (!event.isBeforeUpdate() && loadCounter.get() == 0) {
701 if (ConfigurationEvent.ADD_PROPERTY.equals(event.getEventType())) {
702 final boolean contained = layoutData.containsKey(event.getPropertyName());
703 final PropertyLayoutData data = fetchLayoutData(event.getPropertyName());
704 data.setSingleLine(!contained);
705 } else if (ConfigurationEvent.CLEAR_PROPERTY.equals(event.getEventType())) {
706 layoutData.remove(event.getPropertyName());
707 } else if (ConfigurationEvent.CLEAR.equals(event.getEventType())) {
708 clear();
709 } else if (ConfigurationEvent.SET_PROPERTY.equals(event.getEventType())) {
710 fetchLayoutData(event.getPropertyName());
711 }
712 }
713 }
714
715 /**
716 * Writes the properties file to the given writer, preserving as much of its structure as possible.
717 *
718 * @param config the associated configuration object
719 * @param writer the writer
720 * @throws ConfigurationException if an error occurs
721 */
722 public void save(final PropertiesConfiguration config, final Writer writer) throws ConfigurationException {
723 try {
724 @SuppressWarnings("resource") // createPropertiesReader wraps the writer.
725 final PropertiesConfiguration.PropertiesWriter propWriter = config.getIOFactory().createPropertiesWriter(writer, config.getListDelimiterHandler());
726 propWriter.setGlobalSeparator(getGlobalSeparator());
727 if (getLineSeparator() != null) {
728 propWriter.setLineSeparator(getLineSeparator());
729 }
730 if (headerComment != null) {
731 writeComment(propWriter, getCanonicalHeaderComment(true));
732 }
733 boolean firstKey = true;
734 for (final String key : getKeys()) {
735 if (config.containsKeyInternal(key)) {
736 // preset header comment needs to be separated from key
737 if (firstKey && headerComment != null && getBlankLinesBefore(key) == 0) {
738 propWriter.writeln(null);
739 }
740 // Output blank lines before property
741 for (int i = 0; i < getBlankLinesBefore(key); i++) {
742 propWriter.writeln(null);
743 }
744 // Output the comment
745 writeComment(propWriter, getCanonicalComment(key, true));
746 // Output the property and its value
747 final boolean singleLine = isForceSingleLine() || isSingleLine(key);
748 propWriter.setCurrentSeparator(getSeparator(key));
749 propWriter.writeProperty(key, config.getPropertyInternal(key), singleLine);
750 }
751 firstKey = false;
752 }
753 writeComment(propWriter, getCanonicalFooterCooment(true));
754 propWriter.flush();
755 } catch (final IOException ioex) {
756 throw new ConfigurationException(ioex);
757 }
758 }
759
760 /**
761 * Sets the number of blank lines before the given property key. This can be used for a logical grouping of properties.
762 *
763 * @param key the property key
764 * @param number the number of blank lines to add before this property definition
765 * @deprecated Use {@link PropertiesConfigurationLayout#setBlankLinesBefore(String, int)}.
766 */
767 @Deprecated
768 public void setBlancLinesBefore(final String key, final int number) {
769 setBlankLinesBefore(key, number);
770 }
771
772 /**
773 * Sets the number of blank lines before the given property key. This can be used for a logical grouping of properties.
774 *
775 * @param key the property key
776 * @param number the number of blank lines to add before this property definition
777 * @since 2.8.0
778 */
779 public void setBlankLinesBefore(final String key, final int number) {
780 fetchLayoutData(key).setBlankLines(number);
781 }
782
783 /**
784 * Sets the comment for the specified property key. The comment (or its single lines if it is a multi-line comment) can
785 * start with a comment character. If this is the case, it will be written without changes. Otherwise a default comment
786 * character is added automatically.
787 *
788 * @param key the key of the property
789 * @param comment the comment for this key (can be <strong>null</strong>, then the comment will be removed)
790 */
791 public void setComment(final String key, final String comment) {
792 fetchLayoutData(key).setComment(comment);
793 }
794
795 /**
796 * Sets the footer comment for the represented properties file. This comment will be output at the bottom of the file.
797 *
798 * @param footerComment the footer comment
799 * @since 2.0
800 */
801 public void setFooterComment(final String footerComment) {
802 this.footerComment = footerComment;
803 }
804
805 /**
806 * Sets the "force single line" flag. If this flag is set, all properties with multiple values are written on
807 * single lines. This mode provides more compatibility with {@link Properties}, which cannot deal with
808 * multiple definitions of a single property. This mode has no effect if the list delimiter parsing is disabled.
809 *
810 * @param f the force single line flag
811 */
812 public void setForceSingleLine(final boolean f) {
813 forceSingleLine = f;
814 }
815
816 /**
817 * Sets the global separator for properties. With this method a separator can be set that will be used for all
818 * properties when writing the configuration. This is an easy way of determining the properties separator globally. To
819 * be compatible with the properties format only the characters {@code =} and {@code :} (with or without whitespace)
820 * should be used, but this method does not enforce this - it accepts arbitrary strings. If the global separator is set
821 * to <strong>null</strong>, property separators are not changed. This is the default behavior as it produces results that are
822 * closer to the original properties file.
823 *
824 * @param globalSeparator the separator to be used for all properties
825 * @since 1.7
826 */
827 public void setGlobalSeparator(final String globalSeparator) {
828 this.globalSeparator = globalSeparator;
829 }
830
831 /**
832 * Sets the header comment for the represented properties file. This comment will be output on top of the file.
833 *
834 * @param comment the comment
835 */
836 public void setHeaderComment(final String comment) {
837 headerComment = comment;
838 }
839
840 /**
841 * Sets the line separator. When writing the properties configuration, all lines are terminated with this separator. If
842 * no separator was set, the platform-specific default line separator is used.
843 *
844 * @param lineSeparator the line separator
845 * @since 1.7
846 */
847 public void setLineSeparator(final String lineSeparator) {
848 this.lineSeparator = lineSeparator;
849 }
850
851 /**
852 * Sets the separator to be used for the property with the given key. The separator is the string between the property
853 * key and its value. For new properties " = " is used. When a properties file is read, the layout tries to
854 * determine the separator for each property. With this method the separator can be changed. To be compatible with the
855 * properties format only the characters {@code =} and {@code :} (with or without whitespace) should be used, but this
856 * method does not enforce this - it accepts arbitrary strings. If the key refers to a property with multiple values
857 * that are written on multiple lines, this separator will be used on all lines.
858 *
859 * @param key the key for the property
860 * @param sep the separator to be used for this property
861 * @since 1.7
862 */
863 public void setSeparator(final String key, final String sep) {
864 fetchLayoutData(key).setSeparator(sep);
865 }
866
867 /**
868 * Sets the "single line flag" for the specified property key. This flag is evaluated if the property has
869 * 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
870 * single property definition using the list delimiter as separator. Otherwise multiple lines will be written for this
871 * property, each line containing one property value.
872 *
873 * @param key the property key
874 * @param f the single line flag
875 */
876 public void setSingleLine(final String key, final boolean f) {
877 fetchLayoutData(key).setSingleLine(f);
878 }
879 }