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