diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/InternalDistributionModuleCheckTaskProvider.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/InternalDistributionModuleCheckTaskProvider.java index b659f1efeece8..0a29741be8937 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/InternalDistributionModuleCheckTaskProvider.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/InternalDistributionModuleCheckTaskProvider.java @@ -54,6 +54,8 @@ public class InternalDistributionModuleCheckTaskProvider { "org.elasticsearch.geo", "org.elasticsearch.logging", "org.elasticsearch.lz4", + "org.elasticsearch.plugin.analysis.api", + "org.elasticsearch.plugin.api", "org.elasticsearch.pluginclassloader", "org.elasticsearch.securesm", "org.elasticsearch.server", diff --git a/docs/changelog/89969.yaml b/docs/changelog/89969.yaml new file mode 100644 index 0000000000000..513f516cc59a9 --- /dev/null +++ b/docs/changelog/89969.yaml @@ -0,0 +1,5 @@ +pr: 89969 +summary: "[Stable plugin API] Load plugin named components" +area: Infra/Plugins +type: enhancement +issues: [] diff --git a/libs/build.gradle b/libs/build.gradle index fbc946166fb83..f2b7e247fabd8 100644 --- a/libs/build.gradle +++ b/libs/build.gradle @@ -39,5 +39,5 @@ configure(subprojects - project('elasticsearch-log4j')) { } boolean isPluginApi(Project project, Project depProject) { - return project.path.matches(".*elasticsearch-plugin-.*-api") && depProject.path.equals(':libs:elasticsearch-plugin-api') + return project.path.matches(".*elasticsearch-plugin-.*api") } diff --git a/server/build.gradle b/server/build.gradle index a082e30afbba5..9e1985d5643c5 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -30,6 +30,8 @@ dependencies { api project(':libs:elasticsearch-x-content') api project(":libs:elasticsearch-geo") api project(":libs:elasticsearch-lz4") + api project(":libs:elasticsearch-plugin-api") + api project(":libs:elasticsearch-plugin-analysis-api") implementation project(':libs:elasticsearch-plugin-classloader') diff --git a/server/src/main/java/module-info.java b/server/src/main/java/module-info.java index 28391730f7e14..61b28a509d730 100644 --- a/server/src/main/java/module-info.java +++ b/server/src/main/java/module-info.java @@ -22,6 +22,8 @@ requires org.elasticsearch.securesm; requires org.elasticsearch.xcontent; requires org.elasticsearch.logging; + requires org.elasticsearch.plugin.api; + requires org.elasticsearch.plugin.analysis.api; requires com.sun.jna; requires hppc; diff --git a/server/src/main/java/org/elasticsearch/plugins/PluginBundle.java b/server/src/main/java/org/elasticsearch/plugins/PluginBundle.java index cd454015a7f9f..154ffce6ba05f 100644 --- a/server/src/main/java/org/elasticsearch/plugins/PluginBundle.java +++ b/server/src/main/java/org/elasticsearch/plugins/PluginBundle.java @@ -20,14 +20,16 @@ /** * A "bundle" is a group of jars that will be loaded in their own classloader */ -class PluginBundle { +public class PluginBundle { public final PluginDescriptor plugin; + private final Path dir; public final Set urls; public final Set spiUrls; public final Set allUrls; PluginBundle(PluginDescriptor plugin, Path dir) throws IOException { this.plugin = Objects.requireNonNull(plugin); + this.dir = dir; Path spiDir = dir.resolve("spi"); // plugin has defined an explicit api for extension @@ -40,6 +42,10 @@ class PluginBundle { this.allUrls = allUrls; } + public Path getDir() { + return dir; + } + public PluginDescriptor pluginDescriptor() { return this.plugin; } @@ -82,4 +88,5 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(plugin); } + } diff --git a/server/src/main/java/org/elasticsearch/plugins/scanners/ExtensibleFileReader.java b/server/src/main/java/org/elasticsearch/plugins/scanners/ExtensibleFileReader.java new file mode 100644 index 0000000000000..558dd723266ca --- /dev/null +++ b/server/src/main/java/org/elasticsearch/plugins/scanners/ExtensibleFileReader.java @@ -0,0 +1,48 @@ +/* + * 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.scanners; + +import org.elasticsearch.logging.LogManager; +import org.elasticsearch.logging.Logger; +import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.XContentParserConfiguration; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import static org.elasticsearch.xcontent.XContentType.JSON; + +public class ExtensibleFileReader { + private static final Logger logger = LogManager.getLogger(ExtensibleFileReader.class); + + private String extensibleFile; + + public ExtensibleFileReader(String extensibleFile) { + this.extensibleFile = extensibleFile; + } + + public Map readFromFile() { + Map res = new HashMap<>(); + // todo should it be BufferedInputStream ? + try (InputStream in = getClass().getResourceAsStream(extensibleFile)) { + if (in != null) { + try (XContentParser parser = JSON.xContent().createParser(XContentParserConfiguration.EMPTY, in)) { + // TODO should we validate the classes actually exist? + return parser.mapStrings(); + } + } + } catch (IOException e) { + logger.error("failed reading extensible file", e); + } + return res; + } + +} diff --git a/server/src/main/java/org/elasticsearch/plugins/scanners/ExtensiblesRegistry.java b/server/src/main/java/org/elasticsearch/plugins/scanners/ExtensiblesRegistry.java new file mode 100644 index 0000000000000..dc40bd6fcd8e2 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/plugins/scanners/ExtensiblesRegistry.java @@ -0,0 +1,86 @@ +/* + * 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.scanners; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.Map; + +import static org.elasticsearch.core.Strings.format; + +/** + * A registry of Extensible interfaces/classes read from extensibles.json file. + * The file is generated during Elasticsearch built time (or commited) + * basing on the classes declared in stable plugins api (i.e. plugin-analysis-api) + * + * This file is present in server jar. + * a class/interface is directly extensible when is marked with @Extensible annotation + * a class/interface can be indirectly extensible when it extends/implements a directly extensible class + * + * Information about extensible interfaces/classes are stored in a map where: + * key and value are the same cannonical name of the class that is directly marked with @Extensible + * or + * key: a cannonical name of the class that is indirectly extensible but extends another extensible class (directly/indirectly) + * value: cannonical name of the class that is directly extensible + * + * The reason for indirectly extensible classes is to allow stable plugin apis to create hierarchies + * + * Example: + *
+ * @Extensible
+ * interface E{
+ *     public void foo();
+ * }
+ * interface Eprim extends E{
+ * }
+ *
+ * class Aclass implements E{
+ *
+ * }
+ *
+ * @Extensible
+ * class E2 {
+ *     public void bar(){}
+ * }
+ * 
+ * the content of extensibles.json should be + * { + * "E" : "E", + * "Eprim" : "E", + * "A" : "E", + * "E2" : "E2" + * } + * + * @see org.elasticsearch.plugin.api.Extensible + */ +public class ExtensiblesRegistry { + + private static final Logger logger = LogManager.getLogger(ExtensiblesRegistry.class); + + private static final String EXTENSIBLES_FILE = "/org/elasticsearch/plugins/scanners/extensibles.json"; + public static final ExtensiblesRegistry INSTANCE = new ExtensiblesRegistry(EXTENSIBLES_FILE); + + // classname (potentially extending/implementing extensible) to interface/class annotated with extensible + private final Map loadedExtensible; + + ExtensiblesRegistry(String extensiblesFile) { + ExtensibleFileReader extensibleFileReader = new ExtensibleFileReader(extensiblesFile); + + this.loadedExtensible = extensibleFileReader.readFromFile(); + if (loadedExtensible.size() > 0) { + logger.debug(() -> format("Loaded extensible from cache file %s", loadedExtensible)); + } + } + + public boolean hasExtensible(String extensibleName) { + return loadedExtensible.containsKey(extensibleName); + } + +} diff --git a/server/src/main/java/org/elasticsearch/plugins/scanners/NameToPluginInfo.java b/server/src/main/java/org/elasticsearch/plugins/scanners/NameToPluginInfo.java new file mode 100644 index 0000000000000..b90b704b784f7 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/plugins/scanners/NameToPluginInfo.java @@ -0,0 +1,38 @@ +/* + * 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.scanners; + +import java.util.HashMap; +import java.util.Map; + +public record NameToPluginInfo(Map nameToPluginInfoMap) { + + public NameToPluginInfo() { + this(new HashMap<>()); + } + + public NameToPluginInfo put(String name, PluginInfo pluginInfo) { + nameToPluginInfoMap.put(name, pluginInfo); + return this; + } + + public void putAll(Map namedPluginInfoMap) { + this.nameToPluginInfoMap.putAll(namedPluginInfoMap); + } + + public NameToPluginInfo put(NameToPluginInfo nameToPluginInfo) { + putAll(nameToPluginInfo.nameToPluginInfoMap); + return this; + } + + public PluginInfo getForPluginName(String pluginName) { + return nameToPluginInfoMap.get(pluginName); + } + +} diff --git a/server/src/main/java/org/elasticsearch/plugins/scanners/NamedComponentReader.java b/server/src/main/java/org/elasticsearch/plugins/scanners/NamedComponentReader.java new file mode 100644 index 0000000000000..609a7b104f271 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/plugins/scanners/NamedComponentReader.java @@ -0,0 +1,111 @@ +/* + * 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.scanners; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.core.Strings; +import org.elasticsearch.plugins.PluginBundle; +import org.elasticsearch.xcontent.XContentParserConfiguration; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +import static java.util.Collections.emptyMap; +import static org.elasticsearch.xcontent.XContentType.JSON; + +/** + * Reads named components declared by a plugin in a cache file. + * Cache file is expected to be present in plugin's lib directory + *

+ * The content of a cache file is a JSON representation of a map where: + * keys -> name of the extensible interface (a class/interface marked with @Extensible) + * values -> a map of name to implementation class name + */ +public class NamedComponentReader { + + private Logger logger = LogManager.getLogger(NamedComponentReader.class); + private static final String NAMED_COMPONENTS_FILE_NAME = "named_components.json"; + /** + * a registry of known classes marked or indirectly marked (extending marked class) with @Extensible + */ + private final ExtensiblesRegistry extensiblesRegistry; + + public NamedComponentReader() { + this(ExtensiblesRegistry.INSTANCE); + } + + NamedComponentReader(ExtensiblesRegistry extensiblesRegistry) { + this.extensiblesRegistry = extensiblesRegistry; + } + + public Map findNamedComponents(PluginBundle bundle, ClassLoader pluginClassLoader) { + Path pluginDir = bundle.getDir(); + return findNamedComponents(pluginDir, pluginClassLoader); + } + + // scope for testing + Map findNamedComponents(Path pluginDir, ClassLoader pluginClassLoader) { + try { + Path namedComponent = findNamedComponentCacheFile(pluginDir); + if (namedComponent != null) { + Map namedComponents = readFromFile(namedComponent, pluginClassLoader); + logger.debug(() -> Strings.format("Plugin in dir %s declared named components %s.", pluginDir, namedComponents)); + + return namedComponents; + } + logger.debug(() -> Strings.format("No named component defined in plugin dir %s", pluginDir)); + } catch (IOException e) { + logger.error("unable to read named components", e); + } + return emptyMap(); + } + + private Path findNamedComponentCacheFile(Path pluginDir) throws IOException { + try (Stream list = Files.list(pluginDir)) { + return list.filter(p -> p.getFileName().toString().equals(NAMED_COMPONENTS_FILE_NAME)).findFirst().orElse(null); + } + } + + @SuppressWarnings("unchecked") + Map readFromFile(Path namedComponent, ClassLoader pluginClassLoader) throws IOException { + Map res = new HashMap<>(); + + try ( + var json = new BufferedInputStream(Files.newInputStream(namedComponent)); + var parser = JSON.xContent().createParser(XContentParserConfiguration.EMPTY, json) + ) { + Map map = parser.map(); + for (Map.Entry fileAsMap : map.entrySet()) { + String extensibleInterface = fileAsMap.getKey(); + validateExtensible(extensibleInterface); + Map components = (Map) fileAsMap.getValue(); + for (Map.Entry nameToComponent : components.entrySet()) { + String name = nameToComponent.getKey(); + String value = (String) nameToComponent.getValue(); + + res.computeIfAbsent(extensibleInterface, k -> new NameToPluginInfo()) + .put(name, new PluginInfo(name, value, pluginClassLoader)); + } + } + } + return res; + } + + private void validateExtensible(String extensibleInterface) { + if (extensiblesRegistry.hasExtensible(extensibleInterface) == false) { + throw new IllegalStateException("Unknown extensible name " + extensibleInterface); + } + } +} diff --git a/server/src/main/java/org/elasticsearch/plugins/scanners/PluginInfo.java b/server/src/main/java/org/elasticsearch/plugins/scanners/PluginInfo.java new file mode 100644 index 0000000000000..0a4e84e244890 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/plugins/scanners/PluginInfo.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.scanners; + +record PluginInfo(String name, String className, ClassLoader loader) { + +} diff --git a/server/src/main/java/org/elasticsearch/plugins/scanners/StablePluginsRegistry.java b/server/src/main/java/org/elasticsearch/plugins/scanners/StablePluginsRegistry.java new file mode 100644 index 0000000000000..1ab1bf825a372 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/plugins/scanners/StablePluginsRegistry.java @@ -0,0 +1,56 @@ +/* + * 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.scanners; + +import org.elasticsearch.plugins.PluginBundle; + +import java.util.HashMap; +import java.util.Map; + +/** + * A registry of classes declared by plugins as named components. + * Named components are classes annotated with @NamedComponent(name) and can be referred later by a name given in this annotation. + * Named components implement/extend Extensibles (classes/interfaces marked with @Extensible) + */ +public class StablePluginsRegistry { + + /* + A map of an interface/class name marked (effectively) with @Extensible to NameToPluginInfo map + effectively means that an interface which extends another interface marked with @Extensible is also extensible + NameToPluginInfo map is a map of Name to PluginInfo + i.e. + org.elasticsearch.plugin.analysis.api.TokenFilterFactory -> + {"nori" -> {nori, org.elasticserach.plugin.analysis.new_nori.NoriReadingFormFilterFactory, classloaderInstance} + */ + private final Map namedComponents; + private final NamedComponentReader namedComponentsScanner; + + public StablePluginsRegistry() { + this(new NamedComponentReader(), new HashMap<>()); + } + + // for testing + StablePluginsRegistry(NamedComponentReader namedComponentReader, HashMap namedComponents) { + this.namedComponentsScanner = namedComponentReader; + this.namedComponents = namedComponents; + } + + public void scanBundleForStablePlugins(PluginBundle bundle, ClassLoader pluginClassLoader) { + Map namedComponentsFromPlugin = namedComponentsScanner.findNamedComponents(bundle, pluginClassLoader); + for (Map.Entry entry : namedComponentsFromPlugin.entrySet()) { + namedComponents.compute(entry.getKey(), (k, v) -> v != null ? v.put(entry.getValue()) : entry.getValue()); + } + } + + // TODO this will be removed. getPluginForName or similar will be created + public Map getNamedComponents() { + return namedComponents; + } + +} diff --git a/server/src/main/resources/org/elasticsearch/plugins/scanners/extensibles.json b/server/src/main/resources/org/elasticsearch/plugins/scanners/extensibles.json new file mode 100644 index 0000000000000..64d123055df92 --- /dev/null +++ b/server/src/main/resources/org/elasticsearch/plugins/scanners/extensibles.json @@ -0,0 +1,6 @@ +{ + "org.elasticsearch.plugin.analysis.api.AnalyzerFactory":"org.elasticsearch.plugin.analysis.api.AnalyzerFactory", + "org.elasticsearch.plugin.analysis.api.CharFilterFactory":"org.elasticsearch.plugin.analysis.api.CharFilterFactory", + "org.elasticsearch.plugin.analysis.api.TokenFilterFactory":"org.elasticsearch.plugin.analysis.api.TokenFilterFactory", + "org.elasticsearch.plugin.analysis.api.TokenizerFactory":"org.elasticsearch.plugin.analysis.api.TokenizerFactory" +} diff --git a/server/src/test/java/org/elasticsearch/plugins/scanners/ExtensibleFileReaderTests.java b/server/src/test/java/org/elasticsearch/plugins/scanners/ExtensibleFileReaderTests.java new file mode 100644 index 0000000000000..c177b2474df48 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/plugins/scanners/ExtensibleFileReaderTests.java @@ -0,0 +1,37 @@ +/* + * 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.scanners; + +import org.elasticsearch.test.ESTestCase; + +import java.util.Map; + +import static org.hamcrest.Matchers.equalTo; + +public class ExtensibleFileReaderTests extends ESTestCase { + + public void testLoadingFromFile() { + ExtensibleFileReader extensibleFileReader = new ExtensibleFileReader("/test_extensible.json"); + + Map stringStringMap = extensibleFileReader.readFromFile(); + assertThat( + stringStringMap, + equalTo( + Map.of( + "org.elasticsearch.plugins.scanners.extensible_test_classes.ExtensibleClass", + "org.elasticsearch.plugins.scanners.extensible_test_classes.ExtensibleClass", + "org.elasticsearch.plugins.scanners.extensible_test_classes.SubClass", + "org.elasticsearch.plugins.scanners.extensible_test_classes.ExtensibleClass", + "org.elasticsearch.plugins.scanners.extensible_test_classes.ExtensibleInterface", + "org.elasticsearch.plugins.scanners.extensible_test_classes.ExtensibleInterface" + ) + ) + ); + } +} diff --git a/server/src/test/java/org/elasticsearch/plugins/scanners/NamedComponentReaderTests.java b/server/src/test/java/org/elasticsearch/plugins/scanners/NamedComponentReaderTests.java new file mode 100644 index 0000000000000..428f0239d97ff --- /dev/null +++ b/server/src/test/java/org/elasticsearch/plugins/scanners/NamedComponentReaderTests.java @@ -0,0 +1,105 @@ +/* + * 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.scanners; + +import org.elasticsearch.core.PathUtils; +import org.elasticsearch.core.SuppressForbidden; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +import static org.hamcrest.Matchers.equalTo; + +public class NamedComponentReaderTests extends ESTestCase { + ExtensiblesRegistry extensiblesRegistry = new ExtensiblesRegistry("/test_extensible.json"); + NamedComponentReader namedComponentReader = new NamedComponentReader(extensiblesRegistry); + + @SuppressForbidden(reason = "test resource") + public void testReadNamedComponentsFromFile() throws IOException { + final String resource = this.getClass().getClassLoader().getResource("named_components.json").getPath(); + Path namedComponentPath = PathUtils.get(resource); + + Map namedComponents = namedComponentReader.readFromFile( + namedComponentPath, + NamedComponentReaderTests.class.getClassLoader() + ); + + assertThat( + namedComponents.get("org.elasticsearch.plugins.scanners.extensible_test_classes.ExtensibleInterface") + .getForPluginName("test_named_component"), + equalTo( + new PluginInfo( + "test_named_component", + "org.elasticsearch.plugins.scanners.named_components_test_classes.TestNamedComponent", + NamedComponentReaderTests.class.getClassLoader() + ) + ) + ); + } + + public void testUnknownExtensible() throws IOException { + final Path tmp = createTempDir(); + final Path pluginDir = tmp.resolve("plugin-dir"); + Files.createDirectories(pluginDir); + Path namedComponentFile = pluginDir.resolve("named_components.json"); + Files.writeString(namedComponentFile, """ + { + "org.elasticsearch.plugins.scanners.extensible_test_classes.UnknownExtensible": { + "a_component": "p.A", + "b_component": "p.B" + } + } + """); + + ClassLoader classLoader = NamedComponentReaderTests.class.getClassLoader(); + expectThrows(IllegalStateException.class, () -> namedComponentReader.findNamedComponents(pluginDir, classLoader)); + } + + public void testFindNamedComponentInJarWithNamedComponentscacheFile() throws IOException { + final Path tmp = createTempDir(); + final Path pluginDir = tmp.resolve("plugin-dir"); + Files.createDirectories(pluginDir); + Path namedComponentFile = pluginDir.resolve("named_components.json"); + Files.writeString(namedComponentFile, """ + { + "org.elasticsearch.plugins.scanners.extensible_test_classes.ExtensibleInterface": { + "a_component": "p.A", + "b_component": "p.B" + } + } + """); + + ClassLoader classLoader = NamedComponentReaderTests.class.getClassLoader(); + Map namedComponents = namedComponentReader.findNamedComponents(pluginDir, classLoader); + + assertThat( + namedComponents.get("org.elasticsearch.plugins.scanners.extensible_test_classes.ExtensibleInterface") + .getForPluginName("b_component"), + equalTo(new PluginInfo("b_component", "p.B", classLoader)) + ); + assertThat( + namedComponents.get("org.elasticsearch.plugins.scanners.extensible_test_classes.ExtensibleInterface") + .getForPluginName("a_component"), + equalTo(new PluginInfo("a_component", "p.A", classLoader)) + ); + } + + private URL toURL(Path p) { + try { + return p.toUri().toURL(); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } +} diff --git a/server/src/test/java/org/elasticsearch/plugins/scanners/StablePluginsRegistryTests.java b/server/src/test/java/org/elasticsearch/plugins/scanners/StablePluginsRegistryTests.java new file mode 100644 index 0000000000000..38b1e955cbc6c --- /dev/null +++ b/server/src/test/java/org/elasticsearch/plugins/scanners/StablePluginsRegistryTests.java @@ -0,0 +1,64 @@ +/* + * 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.scanners; + +import org.elasticsearch.plugins.PluginBundle; +import org.elasticsearch.test.ESTestCase; +import org.hamcrest.Matchers; +import org.mockito.Mockito; + +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; + +public class StablePluginsRegistryTests extends ESTestCase { + + public void testAddingNamedComponentsFromMultiplePlugins() { + NamedComponentReader scanner = Mockito.mock(NamedComponentReader.class); + ClassLoader loader = Mockito.mock(ClassLoader.class); + ClassLoader loader2 = Mockito.mock(ClassLoader.class); + + NameToPluginInfo pluginInfo1 = new NameToPluginInfo().put( + "namedComponentName1", + new PluginInfo("namedComponentName1", "XXClassName", loader) + ); + NameToPluginInfo pluginInfo2 = new NameToPluginInfo().put( + "namedComponentName2", + new PluginInfo("namedComponentName2", "YYClassName", loader) + ); + NameToPluginInfo pluginInfo3 = new NameToPluginInfo().put( + "namedComponentName3", + new PluginInfo("namedComponentName3", "ZZClassName", loader2) + ); + + Mockito.when(scanner.findNamedComponents(any(PluginBundle.class), any(ClassLoader.class))) + .thenReturn(Map.of("extensibleInterfaceName", pluginInfo1)) + .thenReturn(Map.of("extensibleInterfaceName", pluginInfo2)) + .thenReturn(Map.of("extensibleInterfaceName2", pluginInfo3)); + + StablePluginsRegistry registry = new StablePluginsRegistry(scanner, new HashMap<>()); + registry.scanBundleForStablePlugins(Mockito.mock(PluginBundle.class), loader); // bundle 1 + registry.scanBundleForStablePlugins(Mockito.mock(PluginBundle.class), loader); // bundle 2 + registry.scanBundleForStablePlugins(Mockito.mock(PluginBundle.class), loader2); // bundle 3 + + assertThat( + registry.getNamedComponents(), + Matchers.equalTo( + Map.of( + "extensibleInterfaceName", + new NameToPluginInfo().put("namedComponentName1", new PluginInfo("namedComponentName1", "XXClassName", loader)) + .put("namedComponentName2", new PluginInfo("namedComponentName2", "YYClassName", loader)), + "extensibleInterfaceName2", + new NameToPluginInfo().put("namedComponentName3", new PluginInfo("namedComponentName3", "ZZClassName", loader2)) + ) + ) + ); + } +} diff --git a/server/src/test/resources/named_components.json b/server/src/test/resources/named_components.json new file mode 100644 index 0000000000000..087f2410dc097 --- /dev/null +++ b/server/src/test/resources/named_components.json @@ -0,0 +1,6 @@ +{ + "org.elasticsearch.plugins.scanners.extensible_test_classes.ExtensibleInterface": { + "test_named_component": "org.elasticsearch.plugins.scanners.named_components_test_classes.TestNamedComponent", + "test_named_component2": "org.elasticsearch.plugins.scanners.named_components_test_classes.TestNamedComponent2" + } +} diff --git a/server/src/test/resources/test_extensible.json b/server/src/test/resources/test_extensible.json new file mode 100644 index 0000000000000..0910cbadfd3a2 --- /dev/null +++ b/server/src/test/resources/test_extensible.json @@ -0,0 +1,8 @@ +{ + "org.elasticsearch.plugins.scanners.extensible_test_classes.ExtensibleClass": + "org.elasticsearch.plugins.scanners.extensible_test_classes.ExtensibleClass", + "org.elasticsearch.plugins.scanners.extensible_test_classes.SubClass": + "org.elasticsearch.plugins.scanners.extensible_test_classes.ExtensibleClass", + "org.elasticsearch.plugins.scanners.extensible_test_classes.ExtensibleInterface" : + "org.elasticsearch.plugins.scanners.extensible_test_classes.ExtensibleInterface" +}