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://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 }