View Javadoc
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 &quot;single line&quot; 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&lt;CR&gt;Another vendor}, with
98   * {@code &lt;CR&gt;} meaning the line separator. In addition the &quot;single line&quot; 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. &quot;Canonical&quot; 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 &quot;\n&quot;.
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 &quot;force single line&quot; 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 &quot;force single line&quot; 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 &quot; = &quot; 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 &quot;single line flag&quot; 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 }