Package com.groupon.odo.proxylib

Source Code of com.groupon.odo.proxylib.PluginManager$ClassInformation

/*
Copyright 2014 Groupon, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package com.groupon.odo.proxylib;

import com.groupon.odo.plugin.PluginArguments;
import com.groupon.odo.plugin.ResponseOverride;
import com.groupon.odo.proxylib.models.Configuration;
import com.groupon.odo.proxylib.models.Plugin;

import javassist.ClassPool;
import javassist.NotFoundException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.DataInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;

public class PluginManager {
    private static final Logger logger = LoggerFactory
            .getLogger(PluginManager.class);

    private static PluginManager _instance = null;
    private EditService editService = EditService.getInstance();
    private String proxyLibPath = null;

    private ClassLoader classLoader = null;

    // list of loaded jars
    private ArrayList<String> jarInformation;

    // hashmap to hold method information for a class
    private HashMap<String, com.groupon.odo.proxylib.models.Method> methodInformation;

    // hashmap to hold class information for lazy loading
    private HashMap<String, ClassInformation> classInformation;

    public static void destroy() {
        _instance = null;
    }

    /**
     * Gets the current instance of plugin manager
     *
     * @return
     */
    public static PluginManager getInstance() {
        if (_instance == null) {
            _instance = new PluginManager();
            _instance.classInformation = new HashMap<String, ClassInformation>();
            _instance.methodInformation = new HashMap<String, com.groupon.odo.proxylib.models.Method>();
            _instance.jarInformation = new ArrayList<String>();

            if (_instance.proxyLibPath == null) {
                //Get the System Classloader
                ClassLoader sysClassLoader = Thread.currentThread().getContextClassLoader();

                //Get the URLs
                URL[] urls = ((URLClassLoader) sysClassLoader).getURLs();

                for (int i = 0; i < urls.length; i++) {
                    if (urls[i].getFile().contains("proxylib")) {
                        // store the path to the proxylib
                        _instance.proxyLibPath = urls[i].getFile();
                        break;
                    }
                }
            }
            _instance.initializePlugins();
        }
        return _instance;
    }

    public void initializePlugins() {
        Plugin[] plugins = this.getPlugins(true);

        for (Plugin plugin : plugins) {
            try {
                this.identifyClasses(plugin.getPath());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * This loads plugin file information into a hash for lazy loading later on
     *
     * @param pluginDirectory
     */
    public void identifyClasses(final String pluginDirectory) throws Exception {
        methodInformation.clear();
        jarInformation.clear();
        try {
            new FileTraversal() {
                public void onDirectory(final File d) {
                }

                public void onFile(final File f) {
                    try {
                        // loads class files
                        if (f.getName().endsWith(".class")) {
                            // get the class name for this path
                            String className = f.getAbsolutePath();
                            className = className.replace(pluginDirectory, "");
                            className = getClassNameFromPath(className);

                            logger.info("Storing plugin information: {}, {}", className,
                                    f.getName());

                            ClassInformation classInfo = new ClassInformation();
                            classInfo.pluginPath = pluginDirectory;
                            classInformation.put(className, classInfo);
                        } else if (f.getName().endsWith(".jar")) {
                            // loads JAR packages
                            // open up jar and discover files
                            // look for anything with /proxy/ in it
                            // this may discover things we don't need but that is OK
                            try {
                                jarInformation.add(f.getAbsolutePath());
                                JarFile jarFile = new JarFile(f);
                                Enumeration<?> enumer = jarFile.entries();

                                // Use the Plugin-Name manifest entry to match with the provided pluginName
                                String pluginPackageName = jarFile.getManifest().getMainAttributes().getValue("plugin-package");
                                if(pluginPackageName == null)
                                    return;

                                while (enumer.hasMoreElements()) {
                                    Object element = enumer.nextElement();
                                    String elementName = element.toString();

                                    if (!elementName.endsWith(".class"))
                                        continue;


                                    String className = getClassNameFromPath(elementName);
                                    if (className.contains(pluginPackageName)) {
                                        logger.info("Storing plugin information: {}, {}", className,
                                                f.getAbsolutePath());

                                        ClassInformation classInfo = new ClassInformation();
                                        classInfo.pluginPath = f.getAbsolutePath();
                                        classInformation.put(className, classInfo);
                                    }
                                }
                            } catch (Exception e) {

                            }
                        }
                    } catch (Exception e) {
                        logger.warn("Exception caught: {}, {}", e.getMessage(), e.getCause());
                    }
                }
            }.traverse(new File(pluginDirectory));
        } catch (IOException e) {
            throw new Exception("Could not identify all plugins: " + e.getMessage());
        }
    }

    /**
     * Create a classname from a given path
     *
     * @param path
     * @return
     */
    private String getClassNameFromPath(String path) {
        String className = path.replace(".class", "");

        // for *nix
        if (className.startsWith("/")) {
            className = className.substring(1, className.length());
        }
        className = className.replace("/", ".");

        // for windows
        if (className.startsWith("\\")) {
            className = className.substring(1, className.length());
        }
        className = className.replace("\\", ".");

        return className;
    }

    /**
     * Loads the specified class name and stores it in the hash
     *
     * @param className
     * @throws Exception
     */
    public void loadClass(String className) throws Exception {
        ClassInformation classInfo = classInformation.get(className);

        logger.info("Loading plugin.: {}, {}", className, classInfo.pluginPath);

        // get URL for proxylib
        // need to load this also otherwise the annotations cannot be found later on
        File libFile = new File(proxyLibPath);
        URL libUrl = libFile.toURI().toURL();

        // store the last modified time of the plugin
        File pluginDirectoryFile = new File(classInfo.pluginPath);
        classInfo.lastModified = pluginDirectoryFile.lastModified();

        // load the plugin directory
        URL classURL = new File(classInfo.pluginPath).toURI().toURL();

        URL[] urls = new URL[] {classURL};
        URLClassLoader child = new URLClassLoader (urls, this.getClass().getClassLoader());

        // load the class
        Class<?> cls = child.loadClass(className);

        // put loaded class into classInfo
        classInfo.loadedClass = cls;
        classInfo.loaded = true;

        classInformation.put(className, classInfo);

        logger.info("Loaded plugin: {}, {} method(s)", cls.toString(), cls.getDeclaredMethods().length);
    }

    /**
     * Calls the specified function with the specified arguments. This is used for v2 response overrides
     *
     * @param className
     * @param methodName
     * @param args
     * @return
     * @throws Exception
     */
    public void callFunction(String className, String methodName, PluginArguments pluginArgs, Object... args) throws Exception {
        Class<?> cls = getClass(className);

        ArrayList<Object> newArgs = new ArrayList<Object>();
        newArgs.add(pluginArgs);
        com.groupon.odo.proxylib.models.Method m = preparePluginMethod(newArgs, className, methodName, args);

        m.getMethod().invoke(cls, newArgs.toArray(new Object[0]));
    }

    /**
    * Calls the specified function with the specified arguments. This is used for v1 response overrides
    *
    * @param className
    * @param methodName
    * @param args
    * @return
    * @throws Exception
    */
    public Object callFunction(String className, String methodName, String responseContent, Object... args) throws Exception {
        Object retval;
        Class<?> cls = getClass(className);

        ArrayList<Object> newArgs = new ArrayList<Object>();
        newArgs.add(responseContent);
        com.groupon.odo.proxylib.models.Method m = preparePluginMethod(newArgs, className, methodName, args);

        retval = m.getMethod().invoke(cls, newArgs.toArray(new Object[0]));
        return retval;
    }

    private com.groupon.odo.proxylib.models.Method preparePluginMethod(List<Object> newArgs, String className,
                                                                       String methodName, Object... args) throws Exception {

        com.groupon.odo.proxylib.models.Method method = getMethod(className, methodName);

        // now convert the remaining args as necessary so the function is invoked with the correct types
        if (method.getMethodArguments().length > 0) {
            int x = 0;
            for (Object type : method.getMethodArguments()) {
                if (((String) type).endsWith("Integer")) {
                    newArgs.add(Integer.parseInt((String) args[x]));
                } else if (((String) type).endsWith("String")) {
                    newArgs.add(args[x]);
                } else if (((String) type).endsWith("Boolean")) {
                    newArgs.add(Boolean.valueOf((String) args[x]));
                }
                x++;
            }
        }

        return method;
    }

    /**
     * Get method object for a class/method name
     *
     * @param className
     * @param methodName
     * @return
     * @throws Exception
     */
    public com.groupon.odo.proxylib.models.Method getMethod(String className, String methodName) throws Exception {
        // TODO: fix this so it returns the right override ID
        com.groupon.odo.proxylib.models.Method m = null;

        // calls getClass first in case the loaded class needs to be invalidated
        Class<?> gottenClass = getClass(className);
        ClassInformation classInfo = classInformation.get(className);

        String fullName = className + "." + methodName;
        if (methodInformation.containsKey(fullName)) {
            m = methodInformation.get(fullName);
        } else {
            logger.info("Getting method info: {}", fullName);

            // Make a new classpool with the system classpath URLS
            // We create a new classpool each time since we want to reload plugin information in case it has changed.
            // Once a method is loaded this should not get called so the extra expense is not always taken as a hit
            ClassPool classPool = new ClassPool();
            ClassLoader sysClassLoader = Thread.currentThread().getContextClassLoader();

            //Get the URLs
            URL[] urls = ((URLClassLoader) sysClassLoader).getURLs();
            for (int i = 0; i < urls.length; i++) {
                try {
                    // insert all classpaths into the javassist classpool
                    classPool.insertClassPath(urls[i].getFile());
                } catch (NotFoundException e) {
                    e.printStackTrace();
                }
            }
            classPool.insertClassPath(classInfo.pluginPath);

            // load method information
            Method[] methods = gottenClass.getDeclaredMethods();
            for (Method method : methods) {
                if (method.getName().compareTo(methodName) != 0)
                    continue;

                try {
                    // get annotation information
                    Annotation[] annotations = method.getAnnotations();
                    for (Annotation annotation : annotations) {
                        com.groupon.odo.proxylib.models.Method newMethod = new com.groupon.odo.proxylib.models.Method();
                        newMethod.setClassName(className);
                        newMethod.setMethodName(methodName);
                        newMethod.setMethod(method);
                        newMethod.setMethodType(annotation.annotationType().toString());

                        String[] argNames = null;
                        String description = null;

                        // Convert to the right type and get annotation information
                        if (annotation.annotationType().toString().endsWith(Constants.PLUGIN_RESPONSE_OVERRIDE_CLASS)) {
                            ResponseOverride roAnnotation = (ResponseOverride)annotation;
                            newMethod.setHttpCode(roAnnotation.httpCode());
                            description = roAnnotation.description();
                            argNames = roAnnotation.parameters();
                            newMethod.setOverrideVersion(1);
                        }
                        else if(annotation.annotationType().toString().endsWith(Constants.PLUGIN_RESPONSE_OVERRIDE_V2_CLASS)) {
                            com.groupon.odo.plugin.v2.ResponseOverride roAnnotation = (com.groupon.odo.plugin.v2.ResponseOverride) annotation;
                            description = roAnnotation.description();
                            argNames = roAnnotation.parameters();
                            newMethod.setBlockRequest(roAnnotation.blockRequest());
                            newMethod.setOverrideVersion(2);
                        }
                        else
                            continue;

                        // identify arguments
                        // first arg is always a reserved that we skip
                        ArrayList<String> params = new ArrayList<String>();
                        if (method.getParameterTypes().length > 1) {
                            for (int x = 1; x < method.getParameterTypes().length; x++) {
                                params.add(method.getParameterTypes()[x].getName());
                            }
                        }
                        newMethod.setMethodArguments(params.toArray(new Object[0]));
                        newMethod.setMethodArgumentNames(argNames);
                        newMethod.setDescription(description);
                        newMethod.setIdString(className + "." + methodName);

                        methodInformation.put(fullName, newMethod);
                        m = newMethod;
                    }

                    break;

                } catch (Exception e) {
                    // in this case we just return null since the method would be unuseable
                    return null;
                }
            }
        }

        return m;
    }

    /**
     * Obtain the class of a given className
     *
     * @param className
     * @return
     * @throws Exception
     */
    private synchronized Class<?> getClass(String className) throws Exception {
        // see if we need to invalidate the class
        ClassInformation classInfo = classInformation.get(className);
        File classFile = new File(classInfo.pluginPath);
        if (classFile.lastModified() > classInfo.lastModified) {
            logger.info("Class {} has been modified, reloading", className);
            logger.info("Thread ID: {}", Thread.currentThread().getId());
            classInfo.loaded = false;
            classInformation.put(className, classInfo);

            // also cleanup anything in methodInformation with this className so it gets reloaded
            Iterator<Map.Entry<String, com.groupon.odo.proxylib.models.Method>> iter = methodInformation.entrySet().iterator();
            while (iter.hasNext()) {
                Map.Entry<String, com.groupon.odo.proxylib.models.Method> entry = iter.next();
                if (entry.getKey().startsWith(className)) {
                    iter.remove();
                }
            }
        }

        if (!classInfo.loaded) {
            loadClass(className);
        }

        return classInfo.loadedClass;
    }

    /**
     * Returns a string array of the available classes
     *
     * @return
     */
    public String[] getPluginClasses() {
        return classInformation.keySet().toArray(new String[0]);
    }

    /**
     * Returns a string array of the methods loaded for a class
     *
     * @param pluginClass
     * @return
     */
    public String[] getMethods(String pluginClass) throws Exception {
        ArrayList<String> methodNames = new ArrayList<String>();

        Method[] methods = getClass(pluginClass).getDeclaredMethods();
        for (Method method : methods) {
            logger.info("Checking {}", method.getName());

            com.groupon.odo.proxylib.models.Method methodInfo = this.getMethod(pluginClass, method.getName());
            if (methodInfo == null)
                continue;

            // check annotations
            Boolean matchesAnnotation = false;
            if (methodInfo.getMethodType().endsWith(Constants.PLUGIN_RESPONSE_OVERRIDE_CLASS) ||
                    methodInfo.getMethodType().endsWith(Constants.PLUGIN_RESPONSE_OVERRIDE_V2_CLASS)) {
                matchesAnnotation = true;
            }

            if (!methodNames.contains(method.getName()) && matchesAnnotation)
                methodNames.add(method.getName());
        }

        return methodNames.toArray(new String[0]);
    }

    /**
     * Class to handle some directory/file traversal
     */
    private class FileTraversal {
        public final void traverse(final File f) throws IOException {
            if (f.isDirectory()) {
                onDirectory(f);
                final File[] childs = f.listFiles();
                for (File child : childs) {
                    traverse(child);
                }
                return;
            }
            onFile(f);
        }

        public void onDirectory(final File d) {
        }

        public void onFile(final File f) {
        }
    }

    /**
     * This is used to pass all the methods into the model for editGroup
     * (mostly just for testing and seeing how things work for now)
     * gets all the methods so that i can pass them in as an attribute to our model
     *
     * @return
     * @throws Exception
     */
    public List<com.groupon.odo.proxylib.models.Method> getAllMethods() throws Exception {
        ArrayList<com.groupon.odo.proxylib.models.Method> methods = new ArrayList<com.groupon.odo.proxylib.models.Method>();
        String[] classes = getPluginClasses();
        for (int i = 0; i < classes.length; i++) {
            try {
                String[] methodNames = getMethods(classes[i]);
                for (int j = 0; j < methodNames.length; j++) {
                    com.groupon.odo.proxylib.models.Method method = getMethod(classes[i], methodNames[j]);
                    methods.add(method);
                }
            } catch (java.lang.NoClassDefFoundError e) {
                // this is ok.. might mean an old plugin
            } catch (java.lang.ClassNotFoundException e) {
                // this is also ok..
            }
        }
        return methods;
    }

    /**
     * returns all the methods not in the group, using the same ArrayList<HashMap>> format
     *
     * @param groupId
     * @return
     * @throws Exception
     */
    public List<com.groupon.odo.proxylib.models.Method> getMethodsNotInGroup(int groupId) throws Exception {
        List<com.groupon.odo.proxylib.models.Method> allMethods = getAllMethods();
        List<com.groupon.odo.proxylib.models.Method> methodsNotInGroup = new ArrayList<com.groupon.odo.proxylib.models.Method>();
        List<com.groupon.odo.proxylib.models.Method> methodsInGroup = editService.getMethodsFromGroupId(groupId, null);

        for (int i = 0; i < allMethods.size(); i++) {
            boolean add = true;
            String methodName = allMethods.get(i).getMethodName();
            String className = allMethods.get(i).getClassName();

            for (int j = 0; j < methodsInGroup.size(); j++) {
                if ((methodName.equals(methodsInGroup.get(j).getMethodName())) &&
                        (className.equals(methodsInGroup.get(j).getClassName())))
                    add = false;
            }
            if (add)
                methodsNotInGroup.add(allMethods.get(i));
        }
        return methodsNotInGroup;
    }

    /**
     * Returns the data about all of the plugins that are set
     *
     * @return
     */
    public Plugin[] getPlugins(Boolean onlyValid) {
        Configuration[] configurations = ConfigurationService.getInstance().getConfigurations(Constants.DB_TABLE_CONFIGURATION_PLUGIN_PATH);

        ArrayList<Plugin> plugins = new ArrayList<Plugin>();

        if (configurations == null)
            return new Plugin[0];

        for (Configuration config : configurations) {
            Plugin plugin = new Plugin();
            plugin.setId(config.getId());
            plugin.setPath(config.getValue());

            File path = new File(plugin.getPath());
            if (path.isDirectory()) {
                plugin.setStatus(Constants.PLUGIN_STATUS_VALID);
                plugin.setStatusMessage("Valid");
            } else {
                plugin.setStatus(Constants.PLUGIN_STATUS_NOT_DIRECTORY);
                plugin.setStatusMessage("Path is not a directory");
            }

            if (!onlyValid || plugin.getStatus() == Constants.PLUGIN_STATUS_VALID)
                plugins.add(plugin);
        }

        return plugins.toArray(new Plugin[0]);
    }

    public void addPluginPath(String path) throws Exception {
        ConfigurationService.getInstance().addValue(Constants.DB_TABLE_CONFIGURATION_PLUGIN_PATH, path);
        this.identifyClasses(path);
    }

    public void deletePluginPath(int id) throws Exception {
        ConfigurationService.getInstance().deleteValue(id);
        // TODO: clear these out of memory
    }

    /**
     * Gets a static resource from a plugin
     *
     * @param pluginName - Name of the plugin(defined in the plugin manifest)
     * @param fileName   - Filename to fetch
     * @return
     * @throws Exception
     */
    public byte[] getResource(String pluginName, String fileName) throws Exception {
        // TODO: This is going to be slow.. future improvement is to cache the data instead of searching all jars
        for (String jarFilename : jarInformation) {
            JarFile jarFile = new JarFile(new File(jarFilename));
            Enumeration<?> enumer = jarFile.entries();

            // Use the Plugin-Name manifest entry to match with the provided pluginName
            String jarPluginName = jarFile.getManifest().getMainAttributes().getValue("Plugin-Name");

            if (!jarPluginName.equals(pluginName))
                continue;

            while (enumer.hasMoreElements()) {
                Object element = enumer.nextElement();
                String elementName = element.toString();

                // Skip items in the jar that don't start with "resources/"
                if (!elementName.startsWith("resources/"))
                    continue;

                elementName = elementName.replace("resources/", "");
                if (elementName.equals(fileName)) {
                    // get the file from the jar
                    ZipEntry ze = jarFile.getEntry(element.toString());

                    InputStream fileStream = jarFile.getInputStream(ze);
                    byte[] data = new byte[(int) ze.getSize()];
                    DataInputStream dataIs = new DataInputStream(fileStream);
                    dataIs.readFully(data);
                    dataIs.close();
                    return data;
                }
            }
        }
        throw new FileNotFoundException("Could not find resource");
    }

    /**
     * Simple class to hold information about loaded/unloaded classes
     */
    private class ClassInformation {
        public boolean loaded = false;
        public String pluginPath = null;
        public long lastModified = 0;
        public Class<?> loadedClass = null;
    }
}
TOP

Related Classes of com.groupon.odo.proxylib.PluginManager$ClassInformation

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.