package es.ejie.frmk.control.excepciones;

import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.struts.Globals;
import org.apache.struts.action.ActionError;
import org.apache.struts.action.ActionErrors;

import es.ejie.frmk.Q70Factoria;
import es.ejie.frmk.control.arranque.Q70Arrancable;
import es.ejie.frmk.control.arranque.Q70GestorArranqueConfiguracion;
import es.ejie.frmk.infraestructura.logs.incidencias.Q70LogIncidencias;
import es.ejie.frmk.infraestructura.seguridad.Q70AuthorizationException;
import es.ejie.frmk.infraestructura.seguridad.Q70LogonException;
import es.ejie.frmk.infraestructura.traza.Q70TraceLevel;
import es.ejie.frmk.infraestructura.traza.Q70Traza;
import es.ejie.frmk.infraestructura.traza.Q70TrazaInterface;

/**
 * La clase se encarga de gestionar las excepciones enviadas por las aplicaciones o por otros componentes 
 * de Geremua.
 * <p>
 * Para ello se realizan las siguientes tareas:
 * <ul>
 * <li>registrar la excepción recogida en el Log de Incidencias</li>
 * <li>componer errores de usuario</li>
 * <li>servir una página de errores</li>
 * </ul>
 * <p>
 * Así mismo, Geremua presenta una jerarquía de excepciones propias que permite a los componentes
 * capturar excepciones lanzadas por otros componentes o por las aplicaciones y añadirles más 
 * información como "componente", "clase" y "método" que las causa.
 * 
 */
public class Q70GestorDeExcepciones implements Q70Arrancable
{ 
	/**
	 * Hashmap que relaciona Excepciones de Geremua con páginas de error
	 */
	private Map errorPages = null;
	/**
	* Path del fichero de configuracion
	*/
	private String xmlConfiguracion = null;
	
	/**
	 * Constante que indica el componente al que pertenece dicha clase. Es
	 * necesario para el uso del log de trazas.
	 */
	private static final String COMPONENTE = "GESTOR_EXCEPCIONES";

	/**
	 * Instancia del log de trazas.
	 */
	private static Q70Traza tr = Q70Traza.getSingleton();

	/**
	 * Instancia del log de incidencias.
	 */
	private static Q70LogIncidencias logInc = Q70LogIncidencias.getSingleton();
	/**
	 * Constante que indica la key del Map <code>errorPages</code> para la página de
	 * error por defecto 
	 */
	private static final String DEFAULT_COMPONENT_KEY = "DEFAULT";
	/**
	 * Constante que indica la página de error por defecto de Geremua, si no la encuentra en la Map
	 */
	private static final String DEFAULT_ERROR_PAGE = "/error.jsp";
	/**
	 * Constante que indica la key del recurso con el formato de los errores de Geremua de la página de errores
	 */
	private static final String FRAMEWORK_EXCEPTION_KEY = "framework.exception.msg.error";

	/**
	 * Constante que indica la key del recurso con el formato de los errores Java de la página de errores
	 */
	private static final String JAVA_EXCEPTION_KEY = "java.exception.msg.error";

	/**
	 * Constante que indica el PROPERTY asignado al tag<errors> del error principal de la página de errores
	 */
	private static final String PRIMARY_EXCEPTION_PROPERTY = "primary";
	/**
	 * Constante que indica el PROPERTY asignado al tag<errors> de los errores secundarios de la página de errores
	 */
	private static final String SECONDARY_EXCEPTION_PROPERTY = "secondary";
	/**
	 * Constante que indica el PROPERTY asignado al tag<errors> de los errores java de la página de errores
	 */
	private static final String JAVA_EXCEPTION_PROPERTY = "java";

	/**
	 * Constructor privado necesario para ocultar el constructor por defecto.
	 */
	private Q70GestorDeExcepciones()
	{}

	/**
	 * Método estático que devuelve la única instacia del <code>Q70GestorDeExcepciones</code>, que sigue el patrón de Singleton.
	 * <p>
	 * @return Única instacia del <code>Q70GestorDeExcepciones</code>.
	 */
	public static Q70GestorDeExcepciones getSingleton()
	{
		Q70GestorDeExcepciones instancia = (Q70GestorDeExcepciones)Q70GestorArranqueConfiguracion.getAttribute(COMPONENTE);
		if (instancia == null)
		{
			instancia = new Q70GestorDeExcepciones();
			Q70GestorArranqueConfiguracion.setAttribute(COMPONENTE, instancia);

		}
		return instancia;

	}

	/**
	 * Método que recoge throwables y los registra en el Log de Incidencias de Geremua
	 * 
	 * @param throwable Throwable a tratar.
	 */
	public void recogerThrowable(Throwable throwable)
	{
		Q70GestorDeExcepcionesServidor.getSingleton().recogerThrowable(throwable);
	}
	/**
	 * Método que recoge throwables y reenvía el flujo a la JSP de errores determinada para el componente.
	 * <p>
	 * Delega en el método <code>recogerThrowable(Throwable throwable)</code>, 
	 * calcula el throwable principal y el componente que lo provocó, 
	 * averigua la JSP de errores correspondiente y redirecciona el <code>response</code> hacia ella.
	 * <p>
	 * @param throwable Throwable a tratar.
	 * @param request Petición a procesar
	 * @param response Respuesta a crear
	 */
	public void recogerThrowable(Throwable throwable, HttpServletRequest request, HttpServletResponse response)
	{
		String mainComponente = null;
		String mainClase = null;
		String mainMetodo = null;
		String page = null;
		String logonPage = null;
		String retPath = null;
		String logonURL = null;
		Throwable mainThrowable = null;
		try
		{
			//Registra throwable en el Q70Log de Incidencias
			Q70GestorDeExcepcionesServidor.getSingleton().recogerThrowable(throwable);
			
			//Averigua throwable principal (áquel que arrojó la primera excepción de Geremua)
			mainThrowable = getMainThrowable(throwable);
			if (tr.isTraceActive())
			{
				tr.trace(COMPONENTE, Q70TraceLevel.DEBUG, "Throwable principal = " + mainThrowable.getClass());
			}
			if (mainThrowable instanceof Q70LogonException)
			{
				//Excepcion de Logon (caso particular).Redirige a página de Logon depositada en atributo "message"
				logonPage = mainThrowable.getMessage();
				retPath = ((Q70LogonException)mainThrowable).getRetPath();
				logonURL = logonPage + "?N38API=" + retPath;
				if (tr.isTraceActive())
				{
					tr.trace(
						COMPONENTE,
						Q70TraceLevel.INFO,
						"Detectada throwable de tipo Q70LogonException. Intentará redireccionarse a página de Logon = "
							+ logonURL);

				}
				response.sendRedirect(logonURL);
				return;
			}
			if (mainThrowable instanceof Q70FrameworkException)
			{
				//Intenta que Geremua recupere la situación previa a
				//que el Gestor de Excepciones recogiese el throwable principal
				recoverStatus((Q70FrameworkException)mainThrowable);
				//Averigua que componente envió el throwable principal
				mainComponente = ((Q70FrameworkException)mainThrowable).getComponente();
				//Averigua que clase envió el throwable principal
				mainClase = ((Q70FrameworkException)mainThrowable).getClase();
				//Averigua que clase envió el throwable principal
				mainMetodo = ((Q70FrameworkException)mainThrowable).getMetodo();
			}
			else
			{
				mainComponente = DEFAULT_COMPONENT_KEY;
			}
			if (tr.isTraceActive())
			{
				tr.trace(COMPONENTE, Q70TraceLevel.DEBUG, "Componente que lanzó la excepción = " + mainComponente);
			}
			//Acude a la Map de páginas de error para buscar la jsp asociada al componente
			try
			{
				page = (String)getErrorPages().get(mainComponente);
			}
			catch (Exception e)
			{
				//Si hay error, le asigna la página de error por defecto de Geremua
				page = DEFAULT_ERROR_PAGE;
			}
			if (tr.isTraceActive())
			{
				tr.trace(COMPONENTE, Q70TraceLevel.DEBUG, "1 - Página para redirigir errores =" + page);
			}
			//Si no encuentra la página para el componente, busca en la Map la página por defecto
			if (null == page)
			{
				mainComponente = DEFAULT_COMPONENT_KEY;
				try
				{
					page = (String)getErrorPages().get(mainComponente);
				}
				catch (Exception e)
				{
					//Si hay error, le asigna la página de error por defecto de Geremua
					page = DEFAULT_ERROR_PAGE;
				}
			}
			//Si no encuentra la página por defecto en la Map, usa la página por defecto de Geremua
			if (null == page)
			{
				page = DEFAULT_ERROR_PAGE;
			}
			//Prepara ActionErrors para volcar en página de error
			if (!(mainThrowable instanceof Q70AuthorizationException))
			{
				try
				{
					addActionErrors(request, throwable, mainComponente, mainClase, mainMetodo);
				}
				catch (Exception e)
				{
					if (tr.isTraceActive())
					{
						tr.trace(COMPONENTE, Q70TraceLevel.INFO, "ERROR generando ActionErrors para JSP de errores");
					}
					if (logInc.isEnabled())
					{
						logInc.log(throwable, COMPONENTE, "ERROR generando ActionErrors para JSP de errores");
					}
				}
			}
			else
			{
				//Simplificar la pagina para errores de autorizacion
				ActionErrors errors = null;
				if (null != (ActionErrors)request.getAttribute(Globals.ERROR_KEY))
				{
					errors = (ActionErrors)request.getAttribute(Globals.ERROR_KEY);
				}
				else
				{
					errors = new ActionErrors();
				}
				Q70AuthorizationException authorizationException = (Q70AuthorizationException)mainThrowable;
				String componente = authorizationException.getComponente();
				String clase = authorizationException.getClase();
				String metodo = authorizationException.getMetodo();
				String texto = authorizationException.getMessage();
				errors.add(PRIMARY_EXCEPTION_PROPERTY, new ActionError(FRAMEWORK_EXCEPTION_KEY,componente, clase, metodo, texto));
				request.setAttribute(Globals.ERROR_KEY, errors);
			}
			//Redirecciona a la página de error	
			if (request.getAttribute("q70exception")==null) 
			{
				//Para evitar que entre en bucle	
				request.setAttribute("q70exception",throwable);
				try
				{
					request.getRequestDispatcher(page).forward(request, response);
				}
				catch(Exception exc)
				{
					//Por si falla la llamada a la página de error
					String texto = "<b><span style='color:red'>ERROR: "+mainThrowable.getMessage()+"</span></b>";
					response.getWriter().print(texto);
				}
			}
		}
		catch (Exception e)
		{
			if (tr.isTraceActive())
			{
				tr.trace(COMPONENTE, Q70TraceLevel.INFO, "###ERROR redireccionando a JSP de errores");
			}
			if (logInc.isEnabled())
			{
				logInc.log(throwable, COMPONENTE, "ERROR redireccionando a JSP de errores");
			}
		}
	}

	public void afterCreate(String file) throws Q70FrameworkException{
		try {
			xmlConfiguracion = file;
			Q70ConfiguradorExcepciones conf = new Q70ConfiguradorExcepciones(file);
			conf.lanzarConfigurador();
		} catch (Exception e) {
			Q70TrazaInterface traza = Q70Factoria.getTraza();
			if (traza.isTraceActive()){
				traza.trace(COMPONENTE, Q70TraceLevel.ERROR, "Se ha producido un error en el método afterCreate de la clase Q70GestorDeExcepciones");
			}
			throw new Q70FrameworkException(COMPONENTE,this.getClass().toString(),"afterCreate","Se ha producido un error en el método afterCreate.",e);
		}
	}

	public void beforeDestroy()
	{}

	public void reConfigure() throws Q70FrameworkException{
		try {
			getErrorPages().clear();
			setErrorPages(null);
			afterCreate(xmlConfiguracion);
		} catch (Exception e) {
			Q70TrazaInterface traza = Q70Factoria.getTraza();
			if (traza.isTraceActive()){
				traza.trace(COMPONENTE, Q70TraceLevel.ERROR, "Se ha producido un error en el método reConfigure de la clase Q70GestorDeExcepciones");
			}
			throw new Q70FrameworkException(COMPONENTE,this.getClass().toString(),"reConfigure","Se ha producido un error en el método reConfigure.",e);
		}
	}

	private Map getErrorPages()
	{
		return errorPages;
	}

	protected void setErrorPages(Map map)
	{
		errorPages = map;
	}

	/**
	 * Método que añade tantos <code>ActionErrors</code> en la <code>HttpServletRequest</code> como throwables haya anidados
	 * en el Throwable recibido.
	 * <p>
	 * Para ello instancia la clase <code>ActionErrors</code> y le va añadiendo instancias de<code>ActionError</code>.
	 * Finalmente, deposita la instancia <code>ActionErrors</code> como un atributo de la request, para que el tag <html:errors> de
	 * la JSP de errores acceda a ella y renderice cada error como un mensaje o literal en pantalla.
	 * <p>
	 * @param request Petición a procesar
	 * @param throwable Throwable a tratar.
	 * @param mainComponente Componente que primero detectó el throwable
	 * @param mainClase Clase que primero detectó el throwable
	 * @param mainMetodo Método que primero detectó el throwable
	 * */
	private void addActionErrors(
		HttpServletRequest request,
		Throwable throwable,
		String mainComponente,
		String mainClase,
		String mainMetodo)
	{
		if (tr.isTraceActive())
		{
			tr.trace(COMPONENTE, Q70TraceLevel.DEBUG, "mainComponente = " + mainComponente);
			tr.trace(COMPONENTE, Q70TraceLevel.DEBUG, "mainClase = " + mainClase);
			tr.trace(COMPONENTE, Q70TraceLevel.DEBUG, "mainMetodo = " + mainMetodo);
		}
		ActionErrors errors = null;

		String componente = null;
		String clase = null;
		String metodo = null;
		String texto = null;
		if (null != (ActionErrors)request.getAttribute(Globals.ERROR_KEY))
		{
			errors = (ActionErrors)request.getAttribute(Globals.ERROR_KEY);
		}
		else
		{
			errors = new ActionErrors();
		}
		if (tr.isTraceActive())
		{
			tr.trace(
				COMPONENTE,
				Q70TraceLevel.DEBUG,
				"ActionErrors recuperado para nuevos ActionError de JSP de errores.");
		}
		//Bucle para recorrer todos los throwables apilados
		boolean marca = true;
		int i = 0;
		while (marca && null != throwable)
		{
			if (tr.isTraceActive())
			{
				tr.trace(COMPONENTE, Q70TraceLevel.DEBUG, "Recorriendo throwables");
			}
			if (throwable instanceof Q70FrameworkException)
			{
				//Crear un nuevo error por cada throwable apilado
				componente = ((Q70FrameworkException)throwable).getComponente();
				clase = ((Q70FrameworkException)throwable).getClase();
				metodo = ((Q70FrameworkException)throwable).getMetodo();
				texto = ((Q70FrameworkException)throwable).getMessage();
			}
			else
			{
				clase = throwable.getClass().getName();
				marca = false;
			}
			if (tr.isTraceActive())
			{
				tr.trace(COMPONENTE, Q70TraceLevel.DEBUG, "Throwable" + i + " analizada");
			}
			//Añade error a la lista de errores
			if (tr.isTraceActive())
			{
				tr.trace(COMPONENTE, Q70TraceLevel.DEBUG, "Previo a crear ActionError" + i);
			}
			if (throwable instanceof Q70FrameworkException)
			{
				if (((Q70FrameworkException)throwable).getComponente().equals(mainComponente)
					&& (((Q70FrameworkException)throwable).getClase().equals(mainClase))
					&& (((Q70FrameworkException)throwable).getMetodo().equals(mainMetodo)))
					errors.add(
						PRIMARY_EXCEPTION_PROPERTY,
						new ActionError(FRAMEWORK_EXCEPTION_KEY, componente, clase, metodo, texto));
				else
					errors.add(
						SECONDARY_EXCEPTION_PROPERTY,
						new ActionError(FRAMEWORK_EXCEPTION_KEY, componente, clase, metodo, texto));
			}
			else
			{
				errors.add(JAVA_EXCEPTION_PROPERTY, new ActionError(JAVA_EXCEPTION_KEY, clase + ":" +throwable.getMessage()));
			}
			if (tr.isTraceActive())
			{
				tr.trace(COMPONENTE, Q70TraceLevel.DEBUG, "ActionError" + i + " creado");
			}
			if (throwable instanceof Q70FrameworkException)
			{
				if (tr.isTraceActive())
				{
					tr.trace(COMPONENTE, Q70TraceLevel.DEBUG, "Preparando nuevo paso para recorrer throwables");
				}
				throwable = ((Q70FrameworkException)throwable).getThrowable();
				++i;
			}
		}
		//Depositar los errores en la request
		if (tr.isTraceActive())
		{
			tr.trace(
				COMPONENTE,
				Q70TraceLevel.DEBUG,
				"Número de ActionErrors creados para JSP de errores =" + errors.size());
		}
		request.setAttribute(Globals.ERROR_KEY, errors);
	}
	/**
	 * Método que calcula el throwable principal contenido en el throwable 
	 * recogido por el Gestor de Excepciones.
	 * <p>
	 * El throwable principal es aquel que detectó el throwable en primer lugar,
	 * en el propio componente que lo originó.
	 * 
	 * @param throwable Throwable a tratar.
	 * @return throwable Throwable principal.
	 */
	private Throwable getMainThrowable(Throwable throwable)
	{
		Throwable t = null;
		Throwable prevT = null;
		boolean buscar = true;
		t = throwable;
		while (null != t && buscar)
		{
			if (tr.isTraceActive())
			{
				tr.trace(COMPONENTE, Q70TraceLevel.DEBUG, "Analizando throwable = " + t.getClass().getName());
			}
			if (t instanceof Q70FrameworkException)
			{
				prevT = t;
				t = ((Q70FrameworkException)t).getThrowable();
			}
			else
			{
				buscar = false;
			}
		}
		if (tr.isTraceActive())
		{
			tr.trace(COMPONENTE, Q70TraceLevel.DEBUG, "Devolverá como main Throwable = " + prevT.getClass().getName());
		}
		return prevT;
	}
	/**
	 * Método que recupera la situación de Geremua tras tratar un Throwable recogido 
	 * <p>
	 * Actalmente no implementado.
	 * <p>
	 * @param fe Instancia de <code>Q70FrameworkException</code> a tratar.
	 */
	private void recoverStatus(Q70FrameworkException fe)
	{}
}