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