1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 "single
101 * line" 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<CR>Another vendor</code>, whith
106 * <code><CR></code> meaning the line separator. In addition the
107 * "single line" 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 * "Canonical" 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 "single line flag" 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 "force single line" 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 "force single line" 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
460 for (int i = 0; i < getBlancLinesBefore(key); i++)
461 {
462 writer.writeln(null);
463 }
464
465
466 if (getComment(key) != null)
467 {
468 writer.writeln(getCanonicalComment(key, true));
469 }
470
471
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
626 while (PropertiesConfiguration.COMMENT_CHARS.indexOf(s
627 .charAt(pos)) < 0)
628 {
629 pos++;
630 }
631
632
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
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
851 copy.comment = new StringBuffer(getComment());
852 }
853 return copy;
854 }
855 catch (CloneNotSupportedException cnex)
856 {
857
858 throw new ConfigurationRuntimeException(cnex);
859 }
860 }
861 }
862 }