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://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 * "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. 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
463 for (int i = 0; i < getBlancLinesBefore(key); i++)
464 {
465 writer.writeln(null);
466 }
467
468
469 if (getComment(key) != null)
470 {
471 writer.writeln(getCanonicalComment(key, true));
472 }
473
474
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
631 while (PropertiesConfiguration.COMMENT_CHARS.indexOf(s
632 .charAt(pos)) < 0)
633 {
634 pos++;
635 }
636
637
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
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
856 copy.comment = new StringBuffer(getComment());
857 }
858 return copy;
859 }
860 catch (CloneNotSupportedException cnex)
861 {
862
863 throw new ConfigurationRuntimeException(cnex);
864 }
865 }
866 }
867 }