package es.ejie.frmk.conectores.conectorJDBC;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;

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

import org.apache.commons.collections.Closure;
import org.apache.commons.collections.CollectionUtils;

import es.ejie.frmk.Q70Factoria;
import es.ejie.frmk.conectores.Q70ConectorBase;
import es.ejie.frmk.control.excepciones.Q70FrameworkException;
import es.ejie.frmk.control.excepciones.Q70GestorDeExcepcionesServidor;
import es.ejie.frmk.infraestructura.logs.base.Q70DatosComunes;
import es.ejie.frmk.infraestructura.logs.base.Q70GestorLogsServidor;
import es.ejie.frmk.infraestructura.logs.base.interfaces.Q70GestorDatosComunes;
import es.ejie.frmk.infraestructura.logs.incidencias.Q70LogIncidenciasInterface;
import es.ejie.frmk.infraestructura.traza.Q70TraceLevel;
import es.ejie.frmk.infraestructura.traza.Q70TrazaInterface;

/**
 * El <b>ConectorJDBC</b> es el proveedor de conexiones JDBC a bases de datos relacionales.
 * 
 * Es el punto único proveedor de conexiones JDBC, 
 * basándose en la infraestructura de pools de conexiones manejados por el servidor de aplicaciones WebLogic.
 * Este componente trata, a través de un nombre de DataSource dado, de recuperar el objeto DataSource 
 * correspondiente a dicho nombre del directorio JNDI. Después, almacenará dicho objeto en memoria 
 * para que en posteriores peticiones no tener que volver a consultar el JNDI.
 * <p>
 * El componente devuelve una conexión JDBC perteneciente al DataSource requerido por la clase cliente. 
 * Esta conexión estará lista para ser utilizada y la propia clase cliente será la encargada de cerrarla 
 * cuando considere necesario.
 * @author UFRM006
 */
public class Q70ConectorJDBC extends Q70ConectorBase
{
	private static final String ID_COMPONENTE = "CONECTORJDBC";

	protected static final String VOLUMEN_CONECTORJDBC_CACHE = "volumenConectorJdbcCache";
	protected static final String VOLUMEN_THREAD_CACHE = "volumenThreadCache";
	protected static final String LIMITE_CONEXIONES_CACHE = "limiteControladorConexionesCache";

	private static Q70ConectorJDBC conectorJDBC = null;
	private Map conectorJdbcCache = null;

	private int limiteControladorConexionesCacheadas = 0;
	private int contadorConexionesCacheadas = 0;
	private Map threadsCached = null;

	private class ConectorJDBCKey
	{
		private String initialContextKey;
		private String dataSourceName;

		public ConectorJDBCKey(String initialContextKey, String dataSourceName)
		{
			super();
			this.initialContextKey = initialContextKey;
			this.dataSourceName = dataSourceName;
		}
		public String getDataSourceName()
		{
			if (dataSourceName == null)
				dataSourceName = "";
			return dataSourceName;
		}
		public String getInitialContextKey()
		{
			if (initialContextKey == null)
				initialContextKey = "";
			return initialContextKey;
		}
		public boolean equals(Object obj)
		{
			if (this == obj)
				return true;
			ConectorJDBCKey key = null;
			try
			{
				key = (ConectorJDBCKey) obj;
			}
			catch (ClassCastException e)
			{
				return false;
			}
			if (!getInitialContextKey().equals(key.getInitialContextKey()) || !getDataSourceName().equals(key.getDataSourceName()))
			{
				return false;
			}
			return true;
		}
		public int hashCode()
		{
			Integer rdo = new Integer(getInitialContextKey().hashCode() + getDataSourceName().hashCode());
			return rdo.intValue();
		}
	}

	private Q70ConectorJDBC()
	{
		super();
	}

	/**
	 * Devuelve la referencia al Componente. Esta referencia es unica y no puede ser instanciada por el Constructor, ya que este es privado.
	 * @return Referencia al Componente
	 */
	public static Q70ConectorJDBC getSingleton()
	{
		if (conectorJDBC == null)
		{
			conectorJDBC = new Q70ConectorJDBC();
		}
		return conectorJDBC;
	}

	/**
	 * @see es.ejie.frmk.control.arranque.Q70Arrancable#beforeDestroy()
	 */
	public void beforeDestroy()
	{
		Q70TrazaInterface traza = Q70Factoria.getTraza();
		if (traza.isTraceActive())
		{
			traza.trace(ID_COMPONENTE, Q70TraceLevel.INFO, "beforeDestroy():" + this.getClass().toString());
		}
		removeAllThreadResources();
	}

	/**
	 * @see es.ejie.frmk.control.arranque.Q70Arrancable#reConfigure()
	 */
	public void reConfigure() throws Q70FrameworkException
	{
		Q70TrazaInterface traza = Q70Factoria.getTraza();
		if (traza.isTraceActive())
		{
			traza.trace(ID_COMPONENTE, Q70TraceLevel.INFO, "reConfigure():" + this.getClass().toString());
		}
		try {
			beforeDestroy();
			configure();
		} catch (Exception e) {
			if (traza.isTraceActive()){
				traza.trace(ID_COMPONENTE, Q70TraceLevel.ERROR, "Se ha producido un error en el método reConfigure de la clase Q70ConectorJDBC");
			}
			throw new Q70FrameworkException(ID_COMPONENTE,this.getClass().toString(),"reConfigure","Se ha producido un error en el método reConfigure.",e);
		}
	}

	protected void configure() throws Q70FrameworkException
	{		
		try {
			Q70Configurador conf = null;
			if (fileNameConfiguracion != null)
			{
				conf = new Q70Configurador(fileNameConfiguracion);
				conf.lanzarConfigurador();
			}
			this.contextos = conf.getContextos();
			
			int initialCapacity = Integer.parseInt("" + conf.getConfiguration().get(VOLUMEN_CONECTORJDBC_CACHE));
			float loadFactor = (float) 0.5;
			conectorJdbcCache = new HashMap(initialCapacity, loadFactor);
			
			initialCapacity = Integer.parseInt("" + conf.getConfiguration().get(VOLUMEN_THREAD_CACHE));
			threadsCached = new Hashtable(initialCapacity);
			
			limiteControladorConexionesCacheadas = Integer.parseInt("" + conf.getConfiguration().get(LIMITE_CONEXIONES_CACHE));
		} catch (Exception e) {
			Q70TrazaInterface traza = Q70Factoria.getTraza();
			if (traza.isTraceActive()){
				traza.trace(ID_COMPONENTE, Q70TraceLevel.ERROR, "Se ha producido un error en el método configure de la clase Q70ConectorJDBC");
			}
			throw new Q70FrameworkException(ID_COMPONENTE,this.getClass().toString(),"configure","Se ha producido un error en el método configure.",e);
		}
	}

	/**
	 * Devuelve una conexión perteneciente al DataSource correspondiente al nombre JNDI pasado por parámetro.
	 * Se le pasa también en qué máquina se quiere consultar el árbol JNDI.
	 * 
	 * @param initialContextKey Identificativo del Contexto inicial definido en el fichero de configuracion del componente a usar en la busqueda JNDI
	 * @param dataSourceName Nombre JNDI del DataSource
	 * @return Una conexion al DataSource requerido
	 * @throws SQLException 
	 * @throws NamingException
	 */
	public Connection getConnection(String initialContextKey, String dataSourceName) throws SQLException, NamingException
	{
		return getConnection0(initialContextKey, dataSourceName, null, null);
	}
	/**
	 * Devuelve una conexión perteneciente al DataSource correspondiente al nombre JNDI pasado por parámetro. 
	 * Busca en el árbol JNDI de la propia máquina donde se está ejecutando.
	 * 
	 * @param dataSourceName Nombre JNDI del DataSource
	 * @return Una conexion al DataSource requerido
	 * @throws SQLException
	 * @throws NamingException
	 */
	public Connection getConnection(String dataSourceName) throws SQLException, NamingException
	{
		return getConnection(null, dataSourceName);
	}
	/**
	 * Devuelve una conexión perteneciente al DataSource correspondiente al nombre JNDI pasado por parámetro. 
	 * Busca en el árbol JNDI de la propia máquina donde se está ejecutando. 
	 * Permite aportar un usuario y una password para obtener la conexión.
	 * 
	 * @param initialContextKey Identificativo del Contexto inicial definido en el fichero de configuracion del componente a usar en la busqueda JNDI
	 * @param dataSourceName Nombre JNDI del DataSource
	 * @param idUsuario Identificativo del usuario de bbdd
	 * @param password Password del usuario de bbdd
	 * @return Una conexion al DataSource requerido
	 * @throws SQLException
	 * @throws NamingException
	 */
	public Connection getConnection(String initialContextKey, String dataSourceName, String idUsuario, String password)
		throws SQLException, NamingException
	{
		return getConnection0(initialContextKey, dataSourceName, idUsuario, password);
	}

	/**
	 * Devuelve una conexión perteneciente al DataSource correspondiente al nombre JNDI pasado por parámetro. 
	 * Busca en el árbol JNDI de la propia máquina donde se está ejecutando. 
	 * Permite aportar un usuario y una password para obtener la conexión.
	 * 
	 * @param dataSourceName Nombre JNDI del DataSource
	 * @param idUsuario Identificativo del usuario de bbdd
	 * @param password Password del usuario de bbdd
	 * @return Una conexion al DataSource requerido
	 * @throws SQLException
	 * @throws NamingException
	 */
	public Connection getConnection(String dataSourceName, String idUsuario, String password) throws SQLException, NamingException
	{
		return getConnection(null, dataSourceName, idUsuario, password);
	}

	/**
	 * Devuelve una lista de objetos de <code>Q70DatosDeThread</code> 
	 * con todos los hilos de ejecución activos de Geremua en este momento,
	 * 
	 * @return Lista de <code>Q70DatosDeThread</code>
	 */
	public List getAllThreadResources()
	{
		return new ArrayList(threadsCached.values());
	}

	/**
	 * Elimina todos los hilos de ejecución actual de la lista de hilos de ejecución activos de Geremua, 
	 * que es almacenada en el Componente. 
	 * Previamente, cierra todas las conexiones JDBC que estuviesen abiertas.
	 */
	public void removeAllThreadResources()
	{
		Set listaHilos = new HashSet(threadsCached.keySet());
		CollectionUtils.forAllDo(listaHilos, new Closure()
		{
			public void execute(Object input)
			{
				removeThreadResources0(input, false);
			}
		});
	}

	/**
	 * Elimina el hilo de ejecución actual de la lista de hilos de ejecución activos de Geremua, 
	 * que es almacenada en el Componente. 
	 * Previamente, cierra todas las conexiones JDBC que estuviesen abiertas.
	 */
	public void removeCurrentThreadResources()
	{
		removeThreadResources0(getThreadCachedKey(),true);
	}

	/**
	 * Elimina el hilo de ejecución pasado por parámetro de la lista de hilos de ejecución activos de Geremua, 
	 * que es almacenada en el Componente. 
	 * Previamente, cierra todas las conexiones JDBC que estuviesen abiertas.
	 * 
	 * @param hiloKey Hilo de ejecucion con las conexiones a eliminar.
	 */
	public void removeThreadResources(Object hiloKey)
	{
		removeThreadResources0(hiloKey, false);
	}

	private void removeThreadResources0(Object hiloKey, boolean debeGrabarIncidencia)
	{
		if (threadsCached.containsKey(hiloKey))
		{
			Q70TrazaInterface traza = Q70Factoria.getTraza();
			final List listaConexiones = new ArrayList(((Q70DatosDeThread) threadsCached.get(hiloKey)).getListaConexiones());
			cerrarConexionesThreads(listaConexiones, debeGrabarIncidencia);
			threadsCached.remove(hiloKey);
			if (traza.isTraceActive())
			{
				traza.trace(ID_COMPONENTE, Q70TraceLevel.DEBUG, "removeThreadResources0():" + hiloKey);
			}
		}
	}

	private Object getThreadCachedKey()
	{
		return Thread.currentThread().getName();
	}

	private DataSource getDataSource(String initialContextKey, String dataSourceName) throws NamingException
	{
		Q70TrazaInterface traza = Q70Factoria.getTraza();
		ConectorJDBCKey conectorkey = new ConectorJDBCKey(initialContextKey, dataSourceName);
		if (conectorJdbcCache.containsKey(conectorkey))
		{
			if (traza.isTraceActive())
			{
				traza.trace(
					ID_COMPONENTE,
					Q70TraceLevel.DEBUG,
					"getDataSource(): Entra por la cache del ConectorJDBC, InitialContext:'"
						+ initialContextKey
						+ "',datasource:'"
						+ dataSourceName
						+ "'");
			}
			return (DataSource) conectorJdbcCache.get(conectorkey);
		}

		Object obj = null;
		try
		{
			if (traza.isTraceActive())
			{
				traza.trace(
					ID_COMPONENTE,
					Q70TraceLevel.DEBUG,
					"getDataSource(): Busca el DS con lookup, InitialContext:'" + initialContextKey + "',datasource:'" + dataSourceName + "'");
			}
			obj = getContexto(initialContextKey).lookup(dataSourceName);
			Collections.synchronizedMap(conectorJdbcCache).put(conectorkey, obj);
		}
		catch (NamingException e)
		{
			if (traza.isTraceActive())
			{
				traza.trace(
					ID_COMPONENTE,
					Q70TraceLevel.ERROR,
					"getDataSource(): No encuentra en InitialContext:'"
						+ initialContextKey
						+ "',datasource:'"
						+ dataSourceName
						+ "' en jndi: "
						+ e.getMessage());
			}
			throw e;
		}

		return (DataSource) obj;
	}

	private synchronized Connection getConnection0(String initialContextKey, String dataSourceName, String idUsuario, String password)
		throws SQLException, NamingException
	{
		Q70TrazaInterface traza = Q70Factoria.getTraza();
		Connection conn = null;
		if (idUsuario == null && password == null)
		{
			conn = getDataSource(initialContextKey, dataSourceName).getConnection();
		}
		else
		{
			conn = getDataSource(initialContextKey, dataSourceName).getConnection(idUsuario, password);
		}

		// Asociamos la conexion al hilo de ejecucion, para borrarla posteriormente.
		registrarCurrentThread();
		List listaConexionesAux = ((Q70DatosDeThread) threadsCached.get(getThreadCachedKey())).getListaConexiones();
		synchronized (listaConexionesAux) {
			listaConexionesAux.add(conn);
		}

		contadorConexionesCacheadas++;
		if (contadorConexionesCacheadas > limiteControladorConexionesCacheadas)
		{
			if (traza.isTraceActive())
			{
				traza.trace(ID_COMPONENTE, Q70TraceLevel.DEBUG, "Va a entrar en el metodo limpiarConexionesThreads():");
			}
			limpiarConexionesThreads();
			contadorConexionesCacheadas = 0;
		}
		return conn;
	}

	/**
	 * Registra el hilo de ejecución actual en la lista de hilos de ejecución activos de Geremua, 
	 * que es almacenada en el Componente. 
	 * Deja preparado el Componente para almacenar las conexiones JDBC usadas por el hilo y gestionarlas.
	 */
	public void registrarCurrentThread()
	{
		if (!threadsCached.containsKey(getThreadCachedKey()))
		{
			Q70GestorLogsServidor logManager = Q70GestorLogsServidor.getSingleton();
			Q70GestorDatosComunes dataManager = logManager.getCommonDataManager();
			Q70DatosComunes data = dataManager.getData();
			Q70TrazaInterface traza = Q70Factoria.getTraza();
		
			threadsCached.put(getThreadCachedKey(),new Q70DatosDeThread(Thread.currentThread().getName(),Thread.currentThread(),data,new ArrayList(4)));
			
			if (traza.isTraceActive())
			{
				traza.trace(ID_COMPONENTE, Q70TraceLevel.DEBUG, "registrarCurrentThread():" + ((Q70DatosDeThread)threadsCached.get(getThreadCachedKey())).getThread());
			}
		}
	}

	private synchronized void limpiarConexionesThreads()
	{
		final List listaAux = new ArrayList(threadsCached.values());
		
		CollectionUtils.forAllDo(listaAux, new Closure()
		{
			public void execute(Object input)
			{
				limpiarConexionesThreads(((Q70DatosDeThread) input).getListaConexiones());
			}
		});
	}

	private synchronized void limpiarConexionesThreads(final List listaConexiones)
	{
		final List listaABorrar = new ArrayList(listaConexiones.size());
		Closure clCierraConexiones = new Closure()
		{
			public void execute(Object input)
			{
				//comprobacion de las conexiones nulas (nunca deberia suceder)
				if (null!=input){
					Connection conn = (Connection) input;
					try
					{
						if (conn.isClosed())
						{
							listaABorrar.add(conn);
						}
					}
					catch (SQLException e)
					{
						Q70GestorDeExcepcionesServidor.getSingleton().recogerThrowable(e);
					}
				}else{
					Q70TrazaInterface traza = Q70Factoria.getTraza();
					if (traza.isTraceActive())
					{
						traza.trace(ID_COMPONENTE, Q70TraceLevel.DEBUG, "limpiarConexionesThreads(): conexion nula eliminada de la lista."  );
					}	
					listaABorrar.add(input);
				}
			}
		};

		synchronized (listaConexiones) {
			CollectionUtils.forAllDo(listaConexiones, clCierraConexiones);
		}
		listaConexiones.removeAll(listaABorrar);
		Q70TrazaInterface traza = Q70Factoria.getTraza();
		if (traza.isTraceActive())
		{
			traza.trace(
				ID_COMPONENTE,
				Q70TraceLevel.DEBUG,
				"limpiarConexionesThreads():(Conexiones abiertas=" + listaConexiones.size() + "),(Conexiones limpiadas=" + listaABorrar.size() + ")");
		}
	}

	private void cerrarConexionesThreads(List listaConexiones, final boolean debeGrabarIncidencia)
	{
		Closure clCierraConexiones = new Closure()
		{
			public void execute(Object input)
			{
				Connection conn = (Connection) input;
				try
				{
					if (!conn.isClosed())
					{
						conn.close();

						if (debeGrabarIncidencia)
						{
							Q70LogIncidenciasInterface logInc = Q70Factoria.getLogIncidencias();
							if (logInc.isEnabled())
							{
								Q70ConectorJDBCException navegacionAnomalaException =
									new Q70ConectorJDBCException(
										this.getClass().toString(),
										"removeCurrentThreadResources",
										"Conexion NO cerrada por usuario:" + ((Q70DatosDeThread)threadsCached.get(getThreadCachedKey())).getDatosComunes().getIdUsuario(),
										null);								
								logInc.log(navegacionAnomalaException, ID_COMPONENTE, "WARN");
							}
						}
					}
				}
				catch (SQLException e)
				{
					Q70TrazaInterface traza = Q70Factoria.getTraza();
					if (traza.isTraceActive())
					{
						traza.trace(ID_COMPONENTE, Q70TraceLevel.DEBUG, "Error en conexion :" + e.getMessage());
					}
				}
			}
		};

		CollectionUtils.forAllDo(listaConexiones, clCierraConexiones);
	}
}
