/*
 * Fecha: 16-nov-03
 */
package es.ejie.frmk.presentacion.anomala;

import java.util.Collection;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Set;

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

import org.apache.struts.action.ActionForward;

import es.ejie.frmk.control.apilamiento.Q70GestorDeApilamiento;
import es.ejie.frmk.control.excepciones.Q70FrameworkException;
import es.ejie.frmk.infraestructura.logs.incidencias.Q70LogIncidencias;
import es.ejie.frmk.infraestructura.logs.navegacion.Q70LogNavegacion;
import es.ejie.frmk.infraestructura.traza.Q70TraceLevel;
import es.ejie.frmk.infraestructura.traza.Q70Traza;
import es.ejie.frmk.presentacion.base.Q70PasosOPMapping;
import es.ejie.frmk.presentacion.base.Q70Workflow;
import es.ejie.frmk.presentacion.base.Q70WorkflowContainer;
import es.ejie.frmk.presentacion.base.Q70PasosOPMapping.WorkflowData;
import es.ejie.frmk.presentacion.operacion.Q70AdaptadorLogicaOperacionDePresentacion;
import es.ejie.frmk.presentacion.operacion.Q70LogicaOperacionDePresentacion;


/**
 * Clase que procesa los flujos de navegación para detectar una posible
 * navegación anómala.
 * <p>
 * Implementa la funcionalidad del Gestor de Navegación Anómala y sigue el patrón de Singleton.
 * Expone métodos públicos que responden si el paso de operación de presentación requerido por el 
 * usuario cumple que no se trata de un doble-click y que pasa satisfactoriamente el análisis de 
 * reglas parametrizadas para la navegación solicitada.
 * <p>
 * <b>Análisis de navegación anómala</b>: Analiza la petición y la compara con la información 
 * acerca de transiciones de estado válidas (Encapsulada en la clase <code>es.ejie.frmk.presentacion.base.Q70PasosOPMapping</code>).
 * Si se detecta una situación de ruptura de flujo, se lanza un evento de violación de flujo de navegación.
 * <p>
 * <b>Análisis de doble-click</b>: Analiza si el recurso invocado se considera un doble-click o una petición válida.
 * Mantiene en la sesión de usuario información a modo de semáforo (por aplicación y usuario) en
 * la clase es.ejie.frmk.presentación.anomala.DobleClickBean sobre peticiones en ejecución para el usuario.
 * Si el recurso no ha terminado de ejecutarse, las sucesivas peticiones sobre el mismo serán detectadas
 * como doble-clicks y no desencadenarán su ejecución hasta que la primera petición del recurso termine
 * y libere el semáforo de ese recurso en la sesión http. 
 * Si el recurso finalizó como resultado erróneo, el semáforo también se liberará, con lo que la petición 
 * posterior a ese mismo recurso, no será considerado un doble-click.
 * 
 * @version 1.0
 * Fecha: 16-nov-03
 */
public class Q70GestorDeNavegacionAnomala
{
	/**
	 * Única instancia de la clase <code>GestorDeExcepciones</code> necesaria
	 * para el patrón singleton.
	 */
	private static Q70GestorDeNavegacionAnomala instance = 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 = "NAV_ANOMALA";
	
	/**
	 * Clave del dobleClickBean en <code>HttpSession</code> 
	 */
	public final static String DOBLE_CLICK_KEY = "es.ejie.frmk.presentacion.anomala.doble.click";
	
	/**
	 * Instancia del log de trazas
	 */
	private static Q70Traza tr = Q70Traza.getSingleton();
	
	/**
	 * Instancia del log de incidencias.
	 */
	private static Q70LogIncidencias logInc = Q70LogIncidencias.getSingleton();
	
	/**
	 * Instancia del log de navegación
	 */
	private static Q70LogNavegacion logNav = Q70LogNavegacion.getSingleton();
	
	/**
	 * Constructor del <code>Q70GestorDeNavegacionAnomala</code>.
	 */
	private Q70GestorDeNavegacionAnomala()
	{
	}
	
	/**
	 * Método estático que devuelve la única instancia del
	 * <code>Q70GestorDeNavegacionAnomala</code>.
	 *
	 * @return Única instancia del <code>Q70GestorDeNavegacionAnomala</code>.
	 */
	public static Q70GestorDeNavegacionAnomala getInstance()
	{
		if (instance == null)
			instance = new Q70GestorDeNavegacionAnomala();
		return instance;
	}
	
	/**
	 * Método que procesa la información de flujo de navegación preestablecido para detectar una posible
	 * ruptura del flujo.
	 * <p>
	 * El método analiza la Configuración de los pasos de la Operación de Presentación actual y la información
	 * del diálogo actual para detectar situaciones de violación del flujo.
	 * 
	 * @param <code>Q70AdaptadorLogicaOperacionDePresentacion</code> Procesador de la lógica de
	 * operación de presentación.
	 * @param <code>Q70PasosOPMapping</code> Configuración de los pasos de la Operación de Presentación actual
	 * @param <code>Q70WorkflowContainer</code> Diálogo actual
	 * @param <code>HttpServletRequest</code> Petición a procesar
	 * @param <code>HttpServletResponse</code> Respuesta a crear
	 * @return <code>false</code> -> Si se ha detectado una ruptura del flujo. 
	 * @throws <code>FrameworkException</code>
	 */
	public boolean processRupturaFlujo (Q70AdaptadorLogicaOperacionDePresentacion rp, Q70PasosOPMapping workflowMapping, Q70WorkflowContainer workflowContainer, HttpServletRequest request, HttpServletResponse response) throws Q70FrameworkException
	{
		try {
			boolean retVal = true;
			if (!workflowMapping.hasNoWorkflowChecksSet())
			{
				if (tr.isTraceActive())
				{
					tr.trace(COMPONENTE, Q70TraceLevel.INFO, "Chequeando ruptura de flujo.");
				}
				Q70Workflow prevPrimaryWorkflow = workflowContainer.getPrimary();
				String prevPrimaryWorkflowLabel =
					workflowContainer.getPrimaryLabel();
	
				// the primary workflow (of the previously executed action) must allow
				// the execution of this action
				if (null != prevPrimaryWorkflow)
				{
					Set definedNextStates =
						prevPrimaryWorkflow.getDefinedNextStates();
	
					// only need to do the checks if a next state was defined
					if (!definedNextStates.isEmpty())
					{
						String prevPrimaryNewState;
	
						// let's check, if this action defines a workflow with name of the previous primary workflow
						WorkflowData prevPrimaryWorkflowData =
							workflowMapping.getWorkflowData(
								prevPrimaryWorkflowLabel);
						if (null != prevPrimaryWorkflowData)
						{
							prevPrimaryNewState =
								prevPrimaryWorkflowData.getNewState();
							if (null == prevPrimaryNewState)
							{
								prevPrimaryNewState =
									prevPrimaryWorkflow.getCurrentState();
							}
						}
						else
						{
							prevPrimaryNewState =
								prevPrimaryWorkflow.getCurrentState();
						}
	
						if ((null == prevPrimaryNewState)
							|| (!definedNextStates.contains(prevPrimaryNewState)))
						{
							retVal = false;
							workflowContainer.remove(
								request,
								prevPrimaryWorkflowLabel);
							ActionForward forward =
								workflowMapping.findForward(
									Q70LogicaOperacionDePresentacion
										.WORKFLOW_VIOLATION_FORWARD_PREFIX
										+ prevPrimaryWorkflowLabel);
							request.setAttribute(
								Q70LogicaOperacionDePresentacion
									.NEXT_STATE_VIOLATION_PATH,
								request.getServletPath());
							writeLog("processRupturaFlujo","Navegación anómala detectada.");
							rp.processForwardConfig(request, response, forward);
						}
					}
				}
	
				// if the check did not already fail and there are workflows defined at all, 
				// check the prevState attributes
				if (retVal)
				{
					Collection workflowLabels = workflowMapping.getWorkflows();
					String workflowLabel = null;
					if (0 < workflowLabels.size())
					{
						// Iterate through all given workflows
						Iterator it = workflowLabels.iterator();
						while (it.hasNext() && (retVal))
						{
							workflowLabel = (String)it.next();
							Set definedPrevStates =
								workflowMapping
									.getWorkflowData(workflowLabel)
									.getPrevStates();
							if ((null != definedPrevStates)
								&& (!definedPrevStates.isEmpty()))
							{
								// Try to retrieve the appropriate Q70Workflow object
								Q70Workflow workflow =
									workflowContainer.get(workflowLabel);
								if (null == workflow)
								{
									retVal = false;
								}
								else
								{
									String previousState =
										workflow.getCurrentState();
									retVal =
										(null != previousState)
											&& definedPrevStates.contains(
												previousState);
								}
							}
						}
					}
					if (!retVal)
					{
						workflowContainer.remove(request, workflowLabel);
						ActionForward forward =
							workflowMapping.findForward(
								Q70LogicaOperacionDePresentacion
									.WORKFLOW_VIOLATION_FORWARD_PREFIX
									+ workflowLabel);
						writeLog("processRupturaFlujo","Navegación anómala detectada.");
						rp.processForwardConfig(request, response, forward);
					}
				}
			}
		return retVal;
		} catch (Exception e) {
			throw new Q70NavegacionAnomalaException(this.getClass().toString(), "processRupturaFlujo", "Error al detectar una ruptura de flujo.", e);
		}
	}

	/**
	 * Método que comprueba si ha sucedido una situación de doble-click.
	 * <p>
	 * Entendemos doble-click como ejecución de dos peticiones consecutivas del mismo paso
	 * de Operación de Presentación de tal modo que la segunda petición se realiza
	 * antes de que termine la ejecución de la primera.
	 *
	 * @param <code>Q70PasosOPMapping</code> Configuración de los pasos de la Operación de Presentación actual
	 * @param <code>HttpServletRequest</code> Petición a procesar
	 * @param <code>HttpServletResponse</code> Respuesta a crear
	 * @return <code>false</code> -> Si se ha detectado doble-click.
	 * @throws <code>FrameworkException</code>
	 */
	public boolean processDobleClick (Q70PasosOPMapping workflowMapping, HttpServletRequest request, HttpServletResponse response) throws Q70FrameworkException
	{
		try {
			boolean retVal = true;
			
			//Si no es un forward, ni un include, ni es un retorno de
			//suboperación, hago el control del doble-click.
			if ((null == workflowMapping.getForward()) && (null == workflowMapping.getInclude()) && (!Q70GestorDeApilamiento.getSingleton().isReturnToSuperOp(request, workflowMapping.getSubOp())))
			{
				Hashtable requestedURIs = (Hashtable)request.getSession().getAttribute(DOBLE_CLICK_KEY);
				Q70DobleClickBean dobleClickBean = null;
				
				if (tr.isTraceActive())
				{
					tr.trace(COMPONENTE, Q70TraceLevel.INFO, "Análisis de Doble-click: Comprobando si ha finalizado la ultima petición del recurso actual para el usuario. ");
				}
				
				if (null != requestedURIs)
				{
					if (tr.isTraceActive())
					{
						tr.trace(COMPONENTE, Q70TraceLevel.INFO, "Lista de peticiones no finalizadas para el usuario: "+requestedURIs.toString());
					}
					dobleClickBean = (Q70DobleClickBean)requestedURIs.get(request.getRequestURI());
					
					//Si existe el dobleClickBean es que ha habido una petición
					//con anterioridad y no se ha acabado de procesar (doble-click).
					if (null != dobleClickBean)
					{
						writeLog("processDobleClick","Doble-click detectado para el recurso "+request.getRequestURI()+" aún no finalizado. Retardo = "+dobleClickBean.calcularRetardo()+" milisegundos.");
						
						retVal = false;
					}
					else
					{
						dobleClickBean = new Q70DobleClickBean();
					}
				}
				else
				{
					dobleClickBean = new Q70DobleClickBean(); 
					
					requestedURIs = new Hashtable(1);
				}
				dobleClickBean.setTimestamp();
				dobleClickBean.setResponse(response);
				
				requestedURIs.put(request.getRequestURI(),dobleClickBean);
				
				request.getSession().setAttribute(DOBLE_CLICK_KEY, requestedURIs);
			}
			return retVal;
		} catch (Exception e) {
			throw new Q70NavegacionAnomalaException(this.getClass().toString(), "processDobleClick", "Error al detectar un doble click.", e);
		}
	}
	
	/**
	 * Método que inicializa el control del doble click.
	 * 
	 * <p> Inicializa la marca de control depositada en la sesión de usuario <code>HttpSession</code> 
	 * una vez finalizado el paso de Operación de Presentación.
	 *  
	 * @param <code>request</code> Petición a procesar
	 */
	public void cleanDobleClick (HttpServletRequest request)
	{
		Hashtable requestedURIs = (Hashtable)request.getSession().getAttribute(DOBLE_CLICK_KEY);

		if (null != requestedURIs)
		{
			requestedURIs.remove(request.getRequestURI());
		}
	}
	
	/**
	 * Método que devuelve el response adecuado hacia el usuario cuando se ha detectado una situación de doble-click.
	 * @param <code>request</code> Petición a procesar
	 * @return <code>response</code> Respuesta a crear
	 */
	
	public HttpServletResponse getDobleClickResponse (HttpServletRequest request)
	{
		HttpServletResponse resultado = null;
		Hashtable requestedURIs = (Hashtable)request.getSession().getAttribute(DOBLE_CLICK_KEY);
		
		if (null != requestedURIs)
		{
			Q70DobleClickBean dobleClickBean = (Q70DobleClickBean)requestedURIs.get(request.getRequestURI());
			if (null != dobleClickBean)
			{
				//Se retorna el response en vigor.
				resultado = dobleClickBean.getResponse();
			}
		}
		return resultado;
	}

	/**
	 * Método que registra la navegación anómala en el log de incidencias y en
	 * el log de navegación.
	 * @param <code>nombre</code> Nombre del método que detecta la navegación anómala.
	 * @param <code>mensaje</code> Mensaje a escribir en el log.
	 */
	private void writeLog(String metodo, String mensaje)
	{
		if (tr.isTraceActive())
		{
			tr.trace(COMPONENTE, Q70TraceLevel.ERROR, mensaje);
		}
		if (logNav.isEnabled())
		{
			logNav.log(Q70LogNavegacion.RESPUESTA_NAVEGACION_ANOMALA, Q70LogNavegacion.TIPO_NAVEGACION_ENTRADA);
		}
		if (logInc.isEnabled())
		{
			Q70NavegacionAnomalaException navegacionAnomalaException = new Q70NavegacionAnomalaException(this.getClass().toString(), metodo, mensaje, null);
			logInc.log(navegacionAnomalaException, COMPONENTE, "WARN");
		}
	}
}
