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.File;
20 import java.io.FilterWriter;
21 import java.io.IOException;
22 import java.io.LineNumberReader;
23 import java.io.Reader;
24 import java.io.Writer;
25 import java.io.StringReader;
26 import java.io.BufferedReader;
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.StringEscapeUtils;
33 import org.apache.commons.lang.StringUtils;
34
35 /***
36 * This is the "classic" Properties loader which loads the values from
37 * a single or multiple files (which can be chained with "include =".
38 * All given path references are either absolute or relative to the
39 * file name supplied in the constructor.
40 * <p>
41 * In this class, empty PropertyConfigurations can be built, properties
42 * added and later saved. include statements are (obviously) not supported
43 * if you don't construct a PropertyConfiguration from a file.
44 *
45 * <p>The properties file syntax is explained here:
46 *
47 * <ul>
48 * <li>
49 * Each property has the syntax <code>key = value</code>
50 * </li>
51 * <li>
52 * The <i>key</i> may use any character but the equal sign '='.
53 * </li>
54 * <li>
55 * <i>value</i> may be separated on different lines if a backslash
56 * is placed at the end of the line that continues below.
57 * </li>
58 * <li>
59 * If <i>value</i> is a list of strings, each token is separated
60 * by a comma ',' by default.
61 * </li>
62 * <li>
63 * Commas in each token are escaped placing a backslash right before
64 * the comma.
65 * </li>
66 * <li>
67 * If a <i>key</i> is used more than once, the values are appended
68 * like if they were on the same line separated with commas.
69 * </li>
70 * <li>
71 * Blank lines and lines starting with character '#' are skipped.
72 * </li>
73 * <li>
74 * If a property is named "include" (or whatever is defined by
75 * setInclude() and getInclude() and the value of that property is
76 * the full path to a file on disk, that file will be included into
77 * the configuration. You can also pull in files relative to the parent
78 * configuration file. So if you have something like the following:
79 *
80 * include = additional.properties
81 *
82 * Then "additional.properties" is expected to be in the same
83 * directory as the parent configuration file.
84 *
85 * The properties in the included file are added to the parent configuration,
86 * they do not replace existing properties with the same key.
87 *
88 * </li>
89 * </ul>
90 *
91 * <p>Here is an example of a valid extended properties file:
92 *
93 * <p><pre>
94 * # lines starting with # are comments
95 *
96 * # This is the simplest property
97 * key = value
98 *
99 * # A long property may be separated on multiple lines
100 * longvalue = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
101 * aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
102 *
103 * # This is a property with many tokens
104 * tokens_on_a_line = first token, second token
105 *
106 * # This sequence generates exactly the same result
107 * tokens_on_multiple_lines = first token
108 * tokens_on_multiple_lines = second token
109 *
110 * # commas may be escaped in tokens
111 * commas.excaped = Hi\, what'up?
112 *
113 * # properties can reference other properties
114 * base.prop = /base
115 * first.prop = ${base.prop}/first
116 * second.prop = ${first.prop}/second
117 * </pre>
118 *
119 * @author <a href="mailto:e.bourg@cross-systems.com">Emmanuel Bourg</a>
120 * @author <a href="mailto:stefano@apache.org">Stefano Mazzocchi</a>
121 * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a>
122 * @author <a href="mailto:daveb@miceda-data">Dave Bryson</a>
123 * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
124 * @author <a href="mailto:leon@opticode.co.za">Leon Messerschmidt</a>
125 * @author <a href="mailto:kjohnson@transparent.com">Kent Johnson</a>
126 * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
127 * @author <a href="mailto:ipriha@surfeu.fi">Ilkka Priha</a>
128 * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
129 * @author <a href="mailto:mpoeschl@marmot.at">Martin Poeschl</a>
130 * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
131 * @author <a href="mailto:epugh@upstate.com">Eric Pugh</a>
132 * @author <a href="mailto:oliver.heger@t-online.de">Oliver Heger</a>
133 * @version $Id: PropertiesConfiguration.java 156237 2005-03-05 10:26:22Z oheger $
134 */
135 public class PropertiesConfiguration extends AbstractFileConfiguration
136 {
137 /***
138 * This is the name of the property that can point to other
139 * properties file for including other properties files.
140 */
141 static String include = "include";
142
143 /*** Allow file inclusion or not */
144 private boolean includesAllowed = true;
145
146 /*** Comment header of the .properties file */
147 private String header;
148
149 /***
150 * Creates an empty PropertyConfiguration object which can be
151 * used to synthesize a new Properties file by adding values and
152 * then saving(). An object constructed by this C'tor can not be
153 * tickled into loading included files because it cannot supply a
154 * base for relative includes.
155 */
156 public PropertiesConfiguration()
157 {
158 setIncludesAllowed(false);
159 }
160
161 /***
162 * Creates and loads the extended properties from the specified file.
163 * The specified file can contain "include = " properties which then
164 * are loaded and merged into the properties.
165 *
166 * @param fileName The name of the properties file to load.
167 * @throws ConfigurationException Error while loading the properties file
168 */
169 public PropertiesConfiguration(String fileName) throws ConfigurationException
170 {
171 super(fileName);
172 }
173
174 /***
175 * Creates and loads the extended properties from the specified file.
176 * The specified file can contain "include = " properties which then
177 * are loaded and merged into the properties.
178 *
179 * @param file The properties file to load.
180 * @throws ConfigurationException Error while loading the properties file
181 */
182 public PropertiesConfiguration(File file) throws ConfigurationException
183 {
184 super(file);
185 }
186
187 /***
188 * Creates and loads the extended properties from the specified URL.
189 * The specified file can contain "include = " properties which then
190 * are loaded and merged into the properties.
191 *
192 * @param url The location of the properties file to load.
193 * @throws ConfigurationException Error while loading the properties file
194 */
195 public PropertiesConfiguration(URL url) throws ConfigurationException
196 {
197 super(url);
198 }
199
200 /***
201 * Gets the property value for including other properties files.
202 * By default it is "include".
203 *
204 * @return A String.
205 */
206 public static String getInclude()
207 {
208 return PropertiesConfiguration.include;
209 }
210
211 /***
212 * Sets the property value for including other properties files.
213 * By default it is "include".
214 *
215 * @param inc A String.
216 */
217 public static void setInclude(String inc)
218 {
219 PropertiesConfiguration.include = inc;
220 }
221
222 /***
223 * Controls whether additional files can be loaded by the include = <xxx>
224 * statement or not. Base rule is, that objects created by the empty
225 * C'tor can not have included files.
226 *
227 * @param includesAllowed includesAllowed True if Includes are allowed.
228 */
229 protected void setIncludesAllowed(boolean includesAllowed)
230 {
231 this.includesAllowed = includesAllowed;
232 }
233
234 /***
235 * Reports the status of file inclusion.
236 *
237 * @return True if include files are loaded.
238 */
239 public boolean getIncludesAllowed()
240 {
241 return this.includesAllowed;
242 }
243
244 /***
245 * Return the comment header.
246 *
247 * @since 1.1
248 */
249 public String getHeader()
250 {
251 return header;
252 }
253
254 /***
255 * Set the comment header.
256 *
257 * @since 1.1
258 */
259 public void setHeader(String header)
260 {
261 this.header = header;
262 }
263
264 /***
265 * Load the properties from the given reader.
266 * Note that the <code>clear()</code> method is not called, so
267 * the properties contained in the loaded file will be added to the
268 * actual set of properties.
269 *
270 * @param in An InputStream.
271 *
272 * @throws ConfigurationException
273 */
274 public synchronized void load(Reader in) throws ConfigurationException
275 {
276 PropertiesReader reader = new PropertiesReader(in);
277
278 try
279 {
280 while (true)
281 {
282 String line = reader.readProperty();
283
284 if (line == null)
285 {
286 break;
287 }
288
289 int equalSign = line.indexOf('=');
290 if (equalSign > 0)
291 {
292 String key = line.substring(0, equalSign).trim();
293 String value = line.substring(equalSign + 1).trim();
294
295
296
297
298
299
300 if (StringUtils.isNotEmpty(getInclude())
301 && key.equalsIgnoreCase(getInclude()))
302 {
303 if (getIncludesAllowed())
304 {
305 String [] files = StringUtils.split(value, getDelimiter());
306 for (int i = 0; i < files.length; i++)
307 {
308 load(ConfigurationUtils.locate(getBasePath(), files[i].trim()));
309 }
310 }
311 }
312 else
313 {
314 addProperty(key, unescapeJava(value, getDelimiter()));
315 }
316 }
317 }
318 }
319 catch (IOException ioe)
320 {
321 throw new ConfigurationException("Could not load configuration from input stream.", ioe);
322 }
323 }
324
325 /***
326 * Save the configuration to the specified stream.
327 *
328 * @param writer the output stream used to save the configuration
329 */
330 public void save(Writer writer) throws ConfigurationException
331 {
332 try
333 {
334 PropertiesWriter out = new PropertiesWriter(writer, getDelimiter());
335
336 if (header != null)
337 {
338 BufferedReader reader = new BufferedReader(new StringReader(header));
339 String line;
340 while ((line = reader.readLine()) != null)
341 {
342 out.writeComment(line);
343 }
344 out.write("\n");
345 }
346
347 out.writeComment("written by PropertiesConfiguration");
348 out.writeComment(new Date().toString());
349 out.write("\n");
350
351 Iterator keys = getKeys();
352 while (keys.hasNext())
353 {
354 String key = (String) keys.next();
355 Object value = getProperty(key);
356
357 if (value instanceof List)
358 {
359 out.writeProperty(key, (List) value);
360 }
361 else
362 {
363 out.writeProperty(key, value);
364 }
365 }
366
367 out.flush();
368 }
369 catch (IOException e)
370 {
371 throw new ConfigurationException(e.getMessage(), e);
372 }
373 }
374
375 /***
376 * Extend the setBasePath method to turn includes
377 * on and off based on the existence of a base path.
378 *
379 * @param basePath The new basePath to set.
380 */
381 public void setBasePath(String basePath)
382 {
383 super.setBasePath(basePath);
384 setIncludesAllowed(StringUtils.isNotEmpty(basePath));
385 }
386
387 /***
388 * This class is used to read properties lines. These lines do
389 * not terminate with new-line chars but rather when there is no
390 * backslash sign a the end of the line. This is used to
391 * concatenate multiple lines for readability.
392 */
393 public static class PropertiesReader extends LineNumberReader
394 {
395 /***
396 * Constructor.
397 *
398 * @param reader A Reader.
399 */
400 public PropertiesReader(Reader reader)
401 {
402 super(reader);
403 }
404
405 /***
406 * Read a property. Returns null if Stream is
407 * at EOF. Concatenates lines ending with "\".
408 * Skips lines beginning with "#" and empty lines.
409 *
410 * @return A string containing a property value or null
411 *
412 * @throws IOException
413 */
414 public String readProperty() throws IOException
415 {
416 StringBuffer buffer = new StringBuffer();
417
418 while (true)
419 {
420 String line = readLine();
421 if (line == null)
422 {
423
424 return null;
425 }
426
427 line = line.trim();
428
429
430 if (StringUtils.isEmpty(line) || (line.charAt(0) == '#'))
431 {
432 continue;
433 }
434
435 if (line.endsWith("//"))
436 {
437 line = line.substring(0, line.length() - 1);
438 buffer.append(line);
439 }
440 else
441 {
442 buffer.append(line);
443 break;
444 }
445 }
446 return buffer.toString();
447 }
448 }
449
450 /***
451 * This class is used to write properties lines.
452 */
453 public static class PropertiesWriter extends FilterWriter
454 {
455 private char delimiter;
456
457 /***
458 * Constructor.
459 *
460 * @param writer a Writer object providing the underlying stream
461 */
462 public PropertiesWriter(Writer writer, char delimiter)
463 {
464 super(writer);
465 this.delimiter = delimiter;
466 }
467
468 /***
469 * Write a property.
470 *
471 * @param key
472 * @param value
473 * @throws IOException
474 */
475 public void writeProperty(String key, Object value) throws IOException
476 {
477 write(key);
478 write(" = ");
479 if (value != null)
480 {
481 String v = StringEscapeUtils.escapeJava(String.valueOf(value));
482 v = StringUtils.replace(v, String.valueOf(delimiter), "//" + delimiter);
483 write(v);
484 }
485
486 write('\n');
487 }
488
489 /***
490 * Write a property.
491 *
492 * @param key The key of the property
493 * @param values The array of values of the property
494 */
495 public void writeProperty(String key, List values) throws IOException
496 {
497 for (int i = 0; i < values.size(); i++)
498 {
499 writeProperty(key, values.get(i));
500 }
501 }
502
503 /***
504 * Write a comment.
505 *
506 * @param comment
507 * @throws IOException
508 */
509 public void writeComment(String comment) throws IOException
510 {
511 write("# " + comment + "\n");
512 }
513 }
514
515 /***
516 * <p>Unescapes any Java literals found in the <code>String</code> to a
517 * <code>Writer</code>.</p> This is a slightly modified version of the
518 * StringEscapeUtils.unescapeJava() function in commons-lang that doesn't
519 * drop escaped commas (i.e '\,').
520 *
521 * @param str the <code>String</code> to unescape, may be null
522 *
523 * @throws IllegalArgumentException if the Writer is <code>null</code>
524 */
525 protected static String unescapeJava(String str, char delimiter)
526 {
527 if (str == null)
528 {
529 return null;
530 }
531 int sz = str.length();
532 StringBuffer out = new StringBuffer(sz);
533 StringBuffer unicode = new StringBuffer(4);
534 boolean hadSlash = false;
535 boolean inUnicode = false;
536 for (int i = 0; i < sz; i++)
537 {
538 char ch = str.charAt(i);
539 if (inUnicode)
540 {
541
542
543 unicode.append(ch);
544 if (unicode.length() == 4)
545 {
546
547
548 try
549 {
550 int value = Integer.parseInt(unicode.toString(), 16);
551 out.append((char) value);
552 unicode.setLength(0);
553 inUnicode = false;
554 hadSlash = false;
555 }
556 catch (NumberFormatException nfe)
557 {
558 throw new ConfigurationRuntimeException("Unable to parse unicode value: " + unicode, nfe);
559 }
560 }
561 continue;
562 }
563
564 if (hadSlash)
565 {
566
567 hadSlash = false;
568
569 if (ch=='//'){
570 out.append('//');
571 }
572 else if (ch=='\''){
573 out.append('\'');
574 }
575 else if (ch=='\"'){
576 out.append('"');
577 }
578 else if (ch=='r'){
579 out.append('\r');
580 }
581 else if (ch=='f'){
582 out.append('\f');
583 }
584 else if (ch=='t'){
585 out.append('\t');
586 }
587 else if (ch=='n'){
588 out.append('\n');
589 }
590 else if (ch=='b'){
591 out.append('\b');
592 }
593 else if (ch==delimiter){
594 out.append('//');
595 out.append(delimiter);
596 }
597 else if (ch=='u'){
598
599 inUnicode = true;
600 }
601 else {
602 out.append(ch);
603 }
604
605 continue;
606 }
607 else if (ch == '//')
608 {
609 hadSlash = true;
610 continue;
611 }
612 out.append(ch);
613 }
614
615 if (hadSlash)
616 {
617
618
619 out.append('//');
620 }
621
622 return out.toString();
623 }
624
625 }