diff --git a/NOTICE.TXT b/NOTICE.TXT index 7a5105a33ba..f16c0c964c6 100644 --- a/NOTICE.TXT +++ b/NOTICE.TXT @@ -11379,26 +11379,6 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -========== -Notice for: org.reflections:reflections-0.10.2 ----------- - - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - Version 2, December 2004 - - Copyright (C) 2004 Sam Hocevar - - Everyone is permitted to copy and distribute verbatim or modified - copies of this license document, and changing it is allowed as long - as the name is changed. - - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. You just DO WHAT THE FUCK YOU WANT TO. - -Also licensed under BSD-2-Clause according to https://github.com/ronmamo/reflections/blob/0.9.11/pom.xml#L20-L22 - ========== Notice for: org.slf4j:slf4j-api-1.7.30 ---------- diff --git a/logstash-core/build.gradle b/logstash-core/build.gradle index de201dc6679..693e4413656 100644 --- a/logstash-core/build.gradle +++ b/logstash-core/build.gradle @@ -225,9 +225,6 @@ dependencies { runtimeOnly 'commons-logging:commons-logging:1.3.1' // also handle libraries relying on log4j 1.x to redirect their logs runtimeOnly "org.apache.logging.log4j:log4j-1.2-api:${log4jVersion}" - implementation('org.reflections:reflections:0.10.2') { - exclude group: 'com.google.guava', module: 'guava' - } implementation 'commons-codec:commons-codec:1.17.0' // transitively required by httpclient // Jackson version moved to versions.yml in the project root (the JrJackson version is there too) implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" diff --git a/logstash-core/src/main/java/org/logstash/plugins/discovery/PackageScanner.java b/logstash-core/src/main/java/org/logstash/plugins/discovery/PackageScanner.java new file mode 100644 index 00000000000..45daf1b784c --- /dev/null +++ b/logstash-core/src/main/java/org/logstash/plugins/discovery/PackageScanner.java @@ -0,0 +1,117 @@ +package org.logstash.plugins.discovery; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.annotation.Annotation; +import java.net.JarURLConnection; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * Minimal package scanner to locate classes within Logstash's own packages. + */ +final class PackageScanner { + + private PackageScanner() { + } + + static Set> scanForAnnotation(String basePackage, Class annotation, ClassLoader loader) { + Set classNames = collectClassNames(basePackage, loader); + Set> result = new HashSet<>(); + for (String className : classNames) { + if (className.contains("$")) { + continue; + } + try { + Class candidate = Class.forName(className, false, loader); + if (candidate.isAnnotationPresent(annotation)) { + result.add(candidate); + } + } catch (ClassNotFoundException | LinkageError e) { + throw new IllegalStateException("Unable to load class discovered during scanning: " + className, e); + } + } + return result; + } + + private static Set collectClassNames(String basePackage, ClassLoader loader) { + String resourcePath = basePackage.replace('.', '/'); + Set classNames = new HashSet<>(); + try { + Enumeration resources = loader.getResources(resourcePath); + while (resources.hasMoreElements()) { + URL resource = resources.nextElement(); + String protocol = resource.getProtocol(); + if ("file".equals(protocol)) { + scanDirectory(resource, basePackage, classNames); + } else if ("jar".equals(protocol) || resource.toString().contains("!")) { + scanJar(resource, resourcePath, classNames); + } + } + } catch (IOException e) { + throw new UncheckedIOException("Failed to scan package: " + basePackage, e); + } + return classNames; + } + + private static void scanDirectory(URL resource, String basePackage, Set classNames) { + try { + Path directory = Paths.get(resource.toURI()); + if (!Files.exists(directory)) { + return; + } + Files.walk(directory) + .filter(Files::isRegularFile) + .filter(path -> path.getFileName().toString().endsWith(".class")) + .forEach(path -> { + Path relative = directory.relativize(path); + String className = basePackage + '.' + relative.toString() + .replace('/', '.').replace('\\', '.'); + className = className.substring(0, className.length() - ".class".length()); + classNames.add(className); + }); + } catch (IOException | URISyntaxException e) { + throw new IllegalStateException("Failed to scan directory for classes: " + resource, e); + } + } + + private static void scanJar(URL resource, String resourcePath, Set classNames) { + try { + JarURLConnection connection = (JarURLConnection) resource.openConnection(); + connection.setUseCaches(false); + try (JarFile jarFile = connection.getJarFile()) { + String entryPrefix = connection.getEntryName(); + if (entryPrefix == null || entryPrefix.isEmpty()) { + entryPrefix = resourcePath; + } + if (!entryPrefix.endsWith("/")) { + entryPrefix += "/"; + } + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + if (entry.isDirectory()) { + continue; + } + String name = entry.getName(); + if (!name.endsWith(".class") || !name.startsWith(entryPrefix)) { + continue; + } + String className = name.substring(0, name.length() - ".class".length()) + .replace('/', '.'); + classNames.add(className); + } + } + } catch (IOException e) { + throw new UncheckedIOException("Failed to scan jar for classes: " + resource, e); + } + } +} diff --git a/logstash-core/src/main/java/org/logstash/plugins/discovery/PluginRegistry.java b/logstash-core/src/main/java/org/logstash/plugins/discovery/PluginRegistry.java index 301d5430dc1..984b598fbd3 100644 --- a/logstash-core/src/main/java/org/logstash/plugins/discovery/PluginRegistry.java +++ b/logstash-core/src/main/java/org/logstash/plugins/discovery/PluginRegistry.java @@ -31,12 +31,9 @@ import co.elastic.logstash.api.LogstashPlugin; import co.elastic.logstash.api.Output; import org.logstash.plugins.PluginLookup.PluginType; -import org.reflections.Reflections; -import org.reflections.util.ClasspathHelper; -import org.reflections.util.ConfigurationBuilder; -import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -47,7 +44,7 @@ * This is singleton ofr two reasons: *
    *
  • it's a registry so no need for multiple instances
  • - *
  • the Reflections library used need to run in single thread during discovery phase
  • + *
  • plugin discovery touches the classpath, so we keep it single-threaded
  • *
* */ public final class PluginRegistry { @@ -79,34 +76,30 @@ public static PluginRegistry getInstance() { @SuppressWarnings("unchecked") private void discoverPlugins() { - // the constructor of Reflection must be called only by one thread, else there is a - // risk that the first thread that completes close the Zip files for the others. - // scan all .class present in package classpath - final ConfigurationBuilder configurationBuilder = new ConfigurationBuilder() - .setUrls(ClasspathHelper.forPackage("org.logstash.plugins")) - .filterInputsBy(input -> input.endsWith(".class")); - Reflections reflections = new Reflections(configurationBuilder); - - Set> annotated = reflections.getTypesAnnotatedWith(LogstashPlugin.class); + // the discovery runs single-threaded to avoid concurrent access issues while scanning + Set> annotated = PackageScanner.scanForAnnotation("org.logstash.plugins", LogstashPlugin.class, PluginRegistry.class.getClassLoader()); for (final Class cls : annotated) { - for (final Annotation annotation : cls.getAnnotations()) { - if (annotation instanceof LogstashPlugin) { - String name = ((LogstashPlugin) annotation).name(); - if (Filter.class.isAssignableFrom(cls)) { - filters.put(name, (Class) cls); - } - if (Output.class.isAssignableFrom(cls)) { - outputs.put(name, (Class) cls); - } - if (Input.class.isAssignableFrom(cls)) { - inputs.put(name, (Class) cls); - } - if (Codec.class.isAssignableFrom(cls)) { - codecs.put(name, (Class) cls); - } - - break; - } + if (Modifier.isAbstract(cls.getModifiers())) { + continue; + } + + LogstashPlugin annotation = cls.getAnnotation(LogstashPlugin.class); + if (annotation == null) { + continue; + } + + String name = annotation.name(); + if (Filter.class.isAssignableFrom(cls)) { + filters.put(name, (Class) cls); + } + if (Output.class.isAssignableFrom(cls)) { + outputs.put(name, (Class) cls); + } + if (Input.class.isAssignableFrom(cls)) { + inputs.put(name, (Class) cls); + } + if (Codec.class.isAssignableFrom(cls)) { + codecs.put(name, (Class) cls); } } diff --git a/logstash-core/src/test/java/org/logstash/plugins/discovery/PluginRegistryTest.java b/logstash-core/src/test/java/org/logstash/plugins/discovery/PluginRegistryTest.java new file mode 100644 index 00000000000..a355dd5c2ab --- /dev/null +++ b/logstash-core/src/test/java/org/logstash/plugins/discovery/PluginRegistryTest.java @@ -0,0 +1,43 @@ +package org.logstash.plugins.discovery; + +import co.elastic.logstash.api.Codec; +import co.elastic.logstash.api.Filter; +import co.elastic.logstash.api.Input; +import co.elastic.logstash.api.Output; +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class PluginRegistryTest { + + private final PluginRegistry registry = PluginRegistry.getInstance(); + + @Test + public void discoversBuiltInInputPlugin() { + Class input = registry.getInputClass("java_stdin"); + assertNotNull(input); + assertTrue(Input.class.isAssignableFrom(input)); + } + + @Test + public void discoversBuiltInOutputPlugin() { + Class output = registry.getOutputClass("java_stdout"); + assertNotNull(output); + assertTrue(Output.class.isAssignableFrom(output)); + } + + @Test + public void discoversBuiltInFilterPlugin() { + Class filter = registry.getFilterClass("java_uuid"); + assertNotNull(filter); + assertTrue(Filter.class.isAssignableFrom(filter)); + } + + @Test + public void discoversBuiltInCodecPlugin() { + Class codec = registry.getCodecClass("java_line"); + assertNotNull(codec); + assertTrue(Codec.class.isAssignableFrom(codec)); + } +} diff --git a/tools/dependencies-report/src/main/resources/licenseMapping.csv b/tools/dependencies-report/src/main/resources/licenseMapping.csv index 14fb7c8c28a..f7fb6310cf1 100644 --- a/tools/dependencies-report/src/main/resources/licenseMapping.csv +++ b/tools/dependencies-report/src/main/resources/licenseMapping.csv @@ -172,7 +172,6 @@ dependency,dependencyUrl,licenseOverride,copyright,sourceURL "org.javassist:javassist:",https://github.com/jboss-javassist/javassist,Apache-2.0 "org.jruby:jruby-core:",http://jruby.org/,EPL-2.0 "org.logstash:jvm-options-parser:",http://github.com/elastic/logstash,Apache-2.0 -"org.reflections:reflections:",https://github.com/ronmamo/reflections,BSD-2-Clause "org.slf4j:slf4j-api:",http://www.slf4j.org/,MIT "org.yaml:snakeyaml:",https://bitbucket.org/snakeyaml/snakeyaml/src/master/,Apache-2.0 "ostruct:",https://github.com/ruby/ostruct,BSD-2-Clause diff --git a/tools/dependencies-report/src/main/resources/notices/org.reflections!reflections-NOTICE.txt b/tools/dependencies-report/src/main/resources/notices/org.reflections!reflections-NOTICE.txt deleted file mode 100644 index 77f62ea0f7d..00000000000 --- a/tools/dependencies-report/src/main/resources/notices/org.reflections!reflections-NOTICE.txt +++ /dev/null @@ -1,15 +0,0 @@ - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - Version 2, December 2004 - - Copyright (C) 2004 Sam Hocevar - - Everyone is permitted to copy and distribute verbatim or modified - copies of this license document, and changing it is allowed as long - as the name is changed. - - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. You just DO WHAT THE FUCK YOU WANT TO. - -Also licensed under BSD-2-Clause according to https://github.com/ronmamo/reflections/blob/0.9.11/pom.xml#L20-L22