//
//  ========================================================================
//  Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//  ------------------------------------------------------------------------
//  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.osgi.boot;

import java.io.File;
import java.net.URL;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;

import org.eclipse.jetty.deploy.App;
import org.eclipse.jetty.deploy.AppProvider;
import org.eclipse.jetty.deploy.DeploymentManager;
import org.eclipse.jetty.osgi.boot.internal.serverfactory.ServerInstanceWrapper;
import org.eclipse.jetty.osgi.boot.internal.webapp.OSGiWebappClassLoader;
import org.eclipse.jetty.osgi.boot.utils.BundleFileLocatorHelperFactory;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.Loader;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.resource.JarResource;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.webapp.Configuration;
import org.eclipse.jetty.webapp.WebAppClassLoader;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.xml.XmlConfiguration;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.service.packageadmin.PackageAdmin;

/**
 * AbstractWebAppProvider
 * <p>
 * Base class for Jetty DeploymentManager Providers that are capable of deploying a webapp,
 * either from a bundle or an OSGi service.
 */
public abstract class AbstractWebAppProvider extends AbstractLifeCycle implements AppProvider
{
    private static final Logger LOG = Log.getLogger(AbstractWebAppProvider.class);

    /**
     * Check if we should be enabling annotation processing
     *
     * @return true if the jetty-annotations.jar is present, false otherwise
     */
    private static boolean annotationsAvailable()
    {
        boolean result = false;
        try
        {
            Loader.loadClass(AbstractWebAppProvider.class, "org.eclipse.jetty.annotations.AnnotationConfiguration");
            result = true;
            LOG.debug("Annotation support detected");
        }
        catch (ClassNotFoundException e)
        {
            result = false;
            LOG.debug("No annotation support detected");
        }

        return result;
    }

    /**
     * Check if jndi is support is present.
     *
     * @return true if the jetty-jndi.jar is present, false otherwise
     */
    private static boolean jndiAvailable()
    {
        try
        {
            Loader.loadClass(AbstractWebAppProvider.class, "org.eclipse.jetty.plus.jndi.Resource");
            Loader.loadClass(AbstractWebAppProvider.class, "org.eclipse.jetty.plus.webapp.EnvConfiguration");
            LOG.debug("JNDI support detected");
            return true;
        }
        catch (ClassNotFoundException e)
        {
            LOG.debug("No JNDI support detected");
            return false;
        }
    }

    private boolean _parentLoaderPriority;

    private String _defaultsDescriptor;

    private boolean _extractWars = true; //See WebAppContext.extractWars

    private String _tldBundles;

    private DeploymentManager _deploymentManager;

    private String[] _configurationClasses;

    private ServerInstanceWrapper _serverWrapper;

    /**
     * OSGiApp
     *
     * Represents a deployable webapp.
     */
    public class OSGiApp extends AbstractOSGiApp
    {
        private String _contextPath;
        private String _webAppPath;
        private WebAppContext _webApp;

        public OSGiApp(DeploymentManager manager, AppProvider provider, Bundle bundle, String originId)
        {
            super(manager, provider, bundle, originId);
        }

        public OSGiApp(DeploymentManager manager, AppProvider provider, Bundle bundle, Dictionary properties, String originId)
        {
            super(manager, provider, bundle, properties, originId);
        }

        public void setWebAppContext(WebAppContext webApp)
        {
            _webApp = webApp;
        }

        @Override
        public String getContextPath()
        {
            return _contextPath;
        }

        public void setContextPath(String contextPath)
        {
            this._contextPath = contextPath;
        }

        public String getBundlePath()
        {
            return _webAppPath;
        }

        public void setWebAppPath(String path)
        {
            this._webAppPath = path;
        }

        public ContextHandler createContextHandler()
            throws Exception
        {
            if (_webApp != null)
            {
                configureWebApp();
                return _webApp;
            }

            createWebApp();
            return _webApp;
        }

        protected void createWebApp()
            throws Exception
        {
            _webApp = newWebApp();
            configureWebApp();
        }

        protected WebAppContext newWebApp()
        {
            WebAppContext webApp = new WebAppContext();
            webApp.setAttribute(OSGiWebappConstants.WATERMARK, OSGiWebappConstants.WATERMARK);

            //make sure we protect also the osgi dirs specified by OSGi Enterprise spec
            String[] targets = webApp.getProtectedTargets();
            String[] updatedTargets = null;
            if (targets != null)
            {
                updatedTargets = new String[targets.length + OSGiWebappConstants.DEFAULT_PROTECTED_OSGI_TARGETS.length];
                System.arraycopy(targets, 0, updatedTargets, 0, targets.length);
            }
            else
                updatedTargets = new String[OSGiWebappConstants.DEFAULT_PROTECTED_OSGI_TARGETS.length];
            System.arraycopy(OSGiWebappConstants.DEFAULT_PROTECTED_OSGI_TARGETS, 0, updatedTargets, targets.length, OSGiWebappConstants.DEFAULT_PROTECTED_OSGI_TARGETS.length);
            webApp.setProtectedTargets(updatedTargets);

            return webApp;
        }

        public void configureWebApp()
            throws Exception
        {
            //TODO turn this around and let any context.xml file get applied first, and have the properties override
            _webApp.setContextPath(_contextPath);

            //osgi Enterprise Spec r4 p.427
            _webApp.setAttribute(OSGiWebappConstants.OSGI_BUNDLECONTEXT, _bundle.getBundleContext());

            String overrideBundleInstallLocation = (String)_properties.get(OSGiWebappConstants.JETTY_BUNDLE_INSTALL_LOCATION_OVERRIDE);
            File bundleInstallLocation =
                (overrideBundleInstallLocation == null
                    ? BundleFileLocatorHelperFactory.getFactory().getHelper().getBundleInstallLocation(_bundle)
                    : new File(overrideBundleInstallLocation));

            if (LOG.isDebugEnabled())
            {
                LOG.debug("Bundle location is {}, install location: {}", _bundle.getLocation(), bundleInstallLocation);
            }

            URL url = null;
            Resource rootResource = Resource.newResource(BundleFileLocatorHelperFactory.getFactory().getHelper().getLocalURL(bundleInstallLocation.toURI().toURL()));
            //try and make sure the rootResource is useable - if its a jar then make it a jar file url
            if (rootResource.exists() && !rootResource.isDirectory() && !rootResource.toString().startsWith("jar:"))
            {
                Resource jarResource = JarResource.newJarResource(rootResource);
                if (jarResource.exists() && jarResource.isDirectory())
                    rootResource = jarResource;
            }

            //if the path wasn't set or it was ., then it is the root of the bundle's installed location
            if (_webAppPath == null || _webAppPath.length() == 0 || ".".equals(_webAppPath))
            {
                url = bundleInstallLocation.toURI().toURL();
                if (LOG.isDebugEnabled())
                    LOG.debug("Webapp base using bundle install location: {}", url);
            }
            else
            {
                //Get the location of the root of the webapp inside the installed bundle
                if (_webAppPath.startsWith("/") || _webAppPath.startsWith("file:"))
                {
                    url = new File(_webAppPath).toURI().toURL();
                    if (LOG.isDebugEnabled())
                        LOG.debug("Webapp base using absolute location: {}", url);
                }
                else if (bundleInstallLocation != null && bundleInstallLocation.isDirectory())
                {
                    url = new File(bundleInstallLocation, _webAppPath).toURI().toURL();
                    if (LOG.isDebugEnabled())
                        LOG.debug("Webapp base using path relative to bundle unpacked install location: {}", url);
                }
                else if (bundleInstallLocation != null)
                {
                    Enumeration<URL> urls = BundleFileLocatorHelperFactory.getFactory().getHelper().findEntries(_bundle, _webAppPath);
                    if (urls != null && urls.hasMoreElements())
                    {
                        url = urls.nextElement();
                        if (LOG.isDebugEnabled())
                            LOG.debug("Webapp base using path relative to packed bundle location: {}", url);
                    }
                }
            }

            if (url == null)
            {
                throw new IllegalArgumentException(String.format("Unable to locate %s in %s",
                    _webAppPath,
                    (bundleInstallLocation != null ? bundleInstallLocation.getAbsolutePath() : "missing bundle '" + _bundle.getSymbolicName() + "'")));
            }

            //Sets the location of the war file
            // converts bundleentry: protocol if necessary
            _webApp.setWar(BundleFileLocatorHelperFactory.getFactory().getHelper().getLocalURL(url).toString());

            // Set up what has been configured on the provider
            _webApp.setParentLoaderPriority(isParentLoaderPriority());
            _webApp.setExtractWAR(isExtract());
            _webApp.setConfigurationClasses(getConfigurationClasses());

            if (getDefaultsDescriptor() != null)
                _webApp.setDefaultsDescriptor(getDefaultsDescriptor());

            //Set up configuration from manifest headers
            //extra classpath
            String tmp = (String)_properties.get(OSGiWebappConstants.JETTY_EXTRA_CLASSPATH);
            if (tmp != null)
                _webApp.setExtraClasspath(tmp);

            //web.xml
            tmp = (String)_properties.get(OSGiWebappConstants.JETTY_WEB_XML_PATH);
            if (tmp != null && tmp.trim().length() != 0)
            {
                File webXml = getFile(tmp, bundleInstallLocation);
                if (webXml != null && webXml.exists())
                    _webApp.setDescriptor(webXml.getAbsolutePath());
            }

            //webdefault.xml
            tmp = (String)_properties.get(OSGiWebappConstants.JETTY_DEFAULT_WEB_XML_PATH);
            if (tmp != null)
            {
                File defaultWebXml = getFile(tmp, bundleInstallLocation);
                if (defaultWebXml != null)
                {
                    if (defaultWebXml.exists())
                        _webApp.setDefaultsDescriptor(defaultWebXml.getAbsolutePath());
                    else
                        LOG.warn(defaultWebXml.getAbsolutePath() + " does not exist");
                }
            }

            //Handle Require-TldBundle
            //This is a comma separated list of names of bundles that contain tlds that this webapp uses.
            //We add them to the webapp classloader.
            String requireTldBundles = (String)_properties.get(OSGiWebappConstants.REQUIRE_TLD_BUNDLE);
            String pathsToTldBundles = getPathsToRequiredBundles(requireTldBundles);

            // make sure we provide access to all the jetty bundles by going
            // through this bundle.
            OSGiWebappClassLoader webAppLoader = new OSGiWebappClassLoader(_serverWrapper.getParentClassLoaderForWebapps(), _webApp, _bundle);

            if (pathsToTldBundles != null)
                webAppLoader.addClassPath(pathsToTldBundles);
            _webApp.setClassLoader(webAppLoader);

            // apply any META-INF/context.xml file that is found to configure
            // the webapp first
            applyMetaInfContextXml(rootResource, overrideBundleInstallLocation);

            _webApp.setAttribute(OSGiWebappConstants.REQUIRE_TLD_BUNDLE, requireTldBundles);

            //Set up some attributes
            // rfc66
            _webApp.setAttribute(OSGiWebappConstants.RFC66_OSGI_BUNDLE_CONTEXT, _bundle.getBundleContext());

            // spring-dm-1.2.1 looks for the BundleContext as a different attribute.
            // not a spec... but if we want to support
            // org.springframework.osgi.web.context.support.OsgiBundleXmlWebApplicationContext
            // then we need to do this to:
            _webApp.setAttribute("org.springframework.osgi.web." + BundleContext.class.getName(), _bundle.getBundleContext());

            // also pass the bundle directly. sometimes a bundle does not have a
            // bundlecontext.
            // it is still useful to have access to the Bundle from the servlet
            // context.
            _webApp.setAttribute(OSGiWebappConstants.JETTY_OSGI_BUNDLE, _bundle);
        }

        protected String getPathsToRequiredBundles(String requireTldBundles)
            throws Exception
        {
            if (requireTldBundles == null)
                return null;

            ServiceReference ref = _bundle.getBundleContext().getServiceReference(org.osgi.service.packageadmin.PackageAdmin.class.getName());
            PackageAdmin packageAdmin = (ref == null) ? null : (PackageAdmin)_bundle.getBundleContext().getService(ref);
            if (packageAdmin == null)
                throw new IllegalStateException("Unable to get PackageAdmin reference to locate required Tld bundles");

            StringBuilder paths = new StringBuilder();
            String[] symbNames = requireTldBundles.split("[, ]");

            for (String symbName : symbNames)
            {
                Bundle[] bs = packageAdmin.getBundles(symbName, null);
                if (bs == null || bs.length == 0)
                {
                    throw new IllegalArgumentException("Unable to locate the bundle '" + symbName +
                        "' specified by " + OSGiWebappConstants.REQUIRE_TLD_BUNDLE + " in manifest of " +
                        (_bundle == null ? "unknown" : _bundle.getSymbolicName()));
                }

                File f = BundleFileLocatorHelperFactory.getFactory().getHelper().getBundleInstallLocation(bs[0]);
                if (paths.length() > 0)
                    paths.append(", ");
                paths.append(f.toURI().toURL().toString());
                LOG.debug("getPathsToRequiredBundles: bundle path=" + bs[0].getLocation() + " uri=" + f.toURI());
            }

            return paths.toString();
        }

        protected void applyMetaInfContextXml(Resource rootResource, String overrideBundleInstallLocation)
            throws Exception
        {
            if (_bundle == null)
                return;
            if (_webApp == null)
                return;

            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            LOG.debug("Context classloader = " + cl);
            try
            {

                Thread.currentThread().setContextClassLoader(_webApp.getClassLoader());

                //TODO replace this with getting the InputStream so we don't cache in URL
                //Try looking for a context xml file in META-INF with a specific name
                URL contextXmlUrl = _bundle.getEntry("/META-INF/jetty-webapp-context.xml");

                if (contextXmlUrl == null)
                {
                    //Didn't find specially named file, try looking for a property that names a context xml file to use
                    if (_properties != null)
                    {
                        String tmp = (String)_properties.get(OSGiWebappConstants.JETTY_CONTEXT_FILE_PATH);
                        if (tmp != null)
                        {
                            String[] filenames = tmp.split("[,;]");
                            if (filenames != null && filenames.length > 0)
                            {
                                String filename = filenames[0]; //should only be 1 filename in this usage
                                String jettyHome = (String)getServerInstanceWrapper().getServer().getAttribute(OSGiServerConstants.JETTY_HOME);
                                if (jettyHome == null)
                                    jettyHome = System.getProperty(OSGiServerConstants.JETTY_HOME);
                                Resource res = findFile(filename, jettyHome, overrideBundleInstallLocation, _bundle);
                                if (res != null)
                                    contextXmlUrl = res.getURL();
                            }
                        }
                    }
                }
                if (contextXmlUrl == null)
                    return;

                // Apply it just as the standard jetty ContextProvider would do
                LOG.info("Applying " + contextXmlUrl + " to " + _webApp);

                XmlConfiguration xmlConfiguration = new XmlConfiguration(contextXmlUrl);
                WebAppClassLoader.runWithServerClassAccess(() ->
                {
                    HashMap<String, String> properties = new HashMap<>();
                    xmlConfiguration.getIdMap().put("Server", getDeploymentManager().getServer());
                    properties.put(OSGiWebappConstants.JETTY_BUNDLE_ROOT, rootResource.toString());
                    properties.put(OSGiServerConstants.JETTY_HOME, (String)getDeploymentManager().getServer().getAttribute(OSGiServerConstants.JETTY_HOME));
                    xmlConfiguration.getProperties().putAll(properties);
                    xmlConfiguration.configure(_webApp);
                    return null;
                });
            }
            finally
            {
                Thread.currentThread().setContextClassLoader(cl);
            }
        }

        private File getFile(String file, File bundleInstall)
        {
            if (file == null)
                return null;

            if (file.startsWith("/") || file.startsWith("file:/")) //absolute location
                return new File(file);
            else
            {
                //relative location
                //try inside the bundle first
                File f = new File(bundleInstall, file);
                if (f.exists())
                    return f;
                String jettyHome = (String)getDeploymentManager().getServer().getAttribute(OSGiServerConstants.JETTY_HOME);
                if (jettyHome != null)
                    return new File(jettyHome, file);
            }

            return null;
        }
    }

    public AbstractWebAppProvider(ServerInstanceWrapper wrapper)
    {
        _serverWrapper = wrapper;
    }

    /**
     * Get the parentLoaderPriority.
     *
     * @return the parentLoaderPriority
     */
    public boolean isParentLoaderPriority()
    {
        return _parentLoaderPriority;
    }

    /**
     * Set the parentLoaderPriority.
     *
     * @param parentLoaderPriority the parentLoaderPriority to set
     */
    public void setParentLoaderPriority(boolean parentLoaderPriority)
    {
        _parentLoaderPriority = parentLoaderPriority;
    }

    /**
     * Get the defaultsDescriptor.
     *
     * @return the defaultsDescriptor
     */
    public String getDefaultsDescriptor()
    {
        return _defaultsDescriptor;
    }

    /**
     * Set the defaultsDescriptor.
     *
     * @param defaultsDescriptor the defaultsDescriptor to set
     */
    public void setDefaultsDescriptor(String defaultsDescriptor)
    {
        _defaultsDescriptor = defaultsDescriptor;
    }

    public boolean isExtract()
    {
        return _extractWars;
    }

    public void setExtract(boolean extract)
    {
        _extractWars = extract;
    }

    /**
     * @param tldBundles Comma separated list of bundles that contain tld jars
     * that should be setup on the jetty instances created here.
     */
    public void setTldBundles(String tldBundles)
    {
        _tldBundles = tldBundles;
    }

    /**
     * @return The list of bundles that contain tld jars that should be setup on
     * the jetty instances created here.
     */
    public String getTldBundles()
    {
        return _tldBundles;
    }

    /**
     * @param configurations The configuration class names.
     */
    public void setConfigurationClasses(String[] configurations)
    {
        _configurationClasses = configurations == null ? null : configurations.clone();
    }

    public String[] getConfigurationClasses()
    {
        if (_configurationClasses != null)
            return _configurationClasses;

        Configuration.ClassList defaults = Configuration.ClassList.serverDefault(_serverWrapper.getServer());

        //add before JettyWebXmlConfiguration
        if (annotationsAvailable() && !defaults.contains("org.eclipse.jetty.osgi.annotations.AnnotationConfiguration"))
            defaults.addBefore("org.eclipse.jetty.webapp.JettyWebXmlConfiguration",
                "org.eclipse.jetty.osgi.annotations.AnnotationConfiguration");

        //add in EnvConfiguration and PlusConfiguration just after FragmentConfiguration
        if (jndiAvailable())
        {
            if (!defaults.contains("org.eclipse.jetty.plus.webapp.EnvConfiguration"))
                defaults.addAfter("org.eclipse.jetty.webapp.FragmentConfiguration",
                    "org.eclipse.jetty.plus.webapp.EnvConfiguration");
            if (!defaults.contains("org.eclipse.jetty.plus.webapp.PlusConfiguration"))
                defaults.addAfter("org.eclipse.jetty.plus.webapp.EnvConfiguration", "org.eclipse.jetty.plus.webapp.PlusConfiguration");
        }

        String[] asArray = new String[defaults.size()];
        return defaults.toArray(asArray);
    }

    public void setServerInstanceWrapper(ServerInstanceWrapper wrapper)
    {
        _serverWrapper = wrapper;
    }

    public ServerInstanceWrapper getServerInstanceWrapper()
    {
        return _serverWrapper;
    }

    public DeploymentManager getDeploymentManager()
    {
        return _deploymentManager;
    }

    /**
     * @see org.eclipse.jetty.deploy.AppProvider#setDeploymentManager(org.eclipse.jetty.deploy.DeploymentManager)
     */
    @Override
    public void setDeploymentManager(DeploymentManager deploymentManager)
    {
        _deploymentManager = deploymentManager;
    }

    @Override
    public ContextHandler createContextHandler(App app) throws Exception
    {
        if (app == null)
            return null;
        if (!(app instanceof OSGiApp))
            throw new IllegalStateException(app + " is not a BundleApp");

        //Create a WebAppContext suitable to deploy in OSGi
        ContextHandler ch = ((OSGiApp)app).createContextHandler();
        return ch;
    }

    public static String getOriginId(Bundle contributor, String path)
    {
        return contributor.getSymbolicName() + "-" + contributor.getVersion().toString() + (path.startsWith("/") ? path : "/" + path);
    }
}
