/*
 * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//collections/src/java/org/apache/commons/collections/ExtendedProperties.java,v 1.7 2002/06/12 03:59:15 mas Exp $
 * $Revision: 1.7 $
 * $Date: 2002/06/12 03:59:15 $
 *
 * ====================================================================
 *
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 1999-2002 The Apache Software Foundation.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution, if
 *    any, must include the following acknowlegement:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowlegement may appear in the software itself,
 *    if and wherever such third-party acknowlegements normally appear.
 *
 * 4. The names "The Jakarta Project", "Commons", and "Apache Software
 *    Foundation" must not be used to endorse or promote products derived
 *    from this software without prior written permission. For written
 *    permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache"
 *    nor may "Apache" appear in their names without prior written
 *    permission of the Apache Group.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 */

package org.apache.commons.collections;

import java.io.IOException;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.UnsupportedEncodingException;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;

/**
 * This class extends normal Java properties by adding the possibility
 * to use the same key many times concatenating the value strings
 * instead of overwriting them.
 *
 * <p>The Extended Properties syntax is explained here:
 *
 * <ul>
 *  <li>
 *   Each property has the syntax <code>key = value</code>
 *  </li>
 *  <li>
 *   The <i>key</i> may use any character but the equal sign '='.
 *  </li>
 *  <li>
 *   <i>value</i> may be separated on different lines if a backslash
 *   is placed at the end of the line that continues below.
 *  </li>
 *  <li>
 *   If <i>value</i> is a list of strings, each token is separated
 *   by a comma ','.
 *  </li>
 *  <li>
 *   Commas in each token are escaped placing a backslash right before
 *   the comma.
 *  </li>
 *  <li>
 *   If a <i>key</i> is used more than once, the values are appended
 *   like if they were on the same line separated with commas.
 *  </li>
 *  <li>
 *   Blank lines and lines starting with character '#' are skipped.
 *  </li>
 *  <li>
 *   If a property is named "include" (or whatever is defined by
 *   setInclude() and getInclude() and the value of that property is
 *   the full path to a file on disk, that file will be included into
 *   the ConfigurationsRepository. You can also pull in files relative
 *   to the parent configuration file. So if you have something
 *   like the following:
 *
 *   include = additional.properties
 *
 *   Then "additional.properties" is expected to be in the same
 *   directory as the parent configuration file.
 * 
 *   Duplicate name values will be replaced, so be careful.
 *
 *  </li>
 * </ul>
 *
 * <p>Here is an example of a valid extended properties file:
 *
 * <p><pre>
 *      # lines starting with # are comments
 *
 *      # This is the simplest property
 *      key = value
 *
 *      # A long property may be separated on multiple lines
 *      longvalue = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
 *                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
 *
 *      # This is a property with many tokens
 *      tokens_on_a_line = first token, second token
 *
 *      # This sequence generates exactly the same result
 *      tokens_on_multiple_lines = first token
 *      tokens_on_multiple_lines = second token
 *
 *      # commas may be escaped in tokens
 *      commas.excaped = Hi\, what'up?
 * </pre>
 *
 * <p><b>NOTE</b>: this class has <b>not</b> been written for
 * performance nor low memory usage.  In fact, it's way slower than it
 * could be and generates too much memory garbage.  But since
 * performance is not an issue during intialization (and there is not
 * much time to improve it), I wrote it this way.  If you don't like
 * it, go ahead and tune it up!
 *
 *
 * @since 1.0
 * @author <a href="mailto:stefano@apache.org">Stefano Mazzocchi</a>
 * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a>
 * @author <a href="mailto:daveb@miceda-data">Dave Bryson</a>
 * @author <a href="mailto:jvanzyl@periapt.com">Jason van Zyl</a>
 * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
 * @author <a href="mailto:leon@opticode.co.za">Leon Messerschmidt</a>
 * @author <a href="mailto:kjohnson@transparent.com">Kent Johnson</a>
 * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
 * @author <a href="mailto:ipriha@surfeu.fi">Ilkka Priha</a>
 * @version $Id: ExtendedProperties.java,v 1.7 2002/06/12 03:59:15 mas Exp $
 */
public class ExtendedProperties extends Hashtable
{
    /**
     * Default configurations repository.
     */
    private ExtendedProperties defaults;

    /**
     * The file connected to this repository (holding comments and
     * such).
     *
     * @serial
     */
    protected String file;

    /**
     * Base path of the configuration file used to create
     * this ExtendedProperties object.
     */
    protected String basePath;

    /**
     * File separator.
     */
    protected String fileSeparator = System.getProperty("file.separator");

    /**
     * Has this configuration been intialized.
     */
    protected boolean isInitialized = false;

    /**
     * This is the name of the property that can point to other
     * properties file for including other properties files.
     */
    protected static String include = "include";

    /**
     * These are the keys in the order they listed
     * in the configuration file. This is useful when
     * you wish to perform operations with configuration
     * information in a particular order.
     */
    protected ArrayList keysAsListed = new ArrayList();

    protected final static String START_TOKEN="${";
    protected final static String END_TOKEN="}";

    protected String interpolate(String base)
    {
        if (base == null)
        {
            return null;
        }                        
        
        int begin = -1;
        int end = -1;
        int prec = 0 - END_TOKEN.length();
        String variable = null;
        StringBuffer result = new StringBuffer();
        
        // FIXME: we should probably allow the escaping of the start token
        while ( ((begin=base.indexOf(START_TOKEN,prec+END_TOKEN.length()))>-1)
                && ((end=base.indexOf(END_TOKEN,begin))>-1) ) 
        {
            result.append(base.substring(prec+END_TOKEN.length(),begin));
            variable = base.substring(begin+START_TOKEN.length(),end);
            if (get(variable)!=null) 
            {
                result.append(get(variable));
            }
            prec=end;
        }
        result.append(base.substring(prec+END_TOKEN.length(),base.length()));
        
        return result.toString();
    }

    /**
     * This class is used to read properties lines.  These lines do
     * not terminate with new-line chars but rather when there is no
     * backslash sign a the end of the line.  This is used to
     * concatenate multiple lines for readability.
     */
    class PropertiesReader extends LineNumberReader
    {
        /**
         * Constructor.
         *
         * @param reader A Reader.
         */
        public PropertiesReader(Reader reader)
        {
            super(reader);
        }

        /**
         * Read a property.
         *
         * @return A String.
         * @exception IOException.
         */
        public String readProperty() throws IOException
        {
            StringBuffer buffer = new StringBuffer();

            try
            {
                while (true)
                {
                    String line = readLine().trim();
                    if ((line.length() != 0) && (line.charAt(0) != '#'))
                    {
                        if (line.endsWith("\\"))
                        {
                            line = line.substring(0, line.length() - 1);
                            buffer.append(line);
                        }
                        else
                        {
                            buffer.append(line);
                            break;
                        }
                    }
                }
            }
            catch (NullPointerException e)
            {
                return null;
            }

            return buffer.toString();
        }
    }

    /**
     * This class divides into tokens a property value.  Token
     * separator is "," but commas into the property value are escaped
     * using the backslash in front.
     */
    class PropertiesTokenizer extends StringTokenizer
    {
        /**
         * The property delimiter used while parsing (a comma).
         */
        static final String DELIMITER = ",";

        /**
         * Constructor.
         *
         * @param string A String.
         */
        public PropertiesTokenizer(String string)
        {
            super(string, DELIMITER);
        }

        /**
         * Check whether the object has more tokens.
         *
         * @return True if the object has more tokens.
         */
        public boolean hasMoreTokens()
        {
            return super.hasMoreTokens();
        }

        /**
         * Get next token.
         *
         * @return A String.
         */
        public String nextToken()
        {
            StringBuffer buffer = new StringBuffer();

            while (hasMoreTokens())
            {
                String token = super.nextToken();
                if (token.endsWith("\\"))
                {
                    buffer.append(token.substring(0, token.length() - 1));
                    buffer.append(DELIMITER);
                }
                else
                {
                    buffer.append(token);
                    break;
                }
            }

            return buffer.toString().trim();
        }
    }

    /**
     * Creates an empty extended properties object.
     */
    public ExtendedProperties()
    {
        super();
    }

    /**
     * Creates and loads the extended properties from the specified
     * file.
     *
     * @param file A String.
     * @exception IOException.
     */
    public ExtendedProperties(String file) throws IOException
    {
        this(file,null);
    }

    /**
     * Creates and loads the extended properties from the specified
     * file.
     *
     * @param file A String.
     * @exception IOException.
     */
    public ExtendedProperties(String file, String defaultFile)
        throws IOException
    {
        this.file = file;
        
        basePath = new File(file).getAbsolutePath();
        basePath = basePath.substring(0, basePath.lastIndexOf(fileSeparator) + 1);
        
        this.load(new FileInputStream(file));
        
        if (defaultFile != null)
        {
            defaults = new ExtendedProperties(defaultFile);
        }            
    }

    /**
     * Private initializer method that sets up the generic
     * resources.
     *
     * @exception IOException, if there was an I/O problem.
     */
    private void init( ExtendedProperties exp ) throws IOException
    {
        isInitialized = true;
    }
    
    /**
     * Indicate to client code whether property
     * resources have been initialized or not.
     */
    public boolean isInitialized()
    {
        return isInitialized;
    }        

    /**
     * Gets the property value for including other properties files.
     * By default it is "include".
     *
     * @return A String.
     */
    public String getInclude()
    {
        return this.include;
    }

    /**
     * Sets the property value for including other properties files.
     * By default it is "include".
     *
     * @param inc A String.
     */
    public void setInclude(String inc)
    {
        this.include = inc;
    }

    /**
     * Load the properties from the given input stream.
     *
     * @param input An InputStream.
     * @exception IOException.
     */
    public void load( InputStream input )
        throws IOException
    {
        load(input,null);
    }
    
    /**
     * Load the properties from the given input stream
     * and using the specified encoding.
     *
     * @param input An InputStream.
     * @param enc An encoding.
     * @exception IOException.
     */
    public synchronized void load(InputStream input, String enc)
        throws IOException
    {
        PropertiesReader reader = null;
        if (enc != null)
        {
            try
            {
                reader =
                    new PropertiesReader(new InputStreamReader(input,enc));
            }
            catch (UnsupportedEncodingException e)
            {
                // Get one with the default encoding...
            }
        }
        
        if (reader == null)
        {
            reader =
                new PropertiesReader(new InputStreamReader(input));
        }

        try
        {
            while (true)
            {
                String line = reader.readProperty();
                int equalSign = line.indexOf('=');

                if (equalSign > 0)
                {
                    String key = line.substring(0, equalSign).trim();
                    String value = line.substring(equalSign + 1).trim();

                    /*
                     * Configure produces lines like this ... just
                     * ignore them.
                     */
                    if ("".equals(value))
                        continue;

                    if (getInclude() != null && 
                        key.equalsIgnoreCase(getInclude()))
                    {
                        /*
                         * Recursively load properties files.
                         */
                        File file = null;
                        
                        if (value.startsWith(fileSeparator))
                        {
                            /*
                             * We have an absolute path so we'll
                             * use this.
                             */
                            file = new File(value);
                        }
                        else
                        {   
                            /* 
                             * We have a relative path, and we have
                             * two possible forms here. If we have the
                             * "./" form then just strip that off first
                             * before continuing.
                             */
                            if (value.startsWith("." + fileSeparator))
                            {
                                value = value.substring(2);
                            }
                            
                            file = new File(basePath + value);
                        }
                        
                        if (file != null && file.exists() && file.canRead())
                        {
                            load ( new FileInputStream(file));
                        }
                    }
                    else
                    {
                        addProperty(key,value);
                    }                       
                }
            }
        }
        catch (NullPointerException e)
        {
            /*
             * Should happen only when EOF is reached.
             */
            return;
        }
    }

    /**
     *  Gets a property from the configuration.
     *
     *  @param key property to retrieve
     *  @return value as object. Will return user value if exists,
     *          if not then default value if exists, otherwise null
     */
    public Object getProperty( String key)
    {
        /*
         *  first, try to get from the 'user value' store
         */
        Object o = this.get(key);

        if ( o == null)
        {
            /*
             *  if there isn't a value there, get it from the
             *  defaults if we have them
             */
            if (defaults != null)
            {
                o = defaults.get(key);
            }
        }

        return o;
    }
    
    /**
     * Add a property to the configuration. If it already
     * exists then the value stated here will be added
     * to the configuration entry. For example, if
     *
     * resource.loader = file
     *
     * is already present in the configuration and you
     *
     * addProperty("resource.loader", "classpath")
     *
     * Then you will end up with a Vector like the
     * following:
     *
     * ["file", "classpath"]
     *
     * @param String key
     * @param String value
     */
    public void addProperty(String key, Object token)
    {
        Object o = this.get(key);

        /*
         *  $$$ GMJ
         *  FIXME : post 1.0 release, we need to not assume
         *  that a scalar is a String - it can be an Object
         *  so we should make a little vector-like class
         *  say, Foo that wraps (not extends Vector),
         *  so we can do things like
         *  if ( !( o instanceof Foo) )
         *  so we know it's our 'vector' container
         *
         *  This applies throughout
         */
        
        if (o instanceof String)
        {
            Vector v = new Vector(2);
            v.addElement(o);
            v.addElement(token);
            put(key, v);
        }
        else if (o instanceof Vector)
        {
            ((Vector) o).addElement(token);
        }
        else
        {
            /*
             * This is the first time that we have seen
             * request to place an object in the 
             * configuration with the key 'key'. So
             * we just want to place it directly into
             * the configuration ... but we are going to
             * make a special exception for String objects
             * that contain "," characters. We will take
             * CSV lists and turn the list into a vector of
             * Strings before placing it in the configuration.
             * This is a concession for Properties and the
             * like that cannot parse multiple same key
             * values.
             */
            if (token instanceof String &&
                ((String)token).indexOf(PropertiesTokenizer.DELIMITER) > 0)
            {
                PropertiesTokenizer tokenizer = 
                    new PropertiesTokenizer((String)token);
                    
                while (tokenizer.hasMoreTokens())
                {
                    String value = tokenizer.nextToken();
                   
                    /*
                     * we know this is a string, so make sure it
                     * just goes in rather than risking vectorization
                     * if it contains an escaped comma
                     */
                    addStringProperty(key,value);
                }
            }
            else
            {
                /*
                 * We want to keep track of the order the keys
                 * are parsed, or dynamically entered into
                 * the configuration. So when we see a key
                 * for the first time we will place it in
                 * an ArrayList so that if a client class needs
                 * to perform operations with configuration
                 * in a definite order it will be possible.
                 */

                addPropertyDirect( key, token );
            }                
        }
    }

    /**
     *   Adds a key/value pair to the map.  This routine does
     *   no magic morphing.  It ensures the keylist is maintained
     *
     *  @param key key to use for mapping
     *  @param obj object to store
     */
    private void addPropertyDirect( String key, Object obj )
    {
        /*
         * safety check
         */
        
        if( !containsKey( key ) )
        {
            keysAsListed.add(key);
        }
        
        /*
         * and the value
         */
        put(key, obj);
    }

    /**
     *  Sets a string property w/o checking for commas - used
     *  internally when a property has been broken up into
     *  strings that could contain escaped commas to prevent
     *  the inadvertant vectorization.
     *
     *  Thanks to Leon Messerschmidt for this one.
     *
     */
    private  void addStringProperty(String key, String token)
    {
        Object o = this.get(key);

        /*
         *  $$$ GMJ
         *  FIXME : post 1.0 release, we need to not assume
         *  that a scalar is a String - it can be an Object
         *  so we should make a little vector-like class
         *  say, Foo that wraps (not extends Vector),
         *  so we can do things like
         *  if ( !( o instanceof Foo) )
         *  so we know it's our 'vector' container
         *
         *  This applies throughout
         */

        /*
         *  do the usual thing - if we have a value and 
         *  it's scalar, make a vector, otherwise add
         *  to the vector
         */
 
        if (o instanceof String)
        {
            Vector v = new Vector(2);
            v.addElement(o);
            v.addElement(token);
            put(key, v);
        }
        else if (o instanceof Vector)
        {
            ((Vector) o).addElement(token);
        }
        else
        {
            addPropertyDirect( key, token );
        }
    }

    /**
     * Set a property, this will replace any previously
     * set values. Set values is implicitly a call
     * to clearProperty(key), addProperty(key,value).
     *
     * @param String key
     * @param String value
     */
    public void setProperty(String key, Object value)
    {
        clearProperty(key);
        addProperty(key,value);
    }
    
    /**
     * Save the properties to the given outputstream.
     *
     * @param output An OutputStream.
     * @param header A String.
     * @exception IOException.
     */
    public synchronized void save(OutputStream output,
                                  String Header)
        throws IOException
    {
        if(output != null)
        {
            PrintWriter theWrtr = new PrintWriter(output);
            if(Header != null)
            {
                theWrtr.println(Header);
            }
            Enumeration theKeys = keys();
            while(theKeys.hasMoreElements())
            {
                String key = (String) theKeys.nextElement();
                Object value = get((Object) key);
                if(value != null)
                {
                    if(value instanceof String)
                    {
                        StringBuffer currentOutput = new StringBuffer();
                        currentOutput.append(key);
                        currentOutput.append("=");
                        currentOutput.append((String) value);
                        theWrtr.println(currentOutput.toString());
                    }
                    else if(value instanceof Vector)
                    {
                        Vector values = (Vector) value;
                        Enumeration valuesEnum = values.elements();
                        while(valuesEnum.hasMoreElements())
                        {
                            String currentElement = 
                                   (String) valuesEnum.nextElement();
                            StringBuffer currentOutput = new StringBuffer();
                            currentOutput.append(key);
                            currentOutput.append("=");
                            currentOutput.append(currentElement);
                            theWrtr.println(currentOutput.toString());
                        }
                    }
                }    
                theWrtr.println();
                theWrtr.flush();
            }    
        }        
    }

    /**
     * Combines an existing Hashtable with this Hashtable.
     *
     * Warning: It will overwrite previous entries without warning.
     *
     * @param ExtendedProperties
     */
    public void combine( ExtendedProperties c )
    {
        for (Iterator i = c.getKeys() ; i.hasNext() ;)
        {
            String key = (String) i.next();
            setProperty( key, c.get(key) );
        }
    }
    
    /**
     * Clear a property in the configuration.
     *
     * @param String key to remove along with corresponding value.
     */
    public void clearProperty(String key)
    {
        if (containsKey(key))
        {
            /*
             * we also need to rebuild the keysAsListed or else
             * things get *very* confusing
             */

            for(int i = 0; i < keysAsListed.size(); i++)
            {
                if ( ( (String) keysAsListed.get(i)).equals( key ) )
                {
                    keysAsListed.remove(i);
                    break;
                }
            }

            remove(key);
        }            
    }

    /**
     * Get the list of the keys contained in the configuration
     * repository.
     *
     * @return An Iterator.
     */
    public Iterator getKeys()
    {
        return keysAsListed.iterator();
    }

    /**
     * Get the list of the keys contained in the configuration
     * repository that match the specified prefix.
     *
     * @param prefix The prefix to test against.
     * @return An Iterator of keys that match the prefix.
     */
    public Iterator getKeys(String prefix)
    {
        Iterator keys = getKeys();
        ArrayList matchingKeys = new ArrayList();
        
        while( keys.hasNext() )
        {
            Object key = keys.next();
            
            if( key instanceof String && ((String) key).startsWith(prefix) )
            {
                matchingKeys.add(key);
            }
        }
        return matchingKeys.iterator();
    }

    /**
     * Create an ExtendedProperties object that is a subset
     * of this one. Take into account duplicate keys
     * by using the setProperty() in ExtendedProperties.
     *
     * @param String prefix
     */
    public ExtendedProperties subset(String prefix)
    {
        ExtendedProperties c = new ExtendedProperties();
        Iterator keys = getKeys();
        boolean validSubset = false;
        
        while( keys.hasNext() )
        {
            Object key = keys.next();
            
            if( key instanceof String && ((String) key).startsWith(prefix) )
            {
                if (!validSubset)
                {
                    validSubset = true;
                }
                
                String newKey = null;
                
                /*
                 * Check to make sure that c.subset(prefix) doesn't
                 * blow up when there is only a single property
                 * with the key prefix. This is not a useful
                 * subset but it is a valid subset.
                 */
                if ( ((String)key).length() == prefix.length())
                {
                    newKey = prefix;
                }
                else
                {
                    newKey = ((String)key).substring(prefix.length() + 1);
                }                    
                
                /*
                 *  use addPropertyDirect() - this will plug the data as 
                 *  is into the Map, but will also do the right thing
                 *  re key accounting
                 */

                c.addPropertyDirect( newKey, get(key) );
            }
        }
        
        if (validSubset)
        {
            return c;
        }
        else
        {
            return null;
        }
    }

    /**
     * Display the configuration for debugging
     * purposes.
     */
    public void display()
    {
        Iterator i = getKeys();
        
        while (i.hasNext())
        {
            String key = (String) i.next();
            Object value = get(key);
            System.out.println(key + " => " + value);
        }
    }     

    /**
     * Get a string associated with the given configuration key.
     *
     * @param key The configuration key.
     * @return The associated string.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a String.
     */
    public String getString(String key)
    {
        return getString(key, null);
    }

    /**
     * Get a string associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated string if key is found,
     * default value otherwise.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a String.
     */
    public String getString(String key,
                            String defaultValue)
    {
        Object value = get(key);

        if (value instanceof String)
        {
            return (String) interpolate((String)value);
        }
        else if (value == null)
        {
            if (defaults != null)
            {
                return interpolate(defaults.getString(key, defaultValue));
            }
            else
            {
                return interpolate(defaultValue);
            }
        }
        else if (value instanceof Vector)
        {
            return interpolate((String) ((Vector) value).get(0));
        }
        else
        {
            throw new ClassCastException(
                '\'' + key + "' doesn't map to a String object");
        }
    }

    /**
     * Get a list of properties associated with the given
     * configuration key.
     *
     * @param key The configuration key.
     * @return The associated properties if key is found.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a String/Vector.
     * @exception IllegalArgumentException if one of the tokens is
     * malformed (does not contain an equals sign).
     */
    public Properties getProperties(String key)
    {
        return getProperties(key, new Properties());
    }

    /**
     * Get a list of properties associated with the given
     * configuration key.
     *
     * @param key The configuration key.
     * @return The associated properties if key is found.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a String/Vector.
     * @exception IllegalArgumentException if one of the tokens is
     * malformed (does not contain an equals sign).
     */
    public Properties getProperties(String key,
                                    Properties defaults)
    {
        /*
         * Grab an array of the tokens for this key.
         */
        String[] tokens = getStringArray(key);

        /* 
         * Each token is of the form 'key=value'.
         */
        Properties props = new Properties(defaults);
        for (int i = 0; i < tokens.length; i++)
        {
            String token = tokens[i];
            int equalSign = token.indexOf('=');
            if (equalSign > 0)
            {
                String pkey = token.substring(0, equalSign).trim();
                String pvalue = token.substring(equalSign + 1).trim();
                props.put(pkey, pvalue);
            }
            else
            {
                throw new IllegalArgumentException('\'' + token +
                                                   "' does not contain " +
                                                   "an equals sign");
            }
        }
        return props;
    }

    /**
     * Get an array of strings associated with the given configuration
     * key.
     *
     * @param key The configuration key.
     * @return The associated string array if key is found.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a String/Vector.
     */
    public String[] getStringArray(String key)
    {
        Object value = get(key);

        // What's your vector, Victor?
        Vector vector;
        if (value instanceof String)
        {
            vector = new Vector(1);
            vector.addElement(value);
        }
        else if (value instanceof Vector)
        {
            vector = (Vector)value;
        }
        else if (value == null)
        {
            if (defaults != null)
            {
                return defaults.getStringArray(key);
            }
            else
            {
                return new String[0];
            }
        }
        else
        {
            throw new ClassCastException(
                '\'' + key + "' doesn't map to a String/Vector object");
        }

        String[] tokens = new String[vector.size()];
        for (int i = 0; i < tokens.length; i++)
        {
            tokens[i] = (String)vector.elementAt(i);
        }

        return tokens;
    }

    /**
     * Get a Vector of strings associated with the given configuration
     * key.
     *
     * @param key The configuration key.
     * @return The associated Vector.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a Vector.
     */
    public Vector getVector(String key)
    {
        return getVector(key, null);
    }

    /**
     * Get a Vector of strings associated with the given configuration
     * key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated Vector.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a Vector.
     */
    public Vector getVector(String key,
                            Vector defaultValue)
    {
        Object value = get(key);

        if (value instanceof Vector)
        {
            return (Vector) value;
        }
        else if (value instanceof String)
        {
            Vector v = new Vector(1);
            v.addElement((String) value);
            put(key, v);
            return v;
        }
        else if (value == null)
        {
            if (defaults != null)
            {
                return defaults.getVector(key, defaultValue);
            }
            else
            {
                return ((defaultValue == null) ?
                        new Vector() : defaultValue);
            }
        }
        else
        {
            throw new ClassCastException(
                '\'' + key + "' doesn't map to a Vector object");
        }
    }

    /**
     * Get a boolean associated with the given configuration key.
     *
     * @param key The configuration key.
     * @return The associated boolean.
     * @exception NoSuchElementException is thrown if the key doesn't
     * map to an existing object.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a Boolean.
     */
    public boolean getBoolean(String key)
    {
        Boolean b = getBoolean(key, (Boolean) null);
        if (b != null)
        {
            return b.booleanValue();
        }
        else
        {
            throw new NoSuchElementException(
                '\'' + key + "' doesn't map to an existing object");
        }
    }

    /**
     * Get a boolean associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated boolean.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a Boolean.
     */
    public boolean getBoolean(String key, boolean defaultValue)
    {
        return getBoolean(key, new Boolean(defaultValue)).booleanValue();
    }

    /**
     * Get a boolean associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated boolean if key is found and has valid
     * format, default value otherwise.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a Boolean.
     */
    public Boolean getBoolean(String key, Boolean defaultValue)
    {
    
        Object value = get(key);

        if (value instanceof Boolean)
        {
            return (Boolean) value;
        }
        else if (value instanceof String)
        {
            String s = testBoolean((String)value);
            Boolean b = new Boolean(s);
            put(key, b);
            return b;
        }
        else if (value == null)
        {
            if (defaults != null)
            {
                return defaults.getBoolean(key, defaultValue);
            }
            else
            {
                return defaultValue;
            }
        }
        else
        {
            throw new ClassCastException(
                '\'' + key + "' doesn't map to a Boolean object");
        }
    }
    
    /**
     * Test whether the string represent by value maps to a boolean
     * value or not. We will allow <code>true</code>, <code>on</code>,
     * and <code>yes</code> for a <code>true</code> boolean value, and
     * <code>false</code>, <code>off</code>, and <code>no</code> for
     * <code>false</code> boolean values.  Case of value to test for
     * boolean status is ignored.
     *
     * @param String The value to test for boolean state.
     * @return <code>true</code> or <code>false</code> if the supplied
     * text maps to a boolean value, or <code>null</code> otherwise.
     */
    public String testBoolean(String value)
    {
        String s = ((String)value).toLowerCase();
    
        if (s.equals("true") || s.equals("on") || s.equals("yes"))
        {
            return "true";
        }
        else if (s.equals("false") || s.equals("off") || s.equals("no"))
        {
            return "false";
        }
        else
        {
            return null;
        }
    }

    /**
     * Get a byte associated with the given configuration key.
     *
     * @param key The configuration key.
     * @return The associated byte.
     * @exception NoSuchElementException is thrown if the key doesn't
     * map to an existing object.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a Byte.
     * @exception NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public byte getByte(String key)
    {
        Byte b = getByte(key, null);
        if (b != null)
        {
            return b.byteValue();
        }
        else
        {
            throw new NoSuchElementException(
                '\'' + key + " doesn't map to an existing object");
        }
    }

    /**
     * Get a byte associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated byte.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a Byte.
     * @exception NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public byte getByte(String key,
                        byte defaultValue)
    {
        return getByte(key, new Byte(defaultValue)).byteValue();
    }

    /**
     * Get a byte associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated byte if key is found and has valid
     * format, default value otherwise.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a Byte.
     * @exception NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public Byte getByte(String key,
                        Byte defaultValue)
    {
        Object value = get(key);

        if (value instanceof Byte)
        {
            return (Byte) value;
        }
        else if (value instanceof String)
        {
            Byte b = new Byte((String) value);
            put(key, b);
            return b;
        }
        else if (value == null)
        {
            if (defaults != null)
            {
                return defaults.getByte(key, defaultValue);
            }
            else
            {
                return defaultValue;
            }
        }
        else
        {
            throw new ClassCastException(
                '\'' + key + "' doesn't map to a Byte object");
        }
    }

    /**
     * Get a short associated with the given configuration key.
     *
     * @param key The configuration key.
     * @return The associated short.
     * @exception NoSuchElementException is thrown if the key doesn't
     * map to an existing object.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a Short.
     * @exception NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public short getShort(String key)
    {
        Short s = getShort(key, null);
        if (s != null)
        {
            return s.shortValue();
        }
        else
        {
            throw new NoSuchElementException(
                '\'' + key + "' doesn't map to an existing object");
        }
    }

    /**
     * Get a short associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated short.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a Short.
     * @exception NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public short getShort(String key,
                          short defaultValue)
    {
        return getShort(key, new Short(defaultValue)).shortValue();
    }

    /**
     * Get a short associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated short if key is found and has valid
     * format, default value otherwise.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a Short.
     * @exception NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public Short getShort(String key,
                          Short defaultValue)
    {
        Object value = get(key);

        if (value instanceof Short)
        {
            return (Short) value;
        }
        else if (value instanceof String)
        {
            Short s = new Short((String) value);
            put(key, s);
            return s;
        }
        else if (value == null)
        {
            if (defaults != null)
            {
                return defaults.getShort(key, defaultValue);
            }
            else
            {
                return defaultValue;
            }
        }
        else
        {
            throw new ClassCastException(
                '\'' + key + "' doesn't map to a Short object");
        }
    }

    /**
     * The purpose of this method is to get the configuration resource
     * with the given name as an integer.
     *
     * @param name The resource name.
     * @return The value of the resource as an integer.
     */
    public int getInt(String name)
    {
        return getInteger(name);
    }

    /**
     * The purpose of this method is to get the configuration resource
     * with the given name as an integer, or a default value.
     *
     * @param name The resource name
     * @param def The default value of the resource.
     * @return The value of the resource as an integer.
     */
    public int getInt(String name,
                      int def)
    {
        return getInteger(name, def);
    }

    /**
     * Get a int associated with the given configuration key.
     *
     * @param key The configuration key.
     * @return The associated int.
     * @exception NoSuchElementException is thrown if the key doesn't
     * map to an existing object.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a Integer.
     * @exception NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public int getInteger(String key)
    {
        Integer i = getInteger(key, null);
        if (i != null)
        {
            return i.intValue();
        }
        else
        {
            throw new NoSuchElementException(
                '\'' + key + "' doesn't map to an existing object");
        }
    }

    /**
     * Get a int associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated int.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a Integer.
     * @exception NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public int getInteger(String key,
                          int defaultValue)
    {    
        Integer i = getInteger(key, null);
        
        if (i == null)
        {
            return defaultValue;
        }
        
        return i.intValue();
      }


    /**
     * Get a int associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated int if key is found and has valid
     * format, default value otherwise.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a Integer.
     * @exception NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public Integer getInteger(String key,
                              Integer defaultValue)
    {
        Object value = get(key);

        if (value instanceof Integer)
        {
            return (Integer) value;
        }
        else if (value instanceof String)
        {
            Integer i = new Integer((String) value);
            put(key, i);
            return i;
        }
        else if (value == null)
        {
            if (defaults != null)
            {
                return defaults.getInteger(key, defaultValue);
            }
            else
            {
                return defaultValue;
            }
        }
        else
        {
            throw new ClassCastException(
                '\'' + key + "' doesn't map to a Integer object");
        }
    }

    /**
     * Get a long associated with the given configuration key.
     *
     * @param key The configuration key.
     * @return The associated long.
     * @exception NoSuchElementException is thrown if the key doesn't
     * map to an existing object.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a Long.
     * @exception NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public long getLong(String key)
    {
        Long l = getLong(key, null);
        if (l != null)
        {
            return l.longValue();
        }
        else
        {
            throw new NoSuchElementException(
                '\'' + key + "' doesn't map to an existing object");
        }
    }

    /**
     * Get a long associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated long.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a Long.
     * @exception NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public long getLong(String key,
                        long defaultValue)
    {
        return getLong(key, new Long(defaultValue)).longValue();
    }

    /**
     * Get a long associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated long if key is found and has valid
     * format, default value otherwise.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a Long.
     * @exception NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public Long getLong(String key,
                        Long defaultValue)
    {
        Object value = get(key);

        if (value instanceof Long)
        {
            return (Long) value;
        }
        else if (value instanceof String)
        {
            Long l = new Long((String) value);
            put(key, l);
            return l;
        }
        else if (value == null)
        {
            if (defaults != null)
            {
                return defaults.getLong(key, defaultValue);
            }
            else
            {
                return defaultValue;
            }
        }
        else
        {
            throw new ClassCastException(
                '\'' + key + "' doesn't map to a Long object");
        }
    }

    /**
     * Get a float associated with the given configuration key.
     *
     * @param key The configuration key.
     * @return The associated float.
     * @exception NoSuchElementException is thrown if the key doesn't
     * map to an existing object.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a Float.
     * @exception NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public float getFloat(String key)
    {
        Float f = getFloat(key, null);
        if (f != null)
        {
            return f.floatValue();
        }
        else
        {
            throw new NoSuchElementException(
                '\'' + key + "' doesn't map to an existing object");
        }
    }

    /**
     * Get a float associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated float.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a Float.
     * @exception NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public float getFloat(String key,
                          float defaultValue)
    {
        return getFloat(key, new Float(defaultValue)).floatValue();
    }

    /**
     * Get a float associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated float if key is found and has valid
     * format, default value otherwise.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a Float.
     * @exception NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public Float getFloat(String key,
                          Float defaultValue)
    {
        Object value = get(key);

        if (value instanceof Float)
        {
            return (Float) value;
        }
        else if (value instanceof String)
        {
            Float f = new Float((String) value);
            put(key, f);
            return f;
        }
        else if (value == null)
        {
            if (defaults != null)
            {
                return defaults.getFloat(key, defaultValue);
            }
            else
            {
                return defaultValue;
            }
        }
        else
        {
            throw new ClassCastException(
                '\'' + key + "' doesn't map to a Float object");
        }
    }

    /**
     * Get a double associated with the given configuration key.
     *
     * @param key The configuration key.
     * @return The associated double.
     * @exception NoSuchElementException is thrown if the key doesn't
     * map to an existing object.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a Double.
     * @exception NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public double getDouble(String key)
    {
        Double d = getDouble(key, null);
        if (d != null)
        {
            return d.doubleValue();
        }
        else
        {
            throw new NoSuchElementException(
                '\'' + key + "' doesn't map to an existing object");
        }
    }

    /**
     * Get a double associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated double.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a Double.
     * @exception NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public double getDouble(String key,
                            double defaultValue)
    {
        return getDouble(key, new Double(defaultValue)).doubleValue();
    }

    /**
     * Get a double associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated double if key is found and has valid
     * format, default value otherwise.
     * @exception ClassCastException is thrown if the key maps to an
     * object that is not a Double.
     * @exception NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public Double getDouble(String key,
                            Double defaultValue)
    {
        Object value = get(key);

        if (value instanceof Double)
        {
            return (Double) value;
        }
        else if (value instanceof String)
        {
            Double d = new Double((String) value);
            put(key, d);
            return d;
        }
        else if (value == null)
        {
            if (defaults != null)
            {
                return defaults.getDouble(key, defaultValue);
            }
            else
            {
                return defaultValue;
            }
        }
        else
        {
            throw new ClassCastException(
                '\'' + key + "' doesn't map to a Double object");
        }
    }

    /**
     * Convert a standard properties class into a configuration
     * class.
     *
     * @param p properties object to convert into
     *                   a ExtendedProperties object.
     *
     * @return ExtendedProperties configuration created from the
     *                      properties object.
     */
    public static ExtendedProperties convertProperties(Properties p)
    {
        ExtendedProperties c = new ExtendedProperties();
    
        for (Enumeration e = p.keys(); e.hasMoreElements() ; ) 
        {
            String s = (String) e.nextElement();
            c.setProperty(s, p.getProperty(s));
        }
    
        return c;
    }
}
