org.restheart.plugins.PluginsScanner Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of restheart Show documentation
Show all versions of restheart Show documentation
RESTHeart Core - Core services for RESTHeart
/*-
* ========================LICENSE_START=================================
* restheart-core
* %%
* Copyright (C) 2014 - 2024 SoftInstigate
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
* =========================LICENSE_END==================================
*/
package org.restheart.plugins;
import io.github.classgraph.AnnotationEnumValue;
import io.github.classgraph.ClassGraph;
import io.github.classgraph.ClassInfo;
import io.github.classgraph.ClassInfoList;
import io.github.classgraph.ScanResult;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.AbstractMap;
import org.restheart.graal.ImageInfo;
import org.restheart.Bootstrapper;
import org.restheart.plugins.security.AuthMechanism;
import org.restheart.plugins.security.Authenticator;
import org.restheart.plugins.security.Authorizer;
import org.restheart.plugins.security.TokenManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import graphql.Assert;
/**
* this class is configured to be initialized at build time by native-image
* note: we cannot use logging in this class, otherwise native-image will fail
*
* @author Andrea Di Cesare {@literal }
*/
public class PluginsScanner {
private static final String REGISTER_PLUGIN_CLASS_NAME = RegisterPlugin.class.getName();
private static final String INITIALIZER_CLASS_NAME = Initializer.class.getName();
private static final String AUTHMECHANISM_CLASS_NAME = AuthMechanism.class.getName();
private static final String AUTHORIZER_CLASS_NAME = Authorizer.class.getName();
private static final String TOKEN_MANAGER_CLASS_NAME = TokenManager.class.getName();
private static final String AUTHENTICATOR_CLASS_NAME = Authenticator.class.getName();
private static final String INTERCEPTOR_CLASS_NAME = Interceptor.class.getName();
private static final String SERVICE_CLASS_NAME = Service.class.getName();
private static final String PROVIDER_CLASS_NAME = Provider.class.getName();
private static final ArrayList INITIALIZERS = new ArrayList<>();
private static final ArrayList AUTH_MECHANISMS = new ArrayList<>();
private static final ArrayList AUTHORIZERS = new ArrayList<>();
private static final ArrayList TOKEN_MANAGERS = new ArrayList<>();
private static final ArrayList AUTHENTICATORS = new ArrayList<>();
private static final ArrayList INTERCEPTORS = new ArrayList<>();
private static final ArrayList SERVICES = new ArrayList<>();
private static final ArrayList PROVIDERS = new ArrayList<>();
// ClassGraph.scan() at class initialization time to support native image
// generation with GraalVM
// see https://github.com/SoftInstigate/classgraph-on-graalvm
static {
ClassGraph classGraph;
RuntimeClassGraph rtcg = null;
if (ImageInfo.inImageBuildtimeCode()) {
// initizialize PluginsClassloader with the URL of restheart.jar uber jar
// during build time the class this class is loaded by
var jarPath = PluginsScanner.class.getProtectionDomain().getCodeSource().getLocation().getPath();
try {
var jarFile = new File(jarPath);
var jarURL = jarFile.toURI().toURL();
URL[] urls = { jarURL };
PluginsClassloader.init(urls);
} catch(MalformedURLException mue) {
System.err.println("Error initilizing PluginsClassloader on restheart uber jar " + jarPath + ". Exception: " + mue.getMessage());
}
classGraph = new ClassGraph()
.disableDirScanning() // added for GraalVM
.disableNestedJarScanning() // added for GraalVM
.disableRuntimeInvisibleAnnotations() // added for GraalVM
.overrideClassLoaders(PluginsClassloader.getInstance()) // added for GraalVM. Mandatory, otherwise build fails
.ignoreParentClassLoaders()
.enableAnnotationInfo().enableMethodInfo().enableFieldInfo().ignoreFieldVisibility().initializeLoadedClasses();
} else {
rtcg = new RuntimeClassGraph();
classGraph = rtcg.get();
// apply plugins-scanning-verbose configuration option
classGraph = classGraph.verbose(Bootstrapper.getConfiguration().coreModule().pluginsScanningVerbose());
// apply plugins-packages configuration option
var pluginsPackages = Bootstrapper.getConfiguration().coreModule().pluginsPackages();
if (!Bootstrapper.getConfiguration().coreModule().pluginsPackages().isEmpty()) {
classGraph = classGraph.acceptPackages(pluginsPackages.toArray(String[]::new));
}
rtcg.logStartScan();
}
try (var scanResult = classGraph.scan(Runtime.getRuntime().availableProcessors())) {
INITIALIZERS.addAll(collectPlugins(scanResult, INITIALIZER_CLASS_NAME));
AUTH_MECHANISMS.addAll(collectPlugins(scanResult, AUTHMECHANISM_CLASS_NAME));
AUTHORIZERS.addAll(collectPlugins(scanResult, AUTHORIZER_CLASS_NAME));
TOKEN_MANAGERS.addAll(collectPlugins(scanResult, TOKEN_MANAGER_CLASS_NAME));
AUTHENTICATORS.addAll(collectPlugins(scanResult, AUTHENTICATOR_CLASS_NAME));
INTERCEPTORS.addAll(collectPlugins(scanResult, INTERCEPTOR_CLASS_NAME));
SERVICES.addAll(collectPlugins(scanResult, SERVICE_CLASS_NAME));
PROVIDERS.addAll(collectProviders(scanResult));
}
if (rtcg != null) {
rtcg.logEndScan();
}
}
public static List allPluginsClassNames() {
var ret = new ArrayList();
INITIALIZERS.stream().map(p -> p.clazz()).forEachOrdered(ret::add);
AUTH_MECHANISMS.stream().map(p -> p.clazz()).forEachOrdered(ret::add);
AUTHORIZERS.stream().map(p -> p.clazz()).forEachOrdered(ret::add);
TOKEN_MANAGERS.stream().map(p -> p.clazz()).forEachOrdered(ret::add);
AUTHENTICATORS.stream().map(p -> p.clazz()).forEachOrdered(ret::add);
INTERCEPTORS.stream().map(p -> p.clazz()).forEachOrdered(ret::add);
SERVICES.stream().map(p -> p.clazz()).forEachOrdered(ret::add);
PROVIDERS.stream().map(p -> p.clazz()).forEachOrdered(ret::add);
return ret;
}
static final List providers() {
return PROVIDERS;
}
static final List initializers() {
return INITIALIZERS;
}
static final List authMechanisms() {
return AUTH_MECHANISMS;
}
static final List authorizers() {
return AUTHORIZERS;
}
static final List tokenManagers() {
return TOKEN_MANAGERS;
}
static final List authenticators() {
return AUTHENTICATORS;
}
static final List interceptors() {
return INTERCEPTORS;
}
static final List services() {
return SERVICES;
}
/**
* @param type the class of the plugin , e.g. Initializer.class
*/
private static List collectPlugins(ScanResult scanResult, String className) {
var ret = new ArrayList();
var registeredPlugins = scanResult.getClassesWithAnnotation(REGISTER_PLUGIN_CLASS_NAME);
if (registeredPlugins == null || registeredPlugins.isEmpty()) {
return ret;
}
ClassInfoList listOfType;
if (className.equals(AUTHENTICATOR_CLASS_NAME)) {
var tms = scanResult.getClassesImplementing(TOKEN_MANAGER_CLASS_NAME);
listOfType = scanResult.getClassesImplementing(className).exclude(tms);
} else {
listOfType = scanResult.getClassesImplementing(className);
}
var plugins = registeredPlugins.intersect(listOfType);
return plugins.stream().map(c -> descriptor(c)).collect(Collectors.toList());
}
/**
*
*/
private static List collectProviders(ScanResult scanResult) {
var ret = new ArrayList();
var providers = scanResult.getClassesImplementing(PROVIDER_CLASS_NAME);
if (providers == null || providers.isEmpty()) {
return ret;
}
return providers.stream().map(c -> descriptor(c)).collect(Collectors.toList());
}
private static PluginDescriptor descriptor(ClassInfo pluginClassInfo) {
var clazz = pluginClassInfo.getName();
var name = pluginClassInfo.getAnnotationInfo(REGISTER_PLUGIN_CLASS_NAME).getParameterValues().stream()
.filter(p -> "name".equals(p.getName())).map(p -> p.getValue()).findAny().get().toString();
return new PluginDescriptor(name, clazz, isEnabled(name, pluginClassInfo), collectInjections(pluginClassInfo));
}
private static ArrayList collectInjections(ClassInfo pluginClassInfo) {
var ret = new ArrayList();
ret.addAll(collectFieldInjections(pluginClassInfo, Inject.class));
ret.addAll(collectMethodInjections(pluginClassInfo, OnInit.class));
return ret;
}
/**
* NOTE:returns true at build time, to force native compilation of
* all plugins
*
* @param name
* @param pluginClassInfo
* @return true if the plugin is enabled, taking into account enabledByDefault and its configuration
*/
private static boolean isEnabled(String name, ClassInfo pluginClassInfo) {
if (ImageInfo.inImageBuildtimeCode()) {
return true;
} else {
var isEnabledByDefault = (boolean) pluginClassInfo.getAnnotationInfo(REGISTER_PLUGIN_CLASS_NAME).getParameterValues().stream()
.filter(p -> "enabledByDefault".equals(p.getName())).map(p -> p.getValue()).findAny().get();
Map confArgs = Bootstrapper.getConfiguration().getOrDefault(name, null);
return PluginRecord.isEnabled(isEnabledByDefault, confArgs);
}
}
private static ArrayList collectMethodInjections(ClassInfo pluginClassInfo, Class> clazz) {
var ret = new ArrayList();
var mil = pluginClassInfo.getDeclaredMethodInfo();
for (var mi : mil) {
if (mi.hasAnnotation(clazz.getName())) {
ArrayList> annotationParams = new ArrayList<>();
for (var p : mi.getAnnotationInfo(clazz.getName()).getParameterValues()) {
var value = p.getValue();
if (value instanceof AnnotationEnumValue annotationEnumValue) {
removeRefToScanResult(annotationEnumValue);
}
annotationParams.add(new AbstractMap.SimpleEntry<>(p.getName(), value));
}
var methodParams = new ArrayList();
Arrays.stream(mi.getParameterInfo()).forEachOrdered(pi -> methodParams.add(pi.getTypeDescriptor().toString()));
ret.add(new MethodInjectionDescriptor(mi.getName(), clazz, annotationParams, methodParams, mi.hashCode()));
}
}
return ret;
}
private static ArrayList collectFieldInjections(ClassInfo pluginClassInfo, Class> clazz) {
var ret = new ArrayList();
var fil = pluginClassInfo.getDeclaredFieldInfo();
for (var fi : fil) {
if (fi.hasAnnotation(clazz.getName())) {
var annotationParams = new ArrayList>();
for (var p : fi.getAnnotationInfo(clazz.getName()).getParameterValues()) {
var value = p.getValue();
if (value instanceof AnnotationEnumValue annotationEnumValue) {
removeRefToScanResult(annotationEnumValue);
}
annotationParams.add(new AbstractMap.SimpleEntry<>(p.getName(), value));
}
try {
var fieldClass = PluginsClassloader.getInstance().loadClass(fi.getTypeDescriptor().toString());
ret.add(new FieldInjectionDescriptor(fi.getName(), fieldClass, annotationParams, fi.hashCode()));
} catch(ClassNotFoundException cnfe) {
// should not happen
throw new RuntimeException(cnfe);
}
}
}
return ret;
}
/**
* this removes the reference to scanResult in the annotation info
* otherwise the huge object won't be garbage collected
*
* @param obj
*/
private static void removeRefToScanResult(AnnotationEnumValue obj) {
try {
var f = AnnotationEnumValue.class.getSuperclass().getDeclaredField("scanResult");
f.setAccessible(true);
f.set(obj, null);
} catch (IllegalAccessException | IllegalArgumentException | NoSuchFieldException | SecurityException ex) {
// nothing to do
}
}
static class RuntimeClassGraph {
private static final Logger LOGGER = LoggerFactory.getLogger(PluginsScanner.class);
private final ClassGraph classGraph;
URL[] jars = null;
public RuntimeClassGraph() {
var pdir = getPluginsDirectory();
this.jars = findPluginsJars(pdir);
if (!PluginsClassloader.isInitialized()) {
PluginsClassloader.init(jars);
}
this.classGraph = new ClassGraph().disableModuleScanning().disableDirScanning()
.disableNestedJarScanning().disableRuntimeInvisibleAnnotations()
.addClassLoader(PluginsClassloader.getInstance()).addClassLoader(ClassLoader.getSystemClassLoader())
.enableAnnotationInfo().enableMethodInfo().enableFieldInfo().ignoreFieldVisibility().initializeLoadedClasses();
}
private long starScanTime = 0;
private long endScanTime = 0;
public void logStartScan() {
LOGGER.info("Scanning jars for plugins started");
this.starScanTime = System.currentTimeMillis();
}
public void logEndScan() {
this.endScanTime = System.currentTimeMillis();
LOGGER.info("Scanning jars for plugins completed in {} msec", endScanTime-starScanTime);
}
public ClassGraph get() {
return this.classGraph;
}
private Path getPluginsDirectory() {
var pluginsDir = Bootstrapper.getConfiguration().coreModule().pluginsDirectory();
if (pluginsDir == null) {
return null;
}
if (pluginsDir.startsWith("/")) {
return Paths.get(pluginsDir);
} else {
// this is to allow specifying the plugins directory path
// relative to the jar (also working when running from classes)
var location = PluginsFactory.class.getProtectionDomain().getCodeSource().getLocation();
try {
var decodedLocation = URLDecoder.decode(location.getPath(), StandardCharsets.UTF_8.toString());
var locationFile = new File(decodedLocation);
pluginsDir = locationFile.getParent() + File.separator + pluginsDir;
return FileSystems.getDefault().getPath(pluginsDir);
} catch(UnsupportedEncodingException uee) {
Assert.assertShouldNeverHappen();
throw new RuntimeException(uee);
}
}
}
private URL[] findPluginsJars(Path pluginsDirectory) {
return _findPluginsJars(pluginsDirectory, 0);
}
private URL[] _findPluginsJars(Path dir, int depth) {
var pluginsPackages = Bootstrapper.getConfiguration().coreModule().pluginsPackages();
if (!pluginsPackages.isEmpty()) {
LOGGER.info("Limiting the scanning of plugins to packages {}", pluginsPackages);
}
if (dir == null) {
return new URL[0];
} else {
try {
checkPluginDirectory(dir);
} catch(IllegalStateException ise) {
return new URL[0];
}
}
var urls = new ArrayList();
try (var ds = Files.newDirectoryStream(dir, "*.jar")) {
for (Path path : ds) {
var jar = path.toUri().toURL();
if (!Files.isReadable(path)) {
LOGGER.error("Plugin jar {} is not readable", jar);
throw new IllegalStateException("Plugin jar " + jar + " is not readable");
}
urls.add(jar);
LOGGER.info("Found plugin jar {}", URLDecoder.decode(jar.getPath(), StandardCharsets.UTF_8.toString()));
}
} catch (IOException ex) {
LOGGER.error("Cannot read jars in plugins directory {}", Bootstrapper.getConfiguration().coreModule().pluginsDirectory(), ex.getMessage());
}
// Scans the plugins directory up to two levels deep
if (depth < 2) {
try (var ds = Files.newDirectoryStream(dir, (Filter) (Path entry) -> Files.isDirectory(entry))) {
for (Path subdir : ds) {
if (Files.isReadable(subdir)) {
var subjars = _findPluginsJars(subdir, depth + 1);
if (subjars != null && subjars.length > 0) {
Arrays.stream(subjars).forEach(jar -> urls.add(jar));
}
} else {
LOGGER.warn("Subdirectory {} of plugins directory {} is not readable", subdir, Bootstrapper.getConfiguration().coreModule().pluginsDirectory());
}
}
} catch (IOException ex) {
LOGGER.error("Cannot read jars in plugins subdirectory", ex.getMessage());
}
}
return urls.toArray(URL[]::new);
}
private void checkPluginDirectory(Path pluginsDirectory) {
if (!Files.exists(pluginsDirectory)) {
LOGGER.warn("Plugin directory {} does not exist", pluginsDirectory);
throw new IllegalStateException("Plugins directory " + pluginsDirectory + " does not exist");
}
if (!Files.isReadable(pluginsDirectory)) {
LOGGER.warn("Plugin directory {} is not readable", pluginsDirectory);
throw new IllegalStateException("Plugins directory " + pluginsDirectory + " is not readable");
}
}
}
}
record PluginDescriptor(String name, String clazz, boolean enabled, ArrayList injections) {}
interface InjectionDescriptor {}
record MethodInjectionDescriptor(String method, Class> clazz, ArrayList> annotationParams, ArrayList methodParams, int methodHash) implements InjectionDescriptor {}
record FieldInjectionDescriptor(String field, Class> clazz, ArrayList> annotationParams, int fieldHash) implements InjectionDescriptor {}