diff --git a/server/src/main/java/org/elasticsearch/plugins/PluginsService.java b/server/src/main/java/org/elasticsearch/plugins/PluginsService.java index 59b39fb091ce9..058885de79f81 100644 --- a/server/src/main/java/org/elasticsearch/plugins/PluginsService.java +++ b/server/src/main/java/org/elasticsearch/plugins/PluginsService.java @@ -52,6 +52,7 @@ 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; @@ -279,7 +280,6 @@ private Map loadBundles(Set bundles) { // 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()))); @@ -293,6 +293,28 @@ static void loadExtensions(Collection plugins) { } } + /** + * 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()) { + ServiceLoader.load(service, pluginTuple.loader()).iterator().forEachRemaining(c -> result.add(c)); + } + + return Collections.unmodifiableList(result); + } + private static void loadExtensionsForPlugin(ExtensiblePlugin extensiblePlugin, List extendingPlugins) { ExtensiblePlugin.ExtensionLoader extensionLoader = new ExtensiblePlugin.ExtensionLoader() { @Override diff --git a/server/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java b/server/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java index 7a9fc4398582b..e9960f22ff3d1 100644 --- a/server/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java +++ b/server/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java @@ -12,14 +12,21 @@ import org.apache.lucene.util.Constants; import org.elasticsearch.Version; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.Strings; import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.index.IndexModule; +import org.elasticsearch.plugins.spi.TestService; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.compiler.InMemoryJavaCompiler; +import org.elasticsearch.test.jar.JarUtils; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; import java.nio.file.FileSystemException; import java.nio.file.Files; import java.nio.file.NoSuchFileException; @@ -27,8 +34,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -613,6 +622,70 @@ public void testThrowingConstructor() { assertThat(e.getCause().getCause(), hasToString(containsString("test constructor failure"))); } + private ClassLoader buildTestProviderPlugin(String name) throws Exception { + Map sources = Map.of("r.FooPlugin", """ + package r; + import org.elasticsearch.plugins.ActionPlugin; + import org.elasticsearch.plugins.Plugin; + public final class FooPlugin extends Plugin implements ActionPlugin { } + """, "r.FooTestService", Strings.format(""" + package r; + import org.elasticsearch.plugins.spi.TestService; + public final class FooTestService implements TestService { + @Override + public String name() { + return "%s"; + } + } + """, name)); + + var classToBytes = InMemoryJavaCompiler.compile(sources); + + Map jarEntries = new HashMap<>(); + jarEntries.put("r/FooPlugin.class", classToBytes.get("r.FooPlugin")); + jarEntries.put("r/FooTestService.class", classToBytes.get("r.FooTestService")); + jarEntries.put("META-INF/services/org.elasticsearch.plugins.spi.TestService", "r.FooTestService".getBytes(StandardCharsets.UTF_8)); + + Path topLevelDir = createTempDir(getTestName()); + Path jar = topLevelDir.resolve(Strings.format("custom_plugin_%s.jar", name)); + JarUtils.createJarWithEntries(jar, jarEntries); + URL[] urls = new URL[] { jar.toUri().toURL() }; + + URLClassLoader loader = URLClassLoader.newInstance(urls, this.getClass().getClassLoader()); + return loader; + } + + public void testLoadServiceProviders() throws Exception { + ClassLoader fakeClassLoader = buildTestProviderPlugin("integer"); + @SuppressWarnings("unchecked") + Class fakePluginClass = (Class) fakeClassLoader.loadClass("r.FooPlugin"); + + ClassLoader fakeClassLoader1 = buildTestProviderPlugin("string"); + @SuppressWarnings("unchecked") + Class fakePluginClass1 = (Class) fakeClassLoader1.loadClass("r.FooPlugin"); + + assertFalse(fakePluginClass.getClassLoader().equals(fakePluginClass1.getClassLoader())); + + getClass().getModule().addUses(TestService.class); + + PluginsService service = newMockPluginsService(List.of(fakePluginClass, fakePluginClass1)); + + List providers = service.loadServiceProviders(TestService.class); + assertEquals(2, providers.size()); + assertThat(providers.stream().map(p -> p.name()).toList(), containsInAnyOrder("string", "integer")); + + service = newMockPluginsService(List.of(fakePluginClass)); + providers = service.loadServiceProviders(TestService.class); + + assertEquals(1, providers.size()); + assertThat(providers.stream().map(p -> p.name()).toList(), containsInAnyOrder("integer")); + + service = newMockPluginsService(new ArrayList<>()); + providers = service.loadServiceProviders(TestService.class); + + assertEquals(0, providers.size()); + } + private static class TestExtensiblePlugin extends Plugin implements ExtensiblePlugin { private List extensions; diff --git a/server/src/test/java/org/elasticsearch/plugins/spi/TestService.java b/server/src/test/java/org/elasticsearch/plugins/spi/TestService.java new file mode 100644 index 0000000000000..1156ebc1f809a --- /dev/null +++ b/server/src/test/java/org/elasticsearch/plugins/spi/TestService.java @@ -0,0 +1,13 @@ +/* + * 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.spi; + +public interface TestService { + String name(); +} diff --git a/test/framework/src/main/java/org/elasticsearch/plugins/MockPluginsService.java b/test/framework/src/main/java/org/elasticsearch/plugins/MockPluginsService.java index 6c78faa65b7d3..60c10f430c911 100644 --- a/test/framework/src/main/java/org/elasticsearch/plugins/MockPluginsService.java +++ b/test/framework/src/main/java/org/elasticsearch/plugins/MockPluginsService.java @@ -58,7 +58,7 @@ public MockPluginsService(Settings settings, Environment environment, Collection if (logger.isTraceEnabled()) { logger.trace("plugin loaded from classpath [{}]", pluginInfo); } - pluginsLoaded.add(new LoadedPlugin(pluginInfo, plugin)); + pluginsLoaded.add(new LoadedPlugin(pluginInfo, plugin, pluginClass.getClassLoader(), ModuleLayer.boot())); } this.classpathPlugins = List.copyOf(pluginsLoaded);