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    }