001    /*
002     *  Licensed to the Apache Software Foundation (ASF) under one or more
003     *  contributor license agreements.  See the NOTICE file distributed with
004     *  this work for additional information regarding copyright ownership.
005     *  The ASF licenses this file to You under the Apache License, Version 2.0
006     *  (the "License"); you may not use this file except in compliance with
007     *  the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     *  Unless required by applicable law or agreed to in writing, software
012     *  distributed under the License is distributed on an "AS IS" BASIS,
013     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     *  See the License for the specific language governing permissions and
015     *  limitations under the License.
016     */
017    package org.apache.commons.collections;
018    
019    import java.io.File;
020    import java.io.FileInputStream;
021    import java.io.IOException;
022    import java.io.InputStream;
023    import java.io.InputStreamReader;
024    import java.io.LineNumberReader;
025    import java.io.OutputStream;
026    import java.io.PrintWriter;
027    import java.io.Reader;
028    import java.io.UnsupportedEncodingException;
029    import java.util.ArrayList;
030    import java.util.Enumeration;
031    import java.util.Hashtable;
032    import java.util.Iterator;
033    import java.util.List;
034    import java.util.NoSuchElementException;
035    import java.util.Properties;
036    import java.util.StringTokenizer;
037    import java.util.Vector;
038    
039    /**
040     * This class extends normal Java properties by adding the possibility
041     * to use the same key many times concatenating the value strings
042     * instead of overwriting them.
043     * <p>
044     * <b>Please consider using the <code>PropertiesConfiguration</code> class in
045     * Commons-Configuration as soon as it is released.</b>
046     * <p>
047     * The Extended Properties syntax is explained here:
048     *
049     * <ul>
050     *  <li>
051     *   Each property has the syntax <code>key = value</code>
052     *  </li>
053     *  <li>
054     *   The <i>key</i> may use any character but the equal sign '='.
055     *  </li>
056     *  <li>
057     *   <i>value</i> may be separated on different lines if a backslash
058     *   is placed at the end of the line that continues below.
059     *  </li>
060     *  <li>
061     *   If <i>value</i> is a list of strings, each token is separated
062     *   by a comma ','.
063     *  </li>
064     *  <li>
065     *   Commas in each token are escaped placing a backslash right before
066     *   the comma.
067     *  </li>
068     *  <li>
069     *   Backslashes are escaped by using two consecutive backslashes i.e. \\
070     *  </li>
071     *  <li>
072     *   If a <i>key</i> is used more than once, the values are appended
073     *   as if they were on the same line separated with commas.
074     *  </li>
075     *  <li>
076     *   Blank lines and lines starting with character '#' are skipped.
077     *  </li>
078     *  <li>
079     *   If a property is named "include" (or whatever is defined by
080     *   setInclude() and getInclude() and the value of that property is
081     *   the full path to a file on disk, that file will be included into
082     *   the ConfigurationsRepository. You can also pull in files relative
083     *   to the parent configuration file. So if you have something
084     *   like the following:
085     *
086     *   include = additional.properties
087     *
088     *   Then "additional.properties" is expected to be in the same
089     *   directory as the parent configuration file.
090     * 
091     *   Duplicate name values will be replaced, so be careful.
092     *
093     *  </li>
094     * </ul>
095     *
096     * <p>Here is an example of a valid extended properties file:
097     *
098     * <p><pre>
099     *      # lines starting with # are comments
100     *
101     *      # This is the simplest property
102     *      key = value
103     *
104     *      # A long property may be separated on multiple lines
105     *      longvalue = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
106     *                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
107     *
108     *      # This is a property with many tokens
109     *      tokens_on_a_line = first token, second token
110     *
111     *      # This sequence generates exactly the same result
112     *      tokens_on_multiple_lines = first token
113     *      tokens_on_multiple_lines = second token
114     *
115     *      # commas may be escaped in tokens
116     *      commas.escaped = Hi\, what'up?
117     * </pre>
118     *
119     * <p><b>NOTE</b>: this class has <b>not</b> been written for
120     * performance nor low memory usage.  In fact, it's way slower than it
121     * could be and generates too much memory garbage.  But since
122     * performance is not an issue during intialization (and there is not
123     * much time to improve it), I wrote it this way.  If you don't like
124     * it, go ahead and tune it up!
125     *
126     * @since Commons Collections 1.0
127     * @version $Revision: 646777 $ $Date: 2008-04-10 13:33:15 +0100 (Thu, 10 Apr 2008) $
128     * 
129     * @author <a href="mailto:stefano@apache.org">Stefano Mazzocchi</a>
130     * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a>
131     * @author <a href="mailto:daveb@miceda-data">Dave Bryson</a>
132     * @author <a href="mailto:jvanzyl@periapt.com">Jason van Zyl</a>
133     * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
134     * @author <a href="mailto:leon@opticode.co.za">Leon Messerschmidt</a>
135     * @author <a href="mailto:kjohnson@transparent.com">Kent Johnson</a>
136     * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
137     * @author <a href="mailto:ipriha@surfeu.fi">Ilkka Priha</a>
138     * @author Janek Bogucki
139     * @author Mohan Kishore
140     * @author Stephen Colebourne
141     * @author Shinobu Kawai
142     * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
143     */
144    public class ExtendedProperties extends Hashtable {
145        
146        /**
147         * Default configurations repository.
148         */
149        private ExtendedProperties defaults;
150    
151        /**
152         * The file connected to this repository (holding comments and
153         * such).
154         *
155         * @serial
156         */
157        protected String file;
158    
159        /**
160         * Base path of the configuration file used to create
161         * this ExtendedProperties object.
162         */
163        protected String basePath;
164    
165        /**
166         * File separator.
167         */
168        protected String fileSeparator = System.getProperty("file.separator");
169    
170        /**
171         * Has this configuration been intialized.
172         */
173        protected boolean isInitialized = false;
174    
175        /**
176         * This is the name of the property that can point to other
177         * properties file for including other properties files.
178         */
179        protected static String include = "include";
180    
181        /**
182         * These are the keys in the order they listed
183         * in the configuration file. This is useful when
184         * you wish to perform operations with configuration
185         * information in a particular order.
186         */
187        protected ArrayList keysAsListed = new ArrayList();
188    
189        protected final static String START_TOKEN="${";
190        protected final static String END_TOKEN="}";
191    
192    
193        /**
194         * Interpolate key names to handle ${key} stuff
195         *
196         * @param base string to interpolate
197         * @return returns the key name with the ${key} substituted
198         */
199        protected String interpolate(String base) {
200            // COPIED from [configuration] 2003-12-29
201            return (interpolateHelper(base, null));
202        }
203    
204        /**
205         * Recursive handler for multiple levels of interpolation.
206         *
207         * When called the first time, priorVariables should be null.
208         *
209         * @param base string with the ${key} variables
210         * @param priorVariables serves two purposes: to allow checking for
211         * loops, and creating a meaningful exception message should a loop
212         * occur.  It's 0'th element will be set to the value of base from
213         * the first call.  All subsequent interpolated variables are added
214         * afterward.
215         *
216         * @return the string with the interpolation taken care of
217         */
218        protected String interpolateHelper(String base, List priorVariables) {
219            // COPIED from [configuration] 2003-12-29
220            if (base == null) {
221                return null;
222            }
223    
224            // on the first call initialize priorVariables
225            // and add base as the first element
226            if (priorVariables == null) {
227                priorVariables = new ArrayList();
228                priorVariables.add(base);
229            }
230    
231            int begin = -1;
232            int end = -1;
233            int prec = 0 - END_TOKEN.length();
234            String variable = null;
235            StringBuffer result = new StringBuffer();
236    
237            // FIXME: we should probably allow the escaping of the start token
238            while (((begin = base.indexOf(START_TOKEN, prec + END_TOKEN.length())) > -1)
239                && ((end = base.indexOf(END_TOKEN, begin)) > -1)) {
240                result.append(base.substring(prec + END_TOKEN.length(), begin));
241                variable = base.substring(begin + START_TOKEN.length(), end);
242    
243                // if we've got a loop, create a useful exception message and throw
244                if (priorVariables.contains(variable)) {
245                    String initialBase = priorVariables.remove(0).toString();
246                    priorVariables.add(variable);
247                    StringBuffer priorVariableSb = new StringBuffer();
248    
249                    // create a nice trace of interpolated variables like so:
250                    // var1->var2->var3
251                    for (Iterator it = priorVariables.iterator(); it.hasNext();) {
252                        priorVariableSb.append(it.next());
253                        if (it.hasNext()) {
254                            priorVariableSb.append("->");
255                        }
256                    }
257    
258                    throw new IllegalStateException(
259                        "infinite loop in property interpolation of " + initialBase + ": " + priorVariableSb.toString());
260                }
261                // otherwise, add this variable to the interpolation list.
262                else {
263                    priorVariables.add(variable);
264                }
265    
266                //QUESTION: getProperty or getPropertyDirect
267                Object value = getProperty(variable);
268                if (value != null) {
269                    result.append(interpolateHelper(value.toString(), priorVariables));
270    
271                    // pop the interpolated variable off the stack
272                    // this maintains priorVariables correctness for
273                    // properties with multiple interpolations, e.g.
274                    // prop.name=${some.other.prop1}/blahblah/${some.other.prop2}
275                    priorVariables.remove(priorVariables.size() - 1);
276                } else if (defaults != null && defaults.getString(variable, null) != null) {
277                    result.append(defaults.getString(variable));
278                } else {
279                    //variable not defined - so put it back in the value
280                    result.append(START_TOKEN).append(variable).append(END_TOKEN);
281                }
282                prec = end;
283            }
284            result.append(base.substring(prec + END_TOKEN.length(), base.length()));
285    
286            return result.toString();
287        }
288        
289        /**
290         * Inserts a backslash before every comma and backslash. 
291         */
292        private static String escape(String s) {
293            StringBuffer buf = new StringBuffer(s);
294            for (int i = 0; i < buf.length(); i++) {
295                char c = buf.charAt(i);
296                if (c == ',' || c == '\\') {
297                    buf.insert(i, '\\');
298                    i++;
299                }
300            }
301            return buf.toString();
302        }
303        
304        /**
305         * Removes a backslash from every pair of backslashes. 
306         */
307        private static String unescape(String s) {
308            StringBuffer buf = new StringBuffer(s);
309            for (int i = 0; i < buf.length() - 1; i++) {
310                char c1 = buf.charAt(i);
311                char c2 = buf.charAt(i + 1);
312                if (c1 == '\\' && c2 == '\\') {
313                    buf.deleteCharAt(i);
314                }
315            }
316            return buf.toString();
317        }
318    
319        /**
320         * Counts the number of successive times 'ch' appears in the
321         * 'line' before the position indicated by the 'index'.
322         */
323        private static int countPreceding(String line, int index, char ch) {
324            int i;
325            for (i = index - 1; i >= 0; i--) {
326                if (line.charAt(i) != ch) {
327                    break;
328                }
329            }
330            return index - 1 - i;
331        }
332    
333        /**
334         * Checks if the line ends with odd number of backslashes 
335         */
336        private static boolean endsWithSlash(String line) {
337            if (!line.endsWith("\\")) {
338                return false;
339            }
340            return (countPreceding(line, line.length() - 1, '\\') % 2 == 0);
341        }
342    
343        /**
344         * This class is used to read properties lines.  These lines do
345         * not terminate with new-line chars but rather when there is no
346         * backslash sign a the end of the line.  This is used to
347         * concatenate multiple lines for readability.
348         */
349        static class PropertiesReader extends LineNumberReader {
350            /**
351             * Constructor.
352             *
353             * @param reader A Reader.
354             */
355            public PropertiesReader(Reader reader) {
356                super(reader);
357            }
358    
359            /**
360             * Read a property.
361             *
362             * @return a String property
363             * @throws IOException if there is difficulty reading the source.
364             */
365            public String readProperty() throws IOException {
366                StringBuffer buffer = new StringBuffer();
367                String line = readLine();
368                while (line != null) {
369                    line = line.trim();
370                    if ((line.length() != 0) && (line.charAt(0) != '#')) {
371                        if (endsWithSlash(line)) {
372                            line = line.substring(0, line.length() - 1);
373                            buffer.append(line);
374                        } else {
375                            buffer.append(line);
376                            return buffer.toString();  // normal method end
377                        }
378                    }
379                    line = readLine();
380                }
381                return null;  // EOF reached
382            }
383        }
384    
385        /**
386         * This class divides into tokens a property value.  Token
387         * separator is "," but commas into the property value are escaped
388         * using the backslash in front.
389         */
390        static class PropertiesTokenizer extends StringTokenizer {
391            /**
392             * The property delimiter used while parsing (a comma).
393             */
394            static final String DELIMITER = ",";
395    
396            /**
397             * Constructor.
398             *
399             * @param string A String.
400             */
401            public PropertiesTokenizer(String string) {
402                super(string, DELIMITER);
403            }
404    
405            /**
406             * Check whether the object has more tokens.
407             *
408             * @return True if the object has more tokens.
409             */
410            public boolean hasMoreTokens() {
411                return super.hasMoreTokens();
412            }
413    
414            /**
415             * Get next token.
416             *
417             * @return A String.
418             */
419            public String nextToken() {
420                StringBuffer buffer = new StringBuffer();
421    
422                while (hasMoreTokens()) {
423                    String token = super.nextToken();
424                    if (endsWithSlash(token)) {
425                        buffer.append(token.substring(0, token.length() - 1));
426                        buffer.append(DELIMITER);
427                    } else {
428                        buffer.append(token);
429                        break;
430                    }
431                }
432    
433                return buffer.toString().trim();
434            }
435        }
436    
437        /**
438         * Creates an empty extended properties object.
439         */
440        public ExtendedProperties() {
441            super();
442        }
443    
444        /**
445         * Creates and loads the extended properties from the specified file.
446         *
447         * @param file  the filename to load
448         * @throws IOException if a file error occurs
449         */
450        public ExtendedProperties(String file) throws IOException {
451            this(file, null);
452        }
453    
454        /**
455         * Creates and loads the extended properties from the specified file.
456         *
457         * @param file  the filename to load
458         * @param defaultFile  a second filename to load default values from
459         * @throws IOException if a file error occurs
460         */
461        public ExtendedProperties(String file, String defaultFile) throws IOException {
462            this.file = file;
463    
464            basePath = new File(file).getAbsolutePath();
465            basePath = basePath.substring(0, basePath.lastIndexOf(fileSeparator) + 1);
466    
467            FileInputStream in = null;
468            try {
469                in = new FileInputStream(file);
470                this.load(in);
471            } finally {
472                try {
473                    if (in != null) {
474                        in.close();
475                    }
476                } catch (IOException ex) {}
477            }
478    
479            if (defaultFile != null) {
480                defaults = new ExtendedProperties(defaultFile);
481            }
482        }
483    
484        /**
485         * Indicate to client code whether property
486         * resources have been initialized or not.
487         */
488        public boolean isInitialized() {
489            return isInitialized;
490        }
491    
492        /**
493         * Gets the property value for including other properties files.
494         * By default it is "include".
495         *
496         * @return A String.
497         */
498        public String getInclude() {
499            return include;
500        }
501    
502        /**
503         * Sets the property value for including other properties files.
504         * By default it is "include".
505         *
506         * @param inc A String.
507         */
508        public void setInclude(String inc) {
509            include = inc;
510        }
511    
512        /**
513         * Load the properties from the given input stream.
514         *
515         * @param input  the InputStream to load from
516         * @throws IOException if an IO error occurs
517         */
518        public void load(InputStream input) throws IOException {
519            load(input, null);
520        }
521    
522        /**
523         * Load the properties from the given input stream
524         * and using the specified encoding.
525         *
526         * @param input  the InputStream to load from
527         * @param enc  the encoding to use
528         * @throws IOException if an IO error occurs
529         */
530        public synchronized void load(InputStream input, String enc) throws IOException {
531            PropertiesReader reader = null;
532            if (enc != null) {
533                try {
534                    reader = new PropertiesReader(new InputStreamReader(input, enc));
535                    
536                } catch (UnsupportedEncodingException ex) {
537                    // Another try coming up....
538                }
539            }
540            
541            if (reader == null) {
542                try {
543                    reader = new PropertiesReader(new InputStreamReader(input, "8859_1"));
544                    
545                } catch (UnsupportedEncodingException ex) {
546                    // ISO8859-1 support is required on java platforms but....
547                    // If it's not supported, use the system default encoding
548                    reader = new PropertiesReader(new InputStreamReader(input));
549                }
550            }
551    
552            try {
553                while (true) {
554                    String line = reader.readProperty();
555                    if (line == null) {
556                        return;  // EOF
557                    }
558                    int equalSign = line.indexOf('=');
559    
560                    if (equalSign > 0) {
561                        String key = line.substring(0, equalSign).trim();
562                        String value = line.substring(equalSign + 1).trim();
563    
564                        // Configure produces lines like this ... just ignore them
565                        if ("".equals(value)) {
566                            continue;
567                        }
568    
569                        if (getInclude() != null && key.equalsIgnoreCase(getInclude())) {
570                            // Recursively load properties files.
571                            File file = null;
572    
573                            if (value.startsWith(fileSeparator)) {
574                                // We have an absolute path so we'll use this
575                                file = new File(value);
576                                
577                            } else {
578                                // We have a relative path, and we have two 
579                                // possible forms here. If we have the "./" form
580                                // then just strip that off first before continuing.
581                                if (value.startsWith("." + fileSeparator)) {
582                                    value = value.substring(2);
583                                }
584    
585                                file = new File(basePath + value);
586                            }
587    
588                            if (file != null && file.exists() && file.canRead()) {
589                                load(new FileInputStream(file));
590                            }
591                        } else {
592                            addProperty(key, value);
593                        }
594                    }
595                }
596            } finally {
597                // Loading is initializing
598                isInitialized = true;
599            }
600        }
601    
602        /**
603         * Gets a property from the configuration.
604         *
605         * @param key property to retrieve
606         * @return value as object. Will return user value if exists,
607         *        if not then default value if exists, otherwise null
608         */
609        public Object getProperty(String key) {
610            // first, try to get from the 'user value' store
611            Object obj = this.get(key);
612    
613            if (obj == null) {
614                // if there isn't a value there, get it from the
615                // defaults if we have them
616                if (defaults != null) {
617                    obj = defaults.get(key);
618                }
619            }
620    
621            return obj;
622        }
623        
624        /**
625         * Add a property to the configuration. If it already
626         * exists then the value stated here will be added
627         * to the configuration entry. For example, if
628         *
629         * <code>resource.loader = file</code>
630         *
631         * is already present in the configuration and you
632         *
633         * <code>addProperty("resource.loader", "classpath")</code>
634         *
635         * Then you will end up with a Vector like the
636         * following:
637         *
638         * <code>["file", "classpath"]</code>
639         *
640         * @param key  the key to add
641         * @param value  the value to add
642         */
643        public void addProperty(String key, Object value) {
644            if (value instanceof String) {
645                String str = (String) value;
646                if (str.indexOf(PropertiesTokenizer.DELIMITER) > 0) {
647                    // token contains commas, so must be split apart then added
648                    PropertiesTokenizer tokenizer = new PropertiesTokenizer(str);
649                    while (tokenizer.hasMoreTokens()) {
650                        String token = tokenizer.nextToken();
651                        addPropertyInternal(key, unescape(token));
652                    }
653                } else {
654                    // token contains no commas, so can be simply added
655                    addPropertyInternal(key, unescape(str));
656                }
657            } else {
658                addPropertyInternal(key, value);
659            }
660    
661            // Adding a property connotes initialization
662            isInitialized = true;
663        }
664    
665        /**
666         * Adds a key/value pair to the map.  This routine does
667         * no magic morphing.  It ensures the keylist is maintained
668         *
669         * @param key  the key to store at
670         * @param value  the decoded object to store
671         */
672        private void addPropertyDirect(String key, Object value) {
673            // safety check
674            if (!containsKey(key)) {
675                keysAsListed.add(key);
676            }
677            put(key, value);
678        }
679    
680        /**
681         * Adds a decoded property to the map w/o checking for commas - used
682         * internally when a property has been broken up into
683         * strings that could contain escaped commas to prevent
684         * the inadvertent vectorization.
685         * <p>
686         * Thanks to Leon Messerschmidt for this one.
687         *
688         * @param key  the key to store at
689         * @param value  the decoded object to store
690         */
691        private void addPropertyInternal(String key, Object value) {
692            Object current = this.get(key);
693    
694            if (current instanceof String) {
695                // one object already in map - convert it to a vector
696                List values = new Vector(2);
697                values.add(current);
698                values.add(value);
699                put(key, values);
700                
701            } else if (current instanceof List) {
702                // already a list - just add the new token
703                ((List) current).add(value);
704                
705            } else {
706                // brand new key - store in keysAsListed to retain order
707                if (!containsKey(key)) {
708                    keysAsListed.add(key);
709                }
710                put(key, value);
711            }
712        }
713    
714        /**
715         * Set a property, this will replace any previously
716         * set values. Set values is implicitly a call
717         * to clearProperty(key), addProperty(key,value).
718         *
719         * @param key  the key to set
720         * @param value  the value to set
721         */
722        public void setProperty(String key, Object value) {
723            clearProperty(key);
724            addProperty(key, value);
725        }
726        
727        /**
728         * Save the properties to the given output stream.
729         * <p>
730         * The stream is not closed, but it is flushed.
731         *
732         * @param output  an OutputStream, may be null
733         * @param header  a textual comment to act as a file header
734         * @throws IOException if an IO error occurs
735         */
736        public synchronized void save(OutputStream output, String header) throws IOException {
737            if (output == null) {
738                return;
739            }
740            PrintWriter theWrtr = new PrintWriter(output);
741            if (header != null) {
742                theWrtr.println(header);
743            }
744            
745            Enumeration theKeys = keys();
746            while (theKeys.hasMoreElements()) {
747                String key = (String) theKeys.nextElement();
748                Object value = get(key);
749                if (value != null) {
750                    if (value instanceof String) {
751                        StringBuffer currentOutput = new StringBuffer();
752                        currentOutput.append(key);
753                        currentOutput.append("=");
754                        currentOutput.append(escape((String) value));
755                        theWrtr.println(currentOutput.toString());
756                        
757                    } else if (value instanceof List) {
758                        List values = (List) value;
759                        for (Iterator it = values.iterator(); it.hasNext(); ) {
760                            String currentElement = (String) it.next();
761                            StringBuffer currentOutput = new StringBuffer();
762                            currentOutput.append(key);
763                            currentOutput.append("=");
764                            currentOutput.append(escape(currentElement));
765                            theWrtr.println(currentOutput.toString());
766                        }
767                    }
768                }
769                theWrtr.println();
770                theWrtr.flush();
771            }
772        }
773    
774        /**
775         * Combines an existing Hashtable with this Hashtable.
776         * <p>
777         * Warning: It will overwrite previous entries without warning.
778         *
779         * @param props  the properties to combine
780         */
781        public void combine(ExtendedProperties props) {
782            for (Iterator it = props.getKeys(); it.hasNext();) {
783                String key = (String) it.next();
784                setProperty(key, props.get(key));
785            }
786        }
787        
788        /**
789         * Clear a property in the configuration.
790         *
791         * @param key  the property key to remove along with corresponding value
792         */
793        public void clearProperty(String key) {
794            if (containsKey(key)) {
795                // we also need to rebuild the keysAsListed or else
796                // things get *very* confusing
797                for (int i = 0; i < keysAsListed.size(); i++) {
798                    if (( keysAsListed.get(i)).equals(key)) {
799                        keysAsListed.remove(i);
800                        break;
801                    }
802                }
803                remove(key);
804            }
805        }
806    
807        /**
808         * Get the list of the keys contained in the configuration
809         * repository.
810         *
811         * @return an Iterator over the keys
812         */
813        public Iterator getKeys() {
814            return keysAsListed.iterator();
815        }
816    
817        /**
818         * Get the list of the keys contained in the configuration
819         * repository that match the specified prefix.
820         *
821         * @param prefix  the prefix to match
822         * @return an Iterator of keys that match the prefix
823         */
824        public Iterator getKeys(String prefix) {
825            Iterator keys = getKeys();
826            ArrayList matchingKeys = new ArrayList();
827    
828            while (keys.hasNext()) {
829                Object key = keys.next();
830    
831                if (key instanceof String && ((String) key).startsWith(prefix)) {
832                    matchingKeys.add(key);
833                }
834            }
835            return matchingKeys.iterator();
836        }
837    
838        /**
839         * Create an ExtendedProperties object that is a subset
840         * of this one. Take into account duplicate keys
841         * by using the setProperty() in ExtendedProperties.
842         *
843         * @param prefix  the prefix to get a subset for
844         * @return a new independent ExtendedProperties
845         */
846        public ExtendedProperties subset(String prefix) {
847            ExtendedProperties c = new ExtendedProperties();
848            Iterator keys = getKeys();
849            boolean validSubset = false;
850    
851            while (keys.hasNext()) {
852                Object key = keys.next();
853    
854                if (key instanceof String && ((String) key).startsWith(prefix)) {
855                    if (!validSubset) {
856                        validSubset = true;
857                    }
858    
859                    /*
860                     * Check to make sure that c.subset(prefix) doesn't
861                     * blow up when there is only a single property
862                     * with the key prefix. This is not a useful
863                     * subset but it is a valid subset.
864                     */
865                    String newKey = null;
866                    if (((String) key).length() == prefix.length()) {
867                        newKey = prefix;
868                    } else {
869                        newKey = ((String) key).substring(prefix.length() + 1);
870                    }
871    
872                    /*
873                     *  use addPropertyDirect() - this will plug the data as 
874                     *  is into the Map, but will also do the right thing
875                     *  re key accounting
876                     */
877                    c.addPropertyDirect(newKey, get(key));
878                }
879            }
880    
881            if (validSubset) {
882                return c;
883            } else {
884                return null;
885            }
886        }
887    
888        /**
889         * Display the configuration for debugging purposes to System.out.
890         */
891        public void display() {
892            Iterator i = getKeys();
893    
894            while (i.hasNext()) {
895                String key = (String) i.next();
896                Object value = get(key);
897                System.out.println(key + " => " + value);
898            }
899        }
900    
901        /**
902         * Get a string associated with the given configuration key.
903         *
904         * @param key The configuration key.
905         * @return The associated string.
906         * @throws ClassCastException is thrown if the key maps to an
907         * object that is not a String.
908         */
909        public String getString(String key) {
910            return getString(key, null);
911        }
912    
913        /**
914         * Get a string associated with the given configuration key.
915         *
916         * @param key The configuration key.
917         * @param defaultValue The default value.
918         * @return The associated string if key is found,
919         * default value otherwise.
920         * @throws ClassCastException is thrown if the key maps to an
921         * object that is not a String.
922         */
923        public String getString(String key, String defaultValue) {
924            Object value = get(key);
925    
926            if (value instanceof String) {
927                return interpolate((String) value);
928                
929            } else if (value == null) {
930                if (defaults != null) {
931                    return interpolate(defaults.getString(key, defaultValue));
932                } else {
933                    return interpolate(defaultValue);
934                }
935            } else if (value instanceof List) {
936                return interpolate((String) ((List) value).get(0));
937            } else {
938                throw new ClassCastException('\'' + key + "' doesn't map to a String object");
939            }
940        }
941    
942        /**
943         * Get a list of properties associated with the given
944         * configuration key.
945         *
946         * @param key The configuration key.
947         * @return The associated properties if key is found.
948         * @throws ClassCastException is thrown if the key maps to an
949         * object that is not a String/List.
950         * @throws IllegalArgumentException if one of the tokens is
951         * malformed (does not contain an equals sign).
952         */
953        public Properties getProperties(String key) {
954            return getProperties(key, new Properties());
955        }
956    
957        /**
958         * Get a list of properties associated with the given
959         * configuration key.
960         *
961         * @param key The configuration key.
962         * @return The associated properties if key is found.
963         * @throws ClassCastException is thrown if the key maps to an
964         * object that is not a String/List.
965         * @throws IllegalArgumentException if one of the tokens is
966         * malformed (does not contain an equals sign).
967         */
968        public Properties getProperties(String key, Properties defaults) {
969            /*
970             * Grab an array of the tokens for this key.
971             */
972            String[] tokens = getStringArray(key);
973    
974            // Each token is of the form 'key=value'.
975            Properties props = new Properties(defaults);
976            for (int i = 0; i < tokens.length; i++) {
977                String token = tokens[i];
978                int equalSign = token.indexOf('=');
979                if (equalSign > 0) {
980                    String pkey = token.substring(0, equalSign).trim();
981                    String pvalue = token.substring(equalSign + 1).trim();
982                    props.put(pkey, pvalue);
983                } else {
984                    throw new IllegalArgumentException('\'' + token + "' does not contain " + "an equals sign");
985                }
986            }
987            return props;
988        }
989    
990        /**
991         * Get an array of strings associated with the given configuration
992         * key.
993         *
994         * @param key The configuration key.
995         * @return The associated string array if key is found.
996         * @throws ClassCastException is thrown if the key maps to an
997         * object that is not a String/List.
998         */
999        public String[] getStringArray(String key) {
1000            Object value = get(key);
1001    
1002            List values;
1003            if (value instanceof String) {
1004                values = new Vector(1);
1005                values.add(value);
1006                
1007            } else if (value instanceof List) {
1008                values = (List) value;
1009                
1010            } else if (value == null) {
1011                if (defaults != null) {
1012                    return defaults.getStringArray(key);
1013                } else {
1014                    return new String[0];
1015                }
1016            } else {
1017                throw new ClassCastException('\'' + key + "' doesn't map to a String/List object");
1018            }
1019    
1020            String[] tokens = new String[values.size()];
1021            for (int i = 0; i < tokens.length; i++) {
1022                tokens[i] = (String) values.get(i);
1023            }
1024    
1025            return tokens;
1026        }
1027    
1028        /**
1029         * Get a Vector of strings associated with the given configuration
1030         * key.
1031         *
1032         * @param key The configuration key.
1033         * @return The associated Vector.
1034         * @throws ClassCastException is thrown if the key maps to an
1035         * object that is not a Vector.
1036         */
1037        public Vector getVector(String key) {
1038            return getVector(key, null);
1039        }
1040    
1041        /**
1042         * Get a Vector of strings associated with the given configuration key.
1043         * <p>
1044         * The list is a copy of the internal data of this object, and as
1045         * such you may alter it freely.
1046         *
1047         * @param key The configuration key.
1048         * @param defaultValue The default value.
1049         * @return The associated Vector.
1050         * @throws ClassCastException is thrown if the key maps to an
1051         * object that is not a Vector.
1052         */
1053        public Vector getVector(String key, Vector defaultValue) {
1054            Object value = get(key);
1055    
1056            if (value instanceof List) {
1057                return new Vector((List) value);
1058                
1059            } else if (value instanceof String) {
1060                Vector values = new Vector(1);
1061                values.add(value);
1062                put(key, values);
1063                return values;
1064                
1065            } else if (value == null) {
1066                if (defaults != null) {
1067                    return defaults.getVector(key, defaultValue);
1068                } else {
1069                    return ((defaultValue == null) ? new Vector() : defaultValue);
1070                }
1071            } else {
1072                throw new ClassCastException('\'' + key + "' doesn't map to a Vector object");
1073            }
1074        }
1075    
1076        /**
1077         * Get a List of strings associated with the given configuration key.
1078         * <p>
1079         * The list is a copy of the internal data of this object, and as
1080         * such you may alter it freely.
1081         *
1082         * @param key The configuration key.
1083         * @return The associated List object.
1084         * @throws ClassCastException is thrown if the key maps to an
1085         * object that is not a List.
1086         * @since Commons Collections 3.2
1087         */
1088        public List getList(String key) {
1089            return getList(key, null);
1090        }
1091    
1092        /**
1093         * Get a List of strings associated with the given configuration key.
1094         * <p>
1095         * The list is a copy of the internal data of this object, and as
1096         * such you may alter it freely.
1097         *
1098         * @param key The configuration key.
1099         * @param defaultValue The default value.
1100         * @return The associated List.
1101         * @throws ClassCastException is thrown if the key maps to an
1102         * object that is not a List.
1103         * @since Commons Collections 3.2
1104         */
1105        public List getList(String key, List defaultValue) {
1106            Object value = get(key);
1107    
1108            if (value instanceof List) {
1109                return new ArrayList((List) value);
1110                
1111            } else if (value instanceof String) {
1112                List values = new ArrayList(1);
1113                values.add(value);
1114                put(key, values);
1115                return values;
1116                
1117            } else if (value == null) {
1118                if (defaults != null) {
1119                    return defaults.getList(key, defaultValue);
1120                } else {
1121                    return ((defaultValue == null) ? new ArrayList() : defaultValue);
1122                }
1123            } else {
1124                throw new ClassCastException('\'' + key + "' doesn't map to a List object");
1125            }
1126        }
1127    
1128        /**
1129         * Get a boolean associated with the given configuration key.
1130         *
1131         * @param key The configuration key.
1132         * @return The associated boolean.
1133         * @throws NoSuchElementException is thrown if the key doesn't
1134         * map to an existing object.
1135         * @throws ClassCastException is thrown if the key maps to an
1136         * object that is not a Boolean.
1137         */
1138        public boolean getBoolean(String key) {
1139            Boolean b = getBoolean(key, null);
1140            if (b != null) {
1141                return b.booleanValue();
1142            } else {
1143                throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
1144            }
1145        }
1146    
1147        /**
1148         * Get a boolean associated with the given configuration key.
1149         *
1150         * @param key The configuration key.
1151         * @param defaultValue The default value.
1152         * @return The associated boolean.
1153         * @throws ClassCastException is thrown if the key maps to an
1154         * object that is not a Boolean.
1155         */
1156        public boolean getBoolean(String key, boolean defaultValue) {
1157            return getBoolean(key, new Boolean(defaultValue)).booleanValue();
1158        }
1159    
1160        /**
1161         * Get a boolean associated with the given configuration key.
1162         *
1163         * @param key The configuration key.
1164         * @param defaultValue The default value.
1165         * @return The associated boolean if key is found and has valid
1166         * format, default value otherwise.
1167         * @throws ClassCastException is thrown if the key maps to an
1168         * object that is not a Boolean.
1169         */
1170        public Boolean getBoolean(String key, Boolean defaultValue) {
1171    
1172            Object value = get(key);
1173    
1174            if (value instanceof Boolean) {
1175                return (Boolean) value;
1176                
1177            } else if (value instanceof String) {
1178                String s = testBoolean((String) value);
1179                Boolean b = new Boolean(s);
1180                put(key, b);
1181                return b;
1182                
1183            } else if (value == null) {
1184                if (defaults != null) {
1185                    return defaults.getBoolean(key, defaultValue);
1186                } else {
1187                    return defaultValue;
1188                }
1189            } else {
1190                throw new ClassCastException('\'' + key + "' doesn't map to a Boolean object");
1191            }
1192        }
1193    
1194        /**
1195         * Test whether the string represent by value maps to a boolean
1196         * value or not. We will allow <code>true</code>, <code>on</code>,
1197         * and <code>yes</code> for a <code>true</code> boolean value, and
1198         * <code>false</code>, <code>off</code>, and <code>no</code> for
1199         * <code>false</code> boolean values.  Case of value to test for
1200         * boolean status is ignored.
1201         *
1202         * @param value  the value to test for boolean state
1203         * @return <code>true</code> or <code>false</code> if the supplied
1204         * text maps to a boolean value, or <code>null</code> otherwise.
1205         */
1206        public String testBoolean(String value) {
1207            String s = value.toLowerCase();
1208    
1209            if (s.equals("true") || s.equals("on") || s.equals("yes")) {
1210                return "true";
1211            } else if (s.equals("false") || s.equals("off") || s.equals("no")) {
1212                return "false";
1213            } else {
1214                return null;
1215            }
1216        }
1217    
1218        /**
1219         * Get a byte associated with the given configuration key.
1220         *
1221         * @param key The configuration key.
1222         * @return The associated byte.
1223         * @throws NoSuchElementException is thrown if the key doesn't
1224         * map to an existing object.
1225         * @throws ClassCastException is thrown if the key maps to an
1226         * object that is not a Byte.
1227         * @throws NumberFormatException is thrown if the value mapped
1228         * by the key has not a valid number format.
1229         */
1230        public byte getByte(String key) {
1231            Byte b = getByte(key, null);
1232            if (b != null) {
1233                return b.byteValue();
1234            } else {
1235                throw new NoSuchElementException('\'' + key + " doesn't map to an existing object");
1236            }
1237        }
1238    
1239        /**
1240         * Get a byte associated with the given configuration key.
1241         *
1242         * @param key The configuration key.
1243         * @param defaultValue The default value.
1244         * @return The associated byte.
1245         * @throws ClassCastException is thrown if the key maps to an
1246         * object that is not a Byte.
1247         * @throws NumberFormatException is thrown if the value mapped
1248         * by the key has not a valid number format.
1249         */
1250        public byte getByte(String key, byte defaultValue) {
1251            return getByte(key, new Byte(defaultValue)).byteValue();
1252        }
1253    
1254        /**
1255         * Get a byte associated with the given configuration key.
1256         *
1257         * @param key The configuration key.
1258         * @param defaultValue The default value.
1259         * @return The associated byte if key is found and has valid
1260         * format, default value otherwise.
1261         * @throws ClassCastException is thrown if the key maps to an
1262         * object that is not a Byte.
1263         * @throws NumberFormatException is thrown if the value mapped
1264         * by the key has not a valid number format.
1265         */
1266        public Byte getByte(String key, Byte defaultValue) {
1267            Object value = get(key);
1268    
1269            if (value instanceof Byte) {
1270                return (Byte) value;
1271                
1272            } else if (value instanceof String) {
1273                Byte b = new Byte((String) value);
1274                put(key, b);
1275                return b;
1276                
1277            } else if (value == null) {
1278                if (defaults != null) {
1279                    return defaults.getByte(key, defaultValue);
1280                } else {
1281                    return defaultValue;
1282                }
1283            } else {
1284                throw new ClassCastException('\'' + key + "' doesn't map to a Byte object");
1285            }
1286        }
1287    
1288        /**
1289         * Get a short associated with the given configuration key.
1290         *
1291         * @param key The configuration key.
1292         * @return The associated short.
1293         * @throws NoSuchElementException is thrown if the key doesn't
1294         * map to an existing object.
1295         * @throws ClassCastException is thrown if the key maps to an
1296         * object that is not a Short.
1297         * @throws NumberFormatException is thrown if the value mapped
1298         * by the key has not a valid number format.
1299         */
1300        public short getShort(String key) {
1301            Short s = getShort(key, null);
1302            if (s != null) {
1303                return s.shortValue();
1304            } else {
1305                throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
1306            }
1307        }
1308    
1309        /**
1310         * Get a short associated with the given configuration key.
1311         *
1312         * @param key The configuration key.
1313         * @param defaultValue The default value.
1314         * @return The associated short.
1315         * @throws ClassCastException is thrown if the key maps to an
1316         * object that is not a Short.
1317         * @throws NumberFormatException is thrown if the value mapped
1318         * by the key has not a valid number format.
1319         */
1320        public short getShort(String key, short defaultValue) {
1321            return getShort(key, new Short(defaultValue)).shortValue();
1322        }
1323    
1324        /**
1325         * Get a short associated with the given configuration key.
1326         *
1327         * @param key The configuration key.
1328         * @param defaultValue The default value.
1329         * @return The associated short if key is found and has valid
1330         * format, default value otherwise.
1331         * @throws ClassCastException is thrown if the key maps to an
1332         * object that is not a Short.
1333         * @throws NumberFormatException is thrown if the value mapped
1334         * by the key has not a valid number format.
1335         */
1336        public Short getShort(String key, Short defaultValue) {
1337            Object value = get(key);
1338    
1339            if (value instanceof Short) {
1340                return (Short) value;
1341                
1342            } else if (value instanceof String) {
1343                Short s = new Short((String) value);
1344                put(key, s);
1345                return s;
1346                
1347            } else if (value == null) {
1348                if (defaults != null) {
1349                    return defaults.getShort(key, defaultValue);
1350                } else {
1351                    return defaultValue;
1352                }
1353            } else {
1354                throw new ClassCastException('\'' + key + "' doesn't map to a Short object");
1355            }
1356        }
1357    
1358        /**
1359         * The purpose of this method is to get the configuration resource
1360         * with the given name as an integer.
1361         *
1362         * @param name The resource name.
1363         * @return The value of the resource as an integer.
1364         */
1365        public int getInt(String name) {
1366            return getInteger(name);
1367        }
1368    
1369        /**
1370         * The purpose of this method is to get the configuration resource
1371         * with the given name as an integer, or a default value.
1372         *
1373         * @param name The resource name
1374         * @param def The default value of the resource.
1375         * @return The value of the resource as an integer.
1376         */
1377        public int getInt(String name, int def) {
1378            return getInteger(name, def);
1379        }
1380    
1381        /**
1382         * Get a int associated with the given configuration key.
1383         *
1384         * @param key The configuration key.
1385         * @return The associated int.
1386         * @throws NoSuchElementException is thrown if the key doesn't
1387         * map to an existing object.
1388         * @throws ClassCastException is thrown if the key maps to an
1389         * object that is not a Integer.
1390         * @throws NumberFormatException is thrown if the value mapped
1391         * by the key has not a valid number format.
1392         */
1393        public int getInteger(String key) {
1394            Integer i = getInteger(key, null);
1395            if (i != null) {
1396                return i.intValue();
1397            } else {
1398                throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
1399            }
1400        }
1401    
1402        /**
1403         * Get a int associated with the given configuration key.
1404         *
1405         * @param key The configuration key.
1406         * @param defaultValue The default value.
1407         * @return The associated int.
1408         * @throws ClassCastException is thrown if the key maps to an
1409         * object that is not a Integer.
1410         * @throws NumberFormatException is thrown if the value mapped
1411         * by the key has not a valid number format.
1412         */
1413        public int getInteger(String key, int defaultValue) {
1414            Integer i = getInteger(key, null);
1415    
1416            if (i == null) {
1417                return defaultValue;
1418            }
1419            return i.intValue();
1420        }
1421    
1422        /**
1423         * Get a int associated with the given configuration key.
1424         *
1425         * @param key The configuration key.
1426         * @param defaultValue The default value.
1427         * @return The associated int if key is found and has valid
1428         * format, default value otherwise.
1429         * @throws ClassCastException is thrown if the key maps to an
1430         * object that is not a Integer.
1431         * @throws NumberFormatException is thrown if the value mapped
1432         * by the key has not a valid number format.
1433         */
1434        public Integer getInteger(String key, Integer defaultValue) {
1435            Object value = get(key);
1436    
1437            if (value instanceof Integer) {
1438                return (Integer) value;
1439                
1440            } else if (value instanceof String) {
1441                Integer i = new Integer((String) value);
1442                put(key, i);
1443                return i;
1444                
1445            } else if (value == null) {
1446                if (defaults != null) {
1447                    return defaults.getInteger(key, defaultValue);
1448                } else {
1449                    return defaultValue;
1450                }
1451            } else {
1452                throw new ClassCastException('\'' + key + "' doesn't map to a Integer object");
1453            }
1454        }
1455    
1456        /**
1457         * Get a long associated with the given configuration key.
1458         *
1459         * @param key The configuration key.
1460         * @return The associated long.
1461         * @throws NoSuchElementException is thrown if the key doesn't
1462         * map to an existing object.
1463         * @throws ClassCastException is thrown if the key maps to an
1464         * object that is not a Long.
1465         * @throws NumberFormatException is thrown if the value mapped
1466         * by the key has not a valid number format.
1467         */
1468        public long getLong(String key) {
1469            Long l = getLong(key, null);
1470            if (l != null) {
1471                return l.longValue();
1472            } else {
1473                throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
1474            }
1475        }
1476    
1477        /**
1478         * Get a long associated with the given configuration key.
1479         *
1480         * @param key The configuration key.
1481         * @param defaultValue The default value.
1482         * @return The associated long.
1483         * @throws ClassCastException is thrown if the key maps to an
1484         * object that is not a Long.
1485         * @throws NumberFormatException is thrown if the value mapped
1486         * by the key has not a valid number format.
1487         */
1488        public long getLong(String key, long defaultValue) {
1489            return getLong(key, new Long(defaultValue)).longValue();
1490        }
1491    
1492        /**
1493         * Get a long associated with the given configuration key.
1494         *
1495         * @param key The configuration key.
1496         * @param defaultValue The default value.
1497         * @return The associated long if key is found and has valid
1498         * format, default value otherwise.
1499         * @throws ClassCastException is thrown if the key maps to an
1500         * object that is not a Long.
1501         * @throws NumberFormatException is thrown if the value mapped
1502         * by the key has not a valid number format.
1503         */
1504        public Long getLong(String key, Long defaultValue) {
1505            Object value = get(key);
1506    
1507            if (value instanceof Long) {
1508                return (Long) value;
1509                
1510            } else if (value instanceof String) {
1511                Long l = new Long((String) value);
1512                put(key, l);
1513                return l;
1514                
1515            } else if (value == null) {
1516                if (defaults != null) {
1517                    return defaults.getLong(key, defaultValue);
1518                } else {
1519                    return defaultValue;
1520                }
1521            } else {
1522                throw new ClassCastException('\'' + key + "' doesn't map to a Long object");
1523            }
1524        }
1525    
1526        /**
1527         * Get a float associated with the given configuration key.
1528         *
1529         * @param key The configuration key.
1530         * @return The associated float.
1531         * @throws NoSuchElementException is thrown if the key doesn't
1532         * map to an existing object.
1533         * @throws ClassCastException is thrown if the key maps to an
1534         * object that is not a Float.
1535         * @throws NumberFormatException is thrown if the value mapped
1536         * by the key has not a valid number format.
1537         */
1538        public float getFloat(String key) {
1539            Float f = getFloat(key, null);
1540            if (f != null) {
1541                return f.floatValue();
1542            } else {
1543                throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
1544            }
1545        }
1546    
1547        /**
1548         * Get a float associated with the given configuration key.
1549         *
1550         * @param key The configuration key.
1551         * @param defaultValue The default value.
1552         * @return The associated float.
1553         * @throws ClassCastException is thrown if the key maps to an
1554         * object that is not a Float.
1555         * @throws NumberFormatException is thrown if the value mapped
1556         * by the key has not a valid number format.
1557         */
1558        public float getFloat(String key, float defaultValue) {
1559            return getFloat(key, new Float(defaultValue)).floatValue();
1560        }
1561    
1562        /**
1563         * Get a float associated with the given configuration key.
1564         *
1565         * @param key The configuration key.
1566         * @param defaultValue The default value.
1567         * @return The associated float if key is found and has valid
1568         * format, default value otherwise.
1569         * @throws ClassCastException is thrown if the key maps to an
1570         * object that is not a Float.
1571         * @throws NumberFormatException is thrown if the value mapped
1572         * by the key has not a valid number format.
1573         */
1574        public Float getFloat(String key, Float defaultValue) {
1575            Object value = get(key);
1576    
1577            if (value instanceof Float) {
1578                return (Float) value;
1579                
1580            } else if (value instanceof String) {
1581                Float f = new Float((String) value);
1582                put(key, f);
1583                return f;
1584                
1585            } else if (value == null) {
1586                if (defaults != null) {
1587                    return defaults.getFloat(key, defaultValue);
1588                } else {
1589                    return defaultValue;
1590                }
1591            } else {
1592                throw new ClassCastException('\'' + key + "' doesn't map to a Float object");
1593            }
1594        }
1595    
1596        /**
1597         * Get a double associated with the given configuration key.
1598         *
1599         * @param key The configuration key.
1600         * @return The associated double.
1601         * @throws NoSuchElementException is thrown if the key doesn't
1602         * map to an existing object.
1603         * @throws ClassCastException is thrown if the key maps to an
1604         * object that is not a Double.
1605         * @throws NumberFormatException is thrown if the value mapped
1606         * by the key has not a valid number format.
1607         */
1608        public double getDouble(String key) {
1609            Double d = getDouble(key, null);
1610            if (d != null) {
1611                return d.doubleValue();
1612            } else {
1613                throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
1614            }
1615        }
1616    
1617        /**
1618         * Get a double associated with the given configuration key.
1619         *
1620         * @param key The configuration key.
1621         * @param defaultValue The default value.
1622         * @return The associated double.
1623         * @throws ClassCastException is thrown if the key maps to an
1624         * object that is not a Double.
1625         * @throws NumberFormatException is thrown if the value mapped
1626         * by the key has not a valid number format.
1627         */
1628        public double getDouble(String key, double defaultValue) {
1629            return getDouble(key, new Double(defaultValue)).doubleValue();
1630        }
1631    
1632        /**
1633         * Get a double associated with the given configuration key.
1634         *
1635         * @param key The configuration key.
1636         * @param defaultValue The default value.
1637         * @return The associated double if key is found and has valid
1638         * format, default value otherwise.
1639         * @throws ClassCastException is thrown if the key maps to an
1640         * object that is not a Double.
1641         * @throws NumberFormatException is thrown if the value mapped
1642         * by the key has not a valid number format.
1643         */
1644        public Double getDouble(String key, Double defaultValue) {
1645            Object value = get(key);
1646    
1647            if (value instanceof Double) {
1648                return (Double) value;
1649                
1650            } else if (value instanceof String) {
1651                Double d = new Double((String) value);
1652                put(key, d);
1653                return d;
1654                
1655            } else if (value == null) {
1656                if (defaults != null) {
1657                    return defaults.getDouble(key, defaultValue);
1658                } else {
1659                    return defaultValue;
1660                }
1661            } else {
1662                throw new ClassCastException('\'' + key + "' doesn't map to a Double object");
1663            }
1664        }
1665    
1666        /**
1667         * Convert a standard properties class into a configuration class.
1668         * <p>
1669         * NOTE: From Commons Collections 3.2 this method will pick up
1670         * any default parent Properties of the specified input object.
1671         *
1672         * @param props  the properties object to convert
1673         * @return new ExtendedProperties created from props
1674         */
1675        public static ExtendedProperties convertProperties(Properties props) {
1676            ExtendedProperties c = new ExtendedProperties();
1677    
1678            for (Enumeration e = props.propertyNames(); e.hasMoreElements();) {
1679                String s = (String) e.nextElement();
1680                c.setProperty(s, props.getProperty(s));
1681            }
1682    
1683            return c;
1684        }
1685    
1686    }