/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2003-2006, Geotools Project Managment Committee (PMC)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation; either
 *    version 2.1 of the License, or (at your option) any later version.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.data.oracle;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

import javax.sql.DataSource;

import org.geotools.data.DataSourceException;
import org.geotools.data.DataStore;
import org.geotools.data.DataStoreFactorySpi;
import org.geotools.data.jdbc.datasource.DataSourceUtil;
import org.geotools.data.jdbc.datasource.ManageableDataSource;

/**
 * Creates a PostgisDataStore baed on the correct params.
 * <p>
 * This factory should be registered in the META-INF/ folder, under services/
 * in the DataStoreFactorySpi file.
 * </p>
 *
 * @author Jody Garnett, Refractions Research
 * @author Sean Geoghegan, Defence Science and Technology Organisation
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/unsupported/oracle-spatial/src/main/java/org/geotools/data/oracle/OracleDataStoreFactory.java $
 */
public class OracleDataStoreFactory implements DataStoreFactorySpi {
	private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geotools.data.oracle");     
    private static final String JDBC_DRIVER = "oracle.jdbc.driver.OracleDriver";
    private static final String JDBC_PATH = "jdbc:oracle:thin:@";

    /**
     * Creates a new instance of OracleDataStoreFactory
     */
    public OracleDataStoreFactory() {

    }

    /**
     * Determines whether DataStore created by this factory can process the
     * parameters.
     * <p>
     * Required Parameters are:
     * </p>
     * <ul>
     * <li>
     * <code>dbtype</code> - must equal "oracle"
     * </li>
     * <li>
     * <code>host</code>
     * </li>
     * <li>
     * <code>port</code>
     * </li>
     * <li>
     * <code>user</code>
     * </li>
     * <li>
     * <code>passwd</code>
     * </li>
     * <li>
     * <code>instance</code>
     * </li>     
     * </ul>
     * 
     * <p>
     * There are no defaults since each parameter must be explicitly defined by the user, or
     * another DataSourceFactorySpi should be used. This behaviour is defined in the
     * DataStoreFactorySpi contract.
     * </p>
     *
     * @param params The parameter to check.
     *
     * @return True if all the required parameters are supplied.
     */
    public boolean canProcess(Map params) {
        if (params != null) {
            if (params.get("dbtype") == null || !params.get("dbtype").toString().equalsIgnoreCase("oracle")) {
                return false; //short circuit test
            }
            Param arrayParameters[] = getParametersInfo();
            for (int i = 0; i < arrayParameters.length; i++) {
            	Param param = arrayParameters[i];
            	Object value;
            	if( !params.containsKey( param.key ) ){
            		if( param.required ){
            			return false; // missing required key!
            		}
            		else {
            			continue;
            		}
            	}
				try {
					value = param.lookUp( params );
				} catch (IOException e) {
					LOGGER.warning( param.key+":"+e );
					// could not upconvert/parse to expected type!
					// even if this parameter is not required
					// we are going to refuse to process
					// these params
					return false; 
				}
				if( value == null ){					
					if (param.required) {
                        return (false);
                    }
                }
				else {
					if ( !param.type.isInstance( value )){
						return false; // value was not of the required type
					}
				}
            }
        } else {
            return (false);
        }
        if (!(((String) params.get("dbtype")).equalsIgnoreCase("oracle"))) {
            return (false);
        } else {
            return (true);
        }
    }
    /**
     * Construct a postgis data store using the params.
     *
     * @param params The full set of information needed to construct a live
     *        data source.  Should have  dbtype equal to postgis, as well as
     *        host, user, passwd, database, and table.
     *
     * @return The created DataSource, this may be null if the required
     *         resource was not found or if insufficent parameters were given.
     *         Note that canProcess() should have returned false if the
     *         problem is to do with insuficent parameters.
     *
     * @throws DataSourceException Thrown if there were any problems creating
     *         or connecting the datasource.
     */
    public DataStore createDataStore(Map params) throws IOException {
        /* There are no defaults here. Calling canProcess verifies that
         * all these variables exist.
         */
        String host = (String) HOST.lookUp( params );
        Integer port = (Integer) PORT.lookUp( params );
        String instance = (String) INSTANCE.lookUp( params );
        String user = (String) USER.lookUp( params );
        String passwd = (String) PASSWD.lookUp( params );
        String schema = (String) SCHEMA.lookUp( params ); // checks uppercase
        String namespace = (String) NAMESPACE.lookUp( params );
        String dbtype = (String) DBTYPE.lookUp( params );
        Integer maxConn = (Integer) MAXCONN.lookUp(params);
        Integer minConn = (Integer) MINCONN.lookUp(params);
        Boolean validateConn = (Boolean) VALIDATECONN.lookUp(params);
        
        if( !"oracle".equals( dbtype )){
            throw new IOException( "Parameter 'dbtype' must be oracle");
        }
        
        if (!canProcess(params)) {
            throw new IOException("Cannot connect using provided parameters");
        }
        
        
        boolean validate = validateConn != null && validateConn.booleanValue();
        int maxActive = maxConn != null ? maxConn.intValue() : 10;
        int maxIdle = minConn != null ? minConn.intValue() : 4;
        DataSource source = getDefaultDataSource(host, user, passwd, port.intValue(), instance, maxActive, maxIdle, validate);
        OracleDataStore dataStore = new OracleDataStore(source, namespace, schema, new HashMap());

        return dataStore;
    }
    
    public static ManageableDataSource getDefaultDataSource(String host, String user, String passwd, int port, String instance, int maxActive, int minIdle, boolean validate) throws DataSourceException {
        String dbUrl = null;
        if( instance.startsWith("(") )
            dbUrl = JDBC_PATH + instance;
        else if( instance.startsWith("/") )
            dbUrl = JDBC_PATH + "//" + host + ":" + port + instance;
        else
            dbUrl = JDBC_PATH + host + ":" + port + ":" + instance;
        
        return DataSourceUtil.buildDefaultDataSource(dbUrl, JDBC_DRIVER, user, passwd, maxActive, minIdle, validate ? "select sysdate from dual" : null, false, 0);
    }
    
    /**
     * Oracle cannot create a new database.
     * @param params
     * @throws UnsupportedOperationException Cannot create new database
     */
    public DataStore createNewDataStore(Map params) throws IOException {
        throw new UnsupportedOperationException("Oracle cannot create a new Database");
    }

    public String getDisplayName() {
        return "Oracle";
    }
    /**
     * Describe the nature of the datastore constructed by this factory.
     *
     * @return A human readable description that is suitable for inclusion in a
     *         list of available datasources.
     */
    public String getDescription() {
        return "Oracle Spatial Database";
    }
//	public DataSourceMetadataEnity createMetadata( Map params ) throws IOException {
//	    String host = (String) HOST.lookUp( params );
//        String port = (String) PORT.lookUp( params );
//        String instance = (String) INSTANCE.lookUp( params );
//        String user = (String) USER.lookUp( params );
//        String schema = (String) SCHEMA.lookUp( params ); // checks uppercase        
//        return new DataSourceMetadataEnity( host+":"+port, instance, "Connect to oracle using schema '"+schema+"' as  "+user );
//	}
    /**
     * Returns whether the OracleDataStoreFactory would actually be able to
     * generate a DataStore.  Depends on whether the appropriate libraries
     * are on the classpath.  For now just checks for the presence of the
     * JDBC driver, should probably check for SDOAPI as well.
     * 
     * @return True if the classes to make an oracle connection are present.
     * @task Figure out a class to check the SDOAPI for, and check it.
     */
    public boolean isAvailable() {
        try {
            Class.forName(JDBC_DRIVER);
        } catch (ClassNotFoundException cnfe) {
            return false;
        } 
        return true;
        //check for sdoapi too?
    }

    static final Param DBTYPE = new Param("dbtype", String.class, "This must be 'oracle'.", true,"oracle");
    static final Param HOST = new Param("host", String.class, "The host name of the server.", true);
    static final Param PORT = new Param("port", Integer.class, "The port oracle is running on. " +
            "(Default is 1521)", true, "1521");    
    static final Param USER = new Param("user", String.class, "The user name to log in with.", true);
    static final Param PASSWD = new Param("passwd", String.class, "The password.", true);
    public static final Param MAXCONN = new Param("max connections", Integer.class,
            "maximum number of open connections", false, new Integer(10));
    public static final Param MINCONN = new Param("min connections", Integer.class,
            "minimum number of pooled connection", false, new Integer(4));
    public static final Param VALIDATECONN = new Param("validate connections", Boolean .class,
            "check connection is alive before using it", false, Boolean.FALSE);
    static final Param INSTANCE = new Param("instance", String.class, "The name of the Oracle instance to connect to.", true);
    
    /** Apparently Schema must be uppercase */
    static final Param SCHEMA = new Param("schema", String.class, "The schema name to narrow down the exposed tables (must be upper case).", false){
        public Object lookUp(Map map) throws IOException {
            if (!map.containsKey(key)) {
                if (required) {
                    throw new IOException("Parameter " + key + " is required:"
                        + description);
                } else {
                    return null;
                }
            }
            Object value = map.get(key);
            if (value == null) {
                return null;
            }
            if (value instanceof String) {
                String text = (String) value;
                if( text == null ){
                    return null;
                }
                else if( text.equals( text.toUpperCase() ) ){
                    return text;
                }
                else {
                    throw new IOException("Schema must be supplied in uppercase" );
                }                
            }
            else {
                throw new IOException( "String required for parameter " + key + ": not "
                    + value.getClass().getName());
            }
        }
    };
    static final Param NAMESPACE = new Param("namespace", String.class, "The namespace to give the DataStore", false);
    /**
     * Describe parameters.
     * 
     * @see org.geotools.data.DataStoreFactorySpi#getParametersInfo()
     */
    public Param[] getParametersInfo() {
        return new Param[]{
            DBTYPE,            
            HOST,
            PORT,
            USER,
            PASSWD,
            INSTANCE,
            MAXCONN, 
            MINCONN, 
            VALIDATECONN, 
            SCHEMA,
            NAMESPACE
        };                
    }
 
    /**
     * Returns the implementation hints. The default implementation returns en empty map.
     */
    public Map getImplementationHints() {
        return Collections.EMPTY_MAP;
    }
}
