All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.restheart.plugins.PluginsScanner Maven / Gradle / Ivy

The newest version!
/*-
 * ========================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 {}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy