PluginManagerI.java

// Copyright (c) ZeroC, Inc.

package com.zeroc.Ice;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

final class PluginManagerI implements PluginManager {
    private static final String _kindOfObject = "plugin";

    @Override
    public synchronized void initializePlugins() {
        if (_initialized) {
            throw new InitializationException("plug-ins already initialized");
        }

        //
        // Invoke initialize() on the plug-ins, in the order they were loaded.
        //
        List<Plugin> initializedPlugins = new ArrayList<>();
        try {
            for (PluginInfo p : _plugins) {
                try {
                    p.plugin.initialize();
                } catch (PluginInitializationException ex) {
                    throw ex;
                } catch (RuntimeException ex) {
                    throw new PluginInitializationException(
                        "plugin '" + p.name + "' initialization failed", ex);
                }
                initializedPlugins.add(p.plugin);
            }
        } catch (RuntimeException ex) {
            //
            // Destroy the plug-ins that have been successfully initialized, in the
            // reverse order.
            //
            ListIterator<Plugin> i =
                initializedPlugins.listIterator(initializedPlugins.size());
            while (i.hasPrevious()) {
                Plugin p = i.previous();
                try {
                    p.destroy();
                } catch (RuntimeException e) {
                    // Ignore.
                }
            }
            throw ex;
        }

        _initialized = true;
    }

    @Override
    public synchronized String[] getPlugins() {
        ArrayList<String> names = new ArrayList<>();
        for (PluginInfo p : _plugins) {
            names.add(p.name);
        }
        return names.toArray(new String[0]);
    }

    @Override
    public synchronized Plugin getPlugin(String name) {
        if (_communicator == null) {
            throw new CommunicatorDestroyedException();
        }

        Plugin p = findPlugin(name);
        if (p != null) {
            return p;
        }

        throw new NotRegisteredException(_kindOfObject, name);
    }

    @Override
    public synchronized void addPlugin(String name, Plugin plugin) {
        if (_communicator == null) {
            throw new CommunicatorDestroyedException();
        }

        if (findPlugin(name) != null) {
            throw new AlreadyRegisteredException(_kindOfObject, name);
        }

        PluginInfo info = new PluginInfo();
        info.name = name;
        info.plugin = plugin;
        _plugins.add(info);
    }

    @Override
    public synchronized void destroy() {
        if (_communicator != null) {
            if (_initialized) {
                ListIterator<PluginInfo> i = _plugins.listIterator(_plugins.size());
                while (i.hasPrevious()) {
                    PluginInfo p = i.previous();
                    try {
                        p.plugin.destroy();
                    } catch (RuntimeException ex) {
                        Util.getProcessLogger()
                            .warning(
                                "unexpected exception raised by plug-in `"
                                    + p.name
                                    + "' destruction:\n"
                                    + ex.toString());
                    }
                }
            }

            _communicator = null;
        }

        _plugins.clear();

        if (_classLoaders != null) {
            _classLoaders.clear();
        }
    }

    public PluginManagerI(Communicator communicator, Instance instance) {
        _communicator = communicator;
        _instance = instance;
        _initialized = false;
    }

    void loadPlugins() {
        assert (_communicator != null);

        final String prefix = "Ice.Plugin.";
        Properties properties = _communicator.getProperties();
        Map<String, String> plugins = properties.getPropertiesForPrefix(prefix);

        // First, create plug-ins using the plug-in factories from initData, in order.
        for (PluginFactory pluginFactory : _communicator.getInstance().initializationData().pluginFactories) {
            String name = pluginFactory.getPluginName();
            String key = "Ice.Plugin." + name;
            if (plugins.containsKey(key)) {
                loadPlugin(pluginFactory, name, plugins.get(key));
                plugins.remove(key);
            } else {
                loadPlugin(pluginFactory, name, "");
            }
        }

        //
        // Load and initialize the plug-ins defined in the property set
        // with the prefix "Ice.Plugin.". These properties should
        // have the following format:
        //
        // Ice.Plugin.<name>=entry_point [args]
        //
        // If the Ice.PluginLoadOrder property is defined, load the
        // specified plug-ins in the specified order, then load any
        // remaining plug-ins.
        //

        final String[] loadOrder = properties.getIcePropertyAsList("Ice.PluginLoadOrder");
        for (String name : loadOrder) {
            if (findPlugin(name) != null) {
                throw new PluginInitializationException("plug-in '" + name + "' already loaded");
            }

            String key = "Ice.Plugin." + name;
            boolean hasKey = plugins.containsKey(key);
            if (hasKey) {
                final String value = plugins.get(key);
                loadPlugin(null, name, value);
                plugins.remove(key);
            } else {
                throw new PluginInitializationException("plug-in '" + name + "' not defined");
            }
        }

        //
        // Load any remaining plug-ins that weren't specified in PluginLoadOrder.
        //
        for (var entry : plugins.entrySet()) {
            loadPlugin(null, entry.getKey().substring(prefix.length()), entry.getValue());
        }
    }

    private void loadPlugin(PluginFactory pluginFactory, String name, String pluginSpec) {
        assert (_communicator != null);

        if (findPlugin(name) != null) {
            throw new AlreadyRegisteredException(_kindOfObject, name);
        }

        //
        // We support the following formats:
        //
        // <class-name> [args]
        // <jar-file>:<class-name> [args]
        // <class-dir>:<class-name> [args]
        // "<path with spaces>":<class-name> [args]
        // "<path with spaces>:<class-name>" [args]
        //

        String[] args = new String[0];
        String entryPoint = "";
        String classDir = null; // Path name of JAR file or subdirectory.
        String className = "InvalidClass";
        boolean absolutePath = false;

        if (!pluginSpec.isEmpty()) {
            try {
                args = Options.split(pluginSpec);
            } catch (ParseException ex) {
                throw new PluginInitializationException(
                    "invalid arguments for plug-in `" + name + "'", ex);
            }

            assert (args.length > 0);

            entryPoint = args[0];

            final boolean isWindows = System.getProperty("os.name").startsWith("Windows");

            //
            // Find first ':' that isn't part of the file path.
            //
            int pos = entryPoint.indexOf(':');
            if (isWindows) {
                final String driveLetters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
                if (pos == 1
                    && entryPoint.length() > 2
                    && driveLetters.indexOf(entryPoint.charAt(0)) != -1
                    && (entryPoint.charAt(2) == '\\' || entryPoint.charAt(2) == '/')) {
                    absolutePath = true;
                    pos = entryPoint.indexOf(':', pos + 1);
                }
                if (!absolutePath) {
                    absolutePath = entryPoint.startsWith("\\\\");
                }
            } else {
                absolutePath = entryPoint.startsWith("/");
            }

            if ((pos == -1 && absolutePath) || (pos != -1 && entryPoint.length() <= pos + 1)) {
                //
                // Class name is missing.
                //
                throw new PluginInitializationException(
                    "invalid entry point for plug-in `" + name + "':\n" + entryPoint);
            }

            //
            // Extract the JAR file or subdirectory, if any.
            //

            if (pos == -1) {
                className = entryPoint;
            } else {
                classDir = entryPoint.substring(0, pos).trim();
                className = entryPoint.substring(pos + 1).trim();
            }

            //
            // Shift the arguments.
            //
            String[] tmp = new String[args.length - 1];
            System.arraycopy(args, 1, tmp, 0, args.length - 1);
            args = tmp;

            // Convert command-line options into properties.
            Properties properties = _communicator.getProperties();
            args = properties.parseCommandLineOptions(name, args);
        }

        if (pluginFactory == null) {
            try {
                Class<?> c = null;

                //
                // Use a class loader if the user specified a JAR file or class directory.
                //
                if (classDir != null) {
                    try {
                        if (!absolutePath) {
                            classDir =
                                new File(
                                    System.getProperty("user.dir")
                                        + File.separator
                                        + classDir)
                                    .getCanonicalPath();
                        }

                        if (!classDir.endsWith(File.separator)
                            && !classDir.toLowerCase().endsWith(".jar")) {
                            classDir += File.separator;
                        }
                        classDir = URLEncoder.encode(classDir, "UTF-8");

                        //
                        // Reuse an existing class loader if we have already loaded a plug-in with
                        // the same value for classDir, otherwise create a new one.
                        //
                        ClassLoader cl = null;

                        if (_classLoaders == null) {
                            _classLoaders = new HashMap<>();
                        } else {
                            cl = _classLoaders.get(classDir);
                        }

                        if (cl == null) {
                            final URL[] url =
                                new URL[]{new URL("file", null, classDir)};

                            //
                            // Use the custom class loader (if any) as the parent.
                            //
                            if (_instance.initializationData().classLoader != null) {
                                cl =
                                    new URLClassLoader(
                                        url, _instance.initializationData().classLoader);
                            } else {
                                cl = new URLClassLoader(url);
                            }

                            _classLoaders.put(classDir, cl);
                        }

                        c = cl.loadClass(className);
                    } catch (MalformedURLException ex) {
                        throw new PluginInitializationException(
                            "invalid entry point format '" + pluginSpec + "'", ex);
                    } catch (IOException ex) {
                        throw new PluginInitializationException(
                            "invalid path in entry point '" + pluginSpec + "'", ex);
                    } catch (ClassNotFoundException ex) {
                        // Ignored
                    }
                } else {
                    c = _communicator.getInstance().findClass(className);
                }

                if (c == null) {
                    throw new PluginInitializationException("class " + className + " not found");
                }

                java.lang.Object obj = c.getDeclaredConstructor().newInstance();
                try {
                    pluginFactory = (PluginFactory) obj;
                } catch (ClassCastException ex) {
                    throw new PluginInitializationException(
                        "class " + className + " does not implement PluginFactory", ex);
                }
            } catch (NoSuchMethodException ex) {
                throw new PluginInitializationException("unable to instantiate class " + className, ex);
            } catch (java.lang.reflect.InvocationTargetException ex) {
                throw new PluginInitializationException("unable to instantiate class " + className, ex);
            } catch (IllegalAccessException ex) {
                throw new PluginInitializationException(
                    "unable to access default constructor in class " + className, ex);
            } catch (InstantiationException ex) {
                throw new PluginInitializationException("unable to instantiate class " + className, ex);
            }
        }

        //
        // Invoke the factory.
        //
        Plugin plugin = null;
        try {
            plugin = pluginFactory.create(_communicator, name, args);
        } catch (PluginInitializationException ex) {
            throw ex;
        } catch (Throwable ex) {
            throw new PluginInitializationException("exception in factory " + className, ex);
        }

        if (plugin == null) {
            throw new PluginInitializationException("failure in factory " + className);
        }

        PluginInfo info = new PluginInfo();
        info.name = name;
        info.plugin = plugin;
        _plugins.add(info);
    }

    private Plugin findPlugin(String name) {
        for (PluginInfo p : _plugins) {
            if (name.equals(p.name)) {
                return p.plugin;
            }
        }
        return null;
    }

    static class PluginInfo {
        String name;
        Plugin plugin;
    }

    private Communicator _communicator;
    private final Instance _instance;
    private final List<PluginInfo> _plugins = new ArrayList<>();
    private boolean _initialized;
    private Map<String, ClassLoader> _classLoaders;
}