package tap.config;

/*
 * This file is part of TAPLibrary.
 *
 * TAPLibrary 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 3 of the License, or
 * (at your option) any later version.
 *
 * TAPLibrary 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.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with TAPLibrary.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Copyright 2016-2017 - Astronomisches Rechen Institut (ARI)
 */

import static tap.config.TAPConfiguration.DEFAULT_BACKUP_BY_USER;
import static tap.config.TAPConfiguration.DEFAULT_BACKUP_FREQUENCY;
import static tap.config.TAPConfiguration.KEY_BACKUP_BY_USER;
import static tap.config.TAPConfiguration.KEY_BACKUP_FREQUENCY;
import static tap.config.TAPConfiguration.KEY_DATABASE_ACCESS;
import static tap.config.TAPConfiguration.KEY_DATASOURCE_JNDI_NAME;
import static tap.config.TAPConfiguration.KEY_DB_PASSWORD;
import static tap.config.TAPConfiguration.KEY_DB_USERNAME;
import static tap.config.TAPConfiguration.KEY_JDBC_DRIVER;
import static tap.config.TAPConfiguration.KEY_JDBC_URL;
import static tap.config.TAPConfiguration.KEY_SQL_TRANSLATOR;
import static tap.config.TAPConfiguration.VALUE_JDBC;
import static tap.config.TAPConfiguration.VALUE_JDBC_DRIVERS;
import static tap.config.TAPConfiguration.VALUE_JNDI;
import static tap.config.TAPConfiguration.VALUE_MYSQL;
import static tap.config.TAPConfiguration.VALUE_NEVER;
import static tap.config.TAPConfiguration.VALUE_PGSPHERE;
import static tap.config.TAPConfiguration.VALUE_POSTGRESQL;
import static tap.config.TAPConfiguration.VALUE_SQLSERVER;
import static tap.config.TAPConfiguration.VALUE_USER_ACTION;
import static tap.config.TAPConfiguration.getProperty;

import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Enumeration;
import java.util.Properties;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;

import adql.translator.JDBCTranslator;
import adql.translator.MySQLTranslator;
import adql.translator.PgSphereTranslator;
import adql.translator.PostgreSQLTranslator;
import adql.translator.SQLServerTranslator;
import tap.AbstractTAPFactory;
import tap.ServiceConnection;
import tap.TAPException;
import tap.TAPFactory;
import tap.backup.DefaultTAPBackupManager;
import tap.db.DBConnection;
import tap.db.JDBCConnection;
import uws.UWSException;
import uws.service.UWSService;
import uws.service.backup.UWSBackupManager;
import uws.service.log.UWSLog.LogLevel;

/**
 * <p>Concrete implementation of a {@link TAPFactory} which is parameterized by a TAP configuration file.</p>
 *
 * <p>
 * 	All abstract or NULL-implemented methods/functions left by {@link AbstractTAPFactory} are implemented using values
 *  of a TAP configuration file. The concerned methods are: {@link #getConnection(String)}, {@link #freeConnection(DBConnection)},
 *  {@link #destroy()}, {@link #createADQLTranslator()} and {@link #createUWSBackupManager(UWSService)}.
 * </p>
 *
 * @author Gr&eacute;gory Mantelet (ARI)
 * @version 2.1 (08/2017)
 * @since 2.0
 */
public class ConfigurableTAPFactory extends AbstractTAPFactory {

	/* ADQL to SQL translation: */
	/** The {@link JDBCTranslator} to use when a ADQL query must be executed in the database.
	 * This translator is also used to convert ADQL types into database types. */
	protected Class<? extends JDBCTranslator> translator;

	/* JNDI DB access: */
	/** The {@link DataSource} to use in order to access the database.
	 * <em>This attribute is actually used only if the chosen database access method is JNDI.</em> */
	protected final DataSource datasource;

	/* Simple JDBC access: */
	/** Classpath of the JDBC driver to use in order to access the database.
	 * <em>This attribute is actually used only if the chosen database access method is JDBC.</em> */
	protected final String driverPath;
	/** JDBC URL of the database to access.
	 * <em>This attribute is actually used only if the chosen database access method is JDBC.</em> */
	protected final String dbUrl;
	/** Name of the database user to use in order to access the database.
	 * <em>This attribute is actually used only if the chosen database access method is JDBC.</em> */
	protected final String dbUser;
	/** Password of the database user to use in order to access the database.
	 * <em>This attribute is actually used only if the chosen database access method is JDBC.</em> */
	protected final String dbPassword;

	/* UWS's jobs backup: */
	/** Indicate whether the jobs must be backuped gathered by user or just all mixed together. */
	protected boolean backupByUser;
	/** Frequency at which the jobs must be backuped. */
	protected long backupFrequency;

	/**
	 * Build a {@link TAPFactory} using the given TAP service description and TAP configuration file.
	 *
	 * @param service		The TAP service description.
	 * @param tapConfig		The TAP configuration file containing particularly information about the database access.
	 *
	 * @throws NullPointerException	If one of the parameter is NULL.
	 * @throws TAPException			If some properties of the TAP configuration file are wrong.
	 */
	public ConfigurableTAPFactory(ServiceConnection service, final Properties tapConfig) throws NullPointerException, TAPException{
		super(service);

		if (tapConfig == null)
			throw new NullPointerException("Missing TAP properties! ");

		/* 1. Configure the database access */
		final String dbAccessMethod = getProperty(tapConfig, KEY_DATABASE_ACCESS);

		// Case a: Missing access method => error!
		if (dbAccessMethod == null)
			throw new TAPException("The property \"" + KEY_DATABASE_ACCESS + "\" is missing! It is required to connect to the database. Two possible values: \"" + VALUE_JDBC + "\" and \"" + VALUE_JNDI + "\".");

		// Case b: JDBC ACCESS
		else if (dbAccessMethod.equalsIgnoreCase(VALUE_JDBC)){
			// Extract the DB type and deduce the JDBC Driver path:
			String jdbcDriver = getProperty(tapConfig, KEY_JDBC_DRIVER);
			String dbUrl = getProperty(tapConfig, KEY_JDBC_URL);
			if (jdbcDriver == null){
				if (dbUrl == null)
					throw new TAPException("The property \"" + KEY_JDBC_URL + "\" is missing! Since the choosen database access method is \"" + VALUE_JDBC + "\", this property is required.");
				else if (!dbUrl.startsWith(JDBCConnection.JDBC_PREFIX))
					throw new TAPException("JDBC URL format incorrect! It MUST begins with " + JDBCConnection.JDBC_PREFIX);
				else{
					String dbType = dbUrl.substring(JDBCConnection.JDBC_PREFIX.length());
					if (dbType.indexOf(':') <= 0)
						throw new TAPException("JDBC URL format incorrect! Database type name is missing.");
					dbType = dbType.substring(0, dbType.indexOf(':'));

					jdbcDriver = VALUE_JDBC_DRIVERS.get(dbType);
					if (jdbcDriver == null)
						throw new TAPException("No JDBC driver known for the DBMS \"" + dbType + "\"!");
				}
			}
			// Set the DB connection parameters:
			this.driverPath = jdbcDriver;
			this.dbUrl = dbUrl;
			this.dbUser = getProperty(tapConfig, KEY_DB_USERNAME);
			this.dbPassword = getProperty(tapConfig, KEY_DB_PASSWORD);
			// Set the other DB connection parameters:
			this.datasource = null;
		}
		// Case c: JNDI ACCESS
		else if (dbAccessMethod.equalsIgnoreCase(VALUE_JNDI)){
			// Get the datasource JDNI name:
			String dsName = getProperty(tapConfig, KEY_DATASOURCE_JNDI_NAME);
			if (dsName == null)
				throw new TAPException("The property \"" + KEY_DATASOURCE_JNDI_NAME + "\" is missing! Since the choosen database access method is \"" + VALUE_JNDI + "\", this property is required.");
			try{
				// Load the JNDI context:
				InitialContext cxt = new InitialContext();
				// Look for the specified datasource:
				datasource = (DataSource)cxt.lookup(dsName);
				if (datasource == null)
					throw new TAPException("No datasource found with the JNDI name \"" + dsName + "\"!");
				// Set the other DB connection parameters:
				this.driverPath = null;
				this.dbUrl = null;
				this.dbUser = null;
				this.dbPassword = null;
			}catch(NamingException ne){
				throw new TAPException("No datasource found with the JNDI name \"" + dsName + "\"!");
			}
		}
		// Case d: unsupported value
		else
			throw new TAPException("Unsupported value for the property " + KEY_DATABASE_ACCESS + ": \"" + dbAccessMethod + "\"! Allowed values: \"" + VALUE_JNDI + "\" or \"" + VALUE_JDBC + "\".");

		/* 2. Set the ADQLTranslator to use in function of the sql_translator property */
		String sqlTranslator = getProperty(tapConfig, KEY_SQL_TRANSLATOR);
		// case a: no translator specified
		if (sqlTranslator == null)
			throw new TAPException("The property \"" + KEY_SQL_TRANSLATOR + "\" is missing! ADQL queries can not be translated without it. Allowed values: \"" + VALUE_POSTGRESQL + "\", \"" + VALUE_PGSPHERE + "\" or a class path of a class implementing SQLTranslator.");

		// case b: PostgreSQL translator
		else if (sqlTranslator.equalsIgnoreCase(VALUE_POSTGRESQL))
			translator = PostgreSQLTranslator.class;

		// case c: PgSphere translator
		else if (sqlTranslator.equalsIgnoreCase(VALUE_PGSPHERE))
			translator = PgSphereTranslator.class;

		// case d: SQLServer translator
		else if (sqlTranslator.equalsIgnoreCase(VALUE_SQLSERVER))
			translator = SQLServerTranslator.class;

		// case e: MySQL translator
		else if (sqlTranslator.equalsIgnoreCase(VALUE_MYSQL))
			translator = MySQLTranslator.class;

		// case f: a client defined ADQLTranslator (with the provided class name)
		else if (TAPConfiguration.isClassName(sqlTranslator))
			translator = TAPConfiguration.fetchClass(sqlTranslator, KEY_SQL_TRANSLATOR, JDBCTranslator.class);

		// case g: unsupported value
		else
			throw new TAPException("Unsupported value for the property " + KEY_SQL_TRANSLATOR + ": \"" + sqlTranslator + "\" !");

		/* 3. Test the construction of the ADQLTranslator */
		createADQLTranslator();

		/* 4. Test the DB connection (note: a translator is needed to create a connection) */
		DBConnection dbConn = getConnection("0");
		freeConnection(dbConn);

		/* 5. Set the UWS Backup Parameter */
		// Set the backup frequency:
		String propValue = getProperty(tapConfig, KEY_BACKUP_FREQUENCY);
		// determine whether the value is a time period ; if yes, set the frequency:
		if (propValue != null){
			try{
				backupFrequency = Long.parseLong(propValue);
				if (backupFrequency <= 0)
					backupFrequency = DEFAULT_BACKUP_FREQUENCY;
			}catch(NumberFormatException nfe){
				// if the value was not a valid numeric time period, try to identify the different textual options:
				if (propValue.equalsIgnoreCase(VALUE_NEVER))
					backupFrequency = DefaultTAPBackupManager.MANUAL;
				else if (propValue.equalsIgnoreCase(VALUE_USER_ACTION))
					backupFrequency = DefaultTAPBackupManager.AT_USER_ACTION;
				else
					throw new TAPException("Long expected for the property \"" + KEY_BACKUP_FREQUENCY + "\", instead of: \"" + propValue + "\"!");
			}
		}else
			backupFrequency = DEFAULT_BACKUP_FREQUENCY;
		// Specify whether the backup must be organized by user or not:
		propValue = getProperty(tapConfig, KEY_BACKUP_BY_USER);
		backupByUser = (propValue == null) ? DEFAULT_BACKUP_BY_USER : Boolean.parseBoolean(propValue);
	}

	/**
	 * Build a {@link JDBCTranslator} instance with the given class ({@link #translator} ;
	 * specified by the property sql_translator). If the instance can not be build,
	 * whatever is the reason, a TAPException MUST be thrown.
	 *
	 * Note: This function is called at the initialization of {@link ConfigurableTAPFactory}
	 * in order to check that a translator can be created.
	 */
	protected JDBCTranslator createADQLTranslator() throws TAPException{
		try{
			return translator.getConstructor().newInstance();
		}catch(Exception ex){
			if (ex instanceof TAPException)
				throw (TAPException)ex;
			else
				throw new TAPException("Impossible to create a JDBCTranslator instance with the empty constructor of \"" + translator.getName() + "\" (see the property " + KEY_SQL_TRANSLATOR + ") for the following reason: " + ex.getMessage());
		}
	}

	/**
	 * Build a {@link JDBCConnection} thanks to the database parameters specified
	 * in the TAP configuration file (the properties: jdbc_driver_path, db_url, db_user, db_password).
	 *
	 * @see JDBCConnection#JDBCConnection(java.sql.Connection, JDBCTranslator, String, tap.log.TAPLog)
	 * @see JDBCConnection#JDBCConnection(String, String, String, String, JDBCTranslator, String, tap.log.TAPLog)
	 */
	@Override
	public DBConnection getConnection(String jobID) throws TAPException{
		if (datasource != null){
			try{
				return new JDBCConnection(datasource.getConnection(), createADQLTranslator(), jobID, this.service.getQueryExecutor(), this.service.getLogger());
			}catch(SQLException se){
				throw new TAPException("Impossible to establish a connection to the database using the set up datasource!", se);
			}
		}else
			return new JDBCConnection(driverPath, dbUrl, dbUser, dbPassword, createADQLTranslator(), jobID, this.service.getQueryExecutor(), this.service.getLogger());
	}

	@Override
	public void freeConnection(DBConnection conn){
		try{
			// End properly any query that is not yet stopped and cleaned (i.e. no more transaction opened):
			conn.endQuery();
			// Close the connection (if a connection pool is used, the connection is not really closed but is freed and kept in the pool for further usage):
			((JDBCConnection)conn).getInnerConnection().close();
		}catch(SQLException se){
			service.getLogger().error("Can not close properly the connection \"" + conn.getID() + "\"!", se);
		}
	}

	@Override
	public void destroy(){
		// Unregister the JDBC driver, only if registered by the library (i.e. database_access=jdbc):
		if (dbUrl != null){
			// Now deregister JDBC drivers in this context's ClassLoader:
			// Get the webapp's ClassLoader
			ClassLoader cl = Thread.currentThread().getContextClassLoader();
			// Loop through all drivers
			Enumeration<Driver> drivers = DriverManager.getDrivers();
			while(drivers.hasMoreElements()){
				Driver driver = drivers.nextElement();
				if (driver.getClass().getClassLoader() == cl){
					// This driver was registered by the webapp's ClassLoader, so deregister it:
					try{
						DriverManager.deregisterDriver(driver);
						service.getLogger().logTAP(LogLevel.INFO, null, "STOP", "JDBC driver " + driver.getClass().getName() + " successfully deregistered!", null);
					}catch(SQLException ex){
						service.getLogger().logTAP(LogLevel.FATAL, null, "STOP", "Error deregistering JDBC driver " + driver.getClass().getName() + "!", ex);
					}
				}
			}
		}
	}

	/**
	 * Build an {@link DefaultTAPBackupManager} thanks to the backup manager parameters specified
	 * in the TAP configuration file (the properties: backup_frequency, backup_by_user).
	 *
	 * Note: If the specified backup_frequency is negative, no backup manager is returned.
	 *
	 * @return	null if the specified backup frequency is negative, or an instance of {@link DefaultTAPBackupManager} otherwise.
	 *
	 * @see tap.AbstractTAPFactory#createUWSBackupManager(uws.service.UWSService)
	 * @see DefaultTAPBackupManager
	 */
	@Override
	public UWSBackupManager createUWSBackupManager(UWSService uws) throws TAPException{
		try{
			return (backupFrequency < 0) ? null : new DefaultTAPBackupManager(uws, backupByUser, backupFrequency);
		}catch(UWSException ex){
			throw new TAPException("Impossible to create a backup manager, because: " + ex.getMessage(), ex);
		}
	}

}
