    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}