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 import org.apache.commons.lang3.Strings;
38
39 /**
40 * <p>
41 * A helper class used by {@link PropertiesConfiguration} to keep the layout of a properties file.
42 * </p>
43 * <p>
44 * Instances of this class are associated with a {@code PropertiesConfiguration} object. They are responsible for
45 * analyzing properties files and for extracting as much information about the file layout (for example empty lines, comments)
46 * as possible. When the properties file is written back again it should be close to the original.
47 * </p>
48 * <p>
49 * The {@code PropertiesConfigurationLayout} object associated with a {@code PropertiesConfiguration} object can be
50 * obtained using the {@code getLayout()} method of the configuration. Then the methods provided by this class can be
51 * used to alter the properties file's layout.
52 * </p>
53 * <p>
54 * Implementation note: This is a very simple implementation, which is far away from being perfect, i.e. the original
55 * layout of a properties file won't be reproduced in all cases. One limitation is that comments for multi-valued
56 * property keys are concatenated. Maybe this implementation can later be improved.
57 * </p>
58 * <p>
59 * To get an impression how this class works consider the following properties file:
60 * </p>
61 *
62 * <pre>
63 * # A demo configuration file
64 * # for Demo App 1.42
65 *
66 * # Application name
67 * AppName=Demo App
68 *
69 * # Application vendor
70 * AppVendor=DemoSoft
71 *
72 *
73 * # GUI properties
74 * # Window Color
75 * windowColors=0xFFFFFF,0x000000
76 *
77 * # Include some setting
78 * include=settings.properties
79 * # Another vendor
80 * AppVendor=TestSoft
81 * </pre>
82 *
83 * <p>
84 * For this example the following points are relevant:
85 * </p>
86 * <ul>
87 * <li>The first two lines are set as header comment. The header comment is determined by the last blank line before the
88 * first property definition.</li>
89 * <li>For the property {@code AppName} one comment line and one leading blank line is stored.</li>
90 * <li>For the property {@code windowColors} two comment lines and two leading blank lines are stored.</li>
91 * <li>Include files is something this class cannot deal with well. When saving the properties configuration back, the
92 * included properties are simply contained in the original file. The comment before the include property is
93 * skipped.</li>
94 * <li>For all properties except for {@code AppVendor} the "single line" flag is set. This is relevant only
95 * for {@code windowColors}, which has multiple values defined in one line using the separator character.</li>
96 * <li>The {@code AppVendor} property appears twice. The comment lines are concatenated, so that
97 * {@code layout.getComment("AppVendor");} will result in {@code Application vendor<CR>Another vendor}, with
98 * {@code <CR>} meaning the line separator. In addition the "single line" flag is set to <strong>false</strong>
99 * 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 */
104 public 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 }