/*	ExtResourceBundle.java
 *	Copyright (c) 2002-2004  Brains2B.org
 *
 * 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 name of the author may not be used to endorse or promote products derived from this software 
 * without specific prior written permission. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS 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 AUTHOR 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.
 */
/* @#History
* [When]		[Who]				[What]
* 23-06-2002	dennis@brains2b.org	Check for connection in loadContents,
* 															getContents implemented,
* 															getConnection implemented
* 03-07-2002	dennis@brains2b.org	Improved overwritability 
* 07-08-2002    dennis@brains2b.org	Implemented cache
* 09-08-2002	dennis@brains2b.org	Fixed connection for connections without user
* 01-12-2002	dennis@brains2b.org	loadContents did not call stmt.close();
* 08-03-2003		"	"			removed debug information.
* 15-04-2004						Fixed parent retrieval on getKeys()
*/

package org.brains2b.resx;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.Vector;

/** ResourceBundle that reads Bundles and domains from a database
 *  connection.
 *  
 *  <p>This is a partial rewrite of ResourceBundle to implement
 *  a type of resourcebundle next to List and Properties to take
 *  advantage of a database connection.
 *  <p> It does this by implicitly using properties to create a
 *  connection to a database and then lazily retrieving the information into a
 *  Hashtable.
 *  <p> For reasons of expedience there will be sought first for
 *  a regular ResourceBundle and only then for a database bundle.
 *  This to allow for using the faster ListResourceBundle for
 *  default Locales and a slower database connection for more uncommen
 *  Locales.
 *  <p> The expected table can be any table, but by default is called
 *  LOCALIZED_RESOURCE. It has a fields for the bundleName or Domain,
 *  Key, Value, Country and Language. You can overwrite getSQL to use
 *  your own table.<i>Please make sure that the SQL statement has as first column
 *  the key and as second the value. There are no other restrictions that
 *  I'm aware of.
 *  <p>Due to the finalizing of the getBundle() methods it was not possible
 *  to implement it without creating new static methods getExtBundle()
 *  Needless to say the getBundle() methods will not retrieve any bundles
 *  from the database.
 *
 * @author <A HREF="MAILTO:dennis@brains2b.nl">dennis@brains2b.nl</A>
 * @version 0.15 [21-04-2004]
 */
public class ExtResourceBundle extends ResourceBundle {

    private static Hashtable m_cache = new Hashtable();
    private Hashtable m_contents;
    protected String m_domain;
    protected Locale m_locale;
    protected static Connection m_con;

    /**
     * Constructor for DatabaseBundle.
     */
    public ExtResourceBundle(String baseName, Locale locale) {
        super();
        if (baseName != null) {
            m_domain = baseName.toUpperCase();
        }
        if (locale == null) {
            m_locale = new Locale("", "");
        } else {
            m_locale = locale;
        }

    }

    /**
     * Method getExtBundle.
     * Replacement for getBundle() to get access to Database
     * functions
     * @param baseName, the Name of the Bundle/Domain
     * @return ResourceBundle
     */
    public static ResourceBundle getExtBundle(String baseName) {
        return getBundleImpl(
            new ExtResourceBundle(baseName, Locale.getDefault()));
    }

    /**
     * Method getExtBundle.
     * Replacement for getBundle() to get access to Database
     * @param baseName
     * @param locale
     * @return ResourceBundle
     */
    public static ResourceBundle getExtBundle(String baseName, Locale locale) {
        return getBundleImpl(new ExtResourceBundle(baseName, locale));
    }

    /**
     * Method getBundleImpl.
     * Actual method for getting the Bundle.
     * <UL> sequence is:
     * <LI> try finding a default List or Properties bundle
     * <LI> try to find a database bundle
     * </UL>
     * @param baseName
     * @param locale
     * @return ResourceBundle
     */
    protected static ResourceBundle getBundleImpl(ExtResourceBundle res) {
        try {
            return ResourceBundle.getBundle(res.getBaseName(), res.getLocale());
        } catch (MissingResourceException ex) {
            Locale locale = res.getLocale();
            String baseName = res.getBaseName();
            ExtResourceBundle fromCache = findInCache(baseName, locale);
            if (fromCache != null) {
                return fromCache;
            } else {
                if (m_con == null) {
                    try {
                        res.makeConnection();
                    } catch (SQLException sex) {
                        throw new MissingResourceException(
                            "Could not find ResourceBundle due to:"
                                + sex.getMessage(),
                            ExtResourceBundle.class.getName(),
                            null);
                    }
                }

                ResourceBundle parent = res.getParent();
                res.setParent(parent);
                addToCache(res);
                return res;
            }
        }

    }

	/**
	 * find a resource bundle with given basename and Locale in the cache
	 * @param baseName String, the domain/base name
	 * @param locale Locale, the locale for this bundle
	 * @return ExtResourceBundle, a bundle stored in cache, returns null if non was found
	 * @see #addToCache(ExtResourceBundle)
	 */
    private static ExtResourceBundle findInCache(
        String baseName,
        Locale locale) {
        SoftReference ref =
            (SoftReference) m_cache.get(getCacheKey(baseName, locale));
        if (ref != null) {
            return (ExtResourceBundle) ref.get();
        } else {
            return null;
        }
    }
	
	/**
	 * add a resource bundle to cache. 
	 * <p>Bundles in cache do not have to be recollected from the database and
	 * information can be retrieved faster
	 * @param res ExtResourceBundle
	 */
	private static void addToCache(ExtResourceBundle res) {
        m_cache.put(
            getCacheKey(res.getBaseName(), res.getLocale()),
            new SoftReference(res));
    }

	/**
	 * get a key with which a resource bundle is stored in cache
	 * @param baseName String, the domain/base name for this bundle
	 * @param locale Locale
	 * @return String, a key made up of baseName.language_country
	 */
    private final static String getCacheKey(String baseName, Locale locale) {
        return new StringBuffer()
            .append(baseName)
            .append('.')
            .append(locale.toString())
            .toString();
    }

	/**
	 * get the parent of this resource bundle.
	 * <p>A parent of a resource bundle can be defined
	 * as a less Locale specific resource bundle: <code>en</code>
	 * is the parent of <code>en_GB</code> and has the empy locale
	 * as its owner parent.
	 * @return ResourceBundle, the parent of this resource bundle
	 */
    protected ResourceBundle getParent() {
        ResourceBundle parent = null;
        if (!getLocale().getLanguage().equals("")) {
            if (!getLocale().getCountry().equals("")) {
                Locale pLoc = new Locale(getLocale().getLanguage(), "");
                parent = getExtBundle(getBaseName(), pLoc);
            } else {
                parent = getExtBundle(getBaseName(), new Locale("", ""));
            }
        }
        return parent;
    }

    /**
     * @see ResourceBundle#handleGetObject(String)
     */
    protected Object handleGetObject(String key) {
        if (m_contents == null) {
            loadContents();
            if (m_contents == null) {
                throw new MissingResourceException(
                    "Can't find resource for  " + m_domain + "." + key,
                    m_domain,
                    key);
            }

        }
        return m_contents.get(key);
        
    }

    /**
     * @see ResourceBundle#getKeys()
     */
    public Enumeration getKeys() {
        if (m_contents == null) {
            loadContents();
        }
        if (parent!=null) {
        	Vector v=new Vector(m_contents.keySet());	
        	Enumeration e= parent.getKeys();
        	while (e.hasMoreElements()) {
        		final String key=(String) e.nextElement();
        		if (!v.contains(key)) v.add(key);
        	}
        	return v.elements();
        } else {
        	return m_contents.keys();
        }
        
    }

    /**
     * Make a connection to the database
     * <p>The connection is default made using either:
     * <UL>System.properties 
     * <LI>sql.driver <i> the full classpath name of the driver </i>
     * <LI>sql.url	  <i> the url to connect to the database </i>
     * <LI>sql.user	  <i> the username with which to connect to the database </i>
     * <LI>sql.password <i> the password to connect to the database </i>
     * </UL>
     * <p> or a properties.file in the root of the application
     * named <i>connection.properties</i> in which the four 
     * above properties are defined.
     * <p> Overwrite if you want to use another way of getting 
     * a connection
     * @throws SQLException, in the event a connection could not be established
     */
    protected void makeConnection() throws SQLException {
        String driver = System.getProperty("sql.driver");
        String url = System.getProperty("sql.url");
        String name = System.getProperty("sql.user");
        String password = System.getProperty("sql.password");

        if (driver == null || url == null) {
            Properties prop = new Properties();
            try {
                prop.load(new FileInputStream("connection.properties"));
            } catch (IOException ex) {
                throw new SQLException("Cannot find required connection information");
            }
            driver = prop.getProperty("sql.driver");
            url = prop.getProperty("sql.url");
            name = prop.getProperty("sql.user");
            password = prop.getProperty("sql.password");
        }

        try {
            Class.forName(driver);
        } catch (ClassNotFoundException ex) {
            throw new SQLException("Could not find SQL Driver");
        }
        m_con = DriverManager.getConnection(url, name, password);

    }

    /**
     * Method getSQL.
     * <p>Composes the default query for getting values from
     * a LOCALIZED_RESOURCE table. The query should be defined
     * to get all values from a given base/domain, language and country at once
     * <UL>The table definition is expected to be as Follows:
     * <LI>DOMAIN <tab>	VARCHAR
     * <LI>KEY <tab><tab> VARCHAR
     * <LI>VALUE <tab> VARCHAR
     * <LI>LANGUAGE <tab> VARCHAR
     * <LI>COUNTRY <tab> VARCHAR
     * </UL>
     * <p><i>The lengths are not specified, so you can use your own
     * create scripts are however provided for this table in the 
     * projects package</i></p>
     * <p>Overwrite if you want to use another table
     * @return String, the SQL-query
     */
    public String getSQL() {
        StringBuffer sb = new StringBuffer();
        sb.append("SELECT RESKEY, RESVALUE FROM LOCALIZED_RESOURCE ");
        sb.append("WHERE DOMAIN='" + m_domain + "' ");

        if (m_locale != null) {
            sb.append("AND ");
            if (!m_locale.getLanguage().equals("")) {
                sb.append("LANGUAGE='" + m_locale.getLanguage() + "'");
            } else {
                sb.append("LANGUAGE IS NULL ");
            }

            if (!m_locale.getCountry().equals("")) {
                sb.append(" AND COUNTRY='" + m_locale.getCountry() + "'");
            } else {
                sb.append(" AND COUNTRY IS NULL ");
            }
        }
        return sb.toString();

    }

    /**
     * load the content of the Table into a hashtable which is 
     * used to retrieve the values.
     */
    private synchronized final void loadContents() {

        try {
            m_contents = new Hashtable();
            if (m_con == null) {
                makeConnection();
            }
            Statement stmt = m_con.createStatement();
            ResultSet rs = stmt.executeQuery(getSQL());

            while (rs.next()) {
                m_contents.put(rs.getString(1), (rs.getString(2)!=null?rs.getString(2):""));
            }
            rs.close();
            stmt.close();
        } catch (SQLException ex) {
        	ex.printStackTrace();
            m_contents = new Hashtable();
        }
    }

    /**
     * get the content of this resource bundle as a hashtable
     * @return Hashtable, the content of this resource bundle
     */
    protected Hashtable getContents() {
        if (m_contents == null) {
            loadContents();
        }
        return m_contents;
    }

    /**
     * get the connection to the database from which to collect the resources
     * @return Connection
     * @throws java.sql.SQLException, thrown if the connection to the database
     * fails
     * @see #makeConnection()
     */
    protected Connection getConnection() throws SQLException {
        if (m_con == null) {
            makeConnection();
        }
        return m_con;
    }

    /**
     * get the base name of this resource bundle
     * @return String, the name of the bundle
     */
    public String getBaseName() {
        return m_domain;
    }

    /** 
     * get the locale with which this resource bundle was created
     * <p>This implementation is contrary to ResourceBundle.getLocale() which
     * returns the <code>Locale</code> of the resource bundle which was actually
     * used and could return the <ocde>Locale</code> for the parent 
     * @see ResourceBundle#getLocale()
     */
    public Locale getLocale() {
        return m_locale;
    }
}
