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