Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 0 additions & 20 deletions NOTICE.TXT
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>

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
----------
Expand Down
3 changes: 0 additions & 3 deletions logstash-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
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.net.URLConnection;
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 static final String CLASS_SUFFIX = ".class";

private PackageScanner() {
}

private static String toClassName(String resourcePath) {
return resourcePath
.substring(0, resourcePath.length() - CLASS_SUFFIX.length())
.replace('/', '.')
.replace('\\', '.');
}

static Set<Class<?>> scanForAnnotation(String basePackage, Class<? extends Annotation> annotation, ClassLoader loader) {
Set<String> classNames = collectClassNames(basePackage, loader);
Set<Class<?>> 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;
Comment on lines +38 to +52
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that the size of classNames is more than some tens, I think we could use stream syntax to separate looping from logic to be applied:

Suggested change
Set<Class<?>> 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;
Set<Class<?>> result =classNames.stream()
.filter(PackageScanner::isInnerClass)
.map(className -> loadClass(loader, className))
.filter(cls -> cls.isAnnotationPresent(annotation))
.collect(Collectors.toSet());

where the isInenrClass and loadClass methods are:

    private static Class<?> loadClass(ClassLoader loader, String className) {
        try {
            return Class.forName(className, false, loader);
        } catch (ClassNotFoundException | LinkageError e) {
            throw new IllegalStateException("Unable to load class discovered during scanning: " + className, e);
        }
    }

    private static boolean isInnerClass(String name) {
        return name.contains("$");
    }

}

private static Set<String> collectClassNames(String basePackage, ClassLoader loader) {
String resourcePath = basePackage.replace('.', '/');
Set<String> classNames = new HashSet<>();
try {
Enumeration<URL> resources = loader.getResources(resourcePath);
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
String protocol = resource.getProtocol();
if ("file".equals(protocol)) {
scanDirectory(resource, basePackage, classNames);
} else {
// Open connection once to check type and reuse for scanning
URLConnection connection = resource.openConnection();
if (connection instanceof JarURLConnection) {
scanJar((JarURLConnection) connection, resourcePath, classNames);
}
Comment on lines +67 to +70
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URLConnection opened on line 67 is not explicitly closed. While JarURLConnection is handled with try-with-resources in scanJar, if the connection is not a JarURLConnection but is some other type of URLConnection that requires cleanup, it may lead to a resource leak. Consider closing the connection in a finally block or using try-with-resources if the connection type is uncertain.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scanJar cover with try-with-resources just the JarFile obtained from connection.getJarFile() not the URLConnection itself.

@jsvd maybe we could use a try-with-resources when we get the connection at resource.openConnection();

Suggested change
URLConnection connection = resource.openConnection();
if (connection instanceof JarURLConnection) {
scanJar((JarURLConnection) connection, resourcePath, classNames);
}
try (URLConnection connection = resource.openConnection()) {
if (connection instanceof JarURLConnection) {
scanJar((JarURLConnection) connection, 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<String> classNames) {
try {
Path directory = Paths.get(resource.toURI());
if (!Files.exists(directory)) {
return;
}
try (java.util.stream.Stream<Path> stream = Files.walk(directory)) {
stream.filter(Files::isRegularFile)
.filter(path -> path.getFileName().toString().endsWith(CLASS_SUFFIX))
.forEach(path -> {
Path relative = directory.relativize(path);
classNames.add(basePackage + '.' + toClassName(relative.toString()));
});
}
} catch (IOException | URISyntaxException e) {
throw new IllegalStateException("Failed to scan directory for classes: " + resource, e);
}
}

private static void scanJar(JarURLConnection connection, String resourcePath, Set<String> classNames) {
try {
// Disable caching to prevent file locking issues (especially on Windows)
// and ensure proper JAR file cleanup after scanning
connection.setUseCaches(false);
try (JarFile jarFile = connection.getJarFile()) {
String entryPrefix = connection.getEntryName();
if (entryPrefix == null || entryPrefix.isEmpty()) {
entryPrefix = resourcePath;
}
if (!entryPrefix.endsWith("/")) {
entryPrefix += "/";
}
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (entry.isDirectory()) {
continue;
}
String name = entry.getName();
if (!name.endsWith(CLASS_SUFFIX) || !name.startsWith(entryPrefix)) {
continue;
}
classNames.add(toClassName(name));
}
}
} catch (IOException e) {
throw new UncheckedIOException("Failed to scan jar for classes: " + connection.getURL(), e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -47,7 +44,7 @@
* This is singleton ofr two reasons:
* <ul>
* <li>it's a registry so no need for multiple instances</li>
* <li>the Reflections library used need to run in single thread during discovery phase</li>
* <li>plugin discovery touches the classpath, so we keep it single-threaded</li>
* </ul>
* */
public final class PluginRegistry {
Expand Down Expand Up @@ -79,34 +76,25 @@ 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<Class<?>> annotated = reflections.getTypesAnnotatedWith(LogstashPlugin.class);
// the discovery runs single-threaded to avoid concurrent access issues while scanning
Set<Class<?>> 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<Filter>) cls);
}
if (Output.class.isAssignableFrom(cls)) {
outputs.put(name, (Class<Output>) cls);
}
if (Input.class.isAssignableFrom(cls)) {
inputs.put(name, (Class<Input>) cls);
}
if (Codec.class.isAssignableFrom(cls)) {
codecs.put(name, (Class<Codec>) cls);
}

break;
}
if (Modifier.isAbstract(cls.getModifiers())) {
continue;
}

String name = cls.getAnnotation(LogstashPlugin.class).name();
if (Filter.class.isAssignableFrom(cls)) {
filters.put(name, (Class<Filter>) cls);
}
if (Output.class.isAssignableFrom(cls)) {
outputs.put(name, (Class<Output>) cls);
}
if (Input.class.isAssignableFrom(cls)) {
inputs.put(name, (Class<Input>) cls);
}
if (Codec.class.isAssignableFrom(cls)) {
codecs.put(name, (Class<Codec>) cls);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.snakeyaml:snakeyaml-engine:2.9",https://bitbucket.org/snakeyaml/snakeyaml-engine,Apache-2.0
"org.yaml:snakeyaml:",https://bitbucket.org/snakeyaml/snakeyaml/src/master/,Apache-2.0
Expand Down

This file was deleted.