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