mirror of
https://github.com/elastic/elasticsearch.git
synced 2025-06-28 09:28:55 -04:00
Refactor plugin scanning into lib (#92437)
new stable plugins require generated named_components.json file which contains all analysis components implemented by this plugin. The generation is currently done in build-tools by elasticsearch.stable-esplugin However this makes the generation only available for plugins using gradle. Plugin developers using maven or other building tooling will not be able to use it. This commits refactors the scanning logic into libs:plugin-scanner which will allow for plugin install command to perform the scanning too. relates #88980
This commit is contained in:
parent
62c53e2682
commit
ed83f9cc86
30 changed files with 994 additions and 831 deletions
|
@ -19,15 +19,11 @@ import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.util.stream.Collectors
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
|
import static org.elasticsearch.gradle.fixtures.TestClasspathUtils.setupNamedComponentScanner
|
||||||
|
|
||||||
class StablePluginBuildPluginFuncTest extends AbstractGradleFuncTest {
|
class StablePluginBuildPluginFuncTest extends AbstractGradleFuncTest {
|
||||||
|
|
||||||
def setup() {
|
def setup() {
|
||||||
// underlaying TestClusterPlugin and StandaloneRestIntegTestTask are not cc compatible
|
|
||||||
configurationCacheCompatible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
def "can build stable plugin properties"() {
|
|
||||||
given:
|
|
||||||
buildFile << """plugins {
|
buildFile << """plugins {
|
||||||
id 'elasticsearch.stable-esplugin'
|
id 'elasticsearch.stable-esplugin'
|
||||||
}
|
}
|
||||||
|
@ -38,8 +34,27 @@ class StablePluginBuildPluginFuncTest extends AbstractGradleFuncTest {
|
||||||
name = 'myplugin'
|
name = 'myplugin'
|
||||||
description = 'test plugin'
|
description = 'test plugin'
|
||||||
}
|
}
|
||||||
|
repositories {
|
||||||
|
maven {
|
||||||
|
name = "local-test"
|
||||||
|
url = file("local-repo")
|
||||||
|
metadataSources {
|
||||||
|
artifact()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
// underlaying TestClusterPlugin and StandaloneRestIntegTestTask are not cc compatible
|
||||||
|
configurationCacheCompatible = false
|
||||||
|
|
||||||
|
def version = VersionProperties.elasticsearch
|
||||||
|
setupNamedComponentScanner(dir("local-repo/org/elasticsearch/elasticsearch-plugin-scanner/${version}/"), version)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def "can build stable plugin properties"() {
|
||||||
|
given:
|
||||||
when:
|
when:
|
||||||
def result = gradleRunner(":pluginProperties").build()
|
def result = gradleRunner(":pluginProperties").build()
|
||||||
def props = getPluginProperties()
|
def props = getPluginProperties()
|
||||||
|
@ -62,26 +77,16 @@ class StablePluginBuildPluginFuncTest extends AbstractGradleFuncTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
def "can scan and create named components file"() {
|
def "can scan and create named components file"() {
|
||||||
|
//THIS IS RUNNING A MOCK CONFIGURED IN setup()
|
||||||
given:
|
given:
|
||||||
File jarFolder = new File(testProjectDir.root, "jars")
|
File jarFolder = new File(testProjectDir.root, "jars")
|
||||||
jarFolder.mkdirs()
|
jarFolder.mkdirs()
|
||||||
|
|
||||||
buildFile << """plugins {
|
buildFile << """
|
||||||
id 'elasticsearch.stable-esplugin'
|
|
||||||
}
|
|
||||||
|
|
||||||
version = '1.2.3'
|
|
||||||
|
|
||||||
esplugin {
|
|
||||||
name = 'myplugin'
|
|
||||||
description = 'test plugin'
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation files('${normalized(StableApiJarMocks.createPluginApiJar(jarFolder.toPath()).toAbsolutePath().toString())}')
|
implementation files('${normalized(StableApiJarMocks.createPluginApiJar(jarFolder.toPath()).toAbsolutePath().toString())}')
|
||||||
implementation files('${normalized(StableApiJarMocks.createExtensibleApiJar(jarFolder.toPath()).toAbsolutePath().toString())}')
|
implementation files('${normalized(StableApiJarMocks.createExtensibleApiJar(jarFolder.toPath()).toAbsolutePath().toString())}')
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
file("src/main/java/org/acme/A.java") << """
|
file("src/main/java/org/acme/A.java") << """
|
||||||
|
@ -95,18 +100,16 @@ class StablePluginBuildPluginFuncTest extends AbstractGradleFuncTest {
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
when:
|
when:
|
||||||
def result = gradleRunner(":assemble").build()
|
def result = gradleRunner(":assemble", "-i").build()
|
||||||
Path namedComponents = file("build/generated-named-components/named_components.json").toPath();
|
|
||||||
def map = new JsonSlurper().parse(namedComponents.toFile())
|
|
||||||
then:
|
then:
|
||||||
result.task(":assemble").outcome == TaskOutcome.SUCCESS
|
result.task(":assemble").outcome == TaskOutcome.SUCCESS
|
||||||
|
//we expect that a Fake namedcomponent scanner used in this test will be passed a filename to be created
|
||||||
map == ["org.elasticsearch.plugin.scanner.test_classes.ExtensibleClass" : (["componentA" : "org.acme.A"]) ]
|
File namedComponents = file("build/generated-named-components/named_components.json")
|
||||||
|
namedComponents.exists() == true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Map<String, String> getPluginProperties() {
|
Map<String, String> getPluginProperties() {
|
||||||
Path propsFile = file("build/generated-descriptor/stable-plugin-descriptor.properties").toPath();
|
Path propsFile = file("build/generated-descriptor/stable-plugin-descriptor.properties").toPath();
|
||||||
Properties rawProps = new Properties()
|
Properties rawProps = new Properties()
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* 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.gradle;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public class NamedComponentScannerMock {
|
||||||
|
public static void main(String[] args) throws IOException {
|
||||||
|
// expect a file name to passed in as a parameter
|
||||||
|
// creating a file so that we can assert about this in a test
|
||||||
|
Path path = Path.of(args[0]);
|
||||||
|
Files.createDirectories(path.getParent());
|
||||||
|
Files.createFile(path);
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,55 +8,60 @@
|
||||||
|
|
||||||
package org.elasticsearch.gradle.plugin;
|
package org.elasticsearch.gradle.plugin;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import org.elasticsearch.gradle.LoggedExec;
|
||||||
|
|
||||||
import org.elasticsearch.gradle.plugin.scanner.ClassReaders;
|
|
||||||
import org.elasticsearch.gradle.plugin.scanner.NamedComponentScanner;
|
|
||||||
import org.gradle.api.DefaultTask;
|
import org.gradle.api.DefaultTask;
|
||||||
import org.gradle.api.file.ConfigurableFileCollection;
|
|
||||||
import org.gradle.api.file.FileCollection;
|
import org.gradle.api.file.FileCollection;
|
||||||
import org.gradle.api.file.ProjectLayout;
|
import org.gradle.api.file.ProjectLayout;
|
||||||
import org.gradle.api.file.RegularFileProperty;
|
import org.gradle.api.file.RegularFileProperty;
|
||||||
import org.gradle.api.logging.Logger;
|
import org.gradle.api.logging.Logger;
|
||||||
import org.gradle.api.logging.Logging;
|
import org.gradle.api.logging.Logging;
|
||||||
import org.gradle.api.model.ObjectFactory;
|
|
||||||
import org.gradle.api.tasks.CompileClasspath;
|
import org.gradle.api.tasks.CompileClasspath;
|
||||||
|
import org.gradle.api.tasks.InputFiles;
|
||||||
import org.gradle.api.tasks.OutputFile;
|
import org.gradle.api.tasks.OutputFile;
|
||||||
|
import org.gradle.api.tasks.PathSensitive;
|
||||||
|
import org.gradle.api.tasks.PathSensitivity;
|
||||||
import org.gradle.api.tasks.TaskAction;
|
import org.gradle.api.tasks.TaskAction;
|
||||||
import org.gradle.workers.WorkAction;
|
import org.gradle.process.ExecOperations;
|
||||||
import org.gradle.workers.WorkParameters;
|
import org.gradle.process.ExecResult;
|
||||||
import org.gradle.workers.WorkerExecutor;
|
import org.gradle.workers.WorkerExecutor;
|
||||||
import org.objectweb.asm.ClassReader;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
public abstract class GenerateNamedComponentsTask extends DefaultTask {
|
public abstract class GenerateNamedComponentsTask extends DefaultTask {
|
||||||
private static final Logger LOGGER = Logging.getLogger(GenerateNamedComponentsTask.class);
|
private static final Logger LOGGER = Logging.getLogger(GenerateNamedComponentsTask.class);
|
||||||
|
private static final String NAMED_COMPONENTS_DIR = "generated-named-components/";
|
||||||
private static final String NAMED_COMPONENTS_FILE = "named_components.json";
|
private static final String NAMED_COMPONENTS_FILE = "named_components.json";
|
||||||
|
private static final String NAMED_COMPONENTS_PATH = NAMED_COMPONENTS_DIR + NAMED_COMPONENTS_FILE;
|
||||||
|
|
||||||
private final WorkerExecutor workerExecutor;
|
private final WorkerExecutor workerExecutor;
|
||||||
|
private FileCollection pluginScannerClasspath;
|
||||||
private FileCollection classpath;
|
private FileCollection classpath;
|
||||||
|
private ExecOperations execOperations;
|
||||||
|
private ProjectLayout projectLayout;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public GenerateNamedComponentsTask(WorkerExecutor workerExecutor, ObjectFactory objectFactory, ProjectLayout projectLayout) {
|
public GenerateNamedComponentsTask(WorkerExecutor workerExecutor, ExecOperations execOperations, ProjectLayout projectLayout) {
|
||||||
this.workerExecutor = workerExecutor;
|
this.workerExecutor = workerExecutor;
|
||||||
getOutputFile().convention(projectLayout.getBuildDirectory().file("generated-named-components/" + NAMED_COMPONENTS_FILE));
|
this.execOperations = execOperations;
|
||||||
|
this.projectLayout = projectLayout;
|
||||||
|
|
||||||
|
getOutputFile().convention(projectLayout.getBuildDirectory().file(NAMED_COMPONENTS_PATH));
|
||||||
}
|
}
|
||||||
|
|
||||||
@TaskAction
|
@TaskAction
|
||||||
public void scanPluginClasses() {
|
public void scanPluginClasses() {
|
||||||
workerExecutor.noIsolation().submit(GenerateNamedComponentsAction.class, params -> {
|
File outputFile = projectLayout.getBuildDirectory().file(NAMED_COMPONENTS_PATH).get().getAsFile();
|
||||||
params.getClasspath().from(classpath);
|
|
||||||
params.getOutputFile().set(getOutputFile());
|
ExecResult execResult = LoggedExec.javaexec(execOperations, spec -> {
|
||||||
|
spec.classpath(pluginScannerClasspath.plus(getClasspath()).getAsPath());
|
||||||
|
spec.getMainClass().set("org.elasticsearch.plugin.scanner.NamedComponentScanner");
|
||||||
|
spec.args(outputFile);
|
||||||
|
spec.setErrorOutput(System.err);
|
||||||
|
spec.setStandardOutput(System.out);
|
||||||
});
|
});
|
||||||
|
execResult.assertNormalExitValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@OutputFile
|
@OutputFile
|
||||||
|
@ -71,37 +76,13 @@ public abstract class GenerateNamedComponentsTask extends DefaultTask {
|
||||||
this.classpath = classpath;
|
this.classpath = classpath;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract static class GenerateNamedComponentsAction implements WorkAction<Parameters> {
|
public void setPluginScannerClasspath(FileCollection pluginScannerClasspath) {
|
||||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
this.pluginScannerClasspath = pluginScannerClasspath;
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute() {
|
|
||||||
Set<File> classpathFiles = getParameters().getClasspath().getFiles();
|
|
||||||
|
|
||||||
List<ClassReader> classReaders = ClassReaders.ofPaths(classpathFiles.stream().map(File::toPath)).collect(Collectors.toList());
|
|
||||||
|
|
||||||
NamedComponentScanner namedComponentScanner = new NamedComponentScanner();
|
|
||||||
Map<String, Map<String, String>> namedComponentsMap = namedComponentScanner.scanForNamedClasses(classReaders);
|
|
||||||
writeToFile(namedComponentsMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeToFile(Map<String, Map<String, String>> namedComponentsMap) {
|
|
||||||
try {
|
|
||||||
String json = OBJECT_MAPPER.writeValueAsString(namedComponentsMap);
|
|
||||||
File file = getParameters().getOutputFile().getAsFile().get();
|
|
||||||
Path of = Path.of(file.getAbsolutePath());
|
|
||||||
Files.writeString(of, json);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Parameters extends WorkParameters {
|
@InputFiles
|
||||||
|
@PathSensitive(PathSensitivity.RELATIVE)
|
||||||
ConfigurableFileCollection getClasspath();
|
public FileCollection getPluginScannerClasspath() {
|
||||||
|
return pluginScannerClasspath;
|
||||||
RegularFileProperty getOutputFile();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,12 @@
|
||||||
|
|
||||||
package org.elasticsearch.gradle.plugin;
|
package org.elasticsearch.gradle.plugin;
|
||||||
|
|
||||||
|
import org.elasticsearch.gradle.VersionProperties;
|
||||||
import org.elasticsearch.gradle.util.GradleUtils;
|
import org.elasticsearch.gradle.util.GradleUtils;
|
||||||
import org.gradle.api.Plugin;
|
import org.gradle.api.Plugin;
|
||||||
import org.gradle.api.Project;
|
import org.gradle.api.Project;
|
||||||
|
import org.gradle.api.artifacts.Configuration;
|
||||||
|
import org.gradle.api.artifacts.dsl.DependencyHandler;
|
||||||
import org.gradle.api.file.FileCollection;
|
import org.gradle.api.file.FileCollection;
|
||||||
import org.gradle.api.file.RegularFile;
|
import org.gradle.api.file.RegularFile;
|
||||||
import org.gradle.api.plugins.JavaPlugin;
|
import org.gradle.api.plugins.JavaPlugin;
|
||||||
|
@ -33,12 +36,21 @@ public class StablePluginBuildPlugin implements Plugin<Project> {
|
||||||
});
|
});
|
||||||
|
|
||||||
final var pluginNamedComponents = project.getTasks().register("pluginNamedComponents", GenerateNamedComponentsTask.class, t -> {
|
final var pluginNamedComponents = project.getTasks().register("pluginNamedComponents", GenerateNamedComponentsTask.class, t -> {
|
||||||
|
|
||||||
SourceSet mainSourceSet = GradleUtils.getJavaSourceSets(project).findByName(SourceSet.MAIN_SOURCE_SET_NAME);
|
SourceSet mainSourceSet = GradleUtils.getJavaSourceSets(project).findByName(SourceSet.MAIN_SOURCE_SET_NAME);
|
||||||
FileCollection dependencyJars = project.getConfigurations().getByName(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME);
|
FileCollection dependencyJars = project.getConfigurations().getByName(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME);
|
||||||
FileCollection compiledPluginClasses = mainSourceSet.getOutput().getClassesDirs();
|
FileCollection compiledPluginClasses = mainSourceSet.getOutput().getClassesDirs();
|
||||||
FileCollection classPath = dependencyJars.plus(compiledPluginClasses);
|
FileCollection classPath = dependencyJars.plus(compiledPluginClasses);
|
||||||
t.setClasspath(classPath);
|
t.setClasspath(classPath);
|
||||||
});
|
});
|
||||||
|
Configuration pluginScannerConfig = project.getConfigurations().create("pluginScannerConfig");
|
||||||
|
DependencyHandler dependencyHandler = project.getDependencies();
|
||||||
|
pluginScannerConfig.defaultDependencies(
|
||||||
|
deps -> deps.add(
|
||||||
|
dependencyHandler.create("org.elasticsearch:elasticsearch-plugin-scanner:" + VersionProperties.getElasticsearch())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
pluginNamedComponents.configure(t -> { t.setPluginScannerClasspath(pluginScannerConfig); });
|
||||||
|
|
||||||
final var pluginExtension = project.getExtensions().getByType(PluginPropertiesExtension.class);
|
final var pluginExtension = project.getExtensions().getByType(PluginPropertiesExtension.class);
|
||||||
pluginExtension.getBundleSpec().from(pluginNamedComponents);
|
pluginExtension.getBundleSpec().from(pluginNamedComponents);
|
||||||
|
|
|
@ -1,98 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.gradle.plugin.scanner;
|
|
||||||
|
|
||||||
import org.objectweb.asm.ClassReader;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.UncheckedIOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.jar.JarFile;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
import java.util.zip.ZipFile;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A utility class containing methods to create streams of ASM's ClassReader
|
|
||||||
*
|
|
||||||
* @see ClassReader
|
|
||||||
*/
|
|
||||||
public class ClassReaders {
|
|
||||||
private static final String MODULE_INFO = "module-info.class";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method must be used within a try-with-resources statement or similar
|
|
||||||
* control structure.
|
|
||||||
*/
|
|
||||||
public static Stream<ClassReader> ofDirWithJars(String path) {
|
|
||||||
if (path == null) {
|
|
||||||
return Stream.empty();
|
|
||||||
}
|
|
||||||
Path dir = Paths.get(path);
|
|
||||||
try {
|
|
||||||
return ofPaths(Files.list(dir));
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new UncheckedIOException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method must be used within a try-with-resources statement or similar
|
|
||||||
* control structure.
|
|
||||||
*/
|
|
||||||
public static Stream<ClassReader> ofPaths(Stream<Path> list) {
|
|
||||||
return list.filter(Files::exists).flatMap(p -> {
|
|
||||||
if (p.toString().endsWith(".jar")) {
|
|
||||||
return classesInJar(p);
|
|
||||||
} else {
|
|
||||||
return classesInPath(p);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Stream<ClassReader> classesInJar(Path jar) {
|
|
||||||
try {
|
|
||||||
JarFile jf = new JarFile(jar.toFile(), true, ZipFile.OPEN_READ, Runtime.version());
|
|
||||||
|
|
||||||
Stream<ClassReader> classReaderStream = jf.versionedStream()
|
|
||||||
.filter(e -> e.getName().endsWith(".class") && e.getName().equals(MODULE_INFO) == false)
|
|
||||||
.map(e -> {
|
|
||||||
try (InputStream is = jf.getInputStream(e)) {
|
|
||||||
byte[] classBytes = is.readAllBytes();
|
|
||||||
return new ClassReader(classBytes);
|
|
||||||
} catch (IOException ex) {
|
|
||||||
throw new UncheckedIOException(ex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return classReaderStream;
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new UncheckedIOException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Stream<ClassReader> classesInPath(Path root) {
|
|
||||||
try {
|
|
||||||
Stream<Path> stream = Files.walk(root);
|
|
||||||
return stream.filter(p -> p.toString().endsWith(".class"))
|
|
||||||
.filter(p -> p.toString().endsWith("module-info.class") == false)
|
|
||||||
.map(p -> {
|
|
||||||
try (InputStream is = Files.newInputStream(p)) {
|
|
||||||
byte[] classBytes = is.readAllBytes();
|
|
||||||
return new ClassReader(classBytes);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new UncheckedIOException(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new UncheckedIOException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.gradle.plugin.scanner;
|
|
||||||
|
|
||||||
import org.objectweb.asm.AnnotationVisitor;
|
|
||||||
import org.objectweb.asm.ClassReader;
|
|
||||||
import org.objectweb.asm.Opcodes;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class NamedComponentScanner {
|
|
||||||
|
|
||||||
// returns a Map<String, Map<String,String> - extensible interface -> map{ namedName -> className }
|
|
||||||
public Map<String, Map<String, String>> scanForNamedClasses(Collection<ClassReader> classReaderStream) {
|
|
||||||
// TODO I don't have access to stable-plugin-api here so I have to hardcode class descriptors
|
|
||||||
ClassScanner extensibleClassScanner = new ClassScanner("Lorg/elasticsearch/plugin/api/Extensible;", (classname, map) -> {
|
|
||||||
map.put(classname, classname);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
extensibleClassScanner.visit(classReaderStream.stream());
|
|
||||||
|
|
||||||
ClassScanner namedComponentsScanner = new ClassScanner(
|
|
||||||
"Lorg/elasticsearch/plugin/api/NamedComponent;"/*NamedComponent.class*/,
|
|
||||||
(classname, map) -> new AnnotationVisitor(Opcodes.ASM9) {
|
|
||||||
@Override
|
|
||||||
public void visit(String key, Object value) {
|
|
||||||
assert key.equals("value");
|
|
||||||
assert value instanceof String;
|
|
||||||
map.put(value.toString(), classname);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
namedComponentsScanner.visit(classReaderStream.stream());
|
|
||||||
|
|
||||||
Map<String, Map<String, String>> componentInfo = new HashMap<>();
|
|
||||||
for (var e : namedComponentsScanner.getFoundClasses().entrySet()) {
|
|
||||||
String name = e.getKey();
|
|
||||||
String classnameWithSlashes = e.getValue();
|
|
||||||
String extensibleClassnameWithSlashes = extensibleClassScanner.getFoundClasses().get(classnameWithSlashes);
|
|
||||||
if (extensibleClassnameWithSlashes == null) {
|
|
||||||
throw new RuntimeException(
|
|
||||||
"Named component " + name + "(" + pathToClassName(classnameWithSlashes) + ") does not extend from an extensible class"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
var named = componentInfo.computeIfAbsent(pathToClassName(extensibleClassnameWithSlashes), k -> new HashMap<>());
|
|
||||||
named.put(name, pathToClassName(classnameWithSlashes));
|
|
||||||
}
|
|
||||||
return componentInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String pathToClassName(String classWithSlashes) {
|
|
||||||
return classWithSlashes.replace('/', '.');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,91 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.gradle.plugin.scanner
|
|
||||||
|
|
||||||
import spock.lang.Specification
|
|
||||||
|
|
||||||
import org.elasticsearch.plugin.api.NamedComponent
|
|
||||||
import org.elasticsearch.plugin.scanner.test_classes.ExtensibleClass
|
|
||||||
import org.elasticsearch.plugin.scanner.test_classes.ExtensibleInterface
|
|
||||||
import org.elasticsearch.plugin.scanner.test_classes.ImplementingExtensible
|
|
||||||
import org.elasticsearch.plugin.scanner.test_classes.SubClass
|
|
||||||
import org.elasticsearch.plugin.api.Extensible
|
|
||||||
import org.objectweb.asm.ClassReader
|
|
||||||
import org.objectweb.asm.Type
|
|
||||||
|
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.Path
|
|
||||||
import java.nio.file.Paths
|
|
||||||
|
|
||||||
class AnnotatedHierarchyVisitorSpec extends Specification {
|
|
||||||
Set<String> foundClasses
|
|
||||||
AnnotatedHierarchyVisitor visitor
|
|
||||||
|
|
||||||
def setup() {
|
|
||||||
foundClasses = new HashSet<>()
|
|
||||||
visitor =
|
|
||||||
new AnnotatedHierarchyVisitor(
|
|
||||||
Type.getDescriptor(Extensible.class), (className) -> {
|
|
||||||
foundClasses.add(className)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
def "empty result when no classes annotated"() {
|
|
||||||
when:
|
|
||||||
performScan(visitor, NamedComponent.class)
|
|
||||||
|
|
||||||
then:
|
|
||||||
foundClasses.empty
|
|
||||||
}
|
|
||||||
|
|
||||||
def "single class found when only one is annotated"() {
|
|
||||||
when:
|
|
||||||
performScan(visitor, ExtensibleClass.class)
|
|
||||||
|
|
||||||
then:
|
|
||||||
foundClasses == [classNameToPath(ExtensibleClass.class)] as Set
|
|
||||||
}
|
|
||||||
|
|
||||||
def "class extending an extensible is also found"() {
|
|
||||||
when:
|
|
||||||
performScan(visitor, ExtensibleClass.class, SubClass.class)
|
|
||||||
|
|
||||||
then:
|
|
||||||
foundClasses == [classNameToPath(ExtensibleClass.class)] as Set
|
|
||||||
visitor.getClassHierarchy() == [(classNameToPath(ExtensibleClass.class)) : [classNameToPath(SubClass.class)] as Set]
|
|
||||||
}
|
|
||||||
|
|
||||||
def "interface extending an extensible is also found"() {
|
|
||||||
when:
|
|
||||||
performScan(visitor, ImplementingExtensible.class, ExtensibleInterface.class)
|
|
||||||
|
|
||||||
then:
|
|
||||||
foundClasses == [classNameToPath(ExtensibleInterface.class)] as Set
|
|
||||||
visitor.getClassHierarchy() ==
|
|
||||||
[(classNameToPath(ExtensibleInterface.class)) : [classNameToPath(ImplementingExtensible.class)] as Set]
|
|
||||||
}
|
|
||||||
|
|
||||||
private String classNameToPath(Class<?> clazz) {
|
|
||||||
return clazz.getCanonicalName().replace(".", "/")
|
|
||||||
}
|
|
||||||
|
|
||||||
private void performScan(AnnotatedHierarchyVisitor classVisitor, Class<?>... classes) throws IOException, URISyntaxException {
|
|
||||||
for (Class<?> clazz : classes) {
|
|
||||||
String className = classNameToPath(clazz) + ".class"
|
|
||||||
def stream = this.getClass().getClassLoader().getResourceAsStream(className)
|
|
||||||
try (InputStream fileInputStream = stream) {
|
|
||||||
ClassReader cr = new ClassReader(fileInputStream)
|
|
||||||
cr.accept(classVisitor, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,196 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.gradle.plugin.scanner
|
|
||||||
|
|
||||||
import spock.lang.Specification
|
|
||||||
|
|
||||||
import org.elasticsearch.gradle.internal.test.InMemoryJavaCompiler;
|
|
||||||
|
|
||||||
import org.elasticsearch.gradle.internal.test.JarUtils
|
|
||||||
import org.hamcrest.Matchers
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.rules.TemporaryFolder
|
|
||||||
import org.objectweb.asm.ClassReader
|
|
||||||
|
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.Path
|
|
||||||
import java.util.stream.Collectors
|
|
||||||
import java.util.stream.Stream
|
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat
|
|
||||||
|
|
||||||
class ClassReadersSpec extends Specification {
|
|
||||||
@Rule
|
|
||||||
TemporaryFolder testProjectDir = new TemporaryFolder()
|
|
||||||
|
|
||||||
private Path tmpDir() throws IOException {
|
|
||||||
return testProjectDir.root.toPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
def "module-info is not returned as a class from jar"() {
|
|
||||||
when:
|
|
||||||
final Path tmp = tmpDir();
|
|
||||||
final Path dirWithJar = tmp.resolve("jars-dir");
|
|
||||||
Files.createDirectories(dirWithJar);
|
|
||||||
Path jar = dirWithJar.resolve("api.jar");
|
|
||||||
JarUtils.createJarWithEntries(
|
|
||||||
jar, Map.of(
|
|
||||||
"module-info.class", InMemoryJavaCompiler.compile(
|
|
||||||
"module-info", """
|
|
||||||
module p {}
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
then:
|
|
||||||
try (Stream<ClassReader> classReaderStream = ClassReaders.ofPaths(Stream.of(jar))) {
|
|
||||||
|
|
||||||
assertThat(classReaderStream.collect(Collectors.toList()), Matchers.empty());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def "two classes are returned in a stream from jar"() {
|
|
||||||
when:
|
|
||||||
final Path tmp = tmpDir();
|
|
||||||
final Path dirWithJar = tmp.resolve("jars-dir");
|
|
||||||
Files.createDirectories(dirWithJar);
|
|
||||||
Path jar = dirWithJar.resolve("api.jar");
|
|
||||||
JarUtils.createJarWithEntries(
|
|
||||||
jar, Map.of(
|
|
||||||
"p/A.class", InMemoryJavaCompiler.compile(
|
|
||||||
"p.A", """
|
|
||||||
package p;
|
|
||||||
public class A {}
|
|
||||||
"""),
|
|
||||||
"p/B.class", InMemoryJavaCompiler.compile(
|
|
||||||
"p.B", """
|
|
||||||
package p;
|
|
||||||
public class B {}
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
then:
|
|
||||||
try (Stream<ClassReader> classReaderStream = ClassReaders.ofPaths(Stream.of(jar))) {
|
|
||||||
List<String> collect = classReaderStream.map(cr -> cr.getClassName()).collect(Collectors.toList());
|
|
||||||
assertThat(collect, Matchers.containsInAnyOrder("p/A", "p/B"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def "on a classpath jars and individual classes are returned"() {
|
|
||||||
when:
|
|
||||||
final Path tmp = tmpDir();
|
|
||||||
final Path dirWithJar = tmp.resolve("jars-dir");
|
|
||||||
Files.createDirectories(dirWithJar);
|
|
||||||
|
|
||||||
Path jar = dirWithJar.resolve("a_b.jar");
|
|
||||||
JarUtils.createJarWithEntries(
|
|
||||||
jar, Map.of(
|
|
||||||
"p/A.class", InMemoryJavaCompiler.compile(
|
|
||||||
"p.A", """
|
|
||||||
package p;
|
|
||||||
public class A {}
|
|
||||||
"""),
|
|
||||||
"p/B.class", InMemoryJavaCompiler.compile(
|
|
||||||
"p.B", """
|
|
||||||
package p;
|
|
||||||
public class B {}
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
Path jar2 = dirWithJar.resolve("c_d.jar");
|
|
||||||
JarUtils.createJarWithEntries(
|
|
||||||
jar2, Map.of(
|
|
||||||
"p/C.class", InMemoryJavaCompiler.compile(
|
|
||||||
"p.C", """
|
|
||||||
package p;
|
|
||||||
public class C {}
|
|
||||||
"""),
|
|
||||||
"p/D.class", InMemoryJavaCompiler.compile(
|
|
||||||
"p.D", """
|
|
||||||
package p;
|
|
||||||
public class D {}
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
InMemoryJavaCompiler.compile(
|
|
||||||
"p.E", """
|
|
||||||
package p;
|
|
||||||
public class E {}
|
|
||||||
"""
|
|
||||||
);
|
|
||||||
Files.write(
|
|
||||||
tmp.resolve("E.class"), InMemoryJavaCompiler.compile(
|
|
||||||
"p.E", """
|
|
||||||
package p;
|
|
||||||
public class E {}
|
|
||||||
""")
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
then:
|
|
||||||
try (Stream<ClassReader> classReaderStream = ClassReaders.ofPaths(Stream.of(tmp, jar, jar2))) {
|
|
||||||
|
|
||||||
List<String> collect = classReaderStream.map(cr -> cr.getClassName()).collect(Collectors.toList());
|
|
||||||
assertThat(collect, Matchers.containsInAnyOrder("p/A", "p/B", "p/C", "p/D", "p/E"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def "classes from multiple jars in a dir are returned"() {
|
|
||||||
when:
|
|
||||||
final Path tmp = tmpDir();
|
|
||||||
final Path dirWithJar = tmp.resolve("jars-dir");
|
|
||||||
Files.createDirectories(dirWithJar);
|
|
||||||
|
|
||||||
|
|
||||||
Path jar = dirWithJar.resolve("a_b.jar");
|
|
||||||
JarUtils.createJarWithEntries(
|
|
||||||
jar, Map.of(
|
|
||||||
"p/A.class", InMemoryJavaCompiler.compile(
|
|
||||||
"p.A", """
|
|
||||||
package p;
|
|
||||||
public class A {}
|
|
||||||
"""),
|
|
||||||
"p/B.class", InMemoryJavaCompiler.compile(
|
|
||||||
"p.B", """
|
|
||||||
package p;
|
|
||||||
public class B {}
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
Path jar2 = dirWithJar.resolve("c_d.jar");
|
|
||||||
JarUtils.createJarWithEntries(
|
|
||||||
jar2, Map.of(
|
|
||||||
"p/C.class", InMemoryJavaCompiler.compile(
|
|
||||||
"p.C", """
|
|
||||||
package p;
|
|
||||||
public class C {}
|
|
||||||
"""),
|
|
||||||
"p/D.class", InMemoryJavaCompiler.compile(
|
|
||||||
"p.D", """
|
|
||||||
package p;
|
|
||||||
public class D {}
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
then:
|
|
||||||
try (Stream<ClassReader> classReaderStream = ClassReaders.ofDirWithJars(dirWithJar.toString())) {
|
|
||||||
List<String> collect = classReaderStream.map(cr -> cr.getClassName()).collect(Collectors.toList());
|
|
||||||
assertThat(collect, Matchers.containsInAnyOrder("p/A", "p/B", "p/C", "p/D"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,78 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.gradle.plugin.scanner
|
|
||||||
|
|
||||||
import spock.lang.Specification
|
|
||||||
|
|
||||||
import org.elasticsearch.plugin.api.Extensible
|
|
||||||
import org.hamcrest.Matchers
|
|
||||||
import org.objectweb.asm.ClassReader
|
|
||||||
import org.objectweb.asm.Type
|
|
||||||
|
|
||||||
import java.nio.file.Paths
|
|
||||||
import java.util.stream.Collectors
|
|
||||||
import java.util.stream.Stream
|
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat
|
|
||||||
|
|
||||||
class ClassScannerSpec extends Specification {
|
|
||||||
static final System.Logger logger = System.getLogger(ClassScannerSpec.class.getName())
|
|
||||||
def "class and interface hierarchy is scanned"() {
|
|
||||||
given:
|
|
||||||
def reader = new ClassScanner(
|
|
||||||
Type.getDescriptor(Extensible.class), (classname, map) -> {
|
|
||||||
map.put(classname, classname)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Stream<ClassReader> classReaderStream = ofClassPath()
|
|
||||||
logger.log(System.Logger.Level.INFO, "classReaderStream size "+ofClassPath().collect(Collectors.toList()).size())
|
|
||||||
|
|
||||||
when:
|
|
||||||
reader.visit(classReaderStream);
|
|
||||||
Map<String, String> extensibleClasses = reader.getFoundClasses()
|
|
||||||
|
|
||||||
then:
|
|
||||||
assertThat(
|
|
||||||
extensibleClasses,
|
|
||||||
Matchers.allOf(
|
|
||||||
Matchers.hasEntry(
|
|
||||||
"org/elasticsearch/plugin/scanner/test_classes/ExtensibleClass",
|
|
||||||
"org/elasticsearch/plugin/scanner/test_classes/ExtensibleClass"
|
|
||||||
),
|
|
||||||
Matchers.hasEntry(
|
|
||||||
"org/elasticsearch/plugin/scanner/test_classes/ImplementingExtensible",
|
|
||||||
"org/elasticsearch/plugin/scanner/test_classes/ExtensibleInterface"
|
|
||||||
),
|
|
||||||
Matchers.hasEntry(
|
|
||||||
"org/elasticsearch/plugin/scanner/test_classes/SubClass",
|
|
||||||
"org/elasticsearch/plugin/scanner/test_classes/ExtensibleClass"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Stream<ClassReader> ofClassPath() throws IOException {
|
|
||||||
String classpath = System.getProperty("java.class.path");
|
|
||||||
logger.log(System.Logger.Level.INFO, "classpath "+classpath);
|
|
||||||
return ofClassPath(classpath);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Stream<ClassReader> ofClassPath(String classpath) {
|
|
||||||
if (classpath != null && classpath.equals("") == false) {// todo when do we set cp to "" ?
|
|
||||||
def classpathSeparator = System.getProperty("path.separator")
|
|
||||||
logger.log(System.Logger.Level.INFO, "classpathSeparator "+classpathSeparator);
|
|
||||||
|
|
||||||
String[] pathelements = classpath.split(classpathSeparator);
|
|
||||||
return ClassReaders.ofPaths(Arrays.stream(pathelements).map(Paths::get));
|
|
||||||
}
|
|
||||||
return Stream.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,216 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.plugin.scanner
|
|
||||||
|
|
||||||
import net.bytebuddy.ByteBuddy
|
|
||||||
import net.bytebuddy.dynamic.DynamicType
|
|
||||||
import spock.lang.Specification
|
|
||||||
|
|
||||||
import org.elasticsearch.gradle.internal.test.InMemoryJavaCompiler
|
|
||||||
import org.elasticsearch.gradle.internal.test.JarUtils
|
|
||||||
import org.elasticsearch.gradle.internal.test.StableApiJarMocks
|
|
||||||
import org.elasticsearch.gradle.plugin.scanner.ClassReaders
|
|
||||||
import org.elasticsearch.gradle.plugin.scanner.NamedComponentScanner
|
|
||||||
import org.elasticsearch.plugin.scanner.test_classes.ExtensibleClass
|
|
||||||
import org.elasticsearch.plugin.scanner.test_classes.ExtensibleInterface
|
|
||||||
import org.elasticsearch.plugin.scanner.test_classes.TestNamedComponent
|
|
||||||
import org.elasticsearch.plugin.api.Extensible
|
|
||||||
import org.elasticsearch.plugin.api.NamedComponent
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.rules.TemporaryFolder
|
|
||||||
import org.objectweb.asm.ClassReader
|
|
||||||
|
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.Path
|
|
||||||
import java.nio.file.Paths
|
|
||||||
import java.util.stream.Collectors
|
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat
|
|
||||||
import static org.hamcrest.Matchers.equalTo
|
|
||||||
|
|
||||||
class NamedComponentScannerSpec extends Specification {
|
|
||||||
@Rule
|
|
||||||
TemporaryFolder testProjectDir = new TemporaryFolder()
|
|
||||||
|
|
||||||
private Path tmpDir() throws IOException {
|
|
||||||
return testProjectDir.root.toPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
NamedComponentScanner namedComponentScanner = new NamedComponentScanner();
|
|
||||||
|
|
||||||
def "named component is found when single class provided"() {
|
|
||||||
when:
|
|
||||||
Map<String, Map<String, String>> namedComponents = namedComponentScanner.scanForNamedClasses(
|
|
||||||
classReaderStream(TestNamedComponent.class, ExtensibleInterface.class)
|
|
||||||
)
|
|
||||||
|
|
||||||
then:
|
|
||||||
assertThat(
|
|
||||||
namedComponents,
|
|
||||||
equalTo(
|
|
||||||
Map.of(
|
|
||||||
ExtensibleInterface.class.getCanonicalName(),
|
|
||||||
Map.of("test_named_component", TestNamedComponent.class.getCanonicalName())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
def "named components are found when single jar provided"() {
|
|
||||||
given:
|
|
||||||
final Path tmp = tmpDir();
|
|
||||||
final Path dirWithJar = tmp.resolve("jars-dir");
|
|
||||||
Files.createDirectories(dirWithJar);
|
|
||||||
Path jar = dirWithJar.resolve("plugin.jar");
|
|
||||||
JarUtils.createJarWithEntries(
|
|
||||||
jar, Map.of(
|
|
||||||
"p/A.class", InMemoryJavaCompiler.compile(
|
|
||||||
"p.A", """
|
|
||||||
package p;
|
|
||||||
import org.elasticsearch.plugin.api.*;
|
|
||||||
import org.elasticsearch.plugin.scanner.test_classes.*;
|
|
||||||
@NamedComponent("a_component")
|
|
||||||
public class A extends ExtensibleClass {}
|
|
||||||
"""
|
|
||||||
), "p/B.class", InMemoryJavaCompiler.compile(
|
|
||||||
"p.B", """
|
|
||||||
package p;
|
|
||||||
import org.elasticsearch.plugin.api.*;
|
|
||||||
import org.elasticsearch.plugin.scanner.test_classes.*;
|
|
||||||
@NamedComponent("b_component")
|
|
||||||
public class B implements ExtensibleInterface{}
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
StableApiJarMocks.createPluginApiJar(dirWithJar);
|
|
||||||
StableApiJarMocks.createExtensibleApiJar(dirWithJar);//for instance analysis api
|
|
||||||
|
|
||||||
|
|
||||||
Collection<ClassReader> classReaderStream = ClassReaders.ofDirWithJars(dirWithJar.toString()).collect(Collectors.toList())
|
|
||||||
|
|
||||||
when:
|
|
||||||
Map<String, Map<String, String>> namedComponents = namedComponentScanner.scanForNamedClasses(classReaderStream);
|
|
||||||
|
|
||||||
then:
|
|
||||||
assertThat(
|
|
||||||
namedComponents,
|
|
||||||
equalTo(
|
|
||||||
Map.of(
|
|
||||||
ExtensibleClass.class.getCanonicalName(),
|
|
||||||
Map.of("a_component", "p.A"),
|
|
||||||
ExtensibleInterface.class.getCanonicalName(),
|
|
||||||
Map.of("b_component", "p.B")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
def "named components can extend common super class"() {
|
|
||||||
given:
|
|
||||||
Map<String, CharSequence> sources = Map.of(
|
|
||||||
"p.CustomExtensibleInterface",
|
|
||||||
"""
|
|
||||||
package p;
|
|
||||||
import org.elasticsearch.plugin.api.*;
|
|
||||||
import org.elasticsearch.plugin.scanner.test_classes.*;
|
|
||||||
public interface CustomExtensibleInterface extends ExtensibleInterface {}
|
|
||||||
""",
|
|
||||||
// note that this class implements a custom interface
|
|
||||||
"p.CustomExtensibleClass",
|
|
||||||
"""
|
|
||||||
package p;
|
|
||||||
import org.elasticsearch.plugin.api.*;
|
|
||||||
import org.elasticsearch.plugin.scanner.test_classes.*;
|
|
||||||
public class CustomExtensibleClass implements CustomExtensibleInterface {}
|
|
||||||
""",
|
|
||||||
"p.A",
|
|
||||||
"""
|
|
||||||
package p;
|
|
||||||
import org.elasticsearch.plugin.api.*;
|
|
||||||
import org.elasticsearch.plugin.scanner.test_classes.*;
|
|
||||||
@NamedComponent("a_component")
|
|
||||||
public class A extends CustomExtensibleClass {}
|
|
||||||
""",
|
|
||||||
"p.B",
|
|
||||||
"""
|
|
||||||
package p;
|
|
||||||
import org.elasticsearch.plugin.api.*;
|
|
||||||
import org.elasticsearch.plugin.scanner.test_classes.*;
|
|
||||||
@NamedComponent("b_component")
|
|
||||||
public class B implements CustomExtensibleInterface{}
|
|
||||||
"""
|
|
||||||
);
|
|
||||||
var classToBytes = InMemoryJavaCompiler.compile(sources);
|
|
||||||
|
|
||||||
Map<String, byte[]> jarEntries = new HashMap<>();
|
|
||||||
jarEntries.put("p/CustomExtensibleInterface.class", classToBytes.get("p.CustomExtensibleInterface"));
|
|
||||||
jarEntries.put("p/CustomExtensibleClass.class", classToBytes.get("p.CustomExtensibleClass"));
|
|
||||||
jarEntries.put("p/A.class", classToBytes.get("p.A"));
|
|
||||||
jarEntries.put("p/B.class", classToBytes.get("p.B"));
|
|
||||||
|
|
||||||
final Path tmp = tmpDir();
|
|
||||||
final Path dirWithJar = tmp.resolve("jars-dir");
|
|
||||||
Files.createDirectories(dirWithJar);
|
|
||||||
Path jar = dirWithJar.resolve("plugin.jar");
|
|
||||||
JarUtils.createJarWithEntries(jar, jarEntries);
|
|
||||||
|
|
||||||
StableApiJarMocks.createPluginApiJar(dirWithJar)
|
|
||||||
StableApiJarMocks.createExtensibleApiJar(dirWithJar);//for instance analysis api
|
|
||||||
|
|
||||||
Collection<ClassReader> classReaderStream = ClassReaders.ofDirWithJars(dirWithJar.toString()).collect(Collectors.toList())
|
|
||||||
|
|
||||||
when:
|
|
||||||
Map<String, Map<String, String>> namedComponents = namedComponentScanner.scanForNamedClasses(classReaderStream);
|
|
||||||
|
|
||||||
then:
|
|
||||||
assertThat(
|
|
||||||
namedComponents,
|
|
||||||
equalTo(
|
|
||||||
Map.of(
|
|
||||||
ExtensibleInterface.class.getCanonicalName(),
|
|
||||||
Map.of(
|
|
||||||
"a_component", "p.A",
|
|
||||||
"b_component", "p.B"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private Collection<ClassReader> classReaderStream(Class<?>... classes) {
|
|
||||||
try {
|
|
||||||
return Arrays.stream(classes).map(
|
|
||||||
clazz -> {
|
|
||||||
String className = classNameToPath(clazz) + ".class";
|
|
||||||
def stream = this.getClass().getClassLoader().getResourceAsStream(className)
|
|
||||||
try (InputStream is = stream) {
|
|
||||||
byte[] classBytes = is.readAllBytes();
|
|
||||||
ClassReader classReader = new ClassReader(classBytes);
|
|
||||||
return classReader;
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new UncheckedIOException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
).collect(Collectors.toList())
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private String classNameToPath(Class<?> clazz) {
|
|
||||||
return clazz.getCanonicalName().replace(".", "/");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -16,17 +16,40 @@ import net.bytebuddy.dynamic.DynamicType
|
||||||
import net.bytebuddy.implementation.ExceptionMethod
|
import net.bytebuddy.implementation.ExceptionMethod
|
||||||
import net.bytebuddy.implementation.FixedValue
|
import net.bytebuddy.implementation.FixedValue
|
||||||
import net.bytebuddy.implementation.Implementation
|
import net.bytebuddy.implementation.Implementation
|
||||||
|
import net.bytebuddy.implementation.MethodDelegation
|
||||||
|
|
||||||
|
import org.elasticsearch.gradle.NamedComponentScannerMock
|
||||||
|
|
||||||
import static org.junit.Assert.fail
|
import static org.junit.Assert.fail
|
||||||
|
|
||||||
class TestClasspathUtils {
|
class TestClasspathUtils {
|
||||||
|
|
||||||
|
// we cannot access real NamedComponentScanner in libs:plugin-api-scanner so we create a fake class
|
||||||
|
static void setupNamedComponentScanner(File projectRoot, String version) {
|
||||||
|
def value = MethodDelegation.to(NamedComponentScannerMock.class)
|
||||||
|
|
||||||
|
DynamicType.Unloaded<?> dynamicType = new ByteBuddy().subclass(Object.class)
|
||||||
|
.name("org.elasticsearch.plugin.scanner.NamedComponentScanner")
|
||||||
|
.defineMethod("main", void.class, Visibility.PUBLIC, Ownership.STATIC)
|
||||||
|
.withParameters(String[].class)
|
||||||
|
.intercept(value)
|
||||||
|
.make()
|
||||||
|
.include(new ByteBuddy().redefine(NamedComponentScannerMock.class).make())
|
||||||
|
|
||||||
|
try {
|
||||||
|
dynamicType.toJar(targetFile(projectRoot, "elasticsearch-plugin-scanner", version))
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace()
|
||||||
|
fail("Cannot setup jdk jar hell classpath")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void setupJarHellJar(File projectRoot) {
|
static void setupJarHellJar(File projectRoot) {
|
||||||
generateJdkJarHellCheck(projectRoot, "org.elasticsearch.jdk.JarHell", "current", FixedValue.value(TypeDescription.VOID))
|
generateJarWithClass(projectRoot, "org.elasticsearch.jdk.JarHell","elasticsearch-core", "current", FixedValue.value(TypeDescription.VOID))
|
||||||
}
|
}
|
||||||
|
|
||||||
static void setupJarHellJar(File projectRoot, String version) {
|
static void setupJarHellJar(File projectRoot, String version) {
|
||||||
generateJdkJarHellCheck(projectRoot, "org.elasticsearch.jdk.JarHell", version, FixedValue.value(TypeDescription.VOID))
|
generateJarWithClass(projectRoot, "org.elasticsearch.jdk.JarHell", "elasticsearch-core", version, FixedValue.value(TypeDescription.VOID))
|
||||||
}
|
}
|
||||||
|
|
||||||
static void setupJarJdkClasspath(File projectRoot) {
|
static void setupJarJdkClasspath(File projectRoot) {
|
||||||
|
@ -39,26 +62,28 @@ class TestClasspathUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void generateJdkJarHellCheck(File targetDir, String className, Implementation mainImplementation) {
|
private static void generateJdkJarHellCheck(File targetDir, String className, Implementation mainImplementation) {
|
||||||
generateJdkJarHellCheck(targetDir, className, "current", mainImplementation)
|
generateJarWithClass(targetDir, className, "elasticsearch-core", "current", mainImplementation)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void generateJdkJarHellCheck(File targetDir, String className, String version, Implementation mainImplementation) {
|
|
||||||
|
private static void generateJarWithClass(File targetDir, String className, String artifactName, String version, Implementation mainImplementation) {
|
||||||
DynamicType.Unloaded<?> dynamicType = new ByteBuddy().subclass(Object.class)
|
DynamicType.Unloaded<?> dynamicType = new ByteBuddy().subclass(Object.class)
|
||||||
.name(className)
|
.name(className)
|
||||||
.defineMethod("main", void.class, Visibility.PUBLIC, Ownership.STATIC)
|
.defineMethod("main", void.class, Visibility.PUBLIC, Ownership.STATIC)
|
||||||
.withParameters(String[].class)
|
.withParameters(String[].class)
|
||||||
.intercept(mainImplementation)
|
.intercept(mainImplementation)
|
||||||
.make()
|
.make()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
dynamicType.toJar(targetFile(targetDir, version))
|
dynamicType.toJar(targetFile(targetDir, artifactName, version))
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
fail("Cannot setup jdk jar hell classpath")
|
fail("Cannot setup jdk jar hell classpath")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static File targetFile(File projectRoot, String version) {
|
private static File targetFile(File projectRoot, String artifactName, String version) {
|
||||||
File targetFile = new File(projectRoot, "elasticsearch-core-${version}.jar")
|
File targetFile = new File(projectRoot, "${artifactName}-${version}.jar")
|
||||||
|
|
||||||
println "targetFile = $targetFile"
|
println "targetFile = $targetFile"
|
||||||
targetFile.getParentFile().mkdirs()
|
targetFile.getParentFile().mkdirs()
|
||||||
|
|
|
@ -26,6 +26,7 @@ configure(subprojects - project('elasticsearch-log4j')) {
|
||||||
&& false == isPluginApi(project, depProject)
|
&& false == isPluginApi(project, depProject)
|
||||||
&& false == depProject.path.equals(':libs:elasticsearch-x-content')
|
&& false == depProject.path.equals(':libs:elasticsearch-x-content')
|
||||||
&& false == depProject.path.equals(':libs:elasticsearch-core')
|
&& false == depProject.path.equals(':libs:elasticsearch-core')
|
||||||
|
&& false == depProject.path.equals(':libs:elasticsearch-plugin-api')
|
||||||
&& depProject.path.startsWith(':libs')
|
&& depProject.path.startsWith(':libs')
|
||||||
&& depProject.name.startsWith('elasticsearch-')) {
|
&& depProject.name.startsWith('elasticsearch-')) {
|
||||||
throw new InvalidUserDataException("projects in :libs "
|
throw new InvalidUserDataException("projects in :libs "
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
* </li>
|
* </li>
|
||||||
* <li> Interfaces and annotations used by plugin developers are in `api` package
|
* <li> Interfaces and annotations used by plugin developers are in `api` package
|
||||||
* i.e org.elasticsearch.plugin.analysis.api or org.elasticsearch.plugin.api
|
* i.e org.elasticsearch.plugin.analysis.api or org.elasticsearch.plugin.api
|
||||||
|
* </li>
|
||||||
* <li> packages which are not meant to be used by plugin developers should not be subpackages of api
|
* <li> packages which are not meant to be used by plugin developers should not be subpackages of api
|
||||||
* i.e org.elasticsearch.plugin.analysis.internal
|
* i.e org.elasticsearch.plugin.analysis.internal
|
||||||
* </li>
|
* </li>
|
||||||
|
|
34
libs/plugin-scanner/build.gradle
Normal file
34
libs/plugin-scanner/build.gradle
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
apply plugin: 'elasticsearch.publish'
|
||||||
|
|
||||||
|
tasks.named("jarHell").configure { enabled = false }
|
||||||
|
|
||||||
|
|
||||||
|
tasks.named("dependencyLicenses").configure {
|
||||||
|
mapping from: /asm-.*/, to: 'asm'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api project(':libs:elasticsearch-core')
|
||||||
|
api project(':libs:elasticsearch-plugin-api')
|
||||||
|
api project(":libs:elasticsearch-x-content")
|
||||||
|
|
||||||
|
implementation 'org.ow2.asm:asm:9.2'
|
||||||
|
implementation 'org.ow2.asm:asm-tree:9.2'
|
||||||
|
|
||||||
|
testImplementation "junit:junit:${versions.junit}"
|
||||||
|
testImplementation(project(":test:framework")) {
|
||||||
|
exclude group: 'org.elasticsearch', module: 'elasticsearch-plugin-scanner'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tasks.named('forbiddenApisMain').configure {
|
||||||
|
// TODO: Need to decide how we want to handle for forbidden signatures with the changes to core
|
||||||
|
replaceSignatureFiles 'jdk-signatures'
|
||||||
|
}
|
||||||
|
|
26
libs/plugin-scanner/licenses/asm-LICENSE.txt
Normal file
26
libs/plugin-scanner/licenses/asm-LICENSE.txt
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
Copyright (c) 2012 France Télécom
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
1. Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
3. Neither the name of the copyright holders nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||||
|
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
||||||
|
THE POSSIBILITY OF SUCH DAMAGE.
|
1
libs/plugin-scanner/licenses/asm-NOTICE.txt
Normal file
1
libs/plugin-scanner/licenses/asm-NOTICE.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
16
libs/plugin-scanner/src/main/java/module-info.java
Normal file
16
libs/plugin-scanner/src/main/java/module-info.java
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module org.elasticsearch.plugin.scanner {
|
||||||
|
requires org.elasticsearch.base;
|
||||||
|
requires org.objectweb.asm;
|
||||||
|
requires org.elasticsearch.plugin.api;
|
||||||
|
requires org.elasticsearch.xcontent;
|
||||||
|
|
||||||
|
exports org.elasticsearch.plugin.scanner;
|
||||||
|
}
|
|
@ -6,7 +6,7 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.elasticsearch.gradle.plugin.scanner;
|
package org.elasticsearch.plugin.scanner;
|
||||||
|
|
||||||
import org.objectweb.asm.AnnotationVisitor;
|
import org.objectweb.asm.AnnotationVisitor;
|
||||||
import org.objectweb.asm.ClassVisitor;
|
import org.objectweb.asm.ClassVisitor;
|
|
@ -0,0 +1,124 @@
|
||||||
|
/*
|
||||||
|
* 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.plugin.scanner;
|
||||||
|
|
||||||
|
import org.elasticsearch.core.PathUtils;
|
||||||
|
import org.objectweb.asm.ClassReader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.FileSystem;
|
||||||
|
import java.nio.file.FileSystems;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility class containing methods to create streams of ASM's ClassReader
|
||||||
|
*
|
||||||
|
* @see ClassReader
|
||||||
|
*/
|
||||||
|
public class ClassReaders {
|
||||||
|
private static final String MODULE_INFO = "module-info.class";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method must be used within a try-with-resources statement or similar
|
||||||
|
* control structure.
|
||||||
|
*/
|
||||||
|
public static List<ClassReader> ofDirWithJars(String path) {
|
||||||
|
if (path == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
Path dir = Paths.get(path);
|
||||||
|
try {
|
||||||
|
return ofPaths(Files.list(dir));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ClassReader> ofPaths(Set<URL> classpathFiles) {
|
||||||
|
return ofPaths(classpathFiles.stream().map(ClassReaders::toPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path toPath(URL url) {
|
||||||
|
try {
|
||||||
|
return PathUtils.get(url.toURI());
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method must be used within a try-with-resources statement or similar
|
||||||
|
* control structure.
|
||||||
|
*/
|
||||||
|
public static List<ClassReader> ofPaths(Stream<Path> list) {
|
||||||
|
return list.filter(Files::exists).flatMap(p -> {
|
||||||
|
if (p.toString().endsWith(".jar")) {
|
||||||
|
return classesInJar(p).stream();
|
||||||
|
} else {
|
||||||
|
return classesInPath(p).stream();
|
||||||
|
}
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ClassReader> classesInJar(Path jar) {
|
||||||
|
try (FileSystem jarFs = FileSystems.newFileSystem(jar)) {
|
||||||
|
Path root = jarFs.getPath("/");
|
||||||
|
return classesInPath(root);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ClassReader> classesInPath(Path root) {
|
||||||
|
try {
|
||||||
|
return Files.walk(root)
|
||||||
|
.filter(p -> p.toString().endsWith(".class"))
|
||||||
|
.filter(p -> p.toString().endsWith(MODULE_INFO) == false)
|
||||||
|
.filter(p -> p.toString().startsWith("/META-INF") == false)// skip multi-release files
|
||||||
|
.map(p -> {
|
||||||
|
try (InputStream is = Files.newInputStream(p)) {
|
||||||
|
byte[] classBytes = is.readAllBytes();
|
||||||
|
return new ClassReader(classBytes);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new UncheckedIOException(ex);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ClassReader> ofClassPath() throws IOException {
|
||||||
|
String classpath = System.getProperty("java.class.path");
|
||||||
|
return ofClassPath(classpath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ClassReader> ofClassPath(String classpath) {
|
||||||
|
if (classpath != null && classpath.equals("") == false) {// todo when do we set cp to "" ?
|
||||||
|
var classpathSeparator = System.getProperty("path.separator");
|
||||||
|
|
||||||
|
String[] pathelements = classpath.split(classpathSeparator);
|
||||||
|
return ofPaths(Arrays.stream(pathelements).map(Paths::get));
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,7 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.elasticsearch.gradle.plugin.scanner;
|
package org.elasticsearch.plugin.scanner;
|
||||||
|
|
||||||
import org.objectweb.asm.AnnotationVisitor;
|
import org.objectweb.asm.AnnotationVisitor;
|
||||||
import org.objectweb.asm.ClassReader;
|
import org.objectweb.asm.ClassReader;
|
||||||
|
@ -15,10 +15,10 @@ import java.util.ArrayDeque;
|
||||||
import java.util.Deque;
|
import java.util.Deque;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
public class ClassScanner {
|
public class ClassScanner {
|
||||||
private final Map<String, String> foundClasses;
|
private final Map<String, String> foundClasses;
|
||||||
|
@ -32,10 +32,8 @@ public class ClassScanner {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void visit(Stream<ClassReader> classReaderStream) {
|
public void visit(List<ClassReader> classReaders) {
|
||||||
try (classReaderStream) {
|
classReaders.forEach(classReader -> classReader.accept(annotatedHierarchyVisitor, ClassReader.SKIP_CODE));
|
||||||
classReaderStream.forEach(classReader -> classReader.accept(annotatedHierarchyVisitor, ClassReader.SKIP_CODE));
|
|
||||||
}
|
|
||||||
addExtensibleDescendants(annotatedHierarchyVisitor.getClassHierarchy());
|
addExtensibleDescendants(annotatedHierarchyVisitor.getClassHierarchy());
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
* 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.plugin.scanner;
|
||||||
|
|
||||||
|
import org.elasticsearch.plugin.api.Extensible;
|
||||||
|
import org.elasticsearch.plugin.api.NamedComponent;
|
||||||
|
import org.elasticsearch.xcontent.XContentBuilder;
|
||||||
|
import org.elasticsearch.xcontent.XContentFactory;
|
||||||
|
import org.objectweb.asm.AnnotationVisitor;
|
||||||
|
import org.objectweb.asm.ClassReader;
|
||||||
|
import org.objectweb.asm.Opcodes;
|
||||||
|
import org.objectweb.asm.Type;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class NamedComponentScanner {
|
||||||
|
// main method to be used by gradle build plugin
|
||||||
|
public static void main(String[] args) throws IOException {
|
||||||
|
List<ClassReader> classReaders = ClassReaders.ofClassPath();
|
||||||
|
|
||||||
|
NamedComponentScanner scanner = new NamedComponentScanner();
|
||||||
|
Map<String, Map<String, String>> namedComponentsMap = scanner.scanForNamedClasses(classReaders);
|
||||||
|
Path outputFile = Path.of(args[0]);
|
||||||
|
scanner.writeToFile(namedComponentsMap, outputFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// scope for testing
|
||||||
|
void writeToFile(Map<String, Map<String, String>> namedComponentsMap, Path outputFile) throws IOException {
|
||||||
|
// String json = OBJECT_MAPPER.writeValueAsString(namedComponentsMap);
|
||||||
|
Files.createDirectories(outputFile.getParent());
|
||||||
|
|
||||||
|
try (OutputStream outputStream = Files.newOutputStream(outputFile)) {
|
||||||
|
try (XContentBuilder namedComponents = XContentFactory.jsonBuilder(outputStream)) {
|
||||||
|
namedComponents.startObject();
|
||||||
|
for (Map.Entry<String, Map<String, String>> extensibleToComponents : namedComponentsMap.entrySet()) {
|
||||||
|
namedComponents.startObject(extensibleToComponents.getKey());// extensible class name
|
||||||
|
for (Map.Entry<String, String> components : extensibleToComponents.getValue().entrySet()) {
|
||||||
|
namedComponents.field(components.getKey(), components.getValue());// component name : component class
|
||||||
|
}
|
||||||
|
namedComponents.endObject();
|
||||||
|
}
|
||||||
|
namedComponents.endObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns a Map<String, Map<String,String> - extensible interface -> map{ namedName -> className }
|
||||||
|
public Map<String, Map<String, String>> scanForNamedClasses(List<ClassReader> classReaderStream) {
|
||||||
|
ClassScanner extensibleClassScanner = new ClassScanner(Type.getDescriptor(Extensible.class), (classname, map) -> {
|
||||||
|
map.put(classname, classname);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
extensibleClassScanner.visit(classReaderStream);
|
||||||
|
|
||||||
|
ClassScanner namedComponentsScanner = new ClassScanner(
|
||||||
|
Type.getDescriptor(NamedComponent.class),
|
||||||
|
(classname, map) -> new AnnotationVisitor(Opcodes.ASM9) {
|
||||||
|
@Override
|
||||||
|
public void visit(String key, Object value) {
|
||||||
|
assert key.equals("value");
|
||||||
|
assert value instanceof String;
|
||||||
|
map.put(value.toString(), classname);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
namedComponentsScanner.visit(classReaderStream);
|
||||||
|
|
||||||
|
Map<String, Map<String, String>> componentInfo = new HashMap<>();
|
||||||
|
for (var e : namedComponentsScanner.getFoundClasses().entrySet()) {
|
||||||
|
String name = e.getKey();
|
||||||
|
String classnameWithSlashes = e.getValue();
|
||||||
|
String extensibleClassnameWithSlashes = extensibleClassScanner.getFoundClasses().get(classnameWithSlashes);
|
||||||
|
if (extensibleClassnameWithSlashes == null) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Named component " + name + "(" + pathToClassName(classnameWithSlashes) + ") does not extend from an extensible class"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
var named = componentInfo.computeIfAbsent(pathToClassName(extensibleClassnameWithSlashes), k -> new HashMap<>());
|
||||||
|
named.put(name, pathToClassName(classnameWithSlashes));
|
||||||
|
}
|
||||||
|
return componentInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String pathToClassName(String classWithSlashes) {
|
||||||
|
return classWithSlashes.replace('/', '.');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
/*
|
||||||
|
* 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.plugin.scanner;
|
||||||
|
|
||||||
|
import org.elasticsearch.plugin.api.Extensible;
|
||||||
|
import org.elasticsearch.plugin.api.NamedComponent;
|
||||||
|
import org.elasticsearch.plugin.scanner.test_model.ExtensibleClass;
|
||||||
|
import org.elasticsearch.plugin.scanner.test_model.ExtensibleInterface;
|
||||||
|
import org.elasticsearch.plugin.scanner.test_model.ImplementingExtensible;
|
||||||
|
import org.elasticsearch.plugin.scanner.test_model.SubClass;
|
||||||
|
import org.elasticsearch.test.ESTestCase;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.objectweb.asm.ClassReader;
|
||||||
|
import org.objectweb.asm.Type;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
|
||||||
|
public class AnnotatedHierarchyVisitorTests extends ESTestCase {
|
||||||
|
Set<String> foundClasses;
|
||||||
|
AnnotatedHierarchyVisitor visitor;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void init() {
|
||||||
|
foundClasses = new HashSet<>();
|
||||||
|
visitor = new AnnotatedHierarchyVisitor(Type.getDescriptor(Extensible.class), (className) -> {
|
||||||
|
foundClasses.add(className);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testNoClassesAnnotated() throws IOException, URISyntaxException {
|
||||||
|
performScan(visitor, NamedComponent.class);
|
||||||
|
|
||||||
|
assertTrue(foundClasses.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testSingleAnnotatedClass() throws IOException, URISyntaxException {
|
||||||
|
performScan(visitor, ExtensibleClass.class);
|
||||||
|
|
||||||
|
assertThat(foundClasses, equalTo(Set.of(classNameToPath(ExtensibleClass.class))));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testSubClassofExtensible() throws IOException, URISyntaxException {
|
||||||
|
performScan(visitor, ExtensibleClass.class, SubClass.class);
|
||||||
|
|
||||||
|
assertThat(foundClasses, equalTo(Set.of(classNameToPath(ExtensibleClass.class))));
|
||||||
|
assertThat(
|
||||||
|
visitor.getClassHierarchy(),
|
||||||
|
equalTo(Map.of(classNameToPath(ExtensibleClass.class), Set.of(classNameToPath(SubClass.class))))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testSubInterfaceOfExtensible() throws IOException, URISyntaxException {
|
||||||
|
performScan(visitor, ImplementingExtensible.class, ExtensibleInterface.class);
|
||||||
|
|
||||||
|
assertThat(foundClasses, equalTo(Set.of(classNameToPath(ExtensibleInterface.class))));
|
||||||
|
assertThat(
|
||||||
|
visitor.getClassHierarchy(),
|
||||||
|
equalTo(Map.of(classNameToPath(ExtensibleInterface.class), Set.of(classNameToPath(ImplementingExtensible.class))))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String classNameToPath(Class<?> clazz) {
|
||||||
|
return clazz.getCanonicalName().replace(".", "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void performScan(AnnotatedHierarchyVisitor classVisitor, Class<?>... classes) throws IOException, URISyntaxException {
|
||||||
|
for (Class<?> clazz : classes) {
|
||||||
|
String className = classNameToPath(clazz) + ".class";
|
||||||
|
var stream = this.getClass().getClassLoader().getResourceAsStream(className);
|
||||||
|
try (InputStream fileInputStream = stream) {
|
||||||
|
ClassReader cr = new ClassReader(fileInputStream);
|
||||||
|
cr.accept(classVisitor, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,126 @@
|
||||||
|
/*
|
||||||
|
* 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.plugin.scanner;
|
||||||
|
|
||||||
|
import org.elasticsearch.test.ESTestCase;
|
||||||
|
import org.elasticsearch.test.compiler.InMemoryJavaCompiler;
|
||||||
|
import org.elasticsearch.test.jar.JarUtils;
|
||||||
|
import org.hamcrest.Matchers;
|
||||||
|
import org.objectweb.asm.ClassReader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public class ClassReadersTests extends ESTestCase {
|
||||||
|
|
||||||
|
private Path tmpDir() throws IOException {
|
||||||
|
return createTempDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testModuleInfoIsNotReturnedAsAClassFromJar() throws IOException {
|
||||||
|
final Path tmp = tmpDir();
|
||||||
|
final Path dirWithJar = tmp.resolve("jars-dir");
|
||||||
|
Files.createDirectories(dirWithJar);
|
||||||
|
Path jar = dirWithJar.resolve("api.jar");
|
||||||
|
JarUtils.createJarWithEntries(jar, Map.of("module-info.class", InMemoryJavaCompiler.compile("module-info", """
|
||||||
|
module p {}
|
||||||
|
""")));
|
||||||
|
|
||||||
|
List<ClassReader> classReaders = ClassReaders.ofPaths(Stream.of(jar));
|
||||||
|
org.hamcrest.MatcherAssert.assertThat(classReaders, Matchers.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testTwoClassesInAStreamFromJar() throws IOException {
|
||||||
|
final Path tmp = tmpDir();
|
||||||
|
final Path dirWithJar = tmp.resolve("jars-dir");
|
||||||
|
Files.createDirectories(dirWithJar);
|
||||||
|
Path jar = dirWithJar.resolve("api.jar");
|
||||||
|
JarUtils.createJarWithEntries(jar, Map.of("p/A.class", InMemoryJavaCompiler.compile("p.A", """
|
||||||
|
package p;
|
||||||
|
public class A {}
|
||||||
|
"""), "p/B.class", InMemoryJavaCompiler.compile("p.B", """
|
||||||
|
package p;
|
||||||
|
public class B {}
|
||||||
|
""")));
|
||||||
|
|
||||||
|
List<ClassReader> classReaders = ClassReaders.ofPaths(Stream.of(jar));
|
||||||
|
List<String> collect = classReaders.stream().map(cr -> cr.getClassName()).collect(Collectors.toList());
|
||||||
|
org.hamcrest.MatcherAssert.assertThat(collect, Matchers.containsInAnyOrder("p/A", "p/B"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testStreamOfJarsAndIndividualClasses() throws IOException {
|
||||||
|
final Path tmp = tmpDir();
|
||||||
|
final Path dirWithJar = tmp.resolve("jars-dir");
|
||||||
|
Files.createDirectories(dirWithJar);
|
||||||
|
|
||||||
|
Path jar = dirWithJar.resolve("a_b.jar");
|
||||||
|
JarUtils.createJarWithEntries(jar, Map.of("p/A.class", InMemoryJavaCompiler.compile("p.A", """
|
||||||
|
package p;
|
||||||
|
public class A {}
|
||||||
|
"""), "p/B.class", InMemoryJavaCompiler.compile("p.B", """
|
||||||
|
package p;
|
||||||
|
public class B {}
|
||||||
|
""")));
|
||||||
|
|
||||||
|
Path jar2 = dirWithJar.resolve("c_d.jar");
|
||||||
|
JarUtils.createJarWithEntries(jar2, Map.of("p/C.class", InMemoryJavaCompiler.compile("p.C", """
|
||||||
|
package p;
|
||||||
|
public class C {}
|
||||||
|
"""), "p/D.class", InMemoryJavaCompiler.compile("p.D", """
|
||||||
|
package p;
|
||||||
|
public class D {}
|
||||||
|
""")));
|
||||||
|
|
||||||
|
InMemoryJavaCompiler.compile("p.E", """
|
||||||
|
package p;
|
||||||
|
public class E {}
|
||||||
|
""");
|
||||||
|
Files.write(tmp.resolve("E.class"), InMemoryJavaCompiler.compile("p.E", """
|
||||||
|
package p;
|
||||||
|
public class E {}
|
||||||
|
"""));
|
||||||
|
|
||||||
|
List<ClassReader> classReaders = ClassReaders.ofPaths(Stream.of(tmp, jar, jar2));
|
||||||
|
List<String> collect = classReaders.stream().map(cr -> cr.getClassName()).collect(Collectors.toList());
|
||||||
|
org.hamcrest.MatcherAssert.assertThat(collect, Matchers.containsInAnyOrder("p/A", "p/B", "p/C", "p/D", "p/E"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testMultipleJarsInADir() throws IOException {
|
||||||
|
final Path tmp = tmpDir();
|
||||||
|
final Path dirWithJar = tmp.resolve("jars-dir");
|
||||||
|
Files.createDirectories(dirWithJar);
|
||||||
|
|
||||||
|
Path jar = dirWithJar.resolve("a_b.jar");
|
||||||
|
JarUtils.createJarWithEntries(jar, Map.of("p/A.class", InMemoryJavaCompiler.compile("p.A", """
|
||||||
|
package p;
|
||||||
|
public class A {}
|
||||||
|
"""), "p/B.class", InMemoryJavaCompiler.compile("p.B", """
|
||||||
|
package p;
|
||||||
|
public class B {}
|
||||||
|
""")));
|
||||||
|
|
||||||
|
Path jar2 = dirWithJar.resolve("c_d.jar");
|
||||||
|
JarUtils.createJarWithEntries(jar2, Map.of("p/C.class", InMemoryJavaCompiler.compile("p.C", """
|
||||||
|
package p;
|
||||||
|
public class C {}
|
||||||
|
"""), "p/D.class", InMemoryJavaCompiler.compile("p.D", """
|
||||||
|
package p;
|
||||||
|
public class D {}
|
||||||
|
""")));
|
||||||
|
|
||||||
|
List<ClassReader> classReaders = ClassReaders.ofDirWithJars(dirWithJar.toString());
|
||||||
|
List<String> collect = classReaders.stream().map(cr -> cr.getClassName()).collect(Collectors.toList());
|
||||||
|
org.hamcrest.MatcherAssert.assertThat(collect, Matchers.containsInAnyOrder("p/A", "p/B", "p/C", "p/D"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* 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.plugin.scanner;
|
||||||
|
|
||||||
|
import org.elasticsearch.plugin.api.Extensible;
|
||||||
|
import org.elasticsearch.test.ESTestCase;
|
||||||
|
import org.hamcrest.Matchers;
|
||||||
|
import org.objectweb.asm.ClassReader;
|
||||||
|
import org.objectweb.asm.Type;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class ClassScannerTests extends ESTestCase {
|
||||||
|
static final System.Logger logger = System.getLogger(ClassScannerTests.class.getName());
|
||||||
|
|
||||||
|
public void testClassAndInterfaceHierarchy() throws IOException {
|
||||||
|
var reader = new ClassScanner(Type.getDescriptor(Extensible.class), (classname, map) -> {
|
||||||
|
map.put(classname, classname);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
List<ClassReader> classReaders = ClassReaders.ofClassPath();
|
||||||
|
logger.log(System.Logger.Level.INFO, "classReaderStream size " + classReaders.size());
|
||||||
|
|
||||||
|
reader.visit(classReaders);
|
||||||
|
Map<String, String> extensibleClasses = reader.getFoundClasses();
|
||||||
|
|
||||||
|
org.hamcrest.MatcherAssert.assertThat(
|
||||||
|
extensibleClasses,
|
||||||
|
Matchers.allOf(
|
||||||
|
Matchers.hasEntry(
|
||||||
|
"org/elasticsearch/plugin/scanner/test_model/ExtensibleClass",
|
||||||
|
"org/elasticsearch/plugin/scanner/test_model/ExtensibleClass"
|
||||||
|
),
|
||||||
|
Matchers.hasEntry(
|
||||||
|
"org/elasticsearch/plugin/scanner/test_model/ImplementingExtensible",
|
||||||
|
"org/elasticsearch/plugin/scanner/test_model/ExtensibleInterface"
|
||||||
|
),
|
||||||
|
Matchers.hasEntry(
|
||||||
|
"org/elasticsearch/plugin/scanner/test_model/SubClass",
|
||||||
|
"org/elasticsearch/plugin/scanner/test_model/ExtensibleClass"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,222 @@
|
||||||
|
/*
|
||||||
|
* 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.plugin.scanner;
|
||||||
|
|
||||||
|
import org.elasticsearch.plugin.scanner.test_model.ExtensibleClass;
|
||||||
|
import org.elasticsearch.plugin.scanner.test_model.ExtensibleInterface;
|
||||||
|
import org.elasticsearch.plugin.scanner.test_model.TestNamedComponent;
|
||||||
|
import org.elasticsearch.test.ESTestCase;
|
||||||
|
import org.elasticsearch.test.compiler.InMemoryJavaCompiler;
|
||||||
|
import org.elasticsearch.test.jar.JarUtils;
|
||||||
|
import org.objectweb.asm.ClassReader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
|
||||||
|
public class NamedComponentScannerTests extends ESTestCase {
|
||||||
|
|
||||||
|
private Path tmpDir() throws IOException {
|
||||||
|
return createTempDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
NamedComponentScanner namedComponentScanner = new NamedComponentScanner();
|
||||||
|
|
||||||
|
public void testFindNamedComponentInSingleClass() throws URISyntaxException {
|
||||||
|
Map<String, Map<String, String>> namedComponents = namedComponentScanner.scanForNamedClasses(
|
||||||
|
classReaderStream(TestNamedComponent.class, ExtensibleInterface.class)
|
||||||
|
);
|
||||||
|
|
||||||
|
org.hamcrest.MatcherAssert.assertThat(
|
||||||
|
namedComponents,
|
||||||
|
equalTo(
|
||||||
|
Map.of(
|
||||||
|
ExtensibleInterface.class.getCanonicalName(),
|
||||||
|
Map.of("test_named_component", TestNamedComponent.class.getCanonicalName())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testNamedComponentsAreFoundWhenSingleJarProvided() throws IOException {
|
||||||
|
final Path tmp = tmpDir();
|
||||||
|
final Path dirWithJar = tmp.resolve("jars-dir");
|
||||||
|
Files.createDirectories(dirWithJar);
|
||||||
|
Path jar = dirWithJar.resolve("plugin.jar");
|
||||||
|
JarUtils.createJarWithEntries(jar, Map.of("p/A.class", InMemoryJavaCompiler.compile("p.A", """
|
||||||
|
package p;
|
||||||
|
import org.elasticsearch.plugin.api.*;
|
||||||
|
import org.elasticsearch.plugin.scanner.test_model.*;
|
||||||
|
@NamedComponent("a_component")
|
||||||
|
public class A extends ExtensibleClass {}
|
||||||
|
"""), "p/B.class", InMemoryJavaCompiler.compile("p.B", """
|
||||||
|
package p;
|
||||||
|
import org.elasticsearch.plugin.api.*;
|
||||||
|
import org.elasticsearch.plugin.scanner.test_model.*;
|
||||||
|
@NamedComponent("b_component")
|
||||||
|
public class B implements ExtensibleInterface{}
|
||||||
|
""")));
|
||||||
|
List<ClassReader> classReaderStream = Stream.concat(
|
||||||
|
ClassReaders.ofDirWithJars(dirWithJar.toString()).stream(),
|
||||||
|
ClassReaders.ofClassPath().stream()
|
||||||
|
)// contains plugin-api
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
Map<String, Map<String, String>> namedComponents = namedComponentScanner.scanForNamedClasses(classReaderStream);
|
||||||
|
|
||||||
|
org.hamcrest.MatcherAssert.assertThat(
|
||||||
|
namedComponents,
|
||||||
|
equalTo(
|
||||||
|
Map.of(
|
||||||
|
ExtensibleClass.class.getCanonicalName(),
|
||||||
|
Map.of("a_component", "p.A"),
|
||||||
|
ExtensibleInterface.class.getCanonicalName(),
|
||||||
|
Map.of(
|
||||||
|
"b_component",
|
||||||
|
"p.B",
|
||||||
|
// noise from classpath
|
||||||
|
"test_named_component",
|
||||||
|
"org.elasticsearch.plugin.scanner.test_model.TestNamedComponent"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testNamedComponentsCanExtednCommonSuperClass() throws IOException {
|
||||||
|
Map<String, CharSequence> sources = Map.of(
|
||||||
|
"p.CustomExtensibleInterface",
|
||||||
|
"""
|
||||||
|
package p;
|
||||||
|
import org.elasticsearch.plugin.api.*;
|
||||||
|
import org.elasticsearch.plugin.scanner.test_model.*;
|
||||||
|
public interface CustomExtensibleInterface extends ExtensibleInterface {}
|
||||||
|
""",
|
||||||
|
// note that this class implements a custom interface
|
||||||
|
"p.CustomExtensibleClass",
|
||||||
|
"""
|
||||||
|
package p;
|
||||||
|
import org.elasticsearch.plugin.api.*;
|
||||||
|
import org.elasticsearch.plugin.scanner.test_model.*;
|
||||||
|
public class CustomExtensibleClass implements CustomExtensibleInterface {}
|
||||||
|
""",
|
||||||
|
"p.A",
|
||||||
|
"""
|
||||||
|
package p;
|
||||||
|
import org.elasticsearch.plugin.api.*;
|
||||||
|
import org.elasticsearch.plugin.scanner.test_model.*;
|
||||||
|
@NamedComponent("a_component")
|
||||||
|
public class A extends CustomExtensibleClass {}
|
||||||
|
""",
|
||||||
|
"p.B",
|
||||||
|
"""
|
||||||
|
package p;
|
||||||
|
import org.elasticsearch.plugin.api.*;
|
||||||
|
import org.elasticsearch.plugin.scanner.test_model.*;
|
||||||
|
@NamedComponent("b_component")
|
||||||
|
public class B implements CustomExtensibleInterface{}
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
var classToBytes = InMemoryJavaCompiler.compile(sources);
|
||||||
|
|
||||||
|
Map<String, byte[]> jarEntries = new HashMap<>();
|
||||||
|
jarEntries.put("p/CustomExtensibleInterface.class", classToBytes.get("p.CustomExtensibleInterface"));
|
||||||
|
jarEntries.put("p/CustomExtensibleClass.class", classToBytes.get("p.CustomExtensibleClass"));
|
||||||
|
jarEntries.put("p/A.class", classToBytes.get("p.A"));
|
||||||
|
jarEntries.put("p/B.class", classToBytes.get("p.B"));
|
||||||
|
|
||||||
|
final Path tmp = tmpDir();
|
||||||
|
final Path dirWithJar = tmp.resolve("jars-dir");
|
||||||
|
Files.createDirectories(dirWithJar);
|
||||||
|
Path jar = dirWithJar.resolve("plugin.jar");
|
||||||
|
JarUtils.createJarWithEntries(jar, jarEntries);
|
||||||
|
|
||||||
|
List<ClassReader> classReaderStream = Stream.concat(
|
||||||
|
ClassReaders.ofDirWithJars(dirWithJar.toString()).stream(),
|
||||||
|
ClassReaders.ofClassPath().stream()
|
||||||
|
)// contains plugin-api
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
Map<String, Map<String, String>> namedComponents = namedComponentScanner.scanForNamedClasses(classReaderStream);
|
||||||
|
|
||||||
|
org.hamcrest.MatcherAssert.assertThat(
|
||||||
|
namedComponents,
|
||||||
|
equalTo(
|
||||||
|
Map.of(
|
||||||
|
ExtensibleInterface.class.getCanonicalName(),
|
||||||
|
Map.of(
|
||||||
|
"a_component",
|
||||||
|
"p.A",
|
||||||
|
"b_component",
|
||||||
|
"p.B",
|
||||||
|
"test_named_component",
|
||||||
|
"org.elasticsearch.plugin.scanner.test_model.TestNamedComponent"// noise from classpath
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testWriteToFile() throws IOException {
|
||||||
|
Map<String, String> extensibleInterfaceComponents = new LinkedHashMap<>();
|
||||||
|
extensibleInterfaceComponents.put("a_component", "p.A");
|
||||||
|
extensibleInterfaceComponents.put("b_component", "p.B");
|
||||||
|
Map<String, Map<String, String>> mapToWrite = new LinkedHashMap<>();
|
||||||
|
mapToWrite.put(ExtensibleInterface.class.getCanonicalName(), extensibleInterfaceComponents);
|
||||||
|
|
||||||
|
Path path = tmpDir().resolve("file.json");
|
||||||
|
namedComponentScanner.writeToFile(mapToWrite, path);
|
||||||
|
|
||||||
|
String jsonMap = Files.readString(path);
|
||||||
|
assertThat(jsonMap, equalTo("""
|
||||||
|
{
|
||||||
|
"org.elasticsearch.plugin.scanner.test_model.ExtensibleInterface": {
|
||||||
|
"a_component": "p.A",
|
||||||
|
"b_component": "p.B"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".replaceAll("[\n\r\s]", "")));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ClassReader> classReaderStream(Class<?>... classes) {
|
||||||
|
try {
|
||||||
|
return Arrays.stream(classes).map(clazz -> {
|
||||||
|
String className = classNameToPath(clazz) + ".class";
|
||||||
|
var stream = this.getClass().getClassLoader().getResourceAsStream(className);
|
||||||
|
try (InputStream is = stream) {
|
||||||
|
byte[] classBytes = is.readAllBytes();
|
||||||
|
ClassReader classReader = new ClassReader(classBytes);
|
||||||
|
return classReader;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String classNameToPath(Class<?> clazz) {
|
||||||
|
return clazz.getCanonicalName().replace(".", "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
/*
|
||||||
|
* 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.plugin.scanner.test_model;
|
||||||
|
|
||||||
|
import org.elasticsearch.plugin.api.Extensible;
|
||||||
|
|
||||||
|
@Extensible
|
||||||
|
public class ExtensibleClass {}
|
|
@ -0,0 +1,14 @@
|
||||||
|
/*
|
||||||
|
* 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.plugin.scanner.test_model;
|
||||||
|
|
||||||
|
import org.elasticsearch.plugin.api.Extensible;
|
||||||
|
|
||||||
|
@Extensible
|
||||||
|
public interface ExtensibleInterface {}
|
|
@ -0,0 +1,11 @@
|
||||||
|
/*
|
||||||
|
* 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.plugin.scanner.test_model;
|
||||||
|
|
||||||
|
public class ImplementingExtensible implements ExtensibleInterface {}
|
|
@ -0,0 +1,11 @@
|
||||||
|
/*
|
||||||
|
* 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.plugin.scanner.test_model;
|
||||||
|
|
||||||
|
public class SubClass extends ExtensibleClass {}
|
|
@ -0,0 +1,14 @@
|
||||||
|
/*
|
||||||
|
* 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.plugin.scanner.test_model;
|
||||||
|
|
||||||
|
@org.elasticsearch.plugin.api.NamedComponent("test_named_component")
|
||||||
|
public class TestNamedComponent implements ExtensibleInterface {
|
||||||
|
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue