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 <sam@hocevar.net>

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,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<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;
}

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 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<String> 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<String> 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<JarEntry> 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);
}
}
}
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,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<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;
}

LogstashPlugin annotation = cls.getAnnotation(LogstashPlugin.class);
if (annotation == null) {
continue;
}

String name = 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);
}
}

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.yaml:snakeyaml:",https://bitbucket.org/snakeyaml/snakeyaml/src/master/,Apache-2.0
"ostruct:",https://github.com/ruby/ostruct,BSD-2-Clause
Expand Down

This file was deleted.