/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.openejb.loader;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.logging.Logger;

/*-------------------------------------------------------*/
/* Tomcat ClassLoader Support */
/*-------------------------------------------------------*/

@SuppressWarnings("unchecked")
public class TomcatClassPath extends BasicURLClassPath {

    private final ClassLoader commonLoader;
    private final ClassLoader serverLoader;

    private Method addRepositoryMethod;
    private Method addURLMethod;

    public TomcatClassPath() {
        this(getCommonLoader(getContextClassLoader()));
    }

    public TomcatClassPath(final ClassLoader classLoader) {
        this.commonLoader = classLoader;
        try {
            addRepositoryMethod = getAddRepositoryMethod();
        } catch (final Exception tomcat4Exception) {

            try {
                addURLMethod = getAddURLMethod();
            } catch (final Exception tomcat5Exception) {
                throw new LoaderRuntimeException("Failed accessing classloader for Tomcat 5 or 6", tomcat5Exception);
            }
        }

        final ClassLoader serverLoader = getServerLoader(getContextClassLoader());
        if (serverLoader != null && serverLoader != commonLoader) {
            this.serverLoader = serverLoader;
        } else {
            this.serverLoader = null;
        }

    }

    private static ClassLoader getCommonLoader(ClassLoader loader) {
        ClassLoader bootstrapCL;
        try {
            bootstrapCL = loader.loadClass("org.apache.catalina.startup.Bootstrap").getClassLoader();
        } catch (final ClassNotFoundException e) {
            bootstrapCL = ClassLoader.getSystemClassLoader();
        }

        if (loader == bootstrapCL) {
            // this shouldn't happen...
            // means all the tomcat classes are on the system classpath
            // maybe we are in a junit test case?
            return loader;
        }

        while (loader.getParent() != bootstrapCL && loader.getParent() != null) {
            loader = loader.getParent();
        }
        return loader;
    }

    private static ClassLoader getServerLoader(final ClassLoader loader) {
        try {
            return loader.loadClass("org.apache.catalina.Container").getClassLoader();
        } catch (final ClassNotFoundException e) {
            return null;
        }
    }

    @Override
    public ClassLoader getClassLoader() {
        return (serverLoader != null) ? serverLoader : commonLoader;
        //        return commonLoader;
    }

    @SuppressWarnings("UnusedDeclaration")
    public ClassLoader getCommonLoader() {
        return commonLoader;
    }

    @Override
    public void addJarsToPath(final File dir) throws Exception {
        final String[] jarNames = dir.list(new java.io.FilenameFilter() {
            @Override
            public boolean accept(final File dir, final String name) {
                return (name.endsWith(".jar") || name.endsWith(".zip"));
            }
        });

        if (jarNames == null) {
            return;
        }

        for (final String jarName : jarNames) {
            this.addJarToPath(new File(dir, jarName).toURI().toURL());
        }
        rebuild();
    }

    @Override
    public void addJarToPath(final URL jar) throws Exception {
        this._addJarToPath(jar);
        rebuild();
    }

    public void _addJarToPath(final URL jar) throws Exception {
        ClassLoader classLoader = commonLoader;

        if (serverLoader != null && useServerClassLoader(jar)) {
            classLoader = serverLoader;
        }

        if (addRepositoryMethod != null) {
            final String path = jar.toExternalForm();
            addRepositoryMethod.invoke(classLoader, path);
        } else {
            addURLMethod.invoke(classLoader, jar);
        }
    }

    private boolean useServerClassLoader(final URL jar) {
        try {
            final URL url = findResource("META-INF/org.apache.openejb.tomcat/ServerClassLoader", jar);
            return url != null;
        } catch (final Exception e) {
            return false;
        }
    }

    private Method getGetURLsMethod() {

        return AccessController.doPrivileged(new PrivilegedAction<Method>() {

            @Override
            public Method run() {
                try {
                    final Object cp = getURLClassPath((URLClassLoader) getClassLoader());
                    final Class<?> clazz = cp.getClass();
                    return clazz.getDeclaredMethod("getURLs", URL.class);
                } catch (final Exception e) {
                    throw new LoaderRuntimeException(e);
                }

            }

        });
    }

    protected void rebuild() {
        try {
            final Object cp = getURLClassPath((URLClassLoader) getClassLoader());
            final Method getURLsMethod = getGetURLsMethod();
            //noinspection NullArgumentToVariableArgMethod
            final URL[] urls = (URL[]) getURLsMethod.invoke(cp, (Object) null);

            if (urls.length < 1) {
                return;
            }

            final StringBuilder path = new StringBuilder(urls.length * 32);

            File s;
            try {
                s = new File(URLDecoder.decode(urls[0].getFile(), "UTF-8"));
            } catch (final Exception e) {
                //noinspection deprecation
                s = new File(URLDecoder.decode(urls[0].getFile()));
            }

            path.append(s.getPath());

            for (int i = 1; i < urls.length; i++) {
                path.append(File.pathSeparator);

                try {
                    s = new File(URLDecoder.decode(urls[i].getFile(), "UTF-8"));
                } catch (final Exception e) {
                    //noinspection deprecation
                    s = new File(URLDecoder.decode(urls[i].getFile()));
                }

                path.append(s.getPath());
            }
            System.setProperty("java.class.path", path.toString());
        } catch (final Exception e) {
            Logger.getLogger(TomcatClassPath.class.getName()).log(Level.FINE, "rebuild", e);
        }

    }

    private Method getAddURLMethod() throws Exception {
        return AccessController.doPrivileged(new PrivilegedAction<Method>() {
            @Override
            public Method run() {
                Method method = null;
                try {
                    final Class clazz = URLClassLoader.class;
                    method = clazz.getDeclaredMethod("addURL", URL.class);
                    method.setAccessible(true);
                } catch (final Exception e2) {
                    e2.printStackTrace();
                }
                return method;
            }
        });
    }

    private Method getAddRepositoryMethod() throws Exception {
        return AccessController.doPrivileged(new PrivilegedAction<Method>() {
            @Override
            public Method run() {
                final Method method;
                try {
                    final Class clazz = getClassLoader().getClass();
                    method = clazz.getDeclaredMethod("addRepository", String.class);
                    method.setAccessible(true);
                    return method;
                } catch (final Exception e2) {
                    throw (IllegalStateException) new IllegalStateException("Unable to find or access the addRepository method in StandardClassLoader").initCause(e2);
                }
            }
        });
    }

    private static boolean isDirectory(final URL url) {
        final String file = url.getFile();
        return (file.length() > 0 && file.charAt(file.length() - 1) == '/');
    }

    @SuppressWarnings("ConstantConditions")
    private static URL findResource(final String resourceName, final URL... search) {

        for (int i = 0; i < search.length; i++) {
            URL currentUrl = search[i];
            if (currentUrl == null) {
                continue;
            }

            if (currentUrl == null || isDirectory(currentUrl) || currentUrl.getProtocol().equals("jar")) {
                continue;
            }
            try {
                currentUrl = new URL("jar", "", -1, currentUrl.toString() + "!/");
            } catch (final MalformedURLException e) {
                Logger.getLogger(TomcatClassPath.class.getName()).log(Level.FINE, "findResource", e);
            }

            final JarFile jarFile;
            try {
                final String protocol = currentUrl.getProtocol();
                if (protocol.equals("jar")) {
                    /*
                    * If the connection for currentUrl or resURL is
                    * used, getJarFile() will throw an exception if the
                    * entry doesn't exist.
                    */
                    final URL jarURL = ((JarURLConnection) currentUrl.openConnection()).getJarFileURL();
                    try {
                        final JarURLConnection juc = (JarURLConnection) new URL("jar", "", jarURL.toExternalForm() + "!/").openConnection();
                        jarFile = juc.getJarFile();
                    } catch (final IOException e) {
                        // Don't look for this jar file again
                        search[i] = null;
                        throw e;
                    }

                    final String entryName;
                    if (currentUrl.getFile().endsWith("!/")) {
                        entryName = resourceName;
                    } else {
                        final String file = currentUrl.getFile();
                        int sepIdx = file.lastIndexOf("!/");
                        if (sepIdx == -1) {
                            // Invalid URL, don't look here again
                            search[i] = null;
                            continue;
                        }
                        sepIdx += 2;
                        final StringBuilder sb = new StringBuilder(file.length() - sepIdx + resourceName.length());
                        sb.append(file.substring(sepIdx));
                        sb.append(resourceName);
                        entryName = sb.toString();
                    }
                    if (entryName.equals("META-INF/") && jarFile.getEntry("META-INF/MANIFEST.MF") != null) {
                        return targetURL(currentUrl, "META-INF/MANIFEST.MF");
                    }
                    if (jarFile.getEntry(entryName) != null) {
                        return targetURL(currentUrl, resourceName);
                    }
                } else if (protocol.equals("file")) {
                    final String baseFile = currentUrl.getFile();
                    final String host = currentUrl.getHost();
                    int hostLength = 0;
                    if (host != null) {
                        hostLength = host.length();
                    }
                    final StringBuilder buf = new StringBuilder(2 + hostLength + baseFile.length() + resourceName.length());

                    if (hostLength > 0) {
                        buf.append("//").append(host);
                    }
                    // baseFile always ends with '/'
                    buf.append(baseFile);
                    String fixedResName = resourceName;
                    // Do not create a UNC path, i.e. \\host
                    while (fixedResName.startsWith("/") || fixedResName.startsWith("\\")) {
                        fixedResName = fixedResName.substring(1);
                    }
                    buf.append(fixedResName);
                    final String filename = buf.toString();
                    final File file = new File(filename);
                    File file2;
                    try {
                        file2 = new File(URLDecoder.decode(filename, "UTF-8"));
                    } catch (final Exception e) {
                        //noinspection deprecation
                        file2 = new File(URLDecoder.decode(filename));
                    }
                    if (file.exists() || file2.exists()) {
                        return targetURL(currentUrl, fixedResName);
                    }
                } else {
                    final URL resourceURL = targetURL(currentUrl, resourceName);
                    final URLConnection urlConnection = resourceURL.openConnection();

                    try {
                        urlConnection.getInputStream().close();
                    } catch (final SecurityException e) {
                        return null;
                    }
                    // HTTP can return a stream on a non-existent file
                    // So check for the return code;
                    if (!resourceURL.getProtocol().equals("http")) {
                        return resourceURL;
                    }

                    final int code = ((HttpURLConnection) urlConnection).getResponseCode();
                    if (code >= 200 && code < 300) {
                        return resourceURL;
                    }
                }
            } catch (final MalformedURLException e) {
                // Keep iterating through the URL list
            } catch (final Exception e) {
                Logger.getLogger(TomcatClassPath.class.getName()).log(Level.FINE, "findResource", e);
            }
        }
        return null;
    }

    private static URL targetURL(final URL base, final String name) throws MalformedURLException {
        final StringBuilder sb = new StringBuilder(base.getFile().length() + name.length());
        sb.append(base.getFile());
        sb.append(name);
        final String file = sb.toString();
        return new URL(base.getProtocol(), base.getHost(), base.getPort(), file, null);
    }

}
