Index: /trunk/src/org/openstreetmap/josm/io/CachedFile.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/io/CachedFile.java	(revision 15415)
+++ /trunk/src/org/openstreetmap/josm/io/CachedFile.java	(revision 15416)
@@ -24,4 +24,5 @@
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
@@ -35,4 +36,5 @@
 import org.openstreetmap.josm.tools.Pair;
 import org.openstreetmap.josm.tools.PlatformManager;
+import org.openstreetmap.josm.tools.ResourceProvider;
 import org.openstreetmap.josm.tools.Utils;
 
@@ -41,5 +43,6 @@
  *
  * Supports URLs, local files, and a custom scheme (<code>resource:</code>) to get
- * resources from the current *.jar file. (Local caching is only done for URLs.)
+ * resources from the current JOSM *.jar file as well as plugins *.jar files.
+ * (Local caching is only done for URLs.)
  * <p>
  * The mirrored file is only downloaded if it has been more than 7 days since
@@ -225,10 +228,6 @@
         if (file == null) {
             if (name != null && name.startsWith("resource://")) {
-                String resourceName = name.substring("resource:/".length());
-                InputStream is = Utils.getResourceAsStream(getClass(), resourceName);
-                if (is == null) {
-                    throw new IOException(tr("Failed to open input stream for resource ''{0}''", name));
-                }
-                return is;
+                return Optional.ofNullable(ResourceProvider.getResourceAsStream(name.substring("resource:/".length())))
+                        .orElseThrow(() -> new IOException(tr("Failed to open input stream for resource ''{0}''", name)));
             } else {
                 throw new IOException("No file found for: "+name);
Index: /trunk/src/org/openstreetmap/josm/plugins/PluginHandler.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/plugins/PluginHandler.java	(revision 15415)
+++ /trunk/src/org/openstreetmap/josm/plugins/PluginHandler.java	(revision 15416)
@@ -73,4 +73,5 @@
 import org.openstreetmap.josm.tools.ImageProvider;
 import org.openstreetmap.josm.tools.Logging;
+import org.openstreetmap.josm.tools.ResourceProvider;
 import org.openstreetmap.josm.tools.SubclassFilteredCollection;
 import org.openstreetmap.josm.tools.Utils;
@@ -886,5 +887,5 @@
 
             extendJoinedPluginResourceCL(toLoad);
-            ImageProvider.addAdditionalClassLoaders(getResourceClassLoaders());
+            ResourceProvider.addAdditionalClassLoaders(getResourceClassLoaders());
             monitor.setTicksCount(toLoad.size());
             for (PluginInformation info : toLoad) {
Index: /trunk/src/org/openstreetmap/josm/tools/ImageProvider.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/tools/ImageProvider.java	(revision 15415)
+++ /trunk/src/org/openstreetmap/josm/tools/ImageProvider.java	(revision 15416)
@@ -35,8 +35,6 @@
 import java.util.Base64;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.EnumMap;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Hashtable;
 import java.util.Iterator;
@@ -45,5 +43,4 @@
 import java.util.Map;
 import java.util.Objects;
-import java.util.Set;
 import java.util.TreeSet;
 import java.util.concurrent.CompletableFuture;
@@ -263,19 +260,4 @@
     public static final String PROP_TRANSPARENCY_COLOR = "josm.transparency.color";
 
-    /** set of class loaders to take images from */
-    private static final Set<ClassLoader> classLoaders = Collections.synchronizedSet(new HashSet<>());
-    static {
-        try {
-            classLoaders.add(ClassLoader.getSystemClassLoader());
-        } catch (SecurityException e) {
-            Logging.log(Logging.LEVEL_ERROR, "Unable to get system classloader", e);
-        }
-        try {
-            classLoaders.add(ImageProvider.class.getClassLoader());
-        } catch (SecurityException e) {
-            Logging.log(Logging.LEVEL_ERROR, "Unable to get application classloader", e);
-        }
-    }
-
     /** directories in which images are searched */
     protected Collection<String> dirs;
@@ -617,7 +599,9 @@
      * @return {@code true} if the set changed as a result of the call
      * @since 12870
-     */
+     * @deprecated Use ResourceProvider#addAdditionalClassLoader
+     */
+    @Deprecated
     public static boolean addAdditionalClassLoader(ClassLoader additionalClassLoader) {
-        return classLoaders.add(additionalClassLoader);
+        return ResourceProvider.addAdditionalClassLoader(additionalClassLoader);
     }
 
@@ -627,7 +611,9 @@
      * @return {@code true} if the set changed as a result of the call
      * @since 12870
-     */
+     * @deprecated Use ResourceProvider#addAdditionalClassLoaders
+     */
+    @Deprecated
     public static boolean addAdditionalClassLoaders(Collection<ClassLoader> additionalClassLoaders) {
-        return classLoaders.addAll(additionalClassLoaders);
+        return ResourceProvider.addAdditionalClassLoaders(additionalClassLoaders);
     }
 
@@ -1212,12 +1198,5 @@
     private static URL getImageUrl(String path, String name) {
         if (path != null && path.startsWith("resource://")) {
-            String p = path.substring("resource://".length());
-            synchronized (classLoaders) {
-                for (ClassLoader source : classLoaders) {
-                    URL res;
-                    if ((res = source.getResource(p + name)) != null)
-                        return res;
-                }
-            }
+            return ResourceProvider.getResource(path.substring("resource://".length()) + name);
         } else {
             File f = new File(path, name);
Index: /trunk/src/org/openstreetmap/josm/tools/ResourceProvider.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/tools/ResourceProvider.java	(revision 15416)
+++ /trunk/src/org/openstreetmap/josm/tools/ResourceProvider.java	(revision 15416)
@@ -0,0 +1,85 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.tools;
+
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Function;
+
+/**
+ * Unified provider that looks up for resource in various classloaders (josm, plugins, etc.).
+ * @since 15416
+ */
+public final class ResourceProvider {
+
+    /** set of class loaders to take resources from */
+    private static final Set<ClassLoader> classLoaders = Collections.synchronizedSet(new HashSet<>());
+    static {
+        try {
+            classLoaders.add(ClassLoader.getSystemClassLoader());
+        } catch (SecurityException e) {
+            Logging.log(Logging.LEVEL_ERROR, "Unable to get system classloader", e);
+        }
+        try {
+            classLoaders.add(ResourceProvider.class.getClassLoader());
+        } catch (SecurityException e) {
+            Logging.log(Logging.LEVEL_ERROR, "Unable to get application classloader", e);
+        }
+    }
+
+    private ResourceProvider() {
+        // Hide default constructor for utilities classes
+    }
+
+    /**
+     * Add an additional class loader to search image for.
+     * @param additionalClassLoader class loader to add to the internal set
+     * @return {@code true} if the set changed as a result of the call
+     */
+    public static boolean addAdditionalClassLoader(ClassLoader additionalClassLoader) {
+        return classLoaders.add(additionalClassLoader);
+    }
+
+    /**
+     * Add a collection of additional class loaders to search image for.
+     * @param additionalClassLoaders class loaders to add to the internal set
+     * @return {@code true} if the set changed as a result of the call
+     */
+    public static boolean addAdditionalClassLoaders(Collection<ClassLoader> additionalClassLoaders) {
+        return classLoaders.addAll(additionalClassLoaders);
+    }
+
+    private static <T> T getFirstNotNull(Function<ClassLoader, T> function) {
+        synchronized (classLoaders) {
+            for (ClassLoader source : classLoaders) {
+                T res = function.apply(source);
+                if (res != null)
+                    return res;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Finds the resource with the given name.
+     * @param name The resource name
+     * @return A {@code URL} object for reading the resource, or {@code null} if the resource could not be found
+     *         or the invoker doesn't have adequate  privileges to get the resource.
+     * @see ClassLoader#getResource
+     */
+    public static URL getResource(String name) {
+        return getFirstNotNull(x -> x.getResource(name));
+    }
+
+    /**
+     * Finds a resource with a given name, with robustness to known JDK bugs.
+     * @param name name of the desired resource
+     * @return  A {@link java.io.InputStream} object or {@code null} if no resource with this name is found
+     */
+    public static InputStream getResourceAsStream(String name) {
+        return getFirstNotNull(x -> Utils.getResourceAsStream(x, name));
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/tools/Utils.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/tools/Utils.java	(revision 15415)
+++ /trunk/src/org/openstreetmap/josm/tools/Utils.java	(revision 15416)
@@ -1883,5 +1883,5 @@
     /**
      * Finds a resource with a given name, with robustness to known JDK bugs.
-     * @param klass class on which {@link Class#getResourceAsStream} will be called
+     * @param klass class on which {@link ClassLoader#getResourceAsStream} will be called
      * @param path name of the desired resource
      * @return  A {@link java.io.InputStream} object or {@code null} if no resource with this name is found
@@ -1889,11 +1889,25 @@
      */
     public static InputStream getResourceAsStream(Class<?> klass, String path) {
+        return getResourceAsStream(klass.getClassLoader(), path);
+    }
+
+    /**
+     * Finds a resource with a given name, with robustness to known JDK bugs.
+     * @param cl classloader on which {@link ClassLoader#getResourceAsStream} will be called
+     * @param path name of the desired resource
+     * @return  A {@link java.io.InputStream} object or {@code null} if no resource with this name is found
+     * @since 15416
+     */
+    public static InputStream getResourceAsStream(ClassLoader cl, String path) {
         try {
-            return klass.getResourceAsStream(path);
+            if (path != null && path.startsWith("/")) {
+                path = path.substring(1); // See Class#resolveName
+            }
+            return cl.getResourceAsStream(path);
         } catch (InvalidPathException e) {
             Logging.error("Cannot open {0}: {1}", path, e.getMessage());
             Logging.trace(e);
             try {
-                URL betterUrl = betterJarUrl(klass.getResource(path));
+                URL betterUrl = betterJarUrl(cl.getResource(path));
                 if (betterUrl != null) {
                     return betterUrl.openStream();
