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