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