View Javadoc
1   package com.atlassian.plugin.classloader;
2   
3   import com.atlassian.plugin.Plugin;
4   import com.atlassian.plugin.PluginAccessor;
5   import com.atlassian.plugin.event.PluginEventListener;
6   import com.atlassian.plugin.event.PluginEventManager;
7   import com.atlassian.plugin.event.events.PluginEnabledEvent;
8   import com.atlassian.plugin.event.events.PluginModuleEnabledEvent;
9   import com.atlassian.plugin.event.impl.DefaultPluginEventManager;
10  import org.slf4j.Logger;
11  import org.slf4j.LoggerFactory;
12  
13  import java.net.URL;
14  import java.util.Collection;
15  import java.util.HashMap;
16  import java.util.HashSet;
17  import java.util.Iterator;
18  import java.util.Map;
19  import java.util.Set;
20  
21  import static com.atlassian.plugin.util.Assertions.notNull;
22  
23  /**
24   * A ClassLoader that will loop over all enabled Plugins, attempting to load the given class (or other resource) from
25   * the ClassLoader of each plugin in turn.
26   *
27   * @see com.atlassian.plugin.classloader.PluginClassLoader
28   */
29  public class PluginsClassLoader extends AbstractClassLoader {
30      private static final Logger log = LoggerFactory.getLogger(PluginsClassLoader.class);
31  
32      private final PluginAccessor pluginAccessor;
33  
34      private final Map<String, Plugin> pluginResourceIndex = new HashMap<>();
35      private final Map<String, Plugin> pluginClassIndex = new HashMap<>();
36  
37      private final Set<String> missedPluginResource = new HashSet<>();
38      private final Set<String> missedPluginClass = new HashSet<>();
39      private ClassLoader parentClassLoader;
40  
41      public PluginsClassLoader(final PluginAccessor pluginAccessor) {
42          this(null, pluginAccessor, new DefaultPluginEventManager());
43      }
44  
45      /**
46       * @param parent             The parent classloader
47       * @param pluginAccessor     The plugin accessor
48       * @param pluginEventManager The plugin event manager
49       * @since 2.5.0
50       */
51      public PluginsClassLoader(final ClassLoader parent, final PluginAccessor pluginAccessor, PluginEventManager pluginEventManager) {
52          super(parent);
53          this.parentClassLoader = parent;
54          this.pluginAccessor = notNull("pluginAccessor", pluginAccessor);
55          pluginEventManager.register(this);
56      }
57  
58      @Override
59      protected URL findResource(final String name) {
60          final Plugin indexedPlugin;
61          synchronized (this) {
62              indexedPlugin = pluginResourceIndex.get(name);
63          }
64          final URL result;
65          if (isPluginEnabled(indexedPlugin)) {
66              result = indexedPlugin.getClassLoader().getResource(name);
67          } else {
68              result = getResourceFromPlugins(name);
69          }
70          if (log.isDebugEnabled()) {
71              log.debug("Find resource [ " + name + " ], found [ " + result + " ]");
72          }
73          return result;
74      }
75  
76      @Override
77      protected Class<?> findClass(final String className) throws ClassNotFoundException {
78          final Plugin indexedPlugin;
79          synchronized (this) {
80              indexedPlugin = pluginClassIndex.get(className);
81          }
82  
83          final Class<?> result;
84          if (isPluginEnabled(indexedPlugin)) {
85              result = indexedPlugin.getClassLoader().loadClass(className);
86          } else {
87              result = loadClassFromPlugins(className);
88          }
89          if (log.isDebugEnabled()) {
90              log.debug("Find class [ " + className + " ], found [ " + result + " ]");
91          }
92          if (result != null) {
93              return result;
94          } else {
95              throw new ClassNotFoundException(className);
96          }
97      }
98  
99      private Class<?> loadClassFromPlugins(final String className) {
100         final boolean isMissedClassName;
101         synchronized (this) {
102             isMissedClassName = missedPluginClass.contains(className);
103         }
104         if (isMissedClassName) {
105             return null;
106         }
107         final Collection<Plugin> plugins = pluginAccessor.getEnabledPlugins();
108         if (log.isDebugEnabled()) {
109             log.debug("loadClassFromPlugins (" + className + ") looping through plugins...");
110         }
111         for (final Plugin plugin : plugins) {
112             if (log.isDebugEnabled()) {
113                 log.debug("loadClassFromPlugins (" + className + ") looking in plugin '" + plugin.getKey() + "'.");
114             }
115             try {
116                 final Class<?> result = plugin.getClassLoader().loadClass(className);
117                 //loadClass should never return null
118                 synchronized (this) {
119                     pluginClassIndex.put(className, plugin);
120                 }
121                 if (log.isDebugEnabled()) {
122                     log.debug("loadClassFromPlugins (" + className + ") found in plugin '" + plugin.getKey() + "'.");
123                 }
124                 return result;
125             } catch (final ClassNotFoundException e) {
126                 // continue searching the other plugins
127             }
128         }
129         if (log.isDebugEnabled()) {
130             log.debug("loadClassFromPlugins (" + className + ") not found - caching the miss.");
131         }
132         synchronized (this) {
133             missedPluginClass.add(className);
134         }
135         return null;
136     }
137 
138     private URL getResourceFromPlugins(final String name) {
139         final boolean isMissedResource;
140         synchronized (this) {
141             isMissedResource = missedPluginResource.contains(name);
142         }
143         if (isMissedResource) {
144             return null;
145         }
146         final Collection<Plugin> plugins = pluginAccessor.getEnabledPlugins();
147         for (final Plugin plugin : plugins) {
148             final URL resource = plugin.getClassLoader().getResource(name);
149             if (resource != null) {
150                 synchronized (this) {
151                     pluginResourceIndex.put(name, plugin);
152                 }
153                 return resource;
154             }
155         }
156         synchronized (this) {
157             missedPluginResource.add(name);
158         }
159         return null;
160     }
161 
162     private boolean isPluginEnabled(final Plugin plugin) {
163         return (plugin != null) && pluginAccessor.isPluginEnabled(plugin.getKey());
164     }
165 
166     public synchronized void notifyUninstallPlugin(final Plugin plugin) {
167         flushMissesCaches();
168         for (final Iterator<Map.Entry<String, Plugin>> it = pluginResourceIndex.entrySet().iterator(); it.hasNext(); ) {
169             final Map.Entry<String, Plugin> resourceEntry = it.next();
170             final Plugin pluginForResource = resourceEntry.getValue();
171             if (plugin.getKey().equals(pluginForResource.getKey())) {
172                 it.remove();
173             }
174         }
175         for (final Iterator<Map.Entry<String, Plugin>> it = pluginClassIndex.entrySet().iterator(); it.hasNext(); ) {
176             final Map.Entry<String, Plugin> pluginClassEntry = it.next();
177             final Plugin pluginForClass = pluginClassEntry.getValue();
178             if (plugin.getKey().equals(pluginForClass.getKey())) {
179                 it.remove();
180             }
181         }
182     }
183 
184     /**
185      * Returns the Plugin that will be used to load the given class name.
186      *
187      * If no enabled plugin can load the given class, then null is returned.
188      *
189      * @param className the Class name
190      * @return the Plugin that will be used to load the given class name.
191      * @since 2.3
192      */
193     public Plugin getPluginForClass(String className) {
194         Plugin indexedPlugin;
195         synchronized (this) {
196             indexedPlugin = pluginClassIndex.get(className);
197         }
198 
199         if (isPluginEnabled(indexedPlugin)) {
200             return indexedPlugin;
201         }
202         // Don't let a plugin claim a class that is a system class.
203         if (isSystemClass(className)) {
204             return null;
205         }
206         // Plugin not indexed, or disabled
207         // Try to load the class - this will cache the plugin it came from.
208         Class clazz = loadClassFromPlugins(className);
209         if (clazz == null) {
210             // Class could not be loaded - so return null.
211             return null;
212         }
213         synchronized (this) {
214             // if we get here, then loadClassFromPlugins() has returned a non-null class, and the side effect is that
215             // the plugin for the class name is cached in pluginClassIndex.
216             indexedPlugin = pluginClassIndex.get(className);
217         }
218         return indexedPlugin;
219     }
220 
221     private boolean isSystemClass(final String className) {
222         try {
223             // Assume that we are loaded by the core classloader
224             getClass().getClassLoader().loadClass(className);
225             // Standard ClassLoader was able to load the class
226             return true;
227         } catch (ClassNotFoundException ex) {
228             // Look in the parentClassLoader (if any) - when is this actually used anyway? DefaultPluginManager sets it to null.
229             if (parentClassLoader != null) {
230                 try {
231                     parentClassLoader.loadClass(className);
232                     // parentClassLoader was able to load the class
233                     return true;
234                 } catch (ClassNotFoundException ex2) {
235                     return false;
236                 }
237             } else {
238                 // parentClassLoader == null. This is normal. 
239                 return false;
240             }
241         }
242     }
243 
244     @PluginEventListener
245     public void onPluginEnabled(PluginEnabledEvent event) {
246         notifyPluginOrModuleEnabled();
247     }
248 
249     @PluginEventListener
250     public void onPluginModuleEnabled(PluginModuleEnabledEvent event) {
251         notifyPluginOrModuleEnabled();
252     }
253 
254     public synchronized void notifyPluginOrModuleEnabled() {
255         flushMissesCaches();
256     }
257 
258     private void flushMissesCaches() {
259         missedPluginClass.clear();
260         missedPluginResource.clear();
261     }
262 }