/* $This file is distributed under the terms of the license in LICENSE$ */

package edu.cornell.mannlib.vitro.webapp.startup;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Instantiate and run the ServletContextListeners for Vitro, while accumulating
 * messages in StartupStatus.
 * 
 * The startup listeners are stored in a file with one full-qualified class name
 * per line. Blank lines and comment lines (starting with '#') are ignored.
 * 
 * No exception in the listeners should prevent the successful completion.
 * However, an uncaught exception or a fatal error status will cause the
 * StartupStatusDisplayFilter to disply the problem instead of showing the home
 * page (or any other requested page).
 */
public class StartupManager implements ServletContextListener {
	private static final Log log = LogFactory.getLog(StartupManager.class);

	public static final String FILE_OF_STARTUP_LISTENERS = "/WEB-INF/resources/startup_listeners.txt";

	private final List<ServletContextListener> initializeList = new ArrayList<ServletContextListener>();

	/**
	 * These can be instance variables without risk, since contextInitialized()
	 * will only be called once per instance.
	 */
	private ServletContext ctx;
	private StartupStatus ss;

	/**
	 * Build a list of the listeners, and run contextInitialized() on each of
	 * them, at least until we get a fatal error.
	 * 
	 * Each step of this should handle its own exceptions, but we'll wrap the
	 * whole thing in a try/catch just in case.
	 */
	@Override
	public void contextInitialized(ServletContextEvent sce) {
		ctx = sce.getServletContext();
		ss = StartupStatus.getBean(ctx);

		try {
			findAndInstantiateListeners();

			for (ServletContextListener listener : initializeList) {
				if (ss.isStartupAborted()) {
					ss.listenerNotExecuted(listener);
				} else {
					initialize(listener, sce);
				}
			}
			log.info("Called 'contextInitialized' on all listeners.");
		} catch (Exception e) {
			ss.fatal(this, "Startup threw an unexpected exception.", e);
			log.error("Startup threw an unexpected exception.", e);
		} catch (Throwable t) {
			log.fatal("Startup threw an unexpected error.", t);
			throw t;
		}
	}

	/**
	 * Read the file and instantiate build a list of listener instances.
	 * 
	 * If there is a problem, it will occur and be handled in a sub-method.
	 */
	private void findAndInstantiateListeners() {
		List<String> classNames = readFileOfListeners();

		for (String className : classNames) {
			ServletContextListener listener = instantiateListener(className);
			if (listener != null) {
				initializeList.add(listener);
			}
		}

		checkForDuplicateListeners();
	}

	/**
	 * Read the names of the listener classes.
	 * 
	 * If there is a problem, set a fatal error, and return an empty list.
	 */
	private List<String> readFileOfListeners() {
		List<String> list = new ArrayList<String>();

		InputStream is = null;
		BufferedReader br = null;
		try {
			is = ctx.getResourceAsStream(FILE_OF_STARTUP_LISTENERS);
			br = new BufferedReader(new InputStreamReader(is));

			String line;
			while (null != (line = br.readLine())) {
				String trimmed = line.trim();
				if (!trimmed.isEmpty() && !trimmed.startsWith("#")) {
					list.add(trimmed);
				}
			}
		} catch (NullPointerException e) {
			ss.fatal(this, "Unable to locate the list of startup listeners: "
					+ FILE_OF_STARTUP_LISTENERS);
		} catch (IOException e) {
			ss.fatal(this,
					"Failed while processing the list of startup listeners:  "
							+ FILE_OF_STARTUP_LISTENERS, e);
		} finally {
			if (br != null) {
				try {
					br.close();
				} catch (IOException e) {
					log.error(e);
				}
			}
			if (is != null) {
				try {
					is.close();
				} catch (IOException e) {
					log.error(e);
				}
			}
		}

		log.debug("Classnames of listeners = " + list);
		return list;
	}

	/**
	 * Instantiate a context listener from this class name.
	 * 
	 * If there is a problem, set a fatal error, and return null.
	 */
	private ServletContextListener instantiateListener(String className) {
		try {
			Class<?> c = Class.forName(className);
			Object o = c.newInstance();
			return (ServletContextListener) o;
		} catch (ClassCastException e) {
			ss.fatal(this, "Instance of '" + className
					+ "' is not a ServletContextListener", e);
			return null;
		} catch (Exception | ExceptionInInitializerError e) {
			ss.fatal(this, "Failed to instantiate listener: '" + className
					+ "'", e);
			return null;
		}
    }

	/**
	 * Call contextInitialized() on the listener.
	 * 
	 * If there is an unexpected exception, set a fatal error.
	 */
	private void initialize(ServletContextListener listener,
			ServletContextEvent sce) {
		try {
			log.debug("Initializing '" + listener.getClass().getName() + "'");
			listener.contextInitialized(sce);
			ss.listenerExecuted(listener);
		} catch (Exception e) {
			ss.fatal(listener, "Threw unexpected exception", e);
		} catch (Throwable t) {
			log.fatal(listener + " Threw unexpected error", t);
			throw t;
		}
	}

	/**
	 * If we have more than one listener from the same class, set a fatal error.
	 */
	private void checkForDuplicateListeners() {
		for (int i = 0; i < initializeList.size(); i++) {
			for (int j = i + 1; j < initializeList.size(); j++) {
				ServletContextListener iListener = initializeList.get(i);
				ServletContextListener jListener = initializeList.get(j);
				if (iListener.getClass().equals(jListener.getClass())) {
					ss.fatal(this,
							("File contains duplicate listener classes: '"
									+ iListener.getClass().getName() + "'"));
				}
			}
		}
	}

	/**
	 * Notify the listeners that the context is being destroyed, in the reverse
	 * order from how they were notified at initialization.
	 */
	@Override
	public void contextDestroyed(ServletContextEvent sce) {
		List<ServletContextListener> destroyList = new ArrayList<ServletContextListener>(
				initializeList);
		Collections.reverse(destroyList);

		for (ServletContextListener listener : destroyList) {
			try {
				log.debug("Destroying '" + listener.getClass().getName() + "'");
				listener.contextDestroyed(sce);
			} catch (Exception e) {
				log.error("Unexpected exception from contextDestroyed() on '"
						+ listener.getClass().getName() + "'", e);
			} catch (Throwable t) {
				log.fatal("Unexpected error from contextDestroyed() on '"
						+ listener.getClass().getName() + "'", t);
				throw t;
			}
		}
		log.info("Called 'contextDestroyed' on all listeners.");
	}

}
