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