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 589380 2007-10-28 16:37:35Z 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 = System.getProperty("line.separator");
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        /** A counter for determining nested load calls. */
136        private int loadCounter;
137    
138        /** Stores the force single line flag. */
139        private boolean forceSingleLine;
140    
141        /**
142         * Creates a new instance of <code>PropertiesConfigurationLayout</code>
143         * and initializes it with the associated configuration object.
144         *
145         * @param config the configuration (must not be <b>null</b>)
146         */
147        public PropertiesConfigurationLayout(PropertiesConfiguration config)
148        {
149            this(config, null);
150        }
151    
152        /**
153         * Creates a new instance of <code>PropertiesConfigurationLayout</code>
154         * and initializes it with the given configuration object. The data of the
155         * specified layout object is copied.
156         *
157         * @param config the configuration (must not be <b>null</b>)
158         * @param c the layout object to be copied
159         */
160        public PropertiesConfigurationLayout(PropertiesConfiguration config,
161                PropertiesConfigurationLayout c)
162        {
163            if (config == null)
164            {
165                throw new IllegalArgumentException(
166                        "Configuration must not be null!");
167            }
168            configuration = config;
169            layoutData = new LinkedMap();
170            config.addConfigurationListener(this);
171    
172            if (c != null)
173            {
174                copyFrom(c);
175            }
176        }
177    
178        /**
179         * Returns the associated configuration object.
180         *
181         * @return the associated configuration
182         */
183        public PropertiesConfiguration getConfiguration()
184        {
185            return configuration;
186        }
187    
188        /**
189         * Returns the comment for the specified property key in a cononical form.
190         * &quot;Canonical&quot; means that either all lines start with a comment
191         * character or none. The <code>commentChar</code> parameter is <b>false</b>,
192         * all comment characters are removed, so that the result is only the plain
193         * text of the comment. Otherwise it is ensured that each line of the
194         * comment starts with a comment character.
195         *
196         * @param key the key of the property
197         * @param commentChar determines whether all lines should start with comment
198         * characters or not
199         * @return the canonical comment for this key (can be <b>null</b>)
200         */
201        public String getCanonicalComment(String key, boolean commentChar)
202        {
203            String comment = getComment(key);
204            if (comment == null)
205            {
206                return null;
207            }
208            else
209            {
210                return trimComment(comment, commentChar);
211            }
212        }
213    
214        /**
215         * Returns the comment for the specified property key. The comment is
216         * returned as it was set (either manually by calling
217         * <code>setComment()</code> or when it was loaded from a properties
218         * file). No modifications are performed.
219         *
220         * @param key the key of the property
221         * @return the comment for this key (can be <b>null</b>)
222         */
223        public String getComment(String key)
224        {
225            return fetchLayoutData(key).getComment();
226        }
227    
228        /**
229         * Sets the comment for the specified property key. The comment (or its
230         * single lines if it is a multi-line comment) can start with a comment
231         * character. If this is the case, it will be written without changes.
232         * Otherwise a default comment character is added automatically.
233         *
234         * @param key the key of the property
235         * @param comment the comment for this key (can be <b>null</b>, then the
236         * comment will be removed)
237         */
238        public void setComment(String key, String comment)
239        {
240            fetchLayoutData(key).setComment(comment);
241        }
242    
243        /**
244         * Returns the number of blanc lines before this property key. If this key
245         * does not exist, 0 will be returned.
246         *
247         * @param key the property key
248         * @return the number of blanc lines before the property definition for this
249         * key
250         */
251        public int getBlancLinesBefore(String key)
252        {
253            return fetchLayoutData(key).getBlancLines();
254        }
255    
256        /**
257         * Sets the number of blanc lines before the given property key. This can be
258         * used for a logical grouping of properties.
259         *
260         * @param key the property key
261         * @param number the number of blanc lines to add before this property
262         * definition
263         */
264        public void setBlancLinesBefore(String key, int number)
265        {
266            fetchLayoutData(key).setBlancLines(number);
267        }
268    
269        /**
270         * Returns the header comment of the represented properties file in a
271         * canonical form. With the <code>commentChar</code> parameter it can be
272         * specified whether comment characters should be stripped or be always
273         * present.
274         *
275         * @param commentChar determines the presence of comment characters
276         * @return the header comment (can be <b>null</b>)
277         */
278        public String getCanonicalHeaderComment(boolean commentChar)
279        {
280            return (getHeaderComment() == null) ? null : trimComment(
281                    getHeaderComment(), commentChar);
282        }
283    
284        /**
285         * Returns the header comment of the represented properties file. This
286         * method returns the header comment exactly as it was set using
287         * <code>setHeaderComment()</code> or extracted from the loaded properties
288         * file.
289         *
290         * @return the header comment (can be <b>null</b>)
291         */
292        public String getHeaderComment()
293        {
294            return headerComment;
295        }
296    
297        /**
298         * Sets the header comment for the represented properties file. This comment
299         * will be output on top of the file.
300         *
301         * @param comment the comment
302         */
303        public void setHeaderComment(String comment)
304        {
305            headerComment = comment;
306        }
307    
308        /**
309         * Returns a flag whether the specified property is defined on a single
310         * line. This is meaningful only if this property has multiple values.
311         *
312         * @param key the property key
313         * @return a flag if this property is defined on a single line
314         */
315        public boolean isSingleLine(String key)
316        {
317            return fetchLayoutData(key).isSingleLine();
318        }
319    
320        /**
321         * Sets the &quot;single line flag&quot; for the specified property key.
322         * This flag is evaluated if the property has multiple values (i.e. if it is
323         * a list property). In this case, if the flag is set, all values will be
324         * written in a single property definition using the list delimiter as
325         * separator. Otherwise multiple lines will be written for this property,
326         * each line containing one property value.
327         *
328         * @param key the property key
329         * @param f the single line flag
330         */
331        public void setSingleLine(String key, boolean f)
332        {
333            fetchLayoutData(key).setSingleLine(f);
334        }
335    
336        /**
337         * Returns the &quot;force single line&quot; flag.
338         *
339         * @return the force single line flag
340         * @see #setForceSingleLine(boolean)
341         */
342        public boolean isForceSingleLine()
343        {
344            return forceSingleLine;
345        }
346    
347        /**
348         * Sets the &quot;force single line&quot; flag. If this flag is set, all
349         * properties with multiple values are written on single lines. This mode
350         * provides more compatibility with <code>java.lang.Properties</code>,
351         * which cannot deal with multiple definitions of a single property. This
352         * mode has no effect if the list delimiter parsing is disabled.
353         *
354         * @param f the force single line flag
355         */
356        public void setForceSingleLine(boolean f)
357        {
358            forceSingleLine = f;
359        }
360    
361        /**
362         * Returns a set with all property keys managed by this object.
363         *
364         * @return a set with all contained property keys
365         */
366        public Set getKeys()
367        {
368            return layoutData.keySet();
369        }
370    
371        /**
372         * Reads a properties file and stores its internal structure. The found
373         * properties will be added to the associated configuration object.
374         *
375         * @param in the reader to the properties file
376         * @throws ConfigurationException if an error occurs
377         */
378        public void load(Reader in) throws ConfigurationException
379        {
380            if (++loadCounter == 1)
381            {
382                getConfiguration().removeConfigurationListener(this);
383            }
384            PropertiesConfiguration.PropertiesReader reader = new PropertiesConfiguration.PropertiesReader(
385                    in, getConfiguration().getListDelimiter());
386    
387            try
388            {
389                while (reader.nextProperty())
390                {
391                    if (getConfiguration().propertyLoaded(reader.getPropertyName(),
392                            reader.getPropertyValue()))
393                    {
394                        boolean contained = layoutData.containsKey(reader
395                                .getPropertyName());
396                        int blancLines = 0;
397                        int idx = checkHeaderComment(reader.getCommentLines());
398                        while (idx < reader.getCommentLines().size()
399                                && ((String) reader.getCommentLines().get(idx))
400                                        .length() < 1)
401                        {
402                            idx++;
403                            blancLines++;
404                        }
405                        String comment = extractComment(reader.getCommentLines(),
406                                idx, reader.getCommentLines().size() - 1);
407                        PropertyLayoutData data = fetchLayoutData(reader
408                                .getPropertyName());
409                        if (contained)
410                        {
411                            data.addComment(comment);
412                            data.setSingleLine(false);
413                        }
414                        else
415                        {
416                            data.setComment(comment);
417                            data.setBlancLines(blancLines);
418                        }
419                    }
420                }
421            }
422            catch (IOException ioex)
423            {
424                throw new ConfigurationException(ioex);
425            }
426            finally
427            {
428                if (--loadCounter == 0)
429                {
430                    getConfiguration().addConfigurationListener(this);
431                }
432            }
433        }
434    
435        /**
436         * Writes the properties file to the given writer, preserving as much of its
437         * structure as possible.
438         *
439         * @param out the writer
440         * @throws ConfigurationException if an error occurs
441         */
442        public void save(Writer out) throws ConfigurationException
443        {
444            try
445            {
446                char delimiter = getConfiguration().isDelimiterParsingDisabled() ? 0
447                        : getConfiguration().getListDelimiter();
448                PropertiesConfiguration.PropertiesWriter writer = new PropertiesConfiguration.PropertiesWriter(
449                        out, delimiter);
450                if (headerComment != null)
451                {
452                    writer.writeln(getCanonicalHeaderComment(true));
453                    writer.writeln(null);
454                }
455    
456                for (Iterator it = layoutData.keySet().iterator(); it.hasNext();)
457                {
458                    String key = (String) it.next();
459                    if (getConfiguration().containsKey(key))
460                    {
461    
462                        // Output blank lines before property
463                        for (int i = 0; i < getBlancLinesBefore(key); i++)
464                        {
465                            writer.writeln(null);
466                        }
467    
468                        // Output the comment
469                        if (getComment(key) != null)
470                        {
471                            writer.writeln(getCanonicalComment(key, true));
472                        }
473    
474                        // Output the property and its value
475                        boolean singleLine = (isForceSingleLine() || isSingleLine(key))
476                                && !getConfiguration().isDelimiterParsingDisabled();
477                        writer.writeProperty(key, getConfiguration().getProperty(
478                                key), singleLine);
479                    }
480                }
481                writer.flush();
482            }
483            catch (IOException ioex)
484            {
485                throw new ConfigurationException(ioex);
486            }
487        }
488    
489        /**
490         * The event listener callback. Here event notifications of the
491         * configuration object are processed to update the layout object properly.
492         *
493         * @param event the event object
494         */
495        public void configurationChanged(ConfigurationEvent event)
496        {
497            if (event.isBeforeUpdate())
498            {
499                if (AbstractFileConfiguration.EVENT_RELOAD == event.getType())
500                {
501                    clear();
502                }
503            }
504    
505            else
506            {
507                switch (event.getType())
508                {
509                case AbstractConfiguration.EVENT_ADD_PROPERTY:
510                    boolean contained = layoutData.containsKey(event
511                            .getPropertyName());
512                    PropertyLayoutData data = fetchLayoutData(event
513                            .getPropertyName());
514                    data.setSingleLine(!contained);
515                    break;
516                case AbstractConfiguration.EVENT_CLEAR_PROPERTY:
517                    layoutData.remove(event.getPropertyName());
518                    break;
519                case AbstractConfiguration.EVENT_CLEAR:
520                    clear();
521                    break;
522                case AbstractConfiguration.EVENT_SET_PROPERTY:
523                    fetchLayoutData(event.getPropertyName());
524                    break;
525                }
526            }
527        }
528    
529        /**
530         * Returns a layout data object for the specified key. If this is a new key,
531         * a new object is created and initialized with default values.
532         *
533         * @param key the key
534         * @return the corresponding layout data object
535         */
536        private PropertyLayoutData fetchLayoutData(String key)
537        {
538            if (key == null)
539            {
540                throw new IllegalArgumentException("Property key must not be null!");
541            }
542    
543            PropertyLayoutData data = (PropertyLayoutData) layoutData.get(key);
544            if (data == null)
545            {
546                data = new PropertyLayoutData();
547                data.setSingleLine(true);
548                layoutData.put(key, data);
549            }
550    
551            return data;
552        }
553    
554        /**
555         * Removes all content from this layout object.
556         */
557        private void clear()
558        {
559            layoutData.clear();
560            setHeaderComment(null);
561        }
562    
563        /**
564         * Tests whether a line is a comment, i.e. whether it starts with a comment
565         * character.
566         *
567         * @param line the line
568         * @return a flag if this is a comment line
569         */
570        static boolean isCommentLine(String line)
571        {
572            return PropertiesConfiguration.isCommentLine(line);
573        }
574    
575        /**
576         * Trims a comment. This method either removes all comment characters from
577         * the given string, leaving only the plain comment text or ensures that
578         * every line starts with a valid comment character.
579         *
580         * @param s the string to be processed
581         * @param comment if <b>true</b>, a comment character will always be
582         * enforced; if <b>false</b>, it will be removed
583         * @return the trimmed comment
584         */
585        static String trimComment(String s, boolean comment)
586        {
587            StringBuffer buf = new StringBuffer(s.length());
588            int lastPos = 0;
589            int pos;
590    
591            do
592            {
593                pos = s.indexOf(CR, lastPos);
594                if (pos >= 0)
595                {
596                    String line = s.substring(lastPos, pos);
597                    buf.append(stripCommentChar(line, comment)).append(CR);
598                    lastPos = pos + CR.length();
599                }
600            } while (pos >= 0);
601    
602            if (lastPos < s.length())
603            {
604                buf.append(stripCommentChar(s.substring(lastPos), comment));
605            }
606            return buf.toString();
607        }
608    
609        /**
610         * Either removes the comment character from the given comment line or
611         * ensures that the line starts with a comment character.
612         *
613         * @param s the comment line
614         * @param comment if <b>true</b>, a comment character will always be
615         * enforced; if <b>false</b>, it will be removed
616         * @return the line without comment character
617         */
618        static String stripCommentChar(String s, boolean comment)
619        {
620            if (s.length() < 1 || (isCommentLine(s) == comment))
621            {
622                return s;
623            }
624    
625            else
626            {
627                if (!comment)
628                {
629                    int pos = 0;
630                    // find first comment character
631                    while (PropertiesConfiguration.COMMENT_CHARS.indexOf(s
632                            .charAt(pos)) < 0)
633                    {
634                        pos++;
635                    }
636    
637                    // Remove leading spaces
638                    pos++;
639                    while (pos < s.length()
640                            && Character.isWhitespace(s.charAt(pos)))
641                    {
642                        pos++;
643                    }
644    
645                    return (pos < s.length()) ? s.substring(pos)
646                            : StringUtils.EMPTY;
647                }
648                else
649                {
650                    return COMMENT_PREFIX + s;
651                }
652            }
653        }
654    
655        /**
656         * Extracts a comment string from the given range of the specified comment
657         * lines. The single lines are added using a line feed as separator.
658         *
659         * @param commentLines a list with comment lines
660         * @param from the start index
661         * @param to the end index (inclusive)
662         * @return the comment string (<b>null</b> if it is undefined)
663         */
664        private String extractComment(List commentLines, int from, int to)
665        {
666            if (to < from)
667            {
668                return null;
669            }
670    
671            else
672            {
673                StringBuffer buf = new StringBuffer((String) commentLines.get(from));
674                for (int i = from + 1; i <= to; i++)
675                {
676                    buf.append(CR);
677                    buf.append(commentLines.get(i));
678                }
679                return buf.toString();
680            }
681        }
682    
683        /**
684         * Checks if parts of the passed in comment can be used as header comment.
685         * This method checks whether a header comment can be defined (i.e. whether
686         * this is the first comment in the loaded file). If this is the case, it is
687         * searched for the lates blanc line. This line will mark the end of the
688         * header comment. The return value is the index of the first line in the
689         * passed in list, which does not belong to the header comment.
690         *
691         * @param commentLines the comment lines
692         * @return the index of the next line after the header comment
693         */
694        private int checkHeaderComment(List commentLines)
695        {
696            if (loadCounter == 1 && getHeaderComment() == null
697                    && layoutData.isEmpty())
698            {
699                // This is the first comment. Search for blanc lines.
700                int index = commentLines.size() - 1;
701                while (index >= 0
702                        && ((String) commentLines.get(index)).length() > 0)
703                {
704                    index--;
705                }
706                setHeaderComment(extractComment(commentLines, 0, index - 1));
707                return index + 1;
708            }
709            else
710            {
711                return 0;
712            }
713        }
714    
715        /**
716         * Copies the data from the given layout object.
717         *
718         * @param c the layout object to copy
719         */
720        private void copyFrom(PropertiesConfigurationLayout c)
721        {
722            for (Iterator it = c.getKeys().iterator(); it.hasNext();)
723            {
724                String key = (String) it.next();
725                PropertyLayoutData data = (PropertyLayoutData) c.layoutData
726                        .get(key);
727                layoutData.put(key, data.clone());
728            }
729        }
730    
731        /**
732         * A helper class for storing all layout related information for a
733         * configuration property.
734         */
735        static class PropertyLayoutData implements Cloneable
736        {
737            /** Stores the comment for the property. */
738            private StringBuffer comment;
739    
740            /** Stores the number of blanc lines before this property. */
741            private int blancLines;
742    
743            /** Stores the single line property. */
744            private boolean singleLine;
745    
746            /**
747             * Creates a new instance of <code>PropertyLayoutData</code>.
748             */
749            public PropertyLayoutData()
750            {
751                singleLine = true;
752            }
753    
754            /**
755             * Returns the number of blanc lines before this property.
756             *
757             * @return the number of blanc lines before this property
758             */
759            public int getBlancLines()
760            {
761                return blancLines;
762            }
763    
764            /**
765             * Sets the number of properties before this property.
766             *
767             * @param blancLines the number of properties before this property
768             */
769            public void setBlancLines(int blancLines)
770            {
771                this.blancLines = blancLines;
772            }
773    
774            /**
775             * Returns the single line flag.
776             *
777             * @return the single line flag
778             */
779            public boolean isSingleLine()
780            {
781                return singleLine;
782            }
783    
784            /**
785             * Sets the single line flag.
786             *
787             * @param singleLine the single line flag
788             */
789            public void setSingleLine(boolean singleLine)
790            {
791                this.singleLine = singleLine;
792            }
793    
794            /**
795             * Adds a comment for this property. If already a comment exists, the
796             * new comment is added (separated by a newline).
797             *
798             * @param s the comment to add
799             */
800            public void addComment(String s)
801            {
802                if (s != null)
803                {
804                    if (comment == null)
805                    {
806                        comment = new StringBuffer(s);
807                    }
808                    else
809                    {
810                        comment.append(CR).append(s);
811                    }
812                }
813            }
814    
815            /**
816             * Sets the comment for this property.
817             *
818             * @param s the new comment (can be <b>null</b>)
819             */
820            public void setComment(String s)
821            {
822                if (s == null)
823                {
824                    comment = null;
825                }
826                else
827                {
828                    comment = new StringBuffer(s);
829                }
830            }
831    
832            /**
833             * Returns the comment for this property. The comment is returned as it
834             * is, without processing of comment characters.
835             *
836             * @return the comment (can be <b>null</b>)
837             */
838            public String getComment()
839            {
840                return (comment == null) ? null : comment.toString();
841            }
842    
843            /**
844             * Creates a copy of this object.
845             *
846             * @return the copy
847             */
848            public Object clone()
849            {
850                try
851                {
852                    PropertyLayoutData copy = (PropertyLayoutData) super.clone();
853                    if (comment != null)
854                    {
855                        // must copy string buffer, too
856                        copy.comment = new StringBuffer(getComment());
857                    }
858                    return copy;
859                }
860                catch (CloneNotSupportedException cnex)
861                {
862                    // This cannot happen!
863                    throw new ConfigurationRuntimeException(cnex);
864                }
865            }
866        }
867    }