View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.configuration;
18  
19  import java.io.IOException;
20  import java.io.Reader;
21  import java.io.Writer;
22  import java.util.Iterator;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Set;
26  
27  import org.apache.commons.collections.map.LinkedMap;
28  import org.apache.commons.configuration.event.ConfigurationEvent;
29  import org.apache.commons.configuration.event.ConfigurationListener;
30  import org.apache.commons.lang.StringUtils;
31  
32  /***
33   * <p>
34   * A helper class used by <code>{@link PropertiesConfiguration}</code> to keep
35   * the layout of a properties file.
36   * </p>
37   * <p>
38   * Instances of this class are associated with a
39   * <code>PropertiesConfiguration</code> object. They are responsible for
40   * analyzing properties files and for extracting as much information about the
41   * file layout (e.g. empty lines, comments) as possible. When the properties
42   * file is written back again it should be close to the original.
43   * </p>
44   * <p>
45   * The <code>PropertiesConfigurationLayout</code> object associated with a
46   * <code>PropertiesConfiguration</code> object can be obtained using the
47   * <code>getLayout()</code> method of the configuration. Then the methods
48   * provided by this class can be used to alter the properties file's layout.
49   * </p>
50   * <p>
51   * Implementation note: This is a very simple implementation, which is far away
52   * from being perfect, i.e. the original layout of a properties file won't be
53   * reproduced in all cases. One limitation is that comments for multi-valued
54   * property keys are concatenated. Maybe this implementation can later be
55   * improved.
56   * </p>
57   * <p>
58   * To get an impression how this class works consider the following properties
59   * file:
60   * </p>
61   * <p>
62   *
63   * <pre>
64   * # A demo configuration file
65   * # for Demo App 1.42
66   *
67   * # Application name
68   * AppName=Demo App
69   *
70   * # Application vendor
71   * AppVendor=DemoSoft
72   *
73   *
74   * # GUI properties
75   * # Window Color
76   * windowColors=0xFFFFFF,0x000000
77   *
78   * # Include some setting
79   * include=settings.properties
80   * # Another vendor
81   * AppVendor=TestSoft
82   * </pre>
83   *
84   * </p>
85   * <p>
86   * For this example the following points are relevant:
87   * </p>
88   * <p>
89   * <ul>
90   * <li>The first two lines are set as header comment. The header comment is
91   * determined by the last blanc line before the first property definition.</li>
92   * <li>For the property <code>AppName</code> one comment line and one
93   * leading blanc line is stored.</li>
94   * <li>For the property <code>windowColors</code> two comment lines and two
95   * leading blanc lines are stored.</li>
96   * <li>Include files is something this class cannot deal with well. When saving
97   * the properties configuration back, the included properties are simply
98   * contained in the original file. The comment before the include property is
99   * 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://jakarta.apache.org/commons/configuration/team-list.html">Commons
114  * Configuration team</a>
115  * @version $Id: PropertiesConfigurationLayout.java 439648 2006-09-02 20:42:10Z 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.
352      *
353      * @param f the force single line flag
354      */
355     public void setForceSingleLine(boolean f)
356     {
357         forceSingleLine = f;
358     }
359 
360     /***
361      * Returns a set with all property keys managed by this object.
362      *
363      * @return a set with all contained property keys
364      */
365     public Set getKeys()
366     {
367         return layoutData.keySet();
368     }
369 
370     /***
371      * Reads a properties file and stores its internal structure. The found
372      * properties will be added to the associated configuration object.
373      *
374      * @param in the reader to the properties file
375      * @throws ConfigurationException if an error occurs
376      */
377     public void load(Reader in) throws ConfigurationException
378     {
379         if (++loadCounter == 1)
380         {
381             getConfiguration().removeConfigurationListener(this);
382         }
383         PropertiesConfiguration.PropertiesReader reader = new PropertiesConfiguration.PropertiesReader(
384                 in, getConfiguration().getListDelimiter());
385 
386         try
387         {
388             while (reader.nextProperty())
389             {
390                 if (getConfiguration().propertyLoaded(reader.getPropertyName(),
391                         reader.getPropertyValue()))
392                 {
393                     boolean contained = layoutData.containsKey(reader
394                             .getPropertyName());
395                     int blancLines = 0;
396                     int idx = checkHeaderComment(reader.getCommentLines());
397                     while (idx < reader.getCommentLines().size()
398                             && ((String) reader.getCommentLines().get(idx))
399                                     .length() < 1)
400                     {
401                         idx++;
402                         blancLines++;
403                     }
404                     String comment = extractComment(reader.getCommentLines(),
405                             idx, reader.getCommentLines().size() - 1);
406                     PropertyLayoutData data = fetchLayoutData(reader
407                             .getPropertyName());
408                     if (contained)
409                     {
410                         data.addComment(comment);
411                         data.setSingleLine(false);
412                     }
413                     else
414                     {
415                         data.setComment(comment);
416                         data.setBlancLines(blancLines);
417                     }
418                 }
419             }
420         }
421         catch (IOException ioex)
422         {
423             throw new ConfigurationException(ioex);
424         }
425         finally
426         {
427             if (--loadCounter == 0)
428             {
429                 getConfiguration().addConfigurationListener(this);
430             }
431         }
432     }
433 
434     /***
435      * Writes the properties file to the given writer, preserving as much of its
436      * structure as possible.
437      *
438      * @param out the writer
439      * @throws ConfigurationException if an error occurs
440      */
441     public void save(Writer out) throws ConfigurationException
442     {
443         try
444         {
445             PropertiesConfiguration.PropertiesWriter writer = new PropertiesConfiguration.PropertiesWriter(
446                     out, getConfiguration().getListDelimiter());
447             if (headerComment != null)
448             {
449                 writer.writeln(getCanonicalHeaderComment(true));
450                 writer.writeln(null);
451             }
452 
453             for (Iterator it = layoutData.keySet().iterator(); it.hasNext();)
454             {
455                 String key = (String) it.next();
456                 if (getConfiguration().containsKey(key))
457                 {
458 
459                     // Output blanc lines before property
460                     for (int i = 0; i < getBlancLinesBefore(key); i++)
461                     {
462                         writer.writeln(null);
463                     }
464 
465                     // Output the comment
466                     if (getComment(key) != null)
467                     {
468                         writer.writeln(getCanonicalComment(key, true));
469                     }
470 
471                     // Output the property and its value
472                     writer.writeProperty(key, getConfiguration().getProperty(
473                             key), isForceSingleLine() || isSingleLine(key));
474                 }
475             }
476             writer.flush();
477         }
478         catch (IOException ioex)
479         {
480             throw new ConfigurationException(ioex);
481         }
482     }
483 
484     /***
485      * The event listener callback. Here event notifications of the
486      * configuration object are processed to update the layout object properly.
487      *
488      * @param event the event object
489      */
490     public void configurationChanged(ConfigurationEvent event)
491     {
492         if (event.isBeforeUpdate())
493         {
494             if (AbstractFileConfiguration.EVENT_RELOAD == event.getType())
495             {
496                 clear();
497             }
498         }
499 
500         else
501         {
502             switch (event.getType())
503             {
504             case AbstractConfiguration.EVENT_ADD_PROPERTY:
505                 boolean contained = layoutData.containsKey(event
506                         .getPropertyName());
507                 PropertyLayoutData data = fetchLayoutData(event
508                         .getPropertyName());
509                 data.setSingleLine(!contained);
510                 break;
511             case AbstractConfiguration.EVENT_CLEAR_PROPERTY:
512                 layoutData.remove(event.getPropertyName());
513                 break;
514             case AbstractConfiguration.EVENT_CLEAR:
515                 clear();
516                 break;
517             case AbstractConfiguration.EVENT_SET_PROPERTY:
518                 fetchLayoutData(event.getPropertyName());
519                 break;
520             }
521         }
522     }
523 
524     /***
525      * Returns a layout data object for the specified key. If this is a new key,
526      * a new object is created and initialized with default values.
527      *
528      * @param key the key
529      * @return the corresponding layout data object
530      */
531     private PropertyLayoutData fetchLayoutData(String key)
532     {
533         if (key == null)
534         {
535             throw new IllegalArgumentException("Property key must not be null!");
536         }
537 
538         PropertyLayoutData data = (PropertyLayoutData) layoutData.get(key);
539         if (data == null)
540         {
541             data = new PropertyLayoutData();
542             data.setSingleLine(true);
543             layoutData.put(key, data);
544         }
545 
546         return data;
547     }
548 
549     /***
550      * Removes all content from this layout object.
551      */
552     private void clear()
553     {
554         layoutData.clear();
555         setHeaderComment(null);
556     }
557 
558     /***
559      * Tests whether a line is a comment, i.e. whether it starts with a comment
560      * character.
561      *
562      * @param line the line
563      * @return a flag if this is a comment line
564      */
565     static boolean isCommentLine(String line)
566     {
567         return PropertiesConfiguration.isCommentLine(line);
568     }
569 
570     /***
571      * Trims a comment. This method either removes all comment characters from
572      * the given string, leaving only the plain comment text or ensures that
573      * every line starts with a valid comment character.
574      *
575      * @param s the string to be processed
576      * @param comment if <b>true</b>, a comment character will always be
577      * enforced; if <b>false</b>, it will be removed
578      * @return the trimmed comment
579      */
580     static String trimComment(String s, boolean comment)
581     {
582         StringBuffer buf = new StringBuffer(s.length());
583         int lastPos = 0;
584         int pos;
585 
586         do
587         {
588             pos = s.indexOf(CR, lastPos);
589             if (pos >= 0)
590             {
591                 String line = s.substring(lastPos, pos);
592                 buf.append(stripCommentChar(line, comment)).append(CR);
593                 lastPos = pos + CR.length();
594             }
595         } while (pos >= 0);
596 
597         if (lastPos < s.length())
598         {
599             buf.append(stripCommentChar(s.substring(lastPos), comment));
600         }
601         return buf.toString();
602     }
603 
604     /***
605      * Either removes the comment character from the given comment line or
606      * ensures that the line starts with a comment character.
607      *
608      * @param s the comment line
609      * @param comment if <b>true</b>, a comment character will always be
610      * enforced; if <b>false</b>, it will be removed
611      * @return the line without comment character
612      */
613     static String stripCommentChar(String s, boolean comment)
614     {
615         if (s.length() < 1 || (isCommentLine(s) == comment))
616         {
617             return s;
618         }
619 
620         else
621         {
622             if (!comment)
623             {
624                 int pos = 0;
625                 // find first comment character
626                 while (PropertiesConfiguration.COMMENT_CHARS.indexOf(s
627                         .charAt(pos)) < 0)
628                 {
629                     pos++;
630                 }
631 
632                 // Remove leading spaces
633                 pos++;
634                 while (pos < s.length()
635                         && Character.isWhitespace(s.charAt(pos)))
636                 {
637                     pos++;
638                 }
639 
640                 return (pos < s.length()) ? s.substring(pos)
641                         : StringUtils.EMPTY;
642             }
643             else
644             {
645                 return COMMENT_PREFIX + s;
646             }
647         }
648     }
649 
650     /***
651      * Extracts a comment string from the given range of the specified comment
652      * lines. The single lines are added using a line feed as separator.
653      *
654      * @param commentLines a list with comment lines
655      * @param from the start index
656      * @param to the end index (inclusive)
657      * @return the comment string (<b>null</b> if it is undefined)
658      */
659     private String extractComment(List commentLines, int from, int to)
660     {
661         if (to < from)
662         {
663             return null;
664         }
665 
666         else
667         {
668             StringBuffer buf = new StringBuffer((String) commentLines.get(from));
669             for (int i = from + 1; i <= to; i++)
670             {
671                 buf.append(CR);
672                 buf.append(commentLines.get(i));
673             }
674             return buf.toString();
675         }
676     }
677 
678     /***
679      * Checks if parts of the passed in comment can be used as header comment.
680      * This method checks whether a header comment can be defined (i.e. whether
681      * this is the first comment in the loaded file). If this is the case, it is
682      * searched for the lates blanc line. This line will mark the end of the
683      * header comment. The return value is the index of the first line in the
684      * passed in list, which does not belong to the header comment.
685      *
686      * @param commentLines the comment lines
687      * @return the index of the next line after the header comment
688      */
689     private int checkHeaderComment(List commentLines)
690     {
691         if (loadCounter == 1 && getHeaderComment() == null
692                 && layoutData.isEmpty())
693         {
694             // This is the first comment. Search for blanc lines.
695             int index = commentLines.size() - 1;
696             while (index >= 0
697                     && ((String) commentLines.get(index)).length() > 0)
698             {
699                 index--;
700             }
701             setHeaderComment(extractComment(commentLines, 0, index - 1));
702             return index + 1;
703         }
704         else
705         {
706             return 0;
707         }
708     }
709 
710     /***
711      * Copies the data from the given layout object.
712      *
713      * @param c the layout object to copy
714      */
715     private void copyFrom(PropertiesConfigurationLayout c)
716     {
717         for (Iterator it = c.getKeys().iterator(); it.hasNext();)
718         {
719             String key = (String) it.next();
720             PropertyLayoutData data = (PropertyLayoutData) c.layoutData
721                     .get(key);
722             layoutData.put(key, data.clone());
723         }
724     }
725 
726     /***
727      * A helper class for storing all layout related information for a
728      * configuration property.
729      */
730     static class PropertyLayoutData implements Cloneable
731     {
732         /*** Stores the comment for the property. */
733         private StringBuffer comment;
734 
735         /*** Stores the number of blanc lines before this property. */
736         private int blancLines;
737 
738         /*** Stores the single line property. */
739         private boolean singleLine;
740 
741         /***
742          * Creates a new instance of <code>PropertyLayoutData</code>.
743          */
744         public PropertyLayoutData()
745         {
746             singleLine = true;
747         }
748 
749         /***
750          * Returns the number of blanc lines before this property.
751          *
752          * @return the number of blanc lines before this property
753          */
754         public int getBlancLines()
755         {
756             return blancLines;
757         }
758 
759         /***
760          * Sets the number of properties before this property.
761          *
762          * @param blancLines the number of properties before this property
763          */
764         public void setBlancLines(int blancLines)
765         {
766             this.blancLines = blancLines;
767         }
768 
769         /***
770          * Returns the single line flag.
771          *
772          * @return the single line flag
773          */
774         public boolean isSingleLine()
775         {
776             return singleLine;
777         }
778 
779         /***
780          * Sets the single line flag.
781          *
782          * @param singleLine the single line flag
783          */
784         public void setSingleLine(boolean singleLine)
785         {
786             this.singleLine = singleLine;
787         }
788 
789         /***
790          * Adds a comment for this property. If already a comment exists, the
791          * new comment is added (separated by a newline).
792          *
793          * @param s the comment to add
794          */
795         public void addComment(String s)
796         {
797             if (s != null)
798             {
799                 if (comment == null)
800                 {
801                     comment = new StringBuffer(s);
802                 }
803                 else
804                 {
805                     comment.append(CR).append(s);
806                 }
807             }
808         }
809 
810         /***
811          * Sets the comment for this property.
812          *
813          * @param s the new comment (can be <b>null</b>)
814          */
815         public void setComment(String s)
816         {
817             if (s == null)
818             {
819                 comment = null;
820             }
821             else
822             {
823                 comment = new StringBuffer(s);
824             }
825         }
826 
827         /***
828          * Returns the comment for this property. The comment is returned as it
829          * is, without processing of comment characters.
830          *
831          * @return the comment (can be <b>null</b>)
832          */
833         public String getComment()
834         {
835             return (comment == null) ? null : comment.toString();
836         }
837 
838         /***
839          * Creates a copy of this object.
840          *
841          * @return the copy
842          */
843         public Object clone()
844         {
845             try
846             {
847                 PropertyLayoutData copy = (PropertyLayoutData) super.clone();
848                 if (comment != null)
849                 {
850                     // must copy string buffer, too
851                     copy.comment = new StringBuffer(getComment());
852                 }
853                 return copy;
854             }
855             catch (CloneNotSupportedException cnex)
856             {
857                 // This cannot happen!
858                 throw new ConfigurationRuntimeException(cnex);
859             }
860         }
861     }
862 }