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

rvinh.java-plugins.1.2.source-code.PluginLoader Maven / Gradle / Ivy

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.body.TypeDeclaration;
import org.apache.commons.io.FileUtils;

import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.util.Arrays;
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);
        }
    }

    /**
     * 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 {
        File tempDir = null;
        File sourceFile = null;

        try {
            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");
            }

            tempDir = new File(Long.toString(System.nanoTime())).getAbsoluteFile();
            Files.createDirectory(tempDir.toPath());
            if(!tempDir.exists())
            {
                throw new LoadingException("Could not create temp directory: " + tempDir.getAbsolutePath());
            }

            String className = publicTopLevelClasses.iterator().next().getName().asString();
            sourceFile = new File(tempDir, String.format("%s.java", className)).getAbsoluteFile();
            Files.createFile(sourceFile.toPath());
            if (!sourceFile.exists()){
                throw new LoadingException(String.format("Could not create temp file: %s", sourceFile.getAbsolutePath()));
            }
            try(final DataOutputStream outputStream = new DataOutputStream(new FileOutputStream(sourceFile))){
                outputStream.writeBytes(plugin);
            }

            JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
            compiler.run(null, null, null, sourceFile.getAbsolutePath());
            File classFile = sourceFile.toPath().getParent().resolve(String.format("%s.class", className)).toFile();

            final OBJECT classObject = load(pluginInterface, classFile);
            FileUtils.deleteDirectory(tempDir);
            if (sourceFile.exists()){
                throw new LoadingException(String.format("Could not delete temp file: %s", sourceFile.getAbsolutePath()));
            }
            if (tempDir.exists()){
                throw new LoadingException(String.format("Could not delete temp directory: %s", tempDir.getAbsolutePath()));
            }
            return classObject;
        } catch (LoadingException e){
            try{
                if (tempDir != null){
                    FileUtils.deleteDirectory(tempDir);
                }
                if (sourceFile != null && sourceFile.exists()){
                    throw new LoadingException(String.format("Could not delete temp file: %s", sourceFile.getAbsolutePath()));
                }
                if (tempDir != null && tempDir.exists()){
                    throw new LoadingException(String.format("Could not delete temp directory: %s", tempDir.getAbsolutePath()));
                }
                throw new LoadingException(e);
            } catch (IOException a) {
                throw new LoadingException(a);
            }
        } catch (IOException e) {
            throw new LoadingException(e);
        }
    }

    /**
     * Loads the class from its class file and returns an instance of it
     * @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 loading the class
     */
    public  OBJECT load(Class pluginInterface, File plugin) throws LoadingException {
        try{
            String className = getClassName(plugin);
            URL classUrl = plugin.getParentFile().toURI().toURL();
            URLClassLoader classLoader = URLClassLoader.newInstance(new URL[]{classUrl});
            classLoader.loadClass(className);
            Class compiledClass = Class.forName(className, false, classLoader);

            if (!pluginInterface.isAssignableFrom(compiledClass)){
                throw new LoadingException(String.format("Public class %s in the source code can not be assigned to %s", className, pluginInterface.getCanonicalName()));
            }
            if (Arrays.asList(compiledClass.getConstructors()).parallelStream().noneMatch(constructor -> constructor.getParameterCount() == 0)){
                throw new LoadingException(String.format("Public class %s in the source code has no constructor without arguments", className));
            }
            final Object result = compiledClass.getConstructor().newInstance();

            return (OBJECT) result;
        } catch (MalformedURLException | ReflectiveOperationException e){
            throw new LoadingException(e);
        }
    }

    private String getClassName(File plugin) {
        //first part of the file name is the name of the class
        return plugin.getName().substring(0, plugin.getName().indexOf("."));
    }
}