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

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

There is a newer version: 8.15.1
Show 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.apache.lucene.codecs.Codec;
import org.apache.lucene.codecs.DocValuesFormat;
import org.apache.lucene.codecs.PostingsFormat;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.action.admin.cluster.node.info.PluginsAndModules;
import org.elasticsearch.common.io.Streams;
import org.elasticsearch.common.logging.DeprecationCategory;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Setting.Property;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.core.PathUtils;
import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.core.Tuple;
import org.elasticsearch.jdk.JarHell;
import org.elasticsearch.jdk.ModuleQualifiedExportsService;
import org.elasticsearch.node.ReportingService;
import org.elasticsearch.plugins.scanners.StablePluginsRegistry;
import org.elasticsearch.plugins.spi.SPIClassIterator;

import java.io.IOException;
import java.lang.ModuleLayer.Controller;
import java.lang.module.Configuration;
import java.lang.module.ModuleFinder;
import java.lang.reflect.Constructor;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.elasticsearch.common.io.FileSystemUtils.isAccessibleDirectory;
import static org.elasticsearch.jdk.ModuleQualifiedExportsService.addExportsService;
import static org.elasticsearch.jdk.ModuleQualifiedExportsService.exposeQualifiedExportsAndOpens;

public class PluginsService implements ReportingService {

    public StablePluginsRegistry getStablePluginRegistry() {
        return stablePluginsRegistry;
    }

    /**
     * A loaded plugin is one for which Elasticsearch has successfully constructed an instance of the plugin's class
     * @param descriptor Metadata about the plugin, usually loaded from plugin properties
     * @param instance The constructed instance of the plugin's main class
     * @param loader   The classloader for the plugin
     * @param layer   The module layer for the plugin
     */
    record LoadedPlugin(PluginDescriptor descriptor, Plugin instance, ClassLoader loader, ModuleLayer layer) {

        LoadedPlugin {
            Objects.requireNonNull(descriptor);
            Objects.requireNonNull(instance);
            Objects.requireNonNull(loader);
            Objects.requireNonNull(layer);
        }

        /**
         * Creates a loaded classpath plugin. A classpath plugin is a plugin loaded
         * by the system classloader and defined to the unnamed module of the boot layer.
         */
        LoadedPlugin(PluginDescriptor descriptor, Plugin instance) {
            this(descriptor, instance, PluginsService.class.getClassLoader(), ModuleLayer.boot());
        }
    }

    private static final Logger logger = LogManager.getLogger(PluginsService.class);
    private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(PluginsService.class);

    private final Settings settings;
    private final Path configPath;

    /**
     * We keep around a list of plugins and modules. The order of
     * this list is that which the plugins and modules were loaded in.
     */
    private final List plugins;
    private final PluginsAndModules info;
    private final StablePluginsRegistry stablePluginsRegistry = new StablePluginsRegistry();

    public static final Setting> MANDATORY_SETTING = Setting.stringListSetting("plugin.mandatory", Property.NodeScope);

    /**
     * Constructs a new PluginService
     *
     * @param settings         The settings of the system
     * @param modulesDirectory The directory modules exist in, or null if modules should not be loaded from the filesystem
     * @param pluginsDirectory The directory plugins exist in, or null if plugins should not be loaded from the filesystem
     */
    @SuppressWarnings("this-escape")
    public PluginsService(Settings settings, Path configPath, Path modulesDirectory, Path pluginsDirectory) {
        this.settings = settings;
        this.configPath = configPath;

        Map> qualifiedExports = new HashMap<>(ModuleQualifiedExportsService.getBootServices());
        addServerExportsService(qualifiedExports);

        Set seenBundles = new LinkedHashSet<>();

        // load modules
        List modulesList = new ArrayList<>();
        Set moduleNameList = new HashSet<>();
        if (modulesDirectory != null) {
            try {
                Set modules = PluginsUtils.getModuleBundles(modulesDirectory);
                modules.stream().map(PluginBundle::pluginDescriptor).forEach(m -> {
                    modulesList.add(m);
                    moduleNameList.add(m.getName());
                });
                seenBundles.addAll(modules);
            } catch (IOException ex) {
                throw new IllegalStateException("Unable to initialize modules", ex);
            }
        }

        // load plugins
        List pluginsList = new ArrayList<>();
        if (pluginsDirectory != null) {
            try {
                // TODO: remove this leniency, but tests bogusly rely on it
                if (isAccessibleDirectory(pluginsDirectory, logger)) {
                    PluginsUtils.checkForFailedPluginRemovals(pluginsDirectory);
                    Set plugins = PluginsUtils.getPluginBundles(pluginsDirectory);
                    plugins.stream().map(PluginBundle::pluginDescriptor).forEach(pluginsList::add);
                    seenBundles.addAll(plugins);
                }
            } catch (IOException ex) {
                throw new IllegalStateException("Unable to initialize plugins", ex);
            }
        }

        LinkedHashMap loadedPlugins = loadBundles(seenBundles, qualifiedExports);

        var inspector = PluginIntrospector.getInstance();
        this.info = new PluginsAndModules(getRuntimeInfos(inspector, pluginsList, loadedPlugins), modulesList);
        this.plugins = List.copyOf(loadedPlugins.values());

        checkDeprecations(inspector, pluginsList, loadedPlugins);

        checkMandatoryPlugins(
            pluginsList.stream().map(PluginDescriptor::getName).collect(Collectors.toSet()),
            new HashSet<>(MANDATORY_SETTING.get(settings))
        );

        // we don't log jars in lib/ we really shouldn't log modules,
        // but for now: just be transparent so we can debug any potential issues
        for (String name : loadedPlugins.keySet()) {
            if (moduleNameList.contains(name)) {
                logger.info("loaded module [{}]", name);
            } else {
                logger.info("loaded plugin [{}]", name);
            }
        }
    }

    // package-private for testing
    static void checkMandatoryPlugins(Set existingPlugins, Set mandatoryPlugins) {
        if (mandatoryPlugins.isEmpty()) {
            return;
        }

        Set missingPlugins = Sets.difference(mandatoryPlugins, existingPlugins);
        if (missingPlugins.isEmpty() == false) {
            final String message = "missing mandatory plugins ["
                + String.join(", ", missingPlugins)
                + "], found plugins ["
                + String.join(", ", existingPlugins)
                + "]";
            throw new IllegalStateException(message);
        }
    }

    private static final Set officialPlugins;

    static {
        try (var stream = PluginsService.class.getResourceAsStream("/plugins.txt")) {
            officialPlugins = Streams.readAllLines(stream).stream().map(String::trim).collect(Collectors.toUnmodifiableSet());
        } catch (final IOException e) {
            throw new AssertionError(e);
        }
    }

    private static List getRuntimeInfos(
        PluginIntrospector inspector,
        List pluginDescriptors,
        Map plugins
    ) {
        List runtimeInfos = new ArrayList<>();
        for (PluginDescriptor descriptor : pluginDescriptors) {
            LoadedPlugin plugin = plugins.get(descriptor.getName());
            assert plugin != null;
            Class pluginClazz = plugin.instance.getClass();
            boolean isOfficial = officialPlugins.contains(descriptor.getName());
            PluginApiInfo apiInfo = null;
            if (isOfficial == false) {
                apiInfo = new PluginApiInfo(inspector.interfaces(pluginClazz), inspector.overriddenMethods(pluginClazz));
            }
            runtimeInfos.add(new PluginRuntimeInfo(descriptor, isOfficial, apiInfo));
        }
        return runtimeInfos;
    }

    /**
     * Map a function over all plugins
     * @param function a function that takes a plugin and returns a result
     * @return A stream of results
     * @param  The generic type of the result
     */
    public final  Stream map(Function function) {
        return plugins().stream().map(LoadedPlugin::instance).map(function);
    }

    /**
     * FlatMap a function over all plugins
     * @param function a function that takes a plugin and returns a collection
     * @return A stream of results
     * @param  The generic type of the collection
     */
    public final  Stream flatMap(Function> function) {
        return plugins().stream().map(LoadedPlugin::instance).flatMap(p -> function.apply(p).stream());
    }

    /**
     * Apply a consumer action to each plugin
     * @param consumer An action that consumes a plugin
     */
    public final void forEach(Consumer consumer) {
        plugins().stream().map(LoadedPlugin::instance).forEach(consumer);
    }

    /**
     * Sometimes we want the plugin name for error handling.
     * @return A map of plugin names to plugin instances.
     */
    public final Map pluginMap() {
        return plugins().stream().collect(Collectors.toMap(p -> p.descriptor().getName(), LoadedPlugin::instance));
    }

    /**
     * Get information about plugins and modules
     */
    @Override
    public PluginsAndModules info() {
        return info;
    }

    protected List plugins() {
        return this.plugins;
    }

    private LinkedHashMap loadBundles(
        Set bundles,
        Map> qualifiedExports
    ) {
        LinkedHashMap loaded = new LinkedHashMap<>();
        Map> transitiveUrls = new HashMap<>();
        List sortedBundles = PluginsUtils.sortBundles(bundles);
        if (sortedBundles.isEmpty() == false) {
            Set systemLoaderURLs = JarHell.parseModulesAndClassPath();
            for (PluginBundle bundle : sortedBundles) {
                PluginsUtils.checkBundleJarHell(systemLoaderURLs, bundle, transitiveUrls);
                loadBundle(bundle, loaded, qualifiedExports);
            }
        }

        loadExtensions(loaded.values());
        return loaded;
    }

    // package-private for test visibility
    static void loadExtensions(Collection plugins) {
        Map> extendingPluginsByName = plugins.stream()
            .flatMap(t -> t.descriptor().getExtendedPlugins().stream().map(extendedPlugin -> Tuple.tuple(extendedPlugin, t.instance())))
            .collect(Collectors.groupingBy(Tuple::v1, Collectors.mapping(Tuple::v2, Collectors.toList())));
        for (LoadedPlugin pluginTuple : plugins) {
            if (pluginTuple.instance() instanceof ExtensiblePlugin) {
                loadExtensionsForPlugin(
                    (ExtensiblePlugin) pluginTuple.instance(),
                    extendingPluginsByName.getOrDefault(pluginTuple.descriptor().getName(), List.of())
                );
            }
        }
    }

    /**
     * SPI convenience method that uses the {@link ServiceLoader} JDK class to load various SPI providers
     * from plugins/modules.
     * 

* For example: * *

     * var pluginHandlers = pluginsService.loadServiceProviders(OperatorHandlerProvider.class);
     * 
* @param service A templated service class to look for providers in plugins * @return an immutable {@link List} of discovered providers in the plugins/modules */ public List loadServiceProviders(Class service) { List result = new ArrayList<>(); for (LoadedPlugin pluginTuple : plugins()) { result.addAll(createExtensions(service, pluginTuple.instance)); } return Collections.unmodifiableList(result); } /** * Loads a single SPI extension. * * There should be no more than one extension found. If no service providers * are found, the supplied fallback is used. * * @param service the SPI class that should be loaded * @param fallback a supplier for an instance if no providers are found * @return an instance of the service * @param the SPI service type */ public T loadSingletonServiceProvider(Class service, Supplier fallback) { var services = loadServiceProviders(service); if (services.size() > 1) { throw new IllegalStateException(String.format(Locale.ROOT, "More than one extension found for %s", service.getSimpleName())); } else if (services.isEmpty()) { return fallback.get(); } return services.get(0); } private static void loadExtensionsForPlugin(ExtensiblePlugin extensiblePlugin, List extendingPlugins) { ExtensiblePlugin.ExtensionLoader extensionLoader = new ExtensiblePlugin.ExtensionLoader() { @Override public List loadExtensions(Class extensionPointType) { List result = new ArrayList<>(); for (Plugin extendingPlugin : extendingPlugins) { result.addAll(createExtensions(extensionPointType, extendingPlugin)); } return Collections.unmodifiableList(result); } }; extensiblePlugin.loadExtensions(extensionLoader); } private static List createExtensions(Class extensionPointType, Plugin plugin) { SPIClassIterator classIterator = SPIClassIterator.get(extensionPointType, plugin.getClass().getClassLoader()); List extensions = new ArrayList<>(); while (classIterator.hasNext()) { Class extensionClass = classIterator.next(); extensions.add(createExtension(extensionClass, extensionPointType, plugin)); } return extensions; } // package-private for test visibility static T createExtension(Class extensionClass, Class extensionPointType, Plugin plugin) { @SuppressWarnings("unchecked") Constructor[] constructors = (Constructor[]) extensionClass.getConstructors(); if (constructors.length == 0) { throw new IllegalStateException("no public " + extensionConstructorMessage(extensionClass, extensionPointType)); } Constructor constructor = constructors[0]; // Using modules and SPI requires that we declare the default no-arg constructor apart from our custom // one arg constructor with a plugin. if (constructors.length == 2) { // we prefer the one arg constructor in this case if (constructors[1].getParameterCount() > 0) { constructor = constructors[1]; } } else if (constructors.length > 1) { throw new IllegalStateException("no unique public " + extensionConstructorMessage(extensionClass, extensionPointType)); } if (constructor.getParameterCount() > 1) { throw new IllegalStateException(extensionSignatureMessage(extensionClass, extensionPointType, plugin)); } if (constructor.getParameterCount() == 1 && constructor.getParameterTypes()[0] != plugin.getClass()) { throw new IllegalStateException( extensionSignatureMessage(extensionClass, extensionPointType, plugin) + ", not (" + constructor.getParameterTypes()[0].getName() + ")" ); } try { if (constructor.getParameterCount() == 0) { return constructor.newInstance(); } else { return constructor.newInstance(plugin); } } catch (ReflectiveOperationException e) { throw new IllegalStateException( "failed to create extension [" + extensionClass.getName() + "] of type [" + extensionPointType.getName() + "]", e ); } } private static String extensionSignatureMessage(Class extensionClass, Class extensionPointType, Plugin plugin) { return "signature of " + extensionConstructorMessage(extensionClass, extensionPointType) + " must be either () or (" + plugin.getClass().getName() + ")"; } private static String extensionConstructorMessage(Class extensionClass, Class extensionPointType) { return "constructor for extension [" + extensionClass.getName() + "] of type [" + extensionPointType.getName() + "]"; } private void loadBundle( PluginBundle bundle, Map loaded, Map> qualifiedExports ) { String name = bundle.plugin.getName(); logger.debug(() -> "Loading bundle: " + name); PluginsUtils.verifyCompatibility(bundle.plugin); // collect the list of extended plugins List extendedPlugins = new ArrayList<>(); for (String extendedPluginName : bundle.plugin.getExtendedPlugins()) { LoadedPlugin extendedPlugin = loaded.get(extendedPluginName); assert extendedPlugin != null; if (ExtensiblePlugin.class.isInstance(extendedPlugin.instance()) == false) { throw new IllegalStateException("Plugin [" + name + "] cannot extend non-extensible plugin [" + extendedPluginName + "]"); } assert extendedPlugin.loader() != null : "All non-classpath plugins should be loaded with a classloader"; extendedPlugins.add(extendedPlugin); logger.debug( () -> "Loading bundle: " + name + ", ext plugins: " + extendedPlugins.stream().map(lp -> lp.descriptor().getName()).toList() ); } final ClassLoader parentLoader = PluginLoaderIndirection.createLoader( getClass().getClassLoader(), extendedPlugins.stream().map(LoadedPlugin::loader).toList() ); LayerAndLoader spiLayerAndLoader = null; if (bundle.hasSPI()) { spiLayerAndLoader = createSPI(bundle, parentLoader, extendedPlugins, qualifiedExports); } final ClassLoader pluginParentLoader = spiLayerAndLoader == null ? parentLoader : spiLayerAndLoader.loader(); final LayerAndLoader pluginLayerAndLoader = createPlugin( bundle, pluginParentLoader, extendedPlugins, spiLayerAndLoader, qualifiedExports ); final ClassLoader pluginClassLoader = pluginLayerAndLoader.loader(); if (spiLayerAndLoader == null) { // use full implementation for plugins extending this one spiLayerAndLoader = pluginLayerAndLoader; } // reload SPI with any new services from the plugin reloadLuceneSPI(pluginClassLoader); ClassLoader cl = Thread.currentThread().getContextClassLoader(); try { // Set context class loader to plugin's class loader so that plugins // that have dependencies with their own SPI endpoints have a chance to load // and initialize them appropriately. privilegedSetContextClassLoader(pluginClassLoader); Plugin plugin; if (bundle.pluginDescriptor().isStable()) { stablePluginsRegistry.scanBundleForStablePlugins(bundle, pluginClassLoader); /* Contrary to old plugins we don't need an instance of the plugin here. Stable plugin register components (like CharFilterFactory) in stable plugin registry, which is then used in AnalysisModule when registering char filter factories and other analysis components. We don't have to support for settings, additional components and other methods that are in org.elasticsearch.plugins.Plugin We need to pass a name though so that we can show that a plugin was loaded (via cluster state api) This might need to be revisited once support for settings is added */ plugin = new StablePluginPlaceHolder(bundle.plugin.getName()); } else { Class pluginClass = loadPluginClass(bundle.plugin.getClassname(), pluginClassLoader); if (pluginClassLoader != pluginClass.getClassLoader()) { throw new IllegalStateException( "Plugin [" + name + "] must reference a class loader local Plugin class [" + bundle.plugin.getClassname() + "] (class loader [" + pluginClass.getClassLoader() + "])" ); } plugin = loadPlugin(pluginClass, settings, configPath); } loaded.put(name, new LoadedPlugin(bundle.plugin, plugin, spiLayerAndLoader.loader(), spiLayerAndLoader.layer())); } finally { privilegedSetContextClassLoader(cl); } } static LayerAndLoader createSPI( PluginBundle bundle, ClassLoader parentLoader, List extendedPlugins, Map> qualifiedExports ) { final PluginDescriptor plugin = bundle.plugin; if (plugin.getModuleName().isPresent()) { logger.debug(() -> "Loading bundle: " + plugin.getName() + ", creating spi, modular"); return createSpiModuleLayer( bundle.spiUrls, parentLoader, extendedPlugins.stream().map(LoadedPlugin::layer).toList(), qualifiedExports ); } else { logger.debug(() -> "Loading bundle: " + plugin.getName() + ", creating spi, non-modular"); return LayerAndLoader.ofLoader(URLClassLoader.newInstance(bundle.spiUrls.toArray(new URL[0]), parentLoader)); } } static LayerAndLoader createPlugin( PluginBundle bundle, ClassLoader pluginParentLoader, List extendedPlugins, LayerAndLoader spiLayerAndLoader, Map> qualifiedExports ) { final PluginDescriptor plugin = bundle.plugin; if (plugin.getModuleName().isPresent()) { logger.debug(() -> "Loading bundle: " + plugin.getName() + ", modular"); var parentLayers = Stream.concat( Stream.ofNullable(spiLayerAndLoader != null ? spiLayerAndLoader.layer() : null), extendedPlugins.stream().map(LoadedPlugin::layer) ).toList(); return createPluginModuleLayer(bundle, pluginParentLoader, parentLayers, qualifiedExports); } else if (plugin.isStable()) { logger.debug(() -> "Loading bundle: " + plugin.getName() + ", non-modular as synthetic module"); return LayerAndLoader.ofLoader( UberModuleClassLoader.getInstance( pluginParentLoader, ModuleLayer.boot(), "synthetic." + toModuleName(plugin.getName()), bundle.allUrls, Set.of("org.elasticsearch.server") // TODO: instead of denying server, allow only jvm + stable API modules ) ); } else { logger.debug(() -> "Loading bundle: " + plugin.getName() + ", non-modular"); return LayerAndLoader.ofLoader(URLClassLoader.newInstance(bundle.urls.toArray(URL[]::new), pluginParentLoader)); } } // package-visible for testing static String toModuleName(String name) { String result = name.replaceAll("\\W+", ".") // replace non-alphanumeric character strings with dots .replaceAll("(^[^A-Za-z_]*)", "") // trim non-alpha or underscore characters from start .replaceAll("\\.$", "") // trim trailing dot .toLowerCase(Locale.getDefault()); assert ModuleSupport.isPackageName(result); return result; } private static void checkDeprecations( PluginIntrospector inspector, List pluginDescriptors, Map plugins ) { for (PluginDescriptor descriptor : pluginDescriptors) { LoadedPlugin plugin = plugins.get(descriptor.getName()); Class pluginClazz = plugin.instance.getClass(); for (String deprecatedInterface : inspector.deprecatedInterfaces(pluginClazz)) { deprecationLogger.warn( DeprecationCategory.PLUGINS, pluginClazz.getName() + deprecatedInterface, "Plugin class {} from plugin {} implements deprecated plugin interface {}. " + "This plugin interface will be removed in a future release.", pluginClazz.getName(), descriptor.getName(), deprecatedInterface ); } for (var deprecatedMethodInInterface : inspector.deprecatedMethods(pluginClazz).entrySet()) { String methodName = deprecatedMethodInInterface.getKey(); String interfaceName = deprecatedMethodInInterface.getValue(); deprecationLogger.warn( DeprecationCategory.PLUGINS, pluginClazz.getName() + methodName + interfaceName, "Plugin class {} from plugin {} implements deprecated method {} from plugin interface {}. " + "This method will be removed in a future release.", pluginClazz.getName(), descriptor.getName(), methodName, interfaceName ); } } } /** * Reloads all Lucene SPI implementations using the new classloader. * This method must be called after the new classloader has been created to * register the services for use. */ static void reloadLuceneSPI(ClassLoader loader) { // do NOT change the order of these method calls! // Codecs: PostingsFormat.reloadPostingsFormats(loader); DocValuesFormat.reloadDocValuesFormats(loader); Codec.reloadCodecs(loader); } private static Class loadPluginClass(String className, ClassLoader loader) { try { return Class.forName(className, false, loader).asSubclass(Plugin.class); } catch (ClassNotFoundException e) { throw new ElasticsearchException("Could not find plugin class [" + className + "]", e); } } // package-private for testing static Plugin loadPlugin(Class pluginClass, Settings settings, Path configPath) { final Constructor[] constructors = pluginClass.getConstructors(); if (constructors.length == 0) { throw new IllegalStateException("no public constructor for [" + pluginClass.getName() + "]"); } if (constructors.length > 1) { throw new IllegalStateException("no unique public constructor for [" + pluginClass.getName() + "]"); } final Constructor constructor = constructors[0]; if (constructor.getParameterCount() > 2) { throw new IllegalStateException(signatureMessage(pluginClass)); } final Class[] parameterTypes = constructor.getParameterTypes(); try { if (constructor.getParameterCount() == 2 && parameterTypes[0] == Settings.class && parameterTypes[1] == Path.class) { return (Plugin) constructor.newInstance(settings, configPath); } else if (constructor.getParameterCount() == 1 && parameterTypes[0] == Settings.class) { return (Plugin) constructor.newInstance(settings); } else if (constructor.getParameterCount() == 0) { return (Plugin) constructor.newInstance(); } else { throw new IllegalStateException(signatureMessage(pluginClass)); } } catch (final ReflectiveOperationException e) { throw new IllegalStateException("failed to load plugin class [" + pluginClass.getName() + "]", e); } } private static String signatureMessage(final Class clazz) { return String.format( Locale.ROOT, "no public constructor of correct signature for [%s]; must be [%s], [%s], or [%s]", clazz.getName(), "(org.elasticsearch.common.settings.Settings,java.nio.file.Path)", "(org.elasticsearch.common.settings.Settings)", "()" ); } @SuppressWarnings("unchecked") public final Stream filterPlugins(Class type) { return plugins().stream().filter(x -> type.isAssignableFrom(x.instance().getClass())).map(p -> ((T) p.instance())); } static LayerAndLoader createPluginModuleLayer( PluginBundle bundle, ClassLoader parentLoader, List parentLayers, Map> qualifiedExports ) { assert bundle.plugin.getModuleName().isPresent(); return createModuleLayer( bundle.plugin.getClassname(), bundle.plugin.getModuleName().get(), urlsToPaths(bundle.urls), parentLoader, parentLayers, qualifiedExports ); } static final LayerAndLoader createSpiModuleLayer( Set urls, ClassLoader parentLoader, List parentLayers, Map> qualifiedExports ) { // assert bundle.plugin.getModuleName().isPresent(); return createModuleLayer( null, // no entry point spiModuleName(urls), urlsToPaths(urls), parentLoader, parentLayers, qualifiedExports ); } private static final Module serverModule = PluginsService.class.getModule(); static LayerAndLoader createModuleLayer( String className, String moduleName, Path[] paths, ClassLoader parentLoader, List parentLayers, Map> qualifiedExports ) { logger.debug(() -> "Loading bundle: creating module layer and loader for module " + moduleName); var finder = ModuleFinder.of(paths); var configuration = Configuration.resolveAndBind( ModuleFinder.of(), parentConfigurationOrBoot(parentLayers), finder, Set.of(moduleName) ); var controller = privilegedDefineModulesWithOneLoader(configuration, parentLayersOrBoot(parentLayers), parentLoader); var pluginModule = controller.layer().findModule(moduleName).get(); ensureEntryPointAccessible(controller, pluginModule, className); // export/open upstream modules to this plugin module exposeQualifiedExportsAndOpens(pluginModule, qualifiedExports); // configure qualified exports/opens to other modules/plugins addPluginExportsServices(qualifiedExports, controller); logger.debug(() -> "Loading bundle: created module layer and loader for module " + moduleName); return new LayerAndLoader(controller.layer(), privilegedFindLoader(controller.layer(), moduleName)); } private static List parentLayersOrBoot(List parentLayers) { if (parentLayers == null || parentLayers.isEmpty()) { return List.of(ModuleLayer.boot()); } else { return parentLayers; } } private static List parentConfigurationOrBoot(List parentLayers) { if (parentLayers == null || parentLayers.isEmpty()) { return List.of(ModuleLayer.boot().configuration()); } else { return parentLayers.stream().map(ModuleLayer::configuration).toList(); } } /** Ensures that the plugins main class (its entry point), if any, is accessible to the server. */ private static void ensureEntryPointAccessible(Controller controller, Module pluginModule, String className) { if (className != null) { controller.addOpens(pluginModule, toPackageName(className), serverModule); } } protected void addServerExportsService(Map> qualifiedExports) { final Module serverModule = PluginsService.class.getModule(); var exportsService = new ModuleQualifiedExportsService(serverModule) { @Override protected void addExports(String pkg, Module target) { serverModule.addExports(pkg, target); } @Override protected void addOpens(String pkg, Module target) { serverModule.addOpens(pkg, target); } }; addExportsService(qualifiedExports, exportsService, serverModule.getName()); } private static void addPluginExportsServices(Map> qualifiedExports, Controller controller) { for (Module module : controller.layer().modules()) { var exportsService = new ModuleQualifiedExportsService(module) { @Override protected void addExports(String pkg, Module target) { controller.addExports(module, pkg, target); } @Override protected void addOpens(String pkg, Module target) { controller.addOpens(module, pkg, target); } }; addExportsService(qualifiedExports, exportsService, module.getName()); } } /** Determines the module name of the SPI module, given its URL. */ static String spiModuleName(Set spiURLS) { ModuleFinder finder = ModuleFinder.of(urlsToPaths(spiURLS)); var mrefs = finder.findAll(); assert mrefs.size() == 1 : "Expected a single module, got:" + mrefs; return mrefs.stream().findFirst().get().descriptor().name(); } /** * Tuple of module layer and loader. * Modular Plugins have a plugin specific loader and layer. * Non-Modular plugins have a plugin specific loader and the boot layer. */ record LayerAndLoader(ModuleLayer layer, ClassLoader loader) { LayerAndLoader { Objects.requireNonNull(layer); Objects.requireNonNull(loader); } static LayerAndLoader ofLoader(ClassLoader loader) { return new LayerAndLoader(ModuleLayer.boot(), loader); } } @SuppressForbidden(reason = "I need to convert URL's to Paths") static final Path[] urlsToPaths(Set urls) { return urls.stream().map(PluginsService::uncheckedToURI).map(PathUtils::get).toArray(Path[]::new); } static final URI uncheckedToURI(URL url) { try { return url.toURI(); } catch (URISyntaxException e) { throw new AssertionError(new IOException(e)); } } static final String toPackageName(String className) { assert className.endsWith(".") == false; int index = className.lastIndexOf('.'); if (index == -1) { throw new IllegalStateException("invalid class name:" + className); } return className.substring(0, index); } @SuppressWarnings("removal") private static void privilegedSetContextClassLoader(ClassLoader loader) { AccessController.doPrivileged((PrivilegedAction) () -> { Thread.currentThread().setContextClassLoader(loader); return null; }); } @SuppressWarnings("removal") static Controller privilegedDefineModulesWithOneLoader(Configuration cf, List parentLayers, ClassLoader parentLoader) { return AccessController.doPrivileged( (PrivilegedAction) () -> ModuleLayer.defineModulesWithOneLoader(cf, parentLayers, parentLoader) ); } @SuppressWarnings("removal") static ClassLoader privilegedFindLoader(ModuleLayer layer, String name) { return AccessController.doPrivileged((PrivilegedAction) () -> layer.findLoader(name)); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy