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

pluginloader.PluginLoader Maven / Gradle / Ivy

The newest version!
package pluginloader;

import pluginloader.load.PluginClassLoader;
import pluginloader.load.PluginCompiler;
import pluginloader.load.SourceCodeParser;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
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);
        }
    }

    public static class WrongFileFormatException extends LoadingException{
        public WrongFileFormatException(String message) {
            super(message);
        }

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

    public  OBJECT load(JarInputStream jarInputStream, Class pluginInterface) throws LoadingException{
        Map entries = new HashMap<>();

        try {
            JarEntry jarEntry = jarInputStream.getNextJarEntry();
            while (jarEntry != null){
                if (!jarEntry.isDirectory()){
                    entries.put(jarEntry.getRealName(), jarInputStream.readAllBytes());
                }
                jarEntry = jarInputStream.getNextJarEntry();
            }
        } catch (IOException exception) {
            throw new LoadingException(exception);
        }

        String pluginClassSpecificationFile = String.format("META-INF/services/%s", pluginInterface.getCanonicalName());
        if (!entries.containsKey(pluginClassSpecificationFile)){
            throw new WrongFileFormatException(String.format("Expected JAR to contain file '%s'", pluginClassSpecificationFile));
        }
        List pluginClassSpecificationFileLines = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(entries.get(pluginClassSpecificationFile)))).lines().collect(Collectors.toList());
        if (pluginClassSpecificationFileLines.isEmpty()){
            throw new WrongFileFormatException(String.format("File is empty: %s", pluginClassSpecificationFile));
        }
        if (pluginClassSpecificationFileLines.size() > 1){
            throw new WrongFileFormatException(String.format("File contains more than one lines: %s", pluginClassSpecificationFile));
        }
        String pluginClassName = pluginClassSpecificationFileLines.get(0);

        PluginClassLoader pluginClassLoader = new PluginClassLoader();
        entries.entrySet().stream()
                .filter(file -> file.getKey().endsWith(".class"))
                .forEach(file -> {
                    String className = file.getKey()
                            .replace(".class", "")
                            .replace("/", ".");
                    pluginClassLoader.putClassCode(className, file.getValue());
                });

        try {
            Class pluginClass = pluginClassLoader.findClass(pluginClassName);
            return createInstance(pluginInterface, pluginClass);
        } catch (ClassNotFoundException e) {
            throw new LoadingException(String.format("Did not find class to load: '%s'\nPlease check if it is contained in the provided JAR", pluginClassName));
        }
    }

    /**
     * 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
     * @param  The type of the object to be returned
     * @return 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 = new SourceCodeParser().parseClassInfo(plugin);
            byte[] compile = PluginCompiler.compile(classInfo.getFullQualifiedClassName(), plugin);
            PluginClassLoader pluginClassLoader = new PluginClassLoader();
            pluginClassLoader.putClassCode(classInfo.getFullQualifiedClassName(), compile);

            Class pluginClass = pluginClassLoader.findClass(classInfo.getFullQualifiedClassName());
            return createInstance(pluginInterface, pluginClass);
        } catch (ClassNotFoundException | PluginCompiler.CompilationFailedException e) {
            throw new LoadingException(e);
        }
    }

    private  OBJECT createInstance(Class pluginInterface, Class pluginClass) throws LoadingException{
        if (!pluginInterface.isAssignableFrom(pluginClass)){
            throw new LoadingException(String.format("The compiled class is no implementation or subclass of %s", pluginInterface.getCanonicalName()));
        }
        try{
            Object pluginInstance = pluginClass.getConstructor().newInstance();
            //noinspection unchecked
            return (OBJECT) pluginInstance;
        } catch (InstantiationException | InvocationTargetException | NoSuchMethodException | IllegalAccessException exception) {
            throw new LoadingException(exception);
        }
    }
}