001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *     https://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.configuration2;
018
019import java.io.IOException;
020import java.io.Reader;
021import java.io.Writer;
022import java.net.URL;
023import java.util.ArrayDeque;
024import java.util.LinkedHashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.Objects;
028import java.util.Properties;
029import java.util.Set;
030import java.util.concurrent.atomic.AtomicInteger;
031
032import org.apache.commons.configuration2.event.ConfigurationEvent;
033import org.apache.commons.configuration2.event.EventListener;
034import org.apache.commons.configuration2.ex.ConfigurationException;
035import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
036import org.apache.commons.lang3.StringUtils;
037import org.apache.commons.lang3.Strings;
038
039/**
040 * <p>
041 * A helper class used by {@link PropertiesConfiguration} to keep the layout of a properties file.
042 * </p>
043 * <p>
044 * Instances of this class are associated with a {@code PropertiesConfiguration} object. They are responsible for
045 * analyzing properties files and for extracting as much information about the file layout (for example empty lines, comments)
046 * as possible. When the properties file is written back again it should be close to the original.
047 * </p>
048 * <p>
049 * The {@code PropertiesConfigurationLayout} object associated with a {@code PropertiesConfiguration} object can be
050 * obtained using the {@code getLayout()} method of the configuration. Then the methods provided by this class can be
051 * used to alter the properties file's layout.
052 * </p>
053 * <p>
054 * Implementation note: This is a very simple implementation, which is far away from being perfect, i.e. the original
055 * layout of a properties file won't be reproduced in all cases. One limitation is that comments for multi-valued
056 * property keys are concatenated. Maybe this implementation can later be improved.
057 * </p>
058 * <p>
059 * To get an impression how this class works consider the following properties file:
060 * </p>
061 *
062 * <pre>
063 * # A demo configuration file
064 * # for Demo App 1.42
065 *
066 * # Application name
067 * AppName=Demo App
068 *
069 * # Application vendor
070 * AppVendor=DemoSoft
071 *
072 *
073 * # GUI properties
074 * # Window Color
075 * windowColors=0xFFFFFF,0x000000
076 *
077 * # Include some setting
078 * include=settings.properties
079 * # Another vendor
080 * AppVendor=TestSoft
081 * </pre>
082 *
083 * <p>
084 * For this example the following points are relevant:
085 * </p>
086 * <ul>
087 * <li>The first two lines are set as header comment. The header comment is determined by the last blank line before the
088 * first property definition.</li>
089 * <li>For the property {@code AppName} one comment line and one leading blank line is stored.</li>
090 * <li>For the property {@code windowColors} two comment lines and two leading blank lines are stored.</li>
091 * <li>Include files is something this class cannot deal with well. When saving the properties configuration back, the
092 * included properties are simply contained in the original file. The comment before the include property is
093 * skipped.</li>
094 * <li>For all properties except for {@code AppVendor} the &quot;single line&quot; flag is set. This is relevant only
095 * for {@code windowColors}, which has multiple values defined in one line using the separator character.</li>
096 * <li>The {@code AppVendor} property appears twice. The comment lines are concatenated, so that
097 * {@code layout.getComment("AppVendor");} will result in {@code Application vendor&lt;CR&gt;Another vendor}, with
098 * {@code &lt;CR&gt;} meaning the line separator. In addition the &quot;single line&quot; flag is set to <strong>false</strong>
099 * for this property. When the file is saved, two property definitions will be written (in series).</li>
100 * </ul>
101 *
102 * @since 1.3
103 */
104public class PropertiesConfigurationLayout implements EventListener<ConfigurationEvent> {
105
106    /**
107     * A helper class for storing all layout related information for a configuration property.
108     */
109    static class PropertyLayoutData implements Cloneable {
110
111        /** Stores the comment for the property. */
112        private StringBuffer comment;
113
114        /** The separator to be used for this property. */
115        private String separator;
116
117        /** Stores the number of blank lines before this property. */
118        private int blankLines;
119
120        /** Stores the single line property. */
121        private boolean singleLine;
122
123        /**
124         * Creates a new instance of {@code PropertyLayoutData}.
125         */
126        public PropertyLayoutData() {
127            singleLine = true;
128            separator = PropertiesConfiguration.DEFAULT_SEPARATOR;
129        }
130
131        /**
132         * Adds a comment for this property. If already a comment exists, the new comment is added (separated by a newline).
133         *
134         * @param s the comment to add
135         */
136        public void addComment(final String s) {
137            if (s != null) {
138                if (comment == null) {
139                    comment = new StringBuffer(s);
140                } else {
141                    comment.append(CR).append(s);
142                }
143            }
144        }
145
146        /**
147         * Creates a copy of this object.
148         *
149         * @return the copy
150         */
151        @Override
152        public PropertyLayoutData clone() {
153            try {
154                final PropertyLayoutData copy = (PropertyLayoutData) super.clone();
155                if (comment != null) {
156                    // must copy string buffer, too
157                    copy.comment = new StringBuffer(getComment());
158                }
159                return copy;
160            } catch (final CloneNotSupportedException cnex) {
161                // This cannot happen!
162                throw new ConfigurationRuntimeException(cnex);
163            }
164        }
165
166        /**
167         * Gets the number of blank lines before this property.
168         *
169         * @return the number of blank lines before this property
170         * @deprecated Use {#link {@link #getBlankLines()}}.
171         */
172        @Deprecated
173        public int getBlancLines() {
174            return getBlankLines();
175        }
176
177        /**
178         * Gets the number of blank lines before this property.
179         *
180         * @return the number of blank lines before this property
181         * @since 2.8.0
182         */
183        public int getBlankLines() {
184            return blankLines;
185        }
186
187        /**
188         * Gets the comment for this property. The comment is returned as it is, without processing of comment characters.
189         *
190         * @return the comment (can be <strong>null</strong>)
191         */
192        public String getComment() {
193            return Objects.toString(comment, null);
194        }
195
196        /**
197         * Gets the separator that was used for this property.
198         *
199         * @return the property separator
200         */
201        public String getSeparator() {
202            return separator;
203        }
204
205        /**
206         * Returns the single line flag.
207         *
208         * @return the single line flag
209         */
210        public boolean isSingleLine() {
211            return singleLine;
212        }
213
214        /**
215         * Sets the number of properties before this property.
216         *
217         * @param blankLines the number of properties before this property
218         * @deprecated Use {@link #setBlankLines(int)}.
219         */
220        @Deprecated
221        public void setBlancLines(final int blankLines) {
222            setBlankLines(blankLines);
223        }
224
225        /**
226         * Sets the number of properties before this property.
227         *
228         * @param blankLines the number of properties before this property
229         * @since 2.8.0
230         */
231        public void setBlankLines(final int blankLines) {
232            this.blankLines = blankLines;
233        }
234
235        /**
236         * Sets the comment for this property.
237         *
238         * @param s the new comment (can be <strong>null</strong>)
239         */
240        public void setComment(final String s) {
241            if (s == null) {
242                comment = null;
243            } else {
244                comment = new StringBuffer(s);
245            }
246        }
247
248        /**
249         * Sets the separator to be used for the represented property.
250         *
251         * @param separator the property separator
252         */
253        public void setSeparator(final String separator) {
254            this.separator = separator;
255        }
256
257        /**
258         * Sets the single line flag.
259         *
260         * @param singleLine the single line flag
261         */
262        public void setSingleLine(final boolean singleLine) {
263            this.singleLine = singleLine;
264        }
265    }
266
267    /** Constant for the line break character. */
268    private static final String CR = "\n";
269
270    /** Constant for the default comment prefix. */
271    private static final String COMMENT_PREFIX = "# ";
272
273    /**
274     * Helper method for generating a comment string. Depending on the boolean argument the resulting string either has no
275     * comment characters or a leading comment character at each line.
276     *
277     * @param comment the comment string to be processed
278     * @param commentChar determines the presence of comment characters
279     * @return the canonical comment string (can be <strong>null</strong>)
280     */
281    private static String constructCanonicalComment(final String comment, final boolean commentChar) {
282        return comment == null ? null : trimComment(comment, commentChar);
283    }
284
285    /**
286     * Tests whether a line is a comment, i.e. whether it starts with a comment character.
287     *
288     * @param line the line
289     * @return a flag if this is a comment line
290     */
291    static boolean isCommentLine(final String line) {
292        return PropertiesConfiguration.isCommentLine(line);
293    }
294
295    /**
296     * Either removes the comment character from the given comment line or ensures that the line starts with a comment
297     * character.
298     *
299     * @param s the comment line
300     * @param comment if <strong>true</strong>, a comment character will always be enforced; if <strong>false</strong>, it will be removed
301     * @return the line without comment character
302     */
303    static String stripCommentChar(final String s, final boolean comment) {
304        if (StringUtils.isBlank(s) || isCommentLine(s) == comment) {
305            return s;
306        }
307        if (!comment) {
308            int pos = 0;
309            // find first comment character
310            while (PropertiesConfiguration.COMMENT_CHARS.indexOf(s.charAt(pos)) < 0) {
311                pos++;
312            }
313
314            // Remove leading spaces
315            pos++;
316            while (pos < s.length() && Character.isWhitespace(s.charAt(pos))) {
317                pos++;
318            }
319
320            return pos < s.length() ? s.substring(pos) : StringUtils.EMPTY;
321        }
322        return COMMENT_PREFIX + s;
323    }
324
325    /**
326     * Trims a comment. This method either removes all comment characters from the given string, leaving only the plain
327     * comment text or ensures that every line starts with a valid comment character.
328     *
329     * @param s the string to be processed
330     * @param comment if <strong>true</strong>, a comment character will always be enforced; if <strong>false</strong>, it will be removed
331     * @return the trimmed comment
332     */
333    static String trimComment(final String s, final boolean comment) {
334        final StringBuilder buf = new StringBuilder(s.length());
335        int lastPos = 0;
336        int pos;
337
338        do {
339            pos = s.indexOf(CR, lastPos);
340            if (pos >= 0) {
341                final String line = s.substring(lastPos, pos);
342                buf.append(stripCommentChar(line, comment)).append(CR);
343                lastPos = pos + CR.length();
344            }
345        } while (pos >= 0);
346
347        if (lastPos < s.length()) {
348            buf.append(stripCommentChar(s.substring(lastPos), comment));
349        }
350        return buf.toString();
351    }
352
353    /**
354     * Helper method for writing a comment line. This method ensures that the correct line separator is used if the comment
355     * spans multiple lines.
356     *
357     * @param writer the writer
358     * @param comment the comment to write
359     * @throws IOException if an IO error occurs
360     */
361    private static void writeComment(final PropertiesConfiguration.PropertiesWriter writer, final String comment) throws IOException {
362        if (comment != null) {
363            writer.writeln(Strings.CS.replace(comment, CR, writer.getLineSeparator()));
364        }
365    }
366
367    /** Stores a map with the contained layout information. */
368    private final Map<String, PropertyLayoutData> layoutData;
369
370    /** Stores the header comment. */
371    private String headerComment;
372
373    /** Stores the footer comment. */
374    private String footerComment;
375
376    /** The global separator that will be used for all properties. */
377    private String globalSeparator;
378
379    /** The line separator. */
380    private String lineSeparator;
381
382    /** A counter for determining nested load calls. */
383    private final AtomicInteger loadCounter;
384
385    /** Stores the force single line flag. */
386    private volatile boolean forceSingleLine;
387
388    /** Seen includes. */
389    private final ArrayDeque<URL> seenStack = new ArrayDeque<>();
390
391    /**
392     * Creates a new, empty instance of {@code PropertiesConfigurationLayout}.
393     */
394    public PropertiesConfigurationLayout() {
395        this(null);
396    }
397
398    /**
399     * Creates a new instance of {@code PropertiesConfigurationLayout} and copies the data of the specified layout object.
400     *
401     * @param c the layout object to be copied
402     */
403    public PropertiesConfigurationLayout(final PropertiesConfigurationLayout c) {
404        loadCounter = new AtomicInteger();
405        layoutData = new LinkedHashMap<>();
406
407        if (c != null) {
408            copyFrom(c);
409        }
410    }
411
412    /**
413     * Checks if parts of the passed in comment can be used as header comment. This method checks whether a header comment
414     * can be defined (i.e. whether this is the first comment in the loaded file). If this is the case, it is searched for
415     * the latest blank line. This line will mark the end of the header comment. The return value is the index of the first
416     * line in the passed in list, which does not belong to the header comment.
417     *
418     * @param commentLines the comment lines
419     * @return the index of the next line after the header comment
420     */
421    private int checkHeaderComment(final List<String> commentLines) {
422        if (loadCounter.get() == 1 && layoutData.isEmpty()) {
423            int index = commentLines.size() - 1;
424            // strip comments that belong to first key
425            while (index >= 0 && StringUtils.isNotEmpty(commentLines.get(index))) {
426                index--;
427            }
428            // strip blank lines
429            while (index >= 0 && StringUtils.isEmpty(commentLines.get(index))) {
430                index--;
431            }
432            if (getHeaderComment() == null) {
433                setHeaderComment(extractComment(commentLines, 0, index));
434            }
435            return index + 1;
436        }
437        return 0;
438    }
439
440    /**
441     * Removes all content from this layout object.
442     */
443    private void clear() {
444        seenStack.clear();
445        layoutData.clear();
446        setHeaderComment(null);
447        setFooterComment(null);
448    }
449
450    /**
451     * Copies the data from the given layout object.
452     *
453     * @param c the layout object to copy
454     */
455    private void copyFrom(final PropertiesConfigurationLayout c) {
456        c.getKeys().forEach(key -> layoutData.put(key, c.layoutData.get(key).clone()));
457
458        setHeaderComment(c.getHeaderComment());
459        setFooterComment(c.getFooterComment());
460    }
461
462    /**
463     * Extracts a comment string from the given range of the specified comment lines. The single lines are added using a
464     * line feed as separator.
465     *
466     * @param commentLines a list with comment lines
467     * @param from the start index
468     * @param to the end index (inclusive)
469     * @return the comment string (<strong>null</strong> if it is undefined)
470     */
471    private String extractComment(final List<String> commentLines, final int from, final int to) {
472        if (to < from) {
473            return null;
474        }
475        final StringBuilder buf = new StringBuilder(commentLines.get(from));
476        for (int i = from + 1; i <= to; i++) {
477            buf.append(CR);
478            buf.append(commentLines.get(i));
479        }
480        return buf.toString();
481    }
482
483    /**
484     * Returns a layout data object for the specified key. If this is a new key, a new object is created and initialized
485     * with default values.
486     *
487     * @param key the key
488     * @return the corresponding layout data object
489     */
490    private PropertyLayoutData fetchLayoutData(final String key) {
491        if (key == null) {
492            throw new IllegalArgumentException("Property key must not be null!");
493        }
494
495        // PropertyLayoutData defaults to singleLine = true
496        return layoutData.computeIfAbsent(key, k -> new PropertyLayoutData());
497    }
498
499    /**
500     * Gets the number of blank lines before this property key. If this key does not exist, 0 will be returned.
501     *
502     * @param key the property key
503     * @return the number of blank lines before the property definition for this key
504     * @deprecated Use {@link #getBlankLinesBefore(String)}.
505     */
506    @Deprecated
507    public int getBlancLinesBefore(final String key) {
508        return getBlankLinesBefore(key);
509    }
510
511    /**
512     * Gets the number of blank lines before this property key. If this key does not exist, 0 will be returned.
513     *
514     * @param key the property key
515     * @return the number of blank lines before the property definition for this key
516     */
517    public int getBlankLinesBefore(final String key) {
518        return fetchLayoutData(key).getBlankLines();
519    }
520
521    /**
522     * Gets the comment for the specified property key in a canonical form. &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}