View Javadoc

1   /*
2    * Copyright 2001-2005 The Apache Software Foundation.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License")
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
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 &lt;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     // initialization block to set the encoding before loading the file in the constructors
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; // EOF
335                 }
336 
337                 // parse the line
338                 String[] property = parseProperty(line);
339                 String key = property[0];
340                 String value = property[1];
341 
342                 // Though some software (e.g. autoconf) may produce
343                 // empty values like foo=\n, emulate the behavior of
344                 // java.util.Properties by setting the value to the
345                 // empty string.
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                     // EOF
480                     return null;
481                 }
482 
483                 line = line.trim();
484 
485                 // skip comments and empty lines
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     } // class PropertiesReader
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                     // escape the separator
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     } // class PropertiesWriter
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                 // if in unicode, then we're reading unicode
669                 // values in somehow
670                 unicode.append(ch);
671                 if (unicode.length() == UNICODE_LEN)
672                 {
673                     // unicode now contains the four hex digits
674                     // which represents our unicode character
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                 // handle an escaped value
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                     // uh-oh, we're in unicode country....
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             // then we're in the weird case of a \ at the end of the
756             // string, let's output it anyway.
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         // sorry for this spaghetti code, please replace it as soon as
773         // possible with a regexp when the Java 1.3 requirement is dropped
774 
775         String[] result = new String[2];
776         StringBuffer key = new StringBuffer();
777         StringBuffer value = new StringBuffer();
778 
779         // state of the automaton:
780         // 0: key parsing
781         // 1: antislash found while parsing the key
782         // 2: separator crossing
783         // 3: value parsing
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                         // switch to the separator crossing state
800                         state = 2;
801                     }
802                     else if (ArrayUtils.contains(SEPARATORS, c))
803                     {
804                         // switch to the value parsing state
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                         // this is an escaped separator or white space
818                         key.append(c);
819                     }
820                     else
821                     {
822                         // another escaped character, the '\' is preserved
823                         key.append('//');
824                         key.append(c);
825                     }
826 
827                     // return to the key parsing state
828                     state = 0;
829 
830                     break;
831 
832                 case 2:
833                     if (ArrayUtils.contains(WHITE_SPACE, c))
834                     {
835                         // do nothing, eat all white spaces
836                         state = 2;
837                     }
838                     else if (ArrayUtils.contains(SEPARATORS, c))
839                     {
840                         // switch to the value parsing state
841                         state = 3;
842                     }
843                     else
844                     {
845                         // any other character indicates we encoutered the beginning of the value
846                         value.append(c);
847 
848                         // switch to the value parsing state
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 }