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

pluginloader.PluginLoader Maven / Gradle / Ivy

package pluginloader;

import com.github.javaparser.JavaParser;
import com.github.javaparser.ParseResult;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.Modifier;
import com.github.javaparser.ast.Node;
import com.github.javaparser.ast.PackageDeclaration;
import com.github.javaparser.ast.body.TypeDeclaration;
import pluginloader.load.PluginClassLoader;
import pluginloader.load.PluginCompiler;

import java.lang.reflect.InvocationTargetException;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * This class loads plugins and instantiates them
 */
public class PluginLoader {
    public static class LoadingException extends Exception{
        public LoadingException(String message) {
            super(message);
        }

        public LoadingException(Throwable cause) {
            super(cause);
        }
    }

    private static class ClassInfo{
        private final String className;
        private final String packageName;

        private ClassInfo(String className, String packageName) {
            this.className = className;
            this.packageName = packageName;
        }

        public String getFullQualifiedClassName(){
            String packagePath = "";
            if (packageName != null){
                packagePath = String.format("%s.", packageName);
            }
            return String.format("%s%s", packagePath, className);
        }
    }

    /**
     * Compiles a plugin given as a java class in source code form and loads it into the Java Runtime.
     * It only loads classes that extend the given java class.
     * After successfully loading it, an instance of each found class that extends the given class
     * will be returned to the caller.
     * If a class already exists, it will be updated if its code has changed. If the code has
     * not changed, the loading is skipped and a new instance of the class is returned.
     * @param plugin The string containing the source code of the plugin class
     * @param pluginInterface The interface/class the plugin class has to implement/extend
     * @return OBJECT The new instance of the class
     * @throws LoadingException When something goes wrong while compiling or loading the class
     */
    public  OBJECT load(String plugin, Class pluginInterface) throws LoadingException {
        try {
            ClassInfo classInfo = parseClassInfo(plugin);
            byte[] compile = PluginCompiler.compile(classInfo.getFullQualifiedClassName(), plugin);
            PluginClassLoader pluginClassLoader = new PluginClassLoader();
            pluginClassLoader.putClassCode(classInfo.getFullQualifiedClassName(), compile);

            Class pluginClass = pluginClassLoader.findClass(classInfo.getFullQualifiedClassName());
            if (!pluginInterface.isAssignableFrom(pluginClass)){
                throw new LoadingException(String.format("The compiled class is no implementation or subclass of %s", pluginInterface.getCanonicalName()));
            }
            Object pluginInstance = pluginClass.getConstructor().newInstance();
            //noinspection unchecked
            return (OBJECT) pluginInstance;
        } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException
                | IllegalAccessException | InvocationTargetException | PluginCompiler.CompilationFailedException e) {
            throw new LoadingException(e);
        }
    }

    private ClassInfo parseClassInfo(String plugin) throws LoadingException {
        JavaParser javaParser = new JavaParser();
        ParseResult parseResult = javaParser.parse(plugin);
        if (!parseResult.isSuccessful()){
            StringBuilder message = new StringBuilder();
            message.append("Parsing source file failed:\n");
            parseResult.getProblems().forEach(problem -> message.append(String.format("%s\n", problem.getVerboseMessage())));
            throw new LoadingException(message.toString());
        }
        assert parseResult.getResult().isPresent();
        final Set> publicTopLevelClasses = parseResult.getResult().get().getTypes().stream()
                .filter(TypeDeclaration::isTopLevelType)
                .filter(typeDeclaration -> !typeDeclaration.getModifiers().contains(Modifier.privateModifier()))
                .filter(typeDeclaration -> !typeDeclaration.getModifiers().contains(Modifier.protectedModifier()))
                .collect(Collectors.toSet());
        if (publicTopLevelClasses.isEmpty()){
            throw new LoadingException("Did not found a public top level class in the source code");
        }
        if (publicTopLevelClasses.size() > 1){
            throw new LoadingException("Found multiple public top level classes in the source code");
        }
        String className = publicTopLevelClasses.iterator().next().getName().asString();

        String packageName = null;
        Optional parentNode = publicTopLevelClasses.iterator().next().getParentNode();
        if (parentNode.isPresent() || (parentNode.get() instanceof CompilationUnit)){
            CompilationUnit compilationUnit = (CompilationUnit) parentNode.get();
            Optional packageDeclaration = compilationUnit.getPackageDeclaration();
            if (packageDeclaration.isPresent()){
                packageName = packageDeclaration.get().getNameAsString();
            }
        }
        return new ClassInfo(className, packageName);
    }
}