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