package org.red5.server.war;

/*
 * RED5 Open Source Flash Server - http://www.osflash.org/red5
 *
 * Copyright (c) 2006-2007 by respective authors (see below). All rights reserved.
 *
 * This library is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation; either version 2.1 of the License, or (at your option) any later
 * version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along
 * with this library; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

import java.beans.Introspector;
import java.rmi.Naming;
import java.rmi.RMISecurityManager;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.sql.Driver;
import java.sql.DriverManager;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Timer;
import java.util.TimerTask;

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

import org.apache.commons.logging.LogFactory;
import org.apache.log4j.Logger;
import org.red5.server.ClientRegistry;
import org.red5.server.Context;
import org.red5.server.GlobalScope;
import org.red5.server.MappingStrategy;
import org.red5.server.ScopeResolver;
import org.red5.server.Server;
import org.red5.server.WebScope;
import org.red5.server.jmx.JMXAgent;
import org.red5.server.service.ServiceInvoker;
import org.springframework.beans.factory.access.BeanFactoryReference;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.access.ContextBeanFactoryReference;
import org.springframework.web.context.ConfigurableWebApplicationContext;
import org.springframework.web.context.ContextLoader;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;

/**
 * Entry point from which the server config file is loaded while running within
 * a J2EE application container.
 * 
 * This listener should be registered after Log4jConfigListener in web.xml, if
 * the latter is used.
 * 
 * @author The Red5 Project (red5@osflash.org)
 * @author Paul Gregoire (mondain@gmail.com)
 */
public class RootContextLoaderServlet extends ContextLoaderListener {

	private final static long serialVersionUID = 41919712007L;

	private static ClassLoader myClassloader;

	// Initialize Logging
	public static Logger logger = Logger
			.getLogger(RootContextLoaderServlet.class.getName());

	private static Timer timer;

	private CheckScopeListTask checkScopeList;

	private static ArrayList<ServletContext> registeredContexts = new ArrayList<ServletContext>(
			3);

	private ConfigurableWebApplicationContext applicationContext;

	private DefaultListableBeanFactory parentFactory;

	private static ServletContext servletContext;

	private static RootContextLoaderServlet instance;

	private ClientRegistry clientRegistry;

	private ServiceInvoker globalInvoker;

	private MappingStrategy globalStrategy;

	private ScopeResolver globalResolver;

	private GlobalScope global;

	private Server server;

	protected Integer rmiPort = 1099;

	/**
	 * Main entry point for the Red5 Server as a war
	 */
	// Notification that the web application is ready to process requests
	@Override
	public void contextInitialized(ServletContextEvent sce) {
		if (null != servletContext) {
			return;
		}
		instance = this;
		System.setProperty("red5.deployment.type", "war");

		myClassloader = getClass().getClassLoader();

		servletContext = sce.getServletContext();
		String prefix = servletContext.getRealPath("/");

		servletContext.setAttribute("root.classloader", myClassloader);

		initRegistry(servletContext);

		long time = System.currentTimeMillis();

		logger.info("RED5 Server (http://www.osflash.org/red5)");
		logger.info("Root context loader");
		logger.debug("Path: " + prefix);

		try {
			// instance the context loader
			ContextLoader loader = createContextLoader();
			applicationContext = (ConfigurableWebApplicationContext) loader
					.initWebApplicationContext(servletContext);
			logger.debug("Root context path: "
					+ applicationContext.getServletContext().getContextPath());

			ConfigurableBeanFactory factory = applicationContext
					.getBeanFactory();

			// register default
			factory.registerSingleton("default.context", applicationContext);

			// get the main factory
			parentFactory = (DefaultListableBeanFactory) factory
					.getParentBeanFactory();

			// create a wrapper around our primary context
			BeanFactoryReference beanfactoryRef = new ContextBeanFactoryReference(
					applicationContext);

			// set it in the root servlet context
			servletContext.setAttribute("bean.factory.ref", beanfactoryRef);

			// set a remoting codec factory for AMF use
			servletContext.setAttribute("remoting.codec.factory", parentFactory
					.getBean("remotingCodecFactory"));

			server = (Server) parentFactory.getBean("red5.server");

			clientRegistry = (ClientRegistry) factory
					.getBean("global.clientRegistry");

			globalInvoker = (ServiceInvoker) factory
					.getBean("global.serviceInvoker");

			globalStrategy = (MappingStrategy) factory
					.getBean("global.mappingStrategy");

			global = (GlobalScope) factory.getBean("global.scope");
			logger.debug("GlobalScope: " + global);
			global.setServer(server);
			global.register();
			global.start();

			globalResolver = new ScopeResolver();
			globalResolver.setGlobalScope(global);

			logger.debug("About to grab Webcontext bean for Global");
			Context globalContext = (Context) factory.getBean("global.context");
			globalContext.setCoreBeanFactory(parentFactory);
			globalContext.setClientRegistry(clientRegistry);
			globalContext.setServiceInvoker(globalInvoker);
			globalContext.setScopeResolver(globalResolver);
			globalContext.setMappingStrategy(globalStrategy);

			logger.debug("About to grab Webcontext bean for ROOT");
			Context webContext = (Context) factory.getBean("web.context");
			webContext.setCoreBeanFactory(parentFactory);
			webContext.setClientRegistry(clientRegistry);
			webContext.setServiceInvoker(globalInvoker);
			webContext.setScopeResolver(globalResolver);
			webContext.setMappingStrategy(globalStrategy);

			WebScope scope = (WebScope) factory.getBean("web.scope");
			scope.setServer(server);
			scope.setParent(global);
			scope.register();
			scope.start();

			// grab the scope list (other war/webapps)
			IRemotableList remote = (IRemotableList) Naming
					.lookup("rmi://localhost:" + rmiPort + "/subContextList");
			logger.debug("Children: " + remote.numChildren());
			if (remote.hasChildren()) {
				logger.debug("Children were detected");
				for (int i = 0; i < remote.numChildren(); i++) {
					logger.debug("Enumerating children");
					WebSettings settings = remote.getAt(i);
					registerSubContext(settings.getWebAppKey());
				}
				logger.debug("End of children...");
			}

		} catch (Throwable t) {
			logger.error(t);
		} finally {
			timer = new Timer();
			checkScopeList = new CheckScopeListTask();
			timer.scheduleAtFixedRate(checkScopeList, 1000, 30000);
		}

		long startupIn = System.currentTimeMillis() - time;
		logger.info("Startup done in: " + startupIn + " ms");

	}

	/*
	 * Registers a subcontext with red5
	 */
	public void registerSubContext(String webAppKey) {
		// get the sub contexts - servlet context
		ServletContext ctx = servletContext.getContext(webAppKey);
		logger.info("Registering subcontext for servlet context: "
				+ ctx.getContextPath());
		if (registeredContexts.contains(ctx)) {
			logger.debug("Context is already registered: " + webAppKey);
			return;
		}

		ContextLoader loader = new ContextLoader();

		ConfigurableWebApplicationContext appCtx = (ConfigurableWebApplicationContext) loader
				.initWebApplicationContext(ctx);
		appCtx.setParent(applicationContext);
		appCtx.refresh();

		ctx.setAttribute(
				WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,
				appCtx);

		ConfigurableBeanFactory appFactory = appCtx.getBeanFactory();

		logger.debug("About to grab Webcontext bean for " + webAppKey);
		Context webContext = (Context) appCtx.getBean("web.context");
		webContext.setCoreBeanFactory(parentFactory);
		webContext.setClientRegistry(clientRegistry);
		webContext.setServiceInvoker(globalInvoker);
		webContext.setScopeResolver(globalResolver);
		webContext.setMappingStrategy(globalStrategy);

		WebScope scope = (WebScope) appFactory.getBean("web.scope");
		scope.setServer(server);
		scope.setParent(global);
		scope.register();
		scope.start();

		// register the context so we dont try to reinitialize it
		registeredContexts.add(ctx);

	}

	/**
	 * Clearing the in-memory configuration parameters, we will receive
	 * notification that the servlet context is about to be shut down
	 */
	@Override
	public void contextDestroyed(ServletContextEvent sce) {
		synchronized (instance) {
			logger.info("Webapp shutdown");
			// XXX Paul: grabbed this from
			// http://opensource.atlassian.com/confluence/spring/display/DISC/Memory+leak+-+classloader+won%27t+let+go
			// in hopes that we can clear all the issues with J2EE containers
			// during shutdown
			try {
				ServletContext ctx = sce.getServletContext();
				// if the ctx being destroyed is root then kill the timer
				if (ctx.getContextPath().equals("/ROOT")) {
					timer.cancel();
				} else {
					// remove from registered list
					registeredContexts.remove(ctx);
				}
				// prepare spring for shutdown
				Introspector.flushCaches();
				// dereg any drivers
				for (Enumeration e = DriverManager.getDrivers(); e
						.hasMoreElements();) {
					Driver driver = (Driver) e.nextElement();
					if (driver.getClass().getClassLoader() == getClass()
							.getClassLoader()) {
						DriverManager.deregisterDriver(driver);
					}
				}
				// shutdown jmx
				JMXAgent.shutdown();
				// shutdown spring
				Object attr = ctx
						.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
				if (attr != null) {
					// get web application context from the servlet context
					ConfigurableWebApplicationContext applicationContext = (ConfigurableWebApplicationContext) attr;
					ConfigurableBeanFactory factory = applicationContext
							.getBeanFactory();
					// for (String scope : factory.getRegisteredScopeNames()) {
					// logger.debug("Registered scope: " + scope);
					// }
					try {
						for (String singleton : factory.getSingletonNames()) {
							logger.debug("Registered singleton: " + singleton);
							factory.destroyScopedBean(singleton);
						}
					} catch (RuntimeException e) {
					}
					factory.destroySingletons();

					ctx
							.removeAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
					applicationContext.close();
				}
				instance.getContextLoader().closeWebApplicationContext(ctx);
			} catch (Throwable e) {
				e.printStackTrace();
			} finally {
				// http://jakarta.apache.org/commons/logging/guide.html#Classloader_and_Memory_Management
				// http://wiki.apache.org/jakarta-commons/Logging/UndeployMemoryLeak?action=print
				LogFactory.release(Thread.currentThread()
						.getContextClassLoader());
			}
		}
	}

	protected void initRegistry(ServletContext ctx) {
		Registry r = null;
		try {
			Object o = ctx.getInitParameter("rmiPort");
			if (o != null) {
				rmiPort = Integer.valueOf((String) o);
			}
			if (System.getSecurityManager() != null) {
				System.setSecurityManager(new RMISecurityManager());
			}
			// lookup the registry
			r = LocateRegistry.getRegistry(rmiPort);
			// ensure we are not already registered with the registry
			for (String regName : r.list()) {
				logger.debug("Registry entry: " + regName);
			}
		} catch (RemoteException re) {
			logger.info("RMI Registry server was not found on port " + rmiPort);
			// if we didnt find the registry and the user wants it created
			try {
				logger.info("Starting an internal RMI registry");
				// create registry for rmi
				r = LocateRegistry.createRegistry(rmiPort);
			} catch (RemoteException e) {
				logger.info("RMI Registry server was not started on port "
						+ rmiPort);
			}

		}
	}

	/**
	 * Task to check the scope list periodically for new contexts
	 */
	public final class CheckScopeListTask extends TimerTask {
		@Override
		public void run() {
			logger.debug("Checking scope list");
			try {
				// grab the scope list (other war/webapps)
				IRemotableList remote = (IRemotableList) Naming
						.lookup("rmi://localhost:" + rmiPort
								+ "/subContextList");
				logger.debug("Children: " + remote.numChildren());
				if (remote.hasChildren()) {
					logger.debug("Children were detected");
					for (int i = 0; i < remote.numChildren(); i++) {
						logger.debug("Enumerating children");
						WebSettings settings = remote.getAt(i);
						registerSubContext(settings.getWebAppKey());
					}
					logger.debug("End of children...");
				}
			} catch (Throwable t) {
				logger.error(t);
			}
		}
	}

}
