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.BufferedReader;
20 import java.io.File;
21 import java.io.FilterWriter;
22 import java.io.IOException;
23 import java.io.LineNumberReader;
24 import java.io.Reader;
25 import java.io.StringReader;
26 import java.io.Writer;
27 import java.net.URL;
28 import java.util.Date;
29 import java.util.Iterator;
30 import java.util.List;
31
32 import org.apache.commons.lang.ArrayUtils;
33 import org.apache.commons.lang.StringEscapeUtils;
34 import org.apache.commons.lang.StringUtils;
35
36 /***
37 * This is the "classic" Properties loader which loads the values from
38 * a single or multiple files (which can be chained with "include =".
39 * All given path references are either absolute or relative to the
40 * file name supplied in the constructor.
41 * <p>
42 * In this class, empty PropertyConfigurations can be built, properties
43 * added and later saved. include statements are (obviously) not supported
44 * if you don't construct a PropertyConfiguration from a file.
45 *
46 * <p>The properties file syntax is explained here, basically it follows
47 * the syntax of the stream parsed by {@link java.util.Properties#load} and
48 * adds several useful extensions:
49 *
50 * <ul>
51 * <li>
52 * Each property has the syntax <code>key <separator> value</code>. The
53 * separators accepted are <code>'='</code>, <code>':'</code> and any white
54 * space character. Examples:
55 * <pre>
56 * key1 = value1
57 * key2 : value2
58 * key3 value3</pre>
59 * </li>
60 * <li>
61 * The <i>key</i> may use any character, separators must be escaped:
62 * <pre>
63 * key\:foo = bar</pre>
64 * </li>
65 * <li>
66 * <i>value</i> may be separated on different lines if a backslash
67 * is placed at the end of the line that continues below.
68 * </li>
69 * <li>
70 * <i>value</i> can contain <em>value delimiters</em> and will then be interpreted
71 * as a list of tokens. Default value delimiter is the comma ','. So the
72 * following property definition
73 * <pre>
74 * key = This property, has multiple, values
75 * </pre>
76 * will result in a property with three values. You can change the value
77 * delmiter using the <code>{@link AbstractConfiguration#setDelimiter(char)}</code>
78 * method. Setting the delimiter to 0 will disable value splitting completely.
79 * </li>
80 * <li>
81 * Commas in each token are escaped placing a backslash right before
82 * the comma.
83 * </li>
84 * <li>
85 * If a <i>key</i> is used more than once, the values are appended
86 * like if they were on the same line separated with commas.
87 * </li>
88 * <li>
89 * Blank lines and lines starting with character '#' or '!' are skipped.
90 * </li>
91 * <li>
92 * If a property is named "include" (or whatever is defined by
93 * setInclude() and getInclude() and the value of that property is
94 * the full path to a file on disk, that file will be included into
95 * the configuration. You can also pull in files relative to the parent
96 * configuration file. So if you have something like the following:
97 *
98 * include = additional.properties
99 *
100 * Then "additional.properties" is expected to be in the same
101 * directory as the parent configuration file.
102 *
103 * The properties in the included file are added to the parent configuration,
104 * they do not replace existing properties with the same key.
105 *
106 * </li>
107 * </ul>
108 *
109 * <p>Here is an example of a valid extended properties file:
110 *
111 * <p><pre>
112 * # lines starting with # are comments
113 *
114 * # This is the simplest property
115 * key = value
116 *
117 * # A long property may be separated on multiple lines
118 * longvalue = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
119 * aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
120 *
121 * # This is a property with many tokens
122 * tokens_on_a_line = first token, second token
123 *
124 * # This sequence generates exactly the same result
125 * tokens_on_multiple_lines = first token
126 * tokens_on_multiple_lines = second token
127 *
128 * # commas may be escaped in tokens
129 * commas.escaped = Hi\, what'up?
130 *
131 * # properties can reference other properties
132 * base.prop = /base
133 * first.prop = ${base.prop}/first
134 * second.prop = ${first.prop}/second
135 * </pre>
136 *
137 * @see java.util.Properties#load
138 *
139 * @author <a href="mailto:stefano@apache.org">Stefano Mazzocchi</a>
140 * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a>
141 * @author <a href="mailto:daveb@miceda-data">Dave Bryson</a>
142 * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
143 * @author <a href="mailto:leon@opticode.co.za">Leon Messerschmidt</a>
144 * @author <a href="mailto:kjohnson@transparent.com">Kent Johnson</a>
145 * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
146 * @author <a href="mailto:ipriha@surfeu.fi">Ilkka Priha</a>
147 * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
148 * @author <a href="mailto:mpoeschl@marmot.at">Martin Poeschl</a>
149 * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
150 * @author <a href="mailto:epugh@upstate.com">Eric Pugh</a>
151 * @author <a href="mailto:oliver.heger@t-online.de">Oliver Heger</a>
152 * @author <a href="mailto:ebourg@apache.org">Emmanuel Bourg</a>
153 * @version $Id: PropertiesConfiguration.java 354264 2005-12-06 03:10:27Z ebourg $
154 */
155 public class PropertiesConfiguration extends AbstractFileConfiguration
156 {
157 /***
158 * This is the name of the property that can point to other
159 * properties file for including other properties files.
160 */
161 private static String include = "include";
162
163 /*** The list of possible key/value separators */
164 private static final char[] SEPARATORS = new char[] {'=', ':'};
165
166 /*** The white space characters used as key/value separators. */
167 private static final char[] WHITE_SPACE = new char[]{' ', '\t', '\f'};
168
169 /***
170 * The default encoding (ISO-8859-1 as specified by
171 * http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html)
172 */
173 private static final String DEFAULT_ENCODING = "ISO-8859-1";
174
175 /*** Constant for the platform specific line separator.*/
176 private static final String LINE_SEPARATOR = System.getProperty("line.separator");
177
178 /*** Constant for the radix of hex numbers.*/
179 private static final int HEX_RADIX = 16;
180
181 /*** Constant for the length of a unicode literal.*/
182 private static final int UNICODE_LEN = 4;
183
184 /*** Allow file inclusion or not */
185 private boolean includesAllowed;
186
187 /*** Comment header of the .properties file */
188 private String header;
189
190
191 {
192 setEncoding(DEFAULT_ENCODING);
193 }
194
195 /***
196 * Creates an empty PropertyConfiguration object which can be
197 * used to synthesize a new Properties file by adding values and
198 * then saving().
199 */
200 public PropertiesConfiguration()
201 {
202 setIncludesAllowed(false);
203 }
204
205 /***
206 * Creates and loads the extended properties from the specified file.
207 * The specified file can contain "include = " properties which then
208 * are loaded and merged into the properties.
209 *
210 * @param fileName The name of the properties file to load.
211 * @throws ConfigurationException Error while loading the properties file
212 */
213 public PropertiesConfiguration(String fileName) throws ConfigurationException
214 {
215 super(fileName);
216 }
217
218 /***
219 * Creates and loads the extended properties from the specified file.
220 * The specified file can contain "include = " properties which then
221 * are loaded and merged into the properties.
222 *
223 * @param file The properties file to load.
224 * @throws ConfigurationException Error while loading the properties file
225 */
226 public PropertiesConfiguration(File file) throws ConfigurationException
227 {
228 super(file);
229 }
230
231 /***
232 * Creates and loads the extended properties from the specified URL.
233 * The specified file can contain "include = " properties which then
234 * are loaded and merged into the properties.
235 *
236 * @param url The location of the properties file to load.
237 * @throws ConfigurationException Error while loading the properties file
238 */
239 public PropertiesConfiguration(URL url) throws ConfigurationException
240 {
241 super(url);
242 }
243
244 /***
245 * Gets the property value for including other properties files.
246 * By default it is "include".
247 *
248 * @return A String.
249 */
250 public static String getInclude()
251 {
252 return PropertiesConfiguration.include;
253 }
254
255 /***
256 * Sets the property value for including other properties files.
257 * By default it is "include".
258 *
259 * @param inc A String.
260 */
261 public static void setInclude(String inc)
262 {
263 PropertiesConfiguration.include = inc;
264 }
265
266 /***
267 * Controls whether additional files can be loaded by the include = <xxx>
268 * statement or not. Base rule is, that objects created by the empty
269 * C'tor can not have included files.
270 *
271 * @param includesAllowed includesAllowed True if Includes are allowed.
272 */
273 protected void setIncludesAllowed(boolean includesAllowed)
274 {
275 this.includesAllowed = includesAllowed;
276 }
277
278 /***
279 * Reports the status of file inclusion.
280 *
281 * @return True if include files are loaded.
282 */
283 public boolean getIncludesAllowed()
284 {
285 return this.includesAllowed;
286 }
287
288 /***
289 * Return the comment header.
290 *
291 * @return the comment header
292 * @since 1.1
293 */
294 public String getHeader()
295 {
296 return header;
297 }
298
299 /***
300 * Set the comment header.
301 *
302 * @param header the header to use
303 * @since 1.1
304 */
305 public void setHeader(String header)
306 {
307 this.header = header;
308 }
309
310 /***
311 * Load the properties from the given reader.
312 * Note that the <code>clear()</code> method is not called, so
313 * the properties contained in the loaded file will be added to the
314 * actual set of properties.
315 *
316 * @param in An InputStream.
317 *
318 * @throws ConfigurationException if an error occurs
319 */
320 public synchronized void load(Reader in) throws ConfigurationException
321 {
322 PropertiesReader reader = new PropertiesReader(in);
323 boolean oldAutoSave = isAutoSave();
324 setAutoSave(false);
325
326 try
327 {
328 while (true)
329 {
330 String line = reader.readProperty();
331
332 if (line == null)
333 {
334 break;
335 }
336
337
338 String[] property = parseProperty(line);
339 String key = property[0];
340 String value = property[1];
341
342
343
344
345
346
347 if (StringUtils.isNotEmpty(getInclude()) && key.equalsIgnoreCase(getInclude()))
348 {
349 if (getIncludesAllowed())
350 {
351 String [] files = StringUtils.split(value, getDelimiter());
352 for (int i = 0; i < files.length; i++)
353 {
354 loadIncludeFile(files[i].trim());
355 }
356 }
357 }
358 else
359 {
360 addProperty(StringEscapeUtils.unescapeJava(key), unescapeJava(value, getDelimiter()));
361 }
362
363 }
364 }
365 catch (IOException ioe)
366 {
367 throw new ConfigurationException("Could not load configuration from input stream.", ioe);
368 }
369 finally
370 {
371 setAutoSave(oldAutoSave);
372 }
373 }
374
375 /***
376 * Save the configuration to the specified stream.
377 *
378 * @param writer the output stream used to save the configuration
379 * @throws ConfigurationException if an error occurs
380 */
381 public void save(Writer writer) throws ConfigurationException
382 {
383 enterNoReload();
384 try
385 {
386 PropertiesWriter out = new PropertiesWriter(writer, getDelimiter());
387
388 if (header != null)
389 {
390 BufferedReader reader = new BufferedReader(new StringReader(header));
391 String line;
392 while ((line = reader.readLine()) != null)
393 {
394 out.writeComment(line);
395 }
396 out.writeln(null);
397 }
398
399 out.writeComment("written by PropertiesConfiguration");
400 out.writeComment(new Date().toString());
401 out.writeln(null);
402
403 Iterator keys = getKeys();
404 while (keys.hasNext())
405 {
406 String key = (String) keys.next();
407 Object value = getProperty(key);
408
409 if (value instanceof List)
410 {
411 out.writeProperty(key, (List) value);
412 }
413 else
414 {
415 out.writeProperty(key, value);
416 }
417 }
418
419 out.flush();
420 }
421 catch (IOException e)
422 {
423 throw new ConfigurationException(e.getMessage(), e);
424 }
425 finally
426 {
427 exitNoReload();
428 }
429 }
430
431 /***
432 * Extend the setBasePath method to turn includes
433 * on and off based on the existence of a base path.
434 *
435 * @param basePath The new basePath to set.
436 */
437 public void setBasePath(String basePath)
438 {
439 super.setBasePath(basePath);
440 setIncludesAllowed(StringUtils.isNotEmpty(basePath));
441 }
442
443 /***
444 * This class is used to read properties lines. These lines do
445 * not terminate with new-line chars but rather when there is no
446 * backslash sign a the end of the line. This is used to
447 * concatenate multiple lines for readability.
448 */
449 public static class PropertiesReader extends LineNumberReader
450 {
451 /***
452 * Constructor.
453 *
454 * @param reader A Reader.
455 */
456 public PropertiesReader(Reader reader)
457 {
458 super(reader);
459 }
460
461 /***
462 * Read a property. Returns null if Stream is
463 * at EOF. Concatenates lines ending with "\".
464 * Skips lines beginning with "#" or "!" and empty lines.
465 *
466 * @return A string containing a property value or null
467 *
468 * @throws IOException in case of an I/O error
469 */
470 public String readProperty() throws IOException
471 {
472 StringBuffer buffer = new StringBuffer();
473
474 while (true)
475 {
476 String line = readLine();
477 if (line == null)
478 {
479
480 return null;
481 }
482
483 line = line.trim();
484
485
486 if (StringUtils.isEmpty(line) || (line.charAt(0) == '#') || (line.charAt(0) == '!'))
487 {
488 continue;
489 }
490
491 if (checkCombineLines(line))
492 {
493 line = line.substring(0, line.length() - 1);
494 buffer.append(line);
495 }
496 else
497 {
498 buffer.append(line);
499 break;
500 }
501 }
502 return buffer.toString();
503 }
504
505 /***
506 * Checks if the passed in line should be combined with the following.
507 * This is true, if the line ends with an odd number of backslashes.
508 *
509 * @param line the line
510 * @return a flag if the lines should be combined
511 */
512 private static boolean checkCombineLines(String line)
513 {
514 int bsCount = 0;
515 for (int idx = line.length() - 1; idx >= 0 && line.charAt(idx) == '//'; idx--)
516 {
517 bsCount++;
518 }
519
520 return bsCount % 2 == 1;
521 }
522 }
523
524 /***
525 * This class is used to write properties lines.
526 */
527 public static class PropertiesWriter extends FilterWriter
528 {
529 /*** The delimiter for multi-valued properties.*/
530 private char delimiter;
531
532 /***
533 * Constructor.
534 *
535 * @param writer a Writer object providing the underlying stream
536 * @param delimiter the delimiter character for multi-valued properties
537 */
538 public PropertiesWriter(Writer writer, char delimiter)
539 {
540 super(writer);
541 this.delimiter = delimiter;
542 }
543
544 /***
545 * Write a property.
546 *
547 * @param key the key of the property
548 * @param value the value of the property
549 *
550 * @throws IOException if an I/O error occurs
551 */
552 public void writeProperty(String key, Object value) throws IOException
553 {
554 write(escapeKey(key));
555 write(" = ");
556 if (value != null)
557 {
558 String v = StringEscapeUtils.escapeJava(String.valueOf(value));
559 v = StringUtils.replace(v, String.valueOf(delimiter), "//" + delimiter);
560 write(v);
561 }
562
563 writeln(null);
564 }
565
566 /***
567 * Write a property.
568 *
569 * @param key The key of the property
570 * @param values The array of values of the property
571 *
572 * @throws IOException if an I/O error occurs
573 */
574 public void writeProperty(String key, List values) throws IOException
575 {
576 for (int i = 0; i < values.size(); i++)
577 {
578 writeProperty(key, values.get(i));
579 }
580 }
581
582 /***
583 * Write a comment.
584 *
585 * @param comment the comment to write
586 * @throws IOException if an I/O error occurs
587 */
588 public void writeComment(String comment) throws IOException
589 {
590 writeln("# " + comment);
591 }
592
593 /***
594 * Escape the separators in the key.
595 *
596 * @param key the key
597 * @return the escaped key
598 * @since 1.2
599 */
600 private String escapeKey(String key)
601 {
602 StringBuffer newkey = new StringBuffer();
603
604 for (int i = 0; i < key.length(); i++)
605 {
606 char c = key.charAt(i);
607
608 if (ArrayUtils.contains(SEPARATORS, c) || ArrayUtils.contains(WHITE_SPACE, c))
609 {
610
611 newkey.append('//');
612 newkey.append(c);
613 }
614 else
615 {
616 newkey.append(c);
617 }
618 }
619
620 return newkey.toString();
621 }
622
623 /***
624 * Helper method for writing a line with the platform specific line
625 * ending.
626 *
627 * @param s the content of the line (may be <b>null</b>)
628 * @throws IOException if an error occurs
629 */
630 private void writeln(String s) throws IOException
631 {
632 if (s != null)
633 {
634 write(s);
635 }
636 write(LINE_SEPARATOR);
637 }
638
639 }
640
641 /***
642 * <p>Unescapes any Java literals found in the <code>String</code> to a
643 * <code>Writer</code>.</p> This is a slightly modified version of the
644 * StringEscapeUtils.unescapeJava() function in commons-lang that doesn't
645 * drop escaped separators (i.e '\,').
646 *
647 * @param str the <code>String</code> to unescape, may be null
648 * @param delimiter the delimiter for multi-valued properties
649 * @return the processed string
650 * @throws IllegalArgumentException if the Writer is <code>null</code>
651 */
652 protected static String unescapeJava(String str, char delimiter)
653 {
654 if (str == null)
655 {
656 return null;
657 }
658 int sz = str.length();
659 StringBuffer out = new StringBuffer(sz);
660 StringBuffer unicode = new StringBuffer(UNICODE_LEN);
661 boolean hadSlash = false;
662 boolean inUnicode = false;
663 for (int i = 0; i < sz; i++)
664 {
665 char ch = str.charAt(i);
666 if (inUnicode)
667 {
668
669
670 unicode.append(ch);
671 if (unicode.length() == UNICODE_LEN)
672 {
673
674
675 try
676 {
677 int value = Integer.parseInt(unicode.toString(), HEX_RADIX);
678 out.append((char) value);
679 unicode.setLength(0);
680 inUnicode = false;
681 hadSlash = false;
682 }
683 catch (NumberFormatException nfe)
684 {
685 throw new ConfigurationRuntimeException("Unable to parse unicode value: " + unicode, nfe);
686 }
687 }
688 continue;
689 }
690
691 if (hadSlash)
692 {
693
694 hadSlash = false;
695
696 if (ch == '//')
697 {
698 out.append('//');
699 }
700 else if (ch == '\'')
701 {
702 out.append('\'');
703 }
704 else if (ch == '\"')
705 {
706 out.append('"');
707 }
708 else if (ch == 'r')
709 {
710 out.append('\r');
711 }
712 else if (ch == 'f')
713 {
714 out.append('\f');
715 }
716 else if (ch == 't')
717 {
718 out.append('\t');
719 }
720 else if (ch == 'n')
721 {
722 out.append('\n');
723 }
724 else if (ch == 'b')
725 {
726 out.append('\b');
727 }
728 else if (ch == delimiter)
729 {
730 out.append('//');
731 out.append(delimiter);
732 }
733 else if (ch == 'u')
734 {
735
736 inUnicode = true;
737 }
738 else
739 {
740 out.append(ch);
741 }
742
743 continue;
744 }
745 else if (ch == '//')
746 {
747 hadSlash = true;
748 continue;
749 }
750 out.append(ch);
751 }
752
753 if (hadSlash)
754 {
755
756
757 out.append('//');
758 }
759
760 return out.toString();
761 }
762
763 /***
764 * Parse a property line and return the key and the value in an array.
765 *
766 * @param line the line to parse
767 * @return an array with the property's key and value
768 * @since 1.2
769 */
770 private String[] parseProperty(String line)
771 {
772
773
774
775 String[] result = new String[2];
776 StringBuffer key = new StringBuffer();
777 StringBuffer value = new StringBuffer();
778
779
780
781
782
783
784 int state = 0;
785
786 for (int pos = 0; pos < line.length(); pos++)
787 {
788 char c = line.charAt(pos);
789
790 switch (state)
791 {
792 case 0:
793 if (c == '//')
794 {
795 state = 1;
796 }
797 else if (ArrayUtils.contains(WHITE_SPACE, c))
798 {
799
800 state = 2;
801 }
802 else if (ArrayUtils.contains(SEPARATORS, c))
803 {
804
805 state = 3;
806 }
807 else
808 {
809 key.append(c);
810 }
811
812 break;
813
814 case 1:
815 if (ArrayUtils.contains(SEPARATORS, c) || ArrayUtils.contains(WHITE_SPACE, c))
816 {
817
818 key.append(c);
819 }
820 else
821 {
822
823 key.append('//');
824 key.append(c);
825 }
826
827
828 state = 0;
829
830 break;
831
832 case 2:
833 if (ArrayUtils.contains(WHITE_SPACE, c))
834 {
835
836 state = 2;
837 }
838 else if (ArrayUtils.contains(SEPARATORS, c))
839 {
840
841 state = 3;
842 }
843 else
844 {
845
846 value.append(c);
847
848
849 state = 3;
850 }
851
852 break;
853
854 case 3:
855 value.append(c);
856 break;
857 }
858 }
859
860 result[0] = key.toString().trim();
861 result[1] = value.toString().trim();
862
863 return result;
864 }
865
866 /***
867 * Helper method for loading an included properties file. This method is
868 * called by <code>load()</code> when an <code>include</code> property
869 * is encountered. It tries to resolve relative file names based on the
870 * current base path. If this fails, a resolution based on the location of
871 * this properties file is tried.
872 *
873 * @param fileName the name of the file to load
874 * @throws ConfigurationException if loading fails
875 */
876 private void loadIncludeFile(String fileName) throws ConfigurationException
877 {
878 URL url = ConfigurationUtils.locate(getBasePath(), fileName);
879 if (url == null)
880 {
881 URL baseURL = getURL();
882 if (baseURL != null)
883 {
884 url = ConfigurationUtils.locate(baseURL.toString(), fileName);
885 }
886 }
887
888 if (url == null)
889 {
890 throw new ConfigurationException("Cannot resolve include file "
891 + fileName);
892 }
893 load(url);
894 }
895 }