001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.plugins;
003
004import java.net.URL;
005import java.net.URLClassLoader;
006import java.util.ArrayList;
007import java.util.Arrays;
008import java.util.Collection;
009import java.util.Objects;
010
011import org.openstreetmap.josm.tools.Logging;
012
013/**
014 * Class loader for JOSM plugins.
015 * <p>
016 * In addition to the classes in the plugin jar file, it loads classes of required
017 * plugins. The JOSM core classes should be provided by the parent class loader.
018 * @since 12322
019 */
020public class PluginClassLoader extends URLClassLoader {
021
022    private final Collection<PluginClassLoader> dependencies;
023
024    static {
025        ClassLoader.registerAsParallelCapable();
026    }
027
028    /**
029     * Create a new PluginClassLoader.
030     * @param urls URLs of the plugin jar file (and extra libraries)
031     * @param parent the parent class loader (for JOSM core classes)
032     * @param dependencies class loaders of required plugin; can be null
033     */
034    public PluginClassLoader(URL[] urls, ClassLoader parent, Collection<PluginClassLoader> dependencies) {
035        super(urls, parent);
036        this.dependencies = dependencies == null ? new ArrayList<>() : new ArrayList<>(dependencies);
037    }
038
039    /**
040     * Add class loader of a required plugin.
041     * This plugin will have access to the classes of the dependent plugin
042     * @param dependency the class loader of the required plugin
043     * @return {@code true} if the collection of dependencies changed as a result of the call
044     * @since 12867
045     */
046    public boolean addDependency(PluginClassLoader dependency) {
047        // Add dependency only if not already present (directly or transitively through another one)
048        boolean result = !dependencies.contains(Objects.requireNonNull(dependency, "dependency"))
049                && !dependencies.stream().anyMatch(pcl -> pcl.dependencies.contains(dependency))
050                && dependencies.add(dependency);
051        if (result) {
052            // Now, remove top-level single dependencies, which would be children of the added one
053            dependencies.removeIf(pcl -> pcl.dependencies.isEmpty() && dependency.dependencies.contains(pcl));
054        }
055        return result;
056    }
057
058    @Override
059    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
060        Class<?> result = findLoadedClass(name);
061        if (result == null) {
062            for (PluginClassLoader dep : dependencies) {
063                try {
064                    result = dep.loadClass(name, resolve);
065                    if (result != null) {
066                        return result;
067                    }
068                } catch (ClassNotFoundException e) {
069                    // do nothing
070                    Logging.trace("Plugin class not found in {0}: {1}", dep, e.getMessage());
071                    Logging.trace(e);
072                }
073            }
074            result = super.loadClass(name, resolve);
075        }
076        if (result != null) {
077            return result;
078        }
079        throw new ClassNotFoundException(name);
080    }
081
082    @Override
083    public String toString() {
084        return "PluginClassLoader [urls=" + Arrays.toString(getURLs()) +
085                (dependencies.isEmpty() ? "" : ", dependencies=" + dependencies) + ']';
086    }
087}