Skip to content

Commit 35ea2b1

Browse files
authored
[Stable plugin API] Load plugin named components (#89969)
Stable plugins are using @ extensible and @ NamedComponents annotations to mark components to be loaded. This commit is loading extensible classNames from extensibles.json and named components from named_components.json The scanning mechanism that can generate these files will be done later in a gradle plugin/plugin installer relates #88980
1 parent 9056ff7 commit 35ea2b1

File tree

18 files changed

+598
-2
lines changed

18 files changed

+598
-2
lines changed

build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/InternalDistributionModuleCheckTaskProvider.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ public class InternalDistributionModuleCheckTaskProvider {
5454
"org.elasticsearch.geo",
5555
"org.elasticsearch.logging",
5656
"org.elasticsearch.lz4",
57+
"org.elasticsearch.plugin.analysis.api",
58+
"org.elasticsearch.plugin.api",
5759
"org.elasticsearch.pluginclassloader",
5860
"org.elasticsearch.securesm",
5961
"org.elasticsearch.server",

docs/changelog/89969.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 89969
2+
summary: "[Stable plugin API] Load plugin named components"
3+
area: Infra/Plugins
4+
type: enhancement
5+
issues: []

libs/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,5 @@ configure(subprojects - project('elasticsearch-log4j')) {
3939
}
4040

4141
boolean isPluginApi(Project project, Project depProject) {
42-
return project.path.matches(".*elasticsearch-plugin-.*-api") && depProject.path.equals(':libs:elasticsearch-plugin-api')
42+
return project.path.matches(".*elasticsearch-plugin-.*api")
4343
}

server/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ dependencies {
3030
api project(':libs:elasticsearch-x-content')
3131
api project(":libs:elasticsearch-geo")
3232
api project(":libs:elasticsearch-lz4")
33+
api project(":libs:elasticsearch-plugin-api")
34+
api project(":libs:elasticsearch-plugin-analysis-api")
3335

3436
implementation project(':libs:elasticsearch-plugin-classloader')
3537

server/src/main/java/module-info.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
requires org.elasticsearch.securesm;
2323
requires org.elasticsearch.xcontent;
2424
requires org.elasticsearch.logging;
25+
requires org.elasticsearch.plugin.api;
26+
requires org.elasticsearch.plugin.analysis.api;
2527

2628
requires com.sun.jna;
2729
requires hppc;

server/src/main/java/org/elasticsearch/plugins/PluginBundle.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,16 @@
2020
/**
2121
* A "bundle" is a group of jars that will be loaded in their own classloader
2222
*/
23-
class PluginBundle {
23+
public class PluginBundle {
2424
public final PluginDescriptor plugin;
25+
private final Path dir;
2526
public final Set<URL> urls;
2627
public final Set<URL> spiUrls;
2728
public final Set<URL> allUrls;
2829

2930
PluginBundle(PluginDescriptor plugin, Path dir) throws IOException {
3031
this.plugin = Objects.requireNonNull(plugin);
32+
this.dir = dir;
3133

3234
Path spiDir = dir.resolve("spi");
3335
// plugin has defined an explicit api for extension
@@ -40,6 +42,10 @@ class PluginBundle {
4042
this.allUrls = allUrls;
4143
}
4244

45+
public Path getDir() {
46+
return dir;
47+
}
48+
4349
public PluginDescriptor pluginDescriptor() {
4450
return this.plugin;
4551
}
@@ -82,4 +88,5 @@ public boolean equals(Object o) {
8288
public int hashCode() {
8389
return Objects.hash(plugin);
8490
}
91+
8592
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.plugins.scanners;
10+
11+
import org.elasticsearch.logging.LogManager;
12+
import org.elasticsearch.logging.Logger;
13+
import org.elasticsearch.xcontent.XContentParser;
14+
import org.elasticsearch.xcontent.XContentParserConfiguration;
15+
16+
import java.io.IOException;
17+
import java.io.InputStream;
18+
import java.util.HashMap;
19+
import java.util.Map;
20+
21+
import static org.elasticsearch.xcontent.XContentType.JSON;
22+
23+
public class ExtensibleFileReader {
24+
private static final Logger logger = LogManager.getLogger(ExtensibleFileReader.class);
25+
26+
private String extensibleFile;
27+
28+
public ExtensibleFileReader(String extensibleFile) {
29+
this.extensibleFile = extensibleFile;
30+
}
31+
32+
public Map<String, String> readFromFile() {
33+
Map<String, String> res = new HashMap<>();
34+
// todo should it be BufferedInputStream ?
35+
try (InputStream in = getClass().getResourceAsStream(extensibleFile)) {
36+
if (in != null) {
37+
try (XContentParser parser = JSON.xContent().createParser(XContentParserConfiguration.EMPTY, in)) {
38+
// TODO should we validate the classes actually exist?
39+
return parser.mapStrings();
40+
}
41+
}
42+
} catch (IOException e) {
43+
logger.error("failed reading extensible file", e);
44+
}
45+
return res;
46+
}
47+
48+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.plugins.scanners;
10+
11+
import org.apache.logging.log4j.LogManager;
12+
import org.apache.logging.log4j.Logger;
13+
14+
import java.util.Map;
15+
16+
import static org.elasticsearch.core.Strings.format;
17+
18+
/**
19+
* A registry of Extensible interfaces/classes read from extensibles.json file.
20+
* The file is generated during Elasticsearch built time (or commited)
21+
* basing on the classes declared in stable plugins api (i.e. plugin-analysis-api)
22+
*
23+
* This file is present in server jar.
24+
* a class/interface is directly extensible when is marked with @Extensible annotation
25+
* a class/interface can be indirectly extensible when it extends/implements a directly extensible class
26+
*
27+
* Information about extensible interfaces/classes are stored in a map where:
28+
* key and value are the same cannonical name of the class that is directly marked with @Extensible
29+
* or
30+
* key: a cannonical name of the class that is indirectly extensible but extends another extensible class (directly/indirectly)
31+
* value: cannonical name of the class that is directly extensible
32+
*
33+
* The reason for indirectly extensible classes is to allow stable plugin apis to create hierarchies
34+
*
35+
* Example:
36+
* <pre>
37+
* &#64;Extensible
38+
* interface E{
39+
* public void foo();
40+
* }
41+
* interface Eprim extends E{
42+
* }
43+
*
44+
* class Aclass implements E{
45+
*
46+
* }
47+
*
48+
* &#64;Extensible
49+
* class E2 {
50+
* public void bar(){}
51+
* }
52+
* </pre>
53+
* the content of extensibles.json should be
54+
* {
55+
* "E" : "E",
56+
* "Eprim" : "E",
57+
* "A" : "E",
58+
* "E2" : "E2"
59+
* }
60+
*
61+
* @see org.elasticsearch.plugin.api.Extensible
62+
*/
63+
public class ExtensiblesRegistry {
64+
65+
private static final Logger logger = LogManager.getLogger(ExtensiblesRegistry.class);
66+
67+
private static final String EXTENSIBLES_FILE = "/org/elasticsearch/plugins/scanners/extensibles.json";
68+
public static final ExtensiblesRegistry INSTANCE = new ExtensiblesRegistry(EXTENSIBLES_FILE);
69+
70+
// classname (potentially extending/implementing extensible) to interface/class annotated with extensible
71+
private final Map<String, String> loadedExtensible;
72+
73+
ExtensiblesRegistry(String extensiblesFile) {
74+
ExtensibleFileReader extensibleFileReader = new ExtensibleFileReader(extensiblesFile);
75+
76+
this.loadedExtensible = extensibleFileReader.readFromFile();
77+
if (loadedExtensible.size() > 0) {
78+
logger.debug(() -> format("Loaded extensible from cache file %s", loadedExtensible));
79+
}
80+
}
81+
82+
public boolean hasExtensible(String extensibleName) {
83+
return loadedExtensible.containsKey(extensibleName);
84+
}
85+
86+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.plugins.scanners;
10+
11+
import java.util.HashMap;
12+
import java.util.Map;
13+
14+
public record NameToPluginInfo(Map<String, PluginInfo> nameToPluginInfoMap) {
15+
16+
public NameToPluginInfo() {
17+
this(new HashMap<>());
18+
}
19+
20+
public NameToPluginInfo put(String name, PluginInfo pluginInfo) {
21+
nameToPluginInfoMap.put(name, pluginInfo);
22+
return this;
23+
}
24+
25+
public void putAll(Map<String, PluginInfo> namedPluginInfoMap) {
26+
this.nameToPluginInfoMap.putAll(namedPluginInfoMap);
27+
}
28+
29+
public NameToPluginInfo put(NameToPluginInfo nameToPluginInfo) {
30+
putAll(nameToPluginInfo.nameToPluginInfoMap);
31+
return this;
32+
}
33+
34+
public PluginInfo getForPluginName(String pluginName) {
35+
return nameToPluginInfoMap.get(pluginName);
36+
}
37+
38+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.plugins.scanners;
10+
11+
import org.apache.logging.log4j.LogManager;
12+
import org.apache.logging.log4j.Logger;
13+
import org.elasticsearch.core.Strings;
14+
import org.elasticsearch.plugins.PluginBundle;
15+
import org.elasticsearch.xcontent.XContentParserConfiguration;
16+
17+
import java.io.BufferedInputStream;
18+
import java.io.IOException;
19+
import java.nio.file.Files;
20+
import java.nio.file.Path;
21+
import java.util.HashMap;
22+
import java.util.Map;
23+
import java.util.stream.Stream;
24+
25+
import static java.util.Collections.emptyMap;
26+
import static org.elasticsearch.xcontent.XContentType.JSON;
27+
28+
/**
29+
* Reads named components declared by a plugin in a cache file.
30+
* Cache file is expected to be present in plugin's lib directory
31+
* <p>
32+
* The content of a cache file is a JSON representation of a map where:
33+
* keys -> name of the extensible interface (a class/interface marked with @Extensible)
34+
* values -> a map of name to implementation class name
35+
*/
36+
public class NamedComponentReader {
37+
38+
private Logger logger = LogManager.getLogger(NamedComponentReader.class);
39+
private static final String NAMED_COMPONENTS_FILE_NAME = "named_components.json";
40+
/**
41+
* a registry of known classes marked or indirectly marked (extending marked class) with @Extensible
42+
*/
43+
private final ExtensiblesRegistry extensiblesRegistry;
44+
45+
public NamedComponentReader() {
46+
this(ExtensiblesRegistry.INSTANCE);
47+
}
48+
49+
NamedComponentReader(ExtensiblesRegistry extensiblesRegistry) {
50+
this.extensiblesRegistry = extensiblesRegistry;
51+
}
52+
53+
public Map<String, NameToPluginInfo> findNamedComponents(PluginBundle bundle, ClassLoader pluginClassLoader) {
54+
Path pluginDir = bundle.getDir();
55+
return findNamedComponents(pluginDir, pluginClassLoader);
56+
}
57+
58+
// scope for testing
59+
Map<String, NameToPluginInfo> findNamedComponents(Path pluginDir, ClassLoader pluginClassLoader) {
60+
try {
61+
Path namedComponent = findNamedComponentCacheFile(pluginDir);
62+
if (namedComponent != null) {
63+
Map<String, NameToPluginInfo> namedComponents = readFromFile(namedComponent, pluginClassLoader);
64+
logger.debug(() -> Strings.format("Plugin in dir %s declared named components %s.", pluginDir, namedComponents));
65+
66+
return namedComponents;
67+
}
68+
logger.debug(() -> Strings.format("No named component defined in plugin dir %s", pluginDir));
69+
} catch (IOException e) {
70+
logger.error("unable to read named components", e);
71+
}
72+
return emptyMap();
73+
}
74+
75+
private Path findNamedComponentCacheFile(Path pluginDir) throws IOException {
76+
try (Stream<Path> list = Files.list(pluginDir)) {
77+
return list.filter(p -> p.getFileName().toString().equals(NAMED_COMPONENTS_FILE_NAME)).findFirst().orElse(null);
78+
}
79+
}
80+
81+
@SuppressWarnings("unchecked")
82+
Map<String, NameToPluginInfo> readFromFile(Path namedComponent, ClassLoader pluginClassLoader) throws IOException {
83+
Map<String, NameToPluginInfo> res = new HashMap<>();
84+
85+
try (
86+
var json = new BufferedInputStream(Files.newInputStream(namedComponent));
87+
var parser = JSON.xContent().createParser(XContentParserConfiguration.EMPTY, json)
88+
) {
89+
Map<String, Object> map = parser.map();
90+
for (Map.Entry<String, Object> fileAsMap : map.entrySet()) {
91+
String extensibleInterface = fileAsMap.getKey();
92+
validateExtensible(extensibleInterface);
93+
Map<String, Object> components = (Map<String, Object>) fileAsMap.getValue();
94+
for (Map.Entry<String, Object> nameToComponent : components.entrySet()) {
95+
String name = nameToComponent.getKey();
96+
String value = (String) nameToComponent.getValue();
97+
98+
res.computeIfAbsent(extensibleInterface, k -> new NameToPluginInfo())
99+
.put(name, new PluginInfo(name, value, pluginClassLoader));
100+
}
101+
}
102+
}
103+
return res;
104+
}
105+
106+
private void validateExtensible(String extensibleInterface) {
107+
if (extensiblesRegistry.hasExtensible(extensibleInterface) == false) {
108+
throw new IllegalStateException("Unknown extensible name " + extensibleInterface);
109+
}
110+
}
111+
}

0 commit comments

Comments
 (0)