package hudson;

import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider;
import com.thoughtworks.xstream.core.JVM;
import hudson.model.Hudson;
import hudson.model.User;
import hudson.triggers.SafeTimerTask;
import hudson.triggers.Trigger;
import hudson.util.HudsonIsLoading;
import hudson.util.IncompatibleServletVersionDetected;
import hudson.util.IncompatibleVMDetected;
import hudson.util.InsufficientPermissionDetected;
import hudson.util.NoHomeDir;
import hudson.util.RingBufferLogHandler;
import hudson.util.NoTempDir;
import hudson.util.IncompatibleAntVersionDetected;
import hudson.util.HudsonFailedToLoad;
import org.jvnet.localizer.LocaleProvider;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.apache.tools.ant.types.FileSet;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.ServletResponse;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Locale;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Entry point when Hudson is used as a webapp.
 *
 * @author Kohsuke Kawaguchi
 */
public class WebAppMain implements ServletContextListener {
    private final RingBufferLogHandler handler = new RingBufferLogHandler();
    private static final String APP = "app";

    /**
     * Creates the sole instance of {@link Hudson} and register it to the {@link ServletContext}.
     */
    public void contextInitialized(ServletContextEvent event) {
        try {
            final ServletContext context = event.getServletContext();

            // use the current request to determine the language
            LocaleProvider.setProvider(new LocaleProvider() {
                public Locale get() {
                    Locale locale=null;
                    StaplerRequest req = Stapler.getCurrentRequest();
                    if(req!=null)
                        locale = req.getLocale();
                    if(locale==null)
                        locale = Locale.getDefault();
                    return locale;
                }
            });

            // quick check to see if we (seem to) have enough permissions to run. (see #719)
            JVM jvm;
            try {
                jvm = new JVM();
                new URLClassLoader(new URL[0],getClass().getClassLoader());
            } catch(SecurityException e) {
                context.setAttribute(APP,new InsufficientPermissionDetected(e));
                return;
            }

            installLogger();

            final File home = getHomeDir(event).getAbsoluteFile();
            home.mkdirs();
            System.out.println("hudson home directory: "+home);

            // check that home exists (as mkdirs could have failed silently), otherwise throw a meaningful error
            if (! home.exists()) {
                context.setAttribute(APP,new NoHomeDir(home));
                return;
            }

            // make sure that we are using XStream in the "enhanced" (JVM-specific) mode
            if(jvm.bestReflectionProvider().getClass()==PureJavaReflectionProvider.class) {
                // nope
                context.setAttribute(APP,new IncompatibleVMDetected());
                return;
            }

            // make sure this is servlet 2.4 container or above
            try {
                ServletResponse.class.getMethod("setCharacterEncoding",String.class);
            } catch (NoSuchMethodException e) {
                context.setAttribute(APP,new IncompatibleServletVersionDetected(ServletResponse.class));
                return;
            }

            // make sure that we see Ant 1.7
            try {
                FileSet.class.getMethod("getDirectoryScanner");
            } catch (NoSuchMethodException e) {
                context.setAttribute(APP,new IncompatibleAntVersionDetected(FileSet.class));
                return;
            }

            // some containers (in particular Tomcat) doesn't abort a launch
            // even if the temp directory doesn't exist.
            // check that and report an error
            try {
                File f = File.createTempFile("test", "test");
                f.delete();
            } catch (IOException e) {
                context.setAttribute(APP,new NoTempDir(e));
                return;
            }

            // Tomcat breaks XSLT with JDK 5.0 and onward. Check if that's the case, and if so,
            // try to correct it
            try {
                TransformerFactory.newInstance();
                // if this works we are all happy
            } catch (TransformerFactoryConfigurationError x) {
                // no it didn't.
                Logger logger = Logger.getLogger(WebAppMain.class.getName());

                logger.log(Level.WARNING, "XSLT not configured correctly. Hudson will try to fix this. See http://issues.apache.org/bugzilla/show_bug.cgi?id=40895 for more details",x);
                System.setProperty(TransformerFactory.class.getName(),"com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl");
                try {
                    TransformerFactory.newInstance();
                    logger.info("XSLT is set to the JAXP RI in JRE");
                } catch(TransformerFactoryConfigurationError y) {
                    logger.log(Level.SEVERE, "Failed to correct the problem.");
                }
            }

            Stapler.setExpressionFactory(event, new ExpressionFactory2());

            context.setAttribute(APP,new HudsonIsLoading());

            new Thread("hudson initialization thread") {
                public void run() {
                    try {
                        computeVersion(context);

                        try {
                            context.setAttribute(APP,new Hudson(home,context));
                        } catch( IOException e ) {
                            throw new Error(e);
                        }

                        Trigger.init(); // start running trigger

                        // trigger the loading of changelogs in the background,
                        // but give the system 10 seconds so that the first page
                        // can be served quickly
                        Trigger.timer.schedule(new SafeTimerTask() {
                            public void doRun() {
                                User.getUnknown().getBuilds();
                            }
                        }, 1000*10);
                    } catch (Error e) {
                        LOGGER.log(Level.SEVERE, "Failed to initialize Hudson",e);
                        context.setAttribute(APP,new HudsonFailedToLoad(e));
                        throw e;
                    } catch (RuntimeException e) {
                        LOGGER.log(Level.SEVERE, "Failed to initialize Hudson",e);
                        context.setAttribute(APP,new HudsonFailedToLoad(e));
                        throw e;
                    }
                }
            }.start();
        } catch (Error e) {
            LOGGER.log(Level.SEVERE, "Failed to initialize Hudson",e);
            throw e;
        } catch (RuntimeException e) {
            LOGGER.log(Level.SEVERE, "Failed to initialize Hudson",e);
            throw e;
        }
    }

    protected void computeVersion(ServletContext context) {
        // set the version
        Properties props = new Properties();
        try {
            InputStream is = getClass().getResourceAsStream("hudson-version.properties");
            if(is!=null)
                props.load(is);
        } catch (IOException e) {
            e.printStackTrace(); // if the version properties is missing, that's OK.
        }
        String ver = props.getProperty("version");
        if(ver==null)   ver="?";
        Hudson.VERSION = ver;
        context.setAttribute("version",ver);
        String verHash = Util.getDigestOf(ver).substring(0, 8);

        if(ver.equals("?"))
            Hudson.RESOURCE_PATH = "";
        else
            Hudson.RESOURCE_PATH = "/static/"+verHash;

        Hudson.VIEW_RESOURCE_PATH = "/resources/"+ verHash;
    }

	/**
     * Installs log handler to monitor all Hudson logs.
     */
    private void installLogger() {
        Hudson.logRecords = handler.getView();
        Logger.getLogger("hudson").addHandler(handler);
    }

    /**
     * Determines the home directory for Hudson.
     *
     * People makes configuration mistakes, so we are trying to be nice
     * with those by doing {@link String#trim()}.
     */
    private File getHomeDir(ServletContextEvent event) {
        // check JNDI for the home directory first
        try {
            InitialContext iniCtxt = new InitialContext();
            Context env = (Context) iniCtxt.lookup("java:comp/env");
            String value = (String) env.lookup("HUDSON_HOME");
            if(value!=null && value.trim().length()>0)
                return new File(value.trim());
            // look at one more place. See issue #1314 
            value = (String) iniCtxt.lookup("HUDSON_HOME");
            if(value!=null && value.trim().length()>0)
                return new File(value.trim());
        } catch (NamingException e) {
            // ignore
        }

        // finally check the system property
        String sysProp = System.getProperty("HUDSON_HOME");
        if(sysProp!=null)
            return new File(sysProp.trim());
        
        // look at the env var next
        String env = EnvVars.masterEnvVars.get("HUDSON_HOME");
        if(env!=null)
            return new File(env.trim()).getAbsoluteFile();

        // otherwise pick a place by ourselves

        String root = event.getServletContext().getRealPath("/WEB-INF/workspace");
        if(root!=null) {
            File ws = new File(root.trim());
            if(ws.exists())
                // Hudson <1.42 used to prefer this before ~/.hudson, so
                // check the existence and if it's there, use it.
                // otherwise if this is a new installation, prefer ~/.hudson
                return ws;
        }

        // if for some reason we can't put it within the webapp, use home directory.
        return new File(new File(System.getProperty("user.home")),".hudson");
    }

    public void contextDestroyed(ServletContextEvent event) {
        Hudson instance = Hudson.getInstance();
        if(instance!=null)
            instance.cleanUp();

        // Logger is in the system classloader, so if we don't do this
        // the whole web app will never be undepoyed.
        Logger.getLogger("hudson").removeHandler(handler);
    }

    private static final Logger LOGGER = Logger.getLogger(WebAppMain.class.getName());

}
