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