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