//
//  ========================================================================
//  Copyright (c) 1995-2012 Sabre Holdings.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.ant;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.CodeSource;
import java.security.PermissionCollection;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.EventListener;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.jar.Manifest;

import javax.servlet.Servlet;

import org.apache.tools.ant.AntClassLoader;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.types.FileSet;
import org.eclipse.jetty.annotations.AnnotationConfiguration;
import org.eclipse.jetty.ant.types.Attribute;
import org.eclipse.jetty.ant.types.Attributes;
import org.eclipse.jetty.ant.types.FileMatchingConfiguration;
import org.eclipse.jetty.ant.utils.TaskLog;
import org.eclipse.jetty.plus.webapp.EnvConfiguration;
import org.eclipse.jetty.plus.webapp.PlusConfiguration;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.FilterMapping;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.servlet.ServletMapping;
import org.eclipse.jetty.servlet.Source;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.webapp.Configuration;
import org.eclipse.jetty.webapp.FragmentConfiguration;
import org.eclipse.jetty.webapp.JettyWebXmlConfiguration;
import org.eclipse.jetty.webapp.MetaInfConfiguration;
import org.eclipse.jetty.webapp.WebAppClassLoader;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.webapp.WebInfConfiguration;
import org.eclipse.jetty.webapp.WebXmlConfiguration;
import org.eclipse.jetty.xml.XmlConfiguration;

/**
 * Extension of WebAppContext to allow configuration via Ant environment.
 */
public class AntWebAppContext extends WebAppContext
{
    private static final Logger LOG = Log.getLogger(WebAppContext.class);
    
    public final AntWebInfConfiguration antWebInfConfiguration = new AntWebInfConfiguration();
    public final WebXmlConfiguration webXmlConfiguration = new WebXmlConfiguration();
    public final MetaInfConfiguration metaInfConfiguration = new MetaInfConfiguration();
    public final FragmentConfiguration fragmentConfiguration = new FragmentConfiguration();
    public final EnvConfiguration envConfiguration = new EnvConfiguration();
    public final PlusConfiguration plusConfiguration = new PlusConfiguration();
    public final AnnotationConfiguration annotationConfiguration = new AnnotationConfiguration();
    public final JettyWebXmlConfiguration jettyWebXmlConfiguration = new JettyWebXmlConfiguration();


    public final Configuration[] DEFAULT_CONFIGURATIONS = 
        { 
         antWebInfConfiguration,
         webXmlConfiguration,
         metaInfConfiguration,
         fragmentConfiguration,
         envConfiguration,
         plusConfiguration,
         annotationConfiguration,
         jettyWebXmlConfiguration
        };
    

    public final static String DEFAULT_CONTAINER_INCLUDE_JAR_PATTERN =
    ".*/.*jsp-api-[^/]*\\.jar$|.*/.*jsp-[^/]*\\.jar$|.*/.*taglibs[^/]*\\.jar$|.*/.*jstl[^/]*\\.jar$|.*/.*jsf-impl-[^/]*\\.jar$|.*/.*javax.faces-[^/]*\\.jar$|.*/.*myfaces-impl-[^/]*\\.jar$";


    /** Location of jetty-env.xml file. */
    private File jettyEnvXml;
    
    /** List of web application libraries. */
    private List libraries = new ArrayList();

    /** List of web application class directories. */
    private List classes = new ArrayList();
    
    /** context xml file to apply to the webapp */
    private File contextXml;
    
    /** List of extra scan targets for this web application. */
    private FileSet scanTargets;
    
    /** context attributes to set **/
    private Attributes attributes;
    
    private Project project;
    
    private List<File> scanFiles;
    


    /** Extra scan targets. */
    private FileMatchingConfiguration extraScanTargetsConfiguration;


    private FileMatchingConfiguration librariesConfiguration;
    

    public static void dump(ClassLoader loader)
    {
        while (loader != null)
        {
            System.err.println(loader);
            if (loader instanceof URLClassLoader)
            {
                URL[] urls = ((URLClassLoader)loader).getURLs();
                if (urls != null)
                {
                    for (URL u:urls)
                        System.err.println("\t"+u+"\n");
                }
            }
            loader = loader.getParent();
        }
    }

    
    /**
     * AntURLClassLoader
     *
     * Adapt the AntClassLoader which is not a URLClassLoader - this is needed for
     * jsp to be able to search the classpath.
     */
    public static class AntURLClassLoader extends URLClassLoader
    {
        private AntClassLoader antLoader;
        
        public AntURLClassLoader(AntClassLoader antLoader)
        {
            super(new URL[] {}, antLoader);
            this.antLoader = antLoader;      
        }

        @Override
        public InputStream getResourceAsStream(String name)
        {
            return super.getResourceAsStream(name);
        }

        @Override
        public void close() throws IOException
        {
            super.close();
        }

        @Override
        protected void addURL(URL url)
        {
            super.addURL(url);
        }

        @Override
        public URL[] getURLs()
        {
            Set<URL> urls = new HashSet<URL>();
            
            //convert urls from antLoader
            String[] paths = antLoader.getClasspath().split(new String(new char[]{File.pathSeparatorChar}));
            if (paths != null)
            {
                for (String p:paths)
                {
                    File f = new File(p);
                    try
                    {
                        urls.add(f.toURI().toURL());   
                    }
                    catch (Exception e)
                    {
                        LOG.ignore(e);
                    }
                }
            }
            
            //add in any that may have been added to us as a URL directly
            URL[] ourURLS = super.getURLs();
            if (ourURLS != null)
            {
                for (URL u:ourURLS)
                    urls.add(u);
            }
            
            return urls.toArray(new URL[urls.size()]);
        }

        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException
        {
            return super.findClass(name);
        }

        @Override
        protected Package definePackage(String name, Manifest man, URL url) throws IllegalArgumentException
        {
            return super.definePackage(name, man, url);
        }

        @Override
        public URL findResource(String name)
        {
            return super.findResource(name);
        }

        @Override
        public Enumeration<URL> findResources(String name) throws IOException
        {
            return super.findResources(name);
        }

        @Override
        protected PermissionCollection getPermissions(CodeSource codesource)
        {
            return super.getPermissions(codesource);
        }

        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException
        {
            return super.loadClass(name);
        }

        @Override
        protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
        {
            return super.loadClass(name, resolve);
        }

        @Override
        protected Object getClassLoadingLock(String className)
        {
            return super.getClassLoadingLock(className);
        }

        @Override
        public URL getResource(String name)
        {
            return super.getResource(name);
        }

        @Override
        public Enumeration<URL> getResources(String name) throws IOException
        {
            return super.getResources(name);
        }

        @Override
        protected Package definePackage(String name, String specTitle, String specVersion, String specVendor, String implTitle, String implVersion,
                                        String implVendor, URL sealBase) throws IllegalArgumentException
        {
            return super.definePackage(name, specTitle, specVersion, specVendor, implTitle, implVersion, implVendor, sealBase);
        }

        @Override
        protected Package getPackage(String name)
        {
            return super.getPackage(name);
        }

        @Override
        protected Package[] getPackages()
        {
            return super.getPackages();
        }

        @Override
        protected String findLibrary(String libname)
        {
            return super.findLibrary(libname);
        }

        @Override
        public void setDefaultAssertionStatus(boolean enabled)
        {
            super.setDefaultAssertionStatus(enabled);
        }

        @Override
        public void setPackageAssertionStatus(String packageName, boolean enabled)
        {
            super.setPackageAssertionStatus(packageName, enabled);
        }

        @Override
        public void setClassAssertionStatus(String className, boolean enabled)
        {
            super.setClassAssertionStatus(className, enabled);
        }

        @Override
        public void clearAssertionStatus()
        {
            super.clearAssertionStatus();
        }
    }
    
    
    /**
     * AntServletHolder
     *
     *
     */
    public static class AntServletHolder extends ServletHolder
    {

        public AntServletHolder()
        {
            super();
        }


        public AntServletHolder(Class<? extends Servlet> servlet)
        {
            super(servlet);
        }


        public AntServletHolder(Servlet servlet)
        {
            super(servlet);
        }


        public AntServletHolder(String name, Class<? extends Servlet> servlet)
        {
            super(name, servlet);
        }


        public AntServletHolder(String name, Servlet servlet)
        {
            super(name, servlet);
        }

        protected String getSystemClassPath (ClassLoader loader) throws Exception
        {
            StringBuilder classpath=new StringBuilder();
            while (loader != null)
            {
                if (loader instanceof URLClassLoader)
                {
                    URL[] urls = ((URLClassLoader)loader).getURLs();
                    if (urls != null)
                    {
                        for (int i=0;i<urls.length;i++)
                        {
                            Resource resource = Resource.newResource(urls[i]);
                            File file=resource.getFile();
                            if (file!=null && file.exists())
                            {
                                if (classpath.length()>0)
                                    classpath.append(File.pathSeparatorChar);
                                classpath.append(file.getAbsolutePath());
                            }
                        }
                    }
                }
                else if (loader instanceof AntClassLoader)
                {
                    classpath.append(((AntClassLoader)loader).getClasspath());
                }

                loader = loader.getParent();
            }

            return classpath.toString();
        }

    }

    
    
    /**
     * AntServletHandler
     *
     *
     */
    public static class AntServletHandler extends ServletHandler
    {

        @Override
        public ServletHolder newServletHolder(Source source)
        {
            return new AntServletHolder();
        }

    }



    /**
     * Default constructor. Takes project as an argument
     *
     * @param project the project.
     * @throws Exception if unable to create webapp context
     */
    public AntWebAppContext(Project project) throws Exception
    {
        super();
        this.project = project;
        setConfigurations(DEFAULT_CONFIGURATIONS);
        setAttribute(WebInfConfiguration.CONTAINER_JAR_PATTERN, DEFAULT_CONTAINER_INCLUDE_JAR_PATTERN);
        setParentLoaderPriority(true);
    }
    

    /**
     * Adds a new Ant's attributes tag object if it have not been created yet.
     * @param atts the attributes
     */
    public void addAttributes(Attributes atts)
    {
        if (this.attributes != null)
        {
            throw new BuildException("Only one <attributes> tag is allowed!");
        }

        this.attributes = atts;
    }

    
    public void addLib(FileSet lib)
    {
        libraries.add(lib);
    }


    public void addClasses(FileSet classes)
    {
        this.classes.add(classes);
    }
    
    

    @Override
    protected ServletHandler newServletHandler()
    {
        return new AntServletHandler();
    }


    public void setJettyEnvXml(File jettyEnvXml)
    {
        this.jettyEnvXml = jettyEnvXml;
        TaskLog.log("jetty-env.xml file: = " + (jettyEnvXml == null ? null : jettyEnvXml.getAbsolutePath()));
    }

    public File getJettyEnvXml ()
    {
        return this.jettyEnvXml;
    }

    


    public List getLibraries()
    {
        return librariesConfiguration.getBaseDirectories();
    }

 
    public void addScanTargets(FileSet scanTargets)
    {
        if (this.scanTargets != null)
        {
            throw new BuildException("Only one <scanTargets> tag is allowed!");
        }

        this.scanTargets = scanTargets;
    }
    
    public List getScanTargetFiles () 
    {
        if (this.scanTargets == null)
            return null;
        
      
        FileMatchingConfiguration configuration = new FileMatchingConfiguration();
        configuration.addDirectoryScanner(scanTargets.getDirectoryScanner(project));
        return configuration.getBaseDirectories();
    }
    
    public List<File> getScanFiles()
    {
        if (scanFiles == null)
            scanFiles = initScanFiles();
        return scanFiles;
    }
    
    
    public boolean isScanned (File file)
    {
       List<File> files = getScanFiles();
       if (files == null || files.isEmpty())
           return false;
       return files.contains(file);
    }
    
    
    public List<File> initScanFiles ()
    {
        List<File> scanList = new ArrayList<File>();
        
        if (getDescriptor() != null)
        {
            try (Resource r = Resource.newResource(getDescriptor());)
            {
                scanList.add(r.getFile());
            }
            catch (IOException e)
            {
                throw new BuildException(e);
            }
        }

        if (getJettyEnvXml() != null)
        {
            try (Resource r = Resource.newResource(getJettyEnvXml());)
            {
                scanList.add(r.getFile());
            }
            catch (IOException e)
            {
                throw new BuildException("Problem configuring scanner for jetty-env.xml", e);
            }
        }

        if (getDefaultsDescriptor() != null)
        {
            try (Resource r = Resource.newResource(getDefaultsDescriptor());)
            {
                if (!WebAppContext.WEB_DEFAULTS_XML.equals(getDefaultsDescriptor()))
                {   
                    scanList.add(r.getFile());
                }
            }
            catch (IOException e)
            {
                throw new BuildException("Problem configuring scanner for webdefaults.xml", e);
            }
        }

        if (getOverrideDescriptor() != null)
        {
            try
            {
                Resource r = Resource.newResource(getOverrideDescriptor());
                scanList.add(r.getFile());
            }
            catch (IOException e)
            {
                throw new BuildException("Problem configuring scanner for webdefaults.xml", e);
            }
        }

        //add any extra classpath and libs 
        List<File> cpFiles = getClassPathFiles();
        if (cpFiles != null)
            scanList.addAll(cpFiles);
        
        //any extra scan targets
        @SuppressWarnings("unchecked")
        List<File> scanFiles = (List<File>)getScanTargetFiles();
        if (scanFiles != null)
            scanList.addAll(scanFiles);
        
        return scanList;
    }
    
    
    
    @Override
    public void setWar(String path)
    {
        super.setWar(path);

        try
        {
            Resource war = Resource.newResource(path);
            if (war.exists() && war.isDirectory() && getDescriptor() == null)
            {
                Resource webXml = war.addPath("WEB-INF/web.xml");
                setDescriptor(webXml.toString());
            }
        }
        catch (IOException e)
        {
            throw new BuildException(e);
        }
    }


    /**
     * 
     */
    public void doStart()
    {
        try
        {
            TaskLog.logWithTimestamp("Starting web application "+this.getDescriptor());
            if (jettyEnvXml != null && jettyEnvXml.exists())
                envConfiguration.setJettyEnvXml(Resource.toURL(jettyEnvXml));
            
            ClassLoader parentLoader = this.getClass().getClassLoader();
            if (parentLoader instanceof AntClassLoader)
                parentLoader = new AntURLClassLoader((AntClassLoader)parentLoader);

            setClassLoader(new WebAppClassLoader(parentLoader, this));
            if (attributes != null && attributes.getAttributes() != null)
            {
                for (Attribute a:attributes.getAttributes())
                    setAttribute(a.getName(), a.getValue());
            }
            
            //apply a context xml file if one was supplied
            if (contextXml != null)
            {
                XmlConfiguration xmlConfiguration = new XmlConfiguration(Resource.toURL(contextXml));
                TaskLog.log("Applying context xml file "+contextXml);
                xmlConfiguration.configure(this);   
            }
            
            super.doStart();
        }
        catch (Exception e)
        {
            TaskLog.log(e.toString());
        }
    }

    public void doStop()
    {
        try
        {
            scanFiles = null;
            TaskLog.logWithTimestamp("Stopping web application "+this);
            Thread.currentThread().sleep(500L);
            super.doStop();
            //remove all filters, servlets and listeners. They will be recreated
            //either via application of a context xml file or web.xml or annotation or servlet api
            setEventListeners(new EventListener[0]);
            getServletHandler().setFilters(new FilterHolder[0]);
            getServletHandler().setFilterMappings(new FilterMapping[0]);
            getServletHandler().setServlets(new ServletHolder[0]);
            getServletHandler().setServletMappings(new ServletMapping[0]);
        }
        catch (InterruptedException e)
        {
            TaskLog.log(e.toString());
        }
        catch (Exception e)
        {
            TaskLog.log(e.toString());
        }
    }


    
    /**
     * @return a list of classpath files (libraries and class directories).
     */
    public List<File> getClassPathFiles()
    {
        List<File> classPathFiles = new ArrayList<File>();
        Iterator classesIterator = classes.iterator();
        while (classesIterator.hasNext())
        {
            FileSet clazz = (FileSet) classesIterator.next();
            classPathFiles.add(clazz.getDirectoryScanner(project).getBasedir());
        }

        Iterator iterator = libraries.iterator();
        while (iterator.hasNext())
        {
            FileSet library = (FileSet) iterator.next();
            String[] includedFiles = library.getDirectoryScanner(project).getIncludedFiles();
            File baseDir = library.getDirectoryScanner(project).getBasedir();

            for (int i = 0; i < includedFiles.length; i++)
            {
                classPathFiles.add(new File(baseDir, includedFiles[i]));
            }
        }


        return classPathFiles;
    }

    
    /**
     * @return a <code>FileMatchingConfiguration</code> object describing the
     *         configuration of all libraries added to this particular web app
     *         (both classes and libraries).
     */
    public FileMatchingConfiguration getLibrariesConfiguration()
    {
        FileMatchingConfiguration config = new FileMatchingConfiguration();

        Iterator classesIterator = classes.iterator();
        while (classesIterator.hasNext())
        {
            FileSet clazz = (FileSet) classesIterator.next();
            config.addDirectoryScanner(clazz.getDirectoryScanner(project));
        }

        Iterator librariesIterator = libraries.iterator();
        while (librariesIterator.hasNext())
        {
            FileSet library = (FileSet) librariesIterator.next();
            config.addDirectoryScanner(library.getDirectoryScanner(project));
        }

        return config;
    }


    public File getContextXml()
    {
        return contextXml;
    }


    public void setContextXml(File contextXml)
    {
        this.contextXml = contextXml;
    }
    
}
