/*******************************************************************************
 * Copyright © 2004 VHA. All rights reserved
 ******************************************************************************/

package gov.va.med.fw.service.jms;

// Java classes
import java.lang.reflect.InvocationTargetException;

import javax.ejb.MessageDrivenContext;
import javax.jms.BytesMessage;
import javax.jms.DeliveryMode;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.ObjectMessage;
import javax.jms.TextMessage;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.security.auth.login.LoginException;

import org.apache.commons.lang.ClassUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.access.ContextSingletonBeanFactoryLocator;
import org.springframework.ejb.support.AbstractMessageDrivenBean;
import org.springframework.web.context.ContextLoader;

import gov.va.med.fw.hl7.InvalidMessageException;
import gov.va.med.fw.hl7.builder.JMSMetaData;
import gov.va.med.fw.security.LoginManager;
import gov.va.med.fw.security.SecurityContextHelper;
import gov.va.med.fw.security.UserCredentials;
import gov.va.med.fw.service.ConfigurationConstants;
import gov.va.med.fw.service.ServiceDescriptor;
import gov.va.med.fw.util.ExceptionHandler;
import gov.va.med.fw.util.ExceptionHandlerFactory;
import gov.va.med.fw.util.Reflector;
import gov.va.med.fw.util.builder.Builder;
import gov.va.med.fw.util.builder.BuilderException;

/**
 * Processes JMS message. Each JMS message is mapped to a service descriptor. A
 * service descriptor encapsulates information about a service and its method,
 * which is called to process a JMS message. In addition, a service descriptor
 * also specifies a builder to convert raw data extracted from a JMS message
 * into a structured object. A builder, a service name, and a service's method
 * name are required to process a message.
 * 
 * @author Vu Le
 * @version 1.0
 */
public class MessageDrivenService extends AbstractMessageDrivenBean implements MessageListener {

	/**
	 * An instance of serialVersionUID
	 */
	private static final long serialVersionUID = 8095918876990648447L;

	/**
	 * An instance of FACTORY_LOCATOR_SELECTOR to use in ContextSingletonBeanFactoryLocator
	 * to load a common context for all modules in an EAR file
	 */
	public static final String FACTORY_LOCATOR_SELECTOR = ContextLoader.LOCATOR_FACTORY_SELECTOR_PARAM;

	/**
	 * An instance of FACTORY_LOCATOR_KEY to use as a name of an application
	 * context bean in a default beanRefContext.xml.  A beanRefContext.xml
	 * is used by ContextSingletonBeanFactoryLocator to ensure that all EJBs 
	 * and and WAR modules share the same context 
	 */
	public static final String FACTORY_LOCATOR_KEY = "locatorFactoryKey";

	/**
	 * An instance of logger
	 */
	protected Log logger = LogFactory.getLog(getClass());

	/**
	 * An instance of loginManager
	 */
	private LoginManager loginManager;

	/**
	 * A default constructor
	 */
	public MessageDrivenService() {
		super();
	}
	
	/**
	 * Sets the message driven context
	 * 
	 * @param ctx MessageDrivenContext Context for session
	 */
	public void setMessageDrivenContext( MessageDrivenContext ctx ) {

		super.setMessageDrivenContext(ctx);
		setBeanFactoryLocator(ContextSingletonBeanFactoryLocator.getInstance(getEnvEntry(FACTORY_LOCATOR_SELECTOR)));
		setBeanFactoryLocatorKey(getEnvEntry(FACTORY_LOCATOR_KEY));
	}

	/**
	 * Processes a JMS message. This method is called when a message arrives to a
	 * message queue to which this message-driven bean is registered. A
	 * preProcessMessage method is called first then a service descriptor for a
	 * JMS message is obtained from an ApplicationContext xml file. If access to
	 * a service specified in service descriptor is granted, a processInvocation
	 * method is called to invoke a service for processing a message.
	 * 
	 * @param message Message a JMS message
	 * @see javax.jms.MessageListener#onMessage(javax.jms.Message)
	 */
	public void onMessage( Message message ) {	
		/*StopWatchLogger watch = new StopWatchLogger("MDB onMessage");
		watch.start();*/
		
		boolean isLoggedIn = false;
		try {
			if(message.getJMSDeliveryMode() == DeliveryMode.NON_PERSISTENT) {
				if(logger.isWarnEnabled()) {
					logger.warn("Received a non-persistent JMS Message...if a persistent JMS store is configured, advise sending system to set JMSDeliveryMode to DeliveryMode.PERSISTENT");
				}
			}
			
			// Perform any operation before processing a message
			preProcessMessage(message);

			// Get a service descriptor to process this message
			String msgType = getMessageType(message);
			BeanFactory factory = this.getBeanFactory();

			// Get a service descriptor
			ServiceDescriptor desc = (ServiceDescriptor)factory.getBean( msgType,	ServiceDescriptor.class);
			if( logger.isDebugEnabled() ) {
				logger.debug("Got a service descriptor " + desc	+ " to process message " + msgType);
			}

			// Builds input parameters
			Object builtPayload = processBuilder(desc.getBuilder(), message);

			// authenticate (first determine what to authenticate with, and this
			// was based on what builder did)
			UserCredentials initiater = null;
			if (builtPayload instanceof SecureMessageWrapper) {

				SecureMessageWrapper secureWrapper = (SecureMessageWrapper) builtPayload;
				builtPayload = secureWrapper.getPayload();
				initiater = secureWrapper.getUserCredentials();
			}
			else {
				initiater = new UserCredentials();
				initiater.setAnonymous(true);
				initiater.setLogicalID(getMessageInitiater(message));
			}

			// Check if access is granted
			processAuthenticate(initiater);
			isLoggedIn = true;

			// Invoke a service
			processServiceInvocation(desc, message, builtPayload);
		}
		catch (Throwable e) {
			processException(e, message);
			if(shouldFailConsumption(e)) {
				if(logger.isErrorEnabled()) {
					logger.error("Failed consumption of JMS Message with exception class: " + e.getClass().getName() + " and message: " + e.getMessage());
				}
				
               if(e instanceof RuntimeException)
            	   throw (RuntimeException) e;
               else
            	   throw new RuntimeException(e);
			}
		}
		finally {
			try {
				if (isLoggedIn) {
					getLoginManager().logout();
				}
			}
			catch (Throwable t) { 
				/* nothing we can do here */
				logger.error("MessageDrivenService received a Throwable in the finally clause", t);
			}
		}
		//watch.stopAndLog("onMessage completed");		
	}
	
	protected boolean shouldFailConsumption(Throwable t) {
		return false; // default mode is to not "fail consumption" for anything
	}

	/**
	 * Authenticates user access
	 * 
	 * @param cred
	 * @throws LoginException
	 */
	private void processAuthenticate(UserCredentials cred) throws LoginException {
		SecurityContextHelper.initSecurityContextOnThread(getLoginManager(), cred);
	}

	/** Returns a LoginManager to authenticate a sender of a JMS message's request
	 * @return a LoginManager used to authenticate a request
	 */
	private LoginManager getLoginManager() {
		if (loginManager == null) {
			
			loginManager = (LoginManager)getBeanFactory().getBean( ClassUtils.getShortClassName(LoginManager.class),
																	  LoginManager.class);
		}
		return loginManager;
	}

	/**
	 * Cleans up resources allocated when a bean is created. Derived classes are
	 * encouraged to provide specific implementation.
	 */
	public void onEjbRemove() {
		if( logger.isDebugEnabled() ) {
			logger.debug("In onEjbRemove method");
		}
	}

	/**
	 * Creates a service broker and initializes all components defined in an
	 * ApplicationContext xml file.
	 */
	public void onEjbCreate() {
		if( logger.isDebugEnabled() ) {
			logger.debug("In onEjbCreate method");
		}
	}

	/**
	 * Performs a check if a message is null and is not an instance of
	 * TextMessage. If so, an InvalidMessageException is thrown. Otherwise, a
	 * persistMessage method is called to persist a message before any process
	 * operation is performed. A base exception is declared in this method to
	 * allow a derived class to throw its own exception. Moreover, a derived
	 * class can configured the specific exception handler to handle exception
	 * thrown in this method.
	 * 
	 * @param message An in-coming JMS message
	 * @throws Exception Thrown if a message is invalid.
	 * @see gov.va.med.fw.util.ExceptionHandlerFactory
	 */
	protected void preProcessMessage( Message message ) {

		if( message == null || 
			 ( !(message instanceof TextMessage) && 
			   !(message instanceof BytesMessage) && 
			   !(message instanceof ObjectMessage) ) ) {

			processException( new InvalidMessageException("Invalid message"),	message );
		}
		if (logger.isDebugEnabled()) {
			logger.debug("Incoming message: " + message);
		}
	}

	/**
	 * Invokes a service to process a JMS message. A service descriptor
	 * encapsulates a service's name, a service method's name, and a builder. A
	 * builder is called to convert raw message data extracted from a JMS message
	 * into a structured object. Finally the structured object is passed to a
	 * service's method specified in a service descriptor for process. A base
	 * exception is declared in this method to allow a derived class to throw its
	 * own exception. Moreover, a derived class can configured the specific
	 * exception handler to handle exception thrown in this method. Possible
	 * thrown exceptions in this method are BuilderException and a
	 * ServiceException.
	 * 
	 * @param component A service to process a message
	 * @param desc A service descriptor
	 * @param message A message
	 */
	protected void processServiceInvocation( ServiceDescriptor desc,
														  Message message, 
														  Object structuredMsg ) throws Throwable {
		Object[] params = null;
		try {
			if (structuredMsg instanceof Object[]) {
				params = (Object[]) structuredMsg;
			}
			else {
				params = new Object[] { structuredMsg };
			}

			// Get a service and method name from a service descriptor
			String serviceName = desc.getServiceName();
			String methodName = desc.getMethodName();

			// Get a bean factory to obtain a service
			BeanFactory factory = this.getBeanFactory();
			Object service = factory.getBean(serviceName);

			// Logging information here
			if (logger.isDebugEnabled()) {
				logger.debug("Invoking Service: " + service.getClass().getName());
				logger.debug("Service name: " + serviceName);
				logger.debug("Method name: " + methodName);
			}

			Reflector.invoke(service, methodName, params);
		}
		catch (InvocationTargetException e) {
			throw e.getCause();
		}
	}

	/**
	 * Processes an exception thrown in an onMessage method. To process an
	 * exception, an ExceptionHandler must be configured in an
	 * ExceptionHandlerFactory class. An exception name is used to look up an
	 * exception handler configured in an axception handler factory.
	 * 
	 * @param e An exception to process
	 * @param message A message to pass to an exception handler for reference
	 */
	protected void processException( Throwable e, Object input ) {

		try {
			BeanFactory factory = this.getBeanFactory();
			ExceptionHandlerFactory exceptionFactory = 
				(ExceptionHandlerFactory) factory.getBean(ExceptionHandlerFactory.class.getName());
			ExceptionHandler handler = exceptionFactory.getExceptionHandler(e.getClass().getName());
			handler.handleException(e, e.getMessage(), input);
		}
		catch (Throwable t) {
			if (logger.isDebugEnabled()) {
				logger.debug( "Failed to get an exception handler to process exception ", t );
			}
		}
	}

	/**
	 * Builds an object representation of a jms message using the specific
	 * builder class. An iilegal argument exception is thrown
	 * 
	 * @param builder A builder to build an object from a jms message
	 * @param jmsMsg A jms message
	 * @return The built object
	 * @throws IllegalArgumentException thrown if required parameters are not
	 *            passed in
	 * @throws BuilderException thrown if failed to build an object
	 */
	protected Object processBuilder(Builder builder, Message jmsMsg)
			throws IllegalArgumentException, BuilderException {

		if (builder == null || jmsMsg == null) {
			throw new IllegalArgumentException(
					"Missing required parameters (builder/jms message)");
		}
		return builder.build(new JMSMetaData(jmsMsg));
	}

	/**
	 * Returns a message type. A message type is used to look up for a service
	 * descriptor, which encapsulates information about a service, a service
	 * name, and a builder class. Specific implementation of this method must be
	 * provided in a derived class.
	 * 
	 * @param message A message to query a message type
	 * @return String A message type
	 * @throws Exception Thrown if failed to obtain a message type
	 */
	protected String getMessageType(Message msg) throws InvalidMessageException {

		String type = null;
		try {
			type = msg.getStringProperty(getMessageTypeKey());
		}
		catch (JMSException e) {
			throw new InvalidMessageException( "Failed to get a message type from a message", e);
		}
		return type;
	}

	/** Returns a message initiater in JMS message using a default
	 * message intiater key "Message_Initiater"
	 * @param msg A JMS message
	 * @return a message initiater value
	 */
	protected String getMessageInitiater(Message msg) {

		String initiater = null;
		try {
			initiater = msg.getStringProperty(getMessageInitiaterKey());
		}
		catch (JMSException e) { 
			/* ok, to ignore if not there */
			if( logger.isDebugEnabled() ) {
				logger.debug( "DEFAULT_MESSAGE_INITIATER key is not transfered in a message" );
			}
		}
		return initiater;
	}

	/**
	 * Returns a key to look up a message type in a JMS message
	 * @return An message type key
	 */
	protected String getMessageTypeKey() {
		return ConfigurationConstants.DEFAULT_MESSAGE_TYPE;
	}

	/** Returns a default message initiater key to look for
	 * its value in a JMS message
	 * @return A message initiator
	 */
	protected String getMessageInitiaterKey() {
		return ConfigurationConstants.DEFAULT_MESSAGE_INITIATER;
	}

	/** Returns an environment variable defined in an ejb-jar.xml
	 * @param key A key to lookup an envrionment variable
	 * @return An environment variable
	 */
	protected String getEnvEntry(String key) {
		String envEntry = null;

		try {
			Context ctx = new InitialContext();
			envEntry = (String) ctx.lookup(ConfigurationConstants.JAVA_ENV + key);
		}
		catch (Exception e) {
			throw new RuntimeException("Failed to get Environment Entry for: " + key, e);
		}
		return envEntry;
	}
}