/*
 * Zed Attack Proxy (ZAP) and its related class files.
 *
 * ZAP is an HTTP/HTTPS proxy for assessing web application security.
 *
 * Copyright 2015 The ZAP Development Team
 *
 * Licensed 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.zaproxy.zap.control;

import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.parosproxy.paros.core.scanner.AbstractPlugin;
import org.zaproxy.zap.extension.pscan.PluginPassiveScanner;

/**
 * A class with utility methods to help with add-on loading and (un)installation.
 *
 * @since 2.4.3
 */
final class AddOnLoaderUtils {

    private static final Logger LOGGER = LogManager.getLogger(AddOnLoaderUtils.class);

    private AddOnLoaderUtils() {}

    /**
     * Loads, using the given {@code addOnClassLoader}, and creates an instance with the given
     * {@code classname} of the (expected) given {@code clazz}. The {@code type} is used in error
     * log messages, to indicate the expected type being loaded.
     *
     * @param <T> the type of the class that will be instantiated
     * @param addOnClassLoader the class loader of the add-on that contains the classes
     * @param classname the binary name of the class that will be loaded
     * @param clazz the type of the instance that will be created using the class loaded
     * @param type the expected type being loaded (for example, "extension", "ascanrule"...)
     * @return an instance of the given {@code clazz}, or {@code null} if an error occurred (for
     *     example, not being of the expected type)
     * @throws IllegalArgumentException if any of the parameters is {@code null}.
     */
    public static <T> T loadAndInstantiateClass(
            AddOnClassLoader addOnClassLoader, String classname, Class<T> clazz, String type) {
        validateNotNull(addOnClassLoader, "addOnClassLoader");
        validateNotNull(classname, "classname");
        validateNotNull(clazz, "clazz");
        validateNotNull(type, "type");

        return loadAndInstantiateClassImpl(addOnClassLoader, classname, clazz, type);
    }

    /**
     * Loads, using the given {@code addOnClassLoader}, and creates an instance with the given
     * {@code classname} of the (expected) given {@code clazz}. The {@code type} is used in error
     * log messages, to indicate the expected type being loaded.
     *
     * <p><strong>Note:</strong> Internal method that does not validate that the parameters are not
     * {@code null}.
     *
     * @param <T> the type of the class that will be instantiated
     * @param addOnClassLoader the class loader of the add-on that contains the classes, must not be
     *     {@code null}
     * @param classname the binary name of the class that will be loaded, must not be {@code null}
     * @param clazz the type of the instance that will be created using the class loaded, must not
     *     be {@code null}
     * @param type the expected type being loaded (for example, "extension", "ascanrule"...), must
     *     not be {@code null}
     * @return an instance of the given {@code clazz}, or {@code null} if an error occurred (for
     *     example, not being of the expected type)
     */
    private static <T> T loadAndInstantiateClassImpl(
            AddOnClassLoader addOnClassLoader, String classname, Class<T> clazz, String type) {
        Class<?> cls;
        try {
            cls = addOnClassLoader.loadClass(classname);
        } catch (ClassNotFoundException e) {
            LOGGER.error("Declared \"" + type + "\" was not found: " + classname, e);
            return null;
        } catch (LinkageError e) {
            LOGGER.error("Declared \"" + type + "\" could not be loaded: " + classname, e);
            return null;
        }

        if (Modifier.isAbstract(cls.getModifiers()) || Modifier.isInterface(cls.getModifiers())) {
            LOGGER.error("Declared \"" + type + "\" is abstract or an interface: " + classname);
            return null;
        }

        if (!clazz.isAssignableFrom(cls)) {
            LOGGER.error(
                    "Declared \""
                            + type
                            + "\" is not of type \""
                            + clazz.getName()
                            + "\": "
                            + classname);
            return null;
        }

        try {
            @SuppressWarnings("unchecked")
            Constructor<T> c = (Constructor<T>) cls.getConstructor();
            return c.newInstance();
        } catch (LinkageError | Exception e) {
            LOGGER.error("Failed to initialise: " + classname, e);
        }
        return null;
    }

    /**
     * Loads, using the given {@code addOnClassLoader}, and creates instances with the given {@code
     * classnames} of the (expected) given {@code clazz}. The {@code type} is used in error log
     * messages, to indicate the expected type being loaded. Any classname that leads to an error
     * (for example, not being of the expected type or if it was not found) it will be ignored
     * (after logging the error).
     *
     * @param <T> the type of the class that will be instantiated
     * @param addOnClassLoader the class loader of the add-on that contains the classes
     * @param classnames the binary names of the classes that will be loaded
     * @param clazz the type of the instance that will be created using the classes loaded
     * @param type the expected type being loaded (for example, "extension", "ascanrule"...)
     * @return an unmodifiable {@code List} with the instances of the given {@code clazz}
     * @throws IllegalArgumentException if any of the parameters is {@code null}.
     * @see #loadAndInstantiateClass(AddOnClassLoader, String, Class, String)
     */
    public static <T> List<T> loadDeclaredClasses(
            AddOnClassLoader addOnClassLoader,
            List<String> classnames,
            Class<T> clazz,
            String type) {
        validateNotNull(addOnClassLoader, "addOnClassLoader");
        validateNotNull(classnames, "classnames");
        validateNotNull(clazz, "clazz");
        validateNotNull(type, "type");

        if (classnames.isEmpty()) {
            return Collections.emptyList();
        }

        ArrayList<T> instances = new ArrayList<>(classnames.size());
        for (String classname : classnames) {
            T instance = loadAndInstantiateClassImpl(addOnClassLoader, classname, clazz, type);
            if (instance != null) {
                instances.add(instance);
            }
        }
        instances.trimToSize();
        return Collections.unmodifiableList(instances);
    }

    /**
     * Gets the active scan rules of the given {@code addOn}. The active scan rules are first
     * loaded, if they weren't already.
     *
     * @param addOn the add-on whose active scan rules will be returned
     * @param addOnClassLoader the {@code AddOnClassLoader} of the given {@code addOn}
     * @return an unmodifiable {@code List} with the active scan rules, never {@code null}
     * @throws IllegalArgumentException if any of the parameters is {@code null}.
     */
    public static List<AbstractPlugin> getActiveScanRules(
            AddOn addOn, AddOnClassLoader addOnClassLoader) {
        validateNotNull(addOn, "addOn");
        validateNotNull(addOnClassLoader, "addOnClassLoader");

        synchronized (addOn) {
            if (addOn.isLoadedAscanrulesSet()) {
                return addOn.getLoadedAscanrules();
            }

            List<AbstractPlugin> ascanrules =
                    loadDeclaredClasses(
                            addOnClassLoader,
                            addOn.getAscanrules(),
                            AbstractPlugin.class,
                            "ascanrule");
            addOn.setLoadedAscanrules(ascanrules);
            addOn.setLoadedAscanrulesSet(true);
            return Collections.unmodifiableList(ascanrules);
        }
    }

    /**
     * Gets the passive scan rules of the given {@code addOn}. The passive scan rules are first
     * loaded, if they weren't already.
     *
     * @param addOn the add-on whose passive scan rules will be returned
     * @param addOnClassLoader the {@code AddOnClassLoader} of the given {@code addOn}
     * @return an unmodifiable {@code List} with the passive scan rules, never {@code null}
     * @throws IllegalArgumentException if any of the parameters is {@code null}.
     */
    public static List<PluginPassiveScanner> getPassiveScanRules(
            AddOn addOn, AddOnClassLoader addOnClassLoader) {
        validateNotNull(addOn, "addOn");
        validateNotNull(addOnClassLoader, "addOnClassLoader");

        synchronized (addOn) {
            if (addOn.isLoadedPscanrulesSet()) {
                return addOn.getLoadedPscanrules();
            }

            List<PluginPassiveScanner> pscanrules =
                    loadDeclaredClasses(
                            addOnClassLoader,
                            addOn.getPscanrules(),
                            PluginPassiveScanner.class,
                            "pscanrule");
            addOn.setLoadedPscanrules(pscanrules);
            addOn.setLoadedPscanrulesSet(true);
            return Collections.unmodifiableList(pscanrules);
        }
    }

    /**
     * Helper method that validates that the given {@code object} is not {@code null}, throwing an
     * {@code IllegalArgumentException} if it is.
     *
     * @param object the object that will be validated that it's not {@code null}
     * @param name the name used in the exception message
     * @throws IllegalArgumentException if the given {@code object} is {@code null}.
     */
    private static void validateNotNull(Object object, String name) {
        if (object == null) {
            throw new IllegalArgumentException("Parameter " + name + " must not be null.");
        }
    }
}
