/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2005-2010 Bull S.A.S.
 * Contact: jonas-team@ow2.org
 *
 * 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.web.tomcat7;

import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.naming.Context;
import javax.servlet.ServletException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.catalina.Container;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.Loader;
import org.apache.catalina.Wrapper;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardWrapper;
import org.apache.catalina.deploy.ContextEnvironment;
import org.apache.catalina.deploy.ContextResource;
import org.apache.catalina.deploy.MessageDestinationRef;
import org.apache.catalina.deploy.NamingResources;
import org.apache.catalina.loader.WebappLoader;
import org.ow2.jonas.deployment.web.WebContainerDeploymentDesc;
import org.ow2.jonas.lib.bootstrap.LoaderManager;
import org.ow2.jonas.lib.loader.FilteringClassLoader;
import org.ow2.jonas.lib.management.javaee.J2eeObjectName;
import org.ow2.jonas.web.tomcat7.loader.NoSystemAccessWebappClassLoader;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Text;




/**
 * Define a JOnAS context. It is used to check if a context has been defined in
 * server.xml and to use this context while deploying the war instead of
 * creating a new one
 * @author Florent Benoit
 * @author Philippe Coq (Tomcat 4.0)
 */
public class JOnASStandardContext extends StandardContext {

    /**
     * UID for the serialization.
     */
    private static final long serialVersionUID = 1L;

    /**
     * Unique instance of Catalina.
     */
    private static transient Tomcat7Service catalinaService = null;

    /**
     * Logger for this service.
     */
    private static Log logger = LogFactory.getLog(JOnASStandardContext.class);

    /**
     * URL of the ear.
     */
    private URL earURL = null;

    /**
     * We are in ear case or not ?
     */
    private boolean inEarCase = false;

    /**
     * This context was configured in server.xml ?
     */
    private boolean inServerXml = false;

    /**
     * Java 2 delegation model is use or not ?
     */
    private boolean java2DelegationModel = false;

    /**
     * WEB deployment descriptor.
     */
    private WebContainerDeploymentDesc webDeploymentDescriptor = null;

    /**
     * JOnAS deployment descriptor.
     */
    private String jonasDeploymentDescriptor = null;

    /**
     * Context was started ?
     */
    private boolean startedJStdx = false;

    /**
     * URL of the war.
     */
    private URL warURL = null;

    /**
     * J2EEApplication MBean OBJECT_NAME in ear case.
     */
    private String earON = null;

    /**
     * EJB ClassLoader.
     */
    private ClassLoader ejbClassLoader = null;

    /**
     * EAR ClassLoader.
     */
    private ClassLoader earClassLoader = null;

    /**
     * Filtering ClassLoader.
     */
    private FilteringClassLoader filteringClassLoader = null;

    /**
     * Injections map.
     */
    private Map<String, Map<String, String>> injectionsMap = null;

    /**
     * Env Context.
     */
    private Context envContext = null;
    
    /**
     * The naming resources for this web application.
     */
    private NamingResources namingResources = null;

    /**
     * Constructor of the Context.
     */
    public JOnASStandardContext() {
        this(true, true, false);
    }

    /**
     * Constructor of the Context.
     * @param inServerXml this context was defined in server.xml
     * @param java2DelegationModel delegation model for classloader is true ?
     * @param inEarCase if we are or not in EAR case
     */
    public JOnASStandardContext(final boolean inServerXml, final boolean java2DelegationModel, final boolean inEarCase) {
        super();
        this.inServerXml = inServerXml;
        this.java2DelegationModel = java2DelegationModel;
        this.inEarCase = inEarCase;
    }

    /**
     * Assign the parent Tomcat6 WebContainerService.
     * @param tomcat {@link Tomcat7Service} instance.
     */
    public void setTomcatService(final Tomcat7Service tomcat) {
        catalinaService = tomcat;
    }

    /**
     * @return Returns the earURL.
     */
    public URL getEarURL() {
        return earURL;
    }

    /**
     * Gets the web deployment descriptor content.
     * @return the content of the web deployment descriptor
     */
    public WebContainerDeploymentDesc getWebDeploymentDescriptor() {
        return webDeploymentDescriptor;
    }

    /**
     * Gets the deployment descriptor content of jonas-web.xml file.
     * @return the content of jonas-web.xml file
     */
    public String getJonasDeploymentDescriptor() {
        return jonasDeploymentDescriptor;
    }

    /**
     * @return Returns the warURL.
     */
    public URL getWarURL() {
        return warURL;
    }

    /**
     * @return true if this web module is a part of a EAR
     */
    public boolean isInEarCase() {
        return inEarCase;
    }

    /**
     * This context was defined in server.xml ?
     * @return true if this context was defined in server.xml
     */
    public boolean isInServerXml() {
        return inServerXml;
    }

    /**
     * @return true if the Java2 delegation model is used
     */
    public boolean isJava2DelegationModel() {
        return java2DelegationModel;
    }

    /**
     * @param earURL The earURL to set.
     */
    protected void setEarURL(final URL earURL) {
        this.earURL = earURL;
        // Determine the corresponding J2EEApplication ObjectName
        ObjectName j2eeAppOn = null;
        // Catalina StandardContext provides setter and getter for
        // j2eeApplication attribute
        // The setter was called in
        // CatalinaJWebContainerServiceImpl.doRegisterWar method
        // befor the call to this setter
        String appName = getJ2EEApplication();
        // setServer() was called before this setter
        String serverON = getServer();
        if (appName != null && serverON != null) {
            ObjectName serverOn;
            try {
                serverOn = ObjectName.getInstance(serverON);
                String domainName = serverOn.getDomain();
                String serverName = serverOn.getKeyProperty("name");
                j2eeAppOn = J2eeObjectName.J2EEApplication(domainName, serverName, appName);
            } catch (MalformedObjectNameException e) {
                logger.error("Cannot build the ObjectName for the context ''{0}''", e);
            }
        }
        if (j2eeAppOn != null) {
            earON = j2eeAppOn.toString();
        }
    }

    /**
     * @param inEarCase The inEarCase to set.
     */
    protected void setInEarCase(final boolean inEarCase) {
        this.inEarCase = inEarCase;
    }

    /**
     * @param java2DelegationModel The java2DelegationModel to set.
     */
    protected void setJava2DelegationModel(final boolean java2DelegationModel) {
        this.java2DelegationModel = java2DelegationModel;
    }

    /**
     * Set the web deployment descriptor content.
     * @param webDeploymentDescriptor the content of the web deployment descriptor
     */
    public void setWebDeploymentDescriptor(final WebContainerDeploymentDesc webDeploymentDescriptor) {
        this.webDeploymentDescriptor = webDeploymentDescriptor;
    }

    /**
     * Set the deployment descriptor content of jonas-web.xml file.
     * @param jonasDeploymentDescriptor the content of jonas-web.xml
     */
    public void setJonasDeploymentDescriptor(final String jonasDeploymentDescriptor) {
        this.jonasDeploymentDescriptor = jonasDeploymentDescriptor;
    }

    /**
     * @param warURL The warURL to set.
     */
    protected void setWarURL(final URL warURL) {
        this.warURL = warURL;
    }

    /**
     * Start the JOnAS context if catalina is started.
     * @throws LifecycleException if the context can't be started
     */
    @Override
    public synchronized void startInternal() throws LifecycleException {
        if (catalinaService != null && catalinaService.isTomcatStarted()) {
            if (logger.isDebugEnabled()) {
                logger.debug("Tomcat in Web container service is started, starting the context...");
            }
            startedJStdx = true;
            super.startInternal();
        }

    }

    /**
     * Stop this Context component.
     * @exception LifecycleException if a shutdown error occurs
     */
    @Override
    public synchronized void stopInternal() throws LifecycleException {
        if (startedJStdx) {
            startedJStdx = false;
            super.stopInternal();
        }

    }

    /**
     * Add a resource reference for this web application. Do nothing when
     * running inside JOnAS. Resources are managed by JOnAS
     * @param resource New resource reference
     */
    public void addResource(final ContextResource resource) {
    }

    /**
     * Add an environment entry for this web application. Do nothing when
     * running inside JOnAS. ENC environment is managed by JOnAS
     * @param environment New environment entry
     */
    public void addEnvironment(final ContextEnvironment environment) {
    }

    /**
     * Add a message destination reference for this web application.  Do nothing when
     * running inside JOnAS. Resources are managed by JOnAS
     * @param mdr New message destination reference
     */
    @Override
    public void addMessageDestinationRef(final MessageDestinationRef mdr) {

    }

    /**
     * Set the parent class loader for this web application. Do it only if it is
     * not started.
     * @param parent The new parent class loader
     */
    @Override
    public void setParentClassLoader(final ClassLoader parent) {
        // Do it only if the context is not already started
        if (!startedJStdx) {
            super.setParentClassLoader(parent);
        }
    }

    /**
     * @return OBJECT_NAME of the parent J2EEApplication MBean
     */
    public String getEarON() {
        return earON;
    }

    /**
     * Override the working directory (which is going to Catalina.base/work usually).
     * @return a path which is referencing the JOnAS work directory
     */
    @Override
    public String getWorkDir() {
        String superWorkDir = super.getWorkDir();
        if (superWorkDir == null) {
            // Retrieve our parent (normally a host) name
            String hostName = null;
            String engineName = null;
            Container parentHost = getParent();
            if (parentHost != null) {
                hostName = parentHost.getName();
                Container parentEngine = parentHost.getParent();
                if (parentEngine != null) {
                    engineName = parentEngine.getName();
                }
            }
            if ((hostName == null) || (hostName.length() < 1)) {
                hostName = "_";
            }
            if ((engineName == null) || (engineName.length() < 1)) {
                engineName = "_";
            }

            String temp = getPath();
            if (temp.startsWith("/")) {
                temp = temp.substring(1);
            }
            temp = temp.replace('/', '_');
            temp = temp.replace('\\', '_');
            if (temp.length() < 1) {
                temp = "_";
            }
            superWorkDir = catalinaService.getServerProperties().getWorkDirectory() + File.separator + "tomcat"
                    + File.separator + engineName + File.separator + hostName + File.separator + temp;
            new File(superWorkDir).mkdirs();
            super.setWorkDir(superWorkDir);
        }
        return superWorkDir;

    }

    /**
     * Change the type of the WebappClassLoader to be instanciated.
     *
     * @param loader The newly associated loader
     */
    @Override
    public void setLoader(final Loader loader) {
        // In all cases, update the loader type
        if (loader instanceof WebappLoader) {
            ((WebappLoader) loader).setLoaderClass(NoSystemAccessWebappClassLoader.class.getName());
        }
        super.setLoader(loader);

        // init the instance manager
    }

    /**
     * Defines the env context used to perform lookup in the instance manager.
     * @param envContext the given ENC context
     */
    public void setEnvContext(final Context envContext) {
        this.envContext = envContext;
    }

    /**
     * Defines the injection map used for each class.
     * @param injectionsMap the injection map
     */
    public void setInjectionMap(final Map<String, Map<String, String>> injectionsMap) {
        this.injectionsMap = injectionsMap;
    }


    /**
     * @return ejb classloader
     */
    public ClassLoader getEjbClassLoader() {
        return ejbClassLoader;
    }

    /**
     * Sets ejb classloader.
     * @param ejbClassLoader the given classloader
     */
    public void setEjbClassLoader(final ClassLoader ejbClassLoader) {
        this.ejbClassLoader = ejbClassLoader;
    }

    /**
     * Sets the EAR classloader.
     * @param earClassLoader the given EAR classloader
     */
    public void setEarClassLoader(final ClassLoader earClassLoader) {
        this.earClassLoader = earClassLoader;
    }


    /**
     * Gets data about loading a given class.
     * @param className the class name
     * @return a string description for the given class that needs to be loaded
     */
    public String loadClass(final String className) {
        if (!startedJStdx) {
            throw new IllegalStateException("Cannot load a class if context is not started");
        }

        // Get ClassLoader
        ClassLoader systemClassLoader = null;
        try {
            systemClassLoader = LoaderManager.getInstance().getExternalLoader();
        } catch (Exception e) {
            throw new IllegalStateException("Unable to get LoaderManager", e);
        }


        // Create XML document...

        // Create builder with factory
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = null;
        try {
            builder = factory.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            throw new IllegalStateException("Cannot build document builder", e);
        }

        // Create Document
        Document document = builder.newDocument();

        // Append root element
        Element classElement = document.createElement("class");
        document.appendChild(classElement);

        // name
        classElement.setAttribute("name", className);


        boolean classNotFound = false;
        String error = null;
        ClassLoader webAppClassLoader = getLoader().getClassLoader();
        Class<?> clazz = null;
        try {
            clazz = webAppClassLoader.loadClass(className);
        } catch (ClassNotFoundException e) {
            error = e.toString();
            classNotFound = true;
        } catch (Error e) {
            classNotFound = true;
            error = e.toString();
        }

        // class not found ? (add error content if NotFound)
        classElement.setAttribute("classNotFound", Boolean.toString(classNotFound));
        if (classNotFound) {
            Element errorElement = document.createElement("error");
            classElement.appendChild(errorElement);
            Text errorText = document.createTextNode(error);
            errorElement.appendChild(errorText);
        } else {
            // Class found ! Add details (if any)

            // Search if the classes was loaded from the module, from the application or from the system
            String type = "Application/WebApp (This module)";
            ClassLoader classClassLoader = clazz.getClassLoader();
            ClassLoader cl = webAppClassLoader;
            boolean found = false;
            while (cl != null && !found) {

                // ClassLoader is equals to the classloader that has loaded the class
                if (cl.equals(classClassLoader)) {
                    found = true;
                }

                if (systemClassLoader.equals(cl)) {
                    type = "System";
                }

                if (cl.equals(earClassLoader)) {
                    type = "Application/EAR";
                }

                if (cl.equals(ejbClassLoader)) {
                    type = "Application/EJBs";
                }

                cl = cl.getParent();
            }

            // Add where the class has been found
           classElement.setAttribute("where", type);


           // ClassLoader info (if any)
           if (classClassLoader != null) {
               Element classLoaderElement = document.createElement("class-loader");
               classElement.appendChild(classLoaderElement);
               classLoaderElement.setAttribute("name", classClassLoader.getClass().getName());
               Text classLoaderText = document.createTextNode(classClassLoader.toString());
               classLoaderElement.appendChild(classLoaderText);
            }
        }

        StringWriter stringWriter = new StringWriter();
        StreamResult streamResult = new StreamResult(stringWriter);

        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        Transformer transformer;
        try {
            transformer = transformerFactory.newTransformer();
        } catch (TransformerConfigurationException e) {
            throw new IllegalStateException("Unable to get a new transformer", e);
        }

        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");

        // transform OUTPUT
        try {
            transformer.transform(new DOMSource(document), streamResult);
        } catch (TransformerException e) {
            throw new IllegalStateException("Unable to transform the document", e);
        }

        return stringWriter.toString();
    }

    /**
     * Gets the filters of classloader.
     * @return the filters for the war classloader
     */
    public String[] getClassLoaderFilters() {
        List<String> filters = filteringClassLoader.getFilters();
        return filters.toArray(new String[filters.size()]);
    }

    /**
     * Gets all the URL to the given resource.
     * @param resourceName the name of the resource
     * @return the list of url, if any
     */
    public URL[] getResources(final String resourceName)  {
        if (!startedJStdx) {
            throw new IllegalStateException("Cannot load a resource if context is not started");
        }
        ClassLoader webAppClassLoader = getLoader().getClassLoader();

        Enumeration<URL> urls = null;
        try {
            urls = webAppClassLoader.getResources(resourceName);
        } catch (IOException e) {
            throw new IllegalStateException("Unable to get the resource '" + resourceName + "'.", e);
        }

        List<URL> urlsList = new ArrayList<URL>();
        while (urls.hasMoreElements()) {
            urlsList.add(urls.nextElement());
        }

        return urlsList.toArray(new URL[urlsList.size()]);
    }

    /**
     * Sets the filtering classloader.
     * @param filteringClassLoader the given classloader
     */
    public void setFilteringClassLoader(final FilteringClassLoader filteringClassLoader) {
        this.filteringClassLoader = filteringClassLoader;
    }

    /**
     * Return the real path for a given virtual path, if possible; otherwise
     * return <code>null</code>.
     *
     * @param path The path to the desired resource
     */
    @Override
    public String getRealPath(final String path) {
        if (path != null && path.startsWith("bundle")) {
            return null;
        }
        return super.getRealPath(path);
    }
    
    
    /**
     * Return the naming resources associated with this web application.
     */
    @Override
    public NamingResources getNamingResources() {

        if (namingResources == null) {
            this.namingResources = new JOnASNamingResources();
            setNamingResources(namingResources);
        }
        return super.getNamingResources();

    }

    /**
     * Overrides {@link StandardContext#loadOnStartup(org.apache.catalina.Container[])} because it silently bypass some exceptions.
     * We allow to use a custom attribute in the {@link javax.servlet.ServletContext} to specify a list of exceptions that should
     * not be bypassed. This attribute is named "jonas.tomcat.load.on.startup.preserve.exceptions" and should contains a
     * {@link Collection} of {@link Class}es representing {@link Throwable}s
     * @param children
     */
    @Override
    public void loadOnStartup(Container[] children) {
        // Collect "load on startup" servlets that need to be initialized
        TreeMap<Integer, ArrayList<Wrapper>> map =
                new TreeMap<Integer, ArrayList<Wrapper>>();
        for (Container aChildren : children) {
            Wrapper wrapper = (Wrapper) aChildren;
            int loadOnStartup = wrapper.getLoadOnStartup();
            if (loadOnStartup < 0) {
                continue;
            }
            Integer key = Integer.valueOf(loadOnStartup);
            ArrayList<Wrapper> list = map.get(key);
            if (list == null) {
                list = new ArrayList<Wrapper>();
                map.put(key, list);
            }
            list.add(wrapper);
        }

        // Load the collected "load on startup" servlets
        for (ArrayList<Wrapper> list : map.values()) {
            for (Wrapper wrapper : list) {
                try {
                    wrapper.load();
                } catch (ServletException e) {
                    Throwable rootCause = StandardWrapper.getRootCause(e);
                    getLogger().error(sm.getString("standardWrapper.loadException",
                            getName()), rootCause);
                    // NOTE: load errors (including a servlet that throws
                    // UnavailableException from tht init() method) are NOT
                    // fatal to application startup

                    // Except for configured exceptions
                    Object o = this.getServletContext().getAttribute("jonas.tomcat.load.on.startup.preserve.exceptions");
                    if (o != null && o instanceof Collection && !((Collection)o).isEmpty() &&
                            ((Collection)o).iterator().next() instanceof Class<?>) {
                        Collection<Class<?>> exceptions = (Collection<Class<?>>) o;
                        for (Class<?> exception : exceptions) {
                            if (exception.isAssignableFrom(rootCause.getClass())) {
                                throw new RuntimeException(rootCause);
                            }
                        }
                    }
                }
            }
        }
    }
}
