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                    Logging.trace("Plugin class not found in dep {0}: {1}", dep, e.getMessage());
069                    Logging.trace(e);
070                }
071            }
072            try {
073                // Will delegate to parent.loadClass(name, resolve) if needed
074                result = super.loadClass(name, resolve);
075            } catch (ClassNotFoundException e) {
076                Logging.trace("Plugin class not found in super {0}: {1}", this, e.getMessage());
077                Logging.trace(e);
078            }
079        }
080        // IcedTea-Web JNLPClassLoader overrides loadClass(String) but not loadClass(String, boolean)
081        if (result == null && getParent() != null) {
082            try {
083                result = getParent().loadClass(name);
084            } catch (ClassNotFoundException e) {
085                Logging.trace("Plugin class not found in parent {0}: {1}", getParent(), e.getMessage());
086                Logging.trace(e);
087            }
088        }
089        if (result != null) {
090            return result;
091        }
092        throw new ClassNotFoundException(name);
093    }
094
095    @Override
096    public URL findResource(String name) {
097        URL resource = super.findResource(name);
098        if (resource == null) {
099            for (PluginClassLoader dep : dependencies) {
100                resource = dep.findResource(name);
101                if (resource != null) {
102                    break;
103                }
104            }
105        }
106        return resource;
107    }
108
109    @Override
110    public String toString() {
111        return "PluginClassLoader [urls=" + Arrays.toString(getURLs()) +
112                (dependencies.isEmpty() ? "" : ", dependencies=" + dependencies) + ']';
113    }
114}