All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.elasticsearch.plugins.MockPluginsService Maven / Gradle / Ivy

The newest version!
/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the Elastic License
 * 2.0 and the Server Side Public License, v 1; you may not use this file except
 * in compliance with, at your election, the Elastic License 2.0 or the Server
 * Side Public License, v 1.
 */

package org.elasticsearch.plugins;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.Build;
import org.elasticsearch.action.admin.cluster.node.info.PluginsAndModules;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ConcurrentCollections;
import org.elasticsearch.env.Environment;
import org.elasticsearch.jdk.ModuleQualifiedExportsService;
import org.elasticsearch.plugins.spi.SPIClassIterator;

import java.lang.reflect.Constructor;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;

public class MockPluginsService extends PluginsService {

    private static final Logger logger = LogManager.getLogger(MockPluginsService.class);

    private final List classpathPlugins;

    /**
     * Constructs a new PluginService
     *
     * @param settings         The settings of the system
     * @param environment      The environment for the plugin
     * @param classpathPlugins Plugins that exist in the classpath which should be loaded
     */
    public MockPluginsService(Settings settings, Environment environment, Collection> classpathPlugins) {
        super(settings, environment.configFile(), environment.modulesFile(), environment.pluginsFile());

        final Path configPath = environment.configFile();

        List pluginsLoaded = new ArrayList<>();

        for (Class pluginClass : classpathPlugins) {
            Plugin plugin = loadPlugin(pluginClass, settings, configPath);
            PluginDescriptor pluginInfo = new PluginDescriptor(
                pluginClass.getName(),
                "classpath plugin",
                "NA",
                Build.current().version(),
                Integer.toString(Runtime.version().feature()),
                pluginClass.getName(),
                null,
                Collections.emptyList(),
                false,
                false,
                false,
                false
            );
            if (logger.isTraceEnabled()) {
                logger.trace("plugin loaded from classpath [{}]", pluginInfo);
            }
            pluginsLoaded.add(new LoadedPlugin(pluginInfo, plugin, pluginClass.getClassLoader(), ModuleLayer.boot()));
        }
        loadExtensions(pluginsLoaded);
        this.classpathPlugins = List.copyOf(pluginsLoaded);
    }

    @Override
    protected final List plugins() {
        return this.classpathPlugins;
    }

    @Override
    public PluginsAndModules info() {
        return new PluginsAndModules(
            this.classpathPlugins.stream().map(LoadedPlugin::descriptor).map(PluginRuntimeInfo::new).toList(),
            List.of()
        );
    }

    private static final Map, Collection>> spiClassesByService = ConcurrentCollections.newConcurrentMap();

    @Override
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public  List loadServiceProviders(Class service) {
        // We use a map here to avoid duplicates because SPIClassIterator will match
        // all plugins in MockNode, because all plugins are loaded by the same class loader.
        // Each entry in the map is a unique service provider implementation.
        Map, T> result = new HashMap<>();
        for (LoadedPlugin pluginTuple : plugins()) {
            var plugin = pluginTuple.instance();
            var classLoader = plugin.getClass().getClassLoader();
            final Collection extension;
            if (classLoader == ClassLoader.getSystemClassLoader()) {
                // only determine the spi classes handled by the system classloader that loads most plugins in tests once to save test time;
                extension = createExtensions(service, plugin, (Iterator) spiClassesByService.computeIfAbsent(service, s -> {
                    var res = new ArrayList>();
                    SPIClassIterator.get(service, classLoader).forEachRemaining(res::add);
                    return List.copyOf(res);
                }).iterator(), result::containsKey);
            } else {
                extension = createExtensions(service, plugin, result::containsKey);
            }
            extension.forEach(e -> result.put(e.getClass(), e));
        }

        return List.copyOf(result.values());
    }

    /**
     * When we load tests with MockNode, all plugins are loaded with the same class loader,
     * which breaks loading service providers with our SPIClassIterator. Since all plugins are
     * loaded in the same class loader, we find all plugins for any class found by the SPIClassIterator
     * causing us to pass plugin types to createExtension that aren't actually part of that plugin.
     * This modified createExtensions, checks for the type and returns an empty list if the
     * plugin class type is incompatible. It also skips loading extension types that have already
     * been loaded, so that duplicates are not created.
     */
    static  List createExtensions(
        Class extensionPointType,
        Plugin plugin,
        Predicate> loadedPredicate
    ) {
        Iterator> classIterator = SPIClassIterator.get(extensionPointType, plugin.getClass().getClassLoader());
        return createExtensions(extensionPointType, plugin, classIterator, loadedPredicate);
    }

    private static  List createExtensions(
        Class extensionPointType,
        Plugin plugin,
        Iterator> classIterator,
        Predicate> loadedPredicate
    ) {
        List extensions = new ArrayList<>();
        while (classIterator.hasNext()) {
            Class extensionClass = classIterator.next();
            if (loadedPredicate.test(extensionClass)) {
                // skip extensions that have already been loaded
                continue;
            }

            @SuppressWarnings("unchecked")
            Constructor[] constructors = (Constructor[]) extensionClass.getConstructors();
            boolean compatible = true;

            // We only check if we have incompatible one argument constructor, otherwise we let the code
            // fall-through to the PluginsService method that will check if we have valid service provider.
            // For one argument constructors we cannot validate from which plugin they should be loaded, which
            // is why we de-dup the instances by using a Set in loadServiceProviders.
            for (var constructor : constructors) {
                if (constructor.getParameterCount() == 1 && constructor.getParameterTypes()[0] != plugin.getClass()) {
                    compatible = false;
                    break;
                }
            }
            if (compatible) {
                extensions.add(createExtension(extensionClass, extensionPointType, plugin));
            }
        }
        return extensions;
    }

    @Override
    protected void addServerExportsService(Map> qualifiedExports) {
        // tests don't run modular
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy