View Javadoc

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