/*
 * 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.tomee.catalina;

import org.apache.openejb.classloader.WebAppEnricher;
import org.apache.openejb.component.ClassLoaderEnricher;
import org.apache.openejb.loader.JarLocation;
import org.apache.openejb.loader.SystemInstance;
import org.apache.openejb.util.LogCategory;
import org.apache.openejb.util.Logger;
import org.apache.tomee.installer.Paths;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

public final class TomEEClassLoaderEnricher implements WebAppEnricher {
    private static final Logger LOGGER = Logger.getInstance(LogCategory.OPENEJB, TomEEClassLoaderEnricher.class);

    /**
     * Enrichement part
     */

    public static final String TOMEE_WEBAPP_CLASSLOADER_ENRICHMENT_SKIP = "tomee.webapp.classloader.enrichment.skip";

    public static final String TOMEE_WEBAPP_CLASSLOADER_ENRICHMENT_CLASSES = "tomee.webapp.classloader.enrichment.classes";
    public static final String TOMEE_WEBAPP_CLASSLOADER_ENRICHMENT_PREFIXES = "tomee.webapp.classloader.enrichment.prefixes";

    private static final String[] JAR_TO_ADD_CLASS_HELPERS;
    private static final boolean SKIP_VALIDATION;

    private static final String[] DEFAULT_PREFIXES_TO_ADD = new String[]{
            "openwebbeans-jsf", // to be able to provide jsf impl
            "openwebbeans-jsf12", // if replaces previous one we need to handle it properly, note: not included by default
            "tomee-mojarra",
            "tomee-myfaces", // to be able to embedded myfaces in the webapp
            "openejb-jpa-integration" // to be able to embedded hibernate, eclipselinks....
    };
    private static final String[] PREFIXES_TO_ADD;

    static {
        final Collection<String> classes = new ArrayList<String>();
        final Collection<String> prefixes = new ArrayList<String>();
        if (!SystemInstance.get().getOptions().get(TOMEE_WEBAPP_CLASSLOADER_ENRICHMENT_SKIP, false)) {
            final String additionalEnrichments = SystemInstance.get().getOptions().get(TOMEE_WEBAPP_CLASSLOADER_ENRICHMENT_CLASSES, "");
            if (additionalEnrichments != null && !additionalEnrichments.isEmpty()) {
                for (final String name : additionalEnrichments.split(",")) {
                    classes.add(name.trim());
                }
            }

            final String additionalPrefixes = SystemInstance.get().getOptions().get(TOMEE_WEBAPP_CLASSLOADER_ENRICHMENT_PREFIXES, "");
            prefixes.addAll(Arrays.asList(DEFAULT_PREFIXES_TO_ADD));
            if (additionalPrefixes != null && !additionalPrefixes.isEmpty()) {
                for (final String name : additionalPrefixes.split(",")) {
                    prefixes.add(name.trim());
                }
            }
        }
        JAR_TO_ADD_CLASS_HELPERS = classes.toArray(new String[classes.size()]);
        PREFIXES_TO_ADD = prefixes.toArray(new String[prefixes.size()]);
        SKIP_VALIDATION = SystemInstance.get().getOptions().get("tomee.api.validation.skip", false);
    }

    @Override
    public URL[] enrichment(final ClassLoader appCl) {
        final Collection<URL> urls = new HashSet<URL>();

        // from class
        final ClassLoader cl = TomEEClassLoaderEnricher.class.getClassLoader(); // reference classloader = standardclassloader
        if (cl != appCl && appCl != null) {
            for (final String name : JAR_TO_ADD_CLASS_HELPERS) {
                try { // don't do anything with appCl otherwise in tomcat it will be broken since WebAppClassLoader caches missed resources
                    final String classFileName = name.replace(".", "/") + ".class";
                    final URL parentUrl = cl.getResource(classFileName);
                    final File file = jarLocation(parentUrl, classFileName);
                    if (file == null) {
                        continue;
                    }
                    urls.add(file.toURI().toURL());
                } catch (final Exception | NoClassDefFoundError e) {
                    // ignore
                }
            }
        }

        // from prefix
        final Paths paths = new Paths(new File(System.getProperty("openejb.home"))); // parameter is useless
        for (final String prefix : PREFIXES_TO_ADD) {
            final File file = paths.findTomEELibJar(prefix);
            if (file != null) {
                try {
                    urls.add(file.toURI().toURL());
                } catch (final MalformedURLException e) {
                    // ignored
                }
            }
        }

        // from config
        final ClassLoaderEnricher classLoaderEnricher = SystemInstance.get().getComponent(ClassLoaderEnricher.class);
        if (null != classLoaderEnricher) {
            final URL[] enrichment = classLoaderEnricher.applicationEnrichment();
            if (null != enrichment && enrichment.length > 0) {
                urls.addAll(Arrays.asList(enrichment));
            }
        }

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

    private static File jarLocation(final URL url, final String classFileName) throws MalformedURLException {
        if ("jar".equals(url.getProtocol())) {
            final String spec = url.getFile();
            return new File(JarLocation.decode(new URL(spec.substring(0, spec.indexOf('!'))).getFile()));
        } else if ("file".equals(url.getProtocol())) {
            return JarLocation.toFile(classFileName, url);
        }
        return null;
    }

    /**
     * Validation part
     */
    private static final String[][] FORBIDDEN_CLASSES = new String[][]{ // name, spec +1 name, message. Note: [1] != null => [2] != null
            {"javax.persistence.Entity", null, null}, // JPA
            {"javax.transaction.Transaction", null, null}, // JTA
            {"javax.jws.WebService", null, null}, // JAXWS
            {"javax.validation.Validation", null, null}, // BVal
            {"javax.jms.Queue", null, null}, // JMS
            {"javax.enterprise.context.ApplicationScoped", null, null}, // CDI
            {"javax.inject.Inject", null, null}, // CDI
            {"javax.ws.rs.Path", null, null},
            {"javax.ejb.EJB", null, null}, // EJB
            {"javax.annotation.PostConstruct", null, null} // javax.annotation
    };

    public static boolean validateJarFile(final File file) throws IOException {
        if (SKIP_VALIDATION) {
            return true;
        }

        final ClassLoader parent = TomEEClassLoaderEnricher.class.getClassLoader();

        JarFile jarFile = null;

        try {
            jarFile = new JarFile(file);
            for (final String[] name : FORBIDDEN_CLASSES) {
                // if we can't load if from our classLoader we'll not impose anything on this class
                boolean found = false;
                for (int i = 0; i < 2; i++) {
                    try {
                        try {
                            parent.loadClass(name[0]);
                            found = true;
                            break;
                        } catch (final Exception e) {
                            // found = false
                        }
                    } catch (final LinkageError le) { // would be a pain to fail here
                        // retry
                    }
                }

                if (!found) {
                    continue;
                }

                // we found it so let's check it is or not in the file (potential conflict)
                final String entry = name[0].replace('.', '/') + ".class";
                final JarEntry jarEntry = jarFile.getJarEntry(entry);
                if (jarEntry != null) {
                    if (name[1] != null) {
                        if (jarFile.getJarEntry(name[1].replace('.', '/') + ".class") != null) {
                            LOGGER.warning("jar '" + file.getAbsolutePath() + "' contains offending class: " + name[0]
                                    + "but: " + name[2]);
                            return true;
                        }
                    }
                    final URLClassLoader tmpLoader = new URLClassLoader(new URL[]{ file.toURI().toURL() });
                    final URL resource = tmpLoader.getResource(entry);
                    try {
                        if (resource != null && resource.equals(parent.getResource(entry))) {
                            continue;
                        }
                    } finally {
                        tmpLoader.close();
                    }

                    LOGGER.warning("jar '" + file.getAbsolutePath() + "' contains offending class: " + name[0]
                            + ". It will be ignored.");
                    return !"true".equals(SystemInstance.get().getProperty("openejb.api." + name[0] + ".validation", "true"));
                }
            }
            return true;
        } finally {
            if (jarFile != null) { // in java 6 JarFile is not Closeable so don't use IO.close()
                try {
                    jarFile.close();
                } catch (final IOException ioe) {
                    // Ignored
                }
            }
        }
    }
}
