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